diff --git a/apps/web/src/lib/components/layout/Layout.svelte b/apps/web/src/lib/components/layout/Layout.svelte index 88c6226e0..ff192f35a 100644 --- a/apps/web/src/lib/components/layout/Layout.svelte +++ b/apps/web/src/lib/components/layout/Layout.svelte @@ -7,6 +7,7 @@ import { loggedInUser } from "#lib/features/auth/user-state.ts"; import { getPatrons } from "#lib/features/front-page/front-page.remote.ts"; import { getNotifications } from "#lib/features/notifications/notifications.remote.ts"; import { toNotificationRows } from "#lib/features/notifications/notifications-utils.ts"; +import { setSidenavCollapsed } from "#lib/features/sidenav/sidenav.remote.ts"; import { m } from "#lib/paraglide/messages.js"; import { getLocale } from "#lib/paraglide/runtime.js"; import { GIT_COMMIT } from "#lib/utils/git-commit.ts"; @@ -100,13 +101,14 @@ function unseenRequestsLabel(count: number) { : m.friends_unseenRequests_other({ count }); } -function toggleSideNavCollapsed() { +const sideNavCollapseForm = setSidenavCollapsed.enhance(async ({ submit }) => { sideNavCollapsed = !sideNavCollapsed; - void fetch("/sidenav", { - method: "POST", - body: new URLSearchParams({ collapsed: String(sideNavCollapsed) }), - }); -} + try { + await submit().updates(); + } catch { + // losing the preference cookie is fine, the optimistic state stays + } +}); // mobile hide-on-scroll header, ported from the React useNavOffset const MOBILE_BREAKPOINT = 600; @@ -389,13 +391,17 @@ function formatRelativeDate(timestamp: number) { class="sideNavCollapseButtonContainer" data-testid="sidenav-collapse-button" > - +
+ + +
{#if sideNavCollapsed && showUnseenDot} {/if} @@ -663,6 +669,10 @@ function formatRelativeDate(timestamp: number) { } } + .sideNavCollapseForm { + display: contents; + } + .collapseButton { display: none; align-items: center; diff --git a/apps/web/src/lib/features/sidenav/sidenav.remote.ts b/apps/web/src/lib/features/sidenav/sidenav.remote.ts new file mode 100644 index 000000000..e54f94756 --- /dev/null +++ b/apps/web/src/lib/features/sidenav/sidenav.remote.ts @@ -0,0 +1,11 @@ +import * as v from "valibot"; +import { form, getRequestEvent } from "$app/server"; +import { writeSidenavCollapsed } from "./sidenav.server.ts"; + +/** Persists whether the desktop sidenav is collapsed. */ +export const setSidenavCollapsed = form( + v.object({ collapsed: v.optional(v.boolean(), false) }), + async ({ collapsed }) => { + writeSidenavCollapsed(getRequestEvent().cookies, collapsed); + }, +); diff --git a/apps/web/src/routes/sidenav/+server.ts b/apps/web/src/lib/features/sidenav/sidenav.server.ts similarity index 59% rename from apps/web/src/routes/sidenav/+server.ts rename to apps/web/src/lib/features/sidenav/sidenav.server.ts index c2c08907e..ca1340fbb 100644 --- a/apps/web/src/routes/sidenav/+server.ts +++ b/apps/web/src/lib/features/sidenav/sidenav.server.ts @@ -1,19 +1,21 @@ -import { json } from "@sveltejs/kit"; +import type { Cookies } from "@sveltejs/kit"; import { readSessionCookie, writeSessionCookie, } from "#lib/features/auth/session.server.ts"; import { ServerConfig } from "#lib/server/config.ts"; import { IS_E2E_TEST_RUN } from "#lib/utils/e2e.ts"; -import type { RequestHandler } from "./$types"; const SIDENAV_COOKIE_NAME = "sidenav"; const TEN_YEARS_IN_SECONDS = 315_360_000; -export const POST: RequestHandler = async ({ request, cookies }) => { - const form = new URLSearchParams(await request.text()); - const collapsed = form.get("collapsed") === "true"; +/** Reads the desktop sidenav collapsed preference from its cookie. */ +export function readSidenavCollapsed(cookies: Cookies): boolean { + return readSessionCookie(cookies.get(SIDENAV_COOKIE_NAME))?.collapsed === true; +} +/** Persists the desktop sidenav collapsed preference into its cookie. */ +export function writeSidenavCollapsed(cookies: Cookies, collapsed: boolean) { const session = readSessionCookie(cookies.get(SIDENAV_COOKIE_NAME)) ?? {}; session.collapsed = collapsed; cookies.set(SIDENAV_COOKIE_NAME, writeSessionCookie(session), { @@ -23,6 +25,4 @@ export const POST: RequestHandler = async ({ request, cookies }) => { httpOnly: true, maxAge: TEN_YEARS_IN_SECONDS, }); - - return json({ success: true }); -}; +} diff --git a/apps/web/src/routes/+layout.server.ts b/apps/web/src/routes/+layout.server.ts index 2fcb82e81..1d7a9fcfe 100644 --- a/apps/web/src/routes/+layout.server.ts +++ b/apps/web/src/routes/+layout.server.ts @@ -1,15 +1,11 @@ -import { readSessionCookie } from "#lib/features/auth/session.server.ts"; import { resolveSidebarData } from "#lib/features/sidebar/sidebar.server.ts"; +import { readSidenavCollapsed } from "#lib/features/sidenav/sidenav.server.ts"; import { getTheme } from "#lib/features/theme/theme.server.ts"; import type { LayoutServerLoad } from "./$types"; -const SIDENAV_COOKIE_NAME = "sidenav"; - export const load: LayoutServerLoad = async ({ locals, cookies }) => { const user = locals.user; - const sidenavSession = readSessionCookie(cookies.get(SIDENAV_COOKIE_NAME)); - return { user: user ? { @@ -28,7 +24,7 @@ export const load: LayoutServerLoad = async ({ locals, cookies }) => { createdAt: user.createdAt, } : undefined, - sidenavCollapsed: sidenavSession?.collapsed === true, + sidenavCollapsed: readSidenavCollapsed(cookies), sidebar: await resolveSidebarData(user?.id ?? null), theme: getTheme(cookies), };