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} +
+
+
+
+