From a37575d8104d675fe8c1bf740605f387e13415ef Mon Sep 17 00:00:00 2001 From: eewing Date: Tue, 17 Mar 2026 09:13:27 -0500 Subject: [PATCH] Update training page to support YouTube video links. Refactored video upload handling to accept YouTube URLs, improved layout for better user experience, and added new route type definitions for training videos. Enhanced video display with embedded player support. --- .svelte-kit/generated/client/app.js | 8 +- .svelte-kit/generated/server/internal.js | 2 +- .svelte-kit/non-ambient.d.ts | 8 +- .svelte-kit/types/route_meta_data.json | 4 + .svelte-kit/types/src/routes/$types.d.ts | 2 +- src/routes/training/+page.server.ts | 28 ++-- src/routes/training/+page.svelte | 161 +++++++++++++++-------- src/routes/training/[id]/+page.server.ts | 46 +++++++ src/routes/training/[id]/+page.svelte | 103 +++++++++++++++ 9 files changed, 288 insertions(+), 74 deletions(-) create mode 100644 src/routes/training/[id]/+page.server.ts create mode 100644 src/routes/training/[id]/+page.svelte diff --git a/.svelte-kit/generated/client/app.js b/.svelte-kit/generated/client/app.js index 18faa68c..99133679 100644 --- a/.svelte-kit/generated/client/app.js +++ b/.svelte-kit/generated/client/app.js @@ -12,7 +12,8 @@ export const nodes = [ () => import('./nodes/8'), () => import('./nodes/9'), () => import('./nodes/10'), - () => import('./nodes/11') + () => import('./nodes/11'), + () => import('./nodes/12') ]; export const server_loads = [0]; @@ -24,8 +25,9 @@ export const dictionary = { "/notifications": [~7], "/profile": [8], "/training": [~9], - "/users": [~10], - "/users/[id]": [~11], + "/training/[id]": [~10], + "/users": [~11], + "/users/[id]": [~12], "/[site]": [~4,[2]] }; diff --git a/.svelte-kit/generated/server/internal.js b/.svelte-kit/generated/server/internal.js index 95737441..e6c7ce81 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: "1mkjc4f" + version_hash: "n9d9oa" }; export async function get_hooks() { diff --git a/.svelte-kit/non-ambient.d.ts b/.svelte-kit/non-ambient.d.ts index f853893b..c2924641 100644 --- a/.svelte-kit/non-ambient.d.ts +++ b/.svelte-kit/non-ambient.d.ts @@ -27,9 +27,10 @@ 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" | "/users" | "/users/[id]" | "/[site]"; + 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]"; RouteParams(): { "/api/notifications/[id]": { id: string }; + "/training/[id]": { id: string }; "/users/[id]": { id: string }; "/[site]": { site: string } }; @@ -45,12 +46,13 @@ declare module "$app/types" { "/logout": Record; "/notifications": Record; "/profile": Record; - "/training": Record; + "/training": { id?: string }; + "/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" | "/users" | `/users/${string}` & {} | `/${string}` & {}; + Pathname(): "/" | "/api/darkmode" | "/api/notifications" | `/api/notifications/${string}` & {} | "/api/sso/exchange" | "/login" | "/logout" | "/notifications" | "/profile" | "/training" | `/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 aba4d41a..11473b2d 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/[id]": [ + "src/routes/training/[id]/+page.server.ts", + "src/routes/+layout.server.ts" + ], "/users": [ "src/routes/users/+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 25f82e5e..6391558a 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" | "/users" | "/users/[id]" | null +type LayoutRouteId = RouteId | "/" | "/[site]" | "/login" | "/logout" | "/notifications" | "/profile" | "/training" | "/training/[id]" | "/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 c397f5c2..334cf203 100644 --- a/src/routes/training/+page.server.ts +++ b/src/routes/training/+page.server.ts @@ -9,8 +9,10 @@ type TrainingVideo = { videoUrl: string; }; -const VIDEO_COLLECTION = 'year'; -const VIDEO_FIELD = 'Media'; +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; @@ -27,14 +29,16 @@ export const load: PageServerLoad = async ({ locals }) => { } const videos: TrainingVideo[] = records.map((record: any) => { - const fileName = record[VIDEO_FIELD]; - const videoUrl = fileName - ? locals.pb.files.getUrl(record, fileName) - : ''; + let videoUrl = (record[VIDEO_LINK_FIELD] as string) ?? ''; + + // Fallback: if there is no link but there is a file, build a file URL + if (!videoUrl && record[VIDEO_FILE_FIELD]) { + videoUrl = locals.pb.files.getUrl(record, record[VIDEO_FILE_FIELD]); + } return { id: record.id as string, - title: (record.title as string) ?? 'Untitled video', + 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 @@ -67,7 +71,7 @@ export const actions: Actions = { const title = String(formData.get('title') ?? '').trim(); const description = String(formData.get('description') ?? '').trim(); - const file = formData.get('video'); + const link = String(formData.get('link') ?? '').trim(); if (!title) { return fail(400, { @@ -76,10 +80,10 @@ export const actions: Actions = { }); } - if (!(file instanceof File)) { + if (!link) { return fail(400, { - error: 'Please choose a video file to upload.', - values: { title, description } + error: 'YouTube link is required.', + values: { title, description, link } }); } @@ -88,7 +92,7 @@ export const actions: Actions = { if (description) { createData.set('description', description); } - createData.set(VIDEO_FIELD, file); + createData.set(VIDEO_LINK_FIELD, link); try { await locals.pb.collection(VIDEO_COLLECTION).create(createData); diff --git a/src/routes/training/+page.svelte b/src/routes/training/+page.svelte index 39ff305d..325dd8a5 100644 --- a/src/routes/training/+page.svelte +++ b/src/routes/training/+page.svelte @@ -16,6 +16,7 @@ values?: { title?: string; description?: string; + link?: string; }; } | null; @@ -34,33 +35,72 @@ year: d.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined }); } + + function toEmbedUrl(url: string) { + if (!url) return ''; + + try { + const u = new URL(url); + + // Short youtu.be/ + if (u.hostname === 'youtu.be') { + return `https://www.youtube.com/embed${u.pathname}`; + } + + // www.youtube.com/watch?v= or youtube.com/watch?v= + if (u.hostname.includes('youtube.com')) { + if (u.pathname === '/watch' && u.searchParams.get('v')) { + const id = u.searchParams.get('v'); + return `https://www.youtube.com/embed/${id}`; + } + + // /embed/ already in embed form + if (u.pathname.startsWith('/embed/')) { + return url; + } + + // Shorts: /shorts/ + if (u.pathname.startsWith('/shorts/')) { + const id = u.pathname.split('/')[2]; + if (id) { + return `https://www.youtube.com/embed/${id}`; + } + } + } + } catch { + // fall through to raw url + } + + return url; + } Training — Base -
- - - +
+
+ + + -
-

Training

-
+
+

Training

+
- {#if canUpload} -
+ {#if canUpload} +
- Upload training video - Add a new video for your team. + Add training video + Add a new YouTube video for your team. {#if form?.error}

{form.error}

{/if} -
+
@@ -82,10 +122,17 @@ />
- - + +

- Upload an MP4 or WebM file. Size limits follow PocketBase settings. + Paste a full YouTube URL (for example, https://www.youtube.com/watch?v=...).

@@ -94,45 +141,51 @@ -
- {/if} - -
- {#if videos.length === 0} -

No training videos yet.

- {:else} -
- {#each videos as video (video.id)} - - - {video.title} - {formatDate(video.created)} - - - {#if video.videoUrl} -
-
- -
-
- {:else} -

Video file is missing.

- {/if} - {#if video.description} -

{video.description}

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

No training videos yet.

+ {:else} +
+ {#each videos as video (video.id)} + + +
+ {video.title} + {formatDate(video.created)} +
+ + + +
+ + {#if video.videoUrl} +
+
+ +
+
+ {:else} +

Video link is missing.

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

{video.description}

+ {/if} +
+
+ {/each} +
+ {/if} +
+
diff --git a/src/routes/training/[id]/+page.server.ts b/src/routes/training/[id]/+page.server.ts new file mode 100644 index 00000000..0351f83b --- /dev/null +++ b/src/routes/training/[id]/+page.server.ts @@ -0,0 +1,46 @@ +import { error } from '@sveltejs/kit'; +import type { 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, params }) => { + const { id } = params; + + let record: any; + + try { + record = await locals.pb.collection(VIDEO_COLLECTION).getOne(id); + } catch (e) { + throw error(404, 'Training video not found'); + } + + let videoUrl = (record[VIDEO_LINK_FIELD] as string) ?? ''; + + if (!videoUrl && record[VIDEO_FILE_FIELD]) { + videoUrl = locals.pb.files.getUrl(record, record[VIDEO_FILE_FIELD]); + } + + const video: TrainingVideo = { + 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 { + video + }; +}; + diff --git a/src/routes/training/[id]/+page.svelte b/src/routes/training/[id]/+page.svelte new file mode 100644 index 00000000..d438179c --- /dev/null +++ b/src/routes/training/[id]/+page.svelte @@ -0,0 +1,103 @@ + + + + {video.title || 'Training video'} — Base + + +
+
+
+ + + +

{formatDate(video.created)}

+
+ + + + {video.title} + {#if video.description} + {video.description} + {/if} + + + {#if video.videoUrl} +
+
+ +
+
+ {:else} +

Video link is missing.

+ {/if} +
+
+
+
+