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
5 changes: 2 additions & 3 deletions app/[lang]/game/local/_store/game-settings.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { atom } from 'jotai';
import type z from 'zod';
import type { localGameFormSchema } from '../setup/_components/form';
import type { LocalGameFormSchema } from '../setup/_components/form';

export const gameSettingsAtom = atom<z.infer<typeof localGameFormSchema>>({
export const gameSettingsAtom = atom<LocalGameFormSchema>({
players: [],
numberOfSpies: '0',
randomNumberOfSpies: false,
Expand Down
18 changes: 15 additions & 3 deletions app/[lang]/game/local/play/_components/game.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,28 @@

import { useAtomValue } from 'jotai';
import { type FC, useCallback, useMemo, useState } from 'react';
import type { Dictionary } from '@/dictionaries';
import type { Dictionary, Locale } from '@/dictionaries';
import { GameCard } from '@/game/_components/card';
import enWords from '@/word-bank/en.json';
import esWords from '@/word-bank/es.json';
import { gameSettingsAtom } from '../../_store/game-settings';
import { GameTimer } from './timer';

export const Game: FC<{ dict: Dictionary }> = ({ dict }) => {
export const Game: FC<{ dict: Dictionary; lang: Locale }> = ({
dict,
lang,
}) => {
const gameSettings = useAtomValue(gameSettingsAtom);
const [currentPlayerIndex, setCurrentPlayerIndex] = useState(0);
const [gameKey, setGameKey] = useState(0);

// Get random word based on language
const word = useMemo(() => {
const words = lang === 'es' ? esWords : enWords;
const randomIndex = Math.floor(Math.random() * words.length);
return words[randomIndex];
}, [lang]);

// Generate spy assignments
// biome-ignore lint/correctness/useExhaustiveDependencies: gameKey is intentionally used to trigger regeneration
const playerRoles = useMemo(() => {
Expand Down Expand Up @@ -72,7 +84,7 @@ export const Game: FC<{ dict: Dictionary }> = ({ dict }) => {

<GameCard
player={currentPlayer}
word="WORD"
word={word}
dict={dict}
onFinishCheck={() => setCurrentPlayerIndex(currentPlayerIndex + 1)}
/>
Expand Down
32 changes: 23 additions & 9 deletions app/[lang]/game/local/play/_components/timer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,14 @@ export const GameTimer: FC<GameTimerProps> = ({
}) => {
const [timeLeft, setTimeLeft] = useState(TIMER_DURATION);
const [isExpired, setIsExpired] = useState(false);
const [isStopped, setIsStopped] = useState(false);
const [showSpies, setShowSpies] = useState(false);

useEffect(() => {
if (isStopped) {
return;
}

if (timeLeft <= 0) {
setIsExpired(true);
return;
Expand All @@ -44,19 +49,21 @@ export const GameTimer: FC<GameTimerProps> = ({
}, 1000);

return () => clearInterval(interval);
}, [timeLeft]);
}, [timeLeft, isStopped]);

const minutes = Math.floor(timeLeft / 60);
const seconds = timeLeft % 60;
const formattedTime = `${minutes}:${seconds.toString().padStart(2, '0')}`;

const getTimerColor = () => {
if (isExpired) return 'text-destructive';
if (isExpired || isStopped) return 'text-destructive';
if (timeLeft <= 30) return 'text-destructive';
if (timeLeft <= 60) return 'text-warning';
return 'text-foreground';
};

const isFinished = isExpired || isStopped;

const spies = playerRoles.filter((player) => player.isSpy);

return (
Expand All @@ -65,7 +72,7 @@ export const GameTimer: FC<GameTimerProps> = ({
<div className="text-center">
<h2 className="mb-2 font-bold text-2xl">{dict.timer.title}</h2>
<p className="text-muted-foreground">
{isExpired ? dict.timer.timeUpDescription : dict.timer.description}
{isFinished ? dict.timer.timeUpDescription : dict.timer.description}
</p>
</div>

Expand All @@ -75,25 +82,32 @@ export const GameTimer: FC<GameTimerProps> = ({
getTimerColor(),
{
'animate-pulse border-destructive': isExpired,
'border-destructive': timeLeft <= 30 && !isExpired,
'border-warning': timeLeft > 30 && timeLeft <= 60,
'border-border': timeLeft > 60,
'border-destructive':
(timeLeft <= 30 && !isFinished) || isStopped,
'border-warning': timeLeft > 30 && timeLeft <= 60 && !isFinished,
'border-border': timeLeft > 60 && !isFinished,
},
)}>
<div className="text-center">
<div className={cn('font-bold text-6xl', getTimerColor())}>
{formattedTime}
</div>
{isExpired && (
{isFinished && (
<div className="mt-2 font-semibold text-xl">
{dict.timer.timeUp}
{isExpired ? dict.timer.timeUp : dict.timer.stopped}
</div>
)}
</div>
</div>

{!isFinished && (
<Button variant="outline" onClick={() => setIsStopped(true)}>
{dict.timer.stopTimer}
</Button>
)}
</div>

{isExpired && (
{isFinished && (
<div className="flex flex-col items-center gap-4">
{showSpies && (
<div className="rounded-lg border bg-card p-6 shadow-lg">
Expand Down
2 changes: 1 addition & 1 deletion app/[lang]/game/local/play/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const Page = async ({ params }: PageProps<'/[lang]/game/local/play'>) => {

const dict = await getDictionary(lang);

return <Game dict={dict} />;
return <Game dict={dict} lang={lang} />;
};

export default Page;
4 changes: 4 additions & 0 deletions app/[lang]/game/local/setup/_components/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ const createLocalGameFormSchema = (dict: Dictionary) =>
},
);

export type LocalGameFormSchema = z.infer<
ReturnType<typeof createLocalGameFormSchema>
>;

export const LocalUsersForm: FC<{ dict: Dictionary; lang: string }> = ({
dict,
lang,
Expand Down
9 changes: 5 additions & 4 deletions app/layout.tsx → app/[lang]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import './globals.css';
import { ThemeProvider } from './_providers/theme-provider';
import '../globals.css';
import { ThemeProvider } from '../_providers/theme-provider';

const geistSans = Geist({
variable: '--font-geist-sans',
Expand Down Expand Up @@ -38,9 +38,10 @@ export const metadata: Metadata = {
export default async function RootLayout({
children,
params,
}: LayoutProps<'/[lang]/game'>) {
}: LayoutProps<'/[lang]'>) {
const { lang } = await params;
return (
<html lang={(await params).lang} suppressHydrationWarning>
<html lang={lang} suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<ThemeProvider
Expand Down
4 changes: 3 additions & 1 deletion dictionaries/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@
"title": "Discussion Time",
"description": "Discuss and figure out who the spy is",
"timeUp": "Time's Up!",
"stopped": "Timer Stopped",
"timeUpDescription": "Time's up! Make your final guesses.",
"spiesWere": "The Spies Were:",
"hideSpies": "Hide Spies",
"revealSpies": "Reveal Spies",
"playAgain": "Play Again"
"playAgain": "Play Again",
"stopTimer": "Stop the timer"
},
"errors": {
"playerNameRequired": "Player name is required",
Expand Down
4 changes: 3 additions & 1 deletion dictionaries/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@
"title": "Tiempo de Discusión",
"description": "Discute y descubre quién es el espía",
"timeUp": "¡Se Acabó el Tiempo!",
"stopped": "Reloj Detenido",
"timeUpDescription": "¡Se acabó el tiempo! Haz tus últimas conjeturas.",
"spiesWere": "Los Espías Eran:",
"hideSpies": "Ocultar Espías",
"revealSpies": "Revelar Espías",
"playAgain": "Jugar de Nuevo"
"playAgain": "Jugar de Nuevo",
"stopTimer": "Detener el reloj"
},
"errors": {
"playerNameRequired": "El nombre del jugador es obligatorio",
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"@/game/*": ["./app/[lang]/game/*"],
"@/providers/*": ["./app/_providers/*"],
"@/primitives/*": ["./app/_primitives/*"],
"@/dictionaries": ["./app/[lang]/dictionaries.ts"]
"@/dictionaries": ["./app/[lang]/dictionaries.ts"],
"@/word-bank/*": ["./word-bank/*"]
}
},
"include": [
Expand Down
116 changes: 116 additions & 0 deletions word-bank/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
[
"Airport",
"American football",
"Amusement park",
"Arepa",
"Athletics",
"Avatar",
"Back to the Future",
"Backpack",
"Badminton",
"Bank",
"Basketball",
"Beach",
"Bear",
"Bread",
"Broom",
"Bus stop",
"Cat",
"Cereal",
"Charging cable",
"Cheese",
"Chocolate",
"Clock",
"Cycling",
"Dog",
"Dolphin",
"E.T.",
"Eagle",
"Elephant",
"Forrest Gump",
"French fries",
"Fried chicken",
"Frog",
"Fruit salad",
"Gladiator",
"Giraffe",
"Golf",
"Gym",
"Hamburger",
"Harry Potter",
"Headphones",
"Hockey",
"Hospital",
"Hotel",
"Ice cream",
"Inception",
"Jaws",
"Jurassic Park",
"Kangaroo",
"Key",
"Lamp",
"Library",
"Lion",
"Martial arts",
"Mirror",
"Monkey",
"Movie theater",
"Museum",
"Notebook",
"Owl",
"Panda",
"Park",
"Pasta",
"Pen",
"Penguin",
"Pharmacy",
"Pillow",
"Pizza",
"Pulp Fiction",
"Rabbit",
"Remote control",
"Restaurant",
"Rocky",
"Rugby",
"Salad",
"Sandwich",
"School",
"Scissors",
"Shark",
"Shrek",
"Shoes",
"Skiing",
"Skateboarding",
"Smoothie",
"Snake",
"Soccer",
"Soup",
"Spoon",
"Stadium",
"Star Wars",
"Steak",
"Supermarket",
"Surfing",
"Sushi",
"Table tennis",
"Taco",
"Tennis",
"The Avengers",
"The Dark Knight",
"The Godfather",
"The Lion King",
"The Matrix",
"Tiger",
"Titanic",
"Toothbrush",
"Towel",
"Train station",
"Umbrella",
"University",
"Volleyball",
"Wallet",
"Water bottle",
"Whale",
"Yogurt",
"Zoo"
]
Loading