add dish UI components (player list, host controls, results)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
|||||||
|
import { useState } from "react"
|
||||||
|
import type { Country, Dish } from "@celebrate-esc/shared"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
|
||||||
|
interface DishHostProps {
|
||||||
|
dishes: Dish[]
|
||||||
|
countries: Country[]
|
||||||
|
onAddDish: (name: string, correctCountry: string) => void
|
||||||
|
onReveal: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DishHost({ dishes, countries, onAddDish, onReveal }: DishHostProps) {
|
||||||
|
const [name, setName] = useState("")
|
||||||
|
const [country, setCountry] = useState("")
|
||||||
|
const allRevealed = dishes.length > 0 && dishes.every((d) => d.revealed)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Dish of the Nation</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-3">
|
||||||
|
{!allRevealed && (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Input placeholder="Dish name" value={name} onChange={(e) => setName(e.target.value)} maxLength={100} />
|
||||||
|
<select
|
||||||
|
className="rounded-md border bg-background px-3 py-2 text-sm"
|
||||||
|
value={country}
|
||||||
|
onChange={(e) => setCountry(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Correct country...</option>
|
||||||
|
{countries.map((c) => (
|
||||||
|
<option key={c.code} value={c.code}>
|
||||||
|
{c.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
if (name.trim() && country) {
|
||||||
|
onAddDish(name.trim(), country)
|
||||||
|
setName("")
|
||||||
|
setCountry("")
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!name.trim() || !country}
|
||||||
|
>
|
||||||
|
Add Dish
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{dishes.length > 0 && (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<p className="text-sm font-medium">{dishes.length} dish(es) added:</p>
|
||||||
|
<ul className="text-sm text-muted-foreground">
|
||||||
|
{dishes.map((d) => (
|
||||||
|
<li key={d.id}>
|
||||||
|
{d.name} → {countries.find((c) => c.code === d.correctCountry)?.name ?? d.correctCountry}
|
||||||
|
{d.revealed && " (revealed)"}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{dishes.length > 0 && !allRevealed && (
|
||||||
|
<Button onClick={onReveal}>Reveal All Dishes</Button>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { useState } from "react"
|
||||||
|
import type { Country, Dish, DishGuess } from "@celebrate-esc/shared"
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
|
||||||
|
interface DishListProps {
|
||||||
|
dishes: Dish[]
|
||||||
|
myGuesses: DishGuess[]
|
||||||
|
countries: Country[]
|
||||||
|
onGuess: (dishId: string, guessedCountry: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DishList({ dishes, myGuesses, countries, onGuess }: DishListProps) {
|
||||||
|
if (dishes.length === 0) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-6 text-center text-muted-foreground">
|
||||||
|
No dishes yet — the host will add them.
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Dish of the Nation</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-3">
|
||||||
|
{dishes.map((dish) => (
|
||||||
|
<DishItem
|
||||||
|
key={dish.id}
|
||||||
|
dish={dish}
|
||||||
|
myGuess={myGuesses.find((g) => g.dishId === dish.id)}
|
||||||
|
countries={countries}
|
||||||
|
onGuess={onGuess}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DishItem({
|
||||||
|
dish,
|
||||||
|
myGuess,
|
||||||
|
countries,
|
||||||
|
onGuess,
|
||||||
|
}: {
|
||||||
|
dish: Dish
|
||||||
|
myGuess: DishGuess | undefined
|
||||||
|
countries: Country[]
|
||||||
|
onGuess: (dishId: string, guessedCountry: string) => void
|
||||||
|
}) {
|
||||||
|
const [selected, setSelected] = useState(myGuess?.guessedCountry ?? "")
|
||||||
|
|
||||||
|
if (dish.revealed) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border p-3">
|
||||||
|
<p className="font-medium">{dish.name}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Answer: {countries.find((c) => c.code === dish.correctCountry)?.name}
|
||||||
|
</p>
|
||||||
|
{myGuess && (
|
||||||
|
<p className={`text-sm ${myGuess.guessedCountry === dish.correctCountry ? "text-green-600" : "text-red-500"}`}>
|
||||||
|
Your guess: {countries.find((c) => c.code === myGuess.guessedCountry)?.name}
|
||||||
|
{myGuess.guessedCountry === dish.correctCountry ? " ✓" : " ✗"}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border p-3">
|
||||||
|
<p className="mb-2 font-medium">{dish.name}</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<select
|
||||||
|
className="flex-1 rounded-md border bg-background px-2 py-1 text-sm"
|
||||||
|
value={selected}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelected(e.target.value)
|
||||||
|
if (e.target.value) onGuess(dish.id, e.target.value)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">Guess a country...</option>
|
||||||
|
{countries.map((c) => (
|
||||||
|
<option key={c.code} value={c.code}>
|
||||||
|
{c.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{myGuess && (
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
Guessed: {countries.find((c) => c.code === myGuess.guessedCountry)?.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import type { GameState, Country } from "@celebrate-esc/shared"
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
|
||||||
|
interface DishResultsProps {
|
||||||
|
results: NonNullable<GameState["dishResults"]>
|
||||||
|
countries: Country[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DishResults({ results, countries }: DishResultsProps) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Dish Results</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
{results.map((r) => (
|
||||||
|
<div key={r.dish.id} className="rounded-md border p-3">
|
||||||
|
<p className="font-medium">{r.dish.name}</p>
|
||||||
|
<p className="mb-2 text-sm text-muted-foreground">
|
||||||
|
Answer: {countries.find((c) => c.code === r.dish.correctCountry)?.name}
|
||||||
|
</p>
|
||||||
|
{r.guesses.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">No guesses</p>
|
||||||
|
) : (
|
||||||
|
<ul className="text-sm">
|
||||||
|
{r.guesses.map((g) => (
|
||||||
|
<li key={g.playerId} className={g.correct ? "text-green-600" : "text-red-500"}>
|
||||||
|
{g.displayName}: {countries.find((c) => c.code === g.guessedCountry)?.name}
|
||||||
|
{g.correct ? " ✓" : " ✗"}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user