Vercel/src/admin/AdminNavClient.tsx
Sam Becker dbf55badf6
Optimize Next.js 16 behavior (#349)
* Remove unused desktop redirect component

* Tweak useEffect/setState interactions

* Address more next.js 16 linting

* Tweak secret loading

* Finish linting setstate/useeffect interactions

* Disable ref lint warnings
2025-10-27 09:49:16 -05:00

122 lines
3.8 KiB
TypeScript

'use client';
import LinkWithIconLoader from '@/components/LinkWithIconLoader';
import Note from '@/components/Note';
import AppGrid from '@/components/AppGrid';
import Spinner from '@/components/Spinner';
import {
PATH_ADMIN_CONFIGURATION,
PATH_ADMIN_INSIGHTS,
PATH_ADMIN_PHOTOS_UPDATES,
checkPathPrefix,
isPathAdminInfo,
isPathTopLevelAdmin,
} from '@/app/path';
import { useAppState } from '@/app/AppState';
import { clsx } from 'clsx/lite';
import { differenceInMinutes } from 'date-fns';
import { usePathname } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react';
import { FaRegClock } from 'react-icons/fa';
import AdminAppInfoIcon from './AdminAppInfoIcon';
import AdminInfoNav from './AdminInfoNav';
import LinkWithLoaderBackground from '@/components/LinkWithLoaderBackground';
import MaskedScroll from '@/components/MaskedScroll';
// Updates from past 5 minutes considered recent
const areTimesRecent = (dates: Date[]) => dates
.some(date => differenceInMinutes(new Date(), date) < 5);
export default function AdminNavClient({
items,
mostRecentPhotoUpdateTime,
includeInsights = true,
}: {
items: {
label: string,
href: string,
count: number,
}[]
mostRecentPhotoUpdateTime?: Date
includeInsights?: boolean
}) {
const pathname = usePathname();
const { adminUpdateTimes = [] } = useAppState();
const updateTimes = useMemo(() =>
(mostRecentPhotoUpdateTime ? [mostRecentPhotoUpdateTime] : [])
.concat(adminUpdateTimes)
, [mostRecentPhotoUpdateTime, adminUpdateTimes]);
const [hasRecentUpdates, setHasRecentUpdates] =
useState(areTimesRecent(updateTimes));
useEffect(() => {
// Check every 1 second if update times are recent
const interval = setInterval(() =>
setHasRecentUpdates(areTimesRecent(updateTimes))
, 1_000);
return () => clearInterval(interval);
}, [updateTimes]);
const shouldShowBanner =
hasRecentUpdates &&
isPathTopLevelAdmin(pathname) &&
pathname !== PATH_ADMIN_PHOTOS_UPDATES;
return (
<AppGrid
contentMain={
<div className="space-y-4">
<div className={clsx(
'flex gap-2 pb-3',
'border-b border-gray-200 dark:border-gray-800',
)}>
<MaskedScroll
className="grow -mx-1 flex gap-0.5 md:gap-1.5"
direction="horizontal"
>
{items.map(({ label, href, count }) =>
<LinkWithLoaderBackground
key={label}
href={href}
className={clsx(
'flex gap-0.5',
checkPathPrefix(pathname, href) ? 'font-bold' : 'text-dim',
'hover:text-main active:text-medium',
)}
prefetch={false}
>
<span>{label}</span>
{count > 0 &&
<span>({count})</span>}
</LinkWithLoaderBackground>)}
</MaskedScroll>
<LinkWithIconLoader
href={includeInsights
? PATH_ADMIN_INSIGHTS
: PATH_ADMIN_CONFIGURATION}
className={clsx(
isPathAdminInfo(pathname)
? 'font-bold'
: 'text-dim',
'hover:text-main active:text-dim',
)}
icon={<AdminAppInfoIcon />}
loader={<Spinner className="translate-y-[-0.75px]" />}
/>
</div>
{shouldShowBanner &&
<Note icon={<FaRegClock className="shrink-0" />}>
Photo updates detectedthey may take several minutes to show up
for visitors
</Note>}
{isPathAdminInfo(pathname) &&
<AdminInfoNav {...{ includeInsights }} />}
</div>
}
/>
);
}