Adds display of Team members
This commit is contained in:
@@ -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) =>
|
||||||
|
|||||||
@@ -51,3 +51,8 @@ export interface HunterLeaderboardResponse {
|
|||||||
hunterName: string
|
hunterName: string
|
||||||
score: number
|
score: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HunterSummaryResponse {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user