Remove user proclamations page
Users should not be able to view proclamations from the web portal.
This commit is contained in:
@@ -1,56 +0,0 @@
|
|||||||
import type { PageServerLoad } from './$types';
|
|
||||||
import { prisma } from '$lib/db';
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ url }) => {
|
|
||||||
const search = url.searchParams.get('search') ?? '';
|
|
||||||
const categoryId = url.searchParams.get('category');
|
|
||||||
const where: Record<string, unknown> = {};
|
|
||||||
if (search) {
|
|
||||||
where.name = { contains: search, mode: 'insensitive' };
|
|
||||||
}
|
|
||||||
if (categoryId) {
|
|
||||||
where.categoryId = parseInt(categoryId, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [proclamations, categories] = await Promise.all([
|
|
||||||
prisma.proclamation.findMany({
|
|
||||||
where,
|
|
||||||
include: {
|
|
||||||
solfeggioFrequency: true,
|
|
||||||
category: { include: { parent: true } },
|
|
||||||
subscriptionLevel: true
|
|
||||||
},
|
|
||||||
orderBy: { name: 'asc' }
|
|
||||||
}),
|
|
||||||
prisma.proclamationCategory.findMany({ include: { parent: true }, orderBy: { name: 'asc' } })
|
|
||||||
]);
|
|
||||||
|
|
||||||
const normalizedProclamations = proclamations.map((p) => {
|
|
||||||
if (p.category) {
|
|
||||||
(p.category as Record<string, unknown>).displayName = getCategoryDisplayName(
|
|
||||||
p.category as Record<string, unknown>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return p;
|
|
||||||
});
|
|
||||||
|
|
||||||
const normalizedCategories = categories.map((c) => ({
|
|
||||||
...c,
|
|
||||||
displayName: getCategoryDisplayName(c)
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
proclamations: normalizedProclamations,
|
|
||||||
categories: normalizedCategories,
|
|
||||||
search,
|
|
||||||
categoryId: categoryId ? parseInt(categoryId, 10) : null
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import type { PageProps } from './$types';
|
|
||||||
|
|
||||||
let { data }: PageProps = $props();
|
|
||||||
|
|
||||||
function displayCategoryName(category: { name: string; displayName?: string }) {
|
|
||||||
return category.displayName ?? category.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
let search = $state('');
|
|
||||||
let categoryId: number | null = $state(null);
|
|
||||||
let initialized = $state(false);
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (!initialized) {
|
|
||||||
search = data.search;
|
|
||||||
categoryId = data.categoryId;
|
|
||||||
initialized = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function applyFilters() {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (search) params.set('search', search);
|
|
||||||
if (categoryId) params.set('category', String(categoryId));
|
|
||||||
goto(`/proclamations?${params.toString()}`, { replaceState: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearFilters() {
|
|
||||||
search = '';
|
|
||||||
categoryId = null;
|
|
||||||
goto('/proclamations', { replaceState: true });
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex h-full min-h-0 flex-col gap-6 p-6 dark:text-gray-100">
|
|
||||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Proclamations</h1>
|
|
||||||
|
|
||||||
<!-- Filters -->
|
|
||||||
<div class="flex flex-wrap items-end gap-4">
|
|
||||||
<div>
|
|
||||||
<label for="search" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
||||||
>Search</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="search"
|
|
||||||
bind:value={search}
|
|
||||||
placeholder="Search by name..."
|
|
||||||
class="mt-1 rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
|
||||||
onkeydown={(e) => e.key === 'Enter' && applyFilters()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="category" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
||||||
>Category</label
|
|
||||||
>
|
|
||||||
<select
|
|
||||||
id="category"
|
|
||||||
bind:value={categoryId}
|
|
||||||
onchange={applyFilters}
|
|
||||||
class="mt-1 rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600"
|
|
||||||
>
|
|
||||||
<option value={null}>All Categories</option>
|
|
||||||
{#each data.categories as cat}
|
|
||||||
<option value={cat.id}>{displayCategoryName(cat)}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rounded-lg bg-blue-600 px-4 py-2 font-medium text-white transition-colors hover:bg-blue-700 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600"
|
|
||||||
onclick={applyFilters}
|
|
||||||
>
|
|
||||||
Search
|
|
||||||
</button>
|
|
||||||
{#if data.search || data.categoryId}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
|
|
||||||
onclick={clearFilters}
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Results -->
|
|
||||||
{#if data.proclamations.length > 0}
|
|
||||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{#each data.proclamations as proclamation}
|
|
||||||
<a
|
|
||||||
href="/proclamations/{proclamation.id}"
|
|
||||||
class="flex flex-col rounded-lg border border-gray-200 bg-white p-5 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-900"
|
|
||||||
>
|
|
||||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">{proclamation.name}</h2>
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{displayCategoryName(proclamation.category)}
|
|
||||||
</p>
|
|
||||||
<div class="mt-auto flex items-center justify-between pt-4">
|
|
||||||
<span class="text-sm text-purple-600 dark:text-gray-400">
|
|
||||||
{proclamation.solfeggioFrequency.value} Hz —
|
|
||||||
{proclamation.solfeggioFrequency.name}
|
|
||||||
</span>
|
|
||||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{proclamation.subscriptionLevel?.name ?? 'Standard'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="flex flex-1 items-center justify-center">
|
|
||||||
<p class="text-lg text-gray-500 dark:text-gray-400">No proclamations found.</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import type { PageServerLoad } from './$types';
|
|
||||||
import { error } from '@sveltejs/kit';
|
|
||||||
import { prisma } from '$lib/db';
|
|
||||||
import { canAccessAdmin } from '$lib/svelte-admin/roles';
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params, parent }) => {
|
|
||||||
const parentData = typeof parent === 'function' ? await parent() : {};
|
|
||||||
const userRoleId = (parentData as { userRoleId?: number }).userRoleId ?? null;
|
|
||||||
if (userRoleId != null && !canAccessAdmin(userRoleId)) {
|
|
||||||
throw error(403, 'not authorized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const proclamation = await prisma.proclamation.findUnique({
|
|
||||||
where: { id: params.id },
|
|
||||||
include: {
|
|
||||||
solfeggioFrequency: true,
|
|
||||||
category: { include: { parent: true } },
|
|
||||||
subscriptionLevel: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!proclamation) {
|
|
||||||
throw error(404, 'Proclamation not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { proclamation };
|
|
||||||
};
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { PageProps } from './$types';
|
|
||||||
|
|
||||||
let { data }: PageProps = $props();
|
|
||||||
|
|
||||||
const categoryBreadcrumb = $derived(
|
|
||||||
data.proclamation.category.parent
|
|
||||||
? `${data.proclamation.category.parent.name} / ${data.proclamation.category.name}`
|
|
||||||
: data.proclamation.category.name
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="mx-auto flex h-full min-h-0 w-full max-w-3xl flex-col gap-6 p-6 dark:text-gray-100">
|
|
||||||
<a
|
|
||||||
href="/proclamations"
|
|
||||||
class="text-sm text-blue-600 hover:text-blue-800 hover:underline dark:text-gray-400 dark:hover:text-gray-200"
|
|
||||||
>
|
|
||||||
← Back to Proclamations
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
{data.proclamation.name}
|
|
||||||
</h1>
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{categoryBreadcrumb}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-4">
|
|
||||||
<div
|
|
||||||
class="rounded-lg border border-purple-200 bg-purple-50 px-4 py-2 dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<p class="text-xs font-medium text-purple-600 dark:text-gray-400">Frequency</p>
|
|
||||||
<p class="text-sm font-semibold text-purple-900 dark:text-gray-100">
|
|
||||||
{data.proclamation.solfeggioFrequency.value} Hz —
|
|
||||||
{data.proclamation.solfeggioFrequency.name}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-2 dark:border-gray-700 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Level</p>
|
|
||||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
{data.proclamation.subscriptionLevel?.name ?? 'Standard'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="prose max-w-none rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-900 dark:prose-invert"
|
|
||||||
>
|
|
||||||
{@html data.proclamation.content}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
||||||
import { isHttpError } from '@sveltejs/kit';
|
|
||||||
import { mockProclamation } from '$lib/test-utils';
|
|
||||||
|
|
||||||
vi.mock('$lib/db', () => ({
|
|
||||||
prisma: {
|
|
||||||
proclamation: { findUnique: vi.fn() }
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { prisma } from '$lib/db';
|
|
||||||
import { load } from './+page.server';
|
|
||||||
|
|
||||||
const findUnique = prisma.proclamation.findUnique as ReturnType<typeof vi.fn>;
|
|
||||||
|
|
||||||
describe('(protected)/admin/proclamations/[id] page server load', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
findUnique.mockReset();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws 404 when proclamation is not found', async () => {
|
|
||||||
findUnique.mockResolvedValue(null);
|
|
||||||
|
|
||||||
const err = await Promise.resolve(load({ params: { id: 'nonexistent' } } as any)).catch(
|
|
||||||
(e: unknown) => e
|
|
||||||
);
|
|
||||||
expect(isHttpError(err, 404)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns proclamation when found', async () => {
|
|
||||||
const proclamation = mockProclamation();
|
|
||||||
findUnique.mockResolvedValue(proclamation);
|
|
||||||
|
|
||||||
const result = (await load({ params: { id: 'proc-1' } } as any)) as any;
|
|
||||||
expect(result.proclamation).toEqual(proclamation);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('queries by params.id with frequency and category (including parent)', async () => {
|
|
||||||
findUnique.mockResolvedValue(mockProclamation());
|
|
||||||
|
|
||||||
await load({ params: { id: 'proc-1' } } as any);
|
|
||||||
|
|
||||||
expect(findUnique).toHaveBeenCalledWith({
|
|
||||||
where: { id: 'proc-1' },
|
|
||||||
include: {
|
|
||||||
solfeggioFrequency: true,
|
|
||||||
category: { include: { parent: true } },
|
|
||||||
subscriptionLevel: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns proclamation with parent category when present', async () => {
|
|
||||||
const proclamation = mockProclamation({
|
|
||||||
category: {
|
|
||||||
id: 2,
|
|
||||||
name: 'Inner Healing',
|
|
||||||
parentId: 1,
|
|
||||||
parent: { id: 1, name: 'Healing', parentId: null }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
findUnique.mockResolvedValue(proclamation);
|
|
||||||
|
|
||||||
const result = (await load({ params: { id: 'proc-1' } } as any)) as any;
|
|
||||||
expect(result.proclamation.category.parent.name).toBe('Healing');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
||||||
import { mockProclamation, mockCategory, createUrl } from '$lib/test-utils';
|
|
||||||
|
|
||||||
vi.mock('$lib/db', () => ({
|
|
||||||
prisma: {
|
|
||||||
proclamation: { findMany: vi.fn() },
|
|
||||||
proclamationCategory: { findMany: vi.fn() }
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { prisma } from '$lib/db';
|
|
||||||
import { load } from './+page.server';
|
|
||||||
|
|
||||||
const proclamationFindMany = prisma.proclamation.findMany as ReturnType<typeof vi.fn>;
|
|
||||||
const categoryFindMany = prisma.proclamationCategory.findMany as ReturnType<typeof vi.fn>;
|
|
||||||
|
|
||||||
describe('(protected)/admin/proclamations page server load', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
proclamationFindMany.mockReset();
|
|
||||||
categoryFindMany.mockReset();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns proclamations and categories with no filters', async () => {
|
|
||||||
const proclamations = [mockProclamation()];
|
|
||||||
const categories = [mockCategory()];
|
|
||||||
proclamationFindMany.mockResolvedValue(proclamations);
|
|
||||||
categoryFindMany.mockResolvedValue(categories);
|
|
||||||
|
|
||||||
const url = createUrl('/admin/proclamations');
|
|
||||||
const result = (await load({ url } as any)) as any;
|
|
||||||
expect(result.proclamations).toEqual(proclamations);
|
|
||||||
expect(result.categories[0]).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
...categories[0],
|
|
||||||
displayName: categories[0].name
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(result.search).toBe('');
|
|
||||||
expect(result.categoryId).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('passes search filter to findMany when search param is present', async () => {
|
|
||||||
proclamationFindMany.mockResolvedValue([]);
|
|
||||||
categoryFindMany.mockResolvedValue([]);
|
|
||||||
|
|
||||||
const url = createUrl('/admin/proclamations', { search: 'healing' });
|
|
||||||
await load({ url } as any);
|
|
||||||
|
|
||||||
expect(proclamationFindMany).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
where: { name: { contains: 'healing', mode: 'insensitive' } }
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns search string from URL params', async () => {
|
|
||||||
proclamationFindMany.mockResolvedValue([]);
|
|
||||||
categoryFindMany.mockResolvedValue([]);
|
|
||||||
|
|
||||||
const url = createUrl('/admin/proclamations', { search: 'peace' });
|
|
||||||
const result = (await load({ url } as any)) as any;
|
|
||||||
expect(result.search).toBe('peace');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('passes categoryId filter when category param is present', async () => {
|
|
||||||
proclamationFindMany.mockResolvedValue([]);
|
|
||||||
categoryFindMany.mockResolvedValue([]);
|
|
||||||
|
|
||||||
const url = createUrl('/admin/proclamations', { category: '3' });
|
|
||||||
await load({ url } as any);
|
|
||||||
|
|
||||||
expect(proclamationFindMany).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
where: { categoryId: 3 }
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns parsed categoryId from URL params', async () => {
|
|
||||||
proclamationFindMany.mockResolvedValue([]);
|
|
||||||
categoryFindMany.mockResolvedValue([]);
|
|
||||||
|
|
||||||
const url = createUrl('/admin/proclamations', { category: '5' });
|
|
||||||
const result = (await load({ url } as any)) as any;
|
|
||||||
expect(result.categoryId).toBe(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('combines search and category filters', async () => {
|
|
||||||
proclamationFindMany.mockResolvedValue([]);
|
|
||||||
categoryFindMany.mockResolvedValue([]);
|
|
||||||
|
|
||||||
const url = createUrl('/admin/proclamations', { search: 'love', category: '2' });
|
|
||||||
await load({ url } as any);
|
|
||||||
|
|
||||||
expect(proclamationFindMany).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
where: {
|
|
||||||
name: { contains: 'love', mode: 'insensitive' },
|
|
||||||
categoryId: 2
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('includes solfeggioFrequency and category relations', async () => {
|
|
||||||
proclamationFindMany.mockResolvedValue([]);
|
|
||||||
categoryFindMany.mockResolvedValue([]);
|
|
||||||
|
|
||||||
const url = createUrl('/admin/proclamations');
|
|
||||||
await load({ url } as any);
|
|
||||||
|
|
||||||
expect(proclamationFindMany).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
include: {
|
|
||||||
solfeggioFrequency: true,
|
|
||||||
category: { include: { parent: true } },
|
|
||||||
subscriptionLevel: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user