diff --git a/openhouse_frontend/src/components/game-specific/roulette/BettingCell.tsx b/openhouse_frontend/src/components/game-specific/roulette/BettingCell.tsx index 65ca2921..22712e27 100644 --- a/openhouse_frontend/src/components/game-specific/roulette/BettingCell.tsx +++ b/openhouse_frontend/src/components/game-specific/roulette/BettingCell.tsx @@ -1,6 +1,64 @@ -import React, { useState, useCallback, useRef, useEffect } from 'react'; +import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; +// Helper function to get gradient background style based on color +const getBackgroundStyle = (bgColor: string, isWinner: boolean): React.CSSProperties => { + if (isWinner) { + return { background: 'linear-gradient(180deg, #fbbf24 0%, #f59e0b 50%, #d97706 100%)' }; + } + if (bgColor.includes('red')) { + return { background: 'linear-gradient(180deg, #dc2626 0%, #b91c1c 50%, #991b1b 100%)' }; + } + if (bgColor.includes('green')) { + return { background: 'linear-gradient(180deg, #16a34a 0%, #15803d 50%, #166534 100%)' }; + } + // Black/zinc + return { background: 'linear-gradient(180deg, #3f3f46 0%, #27272a 50%, #18181b 100%)' }; +}; + +// Enhanced chip badge component with casino chip styling +interface ChipBadgeProps { + amount: number; + isNew: boolean; + isMobile: boolean; + style?: React.CSSProperties; +} + +const ChipBadge: React.FC = ({ amount, isNew, isMobile, style }) => { + if (amount <= 0) return null; + + return ( + + + ${amount.toFixed(2)} + + + ); +}; + interface BettingCellProps { /** Display text for the cell (number or label) */ label: string; @@ -48,39 +106,6 @@ const Ripple: React.FC = ({ x, y, color, onComplete }) => ( /> ); -// Chip drop animation component -interface ChipDropProps { - isNew: boolean; - amount: number; - isMobile: boolean; -} - -const ChipDrop: React.FC = ({ isNew, amount, isMobile }) => { - if (amount <= 0) return null; - - return ( - - - ${amount.toFixed(2)} - - - ); -}; export const BettingCell: React.FC = ({ label, @@ -149,8 +174,8 @@ export const BettingCell: React.FC = ({ return ( = ({ }} whileTap={disabled ? {} : { scale: 0.95 }} transition={{ type: 'spring', stiffness: 400, damping: 25 }} - style={isMobile ? { fontSize: '14px' } : { fontSize: '18px' }} + style={{ + fontSize: isMobile ? '14px' : '18px', + ...getBackgroundStyle(bgColor, isWinner), + }} > {/* Winner pulse overlay */} @@ -195,7 +223,7 @@ export const BettingCell: React.FC = ({ {/* Chip amount badge */} - + ); }; @@ -255,13 +283,14 @@ export const ZeroCell: React.FC = ({ return ( = ({ {/* Chip amount badge */} - {amount > 0 && ( - - ${amount.toFixed(2)} - - )} + ); }; @@ -394,21 +409,51 @@ export const OutsideBetCell: React.FC = ({ setRipples(prev => prev.filter(r => r.id !== id)); }, []); + // Memoize outside bet styles to prevent regenerating CSS strings on every render + const outsideBetStyle = useMemo((): React.CSSProperties => { + if (isWinner) { + return { + background: 'linear-gradient(180deg, #fbbf24 0%, #f59e0b 50%, #d97706 100%)' + }; + } + if (bgColor.includes('red')) { + return { + background: `linear-gradient(180deg, rgba(220,38,38,0.95) 0%, rgba(185,28,28,0.95) 50%, rgba(153,27,27,0.95) 100%), repeating-linear-gradient(45deg, transparent, transparent 4px, rgba(255,255,255,0.03) 4px, rgba(255,255,255,0.03) 8px)`, + backgroundBlendMode: 'normal, overlay', + }; + } + // Default zinc/dark with diagonal stripe texture + return { + background: `linear-gradient(180deg, rgba(63,63,70,0.95) 0%, rgba(39,39,42,0.95) 50%, rgba(24,24,27,0.95) 100%), repeating-linear-gradient(45deg, transparent, transparent 4px, rgba(255,255,255,0.03) 4px, rgba(255,255,255,0.03) 8px)`, + backgroundBlendMode: 'normal, overlay', + }; + }, [isWinner, bgColor]); + + // Determine border color based on bet type + const borderClass = bgColor.includes('red') + ? 'border-red-800/60' + : 'border-zinc-500/50'; + return ( {/* Winner pulse overlay */} @@ -433,9 +478,13 @@ export const OutsideBetCell: React.FC = ({ /> ))} - {/* Label */} + {/* Label with enhanced text styling */} @@ -443,24 +492,7 @@ export const OutsideBetCell: React.FC = ({ {/* Chip amount badge */} - {amount > 0 && ( - - ${amount.toFixed(2)} - - )} + ); }; diff --git a/openhouse_frontend/src/components/game-specific/roulette/RouletteWheel.tsx b/openhouse_frontend/src/components/game-specific/roulette/RouletteWheel.tsx index 3123e4db..e2d3f2aa 100644 --- a/openhouse_frontend/src/components/game-specific/roulette/RouletteWheel.tsx +++ b/openhouse_frontend/src/components/game-specific/roulette/RouletteWheel.tsx @@ -8,14 +8,9 @@ import { DIMENSIONS as D, COLORS, isRed, + getPocketColor, } from './rouletteConstants'; -// Get gradient ID for pocket based on number -const getPocketGradient = (num: number): string => { - if (num === 0) return 'url(#pocketGreenGradient)'; - return isRed(num) ? 'url(#pocketRedGradient)' : 'url(#pocketBlackGradient)'; -}; - interface RouletteWheelProps { winningNumber: number | null; isWaitingForResult: boolean; @@ -63,7 +58,9 @@ export const RouletteWheel: React.FC = ({ WHEEL_NUMBERS.map((num, i) => ({ num, path: createSegmentPath(i, D.POCKET_INNER, D.POCKET_OUTER), - gradient: getPocketGradient(num), + // Inner shadow overlay path - thinner strip at inner edge for depth + shadowPath: createSegmentPath(i, D.POCKET_INNER, D.POCKET_INNER + 12), + color: getPocketColor(num), textAngle: i * SEGMENT_ANGLE + SEGMENT_ANGLE / 2, textRadius: (D.POCKET_INNER + D.POCKET_OUTER) / 2, })), @@ -133,7 +130,7 @@ export const RouletteWheel: React.FC = ({
{/* Enhanced metallic gold rim gradient */} @@ -153,28 +150,6 @@ export const RouletteWheel: React.FC = ({ - {/* Enhanced pocket gradients with 3D depth */} - - - - - - - - - - - - - - - - - - - - - {/* Inner cone with wooden texture feel */} @@ -270,9 +245,9 @@ export const RouletteWheel: React.FC = ({ - {/* Text shadow for legibility */} - - + {/* Text shadow for legibility - expanded filter region to prevent clipping */} + + {/* Wheel surface shine overlay */} @@ -290,6 +265,28 @@ export const RouletteWheel: React.FC = ({ + {/* Pocket inner shadow for depth - radial gradient fading inward */} + + + + + + + {/* Rim bevel highlight */} + + + + + + + + {/* Inner rim shadow for depth */} + + + + + + {/* Motion blur filter for ball trail */} @@ -325,14 +322,32 @@ export const RouletteWheel: React.FC = ({ stroke="none" /> - {/* Inner rim edge */} + {/* Rim bevel - outer highlight arc (top-left light source) */} + + {/* Inner rim edge - enhanced */} + + {/* Ball track groove - deeper shadow */} @@ -355,8 +370,8 @@ export const RouletteWheel: React.FC = ({ strokeWidth="1" /> - {/* Pocket segments with gradient fills */} - {segments.map(({ num, path, gradient }) => { + {/* Pocket segments with solid color fills */} + {segments.map(({ num, path, shadowPath, color }) => { const isWinner = showResult && num === winningNumber; return ( @@ -372,49 +387,102 @@ export const RouletteWheel: React.FC = ({ + {/* Inner shadow overlay for pocket depth */} + {!isWinner && ( + + )} ); })} - {/* Frets (pocket dividers) */} + {/* Frets (pocket dividers) with 3D effect */} {frets.map((f, i) => ( - + + {/* Shadow line - offset to create depth */} + + {/* Main fret */} + + {/* Highlight line - subtle gold sheen */} + + ))} - {/* Numbers with shadow for legibility */} + {/* Numbers with shadow for legibility - using duplicate text for shadow instead of SVG filter */} {segments.map(({ num, textAngle, textRadius }) => { const [tx, ty] = polarToCartesian(textAngle, textRadius); const isWinner = showResult && num === winningNumber; return ( - - {num} - + + {/* Shadow text - rendered behind main text for depth */} + {!isWinner && ( + <> + + {num} + + + )} + {/* Main number text */} + + {num} + + ); })} diff --git a/openhouse_frontend/src/pages/roulette/RouletteGame.tsx b/openhouse_frontend/src/pages/roulette/RouletteGame.tsx index 96a4139e..ce5564b0 100644 --- a/openhouse_frontend/src/pages/roulette/RouletteGame.tsx +++ b/openhouse_frontend/src/pages/roulette/RouletteGame.tsx @@ -46,17 +46,23 @@ function RecentResults({ results, maxDisplay = 12 }: RecentResultsProps) { return RED_NUMBERS.has(num) ? 'bg-red-600' : 'bg-zinc-800'; }; - const getBorderColor = (num: number) => { - if (num === 0) return 'ring-green-400'; - return RED_NUMBERS.has(num) ? 'ring-red-400' : 'ring-zinc-500'; + const getBorderColor = (num: number, isFirst: boolean) => { + if (num === 0) return isFirst ? 'border-green-400' : 'border-green-500/60'; + if (RED_NUMBERS.has(num)) return isFirst ? 'border-red-400' : 'border-red-500/60'; + return isFirst ? 'border-zinc-400' : 'border-zinc-500/60'; + }; + + const getGlowColor = (num: number) => { + if (num === 0) return '#22c55e'; + return RED_NUMBERS.has(num) ? '#ef4444' : '#71717a'; }; const displayResults = results.slice(0, maxDisplay); return ( -
- HISTORY -
+
+ History +
{displayResults.map((num, index) => ( {num} @@ -84,7 +94,7 @@ function RecentResults({ results, maxDisplay = 12 }: RecentResultsProps) { ))}
{results.length > maxDisplay && ( - +{results.length - maxDisplay} + +{results.length - maxDisplay} )}
); @@ -255,9 +265,16 @@ export function RouletteGame() { const handleAnimationComplete = useCallback(() => { setAnimationState('showing_result'); - // Add to recent results + // Add to recent results - use functional update with duplicate check if (winningNumber !== null) { - setRecentResults(prev => [winningNumber, ...prev].slice(0, 15)); + setRecentResults(prev => { + // Prevent duplicate if the last entry is the same number + // This can happen if the callback fires twice + if (prev[0] === winningNumber) { + return prev; // Already added, skip + } + return [winningNumber, ...prev].slice(0, 15); + }); } setTimeout(() => { setAnimationState('resetting'); @@ -327,21 +344,14 @@ export function RouletteGame() { } }; - // Get winning number display color - const getNumberColor = (num: number | null) => { - if (num === null) return 'bg-zinc-700'; - if (num === 0) return 'bg-green-600'; - return RED_NUMBERS.has(num) ? 'bg-red-600' : 'bg-zinc-900'; - }; - return ( {/* ===== MOBILE LAYOUT ===== */} -
- {/* Wheel section */} -
+
+ {/* Wheel section - COMPACT with flex-none */} +
{/* Recent Results History */} - + - {/* Result or stats */} -
+ {/* Compact stats - single line */} +
{lastResult && showResults ? ( {Number(lastResult.net_result) > 0 ? ( - <> - - WIN - - - +{formatUSDT(lastResult.total_payout)} - - + WIN +{formatUSDT(lastResult.total_payout)} ) : ( - <> - - LOSE - - - -{formatUSDT(lastResult.total_bet)} - - + LOSE -{formatUSDT(lastResult.total_bet)} )} ) : ( -
-
- {bets.length} bets - ${totalBetAmount.toFixed(2)} - {(isWaitingForResult || isLanding) && ( - - {isWaitingForResult ? 'Spinning...' : 'Landing...'} - - )} -
- {exceedsHouseLimit && ( - - Max payout ${maxPayout.toFixed(2)} exceeds limit ${maxAllowedPayout.toFixed(2)} +
+ {bets.length} bets + ${totalBetAmount.toFixed(2)} + {(isWaitingForResult || isLanding) && ( + + {isWaitingForResult ? 'Spinning...' : 'Landing...'} )}
)} + {exceedsHouseLimit && ( + + Max ${maxPayout.toFixed(2)} > limit ${maxAllowedPayout.toFixed(2)} + + )} + {error && {error}}
- {error &&

{error}

}
- {/* Betting table */} + {/* Visual separator - simplified */} +
+ + {/* Betting table - takes remaining space, scrollable */} {/* Tab buttons */} -
+
+ {/* Payout legend */} +
+ Straight 35:1 + Dozen/Col 2:1 + Even Bets 1:1 +
+ {/* Number grid */}
{/* Zero - only on low tab */} @@ -584,7 +580,11 @@ export function RouletteGame() { @@ -592,9 +592,18 @@ export function RouletteGame() { {error}

}
+ {/* Visual separator */} +
+
+
+ {/* Betting table */} {/* Tab buttons */} -
+
+ {/* Payout legend */} +
+ Straight: 35:1 + Dozen/Col: 2:1 + Even Bets: 1:1 +
+ {/* Number grid */}
{/* Zero - only on low tab */} @@ -874,7 +907,11 @@ export function RouletteGame() { @@ -882,9 +919,18 @@ export function RouletteGame() {