Allows Hunters to join in mid-Hunt
This commit is contained in:
@@ -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`)
|
||||
|
||||
|
||||
@@ -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,9 +58,10 @@
|
||||
})
|
||||
})
|
||||
|
||||
// ── Upcoming hunt sheet ──────────────────────────────────────────────────────
|
||||
// ── Hunt sheet (shared for active + upcoming) ─────────────────────────────────
|
||||
|
||||
let sheetHunt = $state<HuntResponse | null>(null)
|
||||
let sheetIsActive = $state(false)
|
||||
let sheetTeams = $state<TeamResponse[]>([])
|
||||
let sheetMembers = $state<HunterSummaryResponse[]>([])
|
||||
let sheetLoading = $state(false)
|
||||
@@ -62,14 +70,19 @@
|
||||
let joiningTeamId = $state<string | null>(null)
|
||||
let sheetError = $state('')
|
||||
|
||||
async function openSheet(hunt: HuntResponse) {
|
||||
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),
|
||||
@@ -116,18 +129,20 @@
|
||||
try {
|
||||
await apiJoinTeam(sheetHunt.id, teamId)
|
||||
const team = sheetTeams.find(t => t.id === teamId) ?? null
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user