Adds display of Team members

This commit is contained in:
2026-05-18 17:12:43 -05:00
parent 6e4edd96ce
commit 86866f4db7
4 changed files with 84 additions and 8 deletions

View File

@@ -1,6 +1,7 @@
import {BASE_URL, client} from './client' import {BASE_URL, client} from './client'
import type { import type {
HunterLeaderboardResponse, HunterLeaderboardResponse,
HunterSummaryResponse,
HuntResponse, HuntResponse,
ItemResponse, ItemResponse,
LoginResponse, LoginResponse,
@@ -60,6 +61,9 @@ export const apiCreateTeam = (huntId: string, name: string) =>
export const apiGetTeam = (huntId: string, teamId: string) => export const apiGetTeam = (huntId: string, teamId: string) =>
client.get<TeamResponse>(`/hunt/${huntId}/team/${teamId}`) client.get<TeamResponse>(`/hunt/${huntId}/team/${teamId}`)
export const apiGetTeamHunters = (huntId: string, teamId: string) =>
client.get<HunterSummaryResponse[]>(`/hunt/${huntId}/team/${teamId}/hunter`)
// ── Items ───────────────────────────────────────────────────────────────────── // ── Items ─────────────────────────────────────────────────────────────────────
export const apiGetItems = (huntId: string) => export const apiGetItems = (huntId: string) =>

View File

@@ -51,3 +51,8 @@ export interface HunterLeaderboardResponse {
hunterName: string hunterName: string
score: number score: number
} }
export interface HunterSummaryResponse {
id: string
name: string
}

View File

@@ -6,10 +6,11 @@
apiGetHunterTeam, apiGetHunterTeam,
apiGetOngoingHunts, apiGetOngoingHunts,
apiGetUnstartedHunts, apiGetUnstartedHunts,
apiGetTeamHunters,
apiJoinTeam, apiJoinTeam,
apiListTeams, apiListTeams,
} from '../../lib/api/index' } from '../../lib/api/index'
import type {HuntResponse, TeamResponse} from '../../lib/api/types' import type {HuntResponse, HunterSummaryResponse, 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'
@@ -53,6 +54,7 @@
let sheetHunt = $state<HuntResponse | null>(null) let sheetHunt = $state<HuntResponse | null>(null)
let sheetTeams = $state<TeamResponse[]>([]) let sheetTeams = $state<TeamResponse[]>([])
let sheetMembers = $state<HunterSummaryResponse[]>([])
let sheetLoading = $state(false) let sheetLoading = $state(false)
let newTeamName = $state('') let newTeamName = $state('')
let creatingTeam = $state(false) let creatingTeam = $state(false)
@@ -63,9 +65,20 @@
sheetHunt = hunt sheetHunt = hunt
sheetError = '' sheetError = ''
newTeamName = '' newTeamName = ''
sheetMembers = []
sheetLoading = true sheetLoading = true
try { try {
const myTeam = upcomingTeams[hunt.id]
if (myTeam) {
const [teams, members] = await Promise.all([
apiListTeams(hunt.id),
apiGetTeamHunters(hunt.id, myTeam.id),
])
sheetTeams = teams
sheetMembers = members
} else {
sheetTeams = await apiListTeams(hunt.id) sheetTeams = await apiListTeams(hunt.id)
}
} catch (e: unknown) { } catch (e: unknown) {
sheetError = e instanceof Error ? e.message : 'Failed to load teams' sheetError = e instanceof Error ? e.message : 'Failed to load teams'
} finally { } finally {
@@ -237,10 +250,21 @@
<LoadingSpinner /> <LoadingSpinner />
{:else if myTeam} {:else if myTeam}
<!-- Already on a team --> <!-- Already on a team -->
<div class="rounded-xl border border-base-200 p-4"> <div class="rounded-xl border border-base-200 p-4 flex flex-col gap-3">
<div>
<p class="text-xs font-semibold uppercase tracking-wide text-base-content/40 mb-1">Your Team</p> <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> <p class="text-lg font-bold">{myTeam.name}</p>
<!-- Team member list will be available once the backend exposes a members endpoint --> </div>
{#if sheetMembers.length > 0}
<div>
<p class="text-xs font-semibold uppercase tracking-wide text-base-content/40 mb-2">Members</p>
<div class="flex flex-col gap-1">
{#each sheetMembers as member}
<p class="text-sm font-medium">{member.name}</p>
{/each}
</div>
</div>
{/if}
</div> </div>
{:else} {:else}
<!-- Join or create --> <!-- Join or create -->

View File

@@ -8,13 +8,14 @@
apiGetHunterTeam, apiGetHunterTeam,
apiGetItemPhotos, apiGetItemPhotos,
apiGetItems, apiGetItems,
apiGetTeamHunters,
apiGetTeamItem, apiGetTeamItem,
apiJoinTeam, apiJoinTeam,
apiListTeams, apiListTeams,
apiRemovePhoto, apiRemovePhoto,
apiSubmitPhoto apiSubmitPhoto
} from '../../lib/api/index' } from '../../lib/api/index'
import type {HuntResponse, ItemResponse, PhotoResponse, TeamItemResponse, TeamResponse} from '../../lib/api/types' import type {HuntResponse, HunterSummaryResponse, ItemResponse, PhotoResponse, TeamItemResponse, TeamResponse} from '../../lib/api/types'
import StatusBadge from '../../lib/components/StatusBadge.svelte' import StatusBadge from '../../lib/components/StatusBadge.svelte'
import AuthImage from '../../lib/components/AuthImage.svelte' import AuthImage from '../../lib/components/AuthImage.svelte'
import LoadingSpinner from '../../lib/components/LoadingSpinner.svelte' import LoadingSpinner from '../../lib/components/LoadingSpinner.svelte'
@@ -174,6 +175,21 @@
const submitted = $derived(Object.values(itemStatuses).filter(s => s.itemFoundStatus === 'SUBMITTED').length) const submitted = $derived(Object.values(itemStatuses).filter(s => s.itemFoundStatus === 'SUBMITTED').length)
const rejected = $derived(Object.values(itemStatuses).filter(s => s.itemFoundStatus === 'REJECTED').length) const rejected = $derived(Object.values(itemStatuses).filter(s => s.itemFoundStatus === 'REJECTED').length)
let teamMembers = $state<HunterSummaryResponse[]>([])
let teamMembersLoading = $state(false)
let teamMembersOpen = $state(false)
async function openTeamMembers() {
if (!myTeam) return
teamMembersOpen = true
teamMembersLoading = true
try {
teamMembers = await apiGetTeamHunters(params.huntId, myTeam.id)
} finally {
teamMembersLoading = false
}
}
let searchQuery = $state('') let searchQuery = $state('')
let filterStatus = $state<string | null>(null) let filterStatus = $state<string | null>(null)
let filterPoints = $state<number | null>(null) let filterPoints = $state<number | null>(null)
@@ -228,7 +244,9 @@
<StatusBadge status={huntStatus(hunt)} /> <StatusBadge status={huntStatus(hunt)} />
</div> </div>
{#if myTeam} {#if myTeam}
<p class="text-sm text-base-content/50 mt-0.5">Team: <span class="font-medium text-base-content/70">{myTeam.name}</span></p> <button class="text-sm text-base-content/50 mt-0.5 text-left" onclick={openTeamMembers}>
Team: <span class="font-medium text-base-content/70 underline decoration-dotted underline-offset-2">{myTeam.name}</span>
</button>
{#if items.length > 0} {#if items.length > 0}
<div class="flex items-center gap-2 mt-2"> <div class="flex items-center gap-2 mt-2">
<div class="flex-1 h-2 rounded-full overflow-hidden bg-base-300 flex"> <div class="flex-1 h-2 rounded-full overflow-hidden bg-base-300 flex">
@@ -360,6 +378,31 @@
{/if} {/if}
</div> </div>
<!-- Team members popup -->
{#if teamMembersOpen && myTeam}
{@const team = myTeam}
<div class="fixed inset-0 z-50 flex items-center justify-center p-6" role="presentation" onclick={() => teamMembersOpen = false}>
<div class="bg-base-100 rounded-2xl shadow-2xl w-full max-w-sm p-5" role="dialog" aria-modal="true" onclick={(e) => e.stopPropagation()}>
<div class="flex items-center justify-between mb-4">
<div>
<p class="text-xs font-semibold uppercase tracking-wide text-base-content/40">Team</p>
<h3 class="font-bold text-lg leading-tight">{team.name}</h3>
</div>
<button class="btn btn-ghost btn-sm btn-circle" onclick={() => teamMembersOpen = false}>✕</button>
</div>
{#if teamMembersLoading}
<LoadingSpinner />
{:else}
<div class="flex flex-col gap-2">
{#each teamMembers as member}
<p class="text-sm font-medium py-1 border-b border-base-200 last:border-0">{member.name}</p>
{/each}
</div>
{/if}
</div>
</div>
{/if}
<!-- Photo lightbox --> <!-- Photo lightbox -->
{#if expandedPhoto} {#if expandedPhoto}
{@const photo = expandedPhoto} {@const photo = expandedPhoto}