stunning screenshots in seconds https://moocup.jaydip.me
at from-github 7.4 kB view raw
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};