Enhance routing and type definitions for training page. Added new training route and updated related components, including navigation links and user data handling. Refactored layout for improved user experience and consistency across site pages.
This commit is contained in:
@@ -11,20 +11,22 @@ export const nodes = [
|
|||||||
() => import('./nodes/7'),
|
() => import('./nodes/7'),
|
||||||
() => import('./nodes/8'),
|
() => import('./nodes/8'),
|
||||||
() => import('./nodes/9'),
|
() => import('./nodes/9'),
|
||||||
() => import('./nodes/10')
|
() => import('./nodes/10'),
|
||||||
|
() => import('./nodes/11')
|
||||||
];
|
];
|
||||||
|
|
||||||
export const server_loads = [0];
|
export const server_loads = [0];
|
||||||
|
|
||||||
export const dictionary = {
|
export const dictionary = {
|
||||||
"/": [3],
|
"/": [3],
|
||||||
"/login": [~10],
|
"/login": [~5],
|
||||||
"/logout": [~4],
|
"/logout": [~6],
|
||||||
"/notifications": [~9],
|
"/notifications": [~7],
|
||||||
"/profile": [5],
|
"/profile": [8],
|
||||||
"/users": [~6],
|
"/training": [~9],
|
||||||
"/users/[id]": [~7],
|
"/users": [~10],
|
||||||
"/[site]": [~8,[2]]
|
"/users/[id]": [~11],
|
||||||
|
"/[site]": [~4,[2]]
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hooks = {
|
export const hooks = {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { default as component } from "../../../../src/routes/logout/+page.svelte";
|
export { default as component } from "../../../../src/routes/[site]/+page.svelte";
|
||||||
@@ -1 +1 @@
|
|||||||
export { default as component } from "../../../../src/routes/profile/+page.svelte";
|
export { default as component } from "../../../../src/routes/login/+page.svelte";
|
||||||
@@ -1 +1 @@
|
|||||||
export { default as component } from "../../../../src/routes/users/+page.svelte";
|
export { default as component } from "../../../../src/routes/logout/+page.svelte";
|
||||||
@@ -1 +1 @@
|
|||||||
export { default as component } from "../../../../src/routes/users/[id]/+page.svelte";
|
export { default as component } from "../../../../src/routes/notifications/+page.svelte";
|
||||||
@@ -1 +1 @@
|
|||||||
export { default as component } from "../../../../src/routes/[site]/+page.svelte";
|
export { default as component } from "../../../../src/routes/profile/+page.svelte";
|
||||||
@@ -24,7 +24,7 @@ export const options = {
|
|||||||
app: ({ head, body, assets, nonce, env }) => "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <link rel=\"icon\" href=\"" + assets + "/favicon.png\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5, viewport-fit=cover\" />\n <meta name=\"theme-color\" content=\"#0f172a\" />\n " + head + "\n </head>\n <body data-sveltekit-preload-data=\"hover\">\n <div style=\"display: contents\">" + body + "</div>\n </body>\n</html>\n",
|
app: ({ head, body, assets, nonce, env }) => "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <link rel=\"icon\" href=\"" + assets + "/favicon.png\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5, viewport-fit=cover\" />\n <meta name=\"theme-color\" content=\"#0f172a\" />\n " + head + "\n </head>\n <body data-sveltekit-preload-data=\"hover\">\n <div style=\"display: contents\">" + body + "</div>\n </body>\n</html>\n",
|
||||||
error: ({ status, message }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>" + message + "</title>\n\n\t\t<style>\n\t\t\tbody {\n\t\t\t\t--bg: white;\n\t\t\t\t--fg: #222;\n\t\t\t\t--divider: #ccc;\n\t\t\t\tbackground: var(--bg);\n\t\t\t\tcolor: var(--fg);\n\t\t\t\tfont-family:\n\t\t\t\t\tsystem-ui,\n\t\t\t\t\t-apple-system,\n\t\t\t\t\tBlinkMacSystemFont,\n\t\t\t\t\t'Segoe UI',\n\t\t\t\t\tRoboto,\n\t\t\t\t\tOxygen,\n\t\t\t\t\tUbuntu,\n\t\t\t\t\tCantarell,\n\t\t\t\t\t'Open Sans',\n\t\t\t\t\t'Helvetica Neue',\n\t\t\t\t\tsans-serif;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\theight: 100vh;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t.error {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tmax-width: 32rem;\n\t\t\t\tmargin: 0 1rem;\n\t\t\t}\n\n\t\t\t.status {\n\t\t\t\tfont-weight: 200;\n\t\t\t\tfont-size: 3rem;\n\t\t\t\tline-height: 1;\n\t\t\t\tposition: relative;\n\t\t\t\ttop: -0.05rem;\n\t\t\t}\n\n\t\t\t.message {\n\t\t\t\tborder-left: 1px solid var(--divider);\n\t\t\t\tpadding: 0 0 0 1rem;\n\t\t\t\tmargin: 0 0 0 1rem;\n\t\t\t\tmin-height: 2.5rem;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t}\n\n\t\t\t.message h1 {\n\t\t\t\tfont-weight: 400;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t@media (prefers-color-scheme: dark) {\n\t\t\t\tbody {\n\t\t\t\t\t--bg: #222;\n\t\t\t\t\t--fg: #ddd;\n\t\t\t\t\t--divider: #666;\n\t\t\t\t}\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"error\">\n\t\t\t<span class=\"status\">" + status + "</span>\n\t\t\t<div class=\"message\">\n\t\t\t\t<h1>" + message + "</h1>\n\t\t\t</div>\n\t\t</div>\n\t</body>\n</html>\n"
|
error: ({ status, message }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>" + message + "</title>\n\n\t\t<style>\n\t\t\tbody {\n\t\t\t\t--bg: white;\n\t\t\t\t--fg: #222;\n\t\t\t\t--divider: #ccc;\n\t\t\t\tbackground: var(--bg);\n\t\t\t\tcolor: var(--fg);\n\t\t\t\tfont-family:\n\t\t\t\t\tsystem-ui,\n\t\t\t\t\t-apple-system,\n\t\t\t\t\tBlinkMacSystemFont,\n\t\t\t\t\t'Segoe UI',\n\t\t\t\t\tRoboto,\n\t\t\t\t\tOxygen,\n\t\t\t\t\tUbuntu,\n\t\t\t\t\tCantarell,\n\t\t\t\t\t'Open Sans',\n\t\t\t\t\t'Helvetica Neue',\n\t\t\t\t\tsans-serif;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\theight: 100vh;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t.error {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tmax-width: 32rem;\n\t\t\t\tmargin: 0 1rem;\n\t\t\t}\n\n\t\t\t.status {\n\t\t\t\tfont-weight: 200;\n\t\t\t\tfont-size: 3rem;\n\t\t\t\tline-height: 1;\n\t\t\t\tposition: relative;\n\t\t\t\ttop: -0.05rem;\n\t\t\t}\n\n\t\t\t.message {\n\t\t\t\tborder-left: 1px solid var(--divider);\n\t\t\t\tpadding: 0 0 0 1rem;\n\t\t\t\tmargin: 0 0 0 1rem;\n\t\t\t\tmin-height: 2.5rem;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t}\n\n\t\t\t.message h1 {\n\t\t\t\tfont-weight: 400;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t@media (prefers-color-scheme: dark) {\n\t\t\t\tbody {\n\t\t\t\t\t--bg: #222;\n\t\t\t\t\t--fg: #ddd;\n\t\t\t\t\t--divider: #666;\n\t\t\t\t}\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"error\">\n\t\t\t<span class=\"status\">" + status + "</span>\n\t\t\t<div class=\"message\">\n\t\t\t\t<h1>" + message + "</h1>\n\t\t\t</div>\n\t\t</div>\n\t</body>\n</html>\n"
|
||||||
},
|
},
|
||||||
version_hash: "3lzvrw"
|
version_hash: "1mkjc4f"
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function get_hooks() {
|
export async function get_hooks() {
|
||||||
|
|||||||
Vendored
+3
-2
@@ -27,7 +27,7 @@ export {};
|
|||||||
|
|
||||||
declare module "$app/types" {
|
declare module "$app/types" {
|
||||||
export interface AppTypes {
|
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(): {
|
RouteParams(): {
|
||||||
"/api/notifications/[id]": { id: string };
|
"/api/notifications/[id]": { id: string };
|
||||||
"/users/[id]": { id: string };
|
"/users/[id]": { id: string };
|
||||||
@@ -45,11 +45,12 @@ declare module "$app/types" {
|
|||||||
"/logout": Record<string, never>;
|
"/logout": Record<string, never>;
|
||||||
"/notifications": Record<string, never>;
|
"/notifications": Record<string, never>;
|
||||||
"/profile": Record<string, never>;
|
"/profile": Record<string, never>;
|
||||||
|
"/training": Record<string, never>;
|
||||||
"/users": { id?: string };
|
"/users": { id?: string };
|
||||||
"/users/[id]": { id: string };
|
"/users/[id]": { id: string };
|
||||||
"/[site]": { site: 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<AppTypes['Pathname']>}`;
|
ResolvedPathname(): `${"" | `/${string}`}${ReturnType<AppTypes['Pathname']>}`;
|
||||||
Asset(): string & {};
|
Asset(): string & {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,10 @@
|
|||||||
"/profile": [
|
"/profile": [
|
||||||
"src/routes/+layout.server.ts"
|
"src/routes/+layout.server.ts"
|
||||||
],
|
],
|
||||||
|
"/training": [
|
||||||
|
"src/routes/training/+page.server.ts",
|
||||||
|
"src/routes/+layout.server.ts"
|
||||||
|
],
|
||||||
"/users": [
|
"/users": [
|
||||||
"src/routes/users/+page.server.ts",
|
"src/routes/users/+page.server.ts",
|
||||||
"src/routes/+layout.server.ts"
|
"src/routes/+layout.server.ts"
|
||||||
|
|||||||
+2
-2
@@ -12,8 +12,8 @@ type EnsureDefined<T> = T extends null | undefined ? {} : T;
|
|||||||
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
|
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
|
||||||
export type Snapshot<T = any> = Kit.Snapshot<T>;
|
export type Snapshot<T = any> = Kit.Snapshot<T>;
|
||||||
type PageParentData = EnsureDefined<LayoutData>;
|
type PageParentData = EnsureDefined<LayoutData>;
|
||||||
type LayoutRouteId = RouteId | "/" | "/logout" | "/profile" | "/users" | "/users/[id]" | "/[site]" | "/notifications" | "/login" | null
|
type LayoutRouteId = RouteId | "/" | "/[site]" | "/login" | "/logout" | "/notifications" | "/profile" | "/training" | "/users" | "/users/[id]" | null
|
||||||
type LayoutParams = RouteParams & { id?: string; site?: string }
|
type LayoutParams = RouteParams & { site?: string; id?: string }
|
||||||
type LayoutServerParentData = EnsureDefined<{}>;
|
type LayoutServerParentData = EnsureDefined<{}>;
|
||||||
type LayoutParentData = EnsureDefined<{}>;
|
type LayoutParentData = EnsureDefined<{}>;
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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\\`<slot />\\`${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 }"}]}
|
{"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`\\`<slot />\\`${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}"}]}
|
||||||
Vendored
+1
@@ -15,6 +15,7 @@ declare global {
|
|||||||
tasgrid?: boolean;
|
tasgrid?: boolean;
|
||||||
stackq?: boolean;
|
stackq?: boolean;
|
||||||
hrm?: boolean;
|
hrm?: boolean;
|
||||||
|
training_admin?: boolean;
|
||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export function mapUserRecord(r: Record<string, unknown>): UserFromRecord {
|
|||||||
prism: bool(r, 'PRISM', 'prism', 'Prism'),
|
prism: bool(r, 'PRISM', 'prism', 'Prism'),
|
||||||
tasgrid: bool(r, 'TasGrid', 'tasgrid'),
|
tasgrid: bool(r, 'TasGrid', 'tasgrid'),
|
||||||
stackq: bool(r, 'Stackq', 'stackq'),
|
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')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { sites, siteSlugs, type SiteSlug } from '$lib/sites';
|
import { sites, siteSlugs, type SiteSlug } from '$lib/sites';
|
||||||
|
import type { LayoutServerLoad } from './$types';
|
||||||
|
|
||||||
let { data }: { data: App.PageData } = $props();
|
let { data }: { data: { user: App.Locals['user'] } } = $props();
|
||||||
|
|
||||||
const visibleSlugs = $derived(
|
const visibleSlugs = $derived(
|
||||||
data.user
|
data.user
|
||||||
@@ -27,9 +28,12 @@
|
|||||||
{#each visibleSlugs as slug}
|
{#each visibleSlugs as slug}
|
||||||
{@const site = sites[slug]}
|
{@const site = sites[slug]}
|
||||||
<a href="/{slug}" class="block">
|
<a href="/{slug}" class="block">
|
||||||
<Button class="h-16 w-full min-h-16" variant="outline">{site.title} | {site.description}</Button>
|
<Button class="h-16 w-full min-h-16" variant="outline">{site.title}</Button>
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
|
<a href="/training" class="block">
|
||||||
|
<Button class="h-12 w-full" variant="outline">Training</Button>
|
||||||
|
</a>
|
||||||
<a href="/notifications" class="block">
|
<a href="/notifications" class="block">
|
||||||
<Button class="h-12 w-full" variant="outline">Notifications</Button>
|
<Button class="h-12 w-full" variant="outline">Notifications</Button>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { sites, siteSlugs, type SiteSlug } from '$lib/sites';
|
import { sites, siteSlugs, type SiteSlug } from '$lib/sites';
|
||||||
|
|
||||||
let { data }: { data: App.PageData } = $props();
|
let { data }: { data: { user: App.Locals['user'] } } = $props();
|
||||||
|
|
||||||
const visibleSlugs = $derived(
|
const visibleSlugs = $derived(
|
||||||
data.user
|
data.user
|
||||||
@@ -18,10 +18,13 @@
|
|||||||
{@const site = sites[slug]}
|
{@const site = sites[slug]}
|
||||||
<a href="/{slug}" class="block">
|
<a href="/{slug}" class="block">
|
||||||
<Button class="h-16 min-h-16 w-full" variant="outline">
|
<Button class="h-16 min-h-16 w-full" variant="outline">
|
||||||
{site.title} | {site.description}
|
{site.title}
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
|
<a href="/training" class="block">
|
||||||
|
<Button class="h-12 w-full" variant="outline">Training</Button>
|
||||||
|
</a>
|
||||||
<a href="/notifications" class="block">
|
<a href="/notifications" class="block">
|
||||||
<Button class="h-12 w-full" variant="outline">Notifications</Button>
|
<Button class="h-12 w-full" variant="outline">Notifications</Button>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { sites, type SiteSlug } from '$lib/sites';
|
import { sites, type SiteSlug } from '$lib/sites';
|
||||||
import { error } from '@sveltejs/kit';
|
import { error, redirect } from '@sveltejs/kit';
|
||||||
import * as ssoStore from '$lib/sso-store';
|
|
||||||
import { randomUUID } from 'crypto';
|
|
||||||
|
|
||||||
export function load({ params, locals }) {
|
export function load({ params }) {
|
||||||
const slug = params.site?.toLowerCase() as SiteSlug | undefined;
|
const slug = params.site?.toLowerCase() as SiteSlug | undefined;
|
||||||
const site = slug && slug in sites ? sites[slug as SiteSlug] : null;
|
const site = slug && slug in sites ? sites[slug as SiteSlug] : null;
|
||||||
|
|
||||||
@@ -11,22 +9,5 @@ export function load({ params, locals }) {
|
|||||||
throw error(404, `Unknown site: ${params.site}`);
|
throw error(404, `Unknown site: ${params.site}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let ssoToken: string | undefined;
|
throw redirect(303, site.url);
|
||||||
if (locals.user && locals.pb?.authStore?.token) {
|
|
||||||
try {
|
|
||||||
const token = locals.pb.authStore.token;
|
|
||||||
const model = locals.pb.authStore.record as Record<string, unknown> | null;
|
|
||||||
ssoToken = randomUUID();
|
|
||||||
ssoStore.set(ssoToken, { token, model });
|
|
||||||
} catch {
|
|
||||||
// no sso if export fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
slug: slug as SiteSlug,
|
|
||||||
url: site.url,
|
|
||||||
title: site.title,
|
|
||||||
ssoToken
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,30 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/ui/button';
|
|
||||||
|
|
||||||
let { data }: {
|
let { data }: {
|
||||||
data: { slug: string; url: string; title: string; ssoToken?: string };
|
data: { slug: string; url: string; title: string };
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const iframeSrc = $derived(
|
|
||||||
data.ssoToken
|
|
||||||
? `${data.url}${data.url.includes('?') ? '&' : '?'}sso=${data.ssoToken}`
|
|
||||||
: data.url
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{data.title} — Base</title>
|
<title>{data.title} — Base</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main class="relative flex-1 min-h-0 min-w-0 flex flex-col">
|
<main class="flex min-h-0 flex-1 flex-col items-center justify-center gap-4 p-4 sm:gap-6 sm:p-6">
|
||||||
<iframe
|
<h1 class="text-2xl font-semibold">{data.title}</h1>
|
||||||
id="app-iframe"
|
<p class="text-muted-foreground text-sm text-center max-w-md">
|
||||||
title={data.title}
|
This app now opens in a separate tab.
|
||||||
src={iframeSrc}
|
</p>
|
||||||
class="absolute inset-0 w-full h-full border-0 rounded-none"
|
<a
|
||||||
/>
|
href={data.url}
|
||||||
<div
|
rel="noreferrer"
|
||||||
class="absolute top-2 left-1/2 -translate-x-1/2 z-10 flex items-center justify-center pointer-events-none pt-[env(safe-area-inset-top)]"
|
target="_blank"
|
||||||
aria-hidden="true"
|
class="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
>
|
>
|
||||||
<div class="pointer-events-auto">
|
Open {data.title}
|
||||||
<a href="/">
|
</a>
|
||||||
<Button type="button" variant="secondary" class="shadow-md">
|
<a
|
||||||
Base
|
href="/"
|
||||||
</Button>
|
class="text-sm text-muted-foreground underline-offset-4 hover:underline"
|
||||||
|
>
|
||||||
|
← Back to Base
|
||||||
</a>
|
</a>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import Input from '$lib/components/ui/input/input.svelte';
|
||||||
|
|
||||||
|
type TrainingVideo = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
created: string;
|
||||||
|
videoUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UploadFormState = {
|
||||||
|
error?: string;
|
||||||
|
values?: {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
let { data, form }: { data: { videos: TrainingVideo[]; canUpload: boolean }; form: UploadFormState } =
|
||||||
|
$props();
|
||||||
|
|
||||||
|
const videos = $derived(data.videos);
|
||||||
|
const canUpload = $derived(data.canUpload);
|
||||||
|
|
||||||
|
function formatDate(created: string) {
|
||||||
|
if (!created) return '';
|
||||||
|
const d = new Date(created);
|
||||||
|
return d.toLocaleDateString(undefined, {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: d.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Training — Base</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<main class="flex min-h-0 flex-1 flex-col gap-4 p-4 sm:gap-6 sm:p-6">
|
||||||
|
<a href="/" class="self-start">
|
||||||
|
<Button variant="ghost" size="sm">← Base</Button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<h1 class="text-2xl font-semibold">Training</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if canUpload}
|
||||||
|
<section class="space-y-3">
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header class="pb-2">
|
||||||
|
<Card.Title>Upload training video</Card.Title>
|
||||||
|
<Card.Description>Add a new video for your team.</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="space-y-3">
|
||||||
|
{#if form?.error}
|
||||||
|
<p class="text-destructive text-sm">{form.error}</p>
|
||||||
|
{/if}
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="/training" value="1" />
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-medium" for="title">Title</label>
|
||||||
|
<Input
|
||||||
|
id="title"
|
||||||
|
name="title"
|
||||||
|
required
|
||||||
|
value={form?.values?.title ?? ''}
|
||||||
|
placeholder="Onboarding overview"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 space-y-2">
|
||||||
|
<label class="text-sm font-medium" for="description">Description</label>
|
||||||
|
<Input
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
value={form?.values?.description ?? ''}
|
||||||
|
placeholder="Short summary of this training"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 space-y-2">
|
||||||
|
<label class="text-sm font-medium" for="video">Video file</label>
|
||||||
|
<Input id="video" name="video" type="file" accept="video/*" required />
|
||||||
|
<p class="text-muted-foreground text-xs">
|
||||||
|
Upload an MP4 or WebM file. Size limits follow PocketBase settings.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex justify-end">
|
||||||
|
<Button type="submit" name="intent" value="upload">Upload</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<section class="space-y-3">
|
||||||
|
{#if videos.length === 0}
|
||||||
|
<p class="text-muted-foreground text-sm">No training videos yet.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||||
|
{#each videos as video (video.id)}
|
||||||
|
<Card.Root class="flex flex-col">
|
||||||
|
<Card.Header class="pb-3">
|
||||||
|
<Card.Title class="truncate">{video.title}</Card.Title>
|
||||||
|
<Card.Description class="text-xs">{formatDate(video.created)}</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="flex flex-1 flex-col gap-3">
|
||||||
|
{#if video.videoUrl}
|
||||||
|
<div class="relative w-full overflow-hidden rounded-md bg-black/80">
|
||||||
|
<div class="aspect-video">
|
||||||
|
<video
|
||||||
|
src={video.videoUrl}
|
||||||
|
controls
|
||||||
|
class="h-full w-full"
|
||||||
|
>
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
<track kind="captions" srclang="en" label="English captions" />
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="text-muted-foreground text-sm">Video file is missing.</p>
|
||||||
|
{/if}
|
||||||
|
{#if video.description}
|
||||||
|
<p class="text-muted-foreground text-sm line-clamp-3">{video.description}</p>
|
||||||
|
{/if}
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
Reference in New Issue
Block a user