diff --git a/src/components/wheel/FortuneWheel.tsx b/src/components/wheel/FortuneWheel.tsx index e1e1ef6..547dc89 100644 --- a/src/components/wheel/FortuneWheel.tsx +++ b/src/components/wheel/FortuneWheel.tsx @@ -26,16 +26,17 @@ const FortuneWheel = memo(function FortuneWheel({ const [displayRotation, setDisplayRotation] = useState(0); const [lightPhase, setLightPhase] = useState(0); - // Animated lights effect - running lights when spinning + // Animated lights effect - always running, speed depends on spinning state useEffect(() => { - if (isSpinning) { - const interval = setInterval(() => { - setLightPhase((p) => (p + 1) % 20); // Shift position around the wheel - }, 150); // Fast interval for running effect - return () => clearInterval(interval); - } else { - setLightPhase(0); - } + // 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(() => { @@ -58,13 +59,16 @@ const FortuneWheel = memo(function FortuneWheel({ // 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) => { - if (!isSpinning) { - return i % 2 === 0; // Static alternating pattern + // Check if this light index is within the active range + for (let j = 0; j < numLights; j++) { + if (i === (lightPhase + j) % 20) { + return true; + } } - // Running lights - 3 lit lights move around the wheel - const pos = (i - lightPhase + 20) % 20; - return pos < 3; + return false; }); }, [isSpinning, lightPhase]); @@ -118,17 +122,6 @@ const FortuneWheel = memo(function FortuneWheel({ }; }; - // Position for text - between hub and emoji - const getTextPosition = (index: number) => { - const angle = (index * sectorAngle + sectorAngle / 2 - 90) * (Math.PI / 180); - const textRadius = prizeRadius * 0.45; - return { - x: center + textRadius * Math.cos(angle), - y: center + textRadius * Math.sin(angle), - rotation: index * sectorAngle + sectorAngle / 2, - }; - }; - // Alternate colors for sectors const getSectorColors = (index: number, baseColor?: string) => { if (baseColor) return baseColor; @@ -145,15 +138,6 @@ const FortuneWheel = memo(function FortuneWheel({ return colors[index % colors.length]; }; - // Truncate text intelligently - const truncateText = (text: string, maxLen: number) => { - if (text.length <= maxLen) return text; - return text.substring(0, maxLen - 1) + '..'; - }; - - // Calculate max text length based on number of sectors - const maxTextLength = prizes.length <= 4 ? 12 : prizes.length <= 6 ? 10 : 8; - return (
{/* Outer glow effect */} @@ -366,28 +350,7 @@ const FortuneWheel = memo(function FortuneWheel({ ); })} - {/* Prize content - Text */} - {prizes.map((prize, index) => { - const pos = getTextPosition(index); - const displayText = truncateText(prize.display_name, maxTextLength); - return ( - - {displayText} - - ); - })} + {/* Prize content - Text removed, only emoji visible on wheel */} {/* Center hub */} diff --git a/src/components/wheel/WheelLegend.tsx b/src/components/wheel/WheelLegend.tsx new file mode 100644 index 0000000..48dc0eb --- /dev/null +++ b/src/components/wheel/WheelLegend.tsx @@ -0,0 +1,62 @@ +import { memo } from 'react'; +import type { WheelPrize } from '../../api/wheel'; + +interface WheelLegendProps { + prizes: WheelPrize[]; +} + +const WheelLegend = memo(function WheelLegend({ prizes }: WheelLegendProps) { + // Get sector color (same logic as in FortuneWheel) + const getSectorColor = (index: number, baseColor?: string) => { + if (baseColor) return baseColor; + const colors = [ + '#8B5CF6', + '#EC4899', + '#3B82F6', + '#10B981', + '#F59E0B', + '#EF4444', + '#6366F1', + '#14B8A6', + ]; + return colors[index % colors.length]; + }; + + return ( +
+ {prizes.map((prize, index) => { + const color = getSectorColor(index, prize.color); + return ( +
+ {/* Color indicator */} +
+ + {/* Emoji */} +
+ {prize.emoji} +
+ + {/* Prize name */} +
+
{prize.display_name}
+
+ + {/* Prize value (if applicable) */} + {prize.prize_type !== 'nothing' && prize.value && ( +
+ {prize.prize_type === 'balance' && `${prize.value} ₽`} + {prize.prize_type === 'subscription_days' && `${prize.value}д`} + {prize.prize_type === 'promocode' && '🎫'} +
+ )} +
+ ); + })} +
+ ); +}); + +export default WheelLegend; diff --git a/src/locales/en.json b/src/locales/en.json index 5c4e811..23ec6f1 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -678,6 +678,7 @@ "wheel": { "title": "Fortune Wheel", "disabled": "Fortune Wheel is currently unavailable", + "prizes": "Prizes", "spinsRemaining": "Spins remaining today", "choosePayment": "Choose payment method", "stars": "Stars", @@ -983,6 +984,8 @@ "deletePrize": "Delete Prize", "confirmDelete": "Are you sure you want to delete this prize? This action cannot be undone.", "dragToReorder": "Drag to reorder", + "unsavedOrder": "Unsaved order changes", + "unsavedOrderHint": "Save or discard your changes", "types": { "subscription_days": "Subscription Days", "balance_bonus": "Balance Bonus", diff --git a/src/locales/fa.json b/src/locales/fa.json index 0021deb..7ec6825 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -567,6 +567,7 @@ "wheel": { "title": "چرخ شانس", "disabled": "چرخ شانس فعلاً در دسترس نیست", + "prizes": "جوایز", "spinsRemaining": "تعداد چرخش باقی‌مانده امروز", "choosePayment": "روش پرداخت را انتخاب کنید", "stars": "Stars", @@ -704,6 +705,8 @@ "noPrizes": "جایزه‌ای تنظیم نشده. برای فعال‌سازی چرخ جایزه اضافه کنید.", "deletePrize": "این جایزه حذف شود؟", "dragToReorder": "کشیدن برای مرتب‌سازی", + "unsavedOrder": "تغییرات ترتیب ذخیره نشده", + "unsavedOrderHint": "تغییرات را ذخیره یا لغو کنید", "confirmDelete": "آیا از حذف این جایزه مطمئن هستید؟", "types": { "subscription_days": "روز اشتراک", diff --git a/src/locales/ru.json b/src/locales/ru.json index 3028324..a6064db 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -705,6 +705,7 @@ "wheel": { "title": "Колесо удачи", "disabled": "Колесо удачи временно недоступно", + "prizes": "Призы", "spinsRemaining": "Осталось вращений сегодня", "choosePayment": "Выберите способ оплаты", "stars": "Stars", @@ -1007,6 +1008,8 @@ "deletePrize": "Удалить приз", "confirmDelete": "Вы уверены, что хотите удалить этот приз? Это действие нельзя отменить.", "dragToReorder": "Перетащите для изменения порядка", + "unsavedOrder": "Есть несохраненные изменения порядка", + "unsavedOrderHint": "Сохраните изменения или отмените их", "types": { "subscription_days": "Дни подписки", "balance_bonus": "Бонус баланса", diff --git a/src/locales/zh.json b/src/locales/zh.json index a84dcf5..bf4e24b 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -567,6 +567,7 @@ "wheel": { "title": "幸运转盘", "disabled": "幸运转盘暂时不可用", + "prizes": "奖品", "spinsRemaining": "今日剩余次数", "choosePayment": "选择支付方式", "stars": "Stars", @@ -756,6 +757,8 @@ "noPrizes": "未配置奖品。添加奖品以启用转盘。", "deletePrize": "删除此奖品?", "dragToReorder": "拖动排序", + "unsavedOrder": "有未保存的顺序更改", + "unsavedOrderHint": "保存或放弃更改", "confirmDelete": "确定要删除这个奖品吗?", "types": { "subscription_days": "订阅天数", diff --git a/src/pages/Wheel.tsx b/src/pages/Wheel.tsx index 9184a67..df745bc 100644 --- a/src/pages/Wheel.tsx +++ b/src/pages/Wheel.tsx @@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { wheelApi, type SpinResult, type SpinHistoryItem } from '../api/wheel'; import FortuneWheel from '../components/wheel/FortuneWheel'; +import WheelLegend from '../components/wheel/WheelLegend'; import InsufficientBalancePrompt from '../components/InsufficientBalancePrompt'; import { useCurrency } from '../hooks/useCurrency'; import { usePlatform, useHaptic } from '@/platform'; @@ -499,133 +500,145 @@ export default function Wheel() { {/* Wheel Section */} -
- {/* Wheel */} - +
+ {/* Left: Wheel and Controls */} +
+ {/* Wheel */} + - {/* Spin Controls */} -
- {/* Payment type toggle - only if both methods available */} - {bothMethodsAvailable && ( -
-
- - + {/* Spin Controls */} +
+ {/* Payment type toggle - only if both methods available */} + {bothMethodsAvailable && ( +
+
+ + +
-
- )} + )} - {/* Single Spin Button */} - - - {/* Cost subtitle when no toggle */} - {costSubtitle && !bothMethodsAvailable && ( -

{costSubtitle}

- )} - - {/* Cannot spin hint */} - {!config.can_spin && !isSpinning && ( - <> - {config.can_spin_reason === 'daily_limit_reached' ? ( -
-

{t('wheel.errors.dailyLimitReached')}

-
- ) : config.can_spin_reason === 'insufficient_balance' ? ( - - ) : ( -
-

{t('wheel.errors.cannotSpin')}

-
- )} - - )} - - {/* Inline Result Card */} - {spinResult && !isSpinning && ( -
-
-
- {spinResult.success ? spinResult.emoji || '🎉' : '😔'} -
-
-
- {spinResult.success && spinResult.prize_display_name - ? spinResult.prize_display_name - : spinResult.success - ? spinResult.prize_type === 'nothing' - ? t('wheel.noLuck') - : t('wheel.congratulations') - : t('wheel.oops')} -
-
{spinResult.message}
-
- -
+ {isSpinning ? t('wheel.spinning') : t('wheel.spin')} + - {/* Promocode if won */} - {spinResult.promocode && ( -
-

{t('wheel.yourPromoCode')}

-

- {spinResult.promocode} -

+ {/* Cost subtitle when no toggle */} + {costSubtitle && !bothMethodsAvailable && ( +

{costSubtitle}

+ )} + + {/* Cannot spin hint */} + {!config.can_spin && !isSpinning && ( + <> + {config.can_spin_reason === 'daily_limit_reached' ? ( +
+

{t('wheel.errors.dailyLimitReached')}

+
+ ) : config.can_spin_reason === 'insufficient_balance' ? ( + + ) : ( +
+

{t('wheel.errors.cannotSpin')}

+
+ )} + + )} + + {/* Inline Result Card */} + {spinResult && !isSpinning && ( +
+
+
+ {spinResult.success ? spinResult.emoji || '🎉' : '😔'} +
+
+
+ {spinResult.success && spinResult.prize_display_name + ? spinResult.prize_display_name + : spinResult.success + ? spinResult.prize_type === 'nothing' + ? t('wheel.noLuck') + : t('wheel.congratulations') + : t('wheel.oops')} +
+
{spinResult.message}
+
+
- )} -
- )} + + {/* Promocode if won */} + {spinResult.promocode && ( +
+

{t('wheel.yourPromoCode')}

+

+ {spinResult.promocode} +

+
+ )} +
+ )} +
+ {/* End of left column: wheel and controls */} +
+ + {/* Right column (desktop) / Bottom (mobile): Prize Legend */} +
+

+ {t('wheel.prizes') || 'Призы'} +

+