stunning screenshots in seconds https://moocup.jaydip.me
1import React, { useState, useRef } from 'react'; 2import { Card, CardContent } from '@/components/ui/card'; 3import { Button } from '@/components/ui/button'; 4import { Slider } from '@/components/ui/slider'; 5import { Switch } from '@/components/ui/switch'; 6import { GripVertical, Undo, RectangleHorizontal, Palette, CornerUpLeft, Wand2 } from 'lucide-react'; 7import { useMockupStore } from '@/contexts/MockupContext'; 8import { extractDominantColor } from '@/utils/colorExtractor'; 9 10interface ImageBorderPanelProps { 11 onClose: () => void; 12} 13 14export const ImageBorderPanel: React.FC<ImageBorderPanelProps> = ({ onClose }) => { 15 const { 16 imageBorder, 17 setImageBorder, 18 uploadedImage, 19 } = useMockupStore(); 20 21 // Parse RGBA to initialize isTransparent and opacity 22 const parseRgba = (color: string): { r: number; g: number; b: number; a: number } => { 23 const matches = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/); 24 if (matches) { 25 return { 26 r: parseInt(matches[1]), 27 g: parseInt(matches[2]), 28 b: parseInt(matches[3]), 29 a: parseFloat(matches[4]), 30 }; 31 } 32 // Fallback to default color if parsing fails 33 return { r: 156, g: 163, b: 137, a: 1 }; // #9CA389 34 }; 35 36 const initialRgba = parseRgba(imageBorder.color); 37 const [isDragging, setIsDragging] = useState(false); 38 const [windowPosition, setWindowPosition] = useState({ x: 300, y: 100 }); 39 const windowRef = useRef<HTMLDivElement>(null); 40 const dragStartRef = useRef({ x: 0, y: 0 }); 41 const [isTransparent, setIsTransparent] = useState(initialRgba.a < 1); 42 const [isLoadingColor, setIsLoadingColor] = useState(false); 43 const [opacity, setOpacity] = useState(Math.max(0.3, initialRgba.a)); 44 45 const handleWindowDragStart = (e: React.MouseEvent) => { 46 e.preventDefault(); 47 setIsDragging(true); 48 dragStartRef.current = { 49 x: e.clientX - windowPosition.x, 50 y: e.clientY - windowPosition.y 51 }; 52 }; 53 54 const handleWindowDrag = (e: MouseEvent) => { 55 if (!isDragging) return; 56 setWindowPosition({ 57 x: e.clientX - dragStartRef.current.x, 58 y: e.clientY - dragStartRef.current.y 59 }); 60 }; 61 62 const handleWindowDragEnd = () => { 63 setIsDragging(false); 64 }; 65 66 React.useEffect(() => { 67 if (isDragging) { 68 window.addEventListener('mousemove', handleWindowDrag); 69 window.addEventListener('mouseup', handleWindowDragEnd); 70 return () => { 71 window.removeEventListener('mousemove', handleWindowDrag); 72 window.removeEventListener('mouseup', handleWindowDragEnd); 73 }; 74 } 75 }, [isDragging]); 76 77 const resetBorder = () => { 78 setImageBorder({ 79 width: 8, 80 color: 'rgba(156, 163, 137, 1)', // #9CA389 81 radius: 22, 82 enabled: false, 83 shadow: 'rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px', 84 }); 85 setIsTransparent(false); 86 setOpacity(1); 87 }; 88 89 const handleMagicWandClick = async () => { 90 if (!uploadedImage) return; 91 setIsLoadingColor(true); 92 try { 93 const dominantColor = await extractDominantColor(uploadedImage); 94 const matches = dominantColor.match(/^#[0-9A-Fa-f]{6}$/) ? dominantColor : '#9CA389'; 95 const { r, g, b } = hexToRgb(matches); 96 setImageBorder({ 97 color: `rgba(${r}, ${g}, ${b}, ${isTransparent ? opacity : 1})` 98 }); 99 } catch (error) { 100 console.error('Error extracting dominant color:', error); 101 setImageBorder({ 102 color: `rgba(156, 163, 137, ${isTransparent ? opacity : 1})` // #9CA389 103 }); 104 } finally { 105 setIsLoadingColor(false); 106 } 107 }; 108 109 const colorOptions = [ 110 'magic-wand', 111 'rgba(255, 255, 255, 1)', // #FFFFFF 112 'rgba(156, 163, 137, 1)', // #9CA389 113 'rgba(0, 0, 0, 1)', // #000000 114 'rgba(255, 107, 107, 1)', // #FF6B6B 115 'rgba(254, 202, 87, 1)', // #FECA57 116 'rgba(78, 205, 196, 1)', // #4ECDC4 117 'rgba(69, 183, 209, 1)', // #45B7D1 118 ]; 119 120 const hexToRgb = (hex: string): { r: number; g: number; b: number } => { 121 const r = parseInt(hex.slice(1, 3), 16); 122 const g = parseInt(hex.slice(3, 5), 16); 123 const b = parseInt(hex.slice(5, 7), 16); 124 return { r, g, b }; 125 }; 126 127 const convertToRgba = (r: number, g: number, b: number, opacity: number): string => { 128 return `rgba(${r}, ${g}, ${b}, ${opacity})`; 129 }; 130 131 const handleColorSelect = (color: string) => { 132 if (color === 'magic-wand') return; 133 const { r, g, b } = parseRgba(color); 134 setImageBorder({ 135 color: convertToRgba(r, g, b, isTransparent ? opacity : 1) 136 }); 137 }; 138 139 const handleTransparencyToggle = (checked: boolean) => { 140 setIsTransparent(checked); 141 const newOpacity = checked ? 0.5 : 1; 142 setOpacity(newOpacity); 143 const { r, g, b } = parseRgba(imageBorder.color); 144 setImageBorder({ 145 color: convertToRgba(r, g, b, newOpacity) 146 }); 147 }; 148 149 const handleOpacityChange = (value: number[]) => { 150 const newOpacity = Math.max(0.3, value[0]); 151 setOpacity(newOpacity); 152 if (isTransparent) { 153 const { r, g, b } = parseRgba(imageBorder.color); 154 setImageBorder({ 155 color: convertToRgba(r, g, b, newOpacity) 156 }); 157 } 158 }; 159 160 return ( 161 <div 162 ref={windowRef} 163 className="md:fixed z-40 select-none" 164 style={{ 165 left: `${windowPosition.x}px`, 166 top: `${windowPosition.y}px` 167 }} 168 > 169 <Card className="bg-sidebar max-md:border-none border-sidebar-border rounded-2xl overflow-hidden min-w-80"> 170 <div 171 className="flex items-center justify-between p-4 bg-sidebar md:cursor-move border-b border-sidebar-border" 172 onMouseDown={handleWindowDragStart} 173 > 174 <div className="flex items-center gap-3"> 175 <GripVertical className="w-4 h-4 text-primary/70" /> 176 <span className="text-white text-lg font-semibold">Image Border</span> 177 </div> 178 <Button onClick={resetBorder} variant="ghost" size="sm" className="text-white hover:text-primary hover:bg-primary/20 p-3 rounded-full"> 179 <Undo className="w-4 h-4" /> 180 </Button> 181 </div> 182 183 <CardContent className="p-6 bg-sidebar"> 184 <div className="space-y-6"> 185 {/* Enable Border Section */} 186 <div className="flex items-center justify-between"> 187 <span className="text-white text-sm">Enable Border</span> 188 <Switch 189 checked={imageBorder.enabled} 190 onCheckedChange={(checked) => setImageBorder({ enabled: checked })} 191 /> 192 </div> 193 194 {imageBorder.enabled && ( 195 <> 196 {/* Transparent Border Section */} 197 <div className="space-y-3"> 198 <div className="flex items-center justify-between"> 199 <span className="text-white text-sm">Transparent Border</span> 200 <Switch 201 checked={isTransparent} 202 onCheckedChange={handleTransparencyToggle} 203 /> 204 </div> 205 {isTransparent && ( 206 <div className="space-y-3"> 207 <div className="flex items-center justify-between"> 208 <span className="text-white text-sm">Opacity</span> 209 <span className="text-primary text-sm">{Math.round(opacity * 100)}%</span> 210 </div> 211 <Slider 212 value={[opacity]} 213 onValueChange={handleOpacityChange} 214 min={0.3} 215 max={1} 216 step={0.01} 217 /> 218 </div> 219 )} 220 </div> 221 222 {/* Color Section */} 223 <div className="space-y-3"> 224 <div className="flex items-center gap-2"> 225 <Palette className="w-4 h-4 text-primary" /> 226 <span className="text-white text-sm">Color</span> 227 </div> 228 <div className="grid grid-cols-4 gap-2"> 229 {colorOptions.map((color) => ( 230 color === 'magic-wand' ? ( 231 <button 232 key={color} 233 onClick={handleMagicWandClick} 234 disabled={isLoadingColor || !uploadedImage} 235 className={`w-full h-10 rounded-lg border-2 transition-all flex items-center justify-center ${ 236 isLoadingColor || !uploadedImage 237 ? 'opacity-50 cursor-not-allowed' 238 : 'border-primary/30 hover:border-primary/70' 239 }`} 240 > 241 <Wand2 className="w-5 h-5 text-primary" /> 242 </button> 243 ) : ( 244 <button 245 key={color} 246 onClick={() => handleColorSelect(color)} 247 className={`w-full h-10 rounded-lg border-2 transition-all ${ 248 imageBorder.color === color || imageBorder.color === convertToRgba(parseRgba(color).r, parseRgba(color).g, parseRgba(color).b, opacity) 249 ? 'border-primary scale-110' 250 : 'border-primary/30 hover:border-primary/70' 251 }`} 252 style={{ backgroundColor: isTransparent ? convertToRgba(parseRgba(color).r, parseRgba(color).g, parseRgba(color).b, opacity) : color }} 253 /> 254 ) 255 ))} 256 </div> 257 </div> 258 259 {/* Width Section */} 260 <div className="space-y-3"> 261 <div className="flex items-center justify-between"> 262 <div className="flex items-center gap-2"> 263 <RectangleHorizontal className="w-4 h-4 text-primary" /> 264 <span className="text-white text-sm">Width</span> 265 </div> 266 <span className="text-primary text-sm">{imageBorder.width}px</span> 267 </div> 268 <Slider 269 value={[imageBorder.width]} 270 onValueChange={(value) => setImageBorder({ width: value[0] })} 271 min={0} 272 max={20} 273 step={1} 274 /> 275 </div> 276 277 {/* Radius (Corners) Section */} 278 <div className="space-y-3"> 279 <div className="flex items-center justify-between"> 280 <div className="flex items-center gap-2"> 281 <CornerUpLeft className="w-4 h-4 text-primary" /> 282 <span className="text-white text-sm">Radius</span> 283 </div> 284 <span className="text-primary text-sm">{imageBorder.radius}px</span> 285 </div> 286 <Slider 287 value={[imageBorder.radius]} 288 onValueChange={(value) => setImageBorder({ radius: value[0] })} 289 min={0} 290 max={50} 291 step={1} 292 /> 293 </div> 294 </> 295 )} 296 </div> 297 </CardContent> 298 </Card> 299 </div> 300 ); 301};