Files
MulliganMates/components/round/round-lobby.tsx
T
2026-03-18 13:34:41 +01:00

234 lines
6.9 KiB
TypeScript

'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { startRound } from '@/app/actions/rounds'
import { inviteFromLobby } from '@/app/actions/invites'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { ChevronLeft, Plus, X } from 'lucide-react'
import Link from 'next/link'
type Player = {
id: string
role: string
handicapIndex: string | null
courseHandicap: number | null
profile: { id: string; displayName: string; avatarUrl: string | null } | null
}
type Invite = {
id: string
email: string
acceptedAt: Date | null
createdAt: Date
}
type Round = {
id: string
name: string | null
date: string
status: string
holesCount: number
createdBy: string
course: { name: string } | null
tee: { name: string; color: string } | null
}
export function RoundLobby({
round,
players,
pendingInvites,
currentUserId,
isCreator,
}: {
round: Round
players: Player[]
pendingInvites: Invite[]
currentUserId: string
isCreator: boolean
}) {
const router = useRouter()
const [emailInput, setEmailInput] = useState('')
const [inviteError, setInviteError] = useState<string | null>(null)
const [inviting, setInviting] = useState(false)
const [starting, setStarting] = useState(false)
const [startError, setStartError] = useState<string | null>(null)
async function handleInvite() {
const email = emailInput.trim().toLowerCase()
if (!email) return
setInviting(true)
setInviteError(null)
const result = await inviteFromLobby(round.id, email)
if (result.error) {
setInviteError(result.error)
} else {
setEmailInput('')
router.refresh()
}
setInviting(false)
}
async function handleStart() {
setStarting(true)
setStartError(null)
const result = await startRound(round.id)
if (result.error) {
setStartError(result.error)
setStarting(false)
} else {
router.refresh()
}
}
return (
<div className="flex min-h-screen flex-col">
{/* Header */}
<div className="flex items-center gap-3 border-b p-4">
<Link href="/dashboard">
<ChevronLeft className="h-5 w-5" />
</Link>
<div className="flex-1 min-w-0">
<h1 className="truncate font-semibold">
{round.name ?? round.course?.name ?? 'Round'}
</h1>
<p className="text-xs text-muted-foreground">
{round.course?.name} · {round.tee?.name} tees ·{' '}
{round.holesCount} holes ·{' '}
{new Date(round.date).toLocaleDateString()}
</p>
</div>
<span className="shrink-0 rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-800">
Lobby
</span>
</div>
<div className="flex-1 space-y-6 overflow-y-auto p-4">
{/* Players */}
<div className="space-y-3">
<h2 className="font-semibold">
Players ({players.length})
</h2>
<div className="space-y-2">
{players.map((p) => (
<div key={p.id} className="flex items-center gap-3 rounded-xl border p-3">
<Avatar
name={p.profile?.displayName ?? '?'}
url={p.profile?.avatarUrl}
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium truncate">
{p.profile?.displayName}
</span>
{p.profile?.id === currentUserId && (
<span className="text-xs text-muted-foreground">(you)</span>
)}
{p.role === 'admin' && (
<span className="rounded-full bg-muted px-1.5 py-0.5 text-xs">
creator
</span>
)}
</div>
<div className="text-xs text-muted-foreground">
{p.handicapIndex !== null
? `HCP ${p.handicapIndex} · Course HCP ${p.courseHandicap}`
: 'No handicap'}
</div>
</div>
</div>
))}
</div>
</div>
{/* Pending invites */}
{pendingInvites.length > 0 && (
<div className="space-y-2">
<h3 className="text-sm font-medium text-muted-foreground">
Awaiting ({pendingInvites.length})
</h3>
{pendingInvites.map((invite) => (
<div
key={invite.id}
className="flex items-center gap-2 rounded-xl border border-dashed px-3 py-2"
>
<X className="h-3 w-3 text-muted-foreground" />
<span className="text-sm text-muted-foreground">{invite.email}</span>
</div>
))}
</div>
)}
{/* Invite form (creator only, lobby only) */}
{isCreator && round.status === 'lobby' && (
<div className="space-y-2">
<h3 className="text-sm font-medium">Invite player</h3>
<div className="flex gap-2">
<Input
type="email"
value={emailInput}
onChange={(e) => setEmailInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleInvite()}
placeholder="player@example.com"
className="flex-1"
/>
<Button
variant="outline"
onClick={handleInvite}
disabled={!emailInput || inviting}
>
<Plus className="h-4 w-4" />
</Button>
</div>
{inviteError && (
<p className="text-xs text-destructive">{inviteError}</p>
)}
</div>
)}
{/* Start round button */}
{isCreator && round.status === 'lobby' && (
<div className="space-y-2">
{startError && (
<p className="text-sm text-destructive">{startError}</p>
)}
<Button
className="w-full"
disabled={players.length < 1 || starting}
onClick={handleStart}
>
{starting ? 'Starting…' : 'Start round'}
</Button>
</div>
)}
</div>
</div>
)
}
function Avatar({ name, url }: { name: string; url?: string | null }) {
const initials = name
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
.slice(0, 2)
if (url) {
return (
<img
src={url}
alt={name}
className="h-9 w-9 rounded-full object-cover"
/>
)
}
return (
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-semibold text-primary">
{initials}
</div>
)
}