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 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') || 'Призы'}
+
+