mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
Improve wheel: fix lights animation and add prize legend
- Fix lights animation to run continuously (even when idle) - Lights now move sequentially around the circle - Remove text labels from wheel, keep only emoji - Add WheelLegend component with color indicators - Display legend on right (desktop) and bottom (mobile) - Add 'prizes' translation key for all languages
This commit is contained in:
@@ -26,16 +26,17 @@ const FortuneWheel = memo(function FortuneWheel({
|
|||||||
const [displayRotation, setDisplayRotation] = useState(0);
|
const [displayRotation, setDisplayRotation] = useState(0);
|
||||||
const [lightPhase, setLightPhase] = 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(() => {
|
useEffect(() => {
|
||||||
if (isSpinning) {
|
// Faster animation when spinning, slower when idle
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(
|
||||||
setLightPhase((p) => (p + 1) % 20); // Shift position around the wheel
|
() => {
|
||||||
}, 150); // Fast interval for running effect
|
setLightPhase((p) => (p + 1) % 20);
|
||||||
return () => clearInterval(interval);
|
},
|
||||||
} else {
|
isSpinning ? 100 : 300,
|
||||||
setLightPhase(0);
|
); // 100ms when spinning, 300ms when idle
|
||||||
}
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
}, [isSpinning]);
|
}, [isSpinning]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -58,13 +59,16 @@ const FortuneWheel = memo(function FortuneWheel({
|
|||||||
|
|
||||||
// Memoize light pattern calculation
|
// Memoize light pattern calculation
|
||||||
const lightPattern = useMemo(() => {
|
const lightPattern = useMemo(() => {
|
||||||
|
const numLights = isSpinning ? 3 : 4; // 3 lights when spinning, 4 when idle
|
||||||
|
|
||||||
return Array.from({ length: 20 }, (_, i) => {
|
return Array.from({ length: 20 }, (_, i) => {
|
||||||
if (!isSpinning) {
|
// Check if this light index is within the active range
|
||||||
return i % 2 === 0; // Static alternating pattern
|
for (let j = 0; j < numLights; j++) {
|
||||||
|
if (i === (lightPhase + j) % 20) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Running lights - 3 lit lights move around the wheel
|
return false;
|
||||||
const pos = (i - lightPhase + 20) % 20;
|
|
||||||
return pos < 3;
|
|
||||||
});
|
});
|
||||||
}, [isSpinning, lightPhase]);
|
}, [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
|
// Alternate colors for sectors
|
||||||
const getSectorColors = (index: number, baseColor?: string) => {
|
const getSectorColors = (index: number, baseColor?: string) => {
|
||||||
if (baseColor) return baseColor;
|
if (baseColor) return baseColor;
|
||||||
@@ -145,15 +138,6 @@ const FortuneWheel = memo(function FortuneWheel({
|
|||||||
return colors[index % colors.length];
|
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 (
|
return (
|
||||||
<div className="relative mx-auto w-full max-w-[380px] select-none">
|
<div className="relative mx-auto w-full max-w-[380px] select-none">
|
||||||
{/* Outer glow effect */}
|
{/* Outer glow effect */}
|
||||||
@@ -366,28 +350,7 @@ const FortuneWheel = memo(function FortuneWheel({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Prize content - Text */}
|
{/* Prize content - Text removed, only emoji visible on wheel */}
|
||||||
{prizes.map((prize, index) => {
|
|
||||||
const pos = getTextPosition(index);
|
|
||||||
const displayText = truncateText(prize.display_name, maxTextLength);
|
|
||||||
return (
|
|
||||||
<text
|
|
||||||
key={`text-${prize.id}`}
|
|
||||||
x={pos.x}
|
|
||||||
y={pos.y}
|
|
||||||
textAnchor="middle"
|
|
||||||
dominantBaseline="middle"
|
|
||||||
fontSize={prizes.length <= 4 ? '13' : prizes.length <= 6 ? '11' : '9'}
|
|
||||||
fontWeight="700"
|
|
||||||
fill="#FFFFFF"
|
|
||||||
transform={`rotate(${pos.rotation}, ${pos.x}, ${pos.y})`}
|
|
||||||
filter="url(#textShadow)"
|
|
||||||
style={{ letterSpacing: '0.03em' }}
|
|
||||||
>
|
|
||||||
{displayText}
|
|
||||||
</text>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
{/* Center hub */}
|
{/* Center hub */}
|
||||||
|
|||||||
62
src/components/wheel/WheelLegend.tsx
Normal file
62
src/components/wheel/WheelLegend.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{prizes.map((prize, index) => {
|
||||||
|
const color = getSectorColor(index, prize.color);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={prize.id}
|
||||||
|
className="flex items-center gap-3 rounded-lg border border-dark-700/30 bg-dark-800/50 p-2.5 transition-colors hover:bg-dark-800"
|
||||||
|
>
|
||||||
|
{/* Color indicator */}
|
||||||
|
<div className="h-8 w-1 shrink-0 rounded-full" style={{ backgroundColor: color }} />
|
||||||
|
|
||||||
|
{/* Emoji */}
|
||||||
|
<div className="flex h-8 w-8 shrink-0 items-center justify-center text-xl">
|
||||||
|
{prize.emoji}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Prize name */}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="truncate text-sm font-medium text-dark-100">{prize.display_name}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Prize value (if applicable) */}
|
||||||
|
{prize.prize_type !== 'nothing' && prize.value && (
|
||||||
|
<div className="shrink-0 text-xs text-dark-400">
|
||||||
|
{prize.prize_type === 'balance' && `${prize.value} ₽`}
|
||||||
|
{prize.prize_type === 'subscription_days' && `${prize.value}д`}
|
||||||
|
{prize.prize_type === 'promocode' && '🎫'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default WheelLegend;
|
||||||
@@ -678,6 +678,7 @@
|
|||||||
"wheel": {
|
"wheel": {
|
||||||
"title": "Fortune Wheel",
|
"title": "Fortune Wheel",
|
||||||
"disabled": "Fortune Wheel is currently unavailable",
|
"disabled": "Fortune Wheel is currently unavailable",
|
||||||
|
"prizes": "Prizes",
|
||||||
"spinsRemaining": "Spins remaining today",
|
"spinsRemaining": "Spins remaining today",
|
||||||
"choosePayment": "Choose payment method",
|
"choosePayment": "Choose payment method",
|
||||||
"stars": "Stars",
|
"stars": "Stars",
|
||||||
@@ -983,6 +984,8 @@
|
|||||||
"deletePrize": "Delete Prize",
|
"deletePrize": "Delete Prize",
|
||||||
"confirmDelete": "Are you sure you want to delete this prize? This action cannot be undone.",
|
"confirmDelete": "Are you sure you want to delete this prize? This action cannot be undone.",
|
||||||
"dragToReorder": "Drag to reorder",
|
"dragToReorder": "Drag to reorder",
|
||||||
|
"unsavedOrder": "Unsaved order changes",
|
||||||
|
"unsavedOrderHint": "Save or discard your changes",
|
||||||
"types": {
|
"types": {
|
||||||
"subscription_days": "Subscription Days",
|
"subscription_days": "Subscription Days",
|
||||||
"balance_bonus": "Balance Bonus",
|
"balance_bonus": "Balance Bonus",
|
||||||
|
|||||||
@@ -567,6 +567,7 @@
|
|||||||
"wheel": {
|
"wheel": {
|
||||||
"title": "چرخ شانس",
|
"title": "چرخ شانس",
|
||||||
"disabled": "چرخ شانس فعلاً در دسترس نیست",
|
"disabled": "چرخ شانس فعلاً در دسترس نیست",
|
||||||
|
"prizes": "جوایز",
|
||||||
"spinsRemaining": "تعداد چرخش باقیمانده امروز",
|
"spinsRemaining": "تعداد چرخش باقیمانده امروز",
|
||||||
"choosePayment": "روش پرداخت را انتخاب کنید",
|
"choosePayment": "روش پرداخت را انتخاب کنید",
|
||||||
"stars": "Stars",
|
"stars": "Stars",
|
||||||
@@ -704,6 +705,8 @@
|
|||||||
"noPrizes": "جایزهای تنظیم نشده. برای فعالسازی چرخ جایزه اضافه کنید.",
|
"noPrizes": "جایزهای تنظیم نشده. برای فعالسازی چرخ جایزه اضافه کنید.",
|
||||||
"deletePrize": "این جایزه حذف شود؟",
|
"deletePrize": "این جایزه حذف شود؟",
|
||||||
"dragToReorder": "کشیدن برای مرتبسازی",
|
"dragToReorder": "کشیدن برای مرتبسازی",
|
||||||
|
"unsavedOrder": "تغییرات ترتیب ذخیره نشده",
|
||||||
|
"unsavedOrderHint": "تغییرات را ذخیره یا لغو کنید",
|
||||||
"confirmDelete": "آیا از حذف این جایزه مطمئن هستید؟",
|
"confirmDelete": "آیا از حذف این جایزه مطمئن هستید؟",
|
||||||
"types": {
|
"types": {
|
||||||
"subscription_days": "روز اشتراک",
|
"subscription_days": "روز اشتراک",
|
||||||
|
|||||||
@@ -705,6 +705,7 @@
|
|||||||
"wheel": {
|
"wheel": {
|
||||||
"title": "Колесо удачи",
|
"title": "Колесо удачи",
|
||||||
"disabled": "Колесо удачи временно недоступно",
|
"disabled": "Колесо удачи временно недоступно",
|
||||||
|
"prizes": "Призы",
|
||||||
"spinsRemaining": "Осталось вращений сегодня",
|
"spinsRemaining": "Осталось вращений сегодня",
|
||||||
"choosePayment": "Выберите способ оплаты",
|
"choosePayment": "Выберите способ оплаты",
|
||||||
"stars": "Stars",
|
"stars": "Stars",
|
||||||
@@ -1007,6 +1008,8 @@
|
|||||||
"deletePrize": "Удалить приз",
|
"deletePrize": "Удалить приз",
|
||||||
"confirmDelete": "Вы уверены, что хотите удалить этот приз? Это действие нельзя отменить.",
|
"confirmDelete": "Вы уверены, что хотите удалить этот приз? Это действие нельзя отменить.",
|
||||||
"dragToReorder": "Перетащите для изменения порядка",
|
"dragToReorder": "Перетащите для изменения порядка",
|
||||||
|
"unsavedOrder": "Есть несохраненные изменения порядка",
|
||||||
|
"unsavedOrderHint": "Сохраните изменения или отмените их",
|
||||||
"types": {
|
"types": {
|
||||||
"subscription_days": "Дни подписки",
|
"subscription_days": "Дни подписки",
|
||||||
"balance_bonus": "Бонус баланса",
|
"balance_bonus": "Бонус баланса",
|
||||||
|
|||||||
@@ -567,6 +567,7 @@
|
|||||||
"wheel": {
|
"wheel": {
|
||||||
"title": "幸运转盘",
|
"title": "幸运转盘",
|
||||||
"disabled": "幸运转盘暂时不可用",
|
"disabled": "幸运转盘暂时不可用",
|
||||||
|
"prizes": "奖品",
|
||||||
"spinsRemaining": "今日剩余次数",
|
"spinsRemaining": "今日剩余次数",
|
||||||
"choosePayment": "选择支付方式",
|
"choosePayment": "选择支付方式",
|
||||||
"stars": "Stars",
|
"stars": "Stars",
|
||||||
@@ -756,6 +757,8 @@
|
|||||||
"noPrizes": "未配置奖品。添加奖品以启用转盘。",
|
"noPrizes": "未配置奖品。添加奖品以启用转盘。",
|
||||||
"deletePrize": "删除此奖品?",
|
"deletePrize": "删除此奖品?",
|
||||||
"dragToReorder": "拖动排序",
|
"dragToReorder": "拖动排序",
|
||||||
|
"unsavedOrder": "有未保存的顺序更改",
|
||||||
|
"unsavedOrderHint": "保存或放弃更改",
|
||||||
"confirmDelete": "确定要删除这个奖品吗?",
|
"confirmDelete": "确定要删除这个奖品吗?",
|
||||||
"types": {
|
"types": {
|
||||||
"subscription_days": "订阅天数",
|
"subscription_days": "订阅天数",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { wheelApi, type SpinResult, type SpinHistoryItem } from '../api/wheel';
|
import { wheelApi, type SpinResult, type SpinHistoryItem } from '../api/wheel';
|
||||||
import FortuneWheel from '../components/wheel/FortuneWheel';
|
import FortuneWheel from '../components/wheel/FortuneWheel';
|
||||||
|
import WheelLegend from '../components/wheel/WheelLegend';
|
||||||
import InsufficientBalancePrompt from '../components/InsufficientBalancePrompt';
|
import InsufficientBalancePrompt from '../components/InsufficientBalancePrompt';
|
||||||
import { useCurrency } from '../hooks/useCurrency';
|
import { useCurrency } from '../hooks/useCurrency';
|
||||||
import { usePlatform, useHaptic } from '@/platform';
|
import { usePlatform, useHaptic } from '@/platform';
|
||||||
@@ -499,133 +500,145 @@ export default function Wheel() {
|
|||||||
|
|
||||||
{/* Wheel Section */}
|
{/* Wheel Section */}
|
||||||
<Card>
|
<Card>
|
||||||
<div className="p-6 sm:p-8">
|
<div className="grid gap-6 p-6 sm:p-8 lg:grid-cols-[1fr,280px]">
|
||||||
{/* Wheel */}
|
{/* Left: Wheel and Controls */}
|
||||||
<FortuneWheel
|
<div>
|
||||||
prizes={config.prizes}
|
{/* Wheel */}
|
||||||
isSpinning={isSpinning}
|
<FortuneWheel
|
||||||
targetRotation={targetRotation}
|
prizes={config.prizes}
|
||||||
onSpinComplete={handleSpinComplete}
|
isSpinning={isSpinning}
|
||||||
/>
|
targetRotation={targetRotation}
|
||||||
|
onSpinComplete={handleSpinComplete}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Spin Controls */}
|
{/* Spin Controls */}
|
||||||
<div className="mt-8 space-y-4">
|
<div className="mt-8 space-y-4">
|
||||||
{/* Payment type toggle - only if both methods available */}
|
{/* Payment type toggle - only if both methods available */}
|
||||||
{bothMethodsAvailable && (
|
{bothMethodsAvailable && (
|
||||||
<div className="rounded-xl border border-dark-700/30 bg-dark-800/30 p-1">
|
<div className="rounded-xl border border-dark-700/30 bg-dark-800/30 p-1">
|
||||||
<div className="grid grid-cols-2 gap-1">
|
<div className="grid grid-cols-2 gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setPaymentType('telegram_stars')}
|
onClick={() => setPaymentType('telegram_stars')}
|
||||||
disabled={isSpinning}
|
disabled={isSpinning}
|
||||||
className={`flex items-center justify-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium transition-all ${
|
className={`flex items-center justify-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium transition-all ${
|
||||||
paymentType === 'telegram_stars'
|
paymentType === 'telegram_stars'
|
||||||
? 'bg-accent-500/15 text-accent-400'
|
? 'bg-accent-500/15 text-accent-400'
|
||||||
: 'text-dark-400 hover:text-dark-200'
|
: 'text-dark-400 hover:text-dark-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<StarIcon />
|
<StarIcon />
|
||||||
{isTelegramMiniApp
|
{isTelegramMiniApp
|
||||||
? `${config.spin_cost_stars} ⭐`
|
? `${config.spin_cost_stars} ⭐`
|
||||||
: `${formatAmount(config.required_balance_kopeks / 100)} ${currencySymbol}`}
|
: `${formatAmount(config.required_balance_kopeks / 100)} ${currencySymbol}`}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPaymentType('subscription_days')}
|
onClick={() => setPaymentType('subscription_days')}
|
||||||
disabled={isSpinning}
|
disabled={isSpinning}
|
||||||
className={`flex items-center justify-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium transition-all ${
|
className={`flex items-center justify-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium transition-all ${
|
||||||
paymentType === 'subscription_days'
|
paymentType === 'subscription_days'
|
||||||
? 'bg-accent-500/15 text-accent-400'
|
? 'bg-accent-500/15 text-accent-400'
|
||||||
: 'text-dark-400 hover:text-dark-200'
|
: 'text-dark-400 hover:text-dark-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<CalendarIcon />
|
<CalendarIcon />
|
||||||
{t('wheel.days', { count: config.spin_cost_days ?? 0 })}
|
{t('wheel.days', { count: config.spin_cost_days ?? 0 })}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Single Spin Button */}
|
{/* Single Spin Button */}
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="lg"
|
size="lg"
|
||||||
fullWidth
|
fullWidth
|
||||||
onClick={handleUnifiedSpin}
|
onClick={handleUnifiedSpin}
|
||||||
disabled={spinDisabled}
|
disabled={spinDisabled}
|
||||||
loading={isSpinning || isPayingStars}
|
loading={isSpinning || isPayingStars}
|
||||||
>
|
|
||||||
{isSpinning ? t('wheel.spinning') : t('wheel.spin')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Cost subtitle when no toggle */}
|
|
||||||
{costSubtitle && !bothMethodsAvailable && (
|
|
||||||
<p className="text-center text-sm text-dark-400">{costSubtitle}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Cannot spin hint */}
|
|
||||||
{!config.can_spin && !isSpinning && (
|
|
||||||
<>
|
|
||||||
{config.can_spin_reason === 'daily_limit_reached' ? (
|
|
||||||
<div className="rounded-linear border border-dark-700/30 bg-dark-800/30 p-4 text-center">
|
|
||||||
<p className="text-dark-400">{t('wheel.errors.dailyLimitReached')}</p>
|
|
||||||
</div>
|
|
||||||
) : config.can_spin_reason === 'insufficient_balance' ? (
|
|
||||||
<InsufficientBalancePrompt
|
|
||||||
missingAmountKopeks={
|
|
||||||
config.required_balance_kopeks - config.user_balance_kopeks
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="rounded-linear border border-dark-700/30 bg-dark-800/30 p-4 text-center">
|
|
||||||
<p className="text-dark-400">{t('wheel.errors.cannotSpin')}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Inline Result Card */}
|
|
||||||
{spinResult && !isSpinning && (
|
|
||||||
<div
|
|
||||||
className={`animate-fade-in rounded-linear border p-4 ${
|
|
||||||
spinResult.success
|
|
||||||
? 'border-accent-500/30 bg-accent-500/10'
|
|
||||||
: 'border-red-500/30 bg-red-500/10'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
{isSpinning ? t('wheel.spinning') : t('wheel.spin')}
|
||||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-linear bg-dark-700/50 text-2xl">
|
</Button>
|
||||||
{spinResult.success ? spinResult.emoji || '🎉' : '😔'}
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="font-semibold text-dark-100">
|
|
||||||
{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')}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-dark-400">{spinResult.message}</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={closeResultModal}
|
|
||||||
className="shrink-0 rounded-lg p-2 text-dark-400 transition-colors hover:bg-white/5 hover:text-dark-200"
|
|
||||||
>
|
|
||||||
<CloseIcon />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Promocode if won */}
|
{/* Cost subtitle when no toggle */}
|
||||||
{spinResult.promocode && (
|
{costSubtitle && !bothMethodsAvailable && (
|
||||||
<div className="mt-3 rounded-linear border border-accent-500/20 bg-accent-500/10 p-3 text-center">
|
<p className="text-center text-sm text-dark-400">{costSubtitle}</p>
|
||||||
<p className="mb-1 text-xs text-accent-400">{t('wheel.yourPromoCode')}</p>
|
)}
|
||||||
<p className="select-all font-mono text-lg font-bold tracking-wider text-white">
|
|
||||||
{spinResult.promocode}
|
{/* Cannot spin hint */}
|
||||||
</p>
|
{!config.can_spin && !isSpinning && (
|
||||||
|
<>
|
||||||
|
{config.can_spin_reason === 'daily_limit_reached' ? (
|
||||||
|
<div className="rounded-linear border border-dark-700/30 bg-dark-800/30 p-4 text-center">
|
||||||
|
<p className="text-dark-400">{t('wheel.errors.dailyLimitReached')}</p>
|
||||||
|
</div>
|
||||||
|
) : config.can_spin_reason === 'insufficient_balance' ? (
|
||||||
|
<InsufficientBalancePrompt
|
||||||
|
missingAmountKopeks={
|
||||||
|
config.required_balance_kopeks - config.user_balance_kopeks
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-linear border border-dark-700/30 bg-dark-800/30 p-4 text-center">
|
||||||
|
<p className="text-dark-400">{t('wheel.errors.cannotSpin')}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Inline Result Card */}
|
||||||
|
{spinResult && !isSpinning && (
|
||||||
|
<div
|
||||||
|
className={`animate-fade-in rounded-linear border p-4 ${
|
||||||
|
spinResult.success
|
||||||
|
? 'border-accent-500/30 bg-accent-500/10'
|
||||||
|
: 'border-red-500/30 bg-red-500/10'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-linear bg-dark-700/50 text-2xl">
|
||||||
|
{spinResult.success ? spinResult.emoji || '🎉' : '😔'}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="font-semibold text-dark-100">
|
||||||
|
{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')}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-dark-400">{spinResult.message}</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={closeResultModal}
|
||||||
|
className="shrink-0 rounded-lg p-2 text-dark-400 transition-colors hover:bg-white/5 hover:text-dark-200"
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
{/* Promocode if won */}
|
||||||
)}
|
{spinResult.promocode && (
|
||||||
|
<div className="mt-3 rounded-linear border border-accent-500/20 bg-accent-500/10 p-3 text-center">
|
||||||
|
<p className="mb-1 text-xs text-accent-400">{t('wheel.yourPromoCode')}</p>
|
||||||
|
<p className="select-all font-mono text-lg font-bold tracking-wider text-white">
|
||||||
|
{spinResult.promocode}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* End of left column: wheel and controls */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right column (desktop) / Bottom (mobile): Prize Legend */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<h3 className="mb-3 text-sm font-semibold text-dark-300">
|
||||||
|
{t('wheel.prizes') || 'Призы'}
|
||||||
|
</h3>
|
||||||
|
<WheelLegend prizes={config.prizes} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user