From b58eb840f3b924ca273519132ec4f9437770dc43 Mon Sep 17 00:00:00 2001 From: Xevion Date: Thu, 29 Jan 2026 17:01:47 -0600 Subject: [PATCH] refactor: consolidate navigation with top nav bar and route groups --- web/src/lib/components/NavBar.svelte | 55 +++++++++ web/src/lib/components/PageTransition.svelte | 47 +++++-- web/src/lib/stores/navigation.svelte.ts | 21 ++-- web/src/routes/(app)/+layout.svelte | 116 ++++++++++++++++++ web/src/routes/{ => (app)}/admin/+page.svelte | 4 +- .../admin/audit}/+page.svelte | 2 +- .../admin/jobs}/+page.svelte | 2 +- .../{ => (app)}/admin/users/+page.svelte | 2 +- web/src/routes/(app)/profile/+page.svelte | 24 ++++ web/src/routes/(app)/settings/+page.svelte | 5 + web/src/routes/+layout.svelte | 18 ++- web/src/routes/+page.svelte | 6 +- web/src/routes/admin/+layout.svelte | 78 ------------ 13 files changed, 271 insertions(+), 109 deletions(-) create mode 100644 web/src/lib/components/NavBar.svelte create mode 100644 web/src/routes/(app)/+layout.svelte rename web/src/routes/{ => (app)}/admin/+page.svelte (92%) rename web/src/routes/{admin/audit-log => (app)/admin/audit}/+page.svelte (95%) rename web/src/routes/{admin/scrape-jobs => (app)/admin/jobs}/+page.svelte (96%) rename web/src/routes/{ => (app)}/admin/users/+page.svelte (97%) create mode 100644 web/src/routes/(app)/profile/+page.svelte create mode 100644 web/src/routes/(app)/settings/+page.svelte delete mode 100644 web/src/routes/admin/+layout.svelte diff --git a/web/src/lib/components/NavBar.svelte b/web/src/lib/components/NavBar.svelte new file mode 100644 index 0000000..c6e2a42 --- /dev/null +++ b/web/src/lib/components/NavBar.svelte @@ -0,0 +1,55 @@ + + + diff --git a/web/src/lib/components/PageTransition.svelte b/web/src/lib/components/PageTransition.svelte index fab59b3..c3c3a19 100644 --- a/web/src/lib/components/PageTransition.svelte +++ b/web/src/lib/components/PageTransition.svelte @@ -4,36 +4,65 @@ import type { Snippet } from "svelte"; import { cubicOut } from "svelte/easing"; import type { TransitionConfig } from "svelte/transition"; -let { key, children }: { key: string; children: Snippet } = $props(); +type Axis = "horizontal" | "vertical"; -const DURATION = 250; +let { + key, + children, + axis = "horizontal", + inDelay = 0, + outDelay = 0, +}: { + key: string; + children: Snippet; + axis?: Axis; + inDelay?: number; + outDelay?: number; +} = $props(); + +const DURATION = 400; const OFFSET = 40; +function translate(axis: Axis, value: number): string { + return axis === "vertical" ? `translateY(${value}px)` : `translateX(${value}px)`; +} + function inTransition(_node: HTMLElement): TransitionConfig { const dir = navigationStore.direction; if (dir === "fade") { - return { duration: DURATION, easing: cubicOut, css: (t: number) => `opacity: ${t}` }; + return { + duration: DURATION, + delay: inDelay, + easing: cubicOut, + css: (t: number) => `opacity: ${t}`, + }; } - const x = dir === "right" ? OFFSET : -OFFSET; + const offset = dir === "right" ? OFFSET : -OFFSET; return { duration: DURATION, + delay: inDelay, easing: cubicOut, - css: (t: number) => `opacity: ${t}; transform: translateX(${(1 - t) * x}px)`, + css: (t: number) => `opacity: ${t}; transform: ${translate(axis, (1 - t) * offset)}`, }; } function outTransition(_node: HTMLElement): TransitionConfig { const dir = navigationStore.direction; - // Outgoing element is positioned absolutely so incoming flows normally const base = "position: absolute; top: 0; left: 0; width: 100%"; if (dir === "fade") { - return { duration: DURATION, easing: cubicOut, css: (t: number) => `${base}; opacity: ${t}` }; + return { + duration: DURATION, + delay: outDelay, + easing: cubicOut, + css: (t: number) => `${base}; opacity: ${t}`, + }; } - const x = dir === "right" ? -OFFSET : OFFSET; + const offset = dir === "right" ? -OFFSET : OFFSET; return { duration: DURATION, + delay: outDelay, easing: cubicOut, - css: (t: number) => `${base}; opacity: ${t}; transform: translateX(${(1 - t) * x}px)`, + css: (t: number) => `${base}; opacity: ${t}; transform: ${translate(axis, (1 - t) * offset)}`, }; } diff --git a/web/src/lib/stores/navigation.svelte.ts b/web/src/lib/stores/navigation.svelte.ts index e71e0f4..c973f46 100644 --- a/web/src/lib/stores/navigation.svelte.ts +++ b/web/src/lib/stores/navigation.svelte.ts @@ -2,15 +2,22 @@ import { beforeNavigate } from "$app/navigation"; export type NavDirection = "left" | "right" | "fade"; -/** Admin sidebar order — indexes determine slide direction for same-depth siblings */ -const ADMIN_NAV_ORDER = ["/admin", "/admin/scrape-jobs", "/admin/audit-log", "/admin/users"]; +/** Sidebar nav order — indexes determine slide direction for same-depth siblings */ +const SIDEBAR_NAV_ORDER = [ + "/profile", + "/settings", + "/admin", + "/admin/jobs", + "/admin/audit", + "/admin/users", +]; function getDepth(path: string): number { return path.replace(/\/$/, "").split("/").filter(Boolean).length; } -function getAdminIndex(path: string): number { - return ADMIN_NAV_ORDER.indexOf(path); +function getSidebarIndex(path: string): number { + return SIDEBAR_NAV_ORDER.indexOf(path); } function computeDirection(from: string, to: string): NavDirection { @@ -20,9 +27,9 @@ function computeDirection(from: string, to: string): NavDirection { if (toDepth > fromDepth) return "right"; if (toDepth < fromDepth) return "left"; - // Same depth — use admin sidebar ordering if both are admin routes - const fromIdx = getAdminIndex(from); - const toIdx = getAdminIndex(to); + // Same depth — use sidebar ordering if both are sidebar routes + const fromIdx = getSidebarIndex(from); + const toIdx = getSidebarIndex(to); if (fromIdx >= 0 && toIdx >= 0) { return toIdx > fromIdx ? "right" : "left"; } diff --git a/web/src/routes/(app)/+layout.svelte b/web/src/routes/(app)/+layout.svelte new file mode 100644 index 0000000..a1a1e65 --- /dev/null +++ b/web/src/routes/(app)/+layout.svelte @@ -0,0 +1,116 @@ + + +{#if authStore.isLoading} +
+
+

Loading...

+
+
+{:else if !authStore.isAuthenticated} +
+
+

Redirecting to login...

+
+
+{:else} +
+
+ + + + +
+ + {@render children()} + +
+
+
+{/if} diff --git a/web/src/routes/admin/+page.svelte b/web/src/routes/(app)/admin/+page.svelte similarity index 92% rename from web/src/routes/admin/+page.svelte rename to web/src/routes/(app)/admin/+page.svelte index daff06f..4c95b57 100644 --- a/web/src/routes/admin/+page.svelte +++ b/web/src/routes/(app)/admin/+page.svelte @@ -14,7 +14,7 @@ onMount(async () => { }); -

Dashboard

+

Dashboard

{#if error}

{error}

@@ -40,7 +40,7 @@ onMount(async () => { -

Services

+

Services

{#each status.services as service}
diff --git a/web/src/routes/admin/audit-log/+page.svelte b/web/src/routes/(app)/admin/audit/+page.svelte similarity index 95% rename from web/src/routes/admin/audit-log/+page.svelte rename to web/src/routes/(app)/admin/audit/+page.svelte index b94dc9b..fb0092b 100644 --- a/web/src/routes/admin/audit-log/+page.svelte +++ b/web/src/routes/(app)/admin/audit/+page.svelte @@ -14,7 +14,7 @@ onMount(async () => { }); -

Audit Log

+

Audit Log

{#if error}

{error}

diff --git a/web/src/routes/admin/scrape-jobs/+page.svelte b/web/src/routes/(app)/admin/jobs/+page.svelte similarity index 96% rename from web/src/routes/admin/scrape-jobs/+page.svelte rename to web/src/routes/(app)/admin/jobs/+page.svelte index aeaee64..683c9a7 100644 --- a/web/src/routes/admin/scrape-jobs/+page.svelte +++ b/web/src/routes/(app)/admin/jobs/+page.svelte @@ -14,7 +14,7 @@ onMount(async () => { }); -

Scrape Jobs

+

Scrape Jobs

{#if error}

{error}

diff --git a/web/src/routes/admin/users/+page.svelte b/web/src/routes/(app)/admin/users/+page.svelte similarity index 97% rename from web/src/routes/admin/users/+page.svelte rename to web/src/routes/(app)/admin/users/+page.svelte index 6ce37ea..0bb5ea0 100644 --- a/web/src/routes/admin/users/+page.svelte +++ b/web/src/routes/(app)/admin/users/+page.svelte @@ -29,7 +29,7 @@ async function toggleAdmin(user: User) { } -

Users

+

Users

{#if error}

{error}

diff --git a/web/src/routes/(app)/profile/+page.svelte b/web/src/routes/(app)/profile/+page.svelte new file mode 100644 index 0000000..76845a0 --- /dev/null +++ b/web/src/routes/(app)/profile/+page.svelte @@ -0,0 +1,24 @@ + + +

Profile

+ +{#if authStore.user} +
+
+
+

Username

+

{authStore.user.discordUsername}

+
+
+

Discord ID

+

{authStore.user.discordId}

+
+
+

Role

+

{authStore.isAdmin ? "Admin" : "User"}

+
+
+
+{/if} diff --git a/web/src/routes/(app)/settings/+page.svelte b/web/src/routes/(app)/settings/+page.svelte new file mode 100644 index 0000000..4a65a27 --- /dev/null +++ b/web/src/routes/(app)/settings/+page.svelte @@ -0,0 +1,5 @@ +

Settings

+ +
+

No settings available yet.

+
diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index 9e9ec9b..79cf7b3 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -3,7 +3,7 @@ import "overlayscrollbars/overlayscrollbars.css"; import "./layout.css"; import { page } from "$app/state"; import PageTransition from "$lib/components/PageTransition.svelte"; -import ThemeToggle from "$lib/components/ThemeToggle.svelte"; +import NavBar from "$lib/components/NavBar.svelte"; import { useOverlayScrollbars } from "$lib/composables/useOverlayScrollbars.svelte"; import { initNavigation } from "$lib/stores/navigation.svelte"; import { themeStore } from "$lib/stores/theme.svelte"; @@ -12,6 +12,16 @@ import { onMount } from "svelte"; let { children } = $props(); +const APP_PREFIXES = ["/profile", "/settings", "/admin"]; + +/** + * Coarsened key so sub-route navigation within the (app) layout group + * doesn't re-trigger the root page transition — the shared layout handles its own. + */ +let transitionKey = $derived( + APP_PREFIXES.some((p) => page.url.pathname.startsWith(p)) ? "/app" : page.url.pathname +); + initNavigation(); useOverlayScrollbars(() => document.body, { @@ -27,11 +37,9 @@ onMount(() => { -
- -
+ - + {@render children()}
diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 3efdf31..423ac67 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -201,11 +201,7 @@ function handlePageChange(newOffset: number) {
-
- -
-

UTSA Course Search

-
+
diff --git a/web/src/routes/admin/+layout.svelte b/web/src/routes/admin/+layout.svelte deleted file mode 100644 index 35d9f35..0000000 --- a/web/src/routes/admin/+layout.svelte +++ /dev/null @@ -1,78 +0,0 @@ - - -{#if authStore.isLoading} -
-

Loading...

-
-{:else if !authStore.isAdmin} -
-
-

Access Denied

-

You do not have admin access.

-
-
-{:else} -
- -
- - {@render children()} - -
-
-{/if}