diff --git a/src/components/admin/ThemeTab.tsx b/src/components/admin/ThemeTab.tsx index 42b319a..da9f2f3 100644 --- a/src/components/admin/ThemeTab.tsx +++ b/src/components/admin/ThemeTab.tsx @@ -92,40 +92,6 @@ export function ThemeTab() { const hasUnsavedChanges = !colorsEqual(draftColors, savedColorsRef.current); - // Update a single color in the draft and apply preview instantly - const updateDraftColor = useCallback( - (key: keyof ThemeColors, value: string) => { - setDraftColors((prev) => { - const next = { ...prev, [key]: value }; - applyThemeColors(next); - queryClient.setQueryData(['theme-colors'], next); - return next; - }); - }, - [queryClient], - ); - - // Apply a full preset to draft - const applyPreset = useCallback( - (colors: Partial) => { - setDraftColors((prev) => { - const next = { ...prev, ...colors }; - applyThemeColors(next); - queryClient.setQueryData(['theme-colors'], next); - return next; - }); - }, - [queryClient], - ); - - // Cancel: revert draft to saved - const handleCancel = useCallback(() => { - const saved = savedColorsRef.current; - setDraftColors(saved); - applyThemeColors(saved); - queryClient.setQueryData(['theme-colors'], saved); - }, [queryClient]); - // Mutations const updateColorsMutation = useMutation({ mutationFn: themeColorsApi.updateColors, @@ -183,6 +149,42 @@ export function ThemeTab() { }, }); + // Update a single color in the draft and apply preview instantly + const updateDraftColor = useCallback( + (key: keyof ThemeColors, value: string) => { + setDraftColors((prev) => { + const next = { ...prev, [key]: value }; + applyThemeColors(next); + queryClient.setQueryData(['theme-colors'], next); + return next; + }); + }, + [queryClient], + ); + + // Apply a full preset and auto-save to server + const applyPreset = useCallback( + (colors: Partial) => { + setDraftColors((prev) => { + const next = { ...prev, ...colors }; + applyThemeColors(next); + queryClient.setQueryData(['theme-colors'], next); + // Auto-save preset to server so it persists across navigation + updateColorsMutation.mutate(next); + return next; + }); + }, + [queryClient, updateColorsMutation], + ); + + // Cancel: revert draft to saved + const handleCancel = useCallback(() => { + const saved = savedColorsRef.current; + setDraftColors(saved); + applyThemeColors(saved); + queryClient.setQueryData(['theme-colors'], saved); + }, [queryClient]); + return (
{/* Theme toggles */} @@ -416,17 +418,21 @@ export function ThemeTab() { )} -
)} + + {/* Reset all colors */} +
+ +
); } diff --git a/src/components/common/PageLoader.tsx b/src/components/common/PageLoader.tsx index 4f1ade8..f9048dd 100644 --- a/src/components/common/PageLoader.tsx +++ b/src/components/common/PageLoader.tsx @@ -6,7 +6,7 @@ export default function PageLoader({ variant = 'dark' }: PageLoaderProps) { const spinnerColor = variant === 'dark' ? 'border-accent-500' : 'border-blue-500'; return ( -
+
diff --git a/src/components/wheel/FortuneWheel.tsx b/src/components/wheel/FortuneWheel.tsx index 110c094..4563a27 100644 --- a/src/components/wheel/FortuneWheel.tsx +++ b/src/components/wheel/FortuneWheel.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState, useMemo, memo } from 'react'; +import { useEffect, useRef, useState, memo } from 'react'; import type { WheelPrize } from '../../api/wheel'; interface FortuneWheelProps { @@ -24,20 +24,6 @@ const FortuneWheel = memo(function FortuneWheel({ const wheelRef = useRef(null); const accumulatedRotation = useRef(0); const [displayRotation, setDisplayRotation] = useState(0); - const [lightPhase, setLightPhase] = useState(0); - - // Animated lights effect - always running, speed depends on spinning state - useEffect(() => { - // Faster animation when spinning, slower when idle - const interval = setInterval( - () => { - setLightPhase((p) => (p + 1) % 20); - }, - isSpinning ? 100 : 300, - ); // 100ms when spinning, 300ms when idle - - return () => clearInterval(interval); - }, [isSpinning]); useEffect(() => { if (isSpinning && targetRotation !== null && wheelRef.current) { @@ -57,18 +43,6 @@ const FortuneWheel = memo(function FortuneWheel({ } }, [isSpinning, targetRotation, onSpinComplete]); - // Memoize light pattern calculation - const lightPattern = useMemo(() => { - const numLights = isSpinning ? 3 : 4; // 3 lights when spinning, 4 when idle - - return Array.from({ length: 20 }, (_, i) => { - // Calculate distance from current lightPhase - const distance = (i - lightPhase + 20) % 20; - // Light is on if within range [0, numLights) - return distance < numLights; - }); - }, [isSpinning, lightPhase]); - if (prizes.length === 0) { return (
@@ -257,36 +231,53 @@ const FortuneWheel = memo(function FortuneWheel({ strokeWidth="2" /> - {/* LED lights on outer ring - positioned toward outer edge to avoid bleeding into sectors */} - {Array.from({ length: 20 }).map((_, i) => { - const angle = (i * 18 - 90) * (Math.PI / 180); - const ledRadius = outerRadius + 3; - const dotX = center + ledRadius * Math.cos(angle); - const dotY = center + ledRadius * Math.sin(angle); - const isLit = lightPattern[i] ?? i % 2 === 0; - return ( - - {isLit && ( + {/* LED chase animation — pure CSS, no React re-renders */} + + + {Array.from({ length: 20 }).map((_, i) => { + const angle = (i * 18 - 90) * (Math.PI / 180); + const ledRadius = outerRadius + 3; + const dotX = center + ledRadius * Math.cos(angle); + const dotY = center + ledRadius * Math.sin(angle); + // Delay as fraction of full cycle — CSS handles speed via animation-duration + const delay = `${(i / 20) * 6}s`; + return ( + - )} - - - ); - })} + + + ); + })} + {/* Rotating wheel group */}