Unstarted Hunts no longer go to the Items view for Hunters
This commit is contained in:
@@ -1,8 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {push} from 'svelte-spa-router'
|
import {push} from 'svelte-spa-router'
|
||||||
import {auth} from '../../lib/stores/auth.svelte'
|
import {auth} from '../../lib/stores/auth.svelte'
|
||||||
import {apiGetOngoingHunts, apiGetUnstartedHunts} from '../../lib/api/index'
|
import {
|
||||||
import type {HuntResponse} from '../../lib/api/types'
|
apiCreateTeam,
|
||||||
|
apiGetHunterTeam,
|
||||||
|
apiGetOngoingHunts,
|
||||||
|
apiGetUnstartedHunts,
|
||||||
|
apiJoinTeam,
|
||||||
|
apiListTeams,
|
||||||
|
} from '../../lib/api/index'
|
||||||
|
import type {HuntResponse, TeamResponse} from '../../lib/api/types'
|
||||||
import StatusBadge from '../../lib/components/StatusBadge.svelte'
|
import StatusBadge from '../../lib/components/StatusBadge.svelte'
|
||||||
import LoadingSpinner from '../../lib/components/LoadingSpinner.svelte'
|
import LoadingSpinner from '../../lib/components/LoadingSpinner.svelte'
|
||||||
|
|
||||||
@@ -17,13 +24,24 @@
|
|||||||
let error = $state('')
|
let error = $state('')
|
||||||
let tab = $state<'active' | 'upcoming'>('active')
|
let tab = $state<'active' | 'upcoming'>('active')
|
||||||
|
|
||||||
|
// huntId → team the hunter has joined (null = not joined)
|
||||||
|
let upcomingTeams = $state<Record<string, TeamResponse | null>>({})
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
apiGetOngoingHunts(),
|
apiGetOngoingHunts(),
|
||||||
apiGetUnstartedHunts(),
|
apiGetUnstartedHunts(),
|
||||||
]).then(([o, u]) => {
|
]).then(async ([o, u]) => {
|
||||||
ongoing = o
|
ongoing = o
|
||||||
upcoming = u
|
upcoming = u
|
||||||
|
const teamEntries = await Promise.all(
|
||||||
|
u.map(hunt =>
|
||||||
|
apiGetHunterTeam(hunt.id)
|
||||||
|
.then(t => [hunt.id, t] as const)
|
||||||
|
.catch(() => [hunt.id, null] as const)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
upcomingTeams = Object.fromEntries(teamEntries)
|
||||||
}).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(() => {
|
||||||
@@ -31,6 +49,71 @@
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ── Upcoming hunt sheet ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let sheetHunt = $state<HuntResponse | null>(null)
|
||||||
|
let sheetTeams = $state<TeamResponse[]>([])
|
||||||
|
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 = ''
|
||||||
|
sheetLoading = true
|
||||||
|
try {
|
||||||
|
sheetTeams = await apiListTeams(hunt.id)
|
||||||
|
} catch (e: unknown) {
|
||||||
|
sheetError = e instanceof Error ? e.message : 'Failed to load teams'
|
||||||
|
} finally {
|
||||||
|
sheetLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSheet() {
|
||||||
|
sheetHunt = null
|
||||||
|
sheetTeams = []
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTeam() {
|
||||||
|
if (!sheetHunt || !newTeamName.trim()) return
|
||||||
|
creatingTeam = true
|
||||||
|
sheetError = ''
|
||||||
|
try {
|
||||||
|
await apiCreateTeam(sheetHunt.id, newTeamName.trim())
|
||||||
|
sheetTeams = await apiListTeams(sheetHunt.id)
|
||||||
|
const created = sheetTeams.find(t => t.name === newTeamName.trim())
|
||||||
|
if (created) await joinTeam(created.id)
|
||||||
|
else newTeamName = ''
|
||||||
|
} catch (e: unknown) {
|
||||||
|
sheetError = e instanceof Error ? e.message : 'Failed to create team'
|
||||||
|
} finally {
|
||||||
|
creatingTeam = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function joinTeam(teamId: string) {
|
||||||
|
if (!sheetHunt) return
|
||||||
|
joiningTeamId = teamId
|
||||||
|
sheetError = ''
|
||||||
|
try {
|
||||||
|
await apiJoinTeam(sheetHunt.id, teamId)
|
||||||
|
const team = sheetTeams.find(t => t.id === teamId) ?? null
|
||||||
|
upcomingTeams = { ...upcomingTeams, [sheetHunt.id]: team }
|
||||||
|
newTeamName = ''
|
||||||
|
closeSheet()
|
||||||
|
} catch (e: unknown) {
|
||||||
|
sheetError = e instanceof Error ? e.message : 'Failed to join team'
|
||||||
|
} finally {
|
||||||
|
joiningTeamId = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function huntStatus(hunt: HuntResponse): 'ONGOING' | 'UNSTARTED' | 'CLOSED' {
|
function huntStatus(hunt: HuntResponse): 'ONGOING' | 'UNSTARTED' | 'CLOSED' {
|
||||||
if (hunt.isTerminated) return 'CLOSED'
|
if (hunt.isTerminated) return 'CLOSED'
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
@@ -69,15 +152,15 @@
|
|||||||
{:else if error}
|
{:else if error}
|
||||||
<div class="alert alert-error">{error}</div>
|
<div class="alert alert-error">{error}</div>
|
||||||
{:else}
|
{:else}
|
||||||
{@const hunts = tab === 'active' ? ongoing : upcoming}
|
{#if tab === 'active'}
|
||||||
{#if hunts.length === 0}
|
{#if ongoing.length === 0}
|
||||||
<div class="text-center py-16 text-base-content/50">
|
<div class="text-center py-16 text-base-content/50">
|
||||||
<p class="text-4xl mb-3">🔍</p>
|
<p class="text-4xl mb-3">🔍</p>
|
||||||
<p class="font-medium">{tab === 'active' ? 'No active hunts right now' : 'No upcoming hunts'}</p>
|
<p class="font-medium">No active hunts right now</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
{#each hunts as hunt}
|
{#each ongoing as hunt}
|
||||||
<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={() => push(`/hunt/${hunt.id}`)}
|
||||||
@@ -95,5 +178,108 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{:else}
|
||||||
|
{#if upcoming.length === 0}
|
||||||
|
<div class="text-center py-16 text-base-content/50">
|
||||||
|
<p class="text-4xl mb-3">🔍</p>
|
||||||
|
<p class="font-medium">No upcoming hunts</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
{#each upcoming as hunt}
|
||||||
|
{@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)}
|
||||||
|
>
|
||||||
|
<div class="card-body p-4 gap-2">
|
||||||
|
<div class="flex items-start justify-between gap-2">
|
||||||
|
<h2 class="card-title text-base">{hunt.title}</h2>
|
||||||
|
<StatusBadge status={huntStatus(hunt)} />
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-base-content/50">
|
||||||
|
{formatDate(hunt.startDateTime)} – {formatDate(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 a team →</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Upcoming hunt sheet -->
|
||||||
|
{#if sheetHunt}
|
||||||
|
{@const myTeam = upcomingTeams[sheetHunt.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">{formatDate(sheetHunt.startDateTime)} – {formatDate(sheetHunt.endDateTime)}</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-ghost btn-sm btn-circle" onclick={closeSheet}>✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 flex flex-col gap-4">
|
||||||
|
{#if sheetError}
|
||||||
|
<div class="alert alert-error text-sm">{sheetError}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if sheetLoading}
|
||||||
|
<LoadingSpinner />
|
||||||
|
{:else if myTeam}
|
||||||
|
<!-- Already on a team -->
|
||||||
|
<div class="rounded-xl border border-base-200 p-4">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-base-content/40 mb-1">Your Team</p>
|
||||||
|
<p class="text-lg font-bold">{myTeam.name}</p>
|
||||||
|
<!-- Team member list will be available once the backend exposes a members endpoint -->
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- Join or create -->
|
||||||
|
<form onsubmit={(e) => { e.preventDefault(); createTeam() }} class="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="New team name"
|
||||||
|
class="input input-bordered input-sm flex-1"
|
||||||
|
bind:value={newTeamName}
|
||||||
|
disabled={creatingTeam}
|
||||||
|
/>
|
||||||
|
<button type="submit" class="btn btn-secondary btn-sm" disabled={creatingTeam || !newTeamName.trim()}>
|
||||||
|
{#if creatingTeam}<span class="loading loading-spinner loading-xs"></span>{/if}
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{#if sheetTeams.length > 0}
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-base-content/40 mb-2">Or join an existing team</p>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#each sheetTeams as team}
|
||||||
|
<button
|
||||||
|
class="card bg-base-100 shadow-sm border border-base-200 text-left w-full hover:border-primary transition-colors"
|
||||||
|
onclick={() => joinTeam(team.id)}
|
||||||
|
disabled={joiningTeamId !== null}
|
||||||
|
>
|
||||||
|
<div class="card-body p-4 flex-row items-center justify-between">
|
||||||
|
<p class="font-semibold">{team.name}</p>
|
||||||
|
{#if joiningTeamId === team.id}
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user