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);