feat/claude account oauth implementation on onboarding instead of manual token

This commit is contained in:
AndyMik90
2025-12-18 16:06:43 +01:00
parent e44202a066
commit 914a09d0da
2 changed files with 219 additions and 3 deletions
@@ -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.