Vercel/src/app/NavClient.tsx
Sam Becker 5970bfb850
About Page (#378)
* Highlight /about in nav

* Refine full frame icon

* Add timestamp to /about

* Add /about to cmdk menu

* Enrich /about content

* Make /about categories responsive

* Enlarge app nav buttons

* Add /about richer categories

* Widen main nav buttons

* Add more /about category content

* Catch db errors in /about

* Update key /about image

* Add /about avatar

* Add jest TextEncoder polyfill

* Refactor sidebar text configuration

* Show /about hero photo meta

* Hoist about content to server page

* Hide admin email on small screens

* Add basic about page form

* Finalize basic /about upsert functionality

* Make /about/edit safe for blank templates
* Add configuration to hide /about page

* Add default /about title text

* Add interactive photos to /about edit form

* Apply final /about i18n

* Ensure /about static optimization

* Add CTA for admins to add /about descriptions

* Add convenience for accepting full photo urls

* Add photo placeholder icon

* Show /about empty state when there are no photos

* Hide sort control when in app empty state
2026-02-26 09:32:17 -06:00

124 lines
3.4 KiB
TypeScript

'use client';
import { clsx } from 'clsx/lite';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import AppGrid from '../components/AppGrid';
import AppViewSwitcher, { SwitcherSelection } from '@/app/AppViewSwitcher';
import {
PATH_ROOT,
isPathAbout,
isPathAdmin,
isPathFull,
isPathGrid,
isPathProtected,
isPathSignIn,
} from '@/app/path';
import AnimateItems from '../components/AnimateItems';
import {
GRID_HOMEPAGE_ENABLED,
NAV_CAPTION,
} from './config';
import { useRef } from 'react';
import useStickyNav from './useStickyNav';
import { useAppState } from '@/app/AppState';
const NAV_HEIGHT_CLASS = NAV_CAPTION
? 'min-h-[4rem] sm:min-h-[5rem]'
: 'min-h-[4rem]';
export default function NavClient({
navTitle,
navCaption,
isInEmptyState,
}: {
navTitle: string
navCaption?: string
isInEmptyState: boolean
}) {
const ref = useRef<HTMLElement>(null);
const pathname = usePathname();
const showNav = !isPathSignIn(pathname);
const {
hasLoadedWithAnimations,
} = useAppState();
const {
classNameStickyContainer,
classNameStickyNav,
isNavVisible,
} = useStickyNav(ref, !isPathAdmin(pathname));
const renderLink = (
text: string,
linkOrAction: string | (() => void),
) =>
typeof linkOrAction === 'string'
? <Link href={linkOrAction}>{text}</Link>
: <button onClick={linkOrAction} type="button">{text}</button>;
const switcherSelectionForPath = (): SwitcherSelection | undefined => {
if (pathname === PATH_ROOT) {
return GRID_HOMEPAGE_ENABLED ? 'grid' : 'full';
} else if (isPathGrid(pathname)) {
return 'grid';
} else if (isPathFull(pathname)) {
return 'full';
} else if (isPathAbout(pathname)) {
return 'about';
} else if (isPathProtected(pathname)) {
return 'admin';
}
};
return (
<AppGrid
className={classNameStickyContainer}
classNameMain='pointer-events-auto'
contentMain={
<AnimateItems
animateOnFirstLoadOnly
type={!isInEmptyState && !isPathAdmin(pathname) ? 'bottom' : 'none'}
distanceOffset={10}
items={showNav
? [<nav
key="nav"
ref={ref}
className={clsx(
'w-full flex items-center gap-2 bg-main',
NAV_HEIGHT_CLASS,
// Enlarge nav to ensure it fully masks underlying content
'md:w-[calc(100%+8px)] md:translate-x-[-4px] md:px-[4px]',
classNameStickyNav,
)}>
<AppViewSwitcher
currentSelection={switcherSelectionForPath()}
className="translate-x-[-1px]"
animate={hasLoadedWithAnimations && isNavVisible}
hideSortControl={isInEmptyState}
/>
<div className={clsx(
'grow text-right min-w-0',
'translate-y-[-1px]',
)}>
<div className="truncate overflow-hidden select-none">
{renderLink(navTitle, PATH_ROOT)}
</div>
{navCaption &&
<div className={clsx(
'hidden sm:block truncate overflow-hidden',
'leading-tight text-dim',
)}>
{navCaption}
</div>}
</div>
</nav>]
: []}
/>
}
/>
);
};