pridanie pocty online/available hracov
This commit is contained in:
@@ -1,14 +1,21 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
username?: string;
|
||||
onlineCount?: number;
|
||||
availableCount?: number;
|
||||
onHistory: () => void;
|
||||
onDonate: () => void;
|
||||
onLogout: () => void;
|
||||
}
|
||||
|
||||
/** Header navigation: a row of links from `sm:` up, a hamburger dropdown below it. */
|
||||
export default function HeaderMenu({ username, onHistory, onDonate, onLogout }: Props) {
|
||||
export default function HeaderMenu({
|
||||
onlineCount,
|
||||
availableCount,
|
||||
onHistory,
|
||||
onDonate,
|
||||
onLogout,
|
||||
}: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -34,9 +41,16 @@ export default function HeaderMenu({ username, onHistory, onDonate, onLogout }:
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="hidden sm:flex items-center gap-3 text-sm">
|
||||
<span className="text-green-dim">{username}</span>
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
{typeof onlineCount === 'number' && (
|
||||
<span className="flex items-center gap-1.5 text-green-dim" title="Online / dostupní hráči">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-green-score" />
|
||||
{onlineCount}
|
||||
{typeof availableCount === 'number' && `/${availableCount} free`}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="hidden sm:flex items-center gap-3">
|
||||
<button onClick={onHistory} className="text-gold hover:text-gold-bright">
|
||||
História
|
||||
</button>
|
||||
@@ -66,11 +80,6 @@ export default function HeaderMenu({ username, onHistory, onDonate, onLogout }:
|
||||
role="menu"
|
||||
className="absolute right-0 top-11 z-40 w-44 bg-header border border-[#142018] rounded-xl py-1 shadow-[0_20px_60px_rgba(0,0,0,.6)]"
|
||||
>
|
||||
{username && (
|
||||
<p className="px-4 py-2 text-xs text-green-dim border-b border-[#142018] truncate">
|
||||
{username}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
role="menuitem"
|
||||
onClick={pick(onHistory)}
|
||||
@@ -95,6 +104,6 @@ export default function HeaderMenu({ username, onHistory, onDonate, onLogout }:
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ import { useGameStore } from '../store/gameStore';
|
||||
import type {
|
||||
Account,
|
||||
GameDetail,
|
||||
GameInfo,
|
||||
GameStatusPayload,
|
||||
Hand,
|
||||
HistoryGame,
|
||||
MyPlayer,
|
||||
OnlineCountPayload,
|
||||
Registration,
|
||||
} from '../types';
|
||||
|
||||
@@ -29,6 +29,9 @@ export const emit = {
|
||||
confirmAccount: (username: string, code: string) =>
|
||||
socket.emit('confirm_account', username, code),
|
||||
login: (username: string, code: string) => socket.emit('login', username, code),
|
||||
// De-authenticates this connection server-side (no disconnect/reconnect --
|
||||
// see the "logout" handler comment in api/__init__.py for why).
|
||||
logout: () => socket.emit('logout'),
|
||||
// history
|
||||
getPlayerHistory: () => socket.emit('get_player_history'),
|
||||
getGameDetail: (gid: string) => socket.emit('get_game_detail', gid),
|
||||
@@ -64,8 +67,11 @@ export const emit = {
|
||||
};
|
||||
|
||||
export function setupSocketListeners() {
|
||||
socket.on('get_games', ({ games }: { games: GameInfo[] }) => {
|
||||
socket.on('get_games', ({ games, online_count, available_count }: OnlineCountPayload) => {
|
||||
useGameStore.getState().setGames(games);
|
||||
if (typeof online_count === 'number') useGameStore.getState().setOnlineCount(online_count);
|
||||
if (typeof available_count === 'number')
|
||||
useGameStore.getState().setAvailableCount(available_count);
|
||||
});
|
||||
|
||||
// Registration step 1: server returns the otpauth URI to render as a QR code.
|
||||
|
||||
@@ -65,7 +65,7 @@ export default function Auth() {
|
||||
<div className="max-w-sm mx-auto p-4 pt-12 min-h-screen">
|
||||
<h1 className="font-serif text-4xl text-center text-gold tracking-wide mb-1">Bridžik</h1>
|
||||
<p className="text-center text-green-dim text-sm mb-7">
|
||||
Prihlás sa kódom z aplikácie (napr. Google Authenticator).
|
||||
Prihlás sa kódom z aplikácie (<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" className="text-gold underline" target="_blank" rel="noopener noreferrer">napr. Google Authenticator </a>).
|
||||
</p>
|
||||
|
||||
<div className="flex mb-6 rounded-xl overflow-hidden border border-gold/20">
|
||||
@@ -141,7 +141,7 @@ export default function Auth() {
|
||||
{mode === 'register' && registration && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-sm text-green-score">
|
||||
Naskenuj QR kód do autentifikačnej aplikácie a opíš aktuálny kód.
|
||||
Naskenuj QR kód do <a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" className="text-gold underline" target="_blank" rel="noopener noreferrer">autentifikačnej aplikácie</a> a opíš aktuálny kód.
|
||||
</p>
|
||||
<div className="bg-white rounded-xl p-4 flex justify-center">
|
||||
<QRCodeSVG value={registration.otpauth_uri} size={176} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useGameStore } from '../store/gameStore';
|
||||
import { emit, socket, setAuthToken } from '../lib/socket';
|
||||
import { emit, setAuthToken } from '../lib/socket';
|
||||
import { trackEvent } from '../lib/track';
|
||||
import HeaderMenu from '../components/HeaderMenu';
|
||||
import NameModal from '../components/NameModal';
|
||||
@@ -10,6 +10,8 @@ import RulesModal from '../components/RulesModal';
|
||||
export default function GameList() {
|
||||
const navigate = useNavigate();
|
||||
const games = useGameStore((s) => s.games);
|
||||
const onlineCount = useGameStore((s) => s.onlineCount);
|
||||
const availableCount = useGameStore((s) => Math.max(0, s.availableCount - 1));
|
||||
const account = useGameStore((s) => s.account);
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [showRules, setShowRules] = useState(false);
|
||||
@@ -20,13 +22,13 @@ export default function GameList() {
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
// Tell the server first (while the socket auth is still attached), then
|
||||
// clear local state -- no disconnect/reconnect needed (see emit.logout).
|
||||
emit.logout();
|
||||
localStorage.removeItem('bridzik_token');
|
||||
localStorage.removeItem('bridzik_player');
|
||||
setAuthToken(null);
|
||||
useGameStore.getState().logout();
|
||||
// Drop the authenticated server-side session for this connection.
|
||||
socket.disconnect();
|
||||
socket.connect();
|
||||
navigate('/auth', { replace: true });
|
||||
};
|
||||
|
||||
@@ -35,7 +37,8 @@ export default function GameList() {
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="font-serif text-2xl text-gold tracking-wide">Bridžik</h1>
|
||||
<HeaderMenu
|
||||
username={account?.username}
|
||||
onlineCount={onlineCount}
|
||||
availableCount={availableCount}
|
||||
onHistory={() => navigate('/history')}
|
||||
onDonate={() => navigate('/donate')}
|
||||
onLogout={handleLogout}
|
||||
|
||||
@@ -12,6 +12,8 @@ import type {
|
||||
|
||||
interface GameStore {
|
||||
games: GameInfo[];
|
||||
onlineCount: number;
|
||||
availableCount: number;
|
||||
account: Account | null;
|
||||
registration: Registration | null;
|
||||
history: HistoryGame[];
|
||||
@@ -22,6 +24,8 @@ interface GameStore {
|
||||
error: string | null;
|
||||
|
||||
setGames: (games: GameInfo[]) => void;
|
||||
setOnlineCount: (count: number) => void;
|
||||
setAvailableCount: (count: number) => void;
|
||||
setAccount: (account: Account | null) => void;
|
||||
setRegistration: (registration: Registration | null) => void;
|
||||
setHistory: (history: HistoryGame[]) => void;
|
||||
@@ -38,6 +42,8 @@ interface GameStore {
|
||||
|
||||
export const useGameStore = create<GameStore>((set) => ({
|
||||
games: [],
|
||||
onlineCount: 0,
|
||||
availableCount: 0,
|
||||
account: null,
|
||||
registration: null,
|
||||
history: [],
|
||||
@@ -48,6 +54,8 @@ export const useGameStore = create<GameStore>((set) => ({
|
||||
error: null,
|
||||
|
||||
setGames: (games) => set({ games }),
|
||||
setOnlineCount: (onlineCount) => set({ onlineCount }),
|
||||
setAvailableCount: (availableCount) => set({ availableCount }),
|
||||
setAccount: (account) => set({ account }),
|
||||
setRegistration: (registration) => set({ registration }),
|
||||
setHistory: (history) => set({ history }),
|
||||
|
||||
@@ -55,13 +55,18 @@ export interface GameInfo {
|
||||
players: PlayerInfo[];
|
||||
}
|
||||
|
||||
export interface OnlineCountPayload {
|
||||
games: GameInfo[];
|
||||
online_count: number;
|
||||
available_count: number;
|
||||
}
|
||||
|
||||
export type Hand = Record<string, Card>;
|
||||
|
||||
// --- authentication (TOTP) ---
|
||||
|
||||
export interface Account {
|
||||
player_id: number;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface Registration {
|
||||
|
||||
Reference in New Issue
Block a user