stunning screenshots in seconds https://moocup.jaydip.me
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};