Allows Hunters to join in mid-Hunt

This commit is contained in:
2026-05-18 22:14:24 -05:00
parent 1772081495
commit 46060fc150
2 changed files with 68 additions and 38 deletions

View File

@@ -30,6 +30,9 @@ export const apiGetHunt = (huntId: string) =>
export const apiGetUnstartedHunts = () =>
client.get<HuntResponse[]>('/hunt/unstarted')
export const apiGetOngoingHunts = () =>
client.get<HuntResponse[]>('/hunt/ongoing')
export const apiGetAllHunts = (status?: 'UNSTARTED' | 'ONGOING' | 'CLOSED') =>
client.get<HuntResponse[]>(`/hunt${status ? `?status=${status}` : ''}`)
@@ -41,9 +44,6 @@ export const apiUpdateHunt = (huntId: string, title: string, startDateTime: stri
// ── Hunter ────────────────────────────────────────────────────────────────────
export const apiGetOngoingHunts = () =>
client.get<HuntResponse[]>('/hunter/hunt/ongoing')
export const apiGetHunterTeam = (huntId: string) =>
client.get<TeamResponse>(`/hunter/hunt/${huntId}/team`)

View File

@@ -4,8 +4,8 @@
import {huntStatus, formatDateTime} from '../../lib/utils'
import {
apiCreateTeam,
apiGetHunterTeam,
apiGetOngoingHunts,
apiGetHunterTeam,
apiGetUnstartedHunts,
apiGetTeamHunters,
apiJoinTeam,
@@ -27,6 +27,7 @@
let tab = $state<'active' | 'upcoming'>('active')
// huntId → team the hunter has joined (null = not joined)
let ongoingTeams = $state<Record<string, TeamResponse | null>>({})
let upcomingTeams = $state<Record<string, TeamResponse | null>>({})
$effect(() => {
@@ -36,14 +37,20 @@
]).then(async ([o, u]) => {
ongoing = o
upcoming = u
const teamEntries = await Promise.all(
u.map(hunt =>
const [ongoingEntries, upcomingEntries] = await Promise.all([
Promise.all(o.map(hunt =>
apiGetHunterTeam(hunt.id)
.then(t => [hunt.id, t] as const)
.catch(() => [hunt.id, null] as const)
)
)
upcomingTeams = Object.fromEntries(teamEntries)
)),
Promise.all(u.map(hunt =>
apiGetHunterTeam(hunt.id)
.then(t => [hunt.id, t] as const)
.catch(() => [hunt.id, null] as const)
)),
])
ongoingTeams = Object.fromEntries(ongoingEntries)
upcomingTeams = Object.fromEntries(upcomingEntries)
}).catch(e => {
error = e instanceof Error ? e.message : 'Failed to load hunts'
}).finally(() => {
@@ -51,25 +58,31 @@
})
})
// ── Upcoming hunt sheet ──────────────────────────────────────────────────────
// ── Hunt sheet (shared for active + upcoming) ─────────────────────────────────
let sheetHunt = $state<HuntResponse | null>(null)
let sheetTeams = $state<TeamResponse[]>([])
let sheetMembers = $state<HunterSummaryResponse[]>([])
let sheetLoading = $state(false)
let newTeamName = $state('')
let creatingTeam = $state(false)
let joiningTeamId = $state<string | null>(null)
let sheetError = $state('')
let sheetHunt = $state<HuntResponse | null>(null)
let sheetIsActive = $state(false)
let sheetTeams = $state<TeamResponse[]>([])
let sheetMembers = $state<HunterSummaryResponse[]>([])
let sheetLoading = $state(false)
let newTeamName = $state('')
let creatingTeam = $state(false)
let joiningTeamId = $state<string | null>(null)
let sheetError = $state('')
async function openSheet(hunt: HuntResponse) {
sheetHunt = hunt
sheetError = ''
newTeamName = ''
sheetMembers = []
sheetLoading = true
function currentTeam(hunt: HuntResponse) {
return sheetIsActive ? ongoingTeams[hunt.id] : upcomingTeams[hunt.id]
}
async function openSheet(hunt: HuntResponse, isActive: boolean) {
sheetHunt = hunt
sheetIsActive = isActive
sheetError = ''
newTeamName = ''
sheetMembers = []
sheetLoading = true
try {
const myTeam = upcomingTeams[hunt.id]
const myTeam = currentTeam(hunt)
if (myTeam) {
const [teams, members] = await Promise.all([
apiListTeams(hunt.id),
@@ -88,7 +101,7 @@
}
function closeSheet() {
sheetHunt = null
sheetHunt = null
sheetTeams = []
}
@@ -116,18 +129,20 @@
try {
await apiJoinTeam(sheetHunt.id, teamId)
const team = sheetTeams.find(t => t.id === teamId) ?? null
upcomingTeams = { ...upcomingTeams, [sheetHunt.id]: team }
newTeamName = ''
closeSheet()
if (sheetIsActive) {
ongoingTeams = { ...ongoingTeams, [sheetHunt.id]: team }
push(`/hunt/${sheetHunt.id}`)
} else {
upcomingTeams = { ...upcomingTeams, [sheetHunt.id]: team }
newTeamName = ''
closeSheet()
}
} catch (e: unknown) {
sheetError = e instanceof Error ? e.message : 'Failed to join team'
} finally {
joiningTeamId = null
}
}
// ── Helpers ──────────────────────────────────────────────────────────────────
</script>
<div class="p-4 pb-20">
@@ -162,9 +177,10 @@
{:else}
<div class="flex flex-col gap-3">
{#each ongoing as hunt}
{@const myTeam = ongoingTeams[hunt.id]}
<button
class="card bg-base-100 shadow-sm border border-base-200 text-left w-full hover:border-primary hover:shadow-md transition-all"
onclick={() => push(`/hunt/${hunt.id}`)}
onclick={() => myTeam ? push(`/hunt/${hunt.id}`) : openSheet(hunt, true)}
>
<div class="card-body p-4 gap-2">
<div class="flex items-start justify-between gap-2">
@@ -174,6 +190,11 @@
<p class="text-xs text-base-content/50">
{formatDateTime(hunt.startDateTime)} {formatDateTime(hunt.endDateTime)}
</p>
{#if myTeam}
<p class="text-sm font-medium text-primary">Team: {myTeam.name}</p>
{:else}
<p class="text-sm font-medium text-secondary">Join to play →</p>
{/if}
</div>
</button>
{/each}
@@ -192,7 +213,7 @@
{@const myTeam = upcomingTeams[hunt.id]}
<button
class="card bg-base-100 shadow-sm border border-base-200 text-left w-full hover:border-primary hover:shadow-md transition-all"
onclick={() => openSheet(hunt)}
onclick={() => openSheet(hunt, false)}
>
<div class="card-body p-4 gap-2">
<div class="flex items-start justify-between gap-2">
@@ -216,15 +237,16 @@
{/if}
</div>
<!-- Upcoming hunt sheet -->
<!-- Hunt sheet -->
{#if sheetHunt}
{@const myTeam = upcomingTeams[sheetHunt.id]}
{@const hunt = sheetHunt}
{@const myTeam = sheetIsActive ? ongoingTeams[hunt.id] : upcomingTeams[hunt.id]}
<div class="fixed inset-0 z-40" onclick={closeSheet} role="presentation"></div>
<div class="fixed bottom-0 left-0 right-0 z-50 bg-base-100 rounded-t-2xl shadow-2xl max-h-[80vh] overflow-y-auto">
<div class="p-4 border-b border-base-200 flex items-center justify-between sticky top-0 bg-base-100">
<div>
<h3 class="font-bold text-lg">{sheetHunt.title}</h3>
<p class="text-xs text-base-content/50">{formatDateTime(sheetHunt.startDateTime)} {formatDateTime(sheetHunt.endDateTime)}</p>
<h3 class="font-bold text-lg">{hunt.title}</h3>
<p class="text-xs text-base-content/50">{formatDateTime(hunt.startDateTime)} {formatDateTime(hunt.endDateTime)}</p>
</div>
<button class="btn btn-ghost btn-sm btn-circle" onclick={closeSheet}>✕</button>
</div>
@@ -254,8 +276,16 @@
</div>
{/if}
</div>
{#if sheetIsActive}
<button class="btn btn-primary w-full" onclick={() => push(`/hunt/${hunt.id}`)}>
Play Now →
</button>
{/if}
{:else}
<!-- Join or create -->
{#if sheetIsActive}
<div class="alert alert-info text-sm">This hunt is already in progress — join a team to start playing!</div>
{/if}
<form onsubmit={(e) => { e.preventDefault(); createTeam() }} class="flex gap-2">
<input
type="text"