diff --git a/.svelte-kit/generated/client/app.js b/.svelte-kit/generated/client/app.js
index d9690aab..18faa68c 100644
--- a/.svelte-kit/generated/client/app.js
+++ b/.svelte-kit/generated/client/app.js
@@ -11,20 +11,22 @@ export const nodes = [
() => import('./nodes/7'),
() => import('./nodes/8'),
() => import('./nodes/9'),
- () => import('./nodes/10')
+ () => import('./nodes/10'),
+ () => import('./nodes/11')
];
export const server_loads = [0];
export const dictionary = {
"/": [3],
- "/login": [~10],
- "/logout": [~4],
- "/notifications": [~9],
- "/profile": [5],
- "/users": [~6],
- "/users/[id]": [~7],
- "/[site]": [~8,[2]]
+ "/login": [~5],
+ "/logout": [~6],
+ "/notifications": [~7],
+ "/profile": [8],
+ "/training": [~9],
+ "/users": [~10],
+ "/users/[id]": [~11],
+ "/[site]": [~4,[2]]
};
export const hooks = {
diff --git a/.svelte-kit/generated/client/nodes/4.js b/.svelte-kit/generated/client/nodes/4.js
index 2a513383..0f681549 100644
--- a/.svelte-kit/generated/client/nodes/4.js
+++ b/.svelte-kit/generated/client/nodes/4.js
@@ -1 +1 @@
-export { default as component } from "../../../../src/routes/logout/+page.svelte";
\ No newline at end of file
+export { default as component } from "../../../../src/routes/[site]/+page.svelte";
\ No newline at end of file
diff --git a/.svelte-kit/generated/client/nodes/5.js b/.svelte-kit/generated/client/nodes/5.js
index 513e7da1..f2b26cd1 100644
--- a/.svelte-kit/generated/client/nodes/5.js
+++ b/.svelte-kit/generated/client/nodes/5.js
@@ -1 +1 @@
-export { default as component } from "../../../../src/routes/profile/+page.svelte";
\ No newline at end of file
+export { default as component } from "../../../../src/routes/login/+page.svelte";
\ No newline at end of file
diff --git a/.svelte-kit/generated/client/nodes/6.js b/.svelte-kit/generated/client/nodes/6.js
index 81499dfe..2a513383 100644
--- a/.svelte-kit/generated/client/nodes/6.js
+++ b/.svelte-kit/generated/client/nodes/6.js
@@ -1 +1 @@
-export { default as component } from "../../../../src/routes/users/+page.svelte";
\ No newline at end of file
+export { default as component } from "../../../../src/routes/logout/+page.svelte";
\ No newline at end of file
diff --git a/.svelte-kit/generated/client/nodes/7.js b/.svelte-kit/generated/client/nodes/7.js
index e240dfec..6b52a7f8 100644
--- a/.svelte-kit/generated/client/nodes/7.js
+++ b/.svelte-kit/generated/client/nodes/7.js
@@ -1 +1 @@
-export { default as component } from "../../../../src/routes/users/[id]/+page.svelte";
\ No newline at end of file
+export { default as component } from "../../../../src/routes/notifications/+page.svelte";
\ No newline at end of file
diff --git a/.svelte-kit/generated/client/nodes/8.js b/.svelte-kit/generated/client/nodes/8.js
index 0f681549..513e7da1 100644
--- a/.svelte-kit/generated/client/nodes/8.js
+++ b/.svelte-kit/generated/client/nodes/8.js
@@ -1 +1 @@
-export { default as component } from "../../../../src/routes/[site]/+page.svelte";
\ No newline at end of file
+export { default as component } from "../../../../src/routes/profile/+page.svelte";
\ No newline at end of file
diff --git a/.svelte-kit/generated/server/internal.js b/.svelte-kit/generated/server/internal.js
index a6d60d75..95737441 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: "3lzvrw"
+ version_hash: "1mkjc4f"
};
export async function get_hooks() {
diff --git a/.svelte-kit/non-ambient.d.ts b/.svelte-kit/non-ambient.d.ts
index 01b2cccc..f853893b 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" | "/users" | "/users/[id]" | "/[site]";
+ RouteId(): "/" | "/api" | "/api/darkmode" | "/api/notifications" | "/api/notifications/[id]" | "/api/sso" | "/api/sso/exchange" | "/login" | "/logout" | "/notifications" | "/profile" | "/training" | "/users" | "/users/[id]" | "/[site]";
RouteParams(): {
"/api/notifications/[id]": { id: string };
"/users/[id]": { id: string };
@@ -45,11 +45,12 @@ declare module "$app/types" {
"/logout": Record;
"/notifications": Record;
"/profile": Record;
+ "/training": Record;
"/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" | "/users" | `/users/${string}` & {} | `/${string}` & {};
+ Pathname(): "/" | "/api/darkmode" | "/api/notifications" | `/api/notifications/${string}` & {} | "/api/sso/exchange" | "/login" | "/logout" | "/notifications" | "/profile" | "/training" | "/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 4a3bf115..aba4d41a 100644
--- a/.svelte-kit/types/route_meta_data.json
+++ b/.svelte-kit/types/route_meta_data.json
@@ -30,6 +30,10 @@
"/profile": [
"src/routes/+layout.server.ts"
],
+ "/training": [
+ "src/routes/training/+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 7ec9fd77..25f82e5e 100644
--- a/.svelte-kit/types/src/routes/$types.d.ts
+++ b/.svelte-kit/types/src/routes/$types.d.ts
@@ -12,8 +12,8 @@ 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 | "/" | "/logout" | "/profile" | "/users" | "/users/[id]" | "/[site]" | "/notifications" | "/login" | null
-type LayoutParams = RouteParams & { id?: string; site?: string }
+type LayoutRouteId = RouteId | "/" | "/[site]" | "/login" | "/logout" | "/notifications" | "/profile" | "/training" | "/users" | "/users/[id]" | null
+type LayoutParams = RouteParams & { site?: string; id?: string }
type LayoutServerParentData = EnsureDefined<{}>;
type LayoutParentData = EnsureDefined<{}>;
diff --git a/node_modules/.vite/_svelte_metadata.json b/node_modules/.vite/_svelte_metadata.json
index 052624ba..1768eb83 100644
--- a/node_modules/.vite/_svelte_metadata.json
+++ b/node_modules/.vite/_svelte_metadata.json
@@ -1 +1 @@
-{"compilerOptions":{"css":"external","dev":true,"hmr":true},"configFile":false,"extensions":[".svelte"],"preprocess":[{"name":"vite-preprocess","style":"async ({ attributes, content, filename = \"\" }) => {\n const ext = attributes.lang ? `.${attributes.lang}` : \".css\";\n if (attributes.lang && !isCSSRequest(ext))\n return;\n if (!cssTransform)\n cssTransform = createCssTransform(style, config).then((t) => cssTransform = t);\n const transform = await cssTransform, suffix = `${lang_sep}${ext}`, moduleId = `${filename}${suffix}`, { code, map, deps } = await transform(content, moduleId);\n removeLangSuffix(map, suffix);\n mapToRelative(map, filename);\n const dependencies = deps ? Array.from(deps).filter((d) => !d.endsWith(suffix)) : void 0;\n return {\n code,\n map: map ?? void 0,\n dependencies\n };\n }"},{"script":"({ content, filename }) => {\n if (!filename)\n return;\n const basename = path.basename(filename);\n if (basename.startsWith(\"+page.\") || basename.startsWith(\"+layout.\")) {\n const match = content.match(options_regex);\n if (match && match.index !== void 0 && !should_ignore(content, match.index)) {\n const fixed = basename.replace(\".svelte\", \"(.server).js/ts\"), message = `\n${colors.bold().red(path.relative(\".\", filename))}\n\\`${match[1]}\\` will be ignored \\u2014 move it to ${fixed} instead. See https://svelte.dev/docs/kit/page-options for more information.`;\n if (!warned.has(message)) {\n console.log(message);\n warned.add(message);\n }\n }\n }\n }","markup":"({ content, filename }) => {\n if (!filename)\n return;\n if (path.basename(filename).startsWith(\"+layout.\") && !has_children(content, isSvelte5Plus())) {\n const message = `\n${colors.bold().red(path.relative(\".\", filename))}\n\\`\\`${isSvelte5Plus() ? \" or `{@render ...}` tag\" : \"\"}` + \" missing \\u2014 inner content will not be rendered\";\n if (!warned.has(message)) {\n console.log(message);\n warned.add(message);\n }\n }\n }"}]}
\ No newline at end of file
+{"compilerOptions":{"css":"external","dev":true,"hmr":true},"configFile":false,"extensions":[".svelte"],"preprocess":[{"name":"vite-preprocess","style":"async ({ attributes, content, filename = '' }) => {\n\t\tconst ext = attributes.lang ? `.${attributes.lang}` : '.css';\n\t\tif (attributes.lang && !isCSSRequest(ext)) return;\n\t\tif (!cssTransform) {\n\t\t\tcssTransform = createCssTransform(style, config).then((t) => (cssTransform = t));\n\t\t}\n\t\tconst transform = await cssTransform;\n\t\tconst suffix = `${lang_sep}${ext}`;\n\t\tconst moduleId = `${filename}${suffix}`;\n\t\tconst { code, map, deps } = await transform(content, moduleId);\n\t\tremoveLangSuffix(map, suffix);\n\t\tmapToRelative(map, filename);\n\t\tconst dependencies = deps ? Array.from(deps).filter((d) => !d.endsWith(suffix)) : undefined;\n\t\treturn {\n\t\t\tcode,\n\t\t\tmap: map ?? undefined,\n\t\t\tdependencies\n\t\t};\n\t}"},{"script":"({ content, filename }) => {\n\t\tif (!filename) return;\n\n\t\tconst basename = path.basename(filename);\n\t\tif (basename.startsWith('+page.') || basename.startsWith('+layout.')) {\n\t\t\tconst match = content.match(options_regex);\n\t\t\tif (match && match.index !== undefined && !should_ignore(content, match.index)) {\n\t\t\t\tconst fixed = basename.replace('.svelte', '(.server).js/ts');\n\n\t\t\t\tconst message =\n\t\t\t\t\t`\\n${colors.bold().red(path.relative('.', filename))}\\n` +\n\t\t\t\t\t`\\`${match[1]}\\` will be ignored — move it to ${fixed} instead. See https://svelte.dev/docs/kit/page-options for more information.`;\n\n\t\t\t\tif (!warned.has(message)) {\n\t\t\t\t\tconsole.log(message);\n\t\t\t\t\twarned.add(message);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}","markup":"({ content, filename }) => {\n\t\tif (!filename) return;\n\n\t\tconst basename = path.basename(filename);\n\n\t\tif (basename.startsWith('+layout.') && !has_children(content, isSvelte5Plus())) {\n\t\t\tconst message =\n\t\t\t\t`\\n${colors.bold().red(path.relative('.', filename))}\\n` +\n\t\t\t\t`\\`\\`${isSvelte5Plus() ? ' or `{@render ...}` tag' : ''}` +\n\t\t\t\t' missing — inner content will not be rendered';\n\n\t\t\tif (!warned.has(message)) {\n\t\t\t\tconsole.log(message);\n\t\t\t\twarned.add(message);\n\t\t\t}\n\t\t}\n\t}"}]}
\ No newline at end of file
diff --git a/src/app.d.ts b/src/app.d.ts
index 66ce6de4..71b847b3 100644
--- a/src/app.d.ts
+++ b/src/app.d.ts
@@ -15,6 +15,7 @@ declare global {
tasgrid?: boolean;
stackq?: boolean;
hrm?: boolean;
+ training_admin?: boolean;
} | null;
}
}
diff --git a/src/lib/user-from-record.ts b/src/lib/user-from-record.ts
index bb774e4c..2f202932 100644
--- a/src/lib/user-from-record.ts
+++ b/src/lib/user-from-record.ts
@@ -23,6 +23,7 @@ export function mapUserRecord(r: Record): UserFromRecord {
prism: bool(r, 'PRISM', 'prism', 'Prism'),
tasgrid: bool(r, 'TasGrid', 'tasgrid'),
stackq: bool(r, 'Stackq', 'stackq'),
- hrm: bool(r, 'HRM', 'hrm', 'Hrm')
+ hrm: bool(r, 'HRM', 'hrm', 'Hrm'),
+ training_admin: bool(r, 'training_admin', 'TrainingAdmin', 'trainingAdmin')
};
}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 669f8788..b64dc9c2 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,9 +1,10 @@
{data.title} — Base
-
-
-
+
{data.title}
+
+ This app now opens in a separate tab.
+
+
-
-
+ Open {data.title}
+
+
+ ← Back to Base
+
diff --git a/src/routes/training/+page.server.ts b/src/routes/training/+page.server.ts
new file mode 100644
index 00000000..c397f5c2
--- /dev/null
+++ b/src/routes/training/+page.server.ts
@@ -0,0 +1,109 @@
+import { 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 = 'year';
+const VIDEO_FIELD = 'Media';
+
+export const load: PageServerLoad = async ({ locals }) => {
+ const user = locals.user;
+
+ 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) => {
+ const fileName = record[VIDEO_FIELD];
+ const videoUrl = fileName
+ ? locals.pb.files.getUrl(record, fileName)
+ : '';
+
+ return {
+ id: record.id as string,
+ title: (record.title as string) ?? 'Untitled video',
+ description: (record.description as string) ?? null,
+ created: record.created as string,
+ videoUrl
+ };
+ });
+
+ const canUpload = Boolean(user?.training_admin);
+
+ return {
+ videos,
+ canUpload
+ };
+};
+
+export const actions: Actions = {
+ upload: async ({ request, locals }) => {
+ const user = locals.user;
+
+ if (!user) {
+ throw redirect(303, '/login');
+ }
+
+ if (!user.training_admin) {
+ return fail(403, {
+ error: 'You do not have permission to upload training videos.'
+ });
+ }
+
+ const formData = await request.formData();
+
+ const title = String(formData.get('title') ?? '').trim();
+ const description = String(formData.get('description') ?? '').trim();
+ const file = formData.get('video');
+
+ if (!title) {
+ return fail(400, {
+ error: 'Title is required.',
+ values: { title, description }
+ });
+ }
+
+ if (!(file instanceof File)) {
+ return fail(400, {
+ error: 'Please choose a video file to upload.',
+ values: { title, description }
+ });
+ }
+
+ const createData = new FormData();
+ createData.set('title', title);
+ if (description) {
+ createData.set('description', description);
+ }
+ createData.set(VIDEO_FIELD, file);
+
+ try {
+ await locals.pb.collection(VIDEO_COLLECTION).create(createData);
+ } catch (e) {
+ const message =
+ e && typeof e === 'object' && 'message' in e
+ ? String((e as { message: string }).message)
+ : 'Could not upload video.';
+ return fail(500, {
+ error: message,
+ values: { title, description }
+ });
+ }
+
+ throw redirect(303, '/training');
+ }
+};
+
diff --git a/src/routes/training/+page.svelte b/src/routes/training/+page.svelte
new file mode 100644
index 00000000..39ff305d
--- /dev/null
+++ b/src/routes/training/+page.svelte
@@ -0,0 +1,138 @@
+
+
+
+ Training — Base
+
+
+
+
+
+
+
+
+
Training
+
+
+ {#if canUpload}
+
+
+
+ Upload training video
+ Add a new video for your team.
+
+
+ {#if form?.error}
+ {form.error}
+ {/if}
+
+
+
+
+ {/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}
+
+
+