Refactor admin sub-nav
This commit is contained in:
parent
9f483bcf21
commit
bd7cf64f2a
@ -1,13 +1,9 @@
|
|||||||
import ClearCacheButton from '@/admin/ClearCacheButton';
|
|
||||||
import AdminAppConfiguration from '@/admin/AdminAppConfiguration';
|
import AdminAppConfiguration from '@/admin/AdminAppConfiguration';
|
||||||
import AdminInfoPage from '@/admin/AdminInfoPage';
|
import AdminInfoPage from '@/admin/AdminInfoPage';
|
||||||
|
|
||||||
export default function AdminAppConfigurationPage() {
|
export default function AdminAppConfigurationPage() {
|
||||||
return (
|
return (
|
||||||
<AdminInfoPage
|
<AdminInfoPage page="Config">
|
||||||
title="App Configuration"
|
|
||||||
accessory={<ClearCacheButton />}
|
|
||||||
>
|
|
||||||
<AdminAppConfiguration />
|
<AdminAppConfiguration />
|
||||||
</AdminInfoPage>
|
</AdminInfoPage>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import AdminAppInsights from '@/admin/insights/AdminAppInsights';
|
|||||||
import AdminInfoPage from '@/admin/AdminInfoPage';
|
import AdminInfoPage from '@/admin/AdminInfoPage';
|
||||||
|
|
||||||
export default async function AdminInsightsPage() {
|
export default async function AdminInsightsPage() {
|
||||||
return <AdminInfoPage title="App Insights">
|
return <AdminInfoPage page="Insights">
|
||||||
<AdminAppInsights />
|
<AdminAppInsights />
|
||||||
</AdminInfoPage>;
|
</AdminInfoPage>;
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/admin/AdminAppInfoIcon.tsx
Normal file
36
src/admin/AdminAppInfoIcon.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { useAppState } from '@/state/AppState';
|
||||||
|
import clsx from 'clsx/lite';
|
||||||
|
import { FaCircle } from 'react-icons/fa6';
|
||||||
|
import { LuCog } from 'react-icons/lu';
|
||||||
|
|
||||||
|
export default function AdminAppInfoIcon({
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { insightIndicatorStatus } = useAppState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={clsx(
|
||||||
|
'inline-flex relative',
|
||||||
|
className,
|
||||||
|
)}>
|
||||||
|
<LuCog
|
||||||
|
size={20}
|
||||||
|
className="inline-flex translate-y-[1px]"
|
||||||
|
aria-label="App Info"
|
||||||
|
/>
|
||||||
|
{insightIndicatorStatus &&
|
||||||
|
<FaCircle
|
||||||
|
size={8}
|
||||||
|
className={clsx(
|
||||||
|
'absolute',
|
||||||
|
'top-[2px] right-[0.5px]',
|
||||||
|
insightIndicatorStatus === 'blue'
|
||||||
|
? 'text-blue-500'
|
||||||
|
: 'text-amber-500',
|
||||||
|
)}
|
||||||
|
/>}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,14 +1,27 @@
|
|||||||
|
import { PATH_ADMIN_CONFIGURATION, PATH_ADMIN_INSIGHTS } from '@/app/paths';
|
||||||
import Container from '@/components/Container';
|
import Container from '@/components/Container';
|
||||||
|
import LinkWithStatus from '@/components/LinkWithStatus';
|
||||||
|
import ResponsiveText from '@/components/primitives/ResponsiveText';
|
||||||
import SiteGrid from '@/components/SiteGrid';
|
import SiteGrid from '@/components/SiteGrid';
|
||||||
|
import clsx from 'clsx/lite';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
import ClearCacheButton from '@/admin/ClearCacheButton';
|
||||||
|
|
||||||
|
const ADMIN_INFO_PAGES = [{
|
||||||
|
titleShort: 'Insights',
|
||||||
|
path: PATH_ADMIN_INSIGHTS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Configuration',
|
||||||
|
titleShort: 'Config',
|
||||||
|
path: PATH_ADMIN_CONFIGURATION,
|
||||||
|
}];
|
||||||
|
|
||||||
export default function AdminInfoPage({
|
export default function AdminInfoPage({
|
||||||
title,
|
page,
|
||||||
accessory,
|
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
title: string
|
page: (typeof ADMIN_INFO_PAGES)[number]['titleShort']
|
||||||
accessory?: ReactNode
|
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@ -16,10 +29,28 @@ export default function AdminInfoPage({
|
|||||||
contentMain={
|
contentMain={
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-4 min-h-9">
|
<div className="flex items-center gap-4 min-h-9">
|
||||||
<div className="grow">
|
<div className={clsx(
|
||||||
{title}
|
'grow -translate-x-1 -translate-y-1',
|
||||||
|
'flex items-center gap-3',
|
||||||
|
)}>
|
||||||
|
{ADMIN_INFO_PAGES.map(({ title, titleShort, path }) =>
|
||||||
|
<LinkWithStatus
|
||||||
|
key={path}
|
||||||
|
href={path}
|
||||||
|
className={clsx(
|
||||||
|
page === titleShort
|
||||||
|
? 'underline underline-offset-10 decoration-[1.5px]'
|
||||||
|
: 'text-dim',
|
||||||
|
'px-1 py-0.5 rounded-md',
|
||||||
|
)}
|
||||||
|
loadingClassName="bg-dim"
|
||||||
|
>
|
||||||
|
<ResponsiveText shortText={titleShort}>
|
||||||
|
{title ?? titleShort}
|
||||||
|
</ResponsiveText>
|
||||||
|
</LinkWithStatus>)}
|
||||||
</div>
|
</div>
|
||||||
{accessory}
|
<ClearCacheButton />
|
||||||
</div>
|
</div>
|
||||||
<Container spaceChildren={false}>
|
<Container spaceChildren={false}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -6,11 +6,9 @@ import Note from '@/components/Note';
|
|||||||
import SiteGrid from '@/components/SiteGrid';
|
import SiteGrid from '@/components/SiteGrid';
|
||||||
import Spinner from '@/components/Spinner';
|
import Spinner from '@/components/Spinner';
|
||||||
import {
|
import {
|
||||||
PATH_ADMIN_CONFIGURATION,
|
|
||||||
PATH_ADMIN_INSIGHTS,
|
PATH_ADMIN_INSIGHTS,
|
||||||
checkPathPrefix,
|
checkPathPrefix,
|
||||||
isPathAdminConfiguration,
|
isPathAdminInfo,
|
||||||
isPathAdminInsights,
|
|
||||||
isPathTopLevelAdmin,
|
isPathTopLevelAdmin,
|
||||||
} from '@/app/paths';
|
} from '@/app/paths';
|
||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
@ -19,8 +17,7 @@ import { differenceInMinutes } from 'date-fns';
|
|||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { FaRegClock } from 'react-icons/fa';
|
import { FaRegClock } from 'react-icons/fa';
|
||||||
import AdminAppInsightsIcon from './insights/AdminAppInsightsIcon';
|
import AdminAppInfoIcon from './AdminAppInfoIcon';
|
||||||
import { LuCog } from 'react-icons/lu';
|
|
||||||
|
|
||||||
// Updates considered recent if they occurred in past 5 minutes
|
// Updates considered recent if they occurred in past 5 minutes
|
||||||
const areTimesRecent = (dates: Date[]) => dates
|
const areTimesRecent = (dates: Date[]) => dates
|
||||||
@ -29,6 +26,8 @@ const areTimesRecent = (dates: Date[]) => dates
|
|||||||
export default function AdminNavClient({
|
export default function AdminNavClient({
|
||||||
items,
|
items,
|
||||||
mostRecentPhotoUpdateTime,
|
mostRecentPhotoUpdateTime,
|
||||||
|
// TODO: use this with new <AdminSubNav> component
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
includeInsights,
|
includeInsights,
|
||||||
}: {
|
}: {
|
||||||
items: {
|
items: {
|
||||||
@ -91,34 +90,16 @@ export default function AdminNavClient({
|
|||||||
<span>({count})</span>}
|
<span>({count})</span>}
|
||||||
</LinkWithStatus>)}
|
</LinkWithStatus>)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
|
||||||
{includeInsights &&
|
|
||||||
<LinkWithLoader
|
<LinkWithLoader
|
||||||
href={PATH_ADMIN_INSIGHTS}
|
href={PATH_ADMIN_INSIGHTS}
|
||||||
className={clsx(
|
className={isPathAdminInfo(pathname)
|
||||||
'translate-y-[-2px]',
|
|
||||||
isPathAdminInsights(pathname)
|
|
||||||
? 'font-bold'
|
|
||||||
: 'text-dim')}
|
|
||||||
loader={<Spinner className="translate-y-[1px]" />}
|
|
||||||
>
|
|
||||||
<AdminAppInsightsIcon />
|
|
||||||
</LinkWithLoader>}
|
|
||||||
<LinkWithLoader
|
|
||||||
href={PATH_ADMIN_CONFIGURATION}
|
|
||||||
className={isPathAdminConfiguration(pathname)
|
|
||||||
? 'font-bold'
|
? 'font-bold'
|
||||||
: 'text-dim'}
|
: 'text-dim'}
|
||||||
loader={<Spinner className="translate-y-[-0.75px]" />}
|
loader={<Spinner className="translate-y-[-0.75px]" />}
|
||||||
>
|
>
|
||||||
<LuCog
|
<AdminAppInfoIcon />
|
||||||
size={20}
|
|
||||||
className="inline-flex translate-y-[1px]"
|
|
||||||
aria-label="App Configuration"
|
|
||||||
/>
|
|
||||||
</LinkWithLoader>
|
</LinkWithLoader>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{shouldShowBanner &&
|
{shouldShowBanner &&
|
||||||
<Note icon={<FaRegClock className="shrink-0" />}>
|
<Note icon={<FaRegClock className="shrink-0" />}>
|
||||||
Photo updates detected—they may take several minutes to show up
|
Photo updates detected—they may take several minutes to show up
|
||||||
|
|||||||
@ -64,6 +64,7 @@ export const PATHS_ADMIN = [
|
|||||||
PATH_ADMIN_PHOTOS,
|
PATH_ADMIN_PHOTOS,
|
||||||
PATH_ADMIN_UPLOADS,
|
PATH_ADMIN_UPLOADS,
|
||||||
PATH_ADMIN_TAGS,
|
PATH_ADMIN_TAGS,
|
||||||
|
PATH_ADMIN_INSIGHTS,
|
||||||
PATH_ADMIN_CONFIGURATION,
|
PATH_ADMIN_CONFIGURATION,
|
||||||
PATH_ADMIN_BASELINE,
|
PATH_ADMIN_BASELINE,
|
||||||
PATH_ADMIN_COMPONENTS,
|
PATH_ADMIN_COMPONENTS,
|
||||||
@ -225,11 +226,15 @@ export const isPathAdmin = (pathname?: string) =>
|
|||||||
export const isPathTopLevelAdmin = (pathname?: string) =>
|
export const isPathTopLevelAdmin = (pathname?: string) =>
|
||||||
PATHS_ADMIN.some(path => path === pathname);
|
PATHS_ADMIN.some(path => path === pathname);
|
||||||
|
|
||||||
|
export const isPathAdminInsights = (pathname?: string) =>
|
||||||
|
checkPathPrefix(pathname, PATH_ADMIN_INSIGHTS);
|
||||||
|
|
||||||
export const isPathAdminConfiguration = (pathname?: string) =>
|
export const isPathAdminConfiguration = (pathname?: string) =>
|
||||||
checkPathPrefix(pathname, PATH_ADMIN_CONFIGURATION);
|
checkPathPrefix(pathname, PATH_ADMIN_CONFIGURATION);
|
||||||
|
|
||||||
export const isPathAdminInsights = (pathname?: string) =>
|
export const isPathAdminInfo = (pathname?: string) =>
|
||||||
checkPathPrefix(pathname, PATH_ADMIN_INSIGHTS);
|
isPathAdminInsights(pathname) ||
|
||||||
|
isPathAdminConfiguration(pathname);
|
||||||
|
|
||||||
export const isPathProtected = (pathname?: string) =>
|
export const isPathProtected = (pathname?: string) =>
|
||||||
checkPathPrefix(pathname, PATH_ADMIN) ||
|
checkPathPrefix(pathname, PATH_ADMIN) ||
|
||||||
|
|||||||
@ -24,6 +24,7 @@ export type LinkWithStatusProps = Omit<
|
|||||||
children: ReactNode | ((props: {
|
children: ReactNode | ((props: {
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
}) => ReactNode)
|
}) => ReactNode)
|
||||||
|
debugLoading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LinkWithStatus({
|
export default function LinkWithStatus({
|
||||||
@ -32,12 +33,14 @@ export default function LinkWithStatus({
|
|||||||
className,
|
className,
|
||||||
onClick,
|
onClick,
|
||||||
children,
|
children,
|
||||||
|
debugLoading = false,
|
||||||
...props
|
...props
|
||||||
}: LinkWithStatusProps) {
|
}: LinkWithStatusProps) {
|
||||||
const path = usePathname();
|
const path = usePathname();
|
||||||
|
|
||||||
const [pathWhenClicked, setPathWhenClicked] = useState<string>();
|
const [pathWhenClicked, setPathWhenClicked] = useState<string>();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [_isLoading, setIsLoading] = useState(false);
|
||||||
|
const isLoading = _isLoading || debugLoading;
|
||||||
|
|
||||||
const isLoadingStartTime = useRef<number | undefined>(undefined);
|
const isLoadingStartTime = useRef<number | undefined>(undefined);
|
||||||
|
|
||||||
|
|||||||
@ -301,6 +301,7 @@
|
|||||||
disabled:bg-gray-100 dark:disabled:bg-gray-900
|
disabled:bg-gray-100 dark:disabled:bg-gray-900
|
||||||
disabled:border-gray-200 disabled:dark:border-gray-700
|
disabled:border-gray-200 disabled:dark:border-gray-700
|
||||||
border-gray-900 dark:border-gray-100
|
border-gray-900 dark:border-gray-100
|
||||||
|
hover:border-gray-900 dark:hover:border-gray-100
|
||||||
active:bg-gray-700 active:border-gray-700
|
active:bg-gray-700 active:border-gray-700
|
||||||
active:dark:bg-gray-300 active:dark:border-gray-300
|
active:dark:bg-gray-300 active:dark:border-gray-300
|
||||||
shadow-none
|
shadow-none
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user