realtime game configuration
This commit is contained in:
parent
f9ad545e45
commit
d1baf097fa
@ -3,56 +3,120 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { gameConfigPatchSchema } from "@/lib/validations/game";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useGameStore } from "@/lib/store/game-store";
|
||||
import { api } from "@/trpc/react";
|
||||
import { useLobbyStore } from "@/lib/store/lobby-store";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { getMinMax } from "@/lib/validations/utils";
|
||||
import { debounce } from "@/lib/utils";
|
||||
import React from "react";
|
||||
import { useSessionPlayerStore } from "@/lib/store/current-player-store";
|
||||
|
||||
const formSchema = gameConfigPatchSchema.pick({
|
||||
allowHints: true,
|
||||
// scoreMultiplier: true,
|
||||
timeLimit: true,
|
||||
});
|
||||
|
||||
export default function GameConfigForm() {
|
||||
// ...
|
||||
const form = useForm<z.infer<typeof gameConfigPatchSchema>>({
|
||||
resolver: zodResolver(gameConfigPatchSchema),
|
||||
const updateGameConfig = api.gameConfig.update.useMutation();
|
||||
const gameConfig = useGameStore((state) => state.gameConfig);
|
||||
const lobbyId = useLobbyStore((state) => state.lobby.id);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
allowHints: false,
|
||||
scoreMultiplier: 1,
|
||||
timeLimit: 2,
|
||||
allowHints: gameConfig.allowHints,
|
||||
timeLimit: gameConfig.timeLimit,
|
||||
},
|
||||
});
|
||||
|
||||
// 2. Define a submit handler.
|
||||
function onSubmit(values: z.infer<typeof gameConfigPatchSchema>) {
|
||||
// Do something with the form values.
|
||||
// ✅ This will be type-safe and validated.
|
||||
console.log(values);
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
updateGameConfig.mutate({
|
||||
lobbyId,
|
||||
config: {
|
||||
...gameConfig,
|
||||
...values,
|
||||
},
|
||||
});
|
||||
}
|
||||
const save = form.handleSubmit(onSubmit);
|
||||
React.useEffect(() => {
|
||||
form.reset({
|
||||
allowHints: gameConfig.allowHints,
|
||||
timeLimit: gameConfig.timeLimit,
|
||||
});
|
||||
}, [gameConfig]);
|
||||
const isLobbyAdmin = useSessionPlayerStore((state) => state.isLobbyAdmin);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="allowHints"
|
||||
render={({ field }) => (
|
||||
<FormItem className="container-bg flex flex-row items-center justify-between p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Allow hints</FormLabel>
|
||||
</div>
|
||||
<FormItem className="container-bg bg-background/10 flex flex-row items-center justify-between p-3">
|
||||
<FormLabel>Allow hints</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
className="cursor-pointer"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
onCheckedChange={(checked) => {
|
||||
field.onChange(checked);
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* <Button type="submit">Submit</Button> */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="timeLimit"
|
||||
render={({ field }) => {
|
||||
const { min, max } = getMinMax(formSchema.shape.timeLimit);
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
<div className="container-bg bg-background/10 space-y-2 p-3">
|
||||
<FormLabel>
|
||||
Time Limit
|
||||
<span className="text-xl font-bold">
|
||||
{`${field.value}`} min
|
||||
</span>
|
||||
</FormLabel>
|
||||
{isLobbyAdmin && (
|
||||
<FormControl>
|
||||
<Slider
|
||||
className="w-full max-w-xs"
|
||||
value={[field.value]}
|
||||
max={max ?? 100}
|
||||
min={min ?? 1}
|
||||
step={1}
|
||||
onValueChange={([value]) => {
|
||||
field.onChange(value);
|
||||
debounce(save, 1000)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@ -7,14 +7,16 @@ import GameConfigForm from "./game-config-form";
|
||||
import GameVariantSelector from "./game-variant-selector";
|
||||
import { api } from "@/trpc/react";
|
||||
import { useLobbyStore } from "@/lib/store/lobby-store";
|
||||
import { isCallOrNewExpression } from "typescript";
|
||||
import { useSessionPlayerStore } from "@/lib/store/current-player-store";
|
||||
|
||||
function GameConfigurator() {
|
||||
const updateGameConfig = api.gameConfig.update.useMutation();
|
||||
const selectGame = useGameStore((state) => state.setSelectedGame);
|
||||
const selectedGame = useGameStore((state) => state.selectedGame);
|
||||
const setGameConfig = useGameStore((state) => state.setGameConfig);
|
||||
const gameConfig = useGameStore((state) => state.gameConfig);
|
||||
const lobbyId = useLobbyStore((state) => state.lobby.id);
|
||||
const isLobbyAdmin = useSessionPlayerStore((state) => state.isLobbyAdmin);
|
||||
const handleUnselectGame = () => {
|
||||
selectGame(undefined);
|
||||
updateGameConfig.mutate({
|
||||
@ -26,9 +28,19 @@ function GameConfigurator() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleVariantSelect = (gameVariantId?: string) => {
|
||||
updateGameConfig.mutate({
|
||||
lobbyId,
|
||||
config: {
|
||||
...gameConfig,
|
||||
gameVariantId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col justify-between p-6">
|
||||
<div className="space-y-2">
|
||||
<div className="flex size-full flex-col justify-between">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{selectedGame?.name ?? "Select a game to get started"}
|
||||
@ -42,14 +54,14 @@ function GameConfigurator() {
|
||||
|
||||
{selectedGame && (
|
||||
<>
|
||||
<GameVariantSelector />
|
||||
<GameVariantSelector onSelect={handleVariantSelect} />
|
||||
<GameConfigForm />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedGame && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<Button>Start Game</Button>
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
|
||||
@ -25,17 +25,38 @@ const getGameVariants = (id: string) => {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
function GameVariantSelector() {
|
||||
const selectedGame = useGameStore((state) => state.selectedGame);
|
||||
const variants = getGameVariants(selectedGame?.id!);
|
||||
function GameVariantSelector({
|
||||
onSelect,
|
||||
}: {
|
||||
onSelect: (gameVariantId?: string) => void;
|
||||
}) {
|
||||
const gameId = useGameStore((state) => state.gameConfig.gameId);
|
||||
const selectedVariant = useGameStore(
|
||||
(state) => state.gameConfig.gameVariantId,
|
||||
);
|
||||
const setSelectedVariant = useGameStore((state) => state.updateGameConfig);
|
||||
const variants = getGameVariants(gameId!);
|
||||
if (!variants.length) return null;
|
||||
React.useEffect(() => {
|
||||
if (!selectedVariant)
|
||||
setSelectedVariant({ gameVariantId: variants[0]?.value });
|
||||
}, [selectedVariant]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
className="w-full rounded-2xl"
|
||||
onSelect={() => {}}
|
||||
initialValue={variants[0]?.value}
|
||||
data={variants}
|
||||
/>
|
||||
<>
|
||||
<Combobox
|
||||
className="bg-background/10 w-full rounded-2xl"
|
||||
onSelect={(value) => {
|
||||
if (!value?.length) return;
|
||||
onSelect(value);
|
||||
setSelectedVariant({
|
||||
gameVariantId: value,
|
||||
});
|
||||
}}
|
||||
initialValue={selectedVariant}
|
||||
data={variants}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { IMinigame } from "@/game-engien";
|
||||
import { gameLibary } from "@/game-engien/games";
|
||||
import { useSessionPlayerStore } from "@/lib/store/current-player-store";
|
||||
import { useGameStore } from "@/lib/store/game-store";
|
||||
import { useLobbyStore } from "@/lib/store/lobby-store";
|
||||
import { cn } from "@/lib/utils";
|
||||
@ -8,21 +9,23 @@ import Image from "next/image";
|
||||
import React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
function GameSelector({ isAdmin }: { isAdmin: boolean }) {
|
||||
function GameSelector() {
|
||||
const updateGameConfig = api.gameConfig.update.useMutation();
|
||||
const setGameConfig = useGameStore((state) => state.updateGameConfig);
|
||||
const selectedGame = useGameStore((state) => state.selectedGame);
|
||||
const setSelectedGame = useGameStore((state) => state.setSelectedGame);
|
||||
const gameConfig = useGameStore((state) => state.gameConfig);
|
||||
const lobbyId = useLobbyStore((state) => state.lobby.id);
|
||||
const isLobbyAdmin = useSessionPlayerStore((state) => state.isLobbyAdmin);
|
||||
|
||||
const handleGameSelect = (game: IMinigame) => {
|
||||
if (!isAdmin) return toast.error("Only admins can change games");
|
||||
if (!isLobbyAdmin) return toast.error("Only admins can change games");
|
||||
setSelectedGame(game);
|
||||
const newConfig = { ...gameConfig, gameId: game.id };
|
||||
setGameConfig(newConfig);
|
||||
updateGameConfig.mutate({
|
||||
lobbyId,
|
||||
config: {
|
||||
...gameConfig,
|
||||
gameId: game.id,
|
||||
},
|
||||
config: newConfig,
|
||||
});
|
||||
};
|
||||
return (
|
||||
|
||||
@ -24,85 +24,87 @@ function LobbyPage({ initialLobby }: { initialLobby: Lobby }) {
|
||||
const isJoined = sessionPlayer
|
||||
? members.find((m) => m.playerId === sessionPlayer.id)
|
||||
: null;
|
||||
|
||||
const isAdmin = isJoined?.role === "admin";
|
||||
const isAdmin = useSessionPlayerStore((state) => state.isLobbyAdmin);
|
||||
const gameConfig = useGameStore((state) => state.gameConfig);
|
||||
return (
|
||||
<LobbyProvider initialLobby={initialLobby}>
|
||||
<div className="grid w-full max-w-4xl grid-cols-3 grid-rows-2 gap-4">
|
||||
<div className="container-bg col-span-2 w-full space-y-4 p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<CopyToClip
|
||||
target="invite link"
|
||||
text={getBaseUrl() + appRoutes.lobby(lobby.id)}
|
||||
buttonProps={{ variant: "outline" }}
|
||||
>
|
||||
{isJoined ? (
|
||||
<>
|
||||
<UserPlus className="size-4" />
|
||||
<span>Invite Players</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Share2 className="size-4" />
|
||||
<span>Share Lobby</span>
|
||||
</>
|
||||
)}
|
||||
</CopyToClip>
|
||||
<div className="size-full h-screen pb-40">
|
||||
<LobbyProvider initialLobby={initialLobby}>
|
||||
<div className="mx-auto grid size-full max-w-4xl grid-cols-5 grid-rows-4 gap-4">
|
||||
<div className="container-bg col-span-3 row-span-3 size-full space-y-4 overflow-y-auto p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<CopyToClip
|
||||
target="invite link"
|
||||
text={getBaseUrl() + appRoutes.lobby(lobby.id)}
|
||||
buttonProps={{ variant: "outline" }}
|
||||
>
|
||||
{isJoined ? (
|
||||
<>
|
||||
<UserPlus className="size-4" />
|
||||
<span>Invite Players</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Share2 className="size-4" />
|
||||
<span>Share Lobby</span>
|
||||
</>
|
||||
)}
|
||||
</CopyToClip>
|
||||
|
||||
<div className="ml-auto">
|
||||
{sessionPlayer && (
|
||||
<LobbyMembershipDialog lobbyId={lobby.id} join={!isJoined} />
|
||||
)}
|
||||
<div className="ml-auto">
|
||||
{sessionPlayer && (
|
||||
<LobbyMembershipDialog lobbyId={lobby.id} join={!isJoined} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-between text-nowrap">
|
||||
<h1 className="w-full overflow-hidden text-2xl font-bold text-ellipsis">
|
||||
{lobby.name}
|
||||
</h1>
|
||||
{isAdmin && <LobbySettingsDialog lobby={lobby} />}
|
||||
</div>
|
||||
<div className="border-border/20 flex items-center justify-between border-b pb-4">
|
||||
<h2 className="text-sm font-medium">
|
||||
Players ({members?.length || 0}/{lobby.maxPlayers || 8})
|
||||
</h2>
|
||||
<span className="text-xs">
|
||||
{members?.length === (lobby.maxPlayers || 8)
|
||||
? "Lobby Full"
|
||||
: "Waiting..."}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-between text-nowrap">
|
||||
<h1 className="w-full overflow-hidden text-2xl font-bold text-ellipsis">
|
||||
{lobby.name}
|
||||
</h1>
|
||||
{isAdmin && <LobbySettingsDialog lobby={lobby} />}
|
||||
</div>
|
||||
<div className="border-border/20 flex items-center justify-between border-b pb-4">
|
||||
<h2 className="text-sm font-medium">
|
||||
Players ({members?.length || 0}/{lobby.maxPlayers || 8})
|
||||
</h2>
|
||||
<span className="text-xs">
|
||||
{members?.length === (lobby.maxPlayers || 8)
|
||||
? "Lobby Full"
|
||||
: "Waiting..."}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-2">
|
||||
{members?.map((member, idx) => (
|
||||
<li key={idx}>
|
||||
<LobbyPlayerCard
|
||||
lobbyId={lobby.id}
|
||||
highlight={member?.player?.id === sessionPlayer?.id}
|
||||
player={member?.player!}
|
||||
className="relative"
|
||||
showOptions={
|
||||
isAdmin && member?.playerId !== sessionPlayer?.id
|
||||
}
|
||||
>
|
||||
{member?.role === "admin" && (
|
||||
<Badge className="absolute -top-2 right-2 z-20 p-px px-2 text-white">
|
||||
Admin
|
||||
</Badge>
|
||||
)}
|
||||
</LobbyPlayerCard>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<ul className="space-y-2">
|
||||
{members?.map((member, idx) => (
|
||||
<li key={idx}>
|
||||
<LobbyPlayerCard
|
||||
lobbyId={lobby.id}
|
||||
highlight={member?.player?.id === sessionPlayer?.id}
|
||||
player={member?.player!}
|
||||
className="relative"
|
||||
showOptions={
|
||||
isAdmin && member?.playerId !== sessionPlayer?.id
|
||||
}
|
||||
>
|
||||
{member?.role === "admin" && (
|
||||
<Badge className="absolute -top-2 right-2 z-20 p-px px-2 text-white">
|
||||
Admin
|
||||
</Badge>
|
||||
)}
|
||||
</LobbyPlayerCard>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="container-bg col-span-2 row-span-3 size-full overflow-y-auto p-6">
|
||||
<GameConfigurator />
|
||||
</div>
|
||||
<div className="container-bg col-span-full h-max w-full p-4">
|
||||
<GameSelector />
|
||||
{/* {JSON.stringify(gameConfig)} */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="container-bg col-span-1 size-full">
|
||||
<GameConfigurator />
|
||||
</div>
|
||||
<div className="container-bg col-span-3 h-max w-full p-4">
|
||||
<GameSelector isAdmin={isAdmin} />
|
||||
</div>
|
||||
</div>
|
||||
</LobbyProvider>
|
||||
</LobbyProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import { getGame } from "@/game-engien/games";
|
||||
import { useSessionPlayerStore } from "@/lib/store/current-player-store";
|
||||
import { useGameStore } from "@/lib/store/game-store";
|
||||
import { useLobbyStore } from "@/lib/store/lobby-store";
|
||||
import type { GameConfig } from "@/lib/validations/game";
|
||||
import type { Lobby, LobbyMember } from "@/server/db/schema";
|
||||
import { api } from "@/trpc/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
@ -23,14 +24,16 @@ function LobbyProvider({
|
||||
const setMembers = useLobbyStore((state) => state.setMembers);
|
||||
const addMember = useLobbyStore((state) => state.addMember);
|
||||
const removeMember = useLobbyStore((state) => state.removeMember);
|
||||
|
||||
const setGameConfig = useGameStore((state) => state.setGameConfig);
|
||||
const setGameConfig = useGameStore((state) => state.updateGameConfig);
|
||||
const setSelectedGame = useGameStore((state) => state.setSelectedGame);
|
||||
const setIsLobbyAdmin = useSessionPlayerStore(
|
||||
(state) => state.setIsLobbyAdmin,
|
||||
);
|
||||
const selectedGame = useGameStore((state) => state.selectedGame);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const lobbyId = lobby.id ?? "";
|
||||
|
||||
api.lobby.onMemberUpdate.useSubscription(
|
||||
{ lobbyId },
|
||||
{
|
||||
@ -71,6 +74,8 @@ function LobbyProvider({
|
||||
{
|
||||
onData({ data: config }) {
|
||||
if (config) {
|
||||
console.log("Game Config Update", config);
|
||||
|
||||
setGameConfig(config);
|
||||
if (config.gameId !== selectedGame?.id) {
|
||||
const game = getGame(config.gameId!);
|
||||
@ -80,11 +85,17 @@ function LobbyProvider({
|
||||
},
|
||||
},
|
||||
);
|
||||
React.useEffect(() => {
|
||||
const isAdmin =
|
||||
initialLobby?.members?.find((m) => m.playerId === sessionPlayer?.id)
|
||||
?.role === "admin";
|
||||
setIsLobbyAdmin(isAdmin);
|
||||
}, [initialLobby, sessionPlayer, setIsLobbyAdmin]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const setConfigWithGame = () => {
|
||||
const initialConfig = initialLobby?.gameConfig?.config;
|
||||
setGameConfig(initialConfig ?? {});
|
||||
setGameConfig(initialConfig ?? ({} as GameConfig));
|
||||
const game = initialConfig?.gameId?.length
|
||||
? getGame(initialConfig?.gameId!)
|
||||
: undefined;
|
||||
@ -93,7 +104,7 @@ function LobbyProvider({
|
||||
if (initialLobby?.gameConfig?.config) setConfigWithGame();
|
||||
resetLobby(initialLobby);
|
||||
setMembers(initialLobby?.members ?? []);
|
||||
}, [initialLobby]);
|
||||
}, [initialLobby, setGameConfig, setSelectedGame, getGame]);
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
"use client";
|
||||
import { create } from "zustand";
|
||||
import type { Player } from "@/server/db/schema";
|
||||
|
||||
type SessionPlayerStore = {
|
||||
sessionPlayer?: Player | null;
|
||||
setSessionPlayer: (player?: Player | null) => void;
|
||||
isLobbyAdmin: boolean;
|
||||
setIsLobbyAdmin: (isAdmin: boolean) => void;
|
||||
};
|
||||
|
||||
export const useSessionPlayerStore = create<SessionPlayerStore>((set) => ({
|
||||
sessionPlayer: undefined,
|
||||
setSessionPlayer: (player) => set({ sessionPlayer: player }),
|
||||
isLobbyAdmin: false,
|
||||
setIsLobbyAdmin: (isAdmin) => set({ isLobbyAdmin: isAdmin }),
|
||||
}));
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
"use client";
|
||||
import { create } from "zustand";
|
||||
import type { IMinigame } from "@/game-engien";
|
||||
import type { GameConfig } from "../validations/game";
|
||||
@ -7,11 +8,14 @@ type GameStore = {
|
||||
setSelectedGame: (game?: IMinigame) => void;
|
||||
gameConfig: GameConfig;
|
||||
setGameConfig: (config: GameConfig) => void;
|
||||
updateGameConfig: (config: Partial<GameConfig>) => void;
|
||||
};
|
||||
|
||||
export const useGameStore = create<GameStore>((set) => ({
|
||||
export const useGameStore = create<GameStore>((set, get) => ({
|
||||
selectedGame: undefined,
|
||||
setSelectedGame: (game) => set({ selectedGame: game }),
|
||||
gameConfig: {} as GameConfig,
|
||||
setGameConfig: (config) => set({ gameConfig: config }),
|
||||
updateGameConfig: (config) =>
|
||||
set({ gameConfig: { ...get().gameConfig, ...config } }),
|
||||
}));
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { create } from "zustand";
|
||||
import type { Lobby, LobbyMember } from "@/server/db/schema";
|
||||
|
||||
|
||||
@ -14,8 +14,23 @@ export const formatDate = (date: Date) =>
|
||||
minute: "numeric",
|
||||
});
|
||||
|
||||
export function getBaseUrl() {
|
||||
export const getBaseUrl = () => {
|
||||
if (typeof window !== "undefined") return window.location.origin;
|
||||
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
|
||||
return `http://localhost:${process.env.PORT ?? 3000}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const debounce = <T extends (...args: any[]) => any>(
|
||||
callback: T,
|
||||
waitFor: number,
|
||||
) => {
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
return (...args: Parameters<T>): ReturnType<T> => {
|
||||
let result: any;
|
||||
timeout && clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
result = callback(...args);
|
||||
}, waitFor);
|
||||
return result;
|
||||
};
|
||||
};
|
||||
|
||||
@ -2,7 +2,8 @@ import { z } from "zod";
|
||||
|
||||
export const gameConfigPatchSchema = z.object({
|
||||
gameId: z.string().optional(),
|
||||
timeLimit: z.number().default(2).optional(),
|
||||
gameVariantId: z.string().optional(),
|
||||
timeLimit: z.number().min(2).max(120).default(2),
|
||||
scoreMultiplier: z.number().optional().optional(),
|
||||
allowHints: z.boolean().default(false).optional(),
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user