1import React, { useState, useRef } from 'react';
2import { Button } from '@/components/ui/button';
3import {
4 Tooltip,
5 TooltipContent,
6 TooltipTrigger,
7} from '@/components/ui/tooltip';
8import { Separator } from '@/components/ui/separator';
9import {
10 Move,
11 Undo,
12 SquareDashed,
13 Rotate3D
14} from 'lucide-react';
15import { RotationSkewPanel } from './RotationSkewPanel';
16import { PositionScalePanel } from './PositionScalePanel';
17import { ImageBorderPanel } from './ImageBorderPanel';
18import { useMobile } from '@/hooks/use-mobile';
19import { Sheet, SheetContent } from '@/components/ui/sheet';
20import { useMockupStore } from '@/contexts/MockupContext';
21
22export const FloatingBar: React.FC = () => {
23 const {
24 uploadedImage,
25 setUploadedImage,
26 updateDevicePosition,
27 set3DRotation,
28 setImageBorder,
29 setMargin,
30 setFixedMargin
31 } = useMockupStore();
32
33 const [activePanel, setActivePanel] = useState<string | null>(null);
34 const [navPosition, setNavPosition] = useState({ x: 0, y: 0 });
35 const isMobile = useMobile();
36 const hasImageRef = useRef(false);
37
38 // Only update hasImageRef when uploadedImage actually changes between null and non-null
39 const hasImage = !!uploadedImage;
40 if (hasImageRef.current !== hasImage) {
41 hasImageRef.current = hasImage;
42 }
43
44 const handleReset = () => {
45 setMargin({ top: 0, right: 0, bottom: 0, left: 0 });
46 setFixedMargin(false);
47 setUploadedImage(null);
48 updateDevicePosition({ x: 0, y: 0, scale: 1, rotation: 0 });
49 set3DRotation({ rotateX: 0, rotateY: 0, rotateZ: 0, skew: 0 });
50 setImageBorder({ width: 8, color: '#FF6B6B', radius: 22, enabled: false });
51 setActivePanel(null);
52 };
53
54 const togglePanel = (panelName: string) => {
55 setActivePanel(prev => prev === panelName ? null : panelName);
56 };
57
58 // Separate the position calculation logic to only run when necessary
59 React.useEffect(() => {
60 if (!isMobile && hasImageRef.current) {
61 const updatePosition = () => {
62 const canvasElement = document.querySelector('[data-mockup-canvas]') as HTMLElement;
63 if (canvasElement) {
64 const rect = canvasElement.getBoundingClientRect();
65 const navHeight = 60;
66 const navWidth = 280;
67 const padding = 20;
68
69 let centerX = rect.left + rect.width / 2;
70 let bottomY = rect.bottom - 80;
71
72 const maxX = window.innerWidth - navWidth / 2 - padding;
73 const minX = navWidth / 2 + padding;
74 centerX = Math.max(minX, Math.min(maxX, centerX));
75
76 const maxY = window.innerHeight - navHeight - padding;
77 const minY = padding;
78 bottomY = Math.max(minY, Math.min(maxY, bottomY));
79
80 setNavPosition({
81 x: centerX,
82 y: bottomY
83 });
84 }
85 };
86
87 updatePosition();
88 window.addEventListener('resize', updatePosition);
89 window.addEventListener('scroll', updatePosition);
90
91 return () => {
92 window.removeEventListener('resize', updatePosition);
93 window.removeEventListener('scroll', updatePosition);
94 };
95 }
96 }, [isMobile, hasImageRef.current]); // Remove uploadedImage dependency
97
98 // Render panel components only once and keep them stable
99 const panelComponents = React.useMemo(() => ({
100 rotation: <RotationSkewPanel onClose={() => setActivePanel(null)} />,
101 position: <PositionScalePanel onClose={() => setActivePanel(null)} />,
102 border: <ImageBorderPanel onClose={() => setActivePanel(null)} />
103 }), []); // Empty dependency array - components are stable
104
105 const renderPanel = () => {
106 if (!activePanel) return null;
107
108 const PanelContent = panelComponents[activePanel as keyof typeof panelComponents];
109
110 if (isMobile) {
111 return (
112 <Sheet open={!!activePanel} onOpenChange={() => setActivePanel(null)}>
113 <SheetContent side="bottom" className="bg-sidebar border-t border-sidebar-border">
114 {PanelContent}
115 </SheetContent>
116 </Sheet>
117 );
118 }
119
120 return PanelContent;
121 };
122
123 const NavButtons = () => (
124 <>
125 <Tooltip>
126 <TooltipTrigger asChild>
127 <Button
128 onClick={handleReset}
129 variant="outline"
130 className={`text-white hover:text-primary hover:bg-primary/20 rounded-full`}
131 disabled={!hasImage}
132 >
133 <Undo className="w-10 h-10" />
134 Reset
135 </Button>
136 </TooltipTrigger>
137 <TooltipContent><p>Reset Transformations</p></TooltipContent>
138 </Tooltip>
139
140 {!isMobile && <Separator orientation="vertical" className="h-6 bg-primary/40 mx-1" />}
141
142 <Tooltip>
143 <TooltipTrigger asChild>
144 <Button
145 onClick={() => togglePanel('rotation')}
146 variant={activePanel === 'rotation' ? 'default' : 'ghost'}
147 className={`rounded-full
148 ${activePanel === 'rotation'
149 ? 'bg-primary hover:bg-primary/80 text-black'
150 : 'text-white hover:text-primary hover:bg-primary/20'
151 }`}
152 disabled={!hasImage}
153 >
154 <Rotate3D className="w-10 h-10" />
155 {!isMobile && 'Rotate & Transform'}
156 </Button>
157 </TooltipTrigger>
158 <TooltipContent><p>3D Rotation</p></TooltipContent>
159 </Tooltip>
160
161 <Tooltip>
162 <TooltipTrigger asChild>
163 <Button
164 onClick={() => togglePanel('position')}
165 variant={activePanel === 'position' ? 'default' : 'ghost'}
166 className={`rounded-full py-3
167 ${activePanel === 'position'
168 ? 'bg-primary hover:bg-primary/80 text-black'
169 : 'text-white hover:text-primary hover:bg-primary/20'
170 }`}
171 disabled={!hasImage}
172 >
173 <Move className="w-10 h-10" />
174 {!isMobile && 'Position & Scale'}
175 </Button>
176 </TooltipTrigger>
177 <TooltipContent><p>Position & Scale</p></TooltipContent>
178 </Tooltip>
179
180 <Tooltip>
181 <TooltipTrigger asChild>
182 <Button
183 onClick={() => togglePanel('border')}
184 variant={activePanel === 'border' ? 'default' : 'ghost'}
185 className={`rounded-full
186 ${activePanel === 'border'
187 ? 'bg-primary hover:bg-primary/80 text-black'
188 : 'text-white hover:text-primary hover:bg-primary/20'
189 }`}
190 disabled={!hasImage}
191 >
192 <SquareDashed className="w-10 h-10" />
193 {!isMobile && 'Border'}
194 </Button>
195 </TooltipTrigger>
196 <TooltipContent><p>Image Border</p></TooltipContent>
197 </Tooltip>
198 </>
199 );
200
201 if (isMobile) {
202 return (
203 <>
204 {renderPanel()}
205 <div className="fixed bottom-0 left-0 right-0 h-20 bg-sidebar border-t border-sidebar-border flex items-center justify-between px-4">
206 <NavButtons />
207 </div>
208 </>
209 );
210 }
211
212 return (
213 <>
214 {renderPanel()}
215 <div
216 className="fixed z-30 -translate-x-1/2"
217 style={{
218 left: `${navPosition.x}px`,
219 top: `${navPosition.y}px`
220 }}
221 >
222 <div className="flex items-center gap-1 bg-sidebar/80 backdrop-blur-lg border-2 border-primary/60 rounded-full shadow-2xl p-1.5">
223 <NavButtons />
224 </div>
225 </div>
226 </>
227 );
228};