First crack at the app. Still lots of bugs to squash.
This commit is contained in:
81
src/routes/admin/AdminHome.svelte
Normal file
81
src/routes/admin/AdminHome.svelte
Normal file
@@ -0,0 +1,81 @@
|
||||
<script lang="ts">
|
||||
import {push} from 'svelte-spa-router'
|
||||
import {auth} from '../../lib/stores/auth.svelte'
|
||||
import {apiGetAllHunts} from '../../lib/api/index'
|
||||
import type {HuntResponse} from '../../lib/api/types'
|
||||
import StatusBadge from '../../lib/components/StatusBadge.svelte'
|
||||
import LoadingSpinner from '../../lib/components/LoadingSpinner.svelte'
|
||||
|
||||
$effect(() => {
|
||||
if (!auth.isLoggedIn) push('/login')
|
||||
if (!auth.isAdmin) push('/')
|
||||
})
|
||||
|
||||
let hunts = $state<HuntResponse[]>([])
|
||||
let loading = $state(true)
|
||||
let error = $state('')
|
||||
|
||||
$effect(() => {
|
||||
apiGetAllHunts()
|
||||
.then(h => { hunts = h })
|
||||
.catch(e => { error = e instanceof Error ? e.message : 'Failed to load' })
|
||||
.finally(() => { loading = false })
|
||||
})
|
||||
|
||||
function huntStatus(h: HuntResponse): 'ONGOING' | 'UNSTARTED' | 'CLOSED' {
|
||||
if (h.isTerminated) return 'CLOSED'
|
||||
const now = Date.now()
|
||||
if (now < new Date(h.startDateTime).getTime()) return 'UNSTARTED'
|
||||
if (now > new Date(h.endDateTime).getTime()) return 'CLOSED'
|
||||
return 'ONGOING'
|
||||
}
|
||||
|
||||
function formatDate(dt: string) {
|
||||
return new Date(dt).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-4 pb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="text-xl font-bold">Hunts</h1>
|
||||
<button class="btn btn-primary btn-sm" onclick={() => push('/admin/hunt/create')}>
|
||||
+ New Hunt
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<LoadingSpinner />
|
||||
{:else if error}
|
||||
<div class="alert alert-error">{error}</div>
|
||||
{:else if hunts.length === 0}
|
||||
<div class="text-center py-16 text-base-content/50">
|
||||
<p class="text-4xl mb-3">📋</p>
|
||||
<p class="font-medium">No hunts yet</p>
|
||||
<button class="btn btn-primary mt-4" onclick={() => push('/admin/hunt/create')}>Create First Hunt</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each hunts as hunt}
|
||||
<div class="card bg-base-100 shadow-sm border border-base-200">
|
||||
<div class="card-body p-4 gap-2">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<h2 class="font-semibold">{hunt.title}</h2>
|
||||
<StatusBadge status={huntStatus(hunt)} />
|
||||
</div>
|
||||
<p class="text-xs text-base-content/50">
|
||||
{formatDate(hunt.startDateTime)} – {formatDate(hunt.endDateTime)}
|
||||
</p>
|
||||
<div class="card-actions justify-end gap-2 mt-1">
|
||||
<button class="btn btn-outline btn-xs" onclick={() => push(`/admin/hunt/${hunt.id}/review`)}>
|
||||
Review Photos
|
||||
</button>
|
||||
<button class="btn btn-primary btn-xs" onclick={() => push(`/admin/hunt/${hunt.id}`)}>
|
||||
Manage
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
70
src/routes/admin/HuntCreate.svelte
Normal file
70
src/routes/admin/HuntCreate.svelte
Normal file
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import {push} from 'svelte-spa-router'
|
||||
import {auth} from '../../lib/stores/auth.svelte'
|
||||
import {apiCreateHunt} from '../../lib/api/index'
|
||||
import DateTimePicker from '../../lib/components/DateTimePicker.svelte'
|
||||
|
||||
$effect(() => {
|
||||
if (!auth.isLoggedIn) push('/login')
|
||||
if (!auth.isAdmin) push('/')
|
||||
})
|
||||
|
||||
let title = $state('')
|
||||
let startDateTime = $state('')
|
||||
let endDateTime = $state('')
|
||||
let loading = $state(false)
|
||||
let error = $state('')
|
||||
|
||||
async function handleCreate() {
|
||||
if (!title) return
|
||||
loading = true
|
||||
error = ''
|
||||
try {
|
||||
const hunt = await apiCreateHunt(title, startDateTime, endDateTime)
|
||||
push(`/admin/hunt/${hunt.id}`)
|
||||
} catch (e: unknown) {
|
||||
error = e instanceof Error ? e.message : 'Failed to create hunt'
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-4">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<button class="btn btn-ghost btn-sm btn-circle" onclick={() => push('/admin')}>←</button>
|
||||
<h1 class="text-xl font-bold">New Hunt</h1>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="alert alert-error mb-4 text-sm">{error}</div>
|
||||
{/if}
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleCreate() }} class="flex flex-col gap-4">
|
||||
<label class="form-control">
|
||||
<div class="label"><span class="label-text font-medium">Hunt Title</span></div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. Summer Scavenger Hunt 2026"
|
||||
class="input input-bordered"
|
||||
bind:value={title}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
|
||||
<fieldset class="form-control">
|
||||
<legend class="label"><span class="label-text font-medium">Start</span></legend>
|
||||
<DateTimePicker bind:value={startDateTime} defaultTimeIndex={36} />
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="form-control">
|
||||
<legend class="label"><span class="label-text font-medium">End</span></legend>
|
||||
<DateTimePicker bind:value={endDateTime} defaultTimeIndex={68} />
|
||||
</fieldset>
|
||||
|
||||
<button type="submit" class="btn btn-primary mt-2" disabled={loading}>
|
||||
{#if loading}<span class="loading loading-spinner loading-sm"></span>{/if}
|
||||
Create Hunt
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
253
src/routes/admin/HuntManage.svelte
Normal file
253
src/routes/admin/HuntManage.svelte
Normal file
@@ -0,0 +1,253 @@
|
||||
<script lang="ts">
|
||||
import {push} from 'svelte-spa-router'
|
||||
import {auth} from '../../lib/stores/auth.svelte'
|
||||
import {
|
||||
apiAddItem,
|
||||
apiCreateTeam,
|
||||
apiDeleteItem,
|
||||
apiGetHunt,
|
||||
apiGetItems,
|
||||
apiListTeams,
|
||||
apiUpdateItem
|
||||
} from '../../lib/api/index'
|
||||
import type {HuntResponse, ItemResponse, TeamResponse} from '../../lib/api/types'
|
||||
import StatusBadge from '../../lib/components/StatusBadge.svelte'
|
||||
import LoadingSpinner from '../../lib/components/LoadingSpinner.svelte'
|
||||
|
||||
let { params }: { params: { huntId: string } } = $props()
|
||||
|
||||
$effect(() => {
|
||||
if (!auth.isLoggedIn) push('/login')
|
||||
if (!auth.isAdmin) push('/')
|
||||
})
|
||||
|
||||
let hunt = $state<HuntResponse | null>(null)
|
||||
let items = $state<ItemResponse[]>([])
|
||||
let teams = $state<TeamResponse[]>([])
|
||||
let loading = $state(true)
|
||||
let error = $state('')
|
||||
|
||||
let newItemName = $state('')
|
||||
let newItemPoints = $state(10)
|
||||
let addingItem = $state(false)
|
||||
|
||||
let newTeamName = $state('')
|
||||
let addingTeam = $state(false)
|
||||
|
||||
let editingItemId = $state<string | null>(null)
|
||||
let editName = $state('')
|
||||
let editPoints = $state(0)
|
||||
let savingItem = $state(false)
|
||||
|
||||
function startEdit(item: ItemResponse) {
|
||||
editingItemId = item.id
|
||||
editName = item.name
|
||||
editPoints = item.points
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editingItemId = null
|
||||
}
|
||||
|
||||
let deletingItemId = $state<string | null>(null)
|
||||
|
||||
async function deleteItem(item: ItemResponse) {
|
||||
if (!confirm(`Delete "${item.name}"? This cannot be undone.`)) return
|
||||
deletingItemId = item.id
|
||||
try {
|
||||
await apiDeleteItem(params.huntId, item.id)
|
||||
items = items.filter(i => i.id !== item.id)
|
||||
} catch (e: unknown) {
|
||||
error = e instanceof Error ? e.message : 'Failed to delete item'
|
||||
} finally {
|
||||
deletingItemId = null
|
||||
}
|
||||
}
|
||||
|
||||
async function saveItem(item: ItemResponse) {
|
||||
if (!editName.trim() || editPoints < 1) return
|
||||
savingItem = true
|
||||
try {
|
||||
const updated = await apiUpdateItem(params.huntId, item.id, editName.trim(), editPoints)
|
||||
items = items.map(i => i.id === updated.id ? updated : i)
|
||||
editingItemId = null
|
||||
} catch (e: unknown) {
|
||||
error = e instanceof Error ? e.message : 'Failed to update item'
|
||||
} finally {
|
||||
savingItem = false
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const { huntId } = params
|
||||
Promise.all([apiGetHunt(huntId), apiGetItems(huntId), apiListTeams(huntId)])
|
||||
.then(([h, i, t]) => { hunt = h; items = i; teams = t })
|
||||
.catch(e => { error = e instanceof Error ? e.message : 'Failed to load' })
|
||||
.finally(() => { loading = false })
|
||||
})
|
||||
|
||||
async function addItem() {
|
||||
if (!newItemName || newItemPoints < 1) return
|
||||
addingItem = true
|
||||
try {
|
||||
const item = await apiAddItem(params.huntId, newItemName, newItemPoints)
|
||||
items = [...items, item]
|
||||
newItemName = ''
|
||||
newItemPoints = 10
|
||||
} catch (e: unknown) {
|
||||
error = e instanceof Error ? e.message : 'Failed to add item'
|
||||
} finally {
|
||||
addingItem = false
|
||||
}
|
||||
}
|
||||
|
||||
async function addTeam() {
|
||||
if (!newTeamName) return
|
||||
addingTeam = true
|
||||
try {
|
||||
await apiCreateTeam(params.huntId, newTeamName)
|
||||
teams = await apiListTeams(params.huntId)
|
||||
newTeamName = ''
|
||||
} catch (e: unknown) {
|
||||
error = e instanceof Error ? e.message : 'Failed to create team'
|
||||
} finally {
|
||||
addingTeam = false
|
||||
}
|
||||
}
|
||||
|
||||
function huntStatus(h: HuntResponse): 'ONGOING' | 'UNSTARTED' | 'CLOSED' {
|
||||
if (h.isTerminated) return 'CLOSED'
|
||||
const now = Date.now()
|
||||
if (now < new Date(h.startDateTime).getTime()) return 'UNSTARTED'
|
||||
if (now > new Date(h.endDateTime).getTime()) return 'CLOSED'
|
||||
return 'ONGOING'
|
||||
}
|
||||
</script>
|
||||
|
||||
<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')}>←</button>
|
||||
<div>
|
||||
{#if hunt}
|
||||
<h1 class="text-lg font-bold leading-tight">{hunt.title}</h1>
|
||||
<StatusBadge status={huntStatus(hunt)} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<LoadingSpinner />
|
||||
{:else}
|
||||
{#if error}
|
||||
<div class="alert alert-error mb-4 text-sm">{error}</div>
|
||||
{/if}
|
||||
|
||||
<!-- Items -->
|
||||
<section class="mb-6">
|
||||
<h2 class="font-bold text-base mb-3">Items ({items.length})</h2>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); addItem() }} class="flex gap-2 mb-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Item name"
|
||||
class="input input-bordered input-sm flex-1"
|
||||
bind:value={newItemName}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="pts"
|
||||
class="input input-bordered input-sm w-20"
|
||||
bind:value={newItemPoints}
|
||||
min="1"
|
||||
required
|
||||
/>
|
||||
<button type="submit" class="btn btn-primary btn-sm" disabled={addingItem}>
|
||||
{#if addingItem}<span class="loading loading-spinner loading-xs"></span>{:else}Add{/if}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each items as item}
|
||||
<div class="bg-base-100 rounded-xl shadow-sm border border-base-200 px-4 py-3">
|
||||
{#if editingItemId === item.id}
|
||||
<form onsubmit={(e) => { e.preventDefault(); saveItem(item) }} class="flex gap-2 items-center">
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered input-sm flex-1"
|
||||
bind:value={editName}
|
||||
required
|
||||
disabled={savingItem}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
class="input input-bordered input-sm w-20"
|
||||
bind:value={editPoints}
|
||||
min="1"
|
||||
required
|
||||
disabled={savingItem}
|
||||
/>
|
||||
<button type="submit" class="btn btn-primary btn-sm" disabled={savingItem}>
|
||||
{#if savingItem}<span class="loading loading-spinner loading-xs"></span>{:else}Save{/if}
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost btn-sm" onclick={cancelEdit} disabled={savingItem}>✕</button>
|
||||
</form>
|
||||
{:else}
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium">{item.name}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="badge badge-outline">{item.points} pts</span>
|
||||
<button class="btn btn-ghost btn-xs" onclick={() => startEdit(item)} disabled={deletingItemId === item.id}>Edit</button>
|
||||
<button class="btn btn-ghost btn-xs text-error" onclick={() => deleteItem(item)} disabled={deletingItemId === item.id}>
|
||||
{#if deletingItemId === item.id}<span class="loading loading-spinner loading-xs"></span>{:else}Delete{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if items.length === 0}
|
||||
<p class="text-base-content/50 text-sm text-center py-4">No items yet — add one above</p>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Teams -->
|
||||
<section>
|
||||
<h2 class="font-bold text-base mb-3">Teams ({teams.length})</h2>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); addTeam() }} class="flex gap-2 mb-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Team name"
|
||||
class="input input-bordered input-sm flex-1"
|
||||
bind:value={newTeamName}
|
||||
required
|
||||
/>
|
||||
<button type="submit" class="btn btn-secondary btn-sm" disabled={addingTeam}>
|
||||
{#if addingTeam}<span class="loading loading-spinner loading-xs"></span>{:else}Add{/if}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each teams as team}
|
||||
<div class="flex items-center justify-between bg-base-100 rounded-xl px-4 py-3 shadow-sm border border-base-200">
|
||||
<span class="font-medium">{team.name}</span>
|
||||
<button class="btn btn-ghost btn-xs" onclick={() => push(`/admin/hunt/${params.huntId}/review?teamId=${team.id}`)}>
|
||||
Review →
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{#if teams.length === 0}
|
||||
<p class="text-base-content/50 text-sm text-center py-4">No teams yet — add one above</p>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="mt-6">
|
||||
<button class="btn btn-outline w-full" onclick={() => push(`/admin/hunt/${params.huntId}/review`)}>
|
||||
Review All Photos →
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
177
src/routes/admin/PhotoReview.svelte
Normal file
177
src/routes/admin/PhotoReview.svelte
Normal file
@@ -0,0 +1,177 @@
|
||||
<script lang="ts">
|
||||
import {push, router} 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'
|
||||
import StatusBadge from '../../lib/components/StatusBadge.svelte'
|
||||
import AuthImage from '../../lib/components/AuthImage.svelte'
|
||||
import LoadingSpinner from '../../lib/components/LoadingSpinner.svelte'
|
||||
|
||||
let { params }: { params: { huntId: string } } = $props()
|
||||
|
||||
$effect(() => {
|
||||
if (!auth.isLoggedIn) push('/login')
|
||||
if (!auth.isAdmin) push('/')
|
||||
})
|
||||
|
||||
let hunt = $state<HuntResponse | null>(null)
|
||||
let teams = $state<TeamResponse[]>([])
|
||||
let items = $state<ItemResponse[]>([])
|
||||
let selectedTeam = $state<TeamResponse | null>(null)
|
||||
let selectedItem = $state<ItemResponse | null>(null)
|
||||
let photos = $state<PhotoResponse[]>([])
|
||||
let loading = $state(true)
|
||||
let photosLoading = $state(false)
|
||||
let error = $state('')
|
||||
let reviewing = $state<string | null>(null)
|
||||
|
||||
$effect(() => {
|
||||
const { huntId } = params
|
||||
Promise.all([apiGetHunt(huntId), apiListTeams(huntId), apiGetItems(huntId)])
|
||||
.then(([h, t, i]) => {
|
||||
hunt = h
|
||||
teams = t
|
||||
items = i
|
||||
// Pre-select team from query string if provided
|
||||
const qs = new URLSearchParams(router.querystring ?? '')
|
||||
const preTeam = qs.get('teamId')
|
||||
if (preTeam) {
|
||||
selectedTeam = t.find(x => x.id === preTeam) ?? null
|
||||
}
|
||||
})
|
||||
.catch(e => { error = e instanceof Error ? e.message : 'Failed to load' })
|
||||
.finally(() => { loading = false })
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (selectedTeam && selectedItem) {
|
||||
photosLoading = true
|
||||
photos = []
|
||||
apiGetItemPhotos(params.huntId, selectedTeam.id, selectedItem.id)
|
||||
.then(p => { photos = p })
|
||||
.catch(e => { error = e instanceof Error ? e.message : 'Failed to load photos' })
|
||||
.finally(() => { photosLoading = false })
|
||||
}
|
||||
})
|
||||
|
||||
async function review(photo: PhotoResponse, status: 'APPROVED' | 'REJECTED') {
|
||||
reviewing = photo.id
|
||||
try {
|
||||
await apiReviewPhoto(photo.id, status)
|
||||
photos = photos.map(p => p.id === photo.id ? { ...p, photoStatus: status } : p)
|
||||
} catch (e: unknown) {
|
||||
error = e instanceof Error ? e.message : 'Failed to review photo'
|
||||
} finally {
|
||||
reviewing = null
|
||||
}
|
||||
}
|
||||
|
||||
const activePhotos = $derived(photos.filter(p => p.photoStatus !== 'REMOVED'))
|
||||
</script>
|
||||
|
||||
<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>
|
||||
<h1 class="text-lg font-bold">{hunt?.title ?? 'Photo Review'}</h1>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="alert alert-error mb-4 text-sm">{error}</div>
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<LoadingSpinner />
|
||||
{:else}
|
||||
<!-- Team selector -->
|
||||
<div class="mb-4">
|
||||
<p class="text-sm font-medium text-base-content/60 mb-2">Select Team</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each teams as team}
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
class:btn-primary={selectedTeam?.id === team.id}
|
||||
class:btn-outline={selectedTeam?.id !== team.id}
|
||||
onclick={() => { selectedTeam = team; selectedItem = null; photos = [] }}
|
||||
>
|
||||
{team.name}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if selectedTeam}
|
||||
<!-- Item selector -->
|
||||
<div class="mb-4">
|
||||
<p class="text-sm font-medium text-base-content/60 mb-2">Select Item</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each items as item}
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
class:btn-secondary={selectedItem?.id === item.id}
|
||||
class:btn-outline={selectedItem?.id !== item.id}
|
||||
onclick={() => { selectedItem = item }}
|
||||
>
|
||||
{item.name}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selectedTeam && selectedItem}
|
||||
{#if photosLoading}
|
||||
<LoadingSpinner />
|
||||
{:else if activePhotos.length === 0}
|
||||
<div class="text-center py-10 text-base-content/50">
|
||||
<p>No photos submitted for this item</p>
|
||||
</div>
|
||||
{:else}
|
||||
<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" />
|
||||
<div class="p-3">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<p class="font-semibold">{photo.hunterName}</p>
|
||||
<p class="text-xs text-base-content/50">
|
||||
{new Date(photo.photoUploadDateTime).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<StatusBadge status={photo.photoStatus} />
|
||||
</div>
|
||||
{#if photo.photoStatus === 'SUBMITTED'}
|
||||
<div class="flex gap-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>
|
||||
{:else if photo.photoStatus === 'APPROVED'}
|
||||
<button class="btn btn-outline btn-error btn-sm w-full" onclick={() => review(photo, 'REJECTED')} disabled={reviewing === photo.id}>
|
||||
Revoke Approval
|
||||
</button>
|
||||
{:else if photo.photoStatus === 'REJECTED'}
|
||||
<button class="btn btn-outline btn-success btn-sm w-full" onclick={() => review(photo, 'APPROVED')} disabled={reviewing === photo.id}>
|
||||
Approve Instead
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user