Restructure route organization
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { prisma } from '$lib/db';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
if (!locals.user?.id) {
|
||||
throw error(403, 'not authorized');
|
||||
}
|
||||
|
||||
const profile = await prisma.profile.findUnique({
|
||||
where: { userId: locals.user.id },
|
||||
select: { firstName: true }
|
||||
});
|
||||
|
||||
const [keyStats, proclamationCount] = await Promise.all([
|
||||
prisma.licenseKey.groupBy({
|
||||
by: ['active'],
|
||||
where: { userId: locals.user.id },
|
||||
_count: true
|
||||
}),
|
||||
prisma.proclamation.count()
|
||||
]);
|
||||
|
||||
const activeKeys = keyStats.find((s) => s.active)?._count ?? 0;
|
||||
const totalKeys = keyStats.reduce((sum, s) => sum + s._count, 0);
|
||||
|
||||
return {
|
||||
firstName: profile?.firstName ?? 'User',
|
||||
activeKeys,
|
||||
totalKeys,
|
||||
proclamationCount
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex h-full min-h-0 flex-col gap-6 p-6">
|
||||
<h1 class="text-2xl font-semibold text-gray-900">Welcome, {data.firstName}!</h1>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<a
|
||||
href="/license-keys"
|
||||
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md"
|
||||
>
|
||||
<p class="text-sm font-medium text-gray-500">Active License Keys</p>
|
||||
<p class="mt-2 text-3xl font-bold text-gray-900">{data.activeKeys}</p>
|
||||
<p class="mt-1 text-sm text-gray-400">{data.totalKeys} total</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/admin/proclamations"
|
||||
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md"
|
||||
>
|
||||
<p class="text-sm font-medium text-gray-500">Proclamations</p>
|
||||
<p class="mt-2 text-3xl font-bold text-gray-900">{data.proclamationCount}</p>
|
||||
<p class="mt-1 text-sm text-gray-400">Browse all</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/account"
|
||||
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md"
|
||||
>
|
||||
<p class="text-sm font-medium text-gray-500">Account</p>
|
||||
<p class="mt-2 text-lg font-semibold text-gray-900">Manage Profile</p>
|
||||
<p class="mt-1 text-sm text-gray-400">Edit your details</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,88 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { isHttpError } from '@sveltejs/kit';
|
||||
import { createLocals, mockUser } from '$lib/test-utils';
|
||||
|
||||
vi.mock('$lib/db', () => ({
|
||||
prisma: {
|
||||
profile: { findUnique: vi.fn() },
|
||||
licenseKey: { groupBy: vi.fn() },
|
||||
proclamation: { count: vi.fn() }
|
||||
}
|
||||
}));
|
||||
|
||||
import { prisma } from '$lib/db';
|
||||
import { load } from './+page.server';
|
||||
|
||||
const profileFindUnique = prisma.profile.findUnique as ReturnType<typeof vi.fn>;
|
||||
const licenseKeyGroupBy = prisma.licenseKey.groupBy as ReturnType<typeof vi.fn>;
|
||||
const proclamationCount = prisma.proclamation.count as ReturnType<typeof vi.fn>;
|
||||
|
||||
describe('(protected)/dashboard page server load', () => {
|
||||
beforeEach(() => {
|
||||
profileFindUnique.mockReset();
|
||||
licenseKeyGroupBy.mockReset();
|
||||
proclamationCount.mockReset();
|
||||
});
|
||||
|
||||
it('throws 403 when locals.user is undefined', async () => {
|
||||
const err = await Promise.resolve(load({ locals: createLocals() } as any)).catch(
|
||||
(e: unknown) => e
|
||||
);
|
||||
expect(isHttpError(err, 403)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns firstName from profile', async () => {
|
||||
profileFindUnique.mockResolvedValue({ firstName: 'Jane' });
|
||||
licenseKeyGroupBy.mockResolvedValue([]);
|
||||
proclamationCount.mockResolvedValue(0);
|
||||
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = (await load({ locals } as any)) as any;
|
||||
expect(result.firstName).toBe('Jane');
|
||||
});
|
||||
|
||||
it('defaults firstName to "User" when profile is null', async () => {
|
||||
profileFindUnique.mockResolvedValue(null);
|
||||
licenseKeyGroupBy.mockResolvedValue([]);
|
||||
proclamationCount.mockResolvedValue(0);
|
||||
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = (await load({ locals } as any)) as any;
|
||||
expect(result.firstName).toBe('User');
|
||||
});
|
||||
|
||||
it('computes activeKeys and totalKeys from groupBy results', async () => {
|
||||
profileFindUnique.mockResolvedValue({ firstName: 'Jane' });
|
||||
licenseKeyGroupBy.mockResolvedValue([
|
||||
{ active: true, _count: 3 },
|
||||
{ active: false, _count: 2 }
|
||||
]);
|
||||
proclamationCount.mockResolvedValue(10);
|
||||
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = (await load({ locals } as any)) as any;
|
||||
expect(result.activeKeys).toBe(3);
|
||||
expect(result.totalKeys).toBe(5);
|
||||
});
|
||||
|
||||
it('returns 0 for activeKeys when none are active', async () => {
|
||||
profileFindUnique.mockResolvedValue({ firstName: 'Jane' });
|
||||
licenseKeyGroupBy.mockResolvedValue([{ active: false, _count: 4 }]);
|
||||
proclamationCount.mockResolvedValue(0);
|
||||
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = (await load({ locals } as any)) as any;
|
||||
expect(result.activeKeys).toBe(0);
|
||||
expect(result.totalKeys).toBe(4);
|
||||
});
|
||||
|
||||
it('returns proclamationCount', async () => {
|
||||
profileFindUnique.mockResolvedValue({ firstName: 'Jane' });
|
||||
licenseKeyGroupBy.mockResolvedValue([]);
|
||||
proclamationCount.mockResolvedValue(42);
|
||||
|
||||
const locals = createLocals(mockUser() as any);
|
||||
const result = (await load({ locals } as any)) as any;
|
||||
expect(result.proclamationCount).toBe(42);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user