'use client'; import { Button } from '@/ui/Button'; import { Card } from '@/ui/Card'; import { Heading } from '@/ui/Heading'; import { Icon } from '@/ui/Icon'; import { Stack } from '@/ui/Stack'; import { Text } from '@/ui/Text'; import { Image as ImageIcon, RotateCw, Save, Target, ZoomIn, ZoomOut } from 'lucide-react'; import { useCallback, useRef, useState } from 'react'; interface DecalPlacement { id: string; sponsorType: 'main' | 'secondary-1' | 'secondary-2'; sponsorName: string; x: number; // 0-1 normalized y: number; // 0-1 normalized width: number; // 0-1 normalized height: number; // 0-1 normalized rotation: number; // 0-360 degrees } interface LeagueDecalPlacementEditorProps { leagueId: string; seasonId: string; carId: string; carName: string; baseImageUrl?: string; existingPlacements?: DecalPlacement[]; onSave?: (placements: DecalPlacement[]) => void; } const DEFAULT_PLACEMENTS: Omit[] = [ { sponsorType: 'main', sponsorName: 'Main Sponsor', x: 0.3, y: 0.15, width: 0.4, height: 0.15, rotation: 0, }, { sponsorType: 'secondary-1', sponsorName: 'Secondary Sponsor 1', x: 0.05, y: 0.5, width: 0.15, height: 0.1, rotation: 0, }, { sponsorType: 'secondary-2', sponsorName: 'Secondary Sponsor 2', x: 0.8, y: 0.5, width: 0.15, height: 0.1, rotation: 0, }, ]; export function LeagueDecalPlacementEditor({ leagueId, seasonId, carId, carName, baseImageUrl, existingPlacements, onSave, }: LeagueDecalPlacementEditorProps) { const canvasRef = useRef(null); const [placements, setPlacements] = useState( existingPlacements ?? DEFAULT_PLACEMENTS.map((p, i) => ({ ...p, id: `decal-${i}` })) ); const [selectedDecal, setSelectedDecal] = useState(null); const [isDragging, setIsDragging] = useState(false); const [zoom, setZoom] = useState(1); const [saving, setSaving] = useState(false); const selectedPlacement = placements.find(p => p.id === selectedDecal); const handleDecalClick = (id: string) => { setSelectedDecal(id === selectedDecal ? null : id); }; const updatePlacement = useCallback((id: string, updates: Partial) => { setPlacements(prev => prev.map(p => p.id === id ? { ...p, ...updates } : p )); }, []); const handleMouseDown = (e: React.MouseEvent, decalId: string) => { e.stopPropagation(); setSelectedDecal(decalId); setIsDragging(true); }; const handleMouseMove = useCallback((e: React.MouseEvent) => { if (!isDragging || !selectedDecal || !canvasRef.current) return; const rect = canvasRef.current.getBoundingClientRect(); const x = (e.clientX - rect.left) / rect.width; const y = (e.clientY - rect.top) / rect.height; // Clamp to canvas bounds const clampedX = Math.max(0, Math.min(1, x)); const clampedY = Math.max(0, Math.min(1, y)); updatePlacement(selectedDecal, { x: clampedX, y: clampedY }); }, [isDragging, selectedDecal, updatePlacement]); const handleMouseUp = () => { setIsDragging(false); }; const handleRotate = (id: string, delta: number) => { const placement = placements.find(p => p.id === id); if (placement) { const newRotation = (placement.rotation + delta + 360) % 360; updatePlacement(id, { rotation: newRotation }); } }; const handleResize = (id: string, scaleFactor: number) => { const placement = placements.find(p => p.id === id); if (placement) { const newWidth = Math.max(0.05, Math.min(0.5, placement.width * scaleFactor)); const newHeight = Math.max(0.03, Math.min(0.3, placement.height * scaleFactor)); updatePlacement(id, { width: newWidth, height: newHeight }); } }; const handleSave = async () => { setSaving(true); try { // Alpha: In-memory save simulation console.log('Saving decal placements:', { leagueId, seasonId, carId, placements, }); await new Promise(resolve => setTimeout(resolve, 500)); onSave?.(placements); alert('Decal placements saved successfully.'); } catch (err) { console.error('Save failed:', err); alert('Failed to save placements.'); } finally { setSaving(false); } }; const getSponsorTypeColor = (type: DecalPlacement['sponsorType']) => { switch (type) { case 'main': return { border: 'border-primary-blue', bg: 'bg-primary-blue/20', text: 'text-primary-blue' }; case 'secondary-1': return { border: 'border-purple-500', bg: 'bg-purple-500/20', text: 'text-purple-400' }; case 'secondary-2': return { border: 'border-purple-500', bg: 'bg-purple-500/20', text: 'text-purple-400' }; } }; return ( {/* Header */} {carName} Position sponsor decals on this car's template {Math.round(zoom * 100)}% {/* Canvas */} {/* Base Image or Placeholder */} {baseImageUrl ? ( ) : ( No base template uploaded Upload a template image first )} {/* Decal Placeholders */} {placements.map((placement) => { const decalColors = getSponsorTypeColor(placement.sponsorType); return ( handleMouseDown(e, placement.id)} onClick={() => handleDecalClick(placement.id)} position="absolute" cursor="move" border borderWidth="2px" rounded="sm" display="flex" alignItems="center" justifyContent="center" // eslint-disable-next-line gridpilot-rules/component-classification className={`text-xs font-medium transition-all ${ selectedDecal === placement.id ? `${decalColors.border} ${decalColors.bg} ${decalColors.text} shadow-lg` : `${decalColors.border} ${decalColors.bg} ${decalColors.text} opacity-70 hover:opacity-100` }`} // eslint-disable-next-line gridpilot-rules/component-classification style={{ left: `${placement.x * 100}%`, top: `${placement.y * 100}%`, width: `${placement.width * 100}%`, height: `${placement.height * 100}%`, transform: `translate(-50%, -50%) rotate(${placement.rotation}deg)`, }} > {placement.sponsorType === 'main' ? 'Main' : 'Secondary'} {placement.sponsorName} {/* Drag handle indicator */} {selectedDecal === placement.id && ( )} ); })} {/* Grid overlay when dragging */} {isDragging && ( )} Click a decal to select it, then drag to reposition. Use controls on the right to adjust size and rotation. {/* Controls Panel */} {/* Decal List */} Sponsor Slots {placements.map((placement) => { const decalColors = getSponsorTypeColor(placement.sponsorType); return ( setSelectedDecal(placement.id)} w="full" p={3} rounded="lg" border textAlign="left" transition borderColor={selectedDecal === placement.id ? decalColors.border : 'border-charcoal-outline'} bg={selectedDecal === placement.id ? decalColors.bg : 'bg-iron-gray/30'} hoverBg={selectedDecal !== placement.id ? 'bg-iron-gray/50' : undefined} > {placement.sponsorType === 'main' ? 'Main Sponsor' : `Secondary ${placement.sponsorType.split('-')[1]}`} {Math.round(placement.x * 100)}%, {Math.round(placement.y * 100)}% • {placement.rotation}° ); })} {/* Selected Decal Controls */} {selectedPlacement && ( Adjust Selected {/* Position */} Position X ) => updatePlacement(selectedPlacement.id, { x: parseInt(e.target.value) / 100 })} fullWidth h="2" bg="bg-charcoal-outline" rounded="lg" // eslint-disable-next-line gridpilot-rules/component-classification className="appearance-none cursor-pointer accent-primary-blue" /> Y ) => updatePlacement(selectedPlacement.id, { y: parseInt(e.target.value) / 100 })} fullWidth h="2" bg="bg-charcoal-outline" rounded="lg" // eslint-disable-next-line gridpilot-rules/component-classification className="appearance-none cursor-pointer accent-primary-blue" /> {/* Size */} Size {/* Rotation */} Rotation: {selectedPlacement.rotation}° ) => updatePlacement(selectedPlacement.id, { rotation: parseInt(e.target.value) })} flexGrow={1} h="2" bg="bg-charcoal-outline" rounded="lg" // eslint-disable-next-line gridpilot-rules/component-classification className="appearance-none cursor-pointer accent-primary-blue" /> )} {/* Save Button */} {/* Help Text */} Tip: Main sponsor gets the largest, most prominent placement. Secondary sponsors get smaller positions. These decals will be burned onto all driver liveries. ); }