Adds ability to expand photos, and upload both from camera and from gallery

This commit is contained in:
2026-05-18 13:44:23 -05:00
parent 2ba8b60063
commit 67fe81dc41
2 changed files with 145 additions and 23 deletions

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import {push, router} from 'svelte-spa-router'
import {push} from 'svelte-spa-router'
import {auth} from '../../lib/stores/auth.svelte'
import {apiGetHunt, apiGetItemPhotos, apiGetItems, apiListTeams, apiReviewPhoto} from '../../lib/api/index'
import type {HuntResponse, ItemResponse, PhotoResponse, TeamResponse} from '../../lib/api/types'
@@ -7,7 +7,7 @@
import AuthImage from '../../lib/components/AuthImage.svelte'
import LoadingSpinner from '../../lib/components/LoadingSpinner.svelte'
let { params }: { params: { huntId: string } } = $props()
let { params }: { params: { huntId: string } } = $props()
$effect(() => {
if (!auth.isLoggedIn) push('/login')
@@ -25,6 +25,7 @@
let photosLoading = $state(false)
let error = $state('')
let reviewing = $state<string | null>(null)
let expandedPhoto = $state<PhotoWithTeam | null>(null)
$effect(() => {
const { huntId } = params
@@ -54,11 +55,13 @@
}
})
async function review(photo: PhotoWithTeam, status: 'APPROVED' | 'REJECTED') {
async function review(photo: PhotoWithTeam, status: 'APPROVED' | 'REJECTED' | 'REMOVED') {
reviewing = photo.id
try {
await apiReviewPhoto(photo.id, status)
photos = photos.map(p => p.id === photo.id ? { ...p, photoStatus: status } : p)
const updated = { ...photo, photoStatus: status } as PhotoWithTeam
photos = photos.map(p => p.id === photo.id ? updated : p)
if (expandedPhoto?.id === photo.id) expandedPhoto = updated
} catch (e: unknown) {
error = e instanceof Error ? e.message : 'Failed to review photo'
} finally {
@@ -69,6 +72,66 @@
const activePhotos = $derived(photos.filter(p => p.photoStatus !== 'REMOVED'))
</script>
<!-- Lightbox -->
{#if expandedPhoto}
{@const photo = expandedPhoto}
<div
class="fixed inset-0 z-50 bg-black/90 flex flex-col"
role="dialog"
aria-modal="true"
>
<div class="flex items-center justify-between px-4 py-3 shrink-0">
<div>
<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">{new Date(photo.photoUploadDateTime).toLocaleString()}</p>
</div>
<div class="flex items-center gap-3">
<StatusBadge status={photo.photoStatus} />
<button class="btn btn-ghost btn-sm btn-circle text-white" onclick={() => expandedPhoto = null}>✕</button>
</div>
</div>
<div class="flex-1 min-h-0 flex items-center justify-center px-4">
<AuthImage photoId={photo.id} version="LARGE" alt={photo.hunterName} class="max-h-full max-w-full object-contain rounded-lg" />
</div>
<div class="px-4 py-4 shrink-0">
{#if photo.photoStatus === 'SUBMITTED'}
<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-white/40" onclick={() => review(photo, 'REMOVED')} disabled={reviewing === photo.id}>
Remove
</button>
{:else if photo.photoStatus === 'APPROVED'}
<div class="flex gap-2">
<button class="btn btn-outline btn-error btn-sm flex-1" onclick={() => review(photo, 'REJECTED')} disabled={reviewing === photo.id}>
Revoke Approval
</button>
<button class="btn btn-ghost btn-sm text-white/40" onclick={() => review(photo, 'REMOVED')} disabled={reviewing === photo.id}>
Remove
</button>
</div>
{:else if photo.photoStatus === 'REJECTED'}
<div class="flex gap-2">
<button class="btn btn-outline btn-success btn-sm flex-1" onclick={() => review(photo, 'APPROVED')} disabled={reviewing === photo.id}>
Approve Instead
</button>
<button class="btn btn-ghost btn-sm text-white/40" onclick={() => review(photo, 'REMOVED')} disabled={reviewing === photo.id}>
Remove
</button>
</div>
{/if}
</div>
</div>
{/if}
<div class="p-4 pb-6">
<div class="flex items-center gap-3 mb-4">
<button class="btn btn-ghost btn-sm btn-circle" onclick={() => push(`/admin/hunt/${params.huntId}`)}>←</button>
@@ -110,7 +173,9 @@
<div class="flex flex-col gap-4">
{#each activePhotos as photo}
<div class="card bg-base-100 shadow-sm border border-base-200 overflow-hidden">
<AuthImage photoId={photo.id} version="LARGE" alt={photo.hunterName} class="w-full h-56 object-cover" />
<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">
<div class="flex items-center justify-between mb-3">
<div>