feat: enhance Card, CasinoButton, Chip, DealerArea, and TableScreen components; add animation, size adjustments, and improved UI elements for better user experience

This commit is contained in:
2025-12-21 02:54:10 +01:00
parent 038d4f4caa
commit 0c14bab299
5 changed files with 748 additions and 181 deletions

View File

@@ -1,4 +1,5 @@
import { StyleSheet, Text, View } from 'react-native';
import { useEffect, useRef } from 'react';
import { Animated, StyleSheet, Text, View } from 'react-native';
import { colors, fonts } from '../theme';
const suitSymbols = {
@@ -15,12 +16,70 @@ const suitColors = {
D: colors.red
};
export default function Card({ rank, suit, hidden }) {
export default function Card({ rank, suit, hidden, size = 'normal', animate = true, delay = 0 }) {
const scaleMap = { tiny: 0.78, small: 0.92, normal: 1.06, large: 1.18 };
const scale = scaleMap[size] ?? 1;
const appear = useRef(new Animated.Value(animate ? 0 : 1)).current;
useEffect(() => {
if (!animate) {
return;
}
Animated.timing(appear, {
toValue: 1,
duration: 260,
delay,
useNativeDriver: true
}).start();
}, [animate, appear, delay]);
const animatedStyle = animate
? {
opacity: appear,
transform: [
{
translateY: appear.interpolate({
inputRange: [0, 1],
outputRange: [-12 * scale, 0]
})
},
{
scale: appear.interpolate({
inputRange: [0, 1],
outputRange: [0.96, 1]
})
}
]
}
: null;
const cardStyle = [
styles.card,
{
width: 54 * scale,
height: 78 * scale,
borderRadius: 8 * scale,
padding: 6 * scale,
marginRight: 6 * scale
}
];
const cornerStyle = [styles.corner, { fontSize: 12 * scale, lineHeight: 13 * scale }];
const centerStyle = [styles.center, { fontSize: 24 * scale, lineHeight: 26 * scale }];
const backPatternStyle = [
styles.backPattern,
{
width: 32 * scale,
height: 46 * scale,
borderRadius: 6 * scale,
borderWidth: Math.max(1, Math.round(2 * scale))
}
];
if (hidden || rank === 'X') {
return (
<View style={[styles.card, styles.cardBack]}>
<View style={styles.backPattern} />
</View>
<Animated.View style={[cardStyle, styles.cardBack, animatedStyle]}>
<View style={backPatternStyle} />
</Animated.View>
);
}
@@ -28,29 +87,29 @@ export default function Card({ rank, suit, hidden }) {
const color = suitColors[suit] || colors.text;
return (
<View style={styles.card}>
<Text style={[styles.corner, { color }]}>{rank}</Text>
<Text style={[styles.corner, { color }]}>{symbol}</Text>
<Text style={[styles.center, { color }]}>{symbol}</Text>
<Animated.View style={[cardStyle, animatedStyle]}>
<Text style={[cornerStyle, { color }]}>{rank}</Text>
<Text style={[cornerStyle, { color }]}>{symbol}</Text>
<Text style={[centerStyle, { color }]}>{symbol}</Text>
<View style={styles.cornerBottom}>
<Text style={[styles.corner, { color }]}>{rank}</Text>
<Text style={[styles.corner, { color }]}>{symbol}</Text>
</View>
<Text style={[cornerStyle, { color }]}>{rank}</Text>
<Text style={[cornerStyle, { color }]}>{symbol}</Text>
</View>
</Animated.View>
);
}
const styles = StyleSheet.create({
card: {
width: 54,
height: 78,
borderRadius: 8,
backgroundColor: '#fdf8f0',
padding: 6,
marginRight: 6,
borderWidth: 1,
borderColor: '#d2c1a4',
justifyContent: 'space-between'
justifyContent: 'space-between',
overflow: 'hidden',
shadowColor: '#000',
shadowOpacity: 0.18,
shadowRadius: 6,
shadowOffset: { width: 0, height: 3 }
},
cardBack: {
backgroundColor: '#152d52',
@@ -59,21 +118,17 @@ const styles = StyleSheet.create({
justifyContent: 'center'
},
backPattern: {
width: 32,
height: 46,
borderRadius: 6,
borderWidth: 2,
borderColor: 'rgba(255,255,255,0.4)'
},
corner: {
fontSize: 12,
fontFamily: fonts.body,
fontWeight: '700'
fontWeight: '700',
includeFontPadding: false
},
center: {
fontSize: 24,
fontFamily: fonts.display,
textAlign: 'center'
textAlign: 'center',
includeFontPadding: false
},
cornerBottom: {
transform: [{ rotate: '180deg' }]

View File

@@ -42,8 +42,8 @@ const styles = StyleSheet.create({
borderColor: 'rgba(255,255,255,0.2)'
},
buttonSmall: {
paddingVertical: 8,
paddingHorizontal: 16
paddingVertical: 9,
paddingHorizontal: 18
},
inner: {
alignItems: 'center'
@@ -56,7 +56,7 @@ const styles = StyleSheet.create({
},
textSmall: {
fontSize: 13,
letterSpacing: 0.8
letterSpacing: 0.7
},
disabled: {
opacity: 0.5

View File

@@ -11,7 +11,7 @@ export default function Chip({ label, color = 'blue', size = 'default' }) {
const isSmall = size === 'small';
return (
<View style={[styles.chip, isSmall && styles.chipSmall, { backgroundColor: chipColors[color] || colors.chipBlue }]}>
<View style={styles.inner}>
<View style={[styles.inner, isSmall && styles.innerSmall]}>
<Text style={[styles.text, isSmall && styles.textSmall]}>{label}</Text>
</View>
</View>
@@ -20,9 +20,9 @@ export default function Chip({ label, color = 'blue', size = 'default' }) {
const styles = StyleSheet.create({
chip: {
width: 48,
height: 48,
borderRadius: 24,
width: 56,
height: 56,
borderRadius: 28,
alignItems: 'center',
justifyContent: 'center',
borderWidth: 3,
@@ -34,21 +34,27 @@ const styles = StyleSheet.create({
borderRadius: 20
},
inner: {
width: 28,
height: 28,
borderRadius: 14,
width: 32,
height: 32,
borderRadius: 16,
borderWidth: 2,
borderColor: 'rgba(255,255,255,0.7)',
alignItems: 'center',
justifyContent: 'center'
},
innerSmall: {
width: 22,
height: 22,
borderRadius: 11,
borderWidth: 1.5
},
text: {
color: '#f7f2e6',
fontWeight: '700',
fontSize: 12,
fontSize: 13,
fontFamily: fonts.mono
},
textSmall: {
fontSize: 11
fontSize: 10
}
});

View File

@@ -2,13 +2,20 @@ import { StyleSheet, Text, View } from 'react-native';
import Card from './Card';
import { colors, fonts } from '../theme';
export default function DealerArea({ hand }) {
export default function DealerArea({ hand, cardSize = 'small', delayBase = 0 }) {
return (
<View style={styles.wrapper}>
<Text style={styles.label}>Osztó</Text>
<View style={styles.hand}>
{hand.map((card, idx) => (
<Card key={`${card.rank}-${card.suit}-${idx}`} rank={card.rank} suit={card.suit} hidden={card.hidden} />
<Card
key={`${card.rank}-${card.suit}-${idx}`}
rank={card.rank}
suit={card.suit}
hidden={card.hidden}
size={cardSize}
delay={delayBase + idx * 120}
/>
))}
</View>
</View>
@@ -23,7 +30,7 @@ const styles = StyleSheet.create({
color: colors.goldBright,
letterSpacing: 2,
textTransform: 'uppercase',
fontSize: 12,
fontSize: 13,
fontFamily: fonts.body
},
hand: {

View File

@@ -5,12 +5,46 @@ import { BlurView } from 'expo-blur';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { WS_URL } from '../api';
import { colors, fonts } from '../theme';
import Card from '../components/Card';
import CasinoButton from '../components/CasinoButton';
import Chip from '../components/Chip';
import DealerArea from '../components/DealerArea';
import Seat from '../components/Seat';
import TableBackground from '../components/TableBackground';
import TableMarkings from '../components/TableMarkings';
const emptySeat = (index) => ({
index,
userId: null,
username: '',
bet: 0,
hand: [],
result: null,
isYou: false
});
const getHandTotal = (hand) => {
if (!hand || hand.length === 0) {
return null;
}
let total = 0;
let aces = 0;
for (const card of hand) {
if (!card || card.rank === 'X') {
return null;
}
if (card.rank === 'A') {
total += 11;
aces += 1;
} else if (['J', 'Q', 'K'].includes(card.rank)) {
total += 10;
} else {
total += Number(card.rank);
}
}
while (total > 21 && aces > 0) {
total -= 10;
aces -= 1;
}
return total;
};
export default function TableScreen({ token, tableId, user, onLeave }) {
const [table, setTable] = useState(null);
@@ -18,9 +52,14 @@ export default function TableScreen({ token, tableId, user, onLeave }) {
const [message, setMessage] = useState('');
const [betAmount, setBetAmount] = useState(10);
const [turnSeconds, setTurnSeconds] = useState(null);
const [roundSeconds, setRoundSeconds] = useState(null);
const [controlsVisible, setControlsVisible] = useState(true);
const [seatLayout, setSeatLayout] = useState(null);
const [winOverlay, setWinOverlay] = useState(null);
const [confettiKey, setConfettiKey] = useState(0);
const wsRef = useRef(null);
const hideTimerRef = useRef(null);
const lastWinRoundRef = useRef(null);
const pulse = useRef(new Animated.Value(0)).current;
const insets = useSafeAreaInsets();
const { width, height } = useWindowDimensions();
@@ -70,6 +109,8 @@ export default function TableScreen({ token, tableId, user, onLeave }) {
const mySeat = useMemo(() => table?.seats?.find((seat) => seat.isYou), [table]);
const isMyTurn = table?.phase === 'playing' && table?.currentSeatIndex === mySeat?.index;
const bettingLocked = ['playing', 'dealer', 'payout'].includes(table?.phase);
const showBetControls = !bettingLocked;
const showActionControls = table?.phase === 'playing';
const scheduleHide = useCallback(() => {
if (hideTimerRef.current) {
@@ -113,6 +154,22 @@ export default function TableScreen({ token, tableId, user, onLeave }) {
return () => clearInterval(interval);
}, [table?.turnEndsAt, isMyTurn]);
useEffect(() => {
if (!table?.roundStartsAt || table?.phase !== 'betting') {
setRoundSeconds(null);
return;
}
const update = () => {
const msLeft = Math.max(0, table.roundStartsAt - Date.now());
setRoundSeconds(Math.ceil(msLeft / 1000));
};
update();
const interval = setInterval(update, 250);
return () => clearInterval(interval);
}, [table?.roundStartsAt, table?.phase]);
useEffect(() => {
if (!isMyTurn) {
pulse.stopAnimation();
@@ -128,6 +185,24 @@ export default function TableScreen({ token, tableId, user, onLeave }) {
).start();
}, [isMyTurn, pulse]);
useEffect(() => {
if (table?.phase !== 'payout' || !mySeat?.result) {
return;
}
if (lastWinRoundRef.current === table.roundId) {
return;
}
if (['win', 'blackjack'].includes(mySeat.result.outcome)) {
lastWinRoundRef.current = table.roundId;
setWinOverlay({ payout: mySeat.result.payout, outcome: mySeat.result.outcome });
setConfettiKey((prev) => prev + 1);
const timer = setTimeout(() => setWinOverlay(null), 2600);
return () => clearTimeout(timer);
}
}, [table?.phase, table?.roundId, mySeat?.result]);
const pulseStyle = {
transform: [
{
@@ -159,29 +234,65 @@ export default function TableScreen({ token, tableId, user, onLeave }) {
};
const seats = table?.seats || [];
const activePlayers = seats.filter((seat) => seat.userId).length;
const showRoundTimer = roundSeconds !== null && table?.phase === 'betting' && activePlayers > 1;
const uiScale = useMemo(() => {
const base = isPortrait ? Math.min(width / 390, height / 844) : Math.min(width / 844, height / 390);
return Math.max(0.88, Math.min(base, 1.15));
}, [width, height, isPortrait]);
const seatMetrics = useMemo(() => {
if (!seatLayout) {
return {
width: Math.round(150 * uiScale),
height: Math.round(110 * uiScale)
};
}
const baseWidth = seatLayout.width * (isPortrait ? 0.24 : 0.18);
const clamped = Math.max(120, Math.min(baseWidth, 190));
const widthValue = Math.round(clamped * uiScale);
return {
width: widthValue,
height: Math.round(widthValue * 0.78)
};
}, [seatLayout, isPortrait, uiScale]);
const seatAnchors = useMemo(() => {
if (isPortrait) {
return (
<LinearGradient
colors={[colors.backgroundTop, colors.backgroundBottom]}
style={[
styles.container,
{
paddingTop: insets.top + 12,
paddingBottom: insets.bottom + 12
return [
{ x: 0.12, y: 0.32 },
{ x: 0.88, y: 0.32 },
{ x: 0.12, y: 0.58 },
{ x: 0.88, y: 0.58 }
];
}
]}
>
<View style={styles.rotateWrap}>
<Text style={styles.rotateTitle}>Fordítsd el a telefont</Text>
<Text style={styles.rotateSubtitle}>A blackjack asztal fekvő nézetben működik jól.</Text>
<View style={styles.rotateActions}>
<CasinoButton label="Vissza" onPress={onLeave} variant="red" />
</View>
</View>
</LinearGradient>
);
return [
{ x: 0.2, y: 0.26 },
{ x: 0.8, y: 0.26 },
{ x: 0.2, y: 0.64 },
{ x: 0.8, y: 0.64 }
];
}, [isPortrait]);
const seatSlots = useMemo(() => {
return seatAnchors.map((_, idx) => seats[idx] ?? emptySeat(idx));
}, [seatAnchors, seats]);
const seatPositions = useMemo(() => {
if (!seatLayout) {
return [];
}
return seatAnchors.map((anchor) => ({
left: seatLayout.width * anchor.x - seatMetrics.width / 2,
top: seatLayout.height * anchor.y - seatMetrics.height / 2
}));
}, [seatLayout, seatAnchors, seatMetrics]);
const seatCardSize = uiScale > 1.02 ? 'normal' : 'small';
const playerCardSize = uiScale > 1 ? 'large' : 'normal';
const myTotal = getHandTotal(mySeat?.hand);
const dealerDelayBase = seatSlots.length * 140;
return (
<LinearGradient
@@ -189,47 +300,111 @@ export default function TableScreen({ token, tableId, user, onLeave }) {
style={[
styles.container,
{
paddingTop: insets.top + 12,
paddingBottom: insets.bottom + 12
paddingTop: insets.top + 10,
paddingBottom: insets.bottom + 10
}
]}
>
<View style={styles.topBar}>
<View>
<Text style={styles.tableTitle}>Asztal {tableId}</Text>
<Text style={styles.balance}>Egyenleg: {balance} Ft</Text>
<Text style={styles.balance}>{balance} Ft</Text>
</View>
<CasinoButton label="Kilépek" onPress={onLeave} variant="red" />
<CasinoButton label="Kilép" onPress={onLeave} variant="red" size="small" />
</View>
<View style={styles.tableWrap}>
<TableBackground style={styles.tableSurface} showRing={false}>
<View style={styles.tableInner}>
<TableMarkings />
<View style={styles.tableContent}>
<View style={styles.tableSurface} onTouchStart={showControls}>
<LinearGradient colors={[colors.tableFeltDark, colors.tableFelt]} style={styles.tableGradient}>
<View style={styles.tableGlow} />
<View style={[styles.tableArc, styles.tableArcTop, { top: 20 * uiScale, height: 200 * uiScale }]} />
<View style={[styles.tableArc, styles.tableArcMid, { top: 140 * uiScale, height: 240 * uiScale }]} />
<View style={[styles.tableArc, styles.tableArcBottom, { bottom: 220 * uiScale, height: 260 * uiScale }]} />
<View style={[styles.tableContent, { paddingTop: 6 * uiScale, paddingBottom: 10 * uiScale }]}
>
<View style={styles.dealerArea}>
<DealerArea hand={table?.dealerHand || []} />
<DealerArea hand={table?.dealerHand || []} cardSize={seatCardSize} delayBase={dealerDelayBase} />
</View>
<View style={styles.seatLayer}>
{seats.map((seat) => (
<View key={seat.index} style={[styles.seatSpot, seatPositions[seat.index]]}>
<Seat seat={seat} highlight={table.currentSeatIndex === seat.index} />
<View style={styles.seatLayer} onLayout={(event) => setSeatLayout(event.nativeEvent.layout)}>
{seatSlots.map((seat, idx) => {
const pos = seatPositions[idx];
if (!pos) {
return null;
}
const highlight = seat.userId && table?.currentSeatIndex === seat.index;
return (
<View
key={seat.index}
style={[
styles.seatSpot,
{
width: seatMetrics.width,
minHeight: seatMetrics.height
},
pos
]}
>
<PortraitSeat
seat={seat}
highlight={highlight}
cardSize={seatCardSize}
scale={uiScale}
dealIndex={idx}
/>
</View>
);
})}
</View>
<View style={styles.playerArea}>
<View style={styles.playerHand}>
{(mySeat?.hand || []).map((card, idx) => (
<View key={`${card.rank}-${card.suit}-${idx}`} style={[styles.playerCard, { marginLeft: idx === 0 ? 0 : -12 * uiScale }]}
>
<Card
rank={card.rank}
suit={card.suit}
hidden={card.hidden}
size={playerCardSize}
delay={(mySeat?.index ?? 0) * 140 + idx * 120}
/>
</View>
))}
</View>
{myTotal !== null ? (
<Text style={styles.playerTotal}>Összeg: {myTotal}</Text>
) : null}
{mySeat ? (
<Text style={styles.playerBet}>Tét: {mySeat.bet || betAmount} Ft</Text>
) : null}
</View>
{controlsVisible ? (
<View style={styles.controlsOverlay}>
<BlurView intensity={28} tint="dark" style={styles.blurPanel}>
<View style={styles.controlsArea}>
{showRoundTimer ? (
<Text style={styles.roundTimer}>Kezdés: {roundSeconds} mp</Text>
) : null}
{showBetControls ? (
<BlurView intensity={22} tint="dark" style={styles.controlsPanel}>
<View style={styles.betRow}>
<Text style={styles.sectionLabel}>Tét</Text>
<View style={styles.betControls}>
<Pressable onPress={() => adjustBet(-10)} style={[styles.betAdjust, bettingLocked && styles.betAdjustDisabled]} disabled={bettingLocked}>
<Pressable
onPress={() => adjustBet(-10)}
style={[styles.betAdjust, bettingLocked && styles.betAdjustDisabled]}
disabled={bettingLocked}
>
<Text style={styles.betAdjustText}>-</Text>
</Pressable>
<Chip label={`${betAmount}`} color="red" size="small" />
<Pressable onPress={() => adjustBet(10)} style={[styles.betAdjust, bettingLocked && styles.betAdjustDisabled]} disabled={bettingLocked}>
<Chip label={`${betAmount}`} color="red" />
<Pressable
onPress={() => adjustBet(10)}
style={[styles.betAdjust, bettingLocked && styles.betAdjustDisabled]}
disabled={bettingLocked}
>
<Text style={styles.betAdjustText}>+</Text>
</Pressable>
</View>
@@ -237,17 +412,23 @@ export default function TableScreen({ token, tableId, user, onLeave }) {
<CasinoButton label="Kész" onPress={() => send({ type: 'ready' })} variant="green" size="small" disabled={bettingLocked} />
</View>
</BlurView>
<BlurView intensity={28} tint="dark" style={styles.blurPanel}>
{isMyTurn && turnSeconds !== null ? (
<Text style={styles.timer}>Idő: {turnSeconds} mp</Text>
) : null}
{showActionControls ? (
<View style={styles.actionWrap}>
<View style={[styles.actionHint, isMyTurn && styles.actionHintActive]}>
<Text style={styles.actionHintText}>{isMyTurn ? 'HIT' : 'Várakozás'}</Text>
{isMyTurn && turnSeconds !== null ? (
<Text style={styles.turnTimer}>{turnSeconds} mp</Text>
) : null}
</View>
<Animated.View style={[styles.actionRow, isMyTurn && pulseStyle]}>
<CasinoButton label="Hit" onPress={() => send({ type: 'action', action: 'hit' })} variant="gold" size="small" disabled={!isMyTurn} />
<CasinoButton label="Stand" onPress={() => send({ type: 'action', action: 'stand' })} variant="gold" size="small" disabled={!isMyTurn} />
<CasinoButton label="Double" onPress={() => send({ type: 'action', action: 'double' })} variant="gold" size="small" disabled={!isMyTurn} />
</Animated.View>
</BlurView>
</View>
) : null}
</View>
) : (
<Pressable style={styles.controlsReveal} onPress={showControls} />
@@ -255,9 +436,144 @@ export default function TableScreen({ token, tableId, user, onLeave }) {
{message ? <Text style={styles.message}>{message}</Text> : null}
</View>
</TableBackground>
</View>
</LinearGradient>
</View>
</View>
{winOverlay ? (
<View pointerEvents="none" style={styles.winOverlay}>
<ConfettiBurst trigger={confettiKey} width={width} height={height} />
<View style={styles.winCard}>
<Text style={styles.winTitle}>Nyertél!</Text>
<Text style={styles.winAmount}>+{winOverlay.payout} Ft</Text>
</View>
</View>
) : null}
</LinearGradient>
);
}
function PortraitSeat({ seat, highlight, cardSize, scale, dealIndex }) {
const isEmpty = !seat.userId;
const hand = seat.hand || [];
const total = getHandTotal(hand);
const label = seat.userId ? (seat.isYou ? 'ÉN' : seat.username || 'Játékos') : 'Üres hely';
return (
<View style={[styles.seatCard, highlight && styles.seatCardActive]}>
{highlight ? <View style={styles.seatGlow} /> : null}
<View style={styles.avatarWrap}>
<View style={styles.avatarRing}>
<View style={styles.avatarHead} />
<View style={styles.avatarBody} />
</View>
</View>
<Text style={[styles.seatName, seat.isYou && styles.seatNameYou]} numberOfLines={1}>
{label}
</Text>
{hand.length > 0 ? (
<View style={styles.seatHand}>
{hand.map((card, idx) => (
<View
key={`${card.rank}-${card.suit}-${idx}`}
style={[
styles.seatCardStack,
{
marginLeft: idx === 0 ? 0 : -14 * scale,
transform: [{ rotate: `${idx === 0 ? 0 : idx * -3}deg` }]
}
]}
>
<Card
rank={card.rank}
suit={card.suit}
hidden={card.hidden}
size={cardSize}
delay={dealIndex * 140 + idx * 120}
/>
</View>
))}
</View>
) : null}
{total !== null ? <Text style={styles.seatTotal}>Összeg: {total}</Text> : null}
<View style={[styles.betPill, isEmpty && styles.betPillEmpty]}>
<Text style={styles.betPillText}>{seat.bet > 0 ? `${seat.bet} Ft` : 'TÉT'}</Text>
</View>
{seat.result?.outcome === 'blackjack' ? (
<View style={styles.blackjackTag}>
<Text style={styles.blackjackText}>BLACKJACK!</Text>
</View>
) : null}
</View>
);
}
function ConfettiBurst({ trigger, width, height }) {
const pieces = useMemo(() => {
const palette = ['#f7d488', '#d94a3d', '#39c377', '#2f7dd3', '#f2f1e8'];
return Array.from({ length: 22 }, (_, idx) => {
return {
key: `${trigger}-${idx}`,
x: Math.random() * Math.max(0, width - 24),
delay: idx * 70,
rotate: Math.random() * 180,
drift: (Math.random() - 0.5) * 80,
size: 6 + Math.random() * 6,
color: palette[idx % palette.length],
anim: new Animated.Value(0)
};
});
}, [trigger, width]);
useEffect(() => {
const animations = pieces.map((piece) =>
Animated.timing(piece.anim, {
toValue: 1,
duration: 1600,
delay: piece.delay,
useNativeDriver: true
})
);
Animated.parallel(animations).start();
}, [pieces]);
return (
<View pointerEvents="none" style={styles.confettiLayer}>
{pieces.map((piece) => (
<Animated.View
key={piece.key}
style={[
styles.confettiPiece,
{
backgroundColor: piece.color,
width: piece.size,
height: piece.size * 1.6,
left: piece.x,
transform: [
{
translateY: piece.anim.interpolate({
inputRange: [0, 1],
outputRange: [-20, height + 20]
})
},
{
translateX: piece.anim.interpolate({
inputRange: [0, 1],
outputRange: [0, piece.drift]
})
},
{
rotate: piece.anim.interpolate({
inputRange: [0, 1],
outputRange: [`${piece.rotate}deg`, `${piece.rotate + 120}deg`]
})
}
]
}
]}
/>
))}
</View>
);
}
@@ -274,64 +590,215 @@ const styles = StyleSheet.create({
},
tableTitle: {
color: colors.goldBright,
fontSize: 20,
fontSize: 22,
fontFamily: fonts.display,
letterSpacing: 2
letterSpacing: 1
},
balance: {
color: colors.muted,
marginTop: 4,
marginTop: 2,
fontFamily: fonts.mono
},
tableWrap: {
flex: 1,
justifyContent: 'flex-start',
alignItems: 'center',
marginTop: 8,
marginTop: 10,
marginBottom: 8,
width: '100%'
},
tableSurface: {
flex: 1,
width: '100%',
padding: 0
borderRadius: 26,
overflow: 'hidden'
},
tableInner: {
flex: 1,
position: 'relative'
tableGradient: {
flex: 1
},
dealerArea: {
alignItems: 'center',
paddingTop: 8
tableGlow: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(255,255,255,0.02)'
},
tableArc: {
position: 'absolute',
left: '10%',
right: '10%',
borderRadius: 999,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.12)'
},
tableArcTop: {
top: 26
},
tableArcMid: {
top: 130,
borderColor: 'rgba(255,255,255,0.08)'
},
tableArcBottom: {
bottom: 200,
borderColor: 'rgba(255,255,255,0.1)'
},
tableContent: {
flex: 1,
justifyContent: 'space-between',
paddingBottom: 140
paddingHorizontal: 16
},
dealerArea: {
alignItems: 'center',
marginTop: 6
},
seatLayer: {
flex: 1,
position: 'relative'
position: 'relative',
marginTop: 6,
marginBottom: 6
},
seatSpot: {
position: 'absolute'
},
seatCard: {
flex: 1,
alignItems: 'center',
justifyContent: 'flex-start'
},
seatCardActive: {
shadowColor: colors.goldBright,
shadowOpacity: 0.6,
shadowRadius: 12
},
seatGlow: {
position: 'absolute',
width: 90,
transform: [{ translateX: -45 }]
top: -8,
left: -8,
right: -8,
bottom: -8,
borderRadius: 999,
borderWidth: 1,
borderColor: 'rgba(255,214,138,0.6)'
},
controlsOverlay: {
position: 'absolute',
left: 20,
right: 20,
bottom: 10,
gap: 10
avatarWrap: {
marginTop: 2,
marginBottom: 4
},
controlsReveal: {
...StyleSheet.absoluteFillObject
avatarRing: {
width: 38,
height: 38,
borderRadius: 19,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.35)',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(0,0,0,0.25)'
},
blurPanel: {
borderRadius: 18,
paddingVertical: 8,
avatarHead: {
width: 10,
height: 10,
borderRadius: 5,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.3)',
marginBottom: 2
},
avatarBody: {
width: 18,
height: 10,
borderRadius: 6,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.25)'
},
seatName: {
color: colors.text,
fontFamily: fonts.body,
fontSize: 12,
marginBottom: 2
},
seatNameYou: {
fontWeight: '800',
color: colors.goldBright
},
seatHand: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 2
},
seatCardStack: {
alignItems: 'center',
justifyContent: 'center'
},
seatTotal: {
marginTop: 3,
color: colors.muted,
fontSize: 11,
fontFamily: fonts.mono
},
betPill: {
marginTop: 4,
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 10,
borderWidth: 1,
borderColor: colors.goldBright,
backgroundColor: 'rgba(0,0,0,0.25)'
},
betPillEmpty: {
borderColor: 'rgba(255,255,255,0.18)'
},
betPillText: {
color: colors.goldBright,
fontFamily: fonts.mono,
fontSize: 12
},
blackjackTag: {
marginTop: 4,
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 8,
backgroundColor: 'rgba(0,0,0,0.45)',
borderWidth: 1,
borderColor: colors.gold
},
blackjackText: {
color: colors.goldBright,
fontSize: 10,
fontFamily: fonts.body
},
playerArea: {
alignItems: 'center',
marginTop: 4,
marginBottom: 4
},
playerHand: {
flexDirection: 'row',
alignItems: 'center'
},
playerCard: {
alignItems: 'center',
justifyContent: 'center'
},
playerTotal: {
marginTop: 6,
color: colors.muted,
fontFamily: fonts.mono,
fontSize: 12
},
playerBet: {
marginTop: 2,
color: colors.goldBright,
fontFamily: fonts.body,
fontSize: 14
},
controlsArea: {
marginTop: 4,
gap: 12,
paddingBottom: 6
},
roundTimer: {
textAlign: 'center',
color: colors.goldBright,
fontFamily: fonts.mono,
fontSize: 12
},
controlsPanel: {
borderRadius: 18,
paddingVertical: 10,
paddingHorizontal: 12,
backgroundColor: 'rgba(10, 18, 14, 0.45)',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.12)',
@@ -341,17 +808,18 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 8
gap: 10,
flexWrap: 'wrap'
},
betControls: {
flexDirection: 'row',
alignItems: 'center',
gap: 6
gap: 8
},
betAdjust: {
width: 28,
height: 28,
borderRadius: 14,
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: 'rgba(255,255,255,0.12)',
alignItems: 'center',
justifyContent: 'center'
@@ -364,6 +832,38 @@ const styles = StyleSheet.create({
betAdjustDisabled: {
opacity: 0.4
},
actionWrap: {
alignItems: 'center',
gap: 10
},
actionHint: {
minWidth: '80%',
borderRadius: 18,
paddingVertical: 10,
paddingHorizontal: 16,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.2)',
backgroundColor: 'rgba(0,0,0,0.25)',
alignItems: 'center'
},
actionHintActive: {
borderColor: colors.goldBright,
shadowColor: colors.goldBright,
shadowOpacity: 0.4,
shadowRadius: 10
},
actionHintText: {
color: colors.goldBright,
fontFamily: fonts.display,
fontSize: 16,
letterSpacing: 2
},
turnTimer: {
marginTop: 4,
color: colors.muted,
fontFamily: fonts.mono,
fontSize: 12
},
actionRow: {
flexDirection: 'row',
justifyContent: 'space-between',
@@ -377,46 +877,45 @@ const styles = StyleSheet.create({
message: {
color: colors.goldBright,
textAlign: 'center',
marginTop: 8,
marginTop: 4,
fontFamily: fonts.body
},
timer: {
color: colors.goldBright,
textAlign: 'center',
marginBottom: 4,
fontFamily: fonts.mono
controlsReveal: {
...StyleSheet.absoluteFillObject
},
rotateWrap: {
flex: 1,
alignItems: 'center',
winOverlay: {
...StyleSheet.absoluteFillObject,
justifyContent: 'center',
paddingHorizontal: 24
alignItems: 'center'
},
rotateTitle: {
winCard: {
paddingHorizontal: 24,
paddingVertical: 16,
borderRadius: 18,
backgroundColor: 'rgba(8, 18, 12, 0.85)',
borderWidth: 1,
borderColor: colors.goldBright
},
winTitle: {
color: colors.goldBright,
fontFamily: fonts.display,
fontSize: 24,
letterSpacing: 2,
fontSize: 22,
textAlign: 'center'
},
rotateSubtitle: {
color: colors.muted,
fontFamily: fonts.body,
fontSize: 14,
marginTop: 12,
winAmount: {
marginTop: 6,
color: colors.text,
fontFamily: fonts.mono,
fontSize: 18,
textAlign: 'center'
},
rotateActions: {
marginTop: 20
confettiLayer: {
...StyleSheet.absoluteFillObject
},
confettiPiece: {
position: 'absolute',
top: 0,
borderRadius: 3,
opacity: 0.9
}
});
const seatPositions = {
0: { left: '8%', top: '52%' },
1: { left: '22%', top: '45%' },
2: { left: '36%', top: '40%' },
3: { left: '50%', top: '38%' },
4: { left: '64%', top: '40%' },
5: { left: '78%', top: '45%' },
6: { left: '92%', top: '52%' }
};