Spaces:
Sleeping
Sleeping
| import { useCallback, useRef, useState } from "react"; | |
| import { UploadCloud, Loader2, Check } from "lucide-react"; | |
| import { supabase } from "@/integrations/supabase/client"; | |
| import { toast } from "sonner"; | |
| export interface ExtractedData { | |
| total_phone_hours: number | null; | |
| notifications: number | null; | |
| social_hours: number | null; | |
| top_apps: { name: string; hours: number; category: string }[] | null; | |
| } | |
| interface Props { | |
| onExtracted: (data: ExtractedData) => void; | |
| uploadCount: number; | |
| } | |
| export default function ScreenshotUploader({ onExtracted, uploadCount }: Props) { | |
| const [busy, setBusy] = useState(false); | |
| const [dragOver, setDragOver] = useState(false); | |
| const [justAdded, setJustAdded] = useState(false); | |
| const inputRef = useRef<HTMLInputElement>(null); | |
| const handleFile = useCallback(async (file: File) => { | |
| if (!file.type.startsWith("image/")) { | |
| toast.error("Please upload an image (PNG/JPEG)."); | |
| return; | |
| } | |
| if (file.size > 10 * 1024 * 1024) { | |
| toast.error("Image too large. Max 10MB."); | |
| return; | |
| } | |
| const dataUrl = await new Promise<string>((resolve, reject) => { | |
| const r = new FileReader(); | |
| r.onload = () => resolve(r.result as string); | |
| r.onerror = reject; | |
| r.readAsDataURL(file); | |
| }); | |
| setBusy(true); | |
| try { | |
| const { data, error } = await supabase.functions.invoke("extract-screenshot", { | |
| body: { imageDataUrl: dataUrl }, | |
| }); | |
| if (error) throw error; | |
| if ((data as any)?.error) throw new Error((data as any).error); | |
| onExtracted(data as ExtractedData); | |
| setJustAdded(true); | |
| setTimeout(() => setJustAdded(false), 1800); | |
| toast.success("Screenshot decoded."); | |
| } catch (e: any) { | |
| console.error(e); | |
| toast.error(e?.message ?? "Could not extract data. Try a clearer screenshot."); | |
| } finally { | |
| setBusy(false); | |
| if (inputRef.current) inputRef.current.value = ""; | |
| } | |
| }, [onExtracted]); | |
| return ( | |
| <div className="space-y-2"> | |
| <label | |
| onDragOver={(e) => { e.preventDefault(); setDragOver(true); }} | |
| onDragLeave={() => setDragOver(false)} | |
| onDrop={(e) => { | |
| e.preventDefault(); | |
| setDragOver(false); | |
| const f = e.dataTransfer.files?.[0]; | |
| if (f) handleFile(f); | |
| }} | |
| className={`relative block cursor-pointer rounded-2xl border-2 border-dashed p-6 text-center transition-all ${ | |
| dragOver ? "border-primary bg-primary/10" : "border-border hover:border-primary/60 hover:bg-primary/5" | |
| } ${busy ? "pointer-events-none opacity-80" : ""}`} | |
| > | |
| <input | |
| ref={inputRef} | |
| type="file" | |
| accept="image/*" | |
| className="hidden" | |
| onChange={(e) => { | |
| const f = e.target.files?.[0]; | |
| if (f) handleFile(f); | |
| }} | |
| /> | |
| {busy ? ( | |
| <div className="flex flex-col items-center gap-2 py-2"> | |
| <Loader2 className="h-7 w-7 animate-spin text-primary" /> | |
| <span className="text-xs text-muted-foreground">Decoding with AI vision...</span> | |
| </div> | |
| ) : justAdded ? ( | |
| <div className="flex flex-col items-center gap-2 py-2 animate-fade-in"> | |
| <div className="h-9 w-9 rounded-full bg-success/20 flex items-center justify-center"> | |
| <Check className="h-5 w-5 text-success" /> | |
| </div> | |
| <span className="text-xs text-success font-medium">Data added</span> | |
| </div> | |
| ) : ( | |
| <> | |
| <UploadCloud className="h-8 w-8 mx-auto mb-2 text-primary" /> | |
| <div className="text-sm font-medium"> | |
| {uploadCount > 0 ? "Add another screenshot" : "Drop your Digital Wellbeing screenshot"} | |
| </div> | |
| <div className="text-xs text-muted-foreground mt-1">PNG or JPEG · auto-fills your stats</div> | |
| </> | |
| )} | |
| </label> | |
| {uploadCount > 0 && !busy && ( | |
| <p className="text-[11px] text-muted-foreground text-center"> | |
| {uploadCount} screenshot{uploadCount > 1 ? "s" : ""} processed | |
| </p> | |
| )} | |
| </div> | |
| ); | |
| } | |