Portal: calendar-style weekly schedule grid, responsive on mobile
Replace the stacked per-day lists with a Google-Calendar-style WeekSchedule: day columns side by side over a shared, sticky time gutter. Click or drag an empty slot to create a block at that time; click a block to edit it. The daily schedule reuses the same grid with a single 'Every day' column so both modes look consistent. On narrow screens the row scrolls horizontally with short day labels and the gutter pinned, keeping it usable on phones. Also change the type switcher and copy dialog to enhanced form submissions (no full-page reload) so they respond immediately. Removes the now-unused DaySchedule component.
This commit is contained in:
@@ -1,19 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import type { SubmitFunction } from '@sveltejs/kit';
|
||||
import type { PageProps } from './$types';
|
||||
import DaySchedule from '$lib/components/schedule/DaySchedule.svelte';
|
||||
import WeekSchedule from '$lib/components/schedule/WeekSchedule.svelte';
|
||||
import BlockModal from '$lib/components/schedule/BlockModal.svelte';
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
let { data }: PageProps = $props();
|
||||
|
||||
const WEEK_DAYS: { key: string; label: string }[] = [
|
||||
{ key: 'mon', label: 'Monday' },
|
||||
{ key: 'tue', label: 'Tuesday' },
|
||||
{ key: 'wed', label: 'Wednesday' },
|
||||
{ key: 'thu', label: 'Thursday' },
|
||||
{ key: 'fri', label: 'Friday' },
|
||||
{ key: 'sat', label: 'Saturday' },
|
||||
{ key: 'sun', label: 'Sunday' }
|
||||
const WEEK_DAYS: { key: string; label: string; short: string }[] = [
|
||||
{ key: 'mon', label: 'Monday', short: 'Mon' },
|
||||
{ key: 'tue', label: 'Tuesday', short: 'Tue' },
|
||||
{ key: 'wed', label: 'Wednesday', short: 'Wed' },
|
||||
{ key: 'thu', label: 'Thursday', short: 'Thu' },
|
||||
{ key: 'fri', label: 'Friday', short: 'Fri' },
|
||||
{ key: 'sat', label: 'Saturday', short: 'Sat' },
|
||||
{ key: 'sun', label: 'Sunday', short: 'Sun' }
|
||||
];
|
||||
|
||||
type BlockView = {
|
||||
@@ -52,15 +54,17 @@
|
||||
let initialIds = $state<string[]>([]);
|
||||
let initialDynamicAudio = $state(true);
|
||||
|
||||
let typeForm: HTMLFormElement | undefined = $state();
|
||||
let closeTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
// Hidden forms submit the ?/setType and ?/copy actions with use:enhance so the
|
||||
// result comes back as a proper ActionResult (no full-page reload).
|
||||
let typeForm: HTMLFormElement | undefined = $state();
|
||||
let copyForm: HTMLFormElement | undefined = $state();
|
||||
|
||||
// Copy-to dialog state.
|
||||
let copyOpen = $state(false);
|
||||
let copyTargets = $state<string[]>([]);
|
||||
let copyError = $state('');
|
||||
let copyForm: HTMLFormElement | undefined = $state();
|
||||
let copyTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const copyCandidates = $derived(data.licenses.filter((l) => l.id !== data.selectedLicenseId));
|
||||
const overwritingTargets = $derived(
|
||||
@@ -68,21 +72,6 @@
|
||||
);
|
||||
const needsCopyConfirm = $derived(overwritingTargets.length > 0);
|
||||
|
||||
// Surface the copy action's result (summary or error) from the form action.
|
||||
$effect(() => {
|
||||
if (!form || (form as { action?: string }).action !== 'copy') return;
|
||||
if ((form as { success?: boolean }).success) {
|
||||
const targets = (form as { targets?: { label: string }[] }).targets ?? [];
|
||||
copyOpen = false;
|
||||
flash(`Copied to ${targets.map((t) => t.label).join(', ')}.`);
|
||||
// Refresh so hasSchedule flags and the selected system are current.
|
||||
clearTimeout(copyTimer);
|
||||
copyTimer = setTimeout(() => window.location.reload(), 800);
|
||||
} else {
|
||||
copyError = (form as { error?: string }).error ?? 'Copy failed.';
|
||||
}
|
||||
});
|
||||
|
||||
const selectedLicense = $derived(
|
||||
data.licenses.find((l) => l.id === data.selectedLicenseId) ?? null
|
||||
);
|
||||
@@ -92,10 +81,6 @@
|
||||
const dailyBlocks = $derived(blocks.filter((b) => b.day === null));
|
||||
const hasBlocks = $derived(blocks.length > 0);
|
||||
|
||||
function blocksForDay(day: string): BlockView[] {
|
||||
return blocks.filter((b) => b.day === day);
|
||||
}
|
||||
|
||||
function flash(message: string) {
|
||||
successMessage = message;
|
||||
clearTimeout(closeTimer);
|
||||
@@ -119,8 +104,12 @@
|
||||
applyType(next);
|
||||
}
|
||||
|
||||
// Submit the hidden ?/setType form. The enhance callback updates the local
|
||||
// view immediately (the server then prefills/clears as needed) and refreshes
|
||||
// the per-system flags without a full navigation.
|
||||
function applyType(next: 'daily' | 'weekly' | null) {
|
||||
showTypeConfirm = false;
|
||||
errorMessage = '';
|
||||
const form = typeForm;
|
||||
if (!form) return;
|
||||
(form.querySelector('input[name="type"]') as HTMLInputElement).value = next ?? '';
|
||||
@@ -128,6 +117,20 @@
|
||||
form.requestSubmit();
|
||||
}
|
||||
|
||||
const handleTypeSubmit: SubmitFunction = () => {
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'failure') {
|
||||
errorMessage =
|
||||
(result.data as { error?: string })?.error ?? 'Could not change the schedule type.';
|
||||
return;
|
||||
}
|
||||
// Show the new type straight away; invalidateAll() then loads the
|
||||
// server's blocks (e.g. daily→weekly prefill) and hasSchedule flags.
|
||||
await update();
|
||||
await invalidateAll();
|
||||
};
|
||||
};
|
||||
|
||||
function openCreate(day: string | null, startMinutes: number, endMinutes: number) {
|
||||
modalMode = 'create';
|
||||
editingId = undefined;
|
||||
@@ -184,14 +187,37 @@
|
||||
copyError = 'Pick at least one system to copy to.';
|
||||
return;
|
||||
}
|
||||
const form = copyForm;
|
||||
if (!form) return;
|
||||
form.requestSubmit();
|
||||
copyForm?.requestSubmit();
|
||||
}
|
||||
|
||||
const handleCopySubmit: SubmitFunction = () => {
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'failure') {
|
||||
copyError = (result.data as { error?: string })?.error ?? 'Copy failed.';
|
||||
return;
|
||||
}
|
||||
if (result.type === 'error') {
|
||||
copyError = 'Copy failed.';
|
||||
return;
|
||||
}
|
||||
const data =
|
||||
'data' in result ? (result.data as { targets?: { label: string }[] }) : undefined;
|
||||
const targets = (data?.targets ?? []).map((t) => t.label);
|
||||
copyOpen = false;
|
||||
flash(`Copied to ${targets.join(', ')}.`);
|
||||
await update();
|
||||
await invalidateAll();
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if typeForm === undefined}{/if}
|
||||
<form method="POST" action="?/setType" use:enhance bind:this={typeForm} class="hidden">
|
||||
<form
|
||||
method="POST"
|
||||
action="?/setType"
|
||||
use:enhance={handleTypeSubmit}
|
||||
bind:this={typeForm}
|
||||
class="hidden"
|
||||
>
|
||||
<input type="hidden" name="licenseKeyId" value={data.selectedLicenseId ?? ''} />
|
||||
<input type="hidden" name="type" value="" />
|
||||
<input type="hidden" name="confirm" value="false" />
|
||||
@@ -313,22 +339,21 @@
|
||||
</div>
|
||||
{:else if type === 'daily'}
|
||||
<div class="mt-6">
|
||||
<DaySchedule
|
||||
<WeekSchedule
|
||||
days={[{ key: 'daily', label: 'Every day' }]}
|
||||
blocks={dailyBlocks}
|
||||
onCreate={(s, e) => openCreate(null, s, e)}
|
||||
onCreate={(_day, s, e) => openCreate(null, s, e)}
|
||||
onEdit={openEdit}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-6 flex flex-col gap-6">
|
||||
{#each WEEK_DAYS as day (day.key)}
|
||||
<DaySchedule
|
||||
blocks={blocksForDay(day.key)}
|
||||
label={day.label}
|
||||
onCreate={(s, e) => openCreate(day.key, s, e)}
|
||||
onEdit={openEdit}
|
||||
/>
|
||||
{/each}
|
||||
<div class="mt-6">
|
||||
<WeekSchedule
|
||||
days={WEEK_DAYS}
|
||||
{blocks}
|
||||
onCreate={(day, s, e) => openCreate(day, s, e)}
|
||||
onEdit={openEdit}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -486,7 +511,13 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="?/copy" use:enhance bind:this={copyForm} class="hidden">
|
||||
<form
|
||||
method="POST"
|
||||
action="?/copy"
|
||||
use:enhance={handleCopySubmit}
|
||||
bind:this={copyForm}
|
||||
class="hidden"
|
||||
>
|
||||
<input type="hidden" name="sourceLicenseKeyId" value={data.selectedLicenseId ?? ''} />
|
||||
{#each copyTargets as target (target)}
|
||||
<input type="hidden" name="targetIds" value={target} />
|
||||
|
||||
Reference in New Issue
Block a user