add categories admin page
This commit is contained in:
@@ -3,6 +3,14 @@ import { error } from '@sveltejs/kit';
|
||||
import { prisma } from '$lib/db';
|
||||
import { canAccessAdmin } from '$lib/svelte-admin/roles';
|
||||
|
||||
/**
|
||||
* This Layout Server-Side page checks if the user is logged in
|
||||
* and if that user has a sufficient permission level to view any pages
|
||||
* rendering this layout, redirecting them to a 403 error page if not.
|
||||
* It also makes the `adminUser` (the currently logged in user) available
|
||||
* to all of those pages via page data.
|
||||
*/
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: locals.user?.id },
|
||||
@@ -10,6 +18,7 @@ export const load: LayoutServerLoad = async ({ locals }) => {
|
||||
});
|
||||
|
||||
if (!user || !canAccessAdmin(user.roleId)) {
|
||||
console.log(`Attempted to access /admin via ${user?.id} : ${user?.name} with a role of ${user?.role}`);
|
||||
throw error(403, 'not authorized');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getAllModelConfigs } from '$lib/svelte-admin/registry';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const models = getAllModelConfigs().map((m) => ({
|
||||
slug: m.slug,
|
||||
displayName: m.displayName
|
||||
}));
|
||||
|
||||
return { models };
|
||||
};
|
||||
@@ -1,21 +1 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data } = $props<{ data: PageData }>();
|
||||
</script>
|
||||
|
||||
<div class="mb-4 flex h-full min-h-0 flex-col gap-6 p-6">
|
||||
<h1 class="text-2xl font-semibold text-gray-900">Admin Panel</h1>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{#each data.models as model}
|
||||
<a
|
||||
href="/admin/{model.slug}"
|
||||
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md"
|
||||
>
|
||||
<h2 class="text-lg font-medium text-gray-900">{model.displayName}</h2>
|
||||
<p class="mt-1 text-sm text-gray-500">Manage {model.displayName.toLowerCase()}</p>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<h1>Admin Dashboard</h1>
|
||||
@@ -1,208 +0,0 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { error, fail, type Actions, type RequestEvent } from '@sveltejs/kit';
|
||||
import { getModelConfig } from '$lib/svelte-admin/registry';
|
||||
import { getDelegate } from '$lib/svelte-admin/prisma-delegate';
|
||||
import { extractFormData } from '$lib/svelte-admin/form-data';
|
||||
import { prisma } from '$lib/db';
|
||||
import type { RelationOptionConfig } from '$lib/svelte-admin/types';
|
||||
import { generateLicenseKey } from '$lib/utils';
|
||||
|
||||
function getCategoryDisplayName(category: Record<string, unknown> | null | undefined) {
|
||||
if (!category) return '';
|
||||
const name = String(category.name ?? '');
|
||||
const parent = category.parent as Record<string, unknown> | undefined;
|
||||
if (parent?.name) return `${String(parent.name)}/${name}`;
|
||||
return name;
|
||||
}
|
||||
|
||||
function parseIntId(value: unknown): number | null {
|
||||
if (typeof value === 'number' && Number.isInteger(value) && value > 0) return value;
|
||||
if (typeof value === 'string' && value.trim() !== '') {
|
||||
const parsed = parseInt(value, 10);
|
||||
return Number.isNaN(parsed) || parsed <= 0 ? null : parsed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function findOrCreateCategoryByLabel(label: string): Promise<number> {
|
||||
const normalized = label.trim();
|
||||
if (!normalized) throw new Error('Invalid category label');
|
||||
const [parentName, childName] = normalized.split('/').map((v) => v.trim());
|
||||
|
||||
if (childName) {
|
||||
let parent = await prisma.proclamationCategory.findFirst({
|
||||
where: { name: parentName, parentId: null }
|
||||
});
|
||||
if (!parent) {
|
||||
parent = await prisma.proclamationCategory.create({ data: { name: parentName } });
|
||||
}
|
||||
|
||||
let child = await prisma.proclamationCategory.findFirst({
|
||||
where: { name: childName, parentId: parent.id }
|
||||
});
|
||||
if (!child) {
|
||||
child = await prisma.proclamationCategory.create({
|
||||
data: { name: childName, parentId: parent.id }
|
||||
});
|
||||
}
|
||||
return child.id;
|
||||
}
|
||||
|
||||
let category = await prisma.proclamationCategory.findFirst({
|
||||
where: { name: normalized, parentId: null }
|
||||
});
|
||||
if (!category) {
|
||||
category = await prisma.proclamationCategory.create({ data: { name: normalized } });
|
||||
}
|
||||
return category.id;
|
||||
}
|
||||
|
||||
async function resolveCategoryIdFromFormData(
|
||||
formData: FormData,
|
||||
categoryField: string
|
||||
): Promise<number | null> {
|
||||
const categoryValue = formData.get(categoryField);
|
||||
const categoryLabelValue = formData.get(`${categoryField}Label`);
|
||||
|
||||
const parsedId = parseIntId(categoryValue);
|
||||
if (parsedId) return parsedId;
|
||||
|
||||
const label = String(categoryLabelValue ?? categoryValue ?? '').trim();
|
||||
if (!label) return null;
|
||||
|
||||
return await findOrCreateCategoryByLabel(label);
|
||||
}
|
||||
|
||||
async function loadRelationOptions(
|
||||
relationOptions: Record<string, RelationOptionConfig> | undefined
|
||||
) {
|
||||
const result: Record<string, { label: string; value: string | number }[]> = {};
|
||||
if (!relationOptions) return result;
|
||||
|
||||
for (const [field, config] of Object.entries(relationOptions)) {
|
||||
const key = config.model.charAt(0).toLowerCase() + config.model.slice(1);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const delegate = (prisma as any)[key];
|
||||
if (!delegate) continue;
|
||||
|
||||
const shouldIncludeParent = config.model === 'ProclamationCategory';
|
||||
const select: Record<string, unknown> = {
|
||||
[config.valueField]: true,
|
||||
[config.labelField]: true
|
||||
};
|
||||
if (shouldIncludeParent) {
|
||||
(select as Record<string, unknown>).parent = { select: { name: true } };
|
||||
}
|
||||
|
||||
const records = await delegate.findMany({
|
||||
select
|
||||
});
|
||||
|
||||
result[field] = records.map((r: Record<string, unknown>) => {
|
||||
const value = r[config.valueField] as string | number;
|
||||
const label =
|
||||
config.model === 'ProclamationCategory'
|
||||
? getCategoryDisplayName(r)
|
||||
: String(r[config.labelField]);
|
||||
return {
|
||||
label,
|
||||
value
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async ({ params, parent }) => {
|
||||
const config = getModelConfig(params.model);
|
||||
if (!config) throw error(404, `Model "${params.model}" not found`);
|
||||
|
||||
const { adminUser } = await parent();
|
||||
const delegate = getDelegate(config);
|
||||
|
||||
const where = config.accessFilter ? config.accessFilter(adminUser.roleId) : undefined;
|
||||
|
||||
let records = await delegate.findMany({
|
||||
...(config.include ? { include: config.include } : {}),
|
||||
...(where ? { where } : {})
|
||||
});
|
||||
|
||||
if (config.modelName === 'Proclamation') {
|
||||
records = records.map((r: Record<string, unknown>) => {
|
||||
const category = r.category as Record<string, unknown> | undefined;
|
||||
if (category) {
|
||||
(category as Record<string, unknown>).displayName = getCategoryDisplayName(category);
|
||||
}
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
if (config.modelName === 'ProclamationCategory') {
|
||||
records = records.map((r: Record<string, unknown>) => {
|
||||
(r as Record<string, unknown>).displayName = getCategoryDisplayName(
|
||||
r as Record<string, unknown>
|
||||
);
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
const relationOptionsData = await loadRelationOptions(config.relationOptions);
|
||||
|
||||
return {
|
||||
config: {
|
||||
slug: config.slug,
|
||||
displayName: config.displayName,
|
||||
displayNameSingular: config.displayNameSingular,
|
||||
listFields: config.listFields,
|
||||
searchFields: config.searchFields,
|
||||
formFields: config.formFields,
|
||||
fieldTypes: config.fieldTypes,
|
||||
relationOptions: config.relationOptions,
|
||||
listOnly: config.listOnly ?? false
|
||||
},
|
||||
records: JSON.parse(JSON.stringify(records)),
|
||||
relationOptionsData
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
create: async ({ request, params }: RequestEvent) => {
|
||||
const config = getModelConfig(params.model!);
|
||||
if (!config) return fail(400, { error: 'Invalid model' });
|
||||
|
||||
const formData = await request.formData();
|
||||
const data = extractFormData(formData, config.formFields, config.fieldTypes);
|
||||
|
||||
console.log(data);
|
||||
|
||||
if (data.price === null) {
|
||||
data.price = 0.99;
|
||||
}
|
||||
|
||||
console.log(data);
|
||||
|
||||
if (config.slug === 'license-keys') {
|
||||
(data as Record<string, unknown>).key = generateLicenseKey();
|
||||
}
|
||||
|
||||
if (config.modelName === 'Proclamation') {
|
||||
const resolvedCategoryId = await resolveCategoryIdFromFormData(formData, 'categoryId');
|
||||
if (resolvedCategoryId != null) {
|
||||
(data as Record<string, unknown>).categoryId = resolvedCategoryId;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const delegate = getDelegate(config);
|
||||
await delegate.create({ data });
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Admin create error:', err);
|
||||
return fail(500, {
|
||||
error: 'Failed to create record. Please check your input and try again.',
|
||||
...Object.fromEntries(formData)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
<script lang="ts">
|
||||
import AdminTable from '$lib/components/admin/AdminTable.svelte';
|
||||
import AdminForm from '$lib/components/admin/AdminForm.svelte';
|
||||
import Center from '$lib/components/containers/Center.svelte';
|
||||
import type { ActionData, PageData } from './$types';
|
||||
|
||||
let { data, form } = $props<{ data: PageData; form: ActionData }>();
|
||||
let showModal = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (form?.success) {
|
||||
showModal = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mb-4 flex h-full min-h-0 flex-col gap-6 p-6">
|
||||
<div class="flex shrink-0 items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="/admin" class="text-blue-600 hover:text-blue-800 hover:underline">← Admin</a>
|
||||
<h1 class="text-2xl font-semibold text-gray-900">{data.config.displayName}</h1>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg bg-green-600 px-4 py-2 font-medium text-white transition-colors hover:bg-green-700"
|
||||
onclick={() => (showModal = true)}
|
||||
>
|
||||
Create +
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if data.records.length > 0}
|
||||
<AdminTable
|
||||
records={data.records}
|
||||
listFields={data.config.listFields}
|
||||
searchFields={data.config.searchFields}
|
||||
modelSlug={data.config.slug}
|
||||
listOnly={data.config.listOnly}
|
||||
/>
|
||||
{:else}
|
||||
<Center>
|
||||
<div class="text-center">
|
||||
<p class="text-lg text-gray-500">No {data.config.displayName.toLowerCase()} yet</p>
|
||||
<p class="mt-1 text-sm text-gray-400">Click "Create +" to add one</p>
|
||||
</div>
|
||||
</Center>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if showModal}
|
||||
<div class="fixed inset-0 z-50 overflow-y-auto bg-black/50">
|
||||
<div class="flex min-h-full items-center justify-center p-4">
|
||||
<div class="w-full max-w-2xl rounded-lg bg-white p-8 shadow-xl">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold text-gray-900">
|
||||
Create {data.config.displayNameSingular}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
|
||||
onclick={() => (showModal = false)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if form?.error}
|
||||
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700">
|
||||
{form.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<AdminForm
|
||||
fields={data.config.formFields}
|
||||
fieldTypes={data.config.fieldTypes}
|
||||
relationOptions={data.config.relationOptions}
|
||||
relationOptionsData={data.relationOptionsData}
|
||||
{form}
|
||||
action="?/create"
|
||||
submitLabel="Create {data.config.displayNameSingular}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,191 +0,0 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { error, fail, redirect, type Actions, type RequestEvent } from '@sveltejs/kit';
|
||||
import { getModelConfig } from '$lib/svelte-admin/registry';
|
||||
import { getDelegate, parseId } from '$lib/svelte-admin/prisma-delegate';
|
||||
import { extractFormData } from '$lib/svelte-admin/form-data';
|
||||
import { prisma } from '$lib/db';
|
||||
import type { RelationOptionConfig } from '$lib/svelte-admin/types';
|
||||
|
||||
function getCategoryDisplayName(category: Record<string, unknown> | null | undefined) {
|
||||
if (!category) return '';
|
||||
const name = String(category.name ?? '');
|
||||
const parent = category.parent as Record<string, unknown> | undefined;
|
||||
if (parent?.name) return `${String(parent.name)}/${name}`;
|
||||
return name;
|
||||
}
|
||||
|
||||
function parseIntId(value: unknown): number | null {
|
||||
if (typeof value === 'number' && Number.isInteger(value) && value > 0) return value;
|
||||
if (typeof value === 'string' && value.trim() !== '') {
|
||||
const parsed = parseInt(value, 10);
|
||||
return Number.isNaN(parsed) || parsed <= 0 ? null : parsed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function findOrCreateCategoryByLabel(label: string): Promise<number> {
|
||||
const normalized = label.trim();
|
||||
if (!normalized) throw new Error('Invalid category label');
|
||||
const [parentName, childName] = normalized.split('/').map((v) => v.trim());
|
||||
|
||||
if (childName) {
|
||||
let parent = await prisma.proclamationCategory.findFirst({
|
||||
where: { name: parentName, parentId: null }
|
||||
});
|
||||
if (!parent) {
|
||||
parent = await prisma.proclamationCategory.create({ data: { name: parentName } });
|
||||
}
|
||||
|
||||
let child = await prisma.proclamationCategory.findFirst({
|
||||
where: { name: childName, parentId: parent.id }
|
||||
});
|
||||
if (!child) {
|
||||
child = await prisma.proclamationCategory.create({
|
||||
data: { name: childName, parentId: parent.id }
|
||||
});
|
||||
}
|
||||
return child.id;
|
||||
}
|
||||
|
||||
let category = await prisma.proclamationCategory.findFirst({
|
||||
where: { name: normalized, parentId: null }
|
||||
});
|
||||
if (!category) {
|
||||
category = await prisma.proclamationCategory.create({ data: { name: normalized } });
|
||||
}
|
||||
return category.id;
|
||||
}
|
||||
|
||||
async function resolveCategoryIdFromFormData(formData: FormData, categoryField: string): Promise<number | null> {
|
||||
const categoryValue = formData.get(categoryField);
|
||||
const categoryLabelValue = formData.get(`${categoryField}Label`);
|
||||
|
||||
const parsedId = parseIntId(categoryValue);
|
||||
if (parsedId) return parsedId;
|
||||
|
||||
const label = String(categoryLabelValue ?? categoryValue ?? '').trim();
|
||||
if (!label) return null;
|
||||
|
||||
return await findOrCreateCategoryByLabel(label);
|
||||
}
|
||||
|
||||
async function loadRelationOptions(
|
||||
relationOptions: Record<string, RelationOptionConfig> | undefined
|
||||
) {
|
||||
const result: Record<string, { label: string; value: string | number }[]> = {};
|
||||
if (!relationOptions) return result;
|
||||
|
||||
for (const [field, config] of Object.entries(relationOptions)) {
|
||||
const key = config.model.charAt(0).toLowerCase() + config.model.slice(1);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const delegate = (prisma as any)[key];
|
||||
if (!delegate) continue;
|
||||
|
||||
const shouldIncludeParent = config.model === 'ProclamationCategory';
|
||||
const select: Record<string, unknown> = {
|
||||
[config.valueField]: true,
|
||||
[config.labelField]: true
|
||||
};
|
||||
if (shouldIncludeParent) {
|
||||
(select as Record<string, unknown>).parent = { select: { name: true } };
|
||||
}
|
||||
|
||||
const records = await delegate.findMany({
|
||||
select
|
||||
});
|
||||
|
||||
result[field] = records.map((r: Record<string, unknown>) => {
|
||||
const value = r[config.valueField] as string | number;
|
||||
const label =
|
||||
config.model === 'ProclamationCategory'
|
||||
? getCategoryDisplayName(r)
|
||||
: String(r[config.labelField]);
|
||||
return {
|
||||
label,
|
||||
value
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const config = getModelConfig(params.model);
|
||||
if (!config) throw error(404, `Model "${params.model}" not found`);
|
||||
|
||||
const delegate = getDelegate(config);
|
||||
const id = parseId(config, params.id);
|
||||
|
||||
const record = await delegate.findUnique({
|
||||
where: { id },
|
||||
...(config.include ? { include: config.include } : {})
|
||||
});
|
||||
|
||||
if (!record) throw error(404, 'Record not found');
|
||||
|
||||
const relationOptionsData = await loadRelationOptions(config.relationOptions);
|
||||
|
||||
return {
|
||||
config: {
|
||||
slug: config.slug,
|
||||
displayName: config.displayName,
|
||||
displayNameSingular: config.displayNameSingular,
|
||||
listFields: config.listFields,
|
||||
formFields: config.formFields,
|
||||
readonlyFields: config.readonlyFields,
|
||||
fieldTypes: config.fieldTypes,
|
||||
relationOptions: config.relationOptions,
|
||||
relatedRecords: config.relatedRecords
|
||||
},
|
||||
record: JSON.parse(JSON.stringify(record)),
|
||||
relationOptionsData
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
update: async ({ request, params }: RequestEvent) => {
|
||||
const config = getModelConfig(params.model!);
|
||||
if (!config) return fail(400, { error: 'Invalid model' });
|
||||
|
||||
const formData = await request.formData();
|
||||
const id = parseId(config, params.id!);
|
||||
const data = extractFormData(formData, config.formFields, config.fieldTypes);
|
||||
|
||||
if (config.modelName === 'Proclamation') {
|
||||
const resolvedCategoryId = await resolveCategoryIdFromFormData(formData, 'categoryId');
|
||||
if (resolvedCategoryId != null) {
|
||||
(data as Record<string, unknown>).categoryId = resolvedCategoryId;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const delegate = getDelegate(config);
|
||||
await delegate.update({ where: { id }, data });
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Admin update error:', err);
|
||||
return fail(500, {
|
||||
error: 'Failed to update record. Please check your input and try again.',
|
||||
...Object.fromEntries(formData)
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
delete: async ({ params }: RequestEvent) => {
|
||||
const config = getModelConfig(params.model!);
|
||||
if (!config) return fail(400, { error: 'Invalid model' });
|
||||
|
||||
const id = parseId(config, params.id!);
|
||||
|
||||
try {
|
||||
const delegate = getDelegate(config);
|
||||
await delegate.delete({ where: { id } });
|
||||
} catch (err) {
|
||||
console.error('Admin delete error:', err);
|
||||
return fail(500, { error: 'Failed to delete record.' });
|
||||
}
|
||||
|
||||
redirect(303, `/admin/${config.slug}`);
|
||||
}
|
||||
};
|
||||
@@ -1,129 +0,0 @@
|
||||
<script lang="ts">
|
||||
import AdminForm from '$lib/components/admin/AdminForm.svelte';
|
||||
import RelatedHasOne from '$lib/components/admin/RelatedHasOne.svelte';
|
||||
import RelatedHasMany from '$lib/components/admin/RelatedHasMany.svelte';
|
||||
import { fieldNameToLabel } from '$lib/svelte-admin/introspect';
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let { data, form } = $props<{ data: PageData; form: ActionData }>();
|
||||
let seconds = $state(0);
|
||||
let remainder = $derived(5 - seconds);
|
||||
let showDeleteConfirm = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (form?.success) {
|
||||
seconds += 1;
|
||||
if (seconds === 6) {
|
||||
window.location.href = `/admin/${data.config.slug}`;
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mx-auto mt-12 mb-auto w-full max-w-2xl px-4">
|
||||
<a href="/admin/{data.config.slug}" class="text-blue-600 hover:text-blue-800 hover:underline">
|
||||
← Back to {data.config.displayName}
|
||||
</a>
|
||||
|
||||
<h1 class="mt-4 text-2xl font-semibold text-gray-900">
|
||||
Edit {data.config.displayNameSingular}
|
||||
</h1>
|
||||
|
||||
{#if form?.error}
|
||||
<div class="mt-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700">
|
||||
{form.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if form?.success}
|
||||
<div class="mt-4 rounded-lg border border-green-200 bg-green-50 p-3 text-green-700">
|
||||
Record updated successfully! Redirecting in {remainder} seconds.
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if data.config.readonlyFields && data.config.readonlyFields.length > 0}
|
||||
<div class="mt-6 rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||
<h2 class="mb-2 text-sm font-medium text-gray-700">Read-only Fields</h2>
|
||||
<dl class="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
|
||||
{#each data.config.readonlyFields as field}
|
||||
<dt class="text-gray-500">{fieldNameToLabel(field)}</dt>
|
||||
<dd class="text-gray-900">{data.record[field] ?? '—'}</dd>
|
||||
{/each}
|
||||
</dl>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-6">
|
||||
<AdminForm
|
||||
fields={data.config.formFields}
|
||||
fieldTypes={data.config.fieldTypes}
|
||||
relationOptions={data.config.relationOptions}
|
||||
relationOptionsData={data.relationOptionsData}
|
||||
record={data.record}
|
||||
{form}
|
||||
action="?/update"
|
||||
submitLabel="Save {data.config.displayNameSingular}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if data.config.relatedRecords}
|
||||
{#each data.config.relatedRecords as rel}
|
||||
{#if rel.type === 'has-one'}
|
||||
<RelatedHasOne
|
||||
label={rel.label}
|
||||
record={data.record[rel.relationKey] ?? null}
|
||||
displayFields={rel.displayFields}
|
||||
editSlug={rel.editSlug}
|
||||
/>
|
||||
{:else}
|
||||
<RelatedHasMany
|
||||
label={rel.label}
|
||||
records={data.record[rel.relationKey] ?? []}
|
||||
displayFields={rel.displayFields}
|
||||
editSlug={rel.editSlug}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<div class="mt-8 border-t border-gray-200 pt-6">
|
||||
{#if showDeleteConfirm}
|
||||
<div class="rounded-lg border border-red-200 bg-red-50 p-4">
|
||||
<p class="mb-3 text-sm text-red-700">
|
||||
Are you sure you want to delete this {data.config.displayNameSingular.toLowerCase()}? This
|
||||
action cannot be undone.
|
||||
</p>
|
||||
<div class="flex gap-3">
|
||||
<form method="POST" action="?/delete">
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-700"
|
||||
>
|
||||
Yes, Delete
|
||||
</button>
|
||||
</form>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (showDeleteConfirm = false)}
|
||||
class="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (showDeleteConfirm = true)}
|
||||
class="rounded-lg border border-red-300 px-4 py-2 text-sm font-medium text-red-600 transition-colors hover:bg-red-50"
|
||||
>
|
||||
Delete {data.config.displayNameSingular}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import { prisma } from "$lib/db";
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
console.log(locals.user?.name);
|
||||
// Load proclamation categories
|
||||
const categories = await prisma.proclamationCategory.findMany();
|
||||
console.log(categories);
|
||||
return { categories };
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
|
||||
function getCategoryNameFromId(id: number): string {
|
||||
if (data.categories) {
|
||||
const target = data.categories.find((category) => category.id === id);
|
||||
return target ? target.name : '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Display a list of categories -->
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="hidden">ID</th>
|
||||
<th>Name</th>
|
||||
<th>Parent Category</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if data.categories}
|
||||
{#each data.categories as category, i (category.id)}
|
||||
<tr class="hover:bg-gray-300">
|
||||
<td class="hidden">{i}</td>
|
||||
<td>{category.name}</td>
|
||||
{#if category.parentId}
|
||||
<td>{getCategoryNameFromId(category.parentId)}</td>
|
||||
{:else}
|
||||
<td></td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
Reference in New Issue
Block a user