Refactor admin to shared generic CRUD; restructure proclamation categories/groups and APIs
- Replace per-model admin forms with the shared generic AdminTable/AdminModal CRUD - Add ProclamationGroup model and admin + API v2 routes (replaces subscription levels) - Drop subscription levels and flatten proclamation categories - Update v2 serializers and seed data accordingly
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
<script lang="ts">
|
||||
import type { PageProps } from './$types';
|
||||
import type { ActionResult } from '$lib/svelte-admin/types';
|
||||
import { enhance } from '$app/forms';
|
||||
import { Editor } from '@tiptap/core';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import Highlight from '@tiptap/extension-highlight';
|
||||
import { TextStyle } from '@tiptap/extension-text-style';
|
||||
import { Color } from '@tiptap/extension-color';
|
||||
import AdminTable from '$lib/components/admin/AdminTable.svelte';
|
||||
import AdminModal from '$lib/components/admin/AdminModal.svelte';
|
||||
import Toast from '$lib/components/admin/Toast.svelte';
|
||||
import { getModelConfig } from '$lib/svelte-admin/registry';
|
||||
import ProclamationForm from './ProclamationForm.svelte';
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
|
||||
@@ -17,16 +21,25 @@
|
||||
let modalOpen = $state(false);
|
||||
let editingRecord = $state<Record<string, unknown> | null>(null);
|
||||
let toastMessage = $state('');
|
||||
let toastType = $state<'success' | 'error'>('success');
|
||||
let fieldErrors = $state<Record<string, string>>({});
|
||||
let formError = $state('');
|
||||
|
||||
let dirty = $state(false);
|
||||
let name = $state('');
|
||||
let solfeggioFrequencyId = $state('');
|
||||
let categoryId = $state('');
|
||||
let groupId = $state('');
|
||||
|
||||
// Inline TipTap editor
|
||||
let element = $state<HTMLElement>();
|
||||
let editor: Editor | null = null;
|
||||
let content = $state('');
|
||||
|
||||
$effect(() => {
|
||||
if (form && 'success' in (form as any)) {
|
||||
const result = form as any;
|
||||
if (result.success) {
|
||||
toastMessage = 'Saved successfully';
|
||||
toastType = 'success';
|
||||
closeModal();
|
||||
} else {
|
||||
formError = result.error ?? '';
|
||||
@@ -35,6 +48,80 @@
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const record = editingRecord ?? {};
|
||||
name = String(record['name'] ?? '');
|
||||
solfeggioFrequencyId = String(record['solfeggioFrequencyId'] ?? '');
|
||||
categoryId = String(record['categoryId'] ?? '');
|
||||
groupId = String(record['groupId'] ?? '');
|
||||
content = typeof record['content'] === 'string' ? (record['content'] as string) : '';
|
||||
dirty = false;
|
||||
});
|
||||
|
||||
function markDirty() {
|
||||
if (editingRecord?.id) dirty = true;
|
||||
}
|
||||
|
||||
function handleCategoryChange(e: Event) {
|
||||
categoryId = (e.target as HTMLSelectElement).value;
|
||||
}
|
||||
|
||||
function handleGroupChange(e: Event) {
|
||||
groupId = (e.target as HTMLSelectElement).value;
|
||||
}
|
||||
|
||||
let contentKey = $derived(editingRecord?.id ?? 'new');
|
||||
|
||||
$effect(() => {
|
||||
if (!isSuper || !modalOpen || !element) return;
|
||||
const initialContent =
|
||||
typeof (editingRecord ?? {})['content'] === 'string'
|
||||
? ((editingRecord ?? {})['content'] as string)
|
||||
: '';
|
||||
|
||||
editor = new Editor({
|
||||
element,
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
bulletList: false,
|
||||
orderedList: false
|
||||
}),
|
||||
TextStyle,
|
||||
Color,
|
||||
Highlight.configure({ multicolor: true })
|
||||
],
|
||||
content: initialContent,
|
||||
onUpdate: ({ editor }) => {
|
||||
content = editor.getHTML();
|
||||
}
|
||||
});
|
||||
|
||||
const instance = editor;
|
||||
instance.setEditable(true);
|
||||
|
||||
return () => {
|
||||
instance.destroy();
|
||||
};
|
||||
});
|
||||
|
||||
function toggleBold() {
|
||||
editor?.chain().focus().toggleBold().run();
|
||||
}
|
||||
function toggleItalic() {
|
||||
editor?.chain().focus().toggleItalic().run();
|
||||
}
|
||||
function toggleHighlight() {
|
||||
editor?.chain().focus().toggleHighlight().run();
|
||||
}
|
||||
|
||||
function setColor() {
|
||||
const current = editor?.getAttributes('textStyle').color ?? '#000000';
|
||||
const colors = ['#000000', '#FF0000', '#0000FF', '#008000', '#FFA500', '#800080', '#808080'];
|
||||
const idx = colors.indexOf(current);
|
||||
const next = colors[(idx + 1) % colors.length];
|
||||
editor?.chain().focus().setColor(next).run();
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingRecord = null;
|
||||
formError = '';
|
||||
@@ -57,8 +144,8 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<h1 class="mb-4 text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1>
|
||||
<div>
|
||||
<h1>{config.displayName}</h1>
|
||||
<AdminTable
|
||||
records={data.records}
|
||||
listFields={config.listFields}
|
||||
@@ -79,17 +166,109 @@
|
||||
{canDelete}
|
||||
recordId={editingRecord?.id as string | undefined}
|
||||
>
|
||||
<ProclamationForm
|
||||
record={editingRecord ?? {}}
|
||||
relationOptionsData={data.relationOptionsData}
|
||||
categories={data.categories}
|
||||
{fieldErrors}
|
||||
{formError}
|
||||
{isSuper}
|
||||
submitLabel={editingRecord ? 'Update' : 'Create'}
|
||||
/>
|
||||
<form action="?/upsert" method="POST" use:enhance oninput={markDirty}>
|
||||
{#if editingRecord?.id}
|
||||
<input type="hidden" name="id" value={String(editingRecord.id)} />
|
||||
{/if}
|
||||
|
||||
{#if formError}
|
||||
<div>{formError}</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<label for="field-name">Name</label>
|
||||
<input type="text" id="field-name" name="name" bind:value={name} />
|
||||
{#if fieldErrors.name}
|
||||
<p>{fieldErrors.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="field-solfeggioFrequencyId">Frequency</label>
|
||||
<select
|
||||
id="field-solfeggioFrequencyId"
|
||||
name="solfeggioFrequencyId"
|
||||
onchange={(e) => (solfeggioFrequencyId = (e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
<option value="">-- Select Frequency --</option>
|
||||
{#each data.relationOptionsData.solfeggioFrequencyId ?? [] as opt (String(opt.value))}
|
||||
<option value={opt.value} selected={String(opt.value) === solfeggioFrequencyId}
|
||||
>{opt.label}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.solfeggioFrequencyId}
|
||||
<p>{fieldErrors.solfeggioFrequencyId}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="field-categoryId">Category</label>
|
||||
<select id="field-categoryId" name="categoryId" onchange={handleCategoryChange}>
|
||||
<option value="">-- Select Category --</option>
|
||||
{#each data.categories as cat (String(cat.id))}
|
||||
<option value={cat.id} selected={String(cat.id) === categoryId}>{cat.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.categoryId}
|
||||
<p>{fieldErrors.categoryId}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="field-groupId">Group</label>
|
||||
<select id="field-groupId" name="groupId" onchange={handleGroupChange}>
|
||||
<option value="">-- Select Group --</option>
|
||||
{#each data.groups as group (String(group.id))}
|
||||
<option value={group.id} selected={String(group.id) === groupId}>{group.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.groupId}
|
||||
<p>{fieldErrors.groupId}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isSuper}
|
||||
<div>
|
||||
<label for="field-content">Content</label>
|
||||
<div id="field-content">
|
||||
{#key contentKey}
|
||||
<div>
|
||||
<div>
|
||||
<button type="button" onclick={toggleBold} title="Bold">B</button>
|
||||
<button type="button" onclick={toggleItalic} title="Italic">I</button>
|
||||
<button type="button" onclick={toggleHighlight} title="Highlight">
|
||||
<span>H</span>
|
||||
</button>
|
||||
<button type="button" onclick={setColor} title="Cycle text color">A</button>
|
||||
</div>
|
||||
<div
|
||||
bind:this={element}
|
||||
data-testid="tiptap-editor"
|
||||
role="textbox"
|
||||
tabindex="0"
|
||||
contenteditable="true"
|
||||
></div>
|
||||
</div>
|
||||
<input type="hidden" name="content" value={content} />
|
||||
{/key}
|
||||
</div>
|
||||
{#if fieldErrors.content}
|
||||
<p>{fieldErrors.content}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<button type="submit" disabled={!!editingRecord?.id && !dirty}>
|
||||
{editingRecord ? 'Update' : 'Create'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</AdminModal>
|
||||
|
||||
{#if toastMessage}
|
||||
<Toast message={toastMessage} type={toastType} />
|
||||
<Toast message={toastMessage} />
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user