From ab226470a1bd5b56584096c25b229c6c30827463 Mon Sep 17 00:00:00 2001 From: eewing Date: Wed, 18 Mar 2026 07:59:13 -0500 Subject: [PATCH] Enhance training route and admin functionality. Added new admin route for training, updated type definitions to include admin checks, and modified training page to conditionally display admin link based on user permissions. Improved data handling for training videos and user roles. --- .svelte-kit/generated/client/app.js | 8 +- .svelte-kit/generated/server/internal.js | 2 +- .svelte-kit/non-ambient.d.ts | 5 +- .svelte-kit/types/route_meta_data.json | 4 + .svelte-kit/types/src/routes/$types.d.ts | 2 +- src/routes/training/+page.server.ts | 4 +- src/routes/training/+page.svelte | 8 +- src/routes/training/admin/+page.server.ts | 98 +++++++++++++++++++++ src/routes/training/admin/+page.svelte | 100 ++++++++++++++++++++++ 9 files changed, 222 insertions(+), 9 deletions(-) create mode 100644 src/routes/training/admin/+page.server.ts create mode 100644 src/routes/training/admin/+page.svelte diff --git a/.svelte-kit/generated/client/app.js b/.svelte-kit/generated/client/app.js index 99133679..241470c7 100644 --- a/.svelte-kit/generated/client/app.js +++ b/.svelte-kit/generated/client/app.js @@ -13,7 +13,8 @@ export const nodes = [ () => import('./nodes/9'), () => import('./nodes/10'), () => import('./nodes/11'), - () => import('./nodes/12') + () => import('./nodes/12'), + () => import('./nodes/13') ]; export const server_loads = [0]; @@ -25,9 +26,10 @@ export const dictionary = { "/notifications": [~7], "/profile": [8], "/training": [~9], + "/training/admin": [~11], "/training/[id]": [~10], - "/users": [~11], - "/users/[id]": [~12], + "/users": [~12], + "/users/[id]": [~13], "/[site]": [~4,[2]] }; diff --git a/.svelte-kit/generated/server/internal.js b/.svelte-kit/generated/server/internal.js index e6c7ce81..30d359e5 100644 --- a/.svelte-kit/generated/server/internal.js +++ b/.svelte-kit/generated/server/internal.js @@ -24,7 +24,7 @@ export const options = { app: ({ head, body, assets, nonce, env }) => "\n\n \n \n \n \n \n " + head + "\n \n \n
" + body + "
\n \n\n", error: ({ status, message }) => "\n\n\t\n\t\t\n\t\t" + message + "\n\n\t\t\n\t\n\t\n\t\t
\n\t\t\t" + status + "\n\t\t\t
\n\t\t\t\t

" + message + "

\n\t\t\t
\n\t\t
\n\t\n\n" }, - version_hash: "n9d9oa" + version_hash: "sskapp" }; export async function get_hooks() { diff --git a/.svelte-kit/non-ambient.d.ts b/.svelte-kit/non-ambient.d.ts index c2924641..44a3e86c 100644 --- a/.svelte-kit/non-ambient.d.ts +++ b/.svelte-kit/non-ambient.d.ts @@ -27,7 +27,7 @@ export {}; declare module "$app/types" { export interface AppTypes { - RouteId(): "/" | "/api" | "/api/darkmode" | "/api/notifications" | "/api/notifications/[id]" | "/api/sso" | "/api/sso/exchange" | "/login" | "/logout" | "/notifications" | "/profile" | "/training" | "/training/[id]" | "/users" | "/users/[id]" | "/[site]"; + RouteId(): "/" | "/api" | "/api/darkmode" | "/api/notifications" | "/api/notifications/[id]" | "/api/sso" | "/api/sso/exchange" | "/login" | "/logout" | "/notifications" | "/profile" | "/training" | "/training/admin" | "/training/[id]" | "/users" | "/users/[id]" | "/[site]"; RouteParams(): { "/api/notifications/[id]": { id: string }; "/training/[id]": { id: string }; @@ -47,12 +47,13 @@ declare module "$app/types" { "/notifications": Record; "/profile": Record; "/training": { id?: string }; + "/training/admin": Record; "/training/[id]": { id: string }; "/users": { id?: string }; "/users/[id]": { id: string }; "/[site]": { site: string } }; - Pathname(): "/" | "/api/darkmode" | "/api/notifications" | `/api/notifications/${string}` & {} | "/api/sso/exchange" | "/login" | "/logout" | "/notifications" | "/profile" | "/training" | `/training/${string}` & {} | "/users" | `/users/${string}` & {} | `/${string}` & {}; + Pathname(): "/" | "/api/darkmode" | "/api/notifications" | `/api/notifications/${string}` & {} | "/api/sso/exchange" | "/login" | "/logout" | "/notifications" | "/profile" | "/training" | "/training/admin" | `/training/${string}` & {} | "/users" | `/users/${string}` & {} | `/${string}` & {}; ResolvedPathname(): `${"" | `/${string}`}${ReturnType}`; Asset(): string & {}; } diff --git a/.svelte-kit/types/route_meta_data.json b/.svelte-kit/types/route_meta_data.json index 11473b2d..0495d508 100644 --- a/.svelte-kit/types/route_meta_data.json +++ b/.svelte-kit/types/route_meta_data.json @@ -34,6 +34,10 @@ "src/routes/training/+page.server.ts", "src/routes/+layout.server.ts" ], + "/training/admin": [ + "src/routes/training/admin/+page.server.ts", + "src/routes/+layout.server.ts" + ], "/training/[id]": [ "src/routes/training/[id]/+page.server.ts", "src/routes/+layout.server.ts" diff --git a/.svelte-kit/types/src/routes/$types.d.ts b/.svelte-kit/types/src/routes/$types.d.ts index 6391558a..a1e6dcbd 100644 --- a/.svelte-kit/types/src/routes/$types.d.ts +++ b/.svelte-kit/types/src/routes/$types.d.ts @@ -12,7 +12,7 @@ type EnsureDefined = T extends null | undefined ? {} : T; type OptionalUnion, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude]?: never } & U : never; export type Snapshot = Kit.Snapshot; type PageParentData = EnsureDefined; -type LayoutRouteId = RouteId | "/" | "/[site]" | "/login" | "/logout" | "/notifications" | "/profile" | "/training" | "/training/[id]" | "/users" | "/users/[id]" | null +type LayoutRouteId = RouteId | "/" | "/[site]" | "/login" | "/logout" | "/notifications" | "/profile" | "/training" | "/training/[id]" | "/training/admin" | "/users" | "/users/[id]" | null type LayoutParams = RouteParams & { site?: string; id?: string } type LayoutServerParentData = EnsureDefined<{}>; type LayoutParentData = EnsureDefined<{}>; diff --git a/src/routes/training/+page.server.ts b/src/routes/training/+page.server.ts index 334cf203..cee4df91 100644 --- a/src/routes/training/+page.server.ts +++ b/src/routes/training/+page.server.ts @@ -46,10 +46,12 @@ export const load: PageServerLoad = async ({ locals }) => { }); const canUpload = Boolean(user?.training_admin); + const isAdmin = Boolean((user as any)?.Admin); return { videos, - canUpload + canUpload, + isAdmin }; }; diff --git a/src/routes/training/+page.svelte b/src/routes/training/+page.svelte index 325dd8a5..8012fc92 100644 --- a/src/routes/training/+page.svelte +++ b/src/routes/training/+page.svelte @@ -20,11 +20,12 @@ }; } | null; - let { data, form }: { data: { videos: TrainingVideo[]; canUpload: boolean }; form: UploadFormState } = + let { data, form }: { data: { videos: TrainingVideo[]; canUpload: boolean; isAdmin: boolean }; form: UploadFormState } = $props(); const videos = $derived(data.videos); const canUpload = $derived(data.canUpload); + const isAdmin = $derived(data.isAdmin); function formatDate(created: string) { if (!created) return ''; @@ -87,6 +88,11 @@

Training

+ {#if isAdmin} + + + + {/if}
{#if canUpload} diff --git a/src/routes/training/admin/+page.server.ts b/src/routes/training/admin/+page.server.ts new file mode 100644 index 00000000..f02efe8e --- /dev/null +++ b/src/routes/training/admin/+page.server.ts @@ -0,0 +1,98 @@ +import { error, fail, redirect } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; + +type TrainingVideo = { + id: string; + title: string; + description: string | null; + created: string; + videoUrl: string; +}; + +const VIDEO_COLLECTION = 'media'; +const VIDEO_LINK_FIELD = 'Link'; +const VIDEO_FILE_FIELD = 'Media'; +const VIDEO_TITLE_FIELD = 'Title'; + +export const load: PageServerLoad = async ({ locals }) => { + const user = locals.user; + + if (!user) { + throw redirect(303, '/login'); + } + + if (!(user as any)?.Admin) { + throw error(403, 'You do not have access to training admin.'); + } + + let records: any[] = []; + + try { + records = await locals.pb.collection(VIDEO_COLLECTION).getFullList({ + sort: '-created' + }); + } catch (e) { + console.error('Failed to load training videos from PocketBase', e); + records = []; + } + + const videos: TrainingVideo[] = records.map((record: any) => { + let videoUrl = (record[VIDEO_LINK_FIELD] as string) ?? ''; + + if (!videoUrl && record[VIDEO_FILE_FIELD]) { + videoUrl = locals.pb.files.getUrl(record, record[VIDEO_FILE_FIELD]); + } + + return { + id: record.id as string, + title: (record[VIDEO_TITLE_FIELD] as string) ?? (record.title as string) ?? 'Untitled video', + description: (record.description as string) ?? null, + created: record.created as string, + videoUrl + }; + }); + + return { + videos + }; +}; + +export const actions: Actions = { + delete: async ({ request, locals }) => { + const user = locals.user; + + if (!user) { + throw redirect(303, '/login'); + } + + if (!(user as any)?.Admin) { + return fail(403, { + error: 'You do not have permission to manage training videos.' + }); + } + + const formData = await request.formData(); + const id = String(formData.get('id') ?? '').trim(); + + if (!id) { + return fail(400, { + error: 'Missing video id.' + }); + } + + try { + await locals.pb.collection(VIDEO_COLLECTION).delete(id); + } catch (e) { + const message = + e && typeof e === 'object' && 'message' in e + ? String((e as { message: string }).message) + : 'Could not delete video.'; + return fail(500, { + error: message + }); + } + + throw redirect(303, '/training/admin'); + } +}; + diff --git a/src/routes/training/admin/+page.svelte b/src/routes/training/admin/+page.svelte new file mode 100644 index 00000000..5cabe673 --- /dev/null +++ b/src/routes/training/admin/+page.svelte @@ -0,0 +1,100 @@ + + + + Training admin — Base + + +
+
+
+ + + +

Training admin

+
+ + + + Videos + Manage training videos in the Media collection. + + + {#if form?.error} +

{form.error}

+ {/if} + + {#if videos.length === 0} +

No videos found.

+ {:else} +
+ {#each videos as video (video.id)} +
+
+
+

{video.title}

+ {formatDate(video.created)} +
+ {#if video.description} +

+ {video.description} +

+ {/if} + {#if video.videoUrl} +

+ {video.videoUrl} +

+ {/if} +
+
+ + + + + +
+
+ {/each} +
+ {/if} +
+
+
+
+