feat/claude account oauth implementation on onboarding instead of manual token
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback, useRef, useMemo, type ClipboardEvent } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef, useMemo, type ClipboardEvent, type DragEvent } from 'react';
|
||||
import {
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
@@ -124,6 +124,9 @@ export function TaskCreationWizard({
|
||||
// Ref for the textarea to handle paste events
|
||||
const descriptionRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// Drag-and-drop state for images over textarea
|
||||
const [isDragOverTextarea, setIsDragOverTextarea] = useState(false);
|
||||
|
||||
// Setup drag sensors with distance constraint to prevent accidental drags
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
@@ -277,6 +280,105 @@ export function TaskCreationWizard({
|
||||
}
|
||||
}, [images]);
|
||||
|
||||
/**
|
||||
* Handle drag over textarea for image drops
|
||||
*/
|
||||
const handleTextareaDragOver = useCallback((e: DragEvent<HTMLTextAreaElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOverTextarea(true);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle drag leave from textarea
|
||||
*/
|
||||
const handleTextareaDragLeave = useCallback((e: DragEvent<HTMLTextAreaElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOverTextarea(false);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle drop on textarea for image files
|
||||
*/
|
||||
const handleTextareaDrop = useCallback(
|
||||
async (e: DragEvent<HTMLTextAreaElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOverTextarea(false);
|
||||
|
||||
if (isCreating) return;
|
||||
|
||||
const files = e.dataTransfer?.files;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
// Filter for image files
|
||||
const imageFiles: File[] = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
if (file.type.startsWith('image/')) {
|
||||
imageFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
if (imageFiles.length === 0) return;
|
||||
|
||||
// Check if we can add more images
|
||||
const remainingSlots = MAX_IMAGES_PER_TASK - images.length;
|
||||
if (remainingSlots <= 0) {
|
||||
setError(`Maximum of ${MAX_IMAGES_PER_TASK} images allowed`);
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
|
||||
// Process image files
|
||||
const newImages: ImageAttachment[] = [];
|
||||
const existingFilenames = images.map(img => img.filename);
|
||||
|
||||
for (const file of imageFiles.slice(0, remainingSlots)) {
|
||||
// Validate image type
|
||||
if (!isValidImageMimeType(file.type)) {
|
||||
setError(`Invalid image type. Allowed: ${ALLOWED_IMAGE_TYPES_DISPLAY}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const dataUrl = await blobToBase64(file);
|
||||
const thumbnail = await createThumbnail(dataUrl);
|
||||
|
||||
// Use original filename or generate one
|
||||
const baseFilename = file.name || `dropped-image-${Date.now()}.${file.type.split('/')[1] || 'png'}`;
|
||||
const resolvedFilename = resolveFilename(baseFilename, [
|
||||
...existingFilenames,
|
||||
...newImages.map(img => img.filename)
|
||||
]);
|
||||
|
||||
newImages.push({
|
||||
id: generateImageId(),
|
||||
filename: resolvedFilename,
|
||||
mimeType: file.type,
|
||||
size: file.size,
|
||||
data: dataUrl.split(',')[1], // Store base64 without data URL prefix
|
||||
thumbnail
|
||||
});
|
||||
} catch {
|
||||
setError('Failed to process dropped image');
|
||||
}
|
||||
}
|
||||
|
||||
if (newImages.length > 0) {
|
||||
setImages(prev => [...prev, ...newImages]);
|
||||
// Auto-expand images section
|
||||
setShowImages(true);
|
||||
// Show success feedback
|
||||
setPasteSuccess(true);
|
||||
setTimeout(() => setPasteSuccess(false), 2000);
|
||||
}
|
||||
},
|
||||
[images, isCreating]
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle drag start - capture file data for overlay
|
||||
*/
|
||||
@@ -499,9 +601,15 @@ export function TaskCreationWizard({
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
onPaste={handlePaste}
|
||||
onDragOver={handleTextareaDragOver}
|
||||
onDragLeave={handleTextareaDragLeave}
|
||||
onDrop={handleTextareaDrop}
|
||||
rows={5}
|
||||
disabled={isCreating}
|
||||
className="resize-y min-h-[120px] max-h-[400px]"
|
||||
className={cn(
|
||||
"resize-y min-h-[120px] max-h-[400px]",
|
||||
isDragOverTextarea && !isCreating && "border-primary bg-primary/5 ring-2 ring-primary/20"
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Tip: Paste screenshots directly with {navigator.platform.includes('Mac') ? '⌘V' : 'Ctrl+V'} to add reference images.
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
import { useState, useEffect, useCallback, useRef, type ClipboardEvent } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef, type ClipboardEvent, type DragEvent } from 'react';
|
||||
import { Loader2, Image as ImageIcon, ChevronDown, ChevronUp, X } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -106,6 +106,9 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi
|
||||
// Ref for the textarea to handle paste events
|
||||
const descriptionRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// Drag-and-drop state for images over textarea
|
||||
const [isDragOverTextarea, setIsDragOverTextarea] = useState(false);
|
||||
|
||||
// Reset form when task changes or dialog opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
@@ -209,6 +212,105 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi
|
||||
}
|
||||
}, [images]);
|
||||
|
||||
/**
|
||||
* Handle drag over textarea for image drops
|
||||
*/
|
||||
const handleTextareaDragOver = useCallback((e: DragEvent<HTMLTextAreaElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOverTextarea(true);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle drag leave from textarea
|
||||
*/
|
||||
const handleTextareaDragLeave = useCallback((e: DragEvent<HTMLTextAreaElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOverTextarea(false);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle drop on textarea for image files
|
||||
*/
|
||||
const handleTextareaDrop = useCallback(
|
||||
async (e: DragEvent<HTMLTextAreaElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOverTextarea(false);
|
||||
|
||||
if (isSaving) return;
|
||||
|
||||
const files = e.dataTransfer?.files;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
// Filter for image files
|
||||
const imageFiles: File[] = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
if (file.type.startsWith('image/')) {
|
||||
imageFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
if (imageFiles.length === 0) return;
|
||||
|
||||
// Check if we can add more images
|
||||
const remainingSlots = MAX_IMAGES_PER_TASK - images.length;
|
||||
if (remainingSlots <= 0) {
|
||||
setError(`Maximum of ${MAX_IMAGES_PER_TASK} images allowed`);
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
|
||||
// Process image files
|
||||
const newImages: ImageAttachment[] = [];
|
||||
const existingFilenames = images.map(img => img.filename);
|
||||
|
||||
for (const file of imageFiles.slice(0, remainingSlots)) {
|
||||
// Validate image type
|
||||
if (!isValidImageMimeType(file.type)) {
|
||||
setError(`Invalid image type. Allowed: ${ALLOWED_IMAGE_TYPES_DISPLAY}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const dataUrl = await blobToBase64(file);
|
||||
const thumbnail = await createThumbnail(dataUrl);
|
||||
|
||||
// Use original filename or generate one
|
||||
const baseFilename = file.name || `dropped-image-${Date.now()}.${file.type.split('/')[1] || 'png'}`;
|
||||
const resolvedFilename = resolveFilename(baseFilename, [
|
||||
...existingFilenames,
|
||||
...newImages.map(img => img.filename)
|
||||
]);
|
||||
|
||||
newImages.push({
|
||||
id: generateImageId(),
|
||||
filename: resolvedFilename,
|
||||
mimeType: file.type,
|
||||
size: file.size,
|
||||
data: dataUrl.split(',')[1], // Store base64 without data URL prefix
|
||||
thumbnail
|
||||
});
|
||||
} catch {
|
||||
setError('Failed to process dropped image');
|
||||
}
|
||||
}
|
||||
|
||||
if (newImages.length > 0) {
|
||||
setImages(prev => [...prev, ...newImages]);
|
||||
// Auto-expand images section
|
||||
setShowImages(true);
|
||||
// Show success feedback
|
||||
setPasteSuccess(true);
|
||||
setTimeout(() => setPasteSuccess(false), 2000);
|
||||
}
|
||||
},
|
||||
[images, isSaving]
|
||||
);
|
||||
|
||||
const handleSave = async () => {
|
||||
// Validate input - only description is required
|
||||
if (!description.trim()) {
|
||||
@@ -296,8 +398,14 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
onPaste={handlePaste}
|
||||
onDragOver={handleTextareaDragOver}
|
||||
onDragLeave={handleTextareaDragLeave}
|
||||
onDrop={handleTextareaDrop}
|
||||
rows={5}
|
||||
disabled={isSaving}
|
||||
className={cn(
|
||||
isDragOverTextarea && !isSaving && "border-primary bg-primary/5 ring-2 ring-primary/20"
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Tip: Paste screenshots directly with {navigator.platform.includes('Mac') ? '⌘V' : 'Ctrl+V'} to add reference images.
|
||||
|
||||
Reference in New Issue
Block a user