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};