Allows Hunters to join in mid-Hunt
This commit is contained in:
@@ -30,6 +30,9 @@ export const apiGetHunt = (huntId: string) =>
|
|||||||
export const apiGetUnstartedHunts = () =>
|
export const apiGetUnstartedHunts = () =>
|
||||||
client.get<HuntResponse[]>('/hunt/unstarted')
|
client.get<HuntResponse[]>('/hunt/unstarted')
|
||||||
|
|
||||||
|
export const apiGetOngoingHunts = () =>
|
||||||
|
client.get<HuntResponse[]>('/hunt/ongoing')
|
||||||
|
|
||||||
export const apiGetAllHunts = (status?: 'UNSTARTED' | 'ONGOING' | 'CLOSED') =>
|
export const apiGetAllHunts = (status?: 'UNSTARTED' | 'ONGOING' | 'CLOSED') =>
|
||||||
client.get<HuntResponse[]>(`/hunt${status ? `?status=${status}` : ''}`)
|
client.get<HuntResponse[]>(`/hunt${status ? `?status=${status}` : ''}`)
|
||||||
|
|
||||||
@@ -41,9 +44,6 @@ export const apiUpdateHunt = (huntId: string, title: string, startDateTime: stri
|
|||||||
|
|
||||||
// ── Hunter ────────────────────────────────────────────────────────────────────
|
// ── Hunter ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const apiGetOngoingHunts = () =>
|
|
||||||
client.get<HuntResponse[]>('/hunter/hunt/ongoing')
|
|
||||||
|
|
||||||
export const apiGetHunterTeam = (huntId: string) =>
|
export const apiGetHunterTeam = (huntId: string) =>
|
||||||
client.get<TeamResponse>(`/hunter/hunt/${huntId}/team`)
|
client.get<TeamResponse>(`/hunter/hunt/${huntId}/team`)
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
import {huntStatus, formatDateTime} from '../../lib/utils'
|
import {huntStatus, formatDateTime} from '../../lib/utils'
|
||||||
import {
|
import {
|
||||||
apiCreateTeam,
|
apiCreateTeam,
|
||||||
apiGetHunterTeam,
|
|
||||||
apiGetOngoingHunts,
|
apiGetOngoingHunts,
|
||||||
|
apiGetHunterTeam,
|
||||||
apiGetUnstartedHunts,
|
apiGetUnstartedHunts,
|
||||||
apiGetTeamHunters,
|
apiGetTeamHunters,
|
||||||
apiJoinTeam,
|
apiJoinTeam,
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
let tab = $state<'active' | 'upcoming'>('active')
|
let tab = $state<'active' | 'upcoming'>('active')
|
||||||
|
|
||||||
// huntId → team the hunter has joined (null = not joined)
|
// huntId → team the hunter has joined (null = not joined)
|
||||||
|
let ongoingTeams = $state<Record<string, TeamResponse | null>>({})
|
||||||
let upcomingTeams = $state<Record<string, TeamResponse | null>>({})
|
let upcomingTeams = $state<Record<string, TeamResponse | null>>({})
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -36,14 +37,20 @@
|
|||||||
]).then(async ([o, u]) => {
|
]).then(async ([o, u]) => {
|
||||||
ongoing = o
|
ongoing = o
|
||||||
upcoming = u
|
upcoming = u
|
||||||
const teamEntries = await Promise.all(
|
const [ongoingEntries, upcomingEntries] = await Promise.all([
|
||||||
u.map(hunt =>
|
Promise.all(o.map(hunt =>
|
||||||
apiGetHunterTeam(hunt.id)
|
apiGetHunterTeam(hunt.id)
|
||||||
.then(t => [hunt.id, t] as const)
|
.then(t => [hunt.id, t] as const)
|
||||||
.catch(() => [hunt.id, null] as const)
|
.catch(() => [hunt.id, null] as const)
|
||||||
)
|
)),
|
||||||
)
|
Promise.all(u.map(hunt =>
|
||||||
upcomingTeams = Object.fromEntries(teamEntries)
|
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 => {
|
}).catch(e => {
|
||||||
error = e instanceof Error ? e.message : 'Failed to load hunts'
|
error = e instanceof Error ? e.message : 'Failed to load hunts'
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
@@ -51,9 +58,10 @@
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// ── Upcoming hunt sheet ──────────────────────────────────────────────────────
|
// ── Hunt sheet (shared for active + upcoming) ─────────────────────────────────
|
||||||
|
|
||||||
let sheetHunt = $state<HuntResponse | null>(null)
|
let sheetHunt = $state<HuntResponse | null>(null)
|
||||||
|
let sheetIsActive = $state(false)
|
||||||
let sheetTeams = $state<TeamResponse[]>([])
|
let sheetTeams = $state<TeamResponse[]>([])
|
||||||
let sheetMembers = $state<HunterSummaryResponse[]>([])
|
let sheetMembers = $state<HunterSummaryResponse[]>([])
|
||||||
let sheetLoading = $state(false)
|
let sheetLoading = $state(false)
|
||||||
@@ -62,14 +70,19 @@
|
|||||||
let joiningTeamId = $state<string | null>(null)
|
let joiningTeamId = $state<string | null>(null)
|
||||||
let sheetError = $state('')
|
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
|
sheetHunt = hunt
|
||||||
|
sheetIsActive = isActive
|
||||||
sheetError = ''
|
sheetError = ''
|
||||||
newTeamName = ''
|
newTeamName = ''
|
||||||
sheetMembers = []
|
sheetMembers = []
|
||||||
sheetLoading = true
|
sheetLoading = true
|
||||||
try {
|
try {
|
||||||
const myTeam = upcomingTeams[hunt.id]
|
const myTeam = currentTeam(hunt)
|
||||||
if (myTeam) {
|
if (myTeam) {
|
||||||
const [teams, members] = await Promise.all([
|
const [teams, members] = await Promise.all([
|
||||||
apiListTeams(hunt.id),
|
apiListTeams(hunt.id),
|
||||||
@@ -116,18 +129,20 @@
|
|||||||
try {
|
try {
|
||||||
await apiJoinTeam(sheetHunt.id, teamId)
|
await apiJoinTeam(sheetHunt.id, teamId)
|
||||||
const team = sheetTeams.find(t => t.id === teamId) ?? null
|
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 }
|
upcomingTeams = { ...upcomingTeams, [sheetHunt.id]: team }
|
||||||
newTeamName = ''
|
newTeamName = ''
|
||||||
closeSheet()
|
closeSheet()
|
||||||
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
sheetError = e instanceof Error ? e.message : 'Failed to join team'
|
sheetError = e instanceof Error ? e.message : 'Failed to join team'
|
||||||
} finally {
|
} finally {
|
||||||
joiningTeamId = null
|
joiningTeamId = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-4 pb-20">
|
<div class="p-4 pb-20">
|
||||||
@@ -162,9 +177,10 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
{#each ongoing as hunt}
|
{#each ongoing as hunt}
|
||||||
|
{@const myTeam = ongoingTeams[hunt.id]}
|
||||||
<button
|
<button
|
||||||
class="card bg-base-100 shadow-sm border border-base-200 text-left w-full hover:border-primary hover:shadow-md transition-all"
|
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="card-body p-4 gap-2">
|
||||||
<div class="flex items-start justify-between gap-2">
|
<div class="flex items-start justify-between gap-2">
|
||||||
@@ -174,6 +190,11 @@
|
|||||||
<p class="text-xs text-base-content/50">
|
<p class="text-xs text-base-content/50">
|
||||||
{formatDateTime(hunt.startDateTime)} – {formatDateTime(hunt.endDateTime)}
|
{formatDateTime(hunt.startDateTime)} – {formatDateTime(hunt.endDateTime)}
|
||||||
</p>
|
</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>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -192,7 +213,7 @@
|
|||||||
{@const myTeam = upcomingTeams[hunt.id]}
|
{@const myTeam = upcomingTeams[hunt.id]}
|
||||||
<button
|
<button
|
||||||
class="card bg-base-100 shadow-sm border border-base-200 text-left w-full hover:border-primary hover:shadow-md transition-all"
|
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="card-body p-4 gap-2">
|
||||||
<div class="flex items-start justify-between gap-2">
|
<div class="flex items-start justify-between gap-2">
|
||||||
@@ -216,15 +237,16 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Upcoming hunt sheet -->
|
<!-- Hunt sheet -->
|
||||||
{#if sheetHunt}
|
{#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 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="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 class="p-4 border-b border-base-200 flex items-center justify-between sticky top-0 bg-base-100">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-lg">{sheetHunt.title}</h3>
|
<h3 class="font-bold text-lg">{hunt.title}</h3>
|
||||||
<p class="text-xs text-base-content/50">{formatDateTime(sheetHunt.startDateTime)} – {formatDateTime(sheetHunt.endDateTime)}</p>
|
<p class="text-xs text-base-content/50">{formatDateTime(hunt.startDateTime)} – {formatDateTime(hunt.endDateTime)}</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-ghost btn-sm btn-circle" onclick={closeSheet}>✕</button>
|
<button class="btn btn-ghost btn-sm btn-circle" onclick={closeSheet}>✕</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -254,8 +276,16 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{#if sheetIsActive}
|
||||||
|
<button class="btn btn-primary w-full" onclick={() => push(`/hunt/${hunt.id}`)}>
|
||||||
|
Play Now →
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Join or create -->
|
<!-- 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">
|
<form onsubmit={(e) => { e.preventDefault(); createTeam() }} class="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
Reference in New Issue
Block a user