Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import '@/App.css'
import Landing from "@/routes/landing.tsx";
import Lobby from "@/routes/lobby.tsx";
import Login from "@/routes/login.tsx";
import {createBrowserRouter, RouterProvider} from "react-router-dom";
import {createBrowserRouter, Navigate, RouterProvider} from "react-router-dom";
import {AuthGuard} from "@/auth/auth-guard.tsx";
import '@/i18n';
import {MainWrapper} from "@/components/main-wrapper.tsx";
Expand All @@ -24,6 +24,10 @@ const router = createBrowserRouter([
},
{
path: "/lobby",
element: <Navigate to="/lobby/daily" replace/>,
},
{
path: "/lobby/:section",
element: <AuthGuard><MainWrapper><Lobby/></MainWrapper></AuthGuard>,
},
{
Expand Down
1 change: 1 addition & 0 deletions client/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const api = {

// Game
createGame: (data: CreateCustomGameRequest) => generatedApi.postGameCreate({client: customClient, body: data}),
dailyChallenge: () => generatedApi.postGameDaily({client: customClient}),
joinGame: (data: JoinGameRequest) => generatedApi.postGameJoin({client: customClient, body: data}),
playCard: (data: PlayCardRequest) => generatedApi.postGamePlay({client: customClient, body: data}),
chooseTrump: (data: ChooseTrumpRequest) => generatedApi.postGameTrump({client: customClient, body: data}),
Expand Down
4 changes: 2 additions & 2 deletions client/src/api/generated/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// This file is auto-generated by @hey-api/openapi-ts

export { anonLink, anonLogout, anonSignup, connect, getAdminAnalyzeGameByCode, type Options, postAdminMessage, postGameCreate, postGameJoin, postGamePing, postGamePlay, postGameSchiebe, postGameTrump, postGameWeisen, subscribe, whoami } from './sdk.gen';
export type { AnalyzeGameStateResponse, AnalyzeHand, AnonLinkData, AnonLinkErrors, AnonLinkResponse, AnonLinkResponses, AnonLogoutData, AnonLogoutResponse, AnonLogoutResponses, AnonSignupData, AnonSignupErrors, AnonSignupRequest, AnonSignupResponse, AnonSignupResponse2, AnonSignupResponses, Card, CardInHand, CardOnTable, ChooseTrumpRequest, ClientOptions, ConnectData, ConnectErrors, ConnectResponse, ConnectResponses, CreateCustomGameRequest, CreateCustomGameResponse, GameFinished, GetAdminAnalyzeGameByCodeData, GetAdminAnalyzeGameByCodeResponse, GetAdminAnalyzeGameByCodeResponses, JoinGameRequest, JoinGameResponse, MessageRequest, PingSeatRequest, PlayCardRequest, PlayedCard, PlayedCardWithPlayer, Player, PlayerAtTable, PlayerWithCards, PlayerWithWeise, Position, PostAdminMessageData, PostAdminMessageResponse, PostAdminMessageResponses, PostGameCreateData, PostGameCreateResponse, PostGameCreateResponses, PostGameJoinData, PostGameJoinResponse, PostGameJoinResponses, PostGamePingData, PostGamePingResponse, PostGamePingResponses, PostGamePlayData, PostGamePlayResponse, PostGamePlayResponses, PostGameSchiebeData, PostGameSchiebeResponse, PostGameSchiebeResponses, PostGameTrumpData, PostGameTrumpResponse, PostGameTrumpResponses, PostGameWeisenData, PostGameWeisenResponse, PostGameWeisenResponses, Rank, SchiebeRequest, SeatState, State, SubscribeData, SubscribeErrors, SubscribeRequest, SubscribeResponse, SubscribeResponses, SuccessfulActionResponse, Suit, TeamWithPoints, TotalPoints, TrickWithCards, Trump, Weis, WeisenRequest, WeisType, WeisWithPoints, WhoamiData, WhoamiErrors, WhoamiResponse, WhoAmIResponse, WhoamiResponses } from './types.gen';
export { anonLink, anonLogout, anonSignup, connect, getAdminAnalyzeGameByCode, type Options, postAdminMessage, postGameCreate, postGameDaily, postGameJoin, postGamePing, postGamePlay, postGameSchiebe, postGameTrump, postGameWeisen, subscribe, whoami } from './sdk.gen';
export type { AnalyzeGameStateResponse, AnalyzeHand, AnonLinkData, AnonLinkErrors, AnonLinkResponse, AnonLinkResponses, AnonLogoutData, AnonLogoutResponse, AnonLogoutResponses, AnonSignupData, AnonSignupErrors, AnonSignupRequest, AnonSignupResponse, AnonSignupResponse2, AnonSignupResponses, Card, CardInHand, CardOnTable, ChooseTrumpRequest, ClientOptions, ConnectData, ConnectErrors, ConnectResponse, ConnectResponses, CreateCustomGameRequest, CreateCustomGameResponse, CreateDailyChallengeResponse, GameFinished, GetAdminAnalyzeGameByCodeData, GetAdminAnalyzeGameByCodeResponse, GetAdminAnalyzeGameByCodeResponses, JoinGameRequest, JoinGameResponse, MessageRequest, PingSeatRequest, PlayCardRequest, PlayedCard, PlayedCardWithPlayer, Player, PlayerAtTable, PlayerWithCards, PlayerWithWeise, Position, PostAdminMessageData, PostAdminMessageResponse, PostAdminMessageResponses, PostGameCreateData, PostGameCreateResponse, PostGameCreateResponses, PostGameDailyData, PostGameDailyResponse, PostGameDailyResponses, PostGameJoinData, PostGameJoinResponse, PostGameJoinResponses, PostGamePingData, PostGamePingResponse, PostGamePingResponses, PostGamePlayData, PostGamePlayResponse, PostGamePlayResponses, PostGameSchiebeData, PostGameSchiebeResponse, PostGameSchiebeResponses, PostGameTrumpData, PostGameTrumpResponse, PostGameTrumpResponses, PostGameWeisenData, PostGameWeisenResponse, PostGameWeisenResponses, Rank, SchiebeRequest, SeatState, State, SubscribeData, SubscribeErrors, SubscribeRequest, SubscribeResponse, SubscribeResponses, SuccessfulActionResponse, Suit, TeamWithPoints, TotalPoints, TrickWithCards, Trump, Weis, WeisenRequest, WeisType, WeisWithPoints, WhoamiData, WhoamiErrors, WhoamiResponse, WhoAmIResponse, WhoamiResponses } from './types.gen';
14 changes: 13 additions & 1 deletion client/src/api/generated/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import type { Client, Options as Options2, TDataShape } from './client';
import { client } from './client.gen';
import type { AnonLinkData, AnonLinkErrors, AnonLinkResponses, AnonLogoutData, AnonLogoutResponses, AnonSignupData, AnonSignupErrors, AnonSignupResponses, ConnectData, ConnectErrors, ConnectResponses, GetAdminAnalyzeGameByCodeData, GetAdminAnalyzeGameByCodeResponses, PostAdminMessageData, PostAdminMessageResponses, PostGameCreateData, PostGameCreateResponses, PostGameJoinData, PostGameJoinResponses, PostGamePingData, PostGamePingResponses, PostGamePlayData, PostGamePlayResponses, PostGameSchiebeData, PostGameSchiebeResponses, PostGameTrumpData, PostGameTrumpResponses, PostGameWeisenData, PostGameWeisenResponses, SubscribeData, SubscribeErrors, SubscribeResponses, WhoamiData, WhoamiErrors, WhoamiResponses } from './types.gen';
import type { AnonLinkData, AnonLinkErrors, AnonLinkResponses, AnonLogoutData, AnonLogoutResponses, AnonSignupData, AnonSignupErrors, AnonSignupResponses, ConnectData, ConnectErrors, ConnectResponses, GetAdminAnalyzeGameByCodeData, GetAdminAnalyzeGameByCodeResponses, PostAdminMessageData, PostAdminMessageResponses, PostGameCreateData, PostGameCreateResponses, PostGameDailyData, PostGameDailyResponses, PostGameJoinData, PostGameJoinResponses, PostGamePingData, PostGamePingResponses, PostGamePlayData, PostGamePlayResponses, PostGameSchiebeData, PostGameSchiebeResponses, PostGameTrumpData, PostGameTrumpResponses, PostGameWeisenData, PostGameWeisenResponses, SubscribeData, SubscribeErrors, SubscribeResponses, WhoamiData, WhoamiErrors, WhoamiResponses } from './types.gen';

export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, TResponse = unknown> = Options2<TData, ThrowOnError, TResponse> & {
/**
Expand Down Expand Up @@ -131,6 +131,18 @@ export const postGamePing = <ThrowOnError extends boolean = false>(options: Opti
}
});

/**
* Join or create today's daily challenge
*
* Returns the code of today's daily challenge game for the current player. Creates the game on the first call of the day, returns the already running (or finished) game on every later call. Only available for signed up players.
*
*/
export const postGameDaily = <ThrowOnError extends boolean = false>(options?: Options<PostGameDailyData, ThrowOnError>) => (options?.client ?? client).post<PostGameDailyResponses, unknown, ThrowOnError>({
responseType: 'json',
url: '/game/daily',
...options
});

/**
* Get current user information
*/
Expand Down
20 changes: 20 additions & 0 deletions client/src/api/generated/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ export type CreateCustomGameResponse = {
code?: string;
};

export type CreateDailyChallengeResponse = {
code: string;
};

export type JoinGameRequest = {
code: string;
};
Expand Down Expand Up @@ -400,6 +404,22 @@ export type PostGamePingResponses = {

export type PostGamePingResponse = PostGamePingResponses[keyof PostGamePingResponses];

export type PostGameDailyData = {
body?: never;
path?: never;
query?: never;
url: '/game/daily';
};

export type PostGameDailyResponses = {
/**
* Daily challenge joined successfully
*/
200: CreateDailyChallengeResponse;
};

export type PostGameDailyResponse = PostGameDailyResponses[keyof PostGameDailyResponses];

export type WhoamiData = {
body?: never;
path?: never;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger} from "@/components/ui/dialog"
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"
import {Button} from "@/components/ui/button"
import {AlertCircle, Bot, Loader2, Plus, User} from "lucide-react"
import {useTranslation} from "react-i18next"
Expand All @@ -9,7 +9,6 @@ import {api} from "@/api/client"
import {useAxiosErrorHandler} from "@/hooks/use-axios-error-handler"
import {useAsyncAction} from "@/hooks/use-async-action"
import {CreateCustomGameRequest} from "@/api/generated";
import {DialogDescription} from "@radix-ui/react-dialog";
import {useState} from "react";
import {isAxiosError} from "axios";
import {useNavigate} from "react-router-dom";
Expand Down Expand Up @@ -52,10 +51,9 @@ const TeamButton = ({team, selected, onClick}: { team: TeamComposition, selected
)
}

export function CreateGameOverlay() {
export function CreateSection() {
const {t} = useTranslation()
const handleAxiosError = useAxiosErrorHandler()
const [open, setOpen] = useState(false)
const [error, setError] = useState<string | null>(null)
const navigate = useNavigate()
const [nsTeam, setNsTeam] = useState<TeamComposition>('player-bot')
Expand All @@ -80,7 +78,6 @@ export function CreateGameOverlay() {
reset()
try {
const response = await executeCreateGame(settings)
setOpen(false)
navigate(`/game/${response.data?.code}`)
} catch (error: unknown) {
if (isAxiosError(error) && error.response?.status === 422) {
Expand Down Expand Up @@ -146,115 +143,111 @@ export function CreateGameOverlay() {
}

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="w-full" variant="outline">
<Plus className="mr-2 h-4 w-4"/>
{t("main.game.create")}
</Button>
</DialogTrigger>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle>{t("create.title")}</DialogTitle>
<DialogDescription>{t("create.description")}</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4"/>
<AlertTitle>{t("errors.title")}</AlertTitle>
<AlertDescription>
{error}
</AlertDescription>
</Alert>
)}
<div className="grid gap-2">
<div>
<Label>{t("create.players")}</Label>
<p className="text-xs italic text-muted-foreground mt-1">
{t(`create.configDescription.${getConfigDescriptionKey()}`)}
</p>
<Card className="h-full">
<CardHeader>
<div className="flex items-center gap-2">
<Plus className="h-5 w-5 text-primary"/>
<CardTitle className="text-lg">{t("create.title")}</CardTitle>
</div>
<CardDescription>{t("create.description")}</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4"/>
<AlertTitle>{t("errors.title")}</AlertTitle>
<AlertDescription>
{error}
</AlertDescription>
</Alert>
)}
<div className="grid gap-2">
<div>
<Label>{t("create.players")}</Label>
<p className="text-xs italic text-muted-foreground mt-1">
{t(`create.configDescription.${getConfigDescriptionKey()}`)}
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-2">
<TeamButton
team="player-player"
selected={nsTeam === 'player-player'}
onClick={() => handleTeamSelection('NS', 'player-player')}
/>
<TeamButton
team="player-bot"
selected={nsTeam === 'player-bot'}
onClick={() => handleTeamSelection('NS', 'player-bot')}
/>
<TeamButton
team="bot-bot"
selected={nsTeam === 'bot-bot'}
onClick={() => handleTeamSelection('NS', 'bot-bot')}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-2">
<TeamButton
team="player-player"
selected={nsTeam === 'player-player'}
onClick={() => handleTeamSelection('NS', 'player-player')}
/>
<TeamButton
team="player-bot"
selected={nsTeam === 'player-bot'}
onClick={() => handleTeamSelection('NS', 'player-bot')}
/>
<TeamButton
team="bot-bot"
selected={nsTeam === 'bot-bot'}
onClick={() => handleTeamSelection('NS', 'bot-bot')}
/>
</div>

<div className="flex flex-col gap-2">
<TeamButton
team="player-player"
selected={ewTeam === 'player-player'}
onClick={() => handleTeamSelection('EW', 'player-player')}
/>
<TeamButton
team="player-bot"
selected={ewTeam === 'player-bot'}
onClick={() => handleTeamSelection('EW', 'player-bot')}
/>
<TeamButton
team="bot-bot"
selected={ewTeam === 'bot-bot'}
onClick={() => handleTeamSelection('EW', 'bot-bot')}
/>
</div>
<div className="flex flex-col gap-2">
<TeamButton
team="player-player"
selected={ewTeam === 'player-player'}
onClick={() => handleTeamSelection('EW', 'player-player')}
/>
<TeamButton
team="player-bot"
selected={ewTeam === 'player-bot'}
onClick={() => handleTeamSelection('EW', 'player-bot')}
/>
<TeamButton
team="bot-bot"
selected={ewTeam === 'bot-bot'}
onClick={() => handleTeamSelection('EW', 'bot-bot')}
/>
</div>
</div>
</div>

<div className="grid gap-2">
<Label>{t("create.winningCondition")}</Label>
<RadioGroup
value={settings.winningConditionType}
onValueChange={(value) => handleConditionTypeChange(value as 'POINTS' | 'HANDS')}
className="flex gap-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="POINTS" id="points"/>
<Label htmlFor="points">{t("create.points")}</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="HANDS" id="hands"/>
<Label htmlFor="hands">{t("create.hands")}</Label>
</div>
</RadioGroup>
</div>
<div className="grid gap-2">
<Label>{t("create.winningCondition")}</Label>
<RadioGroup
value={settings.winningConditionType}
onValueChange={(value) => handleConditionTypeChange(value as 'POINTS' | 'HANDS')}
className="flex gap-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="POINTS" id="points"/>
<Label htmlFor="points">{t("create.points")}</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="HANDS" id="hands"/>
<Label htmlFor="hands">{t("create.hands")}</Label>
</div>
</RadioGroup>
</div>

<div className="grid gap-2">
<Label>{t("create.value")}</Label>
<Input
type="number"
min="1"
max="9999"
value={settings.winningConditionValue}
onChange={(e) => setSettings(s => ({
...s,
winningConditionValue: parseInt(e.target.value) || 0
}))}
/>
</div>
<div className="grid gap-2">
<Label>{t("create.value")}</Label>
<Input
type="number"
min="1"
max="9999"
value={settings.winningConditionValue}
onChange={(e) => setSettings(s => ({
...s,
winningConditionValue: parseInt(e.target.value) || 0
}))}
/>
</div>

<Button
onClick={handleCreate}
disabled={isLoading}
variant={hasError ? "destructive" : "default"}
>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin"/>}
{isLoading && <Loader2 className="h-4 w-4 animate-spin"/>}
{t("create.submit")}
</Button>
</DialogContent>
</Dialog>
</CardContent>
</Card>
)
}
}
Loading
Loading