Skip nav animation in empty state

This commit is contained in:
Sam Becker 2025-07-19 16:02:43 -05:00
parent 6c73b14b85
commit 8c6e406904
5 changed files with 170 additions and 166 deletions

View File

@ -6,10 +6,8 @@ import {
DEFAULT_THEME,
PRESERVE_ORIGINAL_UPLOADS,
META_DESCRIPTION,
NAV_TITLE,
META_TITLE,
HTML_LANG,
NAV_CAPTION,
SITE_FEEDS_ENABLED,
ADMIN_DEBUG_TOOLS_ENABLED,
} from '@/app/config';
@ -104,10 +102,7 @@ export default function RootLayout({
'mx-3 mb-3',
'lg:mx-6 lg:mb-6',
)}>
<Nav
navTitle={NAV_TITLE}
navCaption={NAV_CAPTION}
/>
<Nav />
<main>
<ShareModals />
<RecipeModal />

View File

@ -866,7 +866,7 @@ export default function AdminAppConfigurationClient({
titleShort={(section as AdminConfigSection).titleShort}
icon={section.icon}
optional={!section.required}
updateHashOnVisible={hasScrolled}
updateHashOnVisible={hasScrolled && !simplifiedView}
>
{renderGroupContent(section.title)}
</ChecklistGroup>

View File

@ -1,117 +1,12 @@
'use client';
import { getPhotosCached } from '@/photo/cache';
import NavClient from './NavClient';
import { NAV_CAPTION, NAV_TITLE } from './config';
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,
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 Nav({
navTitle,
navCaption,
}: {
navTitle: string
navCaption?: string
}) {
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 (isPathProtected(pathname)) {
return 'admin';
export default async function Nav() {
const photos = await getPhotosCached({ limit: 1 }).catch(() => []);
return <NavClient
navTitle={NAV_TITLE}
navCaption={NAV_CAPTION}
animate={photos.length > 0}
/>;
}
};
return (
<AppGrid
className={classNameStickyContainer}
classNameMain='pointer-events-auto'
contentMain={
<AnimateItems
animateOnFirstLoadOnly
type={!isPathAdmin(pathname) ? 'bottom' : 'none'}
distanceOffset={10}
items={showNav
? [<nav
key="nav"
ref={ref}
className={clsx(
'w-full flex items-center 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}
/>
<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>]
: []}
/>
}
/>
);
};

119
src/app/NavClient.tsx Normal file
View File

@ -0,0 +1,119 @@
'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,
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,
animate,
}: {
navTitle: string
navCaption?: string
animate: 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 (isPathProtected(pathname)) {
return 'admin';
}
};
return (
<AppGrid
className={classNameStickyContainer}
classNameMain='pointer-events-auto'
contentMain={
<AnimateItems
animateOnFirstLoadOnly
type={animate && !isPathAdmin(pathname) ? 'bottom' : 'none'}
distanceOffset={10}
items={showNav
? [<nav
key="nav"
ref={ref}
className={clsx(
'w-full flex items-center 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}
/>
<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>]
: []}
/>
}
/>
);
};

View File

@ -11,7 +11,6 @@ import { revalidatePath } from 'next/cache';
import SignInOrUploadClient from '@/admin/SignInOrUploadClient';
import Link from 'next/link';
import { PATH_ADMIN_CONFIGURATION } from '@/app/path';
import AnimateItems from '@/components/AnimateItems';
import { getAppText } from '@/i18n/state/server';
export default async function PhotosEmptyState() {
@ -20,8 +19,6 @@ export default async function PhotosEmptyState() {
return (
<AppGrid
contentMain={
<AnimateItems
items={[
<Container
key="PhotosEmptyState"
className="min-h-[20rem] sm:min-h-[30rem] px-8"
@ -61,9 +58,7 @@ export default async function PhotosEmptyState() {
</Link>
</div>
</div>}
</Container>,
]}
/>
</Container>
}
/>
);