stunning screenshots in seconds https://moocup.jaydip.me
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};