Compare commits
4 Commits
efab8b3083
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c6f1475b84 | |||
| 2c7cb00745 | |||
| ef8ab903ee | |||
| 44a5805b2b |
@@ -1,8 +1,30 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||||
<!-- Magnifying glass circle -->
|
<!-- Shield (navy blue, gold border) -->
|
||||||
<circle cx="26" cy="26" r="18" stroke="#14b8a6" stroke-width="5" fill="#ffffff"/>
|
<path d="M8,5 H56 V36 Q56,57 32,63 Q8,57 8,36 Z" fill="#1e3a8a"/>
|
||||||
<!-- Magnifying glass handle -->
|
<path d="M8,5 H56 V36 Q56,57 32,63 Q8,57 8,36 Z" fill="none" stroke="#d4a017" stroke-width="2.5"/>
|
||||||
<line x1="39" y1="39" x2="56" y2="56" stroke="#f97316" stroke-width="6" stroke-linecap="round"/>
|
|
||||||
<!-- SH text inside the lens -->
|
<!-- Bushy tail — drawn first so body sits on top of it -->
|
||||||
<text x="26" y="31" text-anchor="middle" font-family="Georgia, serif" font-size="15" font-weight="700" fill="#14b8a6">SH</text>
|
<path d="M30,44 C46,46 58,34 54,18 C50,6 36,6 32,16"
|
||||||
|
fill="none" stroke="#d4a017" stroke-width="11" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Body (upright) -->
|
||||||
|
<ellipse cx="24" cy="37" rx="7.5" ry="10.5" fill="#d4a017"/>
|
||||||
|
|
||||||
|
<!-- Head -->
|
||||||
|
<circle cx="24" cy="22" r="6.5" fill="#d4a017"/>
|
||||||
|
|
||||||
|
<!-- Ears -->
|
||||||
|
<polygon points="19,17 16,9 23,14" fill="#d4a017"/>
|
||||||
|
<polygon points="26,16 28,8 32,12" fill="#d4a017"/>
|
||||||
|
|
||||||
|
<!-- Eye -->
|
||||||
|
<circle cx="22" cy="21" r="1.5" fill="#1e3a8a"/>
|
||||||
|
|
||||||
|
<!-- Front paws raised (rearing up) -->
|
||||||
|
<line x1="18" y1="30" x2="12" y2="22" stroke="#d4a017" stroke-width="4.5" stroke-linecap="round"/>
|
||||||
|
<line x1="30" y1="28" x2="37" y2="20" stroke="#d4a017" stroke-width="4.5" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Hind legs -->
|
||||||
|
<line x1="18" y1="46" x2="16" y2="54" stroke="#d4a017" stroke-width="4.5" stroke-linecap="round"/>
|
||||||
|
<line x1="30" y1="47" x2="33" y2="54" stroke="#d4a017" stroke-width="4.5" stroke-linecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 498 B After Width: | Height: | Size: 1.3 KiB |
@@ -104,6 +104,9 @@ export function photoUrl(photoId: string, version: 'ORIGINAL' | 'LARGE' | 'MEDIU
|
|||||||
export const apiReviewPhoto = (photoId: string, status: 'SUBMITTED' | 'APPROVED' | 'REJECTED' | 'REMOVED') =>
|
export const apiReviewPhoto = (photoId: string, status: 'SUBMITTED' | 'APPROVED' | 'REJECTED' | 'REMOVED') =>
|
||||||
client.patch<void>(`/admin/photo/${photoId}`, { status })
|
client.patch<void>(`/admin/photo/${photoId}`, { status })
|
||||||
|
|
||||||
|
export const apiGetUnreviewedPhotos = (huntId: string) =>
|
||||||
|
client.get<PhotoResponse[]>(`/admin/hunt/${huntId}/photo/unreviewed`)
|
||||||
|
|
||||||
// ── Stats ─────────────────────────────────────────────────────────────────────
|
// ── Stats ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const apiGetTeamLeaderboard = (huntId: string) =>
|
export const apiGetTeamLeaderboard = (huntId: string) =>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
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 {huntStatus, formatDate} from '../../lib/utils'
|
import {huntStatus, formatDate} from '../../lib/utils'
|
||||||
import {apiGetAllHunts} from '../../lib/api/index'
|
import {apiGetAllHunts, apiGetUnreviewedPhotos} from '../../lib/api/index'
|
||||||
import type {HuntResponse} from '../../lib/api/types'
|
import type {HuntResponse} 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'
|
||||||
@@ -15,10 +15,21 @@
|
|||||||
let hunts = $state<HuntResponse[]>([])
|
let hunts = $state<HuntResponse[]>([])
|
||||||
let loading = $state(true)
|
let loading = $state(true)
|
||||||
let error = $state('')
|
let error = $state('')
|
||||||
|
let unreviewedCounts = $state<Record<string, number>>({})
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
apiGetAllHunts()
|
apiGetAllHunts()
|
||||||
.then(h => { hunts = h })
|
.then(async h => {
|
||||||
|
hunts = h
|
||||||
|
const counts = await Promise.all(
|
||||||
|
h.map(hunt =>
|
||||||
|
apiGetUnreviewedPhotos(hunt.id)
|
||||||
|
.then(photos => [hunt.id, photos.length] as const)
|
||||||
|
.catch(() => [hunt.id, 0] as const)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
unreviewedCounts = Object.fromEntries(counts)
|
||||||
|
})
|
||||||
.catch(e => { error = e instanceof Error ? e.message : 'Failed to load' })
|
.catch(e => { error = e instanceof Error ? e.message : 'Failed to load' })
|
||||||
.finally(() => { loading = false })
|
.finally(() => { loading = false })
|
||||||
})
|
})
|
||||||
@@ -58,6 +69,9 @@
|
|||||||
<div class="card-actions justify-end gap-2 mt-1">
|
<div class="card-actions justify-end gap-2 mt-1">
|
||||||
<button class="btn btn-outline btn-xs" onclick={() => push(`/admin/hunt/${hunt.id}/review`)}>
|
<button class="btn btn-outline btn-xs" onclick={() => push(`/admin/hunt/${hunt.id}/review`)}>
|
||||||
Review Photos
|
Review Photos
|
||||||
|
{#if (unreviewedCounts[hunt.id] ?? 0) > 0}
|
||||||
|
<span class="badge badge-error badge-sm">{unreviewedCounts[hunt.id]}</span>
|
||||||
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary btn-xs" onclick={() => push(`/admin/hunt/${hunt.id}`)}>
|
<button class="btn btn-primary btn-xs" onclick={() => push(`/admin/hunt/${hunt.id}`)}>
|
||||||
Manage
|
Manage
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<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 {apiGetHunt, apiGetItemPhotos, apiGetItems, apiListTeams, apiReviewPhoto} from '../../lib/api/index'
|
import {apiGetHunt, apiGetItemPhotos, apiGetItems, apiGetUnreviewedPhotos, apiListTeams, apiReviewPhoto} from '../../lib/api/index'
|
||||||
import {parseUTC} from '../../lib/utils'
|
import {parseUTC} from '../../lib/utils'
|
||||||
import type {HuntResponse, ItemResponse, PhotoResponse, TeamResponse} from '../../lib/api/types'
|
import type {HuntResponse, ItemResponse, PhotoResponse, TeamResponse} from '../../lib/api/types'
|
||||||
import StatusBadge from '../../lib/components/StatusBadge.svelte'
|
import StatusBadge from '../../lib/components/StatusBadge.svelte'
|
||||||
@@ -15,26 +15,42 @@
|
|||||||
if (!auth.isAdmin) push('/')
|
if (!auth.isAdmin) push('/')
|
||||||
})
|
})
|
||||||
|
|
||||||
type PhotoWithTeam = PhotoResponse & { teamName: string }
|
type PhotoWithContext = PhotoResponse & { teamName: string; itemName: string }
|
||||||
|
|
||||||
let hunt = $state<HuntResponse | null>(null)
|
let hunt = $state<HuntResponse | null>(null)
|
||||||
let teams = $state<TeamResponse[]>([])
|
let teams = $state<TeamResponse[]>([])
|
||||||
let items = $state<ItemResponse[]>([])
|
let items = $state<ItemResponse[]>([])
|
||||||
let selectedItem = $state<ItemResponse | null>(null)
|
let selectedItem = $state<ItemResponse | null>(null)
|
||||||
let photos = $state<PhotoWithTeam[]>([])
|
let photos = $state<PhotoWithContext[]>([])
|
||||||
|
let unreviewedPhotos = $state<PhotoWithContext[]>([])
|
||||||
let loading = $state(true)
|
let loading = $state(true)
|
||||||
let photosLoading = $state(false)
|
let photosLoading = $state(false)
|
||||||
let error = $state('')
|
let error = $state('')
|
||||||
let reviewing = $state<string | null>(null)
|
let reviewing = $state<string | null>(null)
|
||||||
let expandedPhoto = $state<PhotoWithTeam | null>(null)
|
let expandedPhoto = $state<PhotoWithContext | null>(null)
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const { huntId } = params
|
const { huntId } = params
|
||||||
Promise.all([apiGetHunt(huntId), apiListTeams(huntId), apiGetItems(huntId)])
|
Promise.all([apiGetHunt(huntId), apiListTeams(huntId), apiGetItems(huntId), apiGetUnreviewedPhotos(huntId)])
|
||||||
.then(([h, t, i]) => {
|
.then(async ([h, t, i, unreviewed]) => {
|
||||||
hunt = h
|
hunt = h
|
||||||
teams = t
|
teams = t
|
||||||
items = i
|
items = i
|
||||||
|
// Build photoId → context map from all item+team combos in parallel
|
||||||
|
const rows = await Promise.all(
|
||||||
|
i.flatMap(item =>
|
||||||
|
t.map(team =>
|
||||||
|
apiGetItemPhotos(huntId, team.id, item.id)
|
||||||
|
.then(ps => ps.map(p => [p.id, { itemName: item.name, teamName: team.name }] as [string, { itemName: string; teamName: string }]))
|
||||||
|
.catch(() => [] as [string, { itemName: string; teamName: string }][])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const contextMap = new Map(rows.flat())
|
||||||
|
unreviewedPhotos = unreviewed.map(p => {
|
||||||
|
const ctx = contextMap.get(p.id) ?? { itemName: 'Unknown', teamName: 'Unknown' }
|
||||||
|
return { ...p, ...ctx }
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.catch(e => { error = e instanceof Error ? e.message : 'Failed to load' })
|
.catch(e => { error = e instanceof Error ? e.message : 'Failed to load' })
|
||||||
.finally(() => { loading = false })
|
.finally(() => { loading = false })
|
||||||
@@ -47,7 +63,7 @@
|
|||||||
Promise.all(
|
Promise.all(
|
||||||
teams.map(team =>
|
teams.map(team =>
|
||||||
apiGetItemPhotos(params.huntId, team.id, selectedItem!.id)
|
apiGetItemPhotos(params.huntId, team.id, selectedItem!.id)
|
||||||
.then(ps => ps.map(p => ({ ...p, teamName: team.name })))
|
.then(ps => ps.map(p => ({ ...p, teamName: team.name, itemName: selectedItem!.name })))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.then(results => { photos = results.flat() })
|
.then(results => { photos = results.flat() })
|
||||||
@@ -56,12 +72,13 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
async function review(photo: PhotoWithTeam, status: 'APPROVED' | 'REJECTED' | 'REMOVED') {
|
async function review(photo: PhotoWithContext, status: 'APPROVED' | 'REJECTED' | 'REMOVED') {
|
||||||
reviewing = photo.id
|
reviewing = photo.id
|
||||||
try {
|
try {
|
||||||
await apiReviewPhoto(photo.id, status)
|
await apiReviewPhoto(photo.id, status)
|
||||||
const updated = { ...photo, photoStatus: status } as PhotoWithTeam
|
const updated = { ...photo, photoStatus: status } as PhotoWithContext
|
||||||
photos = photos.map(p => p.id === photo.id ? updated : p)
|
photos = photos.map(p => p.id === photo.id ? updated : p)
|
||||||
|
unreviewedPhotos = unreviewedPhotos.map(p => p.id === photo.id ? updated : p)
|
||||||
if (expandedPhoto?.id === photo.id) expandedPhoto = updated
|
if (expandedPhoto?.id === photo.id) expandedPhoto = updated
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
error = e instanceof Error ? e.message : 'Failed to review photo'
|
error = e instanceof Error ? e.message : 'Failed to review photo'
|
||||||
@@ -70,7 +87,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const activePhotos = $derived(photos.filter(p => p.photoStatus !== 'REMOVED'))
|
const activePhotos = $derived(photos.filter(p => p.photoStatus !== 'REMOVED'))
|
||||||
|
const activeUnreviewed = $derived(unreviewedPhotos.filter(p => p.photoStatus === 'SUBMITTED'))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Lightbox -->
|
<!-- Lightbox -->
|
||||||
@@ -83,6 +101,7 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-center justify-between px-4 py-3 shrink-0">
|
<div class="flex items-center justify-between px-4 py-3 shrink-0">
|
||||||
<div>
|
<div>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-white/40">{photo.itemName}</p>
|
||||||
<p class="font-semibold text-white">{photo.hunterName} <span class="text-white/50 font-normal">· {photo.teamName}</span></p>
|
<p class="font-semibold text-white">{photo.hunterName} <span class="text-white/50 font-normal">· {photo.teamName}</span></p>
|
||||||
<p class="text-xs text-white/40">{parseUTC(photo.photoUploadDateTime).toLocaleString()}</p>
|
<p class="text-xs text-white/40">{parseUTC(photo.photoUploadDateTime).toLocaleString()}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,14 +167,20 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<!-- Item selector -->
|
<!-- Item selector -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<p class="text-sm font-medium text-base-content/60 mb-2">Select Item</p>
|
<p class="text-sm font-medium text-base-content/60 mb-2">
|
||||||
|
{#if selectedItem}
|
||||||
|
Filtering by item — tap again to see all unreviewed
|
||||||
|
{:else}
|
||||||
|
Unreviewed ({activeUnreviewed.length})
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
{#each items as item}
|
{#each items as item}
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm"
|
class="btn btn-sm"
|
||||||
class:btn-secondary={selectedItem?.id === item.id}
|
class:btn-secondary={selectedItem?.id === item.id}
|
||||||
class:btn-outline={selectedItem?.id !== item.id}
|
class:btn-outline={selectedItem?.id !== item.id}
|
||||||
onclick={() => { selectedItem = item }}
|
onclick={() => { selectedItem = selectedItem?.id === item.id ? null : item }}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</button>
|
</button>
|
||||||
@@ -164,6 +189,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if selectedItem}
|
{#if selectedItem}
|
||||||
|
<!-- Per-item photo view -->
|
||||||
{#if photosLoading}
|
{#if photosLoading}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{:else if activePhotos.length === 0}
|
{:else if activePhotos.length === 0}
|
||||||
@@ -172,7 +198,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
{#each activePhotos as photo}
|
{#each activePhotos as photo (photo.id)}
|
||||||
<div class="card bg-base-100 shadow-sm border border-base-200 overflow-hidden">
|
<div class="card bg-base-100 shadow-sm border border-base-200 overflow-hidden">
|
||||||
<button class="w-full cursor-zoom-in" onclick={() => expandedPhoto = photo}>
|
<button class="w-full cursor-zoom-in" onclick={() => expandedPhoto = photo}>
|
||||||
<AuthImage photoId={photo.id} version="SMALL" alt={photo.hunterName} class="w-full h-56 object-cover" />
|
<AuthImage photoId={photo.id} version="SMALL" alt={photo.hunterName} class="w-full h-56 object-cover" />
|
||||||
@@ -189,19 +215,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if photo.photoStatus === 'SUBMITTED'}
|
{#if photo.photoStatus === 'SUBMITTED'}
|
||||||
<div class="flex gap-2 mb-2">
|
<div class="flex gap-2 mb-2">
|
||||||
<button
|
<button class="btn btn-success btn-sm flex-1" onclick={() => review(photo, 'APPROVED')} disabled={reviewing === photo.id}>
|
||||||
class="btn btn-success btn-sm flex-1"
|
|
||||||
onclick={() => review(photo, 'APPROVED')}
|
|
||||||
disabled={reviewing === photo.id}
|
|
||||||
>
|
|
||||||
{#if reviewing === photo.id}<span class="loading loading-spinner loading-xs"></span>{/if}
|
{#if reviewing === photo.id}<span class="loading loading-spinner loading-xs"></span>{/if}
|
||||||
Approve
|
Approve
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class="btn btn-error btn-sm flex-1" onclick={() => review(photo, 'REJECTED')} disabled={reviewing === photo.id}>
|
||||||
class="btn btn-error btn-sm flex-1"
|
|
||||||
onclick={() => review(photo, 'REJECTED')}
|
|
||||||
disabled={reviewing === photo.id}
|
|
||||||
>
|
|
||||||
Reject
|
Reject
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -232,6 +250,48 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{:else}
|
||||||
|
<!-- All-unreviewed view -->
|
||||||
|
{#if activeUnreviewed.length === 0}
|
||||||
|
<div class="text-center py-10 text-base-content/50">
|
||||||
|
<p>All caught up — no unreviewed photos!</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
{#each activeUnreviewed as photo (photo.id)}
|
||||||
|
<div class="card bg-base-100 shadow-sm border border-base-200 overflow-hidden">
|
||||||
|
<button class="w-full cursor-zoom-in" onclick={() => expandedPhoto = photo}>
|
||||||
|
<AuthImage photoId={photo.id} version="SMALL" alt={photo.hunterName} class="w-full h-56 object-cover" />
|
||||||
|
</button>
|
||||||
|
<div class="p-3">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-primary mb-1">{photo.itemName}</p>
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<p class="font-semibold">{photo.hunterName} <span class="text-base-content/50 font-normal">· {photo.teamName}</span></p>
|
||||||
|
<p class="text-xs text-base-content/50">
|
||||||
|
{parseUTC(photo.photoUploadDateTime).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<StatusBadge status={photo.photoStatus} />
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 mb-2">
|
||||||
|
<button class="btn btn-success btn-sm flex-1" onclick={() => review(photo, 'APPROVED')} disabled={reviewing === photo.id}>
|
||||||
|
{#if reviewing === photo.id}<span class="loading loading-spinner loading-xs"></span>{/if}
|
||||||
|
Approve
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-error btn-sm flex-1" onclick={() => review(photo, 'REJECTED')} disabled={reviewing === photo.id}>
|
||||||
|
Reject
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-ghost btn-sm w-full text-base-content/50" onclick={() => review(photo, 'REMOVED')} disabled={reviewing === photo.id}>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -242,8 +242,8 @@
|
|||||||
{@const hunt = sheetHunt}
|
{@const hunt = sheetHunt}
|
||||||
{@const myTeam = sheetIsActive ? ongoingTeams[hunt.id] : upcomingTeams[hunt.id]}
|
{@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-200 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-300 flex items-center justify-between sticky top-0 bg-base-200">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-lg">{hunt.title}</h3>
|
<h3 class="font-bold text-lg">{hunt.title}</h3>
|
||||||
<p class="text-xs text-base-content/50">{formatDateTime(hunt.startDateTime)} – {formatDateTime(hunt.endDateTime)}</p>
|
<p class="text-xs text-base-content/50">{formatDateTime(hunt.startDateTime)} – {formatDateTime(hunt.endDateTime)}</p>
|
||||||
@@ -260,7 +260,7 @@
|
|||||||
<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 flex flex-col gap-3">
|
<div class="rounded-xl bg-base-100 border border-base-300 p-4 flex flex-col gap-3">
|
||||||
<div>
|
<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>
|
||||||
|
|||||||
@@ -376,7 +376,7 @@
|
|||||||
{#if teamMembersOpen && myTeam}
|
{#if teamMembersOpen && myTeam}
|
||||||
{@const team = 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="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="bg-base-200 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 class="flex items-center justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-semibold uppercase tracking-wide text-base-content/40">Team</p>
|
<p class="text-xs font-semibold uppercase tracking-wide text-base-content/40">Team</p>
|
||||||
@@ -435,13 +435,13 @@
|
|||||||
<!-- Item detail bottom sheet -->
|
<!-- Item detail bottom sheet -->
|
||||||
{#if selectedItem}
|
{#if selectedItem}
|
||||||
<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-200 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-300 flex items-center justify-between sticky top-0 bg-base-200">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-lg">{selectedItem.name}</h3>
|
<h3 class="font-bold text-lg">{selectedItem.name}</h3>
|
||||||
<p class="text-xs text-base-content/50">{selectedItem.points} points</p>
|
<p class="text-xs text-base-content/50">{selectedItem.points} points</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-ghost btn-sm btn-circle" onclick={closeSheet}>✕</button>
|
<button class="btn btn-outline btn-sm" onclick={closeSheet}>Done</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4 flex flex-col gap-4">
|
<div class="p-4 flex flex-col gap-4">
|
||||||
{#if submitting}
|
{#if submitting}
|
||||||
@@ -494,9 +494,9 @@
|
|||||||
{:else if itemPhotos.filter(p => p.photoStatus !== 'REMOVED').length > 0}
|
{:else if itemPhotos.filter(p => p.photoStatus !== 'REMOVED').length > 0}
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
{#each itemPhotos.filter(p => p.photoStatus !== 'REMOVED') as photo (photo.id)}
|
{#each itemPhotos.filter(p => p.photoStatus !== 'REMOVED') as photo (photo.id)}
|
||||||
<div class="card bg-base-200 overflow-hidden">
|
<div class="card bg-base-100 overflow-hidden">
|
||||||
<button class="w-full cursor-zoom-in" onclick={() => expandedPhoto = photo}>
|
<button class="w-full cursor-zoom-in" onclick={() => expandedPhoto = photo}>
|
||||||
<AuthImage photoId={photo.id} version="SMALL" alt={photo.hunterName} class="w-full h-48 object-cover" />
|
<AuthImage photoId={photo.id} version="SMALL" alt={photo.hunterName} class="w-full max-h-64 object-contain bg-base-300" />
|
||||||
</button>
|
</button>
|
||||||
<div class="p-3 flex items-center justify-between">
|
<div class="p-3 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -513,6 +513,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<p class="text-center text-base-content/50 py-4 text-sm">No photos submitted yet</p>
|
<p class="text-center text-base-content/50 py-4 text-sm">No photos submitted yet</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user