Allows for editing Hunt details

This commit is contained in:
2026-05-18 16:31:00 -05:00
parent fd4d86db5b
commit 6e4edd96ce
3 changed files with 106 additions and 8 deletions

View File

@@ -35,6 +35,9 @@ export const apiGetAllHunts = (status?: 'UNSTARTED' | 'ONGOING' | 'CLOSED') =>
export const apiCreateHunt = (title: string, startDateTime: string, endDateTime: string) =>
client.post<HuntResponse>('/hunt', { title, startDateTime, endDateTime })
export const apiUpdateHunt = (huntId: string, title: string, startDateTime: string, endDateTime: string, isTerminated: boolean) =>
client.patch<HuntResponse>(`/hunt/${huntId}`, { title, startDateTime, endDateTime, isTerminated })
// ── Hunter ────────────────────────────────────────────────────────────────────
export const apiGetOngoingHunts = () =>

View File

@@ -4,9 +4,11 @@
let {
value = $bindable(''),
defaultTimeIndex = 36,
initialValue = '',
}: {
value?: string
defaultTimeIndex?: number
initialValue?: string
} = $props()
const months = [
@@ -25,11 +27,19 @@
return { value: i, label: `${displayHour}:${minute.toString().padStart(2, '0')} ${ampm}` }
})
const today = new Date()
let month = $state(today.getMonth())
let day = $state(today.getDate())
let year = $state(today.getFullYear())
let timeIndex = $state(untrack(() => defaultTimeIndex))
const seed = untrack(() => {
if (initialValue) {
const d = new Date(initialValue)
return { month: d.getMonth(), day: d.getDate(), year: d.getFullYear(), timeIndex: d.getHours() * 4 + Math.round(d.getMinutes() / 15) }
}
const today = new Date()
return { month: today.getMonth(), day: today.getDate(), year: today.getFullYear(), timeIndex: defaultTimeIndex }
})
let month = $state(seed.month)
let day = $state(seed.day)
let year = $state(seed.year)
let timeIndex = $state(seed.timeIndex)
const daysInMonth = $derived(new Date(year, month + 1, 0).getDate())

View File

@@ -8,11 +8,13 @@
apiGetHunt,
apiGetItems,
apiListTeams,
apiUpdateHunt,
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'
import DateTimePicker from '../../lib/components/DateTimePicker.svelte'
let { params }: { params: { huntId: string } } = $props()
@@ -34,6 +36,40 @@
let newTeamName = $state('')
let addingTeam = $state(false)
let editingHunt = $state(false)
let editTitle = $state('')
let editStart = $state('')
let editEnd = $state('')
let editTerminated = $state(false)
let savingHunt = $state(false)
function startEditHunt() {
if (!hunt) return
editTitle = hunt.title
editStart = hunt.startDateTime
editEnd = hunt.endDateTime
editTerminated = hunt.isTerminated
editingHunt = true
}
function cancelEditHunt() {
editingHunt = false
}
async function saveHunt() {
if (!hunt || !editTitle.trim() || !editStart || !editEnd) return
savingHunt = true
error = ''
try {
hunt = await apiUpdateHunt(params.huntId, editTitle.trim(), editStart, editEnd, editTerminated)
editingHunt = false
} catch (e: unknown) {
error = e instanceof Error ? e.message : 'Failed to update hunt'
} finally {
savingHunt = false
}
}
let editingItemId = $state<string | null>(null)
let editName = $state('')
let editPoints = $state(0)
@@ -127,18 +163,67 @@
<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>
<div class="flex-1 min-w-0">
{#if hunt}
<h1 class="text-lg font-bold leading-tight">{hunt.title}</h1>
<h1 class="text-lg font-bold leading-tight truncate">{hunt.title}</h1>
<StatusBadge status={huntStatus(hunt)} />
{/if}
</div>
{#if hunt && !editingHunt}
<button class="btn btn-outline btn-sm shrink-0" onclick={startEditHunt}>Edit Hunt</button>
{/if}
</div>
{#if editingHunt && hunt}
<div class="bg-base-200 rounded-2xl p-4 mb-4 flex flex-col gap-4">
<h2 class="font-bold text-sm uppercase tracking-wide text-base-content/50">Edit Hunt</h2>
{#if error}
<div class="alert alert-error text-sm">{error}</div>
{/if}
<label class="form-control">
<span class="label-text text-xs font-medium mb-1">Title</span>
<input type="text" class="input input-bordered input-sm" bind:value={editTitle} disabled={savingHunt} required />
</label>
<fieldset class="form-control">
<legend class="label-text text-xs font-medium mb-1">Start</legend>
<DateTimePicker bind:value={editStart} initialValue={editStart} />
</fieldset>
<fieldset class="form-control">
<legend class="label-text text-xs font-medium mb-1">End</legend>
<DateTimePicker bind:value={editEnd} initialValue={editEnd} />
</fieldset>
<button
type="button"
class="flex items-center justify-between p-4 rounded-xl border-2 transition-all {editTerminated ? 'border-error bg-error/10' : 'border-base-300 bg-base-100'}"
onclick={() => editTerminated = !editTerminated}
disabled={savingHunt}
>
<div class="text-left">
<p class="font-semibold {editTerminated ? 'text-error' : 'text-base-content'}">Terminated</p>
<p class="text-xs text-base-content/50">Permanently closes the hunt for all hunters</p>
</div>
<div class="w-6 h-6 rounded-full border-2 flex items-center justify-center shrink-0 {editTerminated ? 'border-error bg-error' : 'border-base-300'}">
{#if editTerminated}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-error-content" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 00-1.414 0L8 12.586 4.707 9.293a1 1 0 00-1.414 1.414l4 4a1 1 0 001.414 0l8-8a1 1 0 000-1.414z" clip-rule="evenodd" />
</svg>
{/if}
</div>
</button>
<div class="flex gap-2">
<button class="btn btn-primary btn-sm flex-1" onclick={saveHunt} disabled={savingHunt}>
{#if savingHunt}<span class="loading loading-spinner loading-xs"></span>{/if}
Save
</button>
<button class="btn btn-ghost btn-sm" onclick={cancelEditHunt} disabled={savingHunt}>Cancel</button>
</div>
</div>
{/if}
{#if loading}
<LoadingSpinner />
{:else}
{#if error}
{#if error && !editingHunt}
<div class="alert alert-error mb-4 text-sm">{error}</div>
{/if}