1import React from 'react';
2import { useMobile } from '@/hooks/use-mobile';
3import { Sheet, SheetContent } from '@/components/ui/sheet';
4import { Button } from '@/components/ui/button';
5import { ChevronUp, Image as ImageIcon, Clipboard } from 'lucide-react';
6import { useMockupStore } from '@/contexts/MockupContext';
7import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
8import { toast } from 'sonner';
9import { DialogDescription } from '@radix-ui/react-dialog';
10
11interface CustomBackground {
12 name: string;
13 image: string; // Base64 string
14}
15
16export const Backgrounds: React.FC = () => {
17 const {
18 backgroundImage,
19 setBackgroundType,
20 setGradientColors,
21 setGradientDirection,
22 setBackgroundImage,
23 customBackgrounds,
24 addCustomBackground,
25 } = useMockupStore();
26
27 const isMobile = useMobile();
28 const [isOpen, setIsOpen] = React.useState(false);
29 const [isDialogOpen, setIsDialogOpen] = React.useState(false);
30 const [isDragOver, setIsDragOver] = React.useState(false);
31 const fileInputRef = React.useRef<HTMLInputElement>(null);
32
33 const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
34
35 const gradientPresets = [
36 {
37 name: 'Deep Horizon',
38 colors: ['#141e30', '#243b55'],
39 direction: 'to-tl',
40 image: '/assets/deep_horizon.webp'
41 },
42
43 {
44 name: 'Ocean Glow',
45 colors: ['#56ccf2', '#2f80ed'],
46 direction: 'to-r',
47 image: '/assets/ocean_glow.webp'
48 },
49 {
50 name: 'Ocean Breeze',
51 colors: ['#ff9a9e', '#fecfef'],
52 direction: 'to-r',
53 image: '/assets/ocean_breeze.png'
54 },
55 {
56 name: 'Purple Haze',
57 colors: ['#c471ed', '#f64f59'],
58 direction: 'to-r',
59 image: '/assets/purple_haze.png'
60 },
61 {
62 name: 'Summer Vibes',
63 colors: ['#56ab2f', '#a8e6cf'],
64 direction: 'to-tr',
65 image: '/assets/summer_vibes.png'
66 },
67 {
68 name: 'Rainbow Dreams',
69 colors: ['#ff6b6b', '#4ecdc4'],
70 direction: 'to-br',
71 image: '/assets/rainbow_dreams.png'
72 },
73 {
74 name: 'Neon Heat',
75 colors: ['#ff0844', '#ffb199'],
76 direction: 'to-br',
77 image: '/assets/neon_heat.webp'
78 },
79 {
80 name: 'Purple Magic',
81 colors: ['#667eea', '#764ba2'],
82 direction: 'to-br',
83 image: '/assets/purple_magic.png'
84 },
85 {
86 name: 'Sunset Glow',
87 colors: ['#ff9a56', '#ff6b9d'],
88 direction: 'to-r',
89 image: '/assets/sunset_glow.png'
90 },
91 {
92 name: 'Warm Embrace',
93 colors: ['#ff9472', '#f2d388'],
94 direction: 'to-tr',
95 image: '/assets/warm_embrace.png'
96 },
97 {
98 name: 'Cosmic Night',
99 colors: ['#667eea', '#764ba2'],
100 direction: 'to-br',
101 image: '/assets/cosmic_night.webp'
102 },
103 {
104 name: 'Mint Breeze',
105 colors: ['#a8edea', '#fed6e3'],
106 direction: 'to-tr',
107 image: '/assets/mint_breeze.webp'
108 },
109 {
110 name: 'Neon Midnight',
111 colors: ['#c471ed', '#f64f59'],
112 direction: 'to-r',
113 image: '/assets/neon_midnight.webp'
114 },
115 {
116 name: 'Monochrome',
117 colors: ['#2c3e50', '#34495e'],
118 direction: 'to-br',
119 image: '/assets/monochrome.png'
120 },
121 {
122 name: 'Arctic Pulse',
123 colors: ['#cce3df', '#3a6c7a', '#0e1a1f'],
124 direction: 'to-r',
125 image: '/assets/arctic_pulse.webp'
126 },
127 {
128 name: 'Molten Dusk',
129 colors: ['#f0e7da', '#f857a6', '#2c2c2c'],
130 direction: 'to-l',
131 image: '/assets/molten_dusk.webp'
132 },
133 {
134 name: 'Twilight Ember',
135 colors: ['#ffb88c', '#ea5753', '#111d2f'],
136 direction: 'to-br',
137 image: '/assets/twilight_ember.webp'
138 }
139 ];
140
141 const handleGradientSelect = (gradient: typeof gradientPresets[0] | CustomBackground) => {
142 setBackgroundType('pattern');
143 if ('colors' in gradient && 'direction' in gradient) {
144 setGradientColors(gradient.colors);
145 setGradientDirection(gradient.direction);
146 } else {
147 setGradientColors(['#ffffff', '#ffffff']);
148 setGradientDirection('to-r');
149 }
150 setBackgroundImage(gradient.image);
151 if (isMobile) setIsOpen(false);
152 };
153
154 const handleImageUpload = (file: File) => {
155 if (!['image/jpeg', 'image/png'].includes(file.type)) {
156 toast.error('Invalid file format! Please use JPG, PNG.');
157 setIsDragOver(false);
158 setIsDialogOpen(false);
159 return;
160 }
161
162 if (file.size > MAX_FILE_SIZE) {
163 toast.error('File too large! Maximum size is 10MB.');
164 setIsDragOver(false);
165 setIsDialogOpen(false);
166 return;
167 }
168
169 const reader = new FileReader();
170 reader.onload = (e) => {
171 const base64Image = e.target?.result as string;
172 if (base64Image) {
173 const newBackground: CustomBackground = {
174 name: `Custom ${customBackgrounds.length + 1}`,
175 image: base64Image
176 };
177 addCustomBackground(newBackground);
178 handleGradientSelect(newBackground);
179 toast('Background added successfully!');
180 setTimeout(() => setIsDialogOpen(false), 300); // Delay for toast visibility
181 setIsDragOver(false);
182 }
183 };
184 reader.onerror = () => {
185 toast.error('Failed to read the file. Please try again.');
186 setIsDragOver(false);
187 setIsDialogOpen(false);
188 };
189 reader.readAsDataURL(file);
190 };
191
192 const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
193 e.preventDefault();
194 setIsDragOver(false);
195 const file = e.dataTransfer.files[0];
196 if (file) {
197 handleImageUpload(file);
198 } else {
199 toast.error('No valid file dropped.');
200 setIsDialogOpen(false);
201 }
202 };
203
204 const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
205 const file = e.target.files?.[0];
206 if (file) {
207 handleImageUpload(file);
208 } else {
209 toast.error('No file selected.');
210 setIsDialogOpen(false);
211 }
212 };
213
214 const handlePaste = (e: React.ClipboardEvent) => {
215 const file = e.clipboardData.files[0];
216 if (file) {
217 handleImageUpload(file);
218 } else {
219 toast.error('No valid image pasted.');
220 setIsDialogOpen(false);
221 }
222 };
223
224 const getDropZoneClasses = () => {
225 const baseClasses = "relative transition-all duration-300 cursor-pointer border-2 border-dashed rounded-xl bg-primary/20";
226 const hoverClasses = "hover:bg-primary/10";
227 const dragOverClasses = isDragOver ? "scale-105 border-primary" : "border-gray-400";
228 return `${baseClasses} ${hoverClasses} ${dragOverClasses}`;
229 };
230
231 const GradientGrid = () => (
232 <div className="space-y-6">
233 <div className="bg-background/80">
234 <div className="pb-4">
235 <h3 className="text-sidebar-foreground text-md font-medium">Background Gradients</h3>
236 </div>
237 <div className="pt-0">
238 <div className={`grid ${isMobile ? 'grid-cols-2' : 'grid-cols-1'} gap-4`}>
239 <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
240 <DialogTrigger asChild>
241 <button
242 className="h-16 rounded transition-all duration-75 relative overflow-hidden cursor-pointer border-2 border-dashed border-gray-400 hover:bg-primary/10"
243 >
244 <span className="text-sm text-white font-medium drop-shadow-lg relative z-10 px-3 py-2 rounded">
245 Custom Background
246 </span>
247 </button>
248 </DialogTrigger>
249 <DialogContent className="sm:max-w-lg" onPaste={handlePaste}>
250 <DialogHeader>
251 <DialogTitle>Upload Custom Background</DialogTitle>
252 <DialogDescription>
253 custom background to personalise your creation.
254 </DialogDescription>
255 </DialogHeader>
256 <div
257 className={getDropZoneClasses()}
258 style={{
259 width: '100%',
260 height: '300px'
261 }}
262 onDrop={handleDrop}
263 onDragOver={(e) => {
264 e.preventDefault();
265 setIsDragOver(true);
266 }}
267 onDragLeave={() => setIsDragOver(false)}
268 onClick={() => fileInputRef.current?.click()}
269 >
270 <div className="w-full h-full flex flex-col items-center justify-center">
271 <ImageIcon
272 size={isMobile ? 40 : 48}
273 className="text-gray-100 mb-4"
274 />
275 <div className="text-center text-white px-4">
276 <p className={`font-semibold mb-1 ${isMobile ? 'text-base' : 'text-lg'}`}>
277 Drop image here or click to upload
278 </p>
279 <p className={`text-white mb-2 ${isMobile ? 'text-xs' : 'text-sm'}`}>
280 Supports JPG, PNG
281 </p>
282
283 </div>
284 </div>
285 {isDragOver && (
286 <div className="absolute inset-0 bg-primary/20 rounded-xl flex items-center justify-center">
287 <div className={`text-primary font-semibold bg-white/90 px-6 py-3 rounded-lg ${isMobile ? 'text-sm' : 'text-base'}`}>
288 Drop image here
289 </div>
290 </div>
291 )}
292 </div>
293 <input
294 type="file"
295 accept="image/jpeg,image/png"
296 onChange={handleFileInput}
297 className="hidden"
298 ref={fileInputRef}
299 />
300 </DialogContent>
301 </Dialog>
302 {[...customBackgrounds, ...gradientPresets].map((gradient, index) => (
303 <button
304 key={gradient.name + index}
305 onClick={() => handleGradientSelect(gradient)}
306 className={`h-16 rounded transition-all duration-200 relative overflow-hidden cursor-pointer
307 ${backgroundImage === gradient.image
308 ? 'ring-2 ring-primary scale-105'
309 : 'hover:scale-102'
310 }`}
311 style={{
312 backgroundImage: `url(${gradient.image})`,
313 backgroundSize: 'cover',
314 backgroundPosition: 'center'
315 }}
316 >
317 <span className="text-sm text-white font-medium drop-shadow-lg relative z-10 bg-black/50 px-3 py-2 rounded">
318 {gradient.name}
319 </span>
320 </button>
321 ))}
322 </div>
323 </div>
324 </div>
325 </div>
326 );
327
328 if (isMobile) {
329 return (
330 <>
331 <Button
332 variant="outline"
333 className="fixed bottom-24 right-4 z-50 rounded-full bg-sidebar border-primary"
334 onClick={() => setIsOpen(true)}
335 >
336 <ChevronUp className="w-6 h-6" />
337 Background Gradients
338 </Button>
339 <Sheet open={isOpen} onOpenChange={setIsOpen}>
340 <SheetContent side="bottom" className="h-[80vh] bg-sidebar border-t border-sidebar-border [&>button:first-of-type]:hidden">
341 <div className="flex items-center justify-center">
342 <div className="h-2 w-16 bg-gradient-to-br from-gray-100 to-gray-400 inset-shadow-lg rounded-full"></div>
343 </div>
344 <div className="p-6 overflow-y-auto h-full">
345 <GradientGrid />
346 </div>
347 </SheetContent>
348 </Sheet>
349 </>
350 );
351 }
352
353 return (
354 <div className="w-80 bg-background/80 p-6 overflow-y-auto">
355 <GradientGrid />
356 </div>
357 );
358};