Initial commit v1
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
'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
|
||||
handicap_index: number | null
|
||||
course_handicap: number | null
|
||||
profile: { id: string; display_name: string; avatar_url: string | null } | null
|
||||
}
|
||||
|
||||
type Invite = {
|
||||
id: string
|
||||
email: string
|
||||
accepted_at: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
type Round = {
|
||||
id: string
|
||||
name: string | null
|
||||
date: string
|
||||
status: string
|
||||
holes_count: number
|
||||
created_by: string
|
||||
course: { id: string; name: string; par: number } | null
|
||||
tee: { id: string; name: string; color: string; course_rating: number; slope_rating: number } | 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.holes_count} 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?.display_name ?? '?'}
|
||||
url={p.profile?.avatar_url}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium truncate">
|
||||
{p.profile?.display_name}
|
||||
</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.handicap_index !== null
|
||||
? `HCP ${p.handicap_index} · Course HCP ${p.course_handicap}`
|
||||
: '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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user