https://altly.madebydanny.uk
1import { useState, useCallback, useEffect } from "react";
2import { Upload, Image as ImageIcon, Loader2, Copy, Check } from "lucide-react";
3import { Button } from "@/components/ui/button";
4import { Card } from "@/components/ui/card";
5import { useToast } from "@/hooks/use-toast";
6import { supabase } from "@/integrations/supabase/client";
7import { User } from "@supabase/supabase-js";
8
9interface ImageUploaderProps {
10 user: User | null;
11}
12
13export const ImageUploader = ({ user }: ImageUploaderProps) => {
14 const [isDragging, setIsDragging] = useState(false);
15 const [imageFile, setImageFile] = useState<File | null>(null);
16 const [imagePreview, setImagePreview] = useState<string | null>(null);
17 const [isGenerating, setIsGenerating] = useState(false);
18 const [altText, setAltText] = useState<string | null>(null);
19 const [isCopied, setIsCopied] = useState(false);
20 const { toast } = useToast();
21
22 const handleDragOver = useCallback((e: React.DragEvent) => {
23 e.preventDefault();
24 setIsDragging(true);
25 }, []);
26
27 const handleDragLeave = useCallback((e: React.DragEvent) => {
28 e.preventDefault();
29 setIsDragging(false);
30 }, []);
31
32 const handleDrop = useCallback((e: React.DragEvent) => {
33 e.preventDefault();
34 setIsDragging(false);
35
36 const file = e.dataTransfer.files[0];
37 if (file && file.type.startsWith('image/')) {
38 handleImageFile(file);
39 } else {
40 toast({
41 title: "Invalid file type",
42 description: "Please upload an image file",
43 variant: "destructive",
44 });
45 }
46 }, [toast]);
47
48 const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
49 const file = e.target.files?.[0];
50 if (file) {
51 handleImageFile(file);
52 }
53 };
54
55 const handleImageFile = (file: File) => {
56 setImageFile(file);
57 setAltText(null);
58 setIsCopied(false);
59
60 const reader = new FileReader();
61 reader.onloadend = () => {
62 setImagePreview(reader.result as string);
63 };
64 reader.readAsDataURL(file);
65 };
66
67 const generateAltText = async () => {
68 if (!imageFile || !user) {
69 toast({
70 title: "Authentication required",
71 description: "Please sign in to generate alt text",
72 variant: "destructive",
73 });
74 return;
75 }
76
77 setIsGenerating(true);
78 setAltText(null);
79 const startTime = Date.now();
80
81 try {
82 // Upload image to custom CDN
83 const formData = new FormData();
84 formData.append('file', imageFile);
85
86 const uploadResponse = await fetch('https://cdn.madebydanny.uk/upload', {
87 method: 'POST',
88 body: formData,
89 });
90
91 if (!uploadResponse.ok) {
92 throw new Error('Failed to upload image to CDN');
93 }
94
95 const uploadData = await uploadResponse.json();
96 const imageUrl = uploadData.url;
97
98 // Get auth token for authenticated request
99 const { data: { session } } = await supabase.auth.getSession();
100 if (!session) {
101 throw new Error('Not authenticated');
102 }
103
104 // Call edge function to generate alt text (authenticated)
105 const { data, error } = await supabase.functions.invoke('generate-alt-text', {
106 body: {
107 imageUrl,
108 generationTimeStart: startTime
109 }
110 });
111
112 if (error) throw error;
113
114 setAltText(data.altText);
115 toast({
116 title: "Success!",
117 description: "Alt text generated successfully",
118 });
119
120 } catch (error: any) {
121 console.error('Error generating alt text:', error);
122
123 let errorMessage = "Failed to generate alt text";
124 if (error?.message?.includes('Rate limit')) {
125 errorMessage = "Daily limit reached (20 per day). Try again tomorrow.";
126 } else if (error?.message?.includes('Authentication')) {
127 errorMessage = "Please sign in to continue";
128 }
129
130 toast({
131 title: "Error",
132 description: errorMessage,
133 variant: "destructive",
134 });
135 } finally {
136 setIsGenerating(false);
137 }
138 };
139
140 const copyToClipboard = async () => {
141 if (!altText) return;
142
143 try {
144 await navigator.clipboard.writeText(altText);
145 setIsCopied(true);
146 toast({
147 title: "Copied!",
148 description: "Alt text copied to clipboard",
149 });
150 setTimeout(() => setIsCopied(false), 2000);
151 } catch (error) {
152 toast({
153 title: "Error",
154 description: "Failed to copy to clipboard",
155 variant: "destructive",
156 });
157 }
158 };
159
160 return (
161 <Card className="bg-card border-border/50 p-8">
162 <div className="flex items-center gap-3 mb-6">
163 <ImageIcon className="w-6 h-6 text-primary" />
164 <h2 className="text-xl font-semibold text-foreground">Upload Image</h2>
165 </div>
166
167 {!imagePreview ? (
168 <div
169 onDragOver={handleDragOver}
170 onDragLeave={handleDragLeave}
171 onDrop={handleDrop}
172 className={`
173 border-2 border-dashed rounded-lg p-12 text-center transition-all
174 ${isDragging
175 ? 'border-primary bg-primary/5'
176 : 'border-border/50 bg-upload-area hover:border-primary/50'
177 }
178 `}
179 >
180 <Upload className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
181 <p className="text-muted-foreground mb-4">
182 Drag & drop your image here, or click to select
183 </p>
184 <input
185 type="file"
186 accept="image/*"
187 onChange={handleFileInput}
188 className="hidden"
189 id="file-upload"
190 />
191 <label htmlFor="file-upload">
192 <Button variant="secondary" className="cursor-pointer" asChild>
193 <span>Choose Image</span>
194 </Button>
195 </label>
196 </div>
197 ) : (
198 <div className="space-y-6">
199 <div className="relative rounded-lg overflow-hidden bg-upload-area">
200 <img
201 src={imagePreview}
202 alt="Preview"
203 className="w-full h-auto max-h-96 object-contain"
204 />
205 </div>
206
207 <Button
208 onClick={generateAltText}
209 disabled={isGenerating}
210 className="w-full bg-primary hover:bg-primary/90 text-primary-foreground font-medium"
211 size="lg"
212 >
213 {isGenerating ? (
214 <>
215 <Loader2 className="w-5 h-5 mr-2 animate-spin" />
216 Generating...
217 </>
218 ) : (
219 <>
220 <ImageIcon className="w-5 h-5 mr-2" />
221 Generate Alt Text
222 </>
223 )}
224 </Button>
225
226 {altText && (
227 <Card className="bg-upload-area border-primary/20 p-6">
228 <div className="flex items-start justify-between gap-4 mb-3">
229 <h3 className="text-sm font-medium text-muted-foreground">Generated Alt Text</h3>
230 <Button
231 variant="ghost"
232 size="sm"
233 onClick={copyToClipboard}
234 className="h-8 px-3"
235 >
236 {isCopied ? (
237 <Check className="w-4 h-4 text-primary" />
238 ) : (
239 <Copy className="w-4 h-4" />
240 )}
241 </Button>
242 </div>
243 <p className="text-foreground leading-relaxed">{altText}</p>
244 </Card>
245 )}
246
247 <Button
248 variant="outline"
249 onClick={() => {
250 setImageFile(null);
251 setImagePreview(null);
252 setAltText(null);
253 setIsCopied(false);
254 }}
255 className="w-full"
256 >
257 Upload Another Image
258 </Button>
259 </div>
260 )}
261 </Card>
262 );
263};