Skip to content

Commit df0528d

Browse files
authored
Navbar and theming improvements (#10)
* Improves navigation by allowing selection of a specific blog category * Improves theming across navbar, especially for links * Fixes /public/ link in docker volume for nginx * Implements custom page titles and descriptions based on route data * Minor additional theming and UI improvements
1 parent 326f991 commit df0528d

17 files changed

+320
-68
lines changed

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ services:
2727
- ./certbot/www/:/var/www/certbot/:ro
2828
- ./certbot/conf/:/etc/nginx/ssl/:ro
2929
- ./frontend/static:/usr/src/app/static
30-
- ./frontend/public:/usr/src/app/public
30+
- ./public:/usr/src/app/public
3131
ports:
3232
- 80:80
3333
- 443:443

frontend/package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"@mdx-js/mdx": "^3.1.0",
1616
"@mdx-js/react": "^3.1.0",
1717
"@mdx-js/rollup": "^3.1.0",
18+
"@radix-ui/react-accordion": "^1.2.11",
1819
"@radix-ui/react-popover": "^1.1.11",
1920
"@radix-ui/react-separator": "^1.1.2",
2021
"@radix-ui/react-slot": "^1.2.0",

frontend/src/components/DarkModeSwitch.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default function DarkModeSwitch() {
1111
setTheme(theme === "dark" ? "light" : "dark");
1212
}}
1313
aria-label="Light/Dark Mode Toggle"
14-
className="flex cursor-pointer items-center justify-center px-1 py-0 m-0 size-[3rem] rounded-full bg-transparent shadow-none border-solid focus-visible:ring-1 focus-visible:ring-blue-500 hover:bg-gray-600/20"
14+
className="flex cursor-pointer items-center justify-center px-1 py-0 m-0 size-[3rem] rounded-full bg-transparent shadow-none border-solid focus-visible:ring-1 focus-visible:ring-blue-500 dark:hover:bg-night-sky-700 hover:bg-night-sky-300"
1515
>
1616
<div className="flex items-center justify-center relative m-auto">
1717
<Sun className="absolute size-[2rem] p-0 m-0 rotate-0 scale-100 transition-all duration-500 dark:-rotate-90 dark:scale-0" />

frontend/src/components/Navbar.tsx

Lines changed: 152 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import DarkModeSwitch from "./DarkModeSwitch";
22
import GitHub from "./ui/GitHubMark";
33
import { Link, useLocation } from "@tanstack/react-router";
4-
import { Popover } from "radix-ui";
5-
import { MenuIcon, HomeIcon } from "lucide-react";
4+
import { Accordion, Popover } from "radix-ui";
5+
import { ChevronDown, MenuIcon } from "lucide-react";
66
import { Route as ProjectsRoute } from "~/routes/projects.index";
77
import { Route as BlogRoute } from "~/routes/blog.index";
88
import { Route as AboutRoute } from "~/routes/about";
@@ -18,26 +18,151 @@ const navStyles = {
1818
font-bold
1919
text-lg
2020
h-[3rem]
21-
rounded-full
21+
rounded
2222
w-30
2323
min-w-30
2424
transition-all
25-
dark:hover:bg-gray-600/20
26-
hover:bg-gray-300/50
25+
dark:hover:bg-night-sky-700
26+
hover:bg-night-sky-300
2727
focus-visible:ring-blue-500
2828
outline-0
2929
focus-visible:ring-1
3030
flex
3131
items-center
3232
justify-center
3333
`,
34-
active: "font-bold text-lg",
34+
active: "font-bold text-lg bg-night-sky-200 dark:bg-night-sky-900",
3535
inactive: "font-normal text-lg",
3636
container: "w-full sticky top-0 bg-dawn-pink-100 dark:bg-night-sky-950 z-50",
3737
nav: "h-[4rem] p-0 items-center m-auto",
3838
};
3939

40-
export default function Navbar() {
40+
function BlogLinkButton({ categories }: { categories?: string[] }) {
41+
const [isOpen, setIsOpen] = useState(false);
42+
const pathname = useLocation().pathname;
43+
const isActive = pathname.startsWith("/blog");
44+
45+
useEffect(() => {
46+
setIsOpen(false);
47+
}, [pathname]);
48+
49+
return categories && categories.length > 0 ? (
50+
<Popover.Root open={isOpen} onOpenChange={setIsOpen}>
51+
<Popover.Trigger className="flex items-center justify-center h-full cursor-pointer outline-0 focus-visible:ring-1 focus-visible:ring-blue-500">
52+
<div className="flex items-center justify-center h-full cursor-pointer outline-0 focus-visible:ring-1 focus-visible:ring-blue-500">
53+
<div
54+
className={`${navStyles.button} ${isActive ? navStyles.active : navStyles.inactive}`}
55+
>
56+
Blog
57+
</div>
58+
<span className="sr-only">Toggle menu</span>
59+
</div>
60+
</Popover.Trigger>
61+
<Popover.Portal>
62+
<Popover.Content
63+
className="w-64 bg-dawn-pink-100 dark:bg-night-sky-950 p-2 rounded-lg shadow-xl z-50 border-night-sky-950 dark:border-dawn-pink-100 border-2"
64+
sideOffset={5}
65+
>
66+
<div className="flex items-center justify-center h-full cursor-pointer outline-0 focus-visible:ring-1 focus-visible:ring-blue-500">
67+
<div className="flex flex-col space-y-1 w-full">
68+
{categories.map((category) => (
69+
<Link
70+
to={BlogRoute.to + `/${category.toLowerCase()}`}
71+
className="px-4 py-2 rounded-md text-center capitalize hover:bg-night-sky-300 dark:hover:bg-night-sky-700"
72+
activeProps={{ className: navStyles.active }}
73+
key={category}
74+
resetScroll
75+
>
76+
{category}
77+
</Link>
78+
))}
79+
<Link
80+
to={BlogRoute.to}
81+
className="px-4 py-2 hover:bg-night-sky-300 dark:hover:bg-night-sky-700 rounded-md text-center"
82+
activeOptions={{ exact: true }}
83+
activeProps={{ className: navStyles.active }}
84+
resetScroll
85+
>
86+
See All Posts
87+
</Link>
88+
</div>
89+
</div>
90+
<Popover.Arrow
91+
height={10}
92+
className="dark:fill-dawn-pink-100 fill-night-sky-950"
93+
/>
94+
</Popover.Content>
95+
</Popover.Portal>
96+
</Popover.Root>
97+
) : (
98+
<Link
99+
to={BlogRoute.to}
100+
className={navStyles.button}
101+
resetScroll
102+
aria-label="Blog link"
103+
activeProps={{ className: navStyles.active }}
104+
inactiveProps={{ className: navStyles.inactive }}
105+
>
106+
Blog
107+
</Link>
108+
);
109+
}
110+
111+
function BlogLinkDropdown({ categories }: { categories?: string[] }) {
112+
const pathname = useLocation().pathname;
113+
const isActive = pathname.startsWith("/blog");
114+
115+
return (
116+
<Accordion.Root type="single" collapsible>
117+
<Accordion.Item value="blog" className="text-center">
118+
<Accordion.Header>
119+
<Accordion.Trigger
120+
className={cn(
121+
"w-full group",
122+
"px-4 py-2 rounded-md dark:hover:bg-night-sky-700 hover:bg-night-sky-300 relative",
123+
isActive ? navStyles.active : navStyles.inactive
124+
)}
125+
>
126+
Blog
127+
<ChevronDown
128+
className="absolute right-2 top-1/2 transform -translate-y-1/2 group-data-[state=open]:rotate-180 transition duration-200"
129+
size={24}
130+
aria-hidden
131+
/>
132+
</Accordion.Trigger>
133+
</Accordion.Header>
134+
<Accordion.Content className="p-2 space-y-1">
135+
<div className="w-full bg-night-sky-950 dark:bg-dawn-pink-100 h-[1px] mx-auto mb-2" />
136+
{categories?.map((category) => (
137+
<Link
138+
key={category}
139+
to={BlogRoute.to + `/${category.toLowerCase()}`}
140+
className="block text-sm px-4 py-2 rounded-md text-center capitalize hover:bg-night-sky-300 dark:hover:bg-night-sky-700"
141+
activeProps={{ className: navStyles.active }}
142+
resetScroll
143+
>
144+
{category}
145+
</Link>
146+
))}
147+
<Link
148+
to={BlogRoute.to}
149+
className="block text-sm px-4 py-2 rounded-md text-center hover:bg-night-sky-300 dark:hover:bg-night-sky-700"
150+
activeOptions={{ exact: true }}
151+
activeProps={{ className: navStyles.active }}
152+
resetScroll
153+
>
154+
See All Posts
155+
</Link>
156+
<div className="w-full bg-night-sky-950 dark:bg-dawn-pink-100 h-[1px] mx-auto mt-2" />
157+
</Accordion.Content>
158+
</Accordion.Item>
159+
</Accordion.Root>
160+
);
161+
}
162+
163+
export default function Navbar(
164+
{ categories }: { categories?: string[] } = { categories: [] }
165+
) {
41166
const [isOpen, setIsOpen] = useState(false);
42167
const pathname = useLocation().pathname;
43168

@@ -55,14 +180,12 @@ export default function Navbar() {
55180
>
56181
<Link
57182
to="/"
58-
activeProps={{ className: navStyles.active }}
59183
inactiveProps={{ className: navStyles.inactive }}
60184
activeOptions={{ exact: true }}
61-
className="flex cursor-pointer items-center justify-center px-1 py-0 m-0 size-[3rem] rounded-full bg-transparent shadow-none border-solid focus-visible:ring-1 focus-visible:ring-blue-500 hover:bg-gray-600/20 justify-self-start"
185+
className="flex cursor-pointer items-center justify-center px-1 py-0 m-0 size-[3rem] rounded-full bg-transparent shadow-none border-solid focus-visible:ring-1 focus-visible:ring-blue-500 hover:bg-night-sky-300 dark:hover:bg-night-sky-700 justify-self-start"
62186
resetScroll
63187
aria-label="Home page link"
64188
>
65-
{/* <HomeIcon className="p-0" size={32} aria-label="Home icon" /> */}
66189
<img src="/static/logo.svg" alt="Logo" className="h-8 dark:hidden" />
67190
<img
68191
src="/static/logo-light.svg"
@@ -71,16 +194,8 @@ export default function Navbar() {
71194
/>
72195
<span className="sr-only">Home</span>
73196
</Link>
74-
<div className="align-middle grow-0 flex px-5 justify-self-stretch justify-center">
75-
<Link
76-
to={BlogRoute.to}
77-
activeProps={{ className: navStyles.active }}
78-
inactiveProps={{ className: navStyles.inactive }}
79-
className={navStyles.button}
80-
resetScroll
81-
>
82-
Blog
83-
</Link>
197+
<div className="align-middle grow-0 flex space-x-2 px-5 justify-self-stretch justify-center">
198+
<BlogLinkButton categories={categories} />
84199
<Link
85200
to={ProjectsRoute.to}
86201
activeProps={{ className: navStyles.active }}
@@ -113,13 +228,22 @@ export default function Navbar() {
113228
<div className="flex items-center justify-between w-full px-2">
114229
<Link
115230
to="/"
116-
activeProps={{ className: navStyles.active }}
117231
inactiveProps={{ className: navStyles.inactive }}
118232
activeOptions={{ exact: true }}
119-
className="flex cursor-pointer items-center justify-center px-1 py-0 m-0 size-[3rem] rounded-full bg-transparent shadow-none border-solid focus-visible:ring-1 focus-visible:ring-blue-500 hover:bg-gray-600/20"
233+
className="flex cursor-pointer items-center justify-center px-1 py-0 m-0 size-[3rem] rounded-full bg-transparent shadow-none border-solid focus-visible:ring-1 focus-visible:ring-blue-500"
120234
resetScroll
121235
>
122-
<HomeIcon className="p-0" size={32} />
236+
<img
237+
src="/static/logo.svg"
238+
alt="Logo"
239+
className="h-8 dark:hidden"
240+
/>
241+
<img
242+
src="/static/logo-light.svg"
243+
alt="Logo"
244+
className="h-8 hidden dark:inline"
245+
/>
246+
<span className="sr-only">Home</span>
123247
</Link>
124248

125249
<div className="align-middle justify-center grow-0 px-4">
@@ -136,34 +260,20 @@ export default function Navbar() {
136260
sideOffset={5}
137261
>
138262
<div className="flex flex-col space-y-1">
139-
<Link
140-
to={BlogRoute.to}
141-
className="px-4 py-2 hover:bg-muted rounded-md text-center"
142-
activeProps={{
143-
className:
144-
"font-bold bg-dawn-pink-300 dark:bg-night-sky-900",
145-
}}
146-
resetScroll
147-
>
148-
Blog
149-
</Link>
263+
<BlogLinkDropdown categories={categories} />
150264
<Link
151265
to={ProjectsRoute.to}
152-
className="px-4 py-2 hover:bg-muted rounded-md text-center"
153-
activeProps={{
154-
className:
155-
"font-bold bg-dawn-pink-300 dark:bg-night-sky-900",
156-
}}
266+
className="px-4 py-2 dark:hover:bg-night-sky-700 hover:bg-night-sky-300 rounded-md text-center"
267+
activeProps={{ className: navStyles.active }}
157268
resetScroll
158269
>
159270
Projects
160271
</Link>
161272
<Link
162273
to={AboutRoute.to}
163-
className="px-4 py-2 hover:bg-muted rounded-md text-center"
274+
className="px-4 py-2 dark:hover:bg-night-sky-700 hover:bg-night-sky-300 dark:active:bg-night-sky-700 active:bg-night-sky-300 rounded-md text-center"
164275
activeProps={{
165-
className:
166-
"font-bold bg-dawn-pink-300 dark:bg-night-sky-900",
276+
className: navStyles.active,
167277
}}
168278
resetScroll
169279
>

frontend/src/components/ThemeProvider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export function initialTheme() {
8484
// https://tanstack.com/router/latest/docs/framework/react/guide/document-head-management/#scripts
8585
return {
8686
children: `
87-
let prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
87+
var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
8888
if (!("theme" in localStorage)) {
8989
localStorage.setItem("theme", prefersDark ? "dark" : "light");
9090
}

frontend/src/components/ui/GitHubMark.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ interface GitHubProps {
77
export default function GitHub({ githubLink }: GitHubProps) {
88
return (
99
<Link
10-
className="flex cursor-pointer items-center justify-center px-1 py-0 m-0 size-[3rem] rounded-full bg-transparent shadow-none border-solid focus-visible:ring-1 focus-visible:ring-blue-500 hover:bg-gray-600/20"
10+
className="flex cursor-pointer items-center justify-center px-1 py-0 m-0 size-[3rem] rounded-full bg-transparent shadow-none border-solid focus-visible:ring-1 focus-visible:ring-blue-500 dark:hover:bg-night-sky-700 hover:bg-night-sky-300"
1111
to={githubLink}
1212
aria-label="GitHub Link"
1313
>

frontend/src/routes/__root.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,13 @@ import { seo } from "~/utils/seo";
1818
import { ThemeProvider, initialTheme } from "../components/ThemeProvider";
1919
import { MotionConfig } from "framer-motion";
2020
import Footer from "~/components/Footer";
21+
import { fetchMDX } from "~/utils/mdx-fetcher";
2122

2223
export const Route = createRootRoute({
24+
loader: async () => {
25+
const categories = await fetchMDX({ data: { directory: "posts" } });
26+
return { categories };
27+
},
2328
head: () => ({
2429
meta: [
2530
{
@@ -31,7 +36,10 @@ export const Route = createRootRoute({
3136
},
3237
...seo({
3338
title: "Andrew Scherer",
34-
description: `Personal website and blog for physical oceanographer and software developer Andrew Scherer.`,
39+
description:
40+
"Personal website and blog for physical oceanographer and software developer Andrew Scherer. Find out about my professional history, publications, and interests in oceanography and software development.",
41+
keywords:
42+
"Andrew, Scherer, Andrew Scherer, Andrew Scherer website, Andrew Scherer blog, Andrew Scherer oceanography, Andrew Scherer software developer, oceanography, software development, personal website, blog, tech, technology, science, ocean science, physical oceanography, software engineering, programming, coding, web development",
3543
}),
3644
],
3745
scripts: [initialTheme()],
@@ -111,6 +119,11 @@ function RootComponent() {
111119

112120
function RootDocument({ children }: { children: ReactNode }) {
113121
const pathname = useLocation().pathname;
122+
const categories = Route.useLoaderData()
123+
.categories.frontmatters.map((fm) => fm.tags)
124+
.flat()
125+
.filter((value, index, self) => self.indexOf(value) === index);
126+
114127
return (
115128
<StrictMode>
116129
<html suppressHydrationWarning lang="en" className="antialiased">
@@ -119,7 +132,7 @@ function RootDocument({ children }: { children: ReactNode }) {
119132
</head>
120133
<body suppressHydrationWarning>
121134
<ThemeProvider>
122-
<Navbar />
135+
<Navbar categories={categories} />
123136
<hr />
124137
<MotionConfig reducedMotion="user">{children}</MotionConfig>
125138
<TanStackRouterDevtools position="bottom-right" />

frontend/src/routes/about-site.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createFileRoute, ParsedLocation } from "@tanstack/react-router";
22
import { fetchMDXCode, fetchSingleMDXFrontMatter } from "~/utils/mdx-fetcher";
33
import { MDXPost } from "~/components/page";
4+
import { seo } from "~/utils/seo";
45

56
let prevLoc: ParsedLocation | null = null;
67

@@ -32,6 +33,17 @@ export const Route = createFileRoute("/about-site")({
3233
prevLoc = match.location;
3334
}
3435
},
36+
head: () => ({
37+
meta: [
38+
...seo({
39+
title: "About Site - Andrew Scherer",
40+
description:
41+
"Learn more about this site, its history and future, and the technologies used to build it.",
42+
keywords:
43+
"Andrew Scherer, about site, personal website, blog, technology, software development, web development",
44+
}),
45+
],
46+
}),
3547
});
3648

3749
function AboutComponent() {

0 commit comments

Comments
 (0)