'use client'; import { useState, useRef, useCallback } from 'react'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import { Move, RotateCw, ZoomIn, ZoomOut, Save, Trash2, Plus, Image, Target } from 'lucide-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 default 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 ? ( Livery template ) : (

No base template uploaded

Upload a template image first

)} {/* Decal Placeholders */} {placements.map((placement) => { const colors = getSponsorTypeColor(placement.sponsorType); return (
handleMouseDown(e, placement.id)} onClick={() => handleDecalClick(placement.id)} className={`absolute cursor-move border-2 rounded flex items-center justify-center text-xs font-medium transition-all ${ selectedDecal === placement.id ? `${colors.border} ${colors.bg} ${colors.text} shadow-lg` : `${colors.border} ${colors.bg} ${colors.text} opacity-70 hover:opacity-100` }`} 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 colors = getSponsorTypeColor(placement.sponsorType); return ( ); })}
{/* Selected Decal Controls */} {selectedPlacement && (

Adjust Selected

{/* Position */}
updatePlacement(selectedPlacement.id, { x: parseInt(e.target.value) / 100 })} className="w-full h-2 bg-charcoal-outline rounded-lg appearance-none cursor-pointer accent-primary-blue" />
updatePlacement(selectedPlacement.id, { y: parseInt(e.target.value) / 100 })} className="w-full h-2 bg-charcoal-outline rounded-lg appearance-none cursor-pointer accent-primary-blue" />
{/* Size */}
{/* Rotation */}
updatePlacement(selectedPlacement.id, { rotation: parseInt(e.target.value) })} className="flex-1 h-2 bg-charcoal-outline rounded-lg 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.

); }