feat: initialize mobile blackjack app with authentication and game features
This commit is contained in:
256
src/screens/TableScreen.js
Normal file
256
src/screens/TableScreen.js
Normal file
@@ -0,0 +1,256 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Animated, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { WS_URL } from '../api';
|
||||
import { colors, fonts } from '../theme';
|
||||
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';
|
||||
|
||||
export default function TableScreen({ token, tableId, user, onLeave }) {
|
||||
const [table, setTable] = useState(null);
|
||||
const [balance, setBalance] = useState(user.balance);
|
||||
const [message, setMessage] = useState('');
|
||||
const [betAmount, setBetAmount] = useState(10);
|
||||
const wsRef = useRef(null);
|
||||
const pulse = useRef(new Animated.Value(0)).current;
|
||||
|
||||
useEffect(() => {
|
||||
const ws = new WebSocket(WS_URL);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
ws.send(JSON.stringify({ type: 'hello', token }));
|
||||
ws.send(JSON.stringify({ type: 'join', tableId }));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const payload = JSON.parse(event.data);
|
||||
if (payload.type === 'table_state') {
|
||||
setTable(payload.table);
|
||||
if (payload.table.minBet) {
|
||||
setBetAmount((prev) => {
|
||||
const clamped = Math.max(payload.table.minBet, Math.min(prev, payload.table.maxBet));
|
||||
return clamped;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (payload.type === 'balance') {
|
||||
setBalance(payload.balance);
|
||||
}
|
||||
if (payload.type === 'error') {
|
||||
setMessage(payload.message);
|
||||
setTimeout(() => setMessage(''), 2400);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
wsRef.current = null;
|
||||
};
|
||||
|
||||
return () => {
|
||||
if (ws.readyState === 1) {
|
||||
ws.send(JSON.stringify({ type: 'leave' }));
|
||||
}
|
||||
ws.close();
|
||||
};
|
||||
}, [tableId, token]);
|
||||
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMyTurn) {
|
||||
pulse.stopAnimation();
|
||||
pulse.setValue(0);
|
||||
return;
|
||||
}
|
||||
|
||||
Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(pulse, { toValue: 1, duration: 700, useNativeDriver: true }),
|
||||
Animated.timing(pulse, { toValue: 0, duration: 700, useNativeDriver: true })
|
||||
])
|
||||
).start();
|
||||
}, [isMyTurn, pulse]);
|
||||
|
||||
const pulseStyle = {
|
||||
transform: [
|
||||
{
|
||||
scale: pulse.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [1, 1.04]
|
||||
})
|
||||
}
|
||||
],
|
||||
opacity: pulse.interpolate({ inputRange: [0, 1], outputRange: [0.85, 1] })
|
||||
};
|
||||
|
||||
const send = (payload) => {
|
||||
if (wsRef.current?.readyState === 1) {
|
||||
wsRef.current.send(JSON.stringify(payload));
|
||||
}
|
||||
};
|
||||
|
||||
const adjustBet = (delta) => {
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
setBetAmount((prev) => {
|
||||
const next = prev + delta;
|
||||
return Math.max(table.minBet, Math.min(table.maxBet, next));
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<LinearGradient colors={[colors.backgroundTop, colors.backgroundBottom]} style={styles.container}>
|
||||
<View style={styles.topBar}>
|
||||
<View>
|
||||
<Text style={styles.tableTitle}>Asztal {tableId}</Text>
|
||||
<Text style={styles.balance}>Egyenleg: {balance} Ft</Text>
|
||||
</View>
|
||||
<CasinoButton label="Kilépek" onPress={onLeave} variant="red" />
|
||||
</View>
|
||||
|
||||
<TableBackground>
|
||||
<View style={styles.dealerArea}>
|
||||
<DealerArea hand={table?.dealerHand || []} />
|
||||
</View>
|
||||
|
||||
<View style={styles.seatsLayer}>
|
||||
{table?.seats?.map((seat) => {
|
||||
const position = seatPositions[seat.index];
|
||||
if (!position) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<View key={seat.index} style={[styles.seatPosition, position]}>
|
||||
<Seat seat={seat} highlight={table.currentSeatIndex === seat.index} />
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</TableBackground>
|
||||
|
||||
<View style={styles.controls}>
|
||||
<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}>
|
||||
<Text style={styles.betAdjustText}>-</Text>
|
||||
</Pressable>
|
||||
<Chip label={`${betAmount}`} color="red" />
|
||||
<Pressable onPress={() => adjustBet(10)} style={[styles.betAdjust, bettingLocked && styles.betAdjustDisabled]} disabled={bettingLocked}>
|
||||
<Text style={styles.betAdjustText}>+</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<CasinoButton label="Tét" onPress={() => send({ type: 'bet', amount: betAmount })} variant="gold" disabled={bettingLocked} />
|
||||
<CasinoButton label="Kész" onPress={() => send({ type: 'ready' })} variant="green" disabled={bettingLocked} />
|
||||
</View>
|
||||
|
||||
<Animated.View style={[styles.actionRow, isMyTurn && pulseStyle]}>
|
||||
<CasinoButton label="Hit" onPress={() => send({ type: 'action', action: 'hit' })} variant="gold" disabled={!isMyTurn} />
|
||||
<CasinoButton label="Stand" onPress={() => send({ type: 'action', action: 'stand' })} variant="gold" disabled={!isMyTurn} />
|
||||
<CasinoButton label="Double" onPress={() => send({ type: 'action', action: 'double' })} variant="gold" disabled={!isMyTurn} />
|
||||
</Animated.View>
|
||||
{message ? <Text style={styles.message}>{message}</Text> : null}
|
||||
</View>
|
||||
</LinearGradient>
|
||||
);
|
||||
}
|
||||
|
||||
const seatPositions = {
|
||||
0: { bottom: 10, left: '38%' },
|
||||
1: { bottom: 20, left: '8%' },
|
||||
2: { bottom: 20, right: '8%' },
|
||||
3: { top: '52%', left: 0 },
|
||||
4: { top: '52%', right: 0 },
|
||||
5: { top: '22%', left: '6%' },
|
||||
6: { top: '22%', right: '6%' }
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingTop: 40
|
||||
},
|
||||
topBar: {
|
||||
paddingHorizontal: 20,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
},
|
||||
tableTitle: {
|
||||
color: colors.goldBright,
|
||||
fontSize: 20,
|
||||
fontFamily: fonts.display,
|
||||
letterSpacing: 2
|
||||
},
|
||||
balance: {
|
||||
color: colors.muted,
|
||||
marginTop: 4,
|
||||
fontFamily: fonts.mono
|
||||
},
|
||||
dealerArea: {
|
||||
alignItems: 'center'
|
||||
},
|
||||
seatsLayer: {
|
||||
flex: 1,
|
||||
position: 'relative'
|
||||
},
|
||||
seatPosition: {
|
||||
position: 'absolute',
|
||||
width: 140
|
||||
},
|
||||
controls: {
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 24
|
||||
},
|
||||
betRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 8,
|
||||
marginBottom: 12
|
||||
},
|
||||
betControls: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6
|
||||
},
|
||||
betAdjust: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
backgroundColor: 'rgba(255,255,255,0.12)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
betAdjustText: {
|
||||
color: colors.text,
|
||||
fontSize: 18,
|
||||
fontWeight: '700'
|
||||
},
|
||||
betAdjustDisabled: {
|
||||
opacity: 0.4
|
||||
},
|
||||
actionRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
gap: 10
|
||||
},
|
||||
sectionLabel: {
|
||||
color: colors.text,
|
||||
fontSize: 14,
|
||||
fontFamily: fonts.body
|
||||
},
|
||||
message: {
|
||||
color: colors.goldBright,
|
||||
textAlign: 'center',
|
||||
marginTop: 10,
|
||||
fontFamily: fonts.body
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user