Skip to content

Commit 56a794c

Browse files
committed
working example of session sidebar state
1 parent d12c641 commit 56a794c

File tree

8 files changed

+148
-37
lines changed

8 files changed

+148
-37
lines changed

app/api/sidebar.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { setSidebarState } from "~/modules/sidebar-state.server";
2+
import type { Route } from "./+types/sidebar";
3+
4+
export async function action({ request }: Route.ActionArgs) {
5+
console.log("action!");
6+
let formData = await request.formData();
7+
let name = formData.get("name");
8+
let open = formData.get("open");
9+
if (!name || typeof name !== "string") {
10+
return new Response("Name is required", { status: 400 });
11+
}
12+
if (open === "true" || open === "false") {
13+
setSidebarState(name, open === "true");
14+
}
15+
return open === "true";
16+
}

app/components/docs-menu/menu.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from "react";
2-
import { Link } from "react-router";
2+
import { Link, useFetcher, useSubmit } from "react-router";
33
import classNames from "classnames";
44

55
import iconsHref from "~/icons.svg";
@@ -8,6 +8,7 @@ import type { MenuDoc } from "~/modules/gh-docs/.server/docs";
88
import { useNavigation } from "~/hooks/use-navigation";
99
import { useDelayedValue } from "~/hooks/use-delayed-value";
1010
import { useHeaderData } from "../docs-header/use-header-data";
11+
import { useSidebarState } from "~/pages/docs-layout";
1112

1213
export function Menu({ menu }: { menu?: MenuDoc[] }) {
1314
// github might be down but the menu but the doc could be cached in memory, so
@@ -90,9 +91,14 @@ function MenuCategoryDetails({
9091
slug,
9192
children,
9293
}: MenuCategoryDetailsType) {
94+
const submit = useSubmit();
95+
const sidebarState = useSidebarState(slug!);
96+
97+
console.log({ slug, sidebarState });
98+
9399
let { isActive } = useNavigation(slug);
94100
// By default only the active path is open
95-
const [isOpen, setIsOpen] = React.useState(true);
101+
const [isOpen, setIsOpen] = React.useState(sidebarState);
96102

97103
// Auto open the details element, necessary when navigating from the index page
98104
React.useEffect(() => {
@@ -106,10 +112,16 @@ function MenuCategoryDetails({
106112
className={classNames(className, "relative flex flex-col")}
107113
open={isOpen}
108114
onToggle={(e) => {
115+
const open = e.currentTarget.open;
109116
// Synchronize the DOM's state with React state to prevent the
110117
// details element from being closed after navigation and re-evaluation
111118
// of useIsActivePath
112-
setIsOpen(e.currentTarget.open);
119+
setIsOpen(open);
120+
121+
submit(
122+
{ name: slug!, open: String(open) },
123+
{ navigate: false, method: "post", action: "/_sidebar" }
124+
);
113125
}}
114126
>
115127
{children}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import {
2+
createCookieSessionStorage,
3+
type MiddlewareFunctionArgs,
4+
type Session,
5+
} from "react-router";
6+
import { createContext, provide, pull } from "@ryanflorence/async-provider";
7+
8+
let storage = createCookieSessionStorage({
9+
cookie: {
10+
name: "_sidebar-state",
11+
maxAge: 60 * 60 * 24 * 365,
12+
httpOnly: true,
13+
secure: process.env.NODE_ENV === "production",
14+
sameSite: "lax",
15+
},
16+
});
17+
18+
let context = createContext<Session>();
19+
20+
export const sidebarSessionMiddleware = async ({
21+
request,
22+
next,
23+
}: MiddlewareFunctionArgs) => {
24+
let cookieHeader = request.headers.get("Cookie");
25+
let session = await storage.getSession(cookieHeader);
26+
// Setting the cookie and wrapping the response in the context
27+
return provide([[context, session]], async () => {
28+
try {
29+
let res = (await next()) as Response;
30+
res.headers.append("Set-Cookie", await storage.commitSession(session));
31+
return res;
32+
} catch (e) {
33+
console.log("session middleware error", request.url);
34+
console.log(e);
35+
return new Response("Oops, something went wrong.", { status: 500 });
36+
}
37+
});
38+
};
39+
40+
export function sidebarSession() {
41+
return pull(context);
42+
}
43+
44+
export function setSidebarState(key: string, value: boolean) {
45+
let session = sidebarSession();
46+
let state = session.get("sidebar-state") || {};
47+
state[key] = value;
48+
session.set("sidebar-state", state);
49+
}
50+
51+
export function getSidebarState(): Record<string, boolean> {
52+
let session = sidebarSession();
53+
let state = session.get("sidebar-state") || {};
54+
return state;
55+
}

app/pages/doc.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { CACHE_CONTROL } from "~/http";
33
import { seo } from "~/seo";
44
import semver from "semver";
55

6-
import type { HeadersArgs } from "react-router";
6+
import { type HeadersArgs } from "react-router";
77

88
import { getDocTitle, getDocsSearch, getRobots } from "~/ui/meta";
99
import { DocLayout } from "~/components/doc-layout";
@@ -39,6 +39,7 @@ export let loader = async ({ request, params }: Route.LoaderArgs) => {
3939
if (!doc) {
4040
throw new Response("Not Found", { status: 404 });
4141
}
42+
4243
return { doc };
4344
} catch (_) {
4445
throw new Response("Not Found", { status: 404 });

app/pages/docs-layout.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Outlet } from "react-router";
1+
import { Outlet, useRouteLoaderData } from "react-router";
22
import classNames from "classnames";
33

44
import { Header } from "~/components/docs-header/docs-header";
@@ -12,8 +12,15 @@ import type { Route } from "./+types/docs-layout";
1212
import semver from "semver";
1313
import { useRef } from "react";
1414
import { useCodeBlockCopyButton } from "~/ui/utils";
15+
import {
16+
getSidebarState,
17+
sidebarSessionMiddleware,
18+
} from "~/modules/sidebar-state.server";
19+
import invariant from "tiny-invariant";
1520

16-
export let loader = async ({ params }: Route.LoaderArgs) => {
21+
export let middleware = [sidebarSessionMiddleware];
22+
23+
export async function loader({ params }: Route.LoaderArgs) {
1724
let splat = params["*"];
1825
let firstSegment = splat?.split("/")[0];
1926
let refParam = params.ref
@@ -31,8 +38,10 @@ export let loader = async ({ params }: Route.LoaderArgs) => {
3138
getHeaderData("en", ref, refParam),
3239
]);
3340

34-
return { menu, header };
35-
};
41+
const sidebarState = getSidebarState();
42+
43+
return { menu, header, sidebarState };
44+
}
3645

3746
export default function DocsLayout({ loaderData }: Route.ComponentProps) {
3847
const { menu } = loaderData;
@@ -73,3 +82,12 @@ export default function DocsLayout({ loaderData }: Route.ComponentProps) {
7382
</div>
7483
);
7584
}
85+
86+
export function useSidebarState(slug: string) {
87+
const loaderData =
88+
useRouteLoaderData<Route.ComponentProps["loaderData"]>("docs");
89+
90+
invariant(loaderData, "No loader data found");
91+
92+
return loaderData.sidebarState[slug] ?? true;
93+
}

app/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const routes: RouteConfig = [
99
route("", "pages/docs-layout.tsx", { id: "docs" }, [
1010
route("home", "pages/doc.tsx", { id: "home" }),
1111
route("*", "pages/doc.tsx"),
12+
route("_sidebar", "api/sidebar.ts"),
1213
]),
1314

1415
route("/:ref", "pages/docs-index.tsx", { id: "docs-index" }),

package-lock.json

Lines changed: 32 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@
2323
"dependencies": {
2424
"@docsearch/css": "^3.8.2",
2525
"@docsearch/react": "^3.8.2",
26-
"@react-router/express": "^7.1.1",
27-
"@react-router/node": "^7.1.1",
26+
"@react-router/express": "0.0.0-experimental-beaa4f52a",
27+
"@react-router/node": "0.0.0-experimental-beaa4f52a",
28+
"@ryanflorence/async-provider": "^0.0.1",
2829
"@types/express": "^5.0.0",
2930
"cheerio": "^1.0.0-rc.12",
3031
"classnames": "^2.3.2",
@@ -45,7 +46,7 @@
4546
"parse-numeric-range": "^1.3.0",
4647
"react": "^19.0.0",
4748
"react-dom": "^19.0.0",
48-
"react-router": "^7.1.1",
49+
"react-router": "0.0.0-experimental-beaa4f52a",
4950
"rehype-autolink-headings": "^7.1.0",
5051
"rehype-slug": "^6.0.0",
5152
"rehype-stringify": "^10.0.1",
@@ -63,7 +64,7 @@
6364
"unist-util-visit": "^5.0.0"
6465
},
6566
"devDependencies": {
66-
"@react-router/dev": "7.1.1",
67+
"@react-router/dev": "0.0.0-experimental-beaa4f52a",
6768
"@testing-library/jest-dom": "^5.16.5",
6869
"@types/eslint": "^8.56.6",
6970
"@types/express-serve-static-core": "^5.0.2",

0 commit comments

Comments
 (0)