1import { useEffect, useRef, useState } 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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
7import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
8import { GripVertical, Undo, Move, Maximize2 } from 'lucide-react';
9import { useMockupStore } from '@/contexts/MockupContext';
10
11interface PositionScalePanelProps {
12 onClose: () => void;
13}
14
15export const PositionScalePanel: React.FC<PositionScalePanelProps> = ({ onClose }) => {
16 const {
17 devicePosition,
18 updateDevicePosition,
19 fixedMargin,
20 setFixedMargin,
21 margin,
22 setMargin,
23 } = useMockupStore();
24
25 const [isDragging, setIsDragging] = useState(false);
26 const [windowPosition, setWindowPosition] = useState({ x: 200, y: 100 });
27 const [scale, setScale] = useState(() => devicePosition.scale);
28 const [activeTab, setActiveTab] = useState('position');
29 const [marginPreset, setMarginPreset] = useState('medium');
30
31 const windowRef = useRef<HTMLDivElement>(null);
32 const dragStartRef = useRef({ x: 0, y: 0 });
33 const gridRef = useRef<HTMLDivElement>(null);
34
35 const handleWindowDragStart = (e: React.MouseEvent) => {
36 e.preventDefault();
37 setIsDragging(true);
38 dragStartRef.current = {
39 x: e.clientX - windowPosition.x,
40 y: e.clientY - windowPosition.y,
41 };
42 };
43
44 const handleWindowDrag = (e: MouseEvent) => {
45 if (!isDragging) return;
46 setWindowPosition({
47 x: e.clientX - dragStartRef.current.x,
48 y: e.clientY - dragStartRef.current.y,
49 });
50 };
51
52 const handleWindowDragEnd = () => {
53 setIsDragging(false);
54 };
55
56 useEffect(() => {
57 if (isDragging) {
58 window.addEventListener('mousemove', handleWindowDrag);
59 window.addEventListener('mouseup', handleWindowDragEnd);
60 return () => {
61 window.removeEventListener('mousemove', handleWindowDrag);
62 window.removeEventListener('mouseup', handleWindowDragEnd);
63 };
64 }
65 }, [isDragging]);
66
67 const handleFixedMarginToggle = (checked) => {
68 setFixedMargin(checked)
69 handleMarginPresetChange('medium')
70 }
71
72 const handleBallClick = (e: React.MouseEvent, ballIndex: number) => {
73 e.preventDefault();
74 if (!gridRef.current) return;
75
76 const rect = gridRef.current.getBoundingClientRect();
77 const ballElement = e.currentTarget as HTMLElement;
78 const ballRect = ballElement.getBoundingClientRect();
79
80 const ballCenterX = ballRect.left + ballRect.width / 2 - rect.left;
81 const ballCenterY = ballRect.top + ballRect.height / 2 - rect.top;
82
83 const ballRadius = 25;
84 const gridWidth = rect.width;
85 const gridHeight = rect.height;
86
87 const deviceX = ((ballCenterX - ballRadius) / (gridWidth - ballRadius * 2)) * 400 - 200;
88 const deviceY = ((ballCenterY - ballRadius) / (gridHeight - ballRadius * 2)) * 400 - 200;
89
90 updateDevicePosition({ x: deviceX, y: deviceY });
91 };
92
93 const resetPosition = () => {
94 setScale(1);
95 updateDevicePosition({ x: 0, y: 0, scale: 1 });
96 setFixedMargin(false);
97 setMargin({ top: 35, right: 35, bottom: 35, left: 35 });
98 setMarginPreset('medium');
99 };
100
101 const handleMarginPresetChange = (preset: string) => {
102 if (!preset) return;
103 setMarginPreset(preset);
104 let marginValue;
105 switch (preset) {
106 case 'small':
107 marginValue = 20;
108 break;
109 case 'medium':
110 marginValue = 35;
111 break;
112 case 'large':
113 marginValue = 50;
114 break;
115 default:
116 marginValue = 35;
117 }
118 setMargin({ top: marginValue, right: marginValue, bottom: marginValue, left: marginValue });
119 };
120
121 const positionBalls = [
122 { style: { left: '25px', top: '25px' } },
123 { style: { right: '25px', top: '25px' } },
124 { style: { left: '25px', bottom: '25px' } },
125 { style: { right: '25px', bottom: '25px' } },
126 { style: { left: '50%', top: '25px', transform: 'translateX(-50%)' } },
127 { style: { left: '50%', bottom: '25px', transform: 'translateX(-50%)' } },
128 { style: { left: '25px', top: '50%', transform: 'translateY(-50%)' } },
129 { style: { right: '25px', top: '50%', transform: 'translateY(-50%)' } },
130 { style: { left: '50%', top: '50%', transform: 'translate(-50%, -50%)' } },
131 ];
132
133 return (
134 <div
135 ref={windowRef}
136 className="md:fixed z-40 select-none"
137 style={{
138 left: `${windowPosition.x}px`,
139 top: `${windowPosition.y}px`,
140 }}
141 >
142 <Card className="bg-sidebar max-md:border-none border-sidebar-border rounded-2xl overflow-hidden min-w-80">
143 <div className="flex items-center justify-between p-4 bg-sidebar md:cursor-move border-b border-sidebar-border">
144 <div className="flex items-center gap-3" onMouseDown={handleWindowDragStart}>
145 <GripVertical className="w-4 h-4 text-primary/70" />
146 <span className="text-white text-lg font-semibold">Position / Scale</span>
147 </div>
148 <Button
149 onClick={resetPosition}
150 variant="ghost"
151 size="sm"
152 className="text-white hover:text-primary hover:bg-primary/20 p-3 rounded-full"
153 >
154 <Undo className="w-4 h-4" />
155 </Button>
156 </div>
157
158 <CardContent className="px-6 pt-4 pb-6 bg-sidebar">
159 <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
160 <TabsList className="grid w-full grid-cols-2 bg-sidebar/80 rounded-full ring-2 ring-secondary">
161 <TabsTrigger value="position" className="flex items-center gap-2 text-white hover:bg-primary/20 hover:text-primary rounded-r-xl rounded-l-3xl data-[state=active]:bg-primary data-[state=active]:text-black">
162 <Move className="w-4 h-4" />
163 Position
164 </TabsTrigger>
165 <TabsTrigger value="margins" className="flex items-center gap-2 text-white hover:bg-primary/20 hover:text-primary rounded-l-xl rounded-r-3xl data-[state=active]:bg-primary data-[state=active]:text-black">
166 <Maximize2 className="w-4 h-4" />
167 Margins
168 </TabsTrigger>
169 </TabsList>
170
171 <TabsContent value="position" className="mt-4">
172 <div
173 ref={gridRef}
174 className="relative w-full h-60 bg-primary/10 rounded-xl border border-primary/30 mb-6 overflow-hidden"
175 >
176 {positionBalls.map((ball, index) => (
177 <div
178 key={index}
179 className="absolute size-12 rounded-full cursor-pointer transition-all duration-200 bg-primary/40 hover:bg-primary/60 hover:scale-105 focus-visible:bg-primary focus-visible:scale-110 focus-visible:shadow-lg focus-visible:shadow-primary/50 focus-visible:ring-2 focus-visible:ring-white outline-none"
180 style={ball.style}
181 onClick={(e) => handleBallClick(e, index)}
182 />
183 ))}
184 </div>
185
186 <div className="space-y-3">
187 <div className="flex items-center justify-between">
188 <span className="text-white text-sm">Scale</span>
189 <span className="text-primary text-sm">{scale.toFixed(1)}x</span>
190 </div>
191 <Slider
192 value={[scale]}
193 onValueChange={(value) => {
194 setScale(value[0]);
195 updateDevicePosition({ scale: value[0] });
196 }}
197 min={0.1}
198 max={3}
199 step={0.1}
200 />
201 </div>
202 </TabsContent>
203
204 <TabsContent value="margins" className="mt-4">
205 <div className="space-y-4">
206 <div className="flex items-center justify-between">
207 <span className="text-white text-sm">Fixed Margin</span>
208 <Switch
209 checked={fixedMargin}
210 onCheckedChange={handleFixedMarginToggle}
211 className="data-[state=checked]:bg-primary"
212 />
213 </div>
214
215 {fixedMargin && (
216 <div className="space-y-4">
217 <ToggleGroup
218 type="single"
219 value={marginPreset}
220 onValueChange={handleMarginPresetChange}
221 className="flex gap-1 bg-sidebar/80 rounded-full ring-2 ring-secondary p-1"
222 >
223 <ToggleGroupItem
224 value="small"
225 className={`flex-1 text-sm rounded-full hover:bg-primary/20 hover:text-primary data-[state=on]:bg-primary data-[state=on]:text-black cursor-pointer`}
226 >
227 Small
228 </ToggleGroupItem>
229 <ToggleGroupItem
230 value="medium"
231 className={`flex-1 text-sm rounded-full hover:bg-primary/20 hover:text-primary data-[state=on]:bg-primary data-[state=on]:text-black cursor-pointer`}
232 >
233 Medium
234 </ToggleGroupItem>
235 <ToggleGroupItem
236 value="large"
237 className={`flex-1 text-sm rounded-full hover:bg-primary/20 hover:text-primary data-[state=on]:bg-primary data-[state=on]:text-black cursor-pointer`}
238 >
239 Large
240 </ToggleGroupItem>
241 </ToggleGroup>
242 <div className="space-y-3">
243 <div className="flex items-center justify-between">
244 <span className="text-white text-sm">Top Margin</span>
245 <span className="text-primary text-sm">{Math.round(margin.top)}</span>
246 </div>
247 <Slider
248 value={[margin.top]}
249 onValueChange={(value) => {
250 setMargin({ ...margin, top: value[0] });
251 setMarginPreset('');
252 }}
253 min={0}
254 max={100}
255 step={1}
256 />
257 <div className="flex items-center justify-between">
258 <span className="text-white text-sm">Right Margin</span>
259 <span className="text-primary text-sm">{Math.round(margin.right)}</span>
260 </div>
261 <Slider
262 value={[margin.right]}
263 onValueChange={(value) => {
264 setMargin({ ...margin, right: value[0] });
265 setMarginPreset('');
266 }}
267 min={0}
268 max={100}
269 step={1}
270 />
271 <div className="flex items-center justify-between">
272 <span className="text-white text-sm">Bottom Margin</span>
273 <span className="text-primary text-sm">{Math.round(margin.bottom)}</span>
274 </div>
275 <Slider
276 value={[margin.bottom]}
277 onValueChange={(value) => {
278 setMargin({ ...margin, bottom: value[0] });
279 setMarginPreset('');
280 }}
281 min={0}
282 max={100}
283 step={1}
284 />
285 <div className="flex items-center justify-between">
286 <span className="text-white text-sm">Left Margin</span>
287 <span className="text-primary text-sm">{Math.round(margin.left)}</span>
288 </div>
289 <Slider
290 value={[margin.left]}
291 onValueChange={(value) => {
292 setMargin({ ...margin, left: value[0] });
293 setMarginPreset('');
294 }}
295 min={0}
296 max={100}
297 step={1}
298 />
299 </div>
300 </div>
301 )}
302 </div>
303 </TabsContent>
304 </Tabs>
305 </CardContent>
306 </Card>
307 </div>
308 );
309};