diff --git a/src/admin/AdminAppMenu.tsx b/src/admin/AdminAppMenu.tsx index a0130339..dc64bdd1 100644 --- a/src/admin/AdminAppMenu.tsx +++ b/src/admin/AdminAppMenu.tsx @@ -81,7 +81,7 @@ export default function AdminAppMenu({ annotation: `${uploadsCount}`, icon: , href: PATH_ADMIN_UPLOADS, }); @@ -114,7 +114,7 @@ export default function AdminAppMenu({ }, icon: , href: PATH_ADMIN_PHOTOS, }); @@ -136,7 +136,7 @@ export default function AdminAppMenu({ annotation: `${recipesCount}`, icon: , href: PATH_ADMIN_RECIPES, }); @@ -175,7 +175,7 @@ export default function AdminAppMenu({ : appText.admin.appConfig, icon: , href: showAppInsightsLink ? PATH_ADMIN_INSIGHTS diff --git a/src/components/LinkWithStatus.tsx b/src/components/LinkWithStatus.tsx index db93cbe1..53c6882b 100644 --- a/src/components/LinkWithStatus.tsx +++ b/src/components/LinkWithStatus.tsx @@ -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, 'children'> & { children: ReactNode | ((props: { isLoading: boolean }) => ReactNode) @@ -18,6 +20,8 @@ export default function LinkWithStatus({ // For hoisting state to a parent component, e.g., 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 - + {typeof children === 'function' ? children({ isLoading }) : children} diff --git a/src/components/LoaderLink.tsx b/src/components/LoaderLink.tsx new file mode 100644 index 00000000..ad0598ed --- /dev/null +++ b/src/components/LoaderLink.tsx @@ -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, 'children'> & { + icon: ReactNode + classNameIcon?: string + children?: ReactNode +}) { + return ( + + {({ isLoading }) => + + + {isLoading + ? + : icon} + + {children && + + {children} + } + } + + ); +} diff --git a/src/components/more/MoreMenuItem.tsx b/src/components/more/MoreMenuItem.tsx index 2a98b124..da167f36 100644 --- a/src/components/more/MoreMenuItem.tsx +++ b/src/components/more/MoreMenuItem.tsx @@ -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['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 = <> + + {labelComplex ?? label} + + {annotation && + + {annotation} + } + ; + return ( { - setIsLoading(false); - dismissMenu?.(); - }); - } else { - setTransitionDidStart(true); - startTransition(() => router.push(href)); - } - } else { - dismissMenu?.(); - } + if (href && hrefDownloadName) { + setIsLoading(true); + downloadFileFromBrowser(href, hrefDownloadName) + .finally(() => { + setIsLoading(false); + dismissMenu?.(); + }); } }} > - - - {labelComplex ?? label} - - {annotation && - - {annotation} - } - + {href && !hrefDownloadName + ? + {buttonContent} + + : + {buttonContent} + } {keyCommand && 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);