DistractIQ / src /components /ScreenshotUploader.tsx
github-actions
Initial clean deploy
7ff860b
Raw
History Blame Contribute Delete
4.16 kB
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>
);
}