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