Switch more menus to proper links

This commit is contained in:
Sam Becker 2025-07-19 13:56:46 -05:00
parent c78c6fd7e0
commit fc8cca3f7f
5 changed files with 103 additions and 63 deletions

View File

@ -81,7 +81,7 @@ export default function AdminAppMenu({
annotation: `${uploadsCount}`,
icon: <IconFolder
size={16}
className="translate-x-[1px] translate-y-[1px]"
className="translate-x-[1px] translate-y-[0.5px]"
/>,
href: PATH_ADMIN_UPLOADS,
});
@ -114,7 +114,7 @@ export default function AdminAppMenu({
},
icon: <IconPhoto
size={15}
className="translate-x-[-0.5px] translate-y-[1px]"
className="translate-x-[-0.5px] translate-y-[0.5px]"
/>,
href: PATH_ADMIN_PHOTOS,
});
@ -136,7 +136,7 @@ export default function AdminAppMenu({
annotation: `${recipesCount}`,
icon: <IconRecipe
size={17}
className="translate-x-[-0.5px] translate-y-[1px]"
className="translate-x-[-0.5px]"
/>,
href: PATH_ADMIN_RECIPES,
});
@ -175,7 +175,7 @@ export default function AdminAppMenu({
: appText.admin.appConfig,
icon: <AdminAppInfoIcon
size="small"
className="translate-x-[-0.5px] translate-y-[0.5px]"
className="translate-x-[-0.5px]"
/>,
href: showAppInsightsLink
? PATH_ADMIN_INSIGHTS

View File

@ -1,6 +1,6 @@
'use client';
import { ComponentProps, ReactNode, useState } from 'react';
import { ComponentProps, ReactNode, useEffect, useRef, useState } from 'react';
import Link from 'next/link';
import LinkWithStatusChild from './primitives/LinkWithStatusChild';
import clsx from 'clsx/lite';
@ -11,6 +11,8 @@ export default function LinkWithStatus({
loadingClassName,
isLoading: isLoadingProp = false,
setIsLoading: setIsLoadingProp,
onLoad,
flickerThreshold,
...props
}: Omit<ComponentProps<typeof Link>, 'children'> & {
children: ReactNode | ((props: { isLoading: boolean }) => ReactNode)
@ -18,6 +20,8 @@ export default function LinkWithStatus({
// For hoisting state to a parent component, e.g., <EntityLink />
isLoading?: boolean
setIsLoading?: (isLoading: boolean) => void
onLoad?: () => void
flickerThreshold?: number
}) {
const [_isLoading, _setIsLoading] = useState(false);
const isLoading = isLoadingProp || _isLoading;
@ -25,6 +29,16 @@ export default function LinkWithStatus({
const isControlled = typeof children === 'function';
const hasStartedRef = useRef(false);
useEffect(() => {
if (isLoading) {
hasStartedRef.current = true;
} else if (hasStartedRef.current) {
onLoad?.();
hasStartedRef.current = false;
}
}, [isLoading, onLoad]);
return <Link
{...props}
className={clsx(
@ -36,7 +50,7 @@ export default function LinkWithStatus({
isLoading && loadingClassName,
)}
>
<LinkWithStatusChild {...{ setIsLoading }}>
<LinkWithStatusChild {...{ setIsLoading, flickerThreshold }}>
{typeof children === 'function'
? children({ isLoading })
: children}

View File

@ -0,0 +1,36 @@
import { ComponentProps, ReactNode } from 'react';
import LinkWithStatus from './LinkWithStatus';
import Spinner from './Spinner';
import clsx from 'clsx/lite';
export default function LoaderLink({
icon,
classNameIcon,
children,
...props
}: Omit<ComponentProps<typeof LinkWithStatus>, 'children'> & {
icon: ReactNode
classNameIcon?: string
children?: ReactNode
}) {
return (
<LinkWithStatus {...props}>
{({ isLoading }) =>
<span className="inline-flex items-center gap-1.5">
<span className={clsx(
'inline-flex items-center justify-center',
'min-w-[1.25rem] h-6',
classNameIcon,
)}>
{isLoading
? <Spinner />
: icon}
</span>
{children &&
<span>
{children}
</span>}
</span>}
</LinkWithStatus>
);
}

View File

@ -5,14 +5,12 @@ import { clsx } from 'clsx/lite';
import {
ComponentProps,
ReactNode,
useEffect,
useState,
useTransition,
} from 'react';
import LoaderButton from '../primitives/LoaderButton';
import { usePathname, useRouter } from 'next/navigation';
import { downloadFileFromBrowser } from '@/utility/url';
import KeyCommand from '../primitives/KeyCommand';
import LoaderLink from '../LoaderLink';
export default function MoreMenuItem({
label,
@ -43,26 +41,8 @@ export default function MoreMenuItem({
keyCommand?: string
keyCommandModifier?: ComponentProps<typeof KeyCommand>['modifier']
}) {
const router = useRouter();
const pathname = usePathname();
const [isPending, startTransition] = useTransition();
const [transitionDidStart, setTransitionDidStart] = useState(false);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (transitionDidStart && !isPending) {
dismissMenu?.();
setTransitionDidStart(false);
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}
}, [isPending, dismissMenu, transitionDidStart]);
const getColorClasses = () => {
switch (color) {
case 'grey': return clsx(
@ -76,6 +56,16 @@ export default function MoreMenuItem({
}
};
const buttonContent = <>
<span>
{labelComplex ?? label}
</span>
{annotation &&
<span className="text-dim ml-3">
{annotation}
</span>}
</>;
return (
<DropdownMenu.Item
disabled={isLoading}
@ -112,41 +102,39 @@ export default function MoreMenuItem({
dismissMenu?.();
}
}
if (href) {
if (href !== pathname) {
if (hrefDownloadName) {
setIsLoading(true);
downloadFileFromBrowser(href, hrefDownloadName)
.finally(() => {
setIsLoading(false);
dismissMenu?.();
});
} else {
setTransitionDidStart(true);
startTransition(() => router.push(href));
}
} else {
dismissMenu?.();
}
if (href && hrefDownloadName) {
setIsLoading(true);
downloadFileFromBrowser(href, hrefDownloadName)
.finally(() => {
setIsLoading(false);
dismissMenu?.();
});
}
}}
>
<LoaderButton
icon={icon}
isLoading={isLoading || isPending}
hideText="never"
styleAs="link-without-hover"
className="translate-y-[0.5px] text-sm grow"
classNameIcon="translate-y-[-0.5px]!"
>
<span>
{labelComplex ?? label}
</span>
{annotation &&
<span className="text-dim ml-3">
{annotation}
</span>}
</LoaderButton>
{href && !hrefDownloadName
? <LoaderLink
icon={icon}
href={href}
className={clsx(
'inline-flex items-center grow',
'text-sm text-main hover:text-main',
)}
onLoad={dismissMenu}
flickerThreshold={0}
>
{buttonContent}
</LoaderLink>
: <LoaderButton
icon={icon}
isLoading={isLoading}
hideText="never"
styleAs="link-without-hover"
className="translate-y-[0.5px] text-sm grow"
classNameIcon="translate-y-[-0.5px]!"
>
{buttonContent}
</LoaderButton>}
{keyCommand &&
<KeyCommand
modifier={keyCommandModifier}

View File

@ -3,14 +3,16 @@
import { ReactNode, useEffect, useRef } from 'react';
import { useLinkStatus } from 'next/link';
const FLICKER_THRESHOLD = 400;
const DEFAULT_FLICKER_THRESHOLD = 400;
export default function LinkWithStatusChild({
children,
setIsLoading,
flickerThreshold = DEFAULT_FLICKER_THRESHOLD,
}: {
children: ReactNode
setIsLoading: (isLoading: boolean) => void
flickerThreshold?: number
}) {
const { pending } = useLinkStatus();
@ -26,7 +28,7 @@ export default function LinkWithStatusChild({
startLoadingTimeout.current = setTimeout(() => {
setIsLoading(true);
isLoadingStartTime.current = Date.now();
}, FLICKER_THRESHOLD);
}, flickerThreshold);
} else if (startLoadingTimeout.current) {
clearTimeout(startLoadingTimeout.current);
startLoadingTimeout.current = undefined;
@ -34,9 +36,9 @@ export default function LinkWithStatusChild({
stopLoadingTimeout.current = setTimeout(() => {
setIsLoading(false);
isLoadingStartTime.current = undefined;
}, FLICKER_THRESHOLD - loadingDuration);
}, Math.max(0, flickerThreshold - loadingDuration));
}
}, [pending, setIsLoading]);
}, [pending, setIsLoading, flickerThreshold]);
useEffect(() => () => {
clearTimeout(startLoadingTimeout.current);