Integrate dynamic data into admin menu, update cmdk-menu

This commit is contained in:
Sam Becker 2025-02-26 17:41:17 -06:00
parent 97d8fef130
commit ac19ed2215
17 changed files with 344 additions and 207 deletions

View File

@ -1,7 +1,7 @@
import { useAppState } from '@/state/AppState'; import { useAppState } from '@/state/AppState';
import clsx from 'clsx/lite'; import clsx from 'clsx/lite';
import { FaCircle } from 'react-icons/fa6';
import { LuCog } from 'react-icons/lu'; import { LuCog } from 'react-icons/lu';
import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
export default function AdminAppInfoIcon({ export default function AdminAppInfoIcon({
size = 'large', size = 'large',
@ -18,22 +18,15 @@ export default function AdminAppInfoIcon({
className, className,
)}> )}>
<LuCog <LuCog
size={size === 'large' ? 20 : 18} size={size === 'large' ? 20 : 17}
className="inline-flex translate-y-[1px]" className="inline-flex translate-y-[1px]"
aria-label="App Info" aria-label="App Info"
/> />
{insightIndicatorStatus && {insightIndicatorStatus &&
<FaCircle <InsightsIndicatorDot
size={size === 'large' ? 8 : 7} size={size}
className={clsx( top={size === 'large' ? 1.5 : 1.5}
'absolute', right={size === 'large' ? 0.5 : 1}
size === 'large'
? 'top-[1.5px] right-[0.5px]'
: 'top-[1px] right-[-0.5px]',
insightIndicatorStatus === 'blue'
? 'text-blue-500'
: 'text-amber-500',
)}
/>} />}
</span> </span>
); );

View File

@ -5,6 +5,7 @@ import {
PATH_ADMIN_INSIGHTS, PATH_ADMIN_INSIGHTS,
PATH_ADMIN_PHOTOS, PATH_ADMIN_PHOTOS,
PATH_ADMIN_TAGS, PATH_ADMIN_TAGS,
PATH_ADMIN_UPLOADS,
PATH_GRID_INFERRED, PATH_GRID_INFERRED,
} from '@/app/paths'; } from '@/app/paths';
import { useAppState } from '@/state/AppState'; import { useAppState } from '@/state/AppState';
@ -17,6 +18,8 @@ import { BiLockAlt } from 'react-icons/bi';
import AdminAppInfoIcon from './AdminAppInfoIcon'; import AdminAppInfoIcon from './AdminAppInfoIcon';
import { PiSignOutBold } from 'react-icons/pi'; import { PiSignOutBold } from 'react-icons/pi';
import { signOutAndRedirectAction } from '@/auth/actions'; import { signOutAndRedirectAction } from '@/auth/actions';
import { ComponentProps } from 'react';
import { FaRegFolderOpen } from 'react-icons/fa';
export default function AdminAppMenu({ export default function AdminAppMenu({
className, className,
@ -26,12 +29,87 @@ export default function AdminAppMenu({
buttonClassName?: string buttonClassName?: string
}) { }) {
const { const {
photosCount,
uploadsCount,
tagsCount,
selectedPhotoIds, selectedPhotoIds,
setSelectedPhotoIds, setSelectedPhotoIds,
} = useAppState(); } = useAppState();
const isSelecting = selectedPhotoIds !== undefined; const isSelecting = selectedPhotoIds !== undefined;
const items: ComponentProps<typeof MoreMenu>['items'] = [{
label: 'Manage Photos',
...photosCount !== undefined && {
annotation: `${photosCount}`,
},
icon: <TbPhoto
size={15}
className="translate-x-[-0.5px] translate-y-[0.5px]"
/>,
href: PATH_ADMIN_PHOTOS,
}];
if (uploadsCount) {
items.push({
label: 'Uploads',
annotation: `${uploadsCount}`,
icon: <FaRegFolderOpen
size={16}
className="translate-y-[0.5px]"
/>,
href: PATH_ADMIN_UPLOADS,
});
}
if (tagsCount) {
items.push({
label: 'Manage Tags',
annotation: `${tagsCount}`,
icon: <FiTag
size={15}
className="translate-y-[0.5px]"
/>,
href: PATH_ADMIN_TAGS,
});
}
items.push({
label: 'App Info',
icon: <AdminAppInfoIcon
size="small"
className="translate-x-[-0.5px] translate-y-[-0.5px]"
/>,
href: PATH_ADMIN_INSIGHTS,
}, {
label: isSelecting
? 'Exit Select'
: 'Edit Multiple …',
icon: isSelecting
? <IoCloseSharp
className="text-[18px] translate-x-[-1px] translate-y-[1px]"
/>
: <ImCheckboxUnchecked
className="translate-x-[-0.5px] text-[0.75rem]"
/>,
href: PATH_GRID_INFERRED,
action: () => {
if (isSelecting) {
setSelectedPhotoIds?.(undefined);
} else {
setSelectedPhotoIds?.([]);
}
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
},
shouldPreventDefault: false,
}, {
label: 'Sign Out',
icon: <PiSignOutBold size={15} />,
action: signOutAndRedirectAction,
});
return ( return (
<MoreMenu <MoreMenu
header="Admin menu" header="Admin menu"
@ -45,55 +123,7 @@ export default function AdminAppMenu({
'rounded-none focus:outline-none', 'rounded-none focus:outline-none',
buttonClassName, buttonClassName,
)} )}
items={[{ items={items}
label: 'Manage Photos',
icon: <TbPhoto
size={16}
className="translate-x-[1px] translate-y-[0.5px]"
/>,
href: PATH_ADMIN_PHOTOS,
}, {
label: 'Manage Tags',
icon: <FiTag
size={16}
className="translate-x-[1.5px] translate-y-[0.5px]"
/>,
href: PATH_ADMIN_TAGS,
}, {
label: 'App Info',
icon: <AdminAppInfoIcon
size="small"
className="translate-x-[1px] translate-y-[-0.5px]"
/>,
href: PATH_ADMIN_INSIGHTS,
}, {
label: isSelecting
? 'Exit Select'
: 'Select Photos',
icon: isSelecting
? <IoCloseSharp
className="text-[18px] translate-y-[-0.5px]"
/>
: <ImCheckboxUnchecked
className="text-[0.75rem] translate-x-[1px]"
/>,
href: PATH_GRID_INFERRED,
action: () => {
if (isSelecting) {
setSelectedPhotoIds?.(undefined);
} else {
setSelectedPhotoIds?.([]);
}
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
},
shouldPreventDefault: false,
}, {
label: 'Sign Out',
icon: <PiSignOutBold size={15} className="translate-x-[1px]" />,
action: signOutAndRedirectAction,
}]}
ariaLabel="Admin Menu" ariaLabel="Admin Menu"
/> />
); );

View File

@ -7,7 +7,7 @@ import clsx from 'clsx/lite';
import ClearCacheButton from '@/admin/ClearCacheButton'; import ClearCacheButton from '@/admin/ClearCacheButton';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { useAppState } from '@/state/AppState'; import { useAppState } from '@/state/AppState';
import { FaCircle } from 'react-icons/fa6'; import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
const ADMIN_INFO_PAGES = [{ const ADMIN_INFO_PAGES = [{
title: 'Insights', title: 'Insights',
@ -65,14 +65,10 @@ export default function AdminInfoPage({
{title} {title}
</ResponsiveText> </ResponsiveText>
{title === 'Insights' && insightIndicatorStatus && {title === 'Insights' && insightIndicatorStatus &&
<FaCircle <InsightsIndicatorDot
size={6} size="small"
className={clsx( top={4}
insightIndicatorStatus === 'blue' right={-2}
? 'text-blue-500'
: 'text-amber-500',
'absolute top-[4px] right-[-2px]',
)}
/>} />}
</LinkWithStatus>)} </LinkWithStatus>)}
</div> </div>

View File

@ -14,20 +14,21 @@ import AdminNavClient from './AdminNavClient';
export default async function AdminNav() { export default async function AdminNav() {
const [ const [
countPhotos, countPhotos,
countUploads,
countTags, countTags,
countUploads,
mostRecentPhotoUpdateTime, mostRecentPhotoUpdateTime,
] = await Promise.all([ ] = await Promise.all([
getPhotosMetaCached({ hidden: 'include' }) getPhotosMetaCached({ hidden: 'include' })
.then(({ count }) => count) .then(({ count }) => count)
.catch(() => 0), .catch(() => 0),
getUniqueTagsCached().then(tags => tags.length)
.catch(() => 0),
getStorageUploadUrlsNoStore() getStorageUploadUrlsNoStore()
.then(urls => urls.length) .then(urls => urls.length)
.catch(e => { .catch(e => {
console.error(`Error getting blob upload urls: ${e}`); console.error(`Error getting blob upload urls: ${e}`);
return 0; return 0;
}), }),
getUniqueTagsCached().then(tags => tags.length).catch(() => 0),
getPhotosMostRecentUpdateCached().catch(() => undefined), getPhotosMostRecentUpdateCached().catch(() => undefined),
]); ]);

View File

@ -23,7 +23,7 @@ import { MdOutlineFileDownload } from 'react-icons/md';
import MoreMenuItem from '@/components/more/MoreMenuItem'; import MoreMenuItem from '@/components/more/MoreMenuItem';
import IconGrSync from '@/app/IconGrSync'; import IconGrSync from '@/app/IconGrSync';
import { isPhotoOutdated } from '@/photo/outdated'; import { isPhotoOutdated } from '@/photo/outdated';
import { FaCircle } from 'react-icons/fa6'; import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
export default function AdminPhotoMenuClient({ export default function AdminPhotoMenuClient({
photo, photo,
@ -81,12 +81,13 @@ export default function AdminPhotoMenuClient({
hrefDownloadName: downloadFileNameForPhoto(photo), hrefDownloadName: downloadFileNameForPhoto(photo),
}); });
items.push({ items.push({
label: <span className="inline-flex items-center gap-2"> label: 'Sync',
labelComplex: <span className="inline-flex items-center gap-2">
<span>Sync</span> <span>Sync</span>
{isPhotoOutdated(photo) && {isPhotoOutdated(photo) &&
<FaCircle <InsightsIndicatorDot
size={8} colorOverride="yellow"
className="text-amber-500 translate-y-[1.5px]" className="translate-y-[1.5px]"
/>} />}
</span>, </span>,
icon: <IconGrSync className="translate-x-[-1px]" />, icon: <IconGrSync className="translate-x-[-1px]" />,

View File

@ -6,6 +6,45 @@ import { testOpenAiConnection } from '@/platforms/openai';
import { testDatabaseConnection } from '@/platforms/postgres'; import { testDatabaseConnection } from '@/platforms/postgres';
import { testStorageConnection } from '@/platforms/storage'; import { testStorageConnection } from '@/platforms/storage';
import { APP_CONFIGURATION } from '@/app/config'; import { APP_CONFIGURATION } from '@/app/config';
import { getStorageUploadUrlsNoStore } from '@/platforms/storage/cache';
import { getPhotosMetaCached, getUniqueTagsCached } from '@/photo/cache';
import { getShouldShowInsightsIndicator } from '@/admin/insights/server';
export const getAdminDataAction = async () =>
runAuthenticatedAdminServerAction(async () => {
const [
countPhotos,
countHiddenPhotos,
countTags,
countUploads,
shouldShowInsightsIndicator,
] = await Promise.all([
getPhotosMetaCached()
.then(({ count }) => count)
.catch(() => 0),
getPhotosMetaCached({ hidden: 'only' })
.then(({ count }) => count)
.catch(() => 0),
getUniqueTagsCached()
.then(tags => tags.length)
.catch(() => 0),
getStorageUploadUrlsNoStore()
.then(urls => urls.length)
.catch(e => {
console.error(`Error getting blob upload urls: ${e}`);
return 0;
}),
getShouldShowInsightsIndicator(),
]);
return {
countPhotos,
countHiddenPhotos,
countTags,
countUploads,
shouldShowInsightsIndicator,
};
});
const scanForError = ( const scanForError = (
shouldCheck: boolean, shouldCheck: boolean,

View File

@ -0,0 +1,50 @@
import { useAppState } from '@/state/AppState';
import clsx from 'clsx/lite';
import { FaCircle } from 'react-icons/fa6';
export default function InsightsIndicatorDot({
className,
size = 'medium',
colorOverride,
top,
right,
bottom,
left,
}: {
className?: string
size?: 'small' | 'medium' | 'large'
colorOverride?: 'blue' | 'yellow'
top?: number
right?: number
bottom?: number
left?: number
}) {
const { insightIndicatorStatus } = useAppState();
const getSize = () => {
switch (size) {
case 'small': return 6;
case 'medium': return 7;
case 'large': return 8;
}
};
return (
<FaCircle
size={getSize()}
className={clsx(
(
top !== undefined ||
right !== undefined ||
bottom !== undefined ||
left !== undefined
) && 'absolute',
(colorOverride ?? insightIndicatorStatus) === 'blue'
? 'text-blue-500'
: 'text-amber-500',
className,
)}
style={{ top, right, bottom, left }}
/>
);
}

View File

@ -1,36 +0,0 @@
'use server';
import { runAuthenticatedAdminServerAction } from '@/auth';
import {
getGitHubMetaForCurrentApp,
getSignificantInsights,
InsightIndicatorStatus,
} from '.';
import { getOutdatedPhotosCount } from '@/photo/db/query';
export const getShouldShowInsightsIndicatorAction =
async (): Promise<InsightIndicatorStatus> =>
runAuthenticatedAdminServerAction(async () => {
const [
codeMeta,
photosCountOutdated,
] = await Promise.all([
getGitHubMetaForCurrentApp(),
getOutdatedPhotosCount(),
]);
const {
forkBehind,
noAiRateLimiting,
outdatedPhotos,
} = getSignificantInsights({
codeMeta,
photosCountOutdated,
});
if (noAiRateLimiting || outdatedPhotos) {
return 'yellow';
} else if (forkBehind) {
return 'blue';
}
});

View File

@ -0,0 +1,28 @@
import { getOutdatedPhotosCount } from '@/photo/db/query';
import { getSignificantInsights } from '.';
import { getGitHubMetaForCurrentApp } from '.';
export const getShouldShowInsightsIndicator = async () => {
const [
codeMeta,
photosCountOutdated,
] = await Promise.all([
getGitHubMetaForCurrentApp(),
getOutdatedPhotosCount(),
]);
const {
forkBehind,
noAiRateLimiting,
outdatedPhotos,
} = getSignificantInsights({
codeMeta,
photosCountOutdated,
});
if (noAiRateLimiting || outdatedPhotos) {
return 'yellow';
} else if (forkBehind) {
return 'blue';
}
};

View File

@ -44,7 +44,7 @@ import { TbPhoto } from 'react-icons/tb';
import { getKeywordsForPhoto, titleForPhoto } from '@/photo'; import { getKeywordsForPhoto, titleForPhoto } from '@/photo';
import PhotoDate from '@/photo/PhotoDate'; import PhotoDate from '@/photo/PhotoDate';
import PhotoSmall from '@/photo/PhotoSmall'; import PhotoSmall from '@/photo/PhotoSmall';
import { FaCheck, FaCircle } from 'react-icons/fa6'; import { FaCheck } from 'react-icons/fa6';
import { Tags, addHiddenToTags, formatTag } from '@/tag'; import { Tags, addHiddenToTags, formatTag } from '@/tag';
import { FaTag } from 'react-icons/fa'; import { FaTag } from 'react-icons/fa';
import { formatCount, formatCountDescriptive } from '@/utility/string'; import { formatCount, formatCountDescriptive } from '@/utility/string';
@ -52,6 +52,7 @@ import CommandKItem from './CommandKItem';
import { GRID_HOMEPAGE_ENABLED } from '@/app/config'; import { GRID_HOMEPAGE_ENABLED } from '@/app/config';
import { DialogDescription, DialogTitle } from '@radix-ui/react-dialog'; import { DialogDescription, DialogTitle } from '@radix-ui/react-dialog';
import * as VisuallyHidden from '@radix-ui/react-visually-hidden'; import * as VisuallyHidden from '@radix-ui/react-visually-hidden';
import InsightsIndicatorDot from '@/admin/insights/InsightsIndicatorDot';
const DIALOG_TITLE = 'Global Command-K Menu'; const DIALOG_TITLE = 'Global Command-K Menu';
const DIALOG_DESCRIPTION = 'For searching photos, views, and settings'; const DIALOG_DESCRIPTION = 'For searching photos, views, and settings';
@ -102,7 +103,9 @@ export default function CommandKClient({
isUserSignedIn, isUserSignedIn,
setUserEmail, setUserEmail,
isCommandKOpen: isOpen, isCommandKOpen: isOpen,
hiddenPhotosCount, photosCountHidden,
uploadsCount,
tagsCount,
selectedPhotoIds, selectedPhotoIds,
setSelectedPhotoIds, setSelectedPhotoIds,
insightIndicatorStatus, insightIndicatorStatus,
@ -228,8 +231,8 @@ export default function CommandKClient({
}, [isOpen, setShouldRespondToKeyboardCommands]); }, [isOpen, setShouldRespondToKeyboardCommands]);
const tagsIncludingHidden = useMemo(() => const tagsIncludingHidden = useMemo(() =>
addHiddenToTags(tags, hiddenPhotosCount) addHiddenToTags(tags, photosCountHidden)
, [tags, hiddenPhotosCount]); , [tags, photosCountHidden]);
const SECTION_TAGS: CommandKSection = { const SECTION_TAGS: CommandKSection = {
heading: 'Tags', heading: 'Tags',
@ -336,70 +339,77 @@ export default function CommandKClient({
const adminSection: CommandKSection = { const adminSection: CommandKSection = {
heading: 'Admin', heading: 'Admin',
accessory: <BiSolidUser size={15} className="translate-x-[-1px]" />, accessory: <BiSolidUser size={15} className="translate-x-[-1px]" />,
items: isUserSignedIn items: [],
? ([{ };
label: 'Manage Photos',
annotation: <BiLockAlt />, if (isUserSignedIn) {
path: PATH_ADMIN_PHOTOS, adminSection.items.push({
}, { label: 'Manage Photos',
annotation: <BiLockAlt />,
path: PATH_ADMIN_PHOTOS,
});
if (uploadsCount) {
adminSection.items.push({
label: 'Manage Uploads', label: 'Manage Uploads',
annotation: <BiLockAlt />, annotation: <BiLockAlt />,
path: PATH_ADMIN_UPLOADS, path: PATH_ADMIN_UPLOADS,
}, { });
}
if (tagsCount) {
adminSection.items.push({
label: 'Manage Tags', label: 'Manage Tags',
annotation: <BiLockAlt />, annotation: <BiLockAlt />,
path: PATH_ADMIN_TAGS, path: PATH_ADMIN_TAGS,
}, { });
label: 'App Config', }
adminSection.items.push({
label: <span className="flex items-center gap-3">
App Insights
{insightIndicatorStatus &&
<InsightsIndicatorDot />}
</span>,
keywords: ['app insights'],
annotation: <BiLockAlt />,
path: PATH_ADMIN_INSIGHTS,
}, {
label: 'App Config',
annotation: <BiLockAlt />,
path: PATH_ADMIN_CONFIGURATION,
}, {
label: selectedPhotoIds === undefined
? 'Select Multiple Photos'
: 'Exit Select Multiple Photos',
annotation: <BiLockAlt />,
path: selectedPhotoIds === undefined
? PATH_GRID_INFERRED
: undefined,
action: selectedPhotoIds === undefined
? () => setSelectedPhotoIds?.([])
: () => setSelectedPhotoIds?.(undefined),
});
if (showDebugTools) {
adminSection.items.push({
label: 'Baseline Overview',
annotation: <BiLockAlt />, annotation: <BiLockAlt />,
path: PATH_ADMIN_CONFIGURATION, path: PATH_ADMIN_BASELINE,
}, { }, {
label: <span className="flex items-center gap-3"> label: 'Components Overview',
App Insights
{insightIndicatorStatus && <FaCircle
size={8}
className={clsx(
insightIndicatorStatus === 'blue'
? 'text-blue-500'
: 'text-amber-500',
)}
/>}
</span>,
keywords: ['app insights'],
annotation: <BiLockAlt />, annotation: <BiLockAlt />,
path: PATH_ADMIN_INSIGHTS, path: PATH_ADMIN_COMPONENTS,
}, { });
label: selectedPhotoIds === undefined }
? 'Select Multiple Photos' adminSection.items.push({
: 'Exit Select Multiple Photos', label: 'Sign Out',
annotation: <BiLockAlt />, action: () => {
path: selectedPhotoIds === undefined signOutAndRedirectAction().then(() => setUserEmail?.(undefined));
? PATH_GRID_INFERRED },
: undefined, });
action: selectedPhotoIds === undefined } else {
? () => setSelectedPhotoIds?.([]) adminSection.items.push({
: () => setSelectedPhotoIds?.(undefined), label: 'Sign In',
}] as CommandKItem[]) path: PATH_SIGN_IN,
.concat(showDebugTools });
? [{ }
label: 'Baseline Overview',
path: PATH_ADMIN_BASELINE,
}, {
label: 'Components Overview',
path: PATH_ADMIN_COMPONENTS,
}]
: [])
.concat({
label: 'Sign Out',
action: () => {
signOutAndRedirectAction().then(() => setUserEmail?.(undefined));
},
})
: [{
label: 'Sign In',
path: PATH_SIGN_IN,
}],
};
return ( return (
<Command.Dialog <Command.Dialog

View File

@ -14,7 +14,7 @@ export default function MoreMenu({
align = 'end', align = 'end',
...props ...props
}: { }: {
items: ComponentProps<typeof MoreMenuItem> [] items: ComponentProps<typeof MoreMenuItem>[]
icon?: ReactNode icon?: ReactNode
header?: ReactNode header?: ReactNode
className?: string className?: string

View File

@ -9,6 +9,8 @@ import { downloadFileFromBrowser } from '@/utility/url';
export default function MoreMenuItem({ export default function MoreMenuItem({
label, label,
labelComplex,
annotation,
icon, icon,
href, href,
hrefDownloadName, hrefDownloadName,
@ -17,7 +19,9 @@ export default function MoreMenuItem({
dismissMenu, dismissMenu,
shouldPreventDefault = true, shouldPreventDefault = true,
}: { }: {
label: ReactNode label: string
labelComplex?: ReactNode
annotation?: string
icon?: ReactNode icon?: ReactNode
href?: string href?: string
hrefDownloadName?: string hrefDownloadName?: string
@ -48,7 +52,7 @@ export default function MoreMenuItem({
disabled={isLoading} disabled={isLoading}
className={clsx( className={clsx(
'flex items-center h-9', 'flex items-center h-9',
'pl-2 pr-4 py-2 rounded-sm', 'pl-2 pr-3 py-2 rounded-sm',
'select-none hover:outline-hidden', 'select-none hover:outline-hidden',
'hover:bg-gray-100/90 active:bg-gray-200/75', 'hover:bg-gray-100/90 active:bg-gray-200/75',
'dark:hover:bg-gray-800/60 dark:active:bg-gray-900/80', 'dark:hover:bg-gray-800/60 dark:active:bg-gray-900/80',
@ -92,7 +96,11 @@ export default function MoreMenuItem({
styleAs="link-without-hover" styleAs="link-without-hover"
className="translate-y-[1px]" className="translate-y-[1px]"
> >
{label} {labelComplex ?? label}
{annotation &&
<span className="text-dim ml-3">
{annotation}
</span>}
</LoaderButton> </LoaderButton>
</DropdownMenu.Item> </DropdownMenu.Item>
); );

View File

@ -37,11 +37,11 @@ export default function PhotoGridSidebar({
}) { }) {
const { start, end } = dateRangeForPhotos(undefined, photosDateRange); const { start, end } = dateRangeForPhotos(undefined, photosDateRange);
const { hiddenPhotosCount } = useAppState(); const { photosCountHidden } = useAppState();
const tagsIncludingHidden = useMemo(() => const tagsIncludingHidden = useMemo(() =>
addHiddenToTags(tags, hiddenPhotosCount) addHiddenToTags(tags, photosCountHidden)
, [tags, hiddenPhotosCount]); , [tags, photosCountHidden]);
return ( return (
<div className="space-y-4"> <div className="space-y-4">

View File

@ -22,7 +22,6 @@ import { redirect } from 'next/navigation';
import { deleteFile } from '@/platforms/storage'; import { deleteFile } from '@/platforms/storage';
import { import {
getPhotosCached, getPhotosCached,
getPhotosMetaCached,
revalidateAdminPaths, revalidateAdminPaths,
revalidateAllKeysAndPaths, revalidateAllKeysAndPaths,
revalidatePhoto, revalidatePhoto,
@ -416,10 +415,6 @@ export const streamAiImageQueryAction = async (
export const getImageBlurAction = async (url: string) => export const getImageBlurAction = async (url: string) =>
runAuthenticatedAdminServerAction(() => blurImageFromUrl(url)); runAuthenticatedAdminServerAction(() => blurImageFromUrl(url));
export const getPhotosHiddenMetaCachedAction = async () =>
runAuthenticatedAdminServerAction(() =>
getPhotosMetaCached({ hidden: 'only' }));
// Public/Private actions // Public/Private actions
export const getPhotosAction = async ( export const getPhotosAction = async (

View File

@ -26,7 +26,10 @@ export interface AppStateContext {
isUserSignedIn?: boolean isUserSignedIn?: boolean
adminUpdateTimes?: Date[] adminUpdateTimes?: Date[]
registerAdminUpdate?: () => void registerAdminUpdate?: () => void
hiddenPhotosCount?: number photosCount?: number
photosCountHidden?: number
uploadsCount?: number
tagsCount?: number
selectedPhotoIds?: string[] selectedPhotoIds?: string[]
setSelectedPhotoIds?: Dispatch<SetStateAction<string[] | undefined>> setSelectedPhotoIds?: Dispatch<SetStateAction<string[] | undefined>>
isPerformingSelectEdit?: boolean isPerformingSelectEdit?: boolean

View File

@ -12,11 +12,10 @@ import {
MATTE_PHOTOS, MATTE_PHOTOS,
SHOW_ZOOM_CONTROLS, SHOW_ZOOM_CONTROLS,
} from '@/app/config'; } from '@/app/config';
import { getPhotosHiddenMetaCachedAction } from '@/photo/actions';
import { ShareModalProps } from '@/share'; import { ShareModalProps } from '@/share';
import { storeTimezoneCookie } from '@/utility/timezone'; import { storeTimezoneCookie } from '@/utility/timezone';
import { getShouldShowInsightsIndicatorAction } from '@/admin/insights/actions';
import { InsightIndicatorStatus } from '@/admin/insights'; import { InsightIndicatorStatus } from '@/admin/insights';
import { getAdminDataAction } from '@/admin/actions';
export default function AppStateProvider({ export default function AppStateProvider({
children, children,
@ -44,8 +43,14 @@ export default function AppStateProvider({
useState<string>(); useState<string>();
const [adminUpdateTimes, setAdminUpdateTimes] = const [adminUpdateTimes, setAdminUpdateTimes] =
useState<Date[]>([]); useState<Date[]>([]);
const [hiddenPhotosCount, setHiddenPhotosCount] = const [photosCount, setPhotosCount] =
useState(0); useState<number>();
const [photosCountHidden, setPhotosCountHidden] =
useState<number>();
const [uploadsCount, setUploadsCount] =
useState<number>();
const [tagsCount, setTagsCount] =
useState<number>();
const [selectedPhotoIds, setSelectedPhotoIds] = const [selectedPhotoIds, setSelectedPhotoIds] =
useState<string[] | undefined>(); useState<string[] | undefined>();
const [isPerformingSelectEdit, setIsPerformingSelectEdit] = const [isPerformingSelectEdit, setIsPerformingSelectEdit] =
@ -70,26 +75,37 @@ export default function AppStateProvider({
const invalidateSwr = useCallback(() => setSwrTimestamp(Date.now()), []); const invalidateSwr = useCallback(() => setSwrTimestamp(Date.now()), []);
const { data, error } = useSWR('getAuth', getAuthAction); const { data: auth, error: authError } = useSWR('getAuth', getAuthAction);
useEffect(() => { useEffect(() => {
if (!error) { if (!authError) {
setUserEmail(data?.user?.email ?? undefined); setUserEmail(auth?.user?.email ?? undefined);
} }
}, [data, error]); }, [auth, authError]);
const isUserSignedIn = Boolean(userEmail); const isUserSignedIn = Boolean(userEmail);
const { data: adminData, error: adminError } = useSWR(
isUserSignedIn ? 'getAdminData' : null,
getAdminDataAction, {
refreshInterval: 1000 * 60 * 5,
},
);
useEffect(() => { useEffect(() => {
if (isUserSignedIn) { if (isUserSignedIn) {
const timeout = setTimeout(() =>{ if (adminData) {
getPhotosHiddenMetaCachedAction() const timeout = setTimeout(() => {
.then(({ count }) => setHiddenPhotosCount(count)); setPhotosCount(adminData.countPhotos);
getShouldShowInsightsIndicatorAction() setPhotosCountHidden(adminData.countHiddenPhotos);
.then(setInsightIndicatorStatus); setUploadsCount(adminData.countUploads);
}, 100); setTagsCount(adminData.countTags);
return () => clearTimeout(timeout); setInsightIndicatorStatus(adminData.shouldShowInsightsIndicator);
}, 100);
return () => clearTimeout(timeout);
}
} else { } else {
setHiddenPhotosCount(0); setPhotosCountHidden(0);
} }
}, [isUserSignedIn]); }, [adminData, adminError, isUserSignedIn]);
const registerAdminUpdate = useCallback(() => const registerAdminUpdate = useCallback(() =>
setAdminUpdateTimes(updates => [...updates, new Date()]) setAdminUpdateTimes(updates => [...updates, new Date()])
@ -125,7 +141,10 @@ export default function AppStateProvider({
isUserSignedIn, isUserSignedIn,
adminUpdateTimes, adminUpdateTimes,
registerAdminUpdate, registerAdminUpdate,
hiddenPhotosCount, photosCount,
photosCountHidden,
uploadsCount,
tagsCount,
selectedPhotoIds, selectedPhotoIds,
setSelectedPhotoIds, setSelectedPhotoIds,
isPerformingSelectEdit, isPerformingSelectEdit,

View File

@ -105,11 +105,11 @@ export const isPathFavs = (pathname?: string) =>
export const isTagHidden = (tag: string) => tag.toLowerCase() === TAG_HIDDEN; export const isTagHidden = (tag: string) => tag.toLowerCase() === TAG_HIDDEN;
export const addHiddenToTags = (tags: Tags, hiddenPhotosCount = 0) => { export const addHiddenToTags = (tags: Tags, photosCountHidden = 0) => {
if (hiddenPhotosCount > 0) { if (photosCountHidden > 0) {
return tags return tags
.filter(({ tag }) => tag === TAG_FAVS) .filter(({ tag }) => tag === TAG_FAVS)
.concat({ tag: TAG_HIDDEN, count: hiddenPhotosCount }) .concat({ tag: TAG_HIDDEN, count: photosCountHidden })
.concat(tags.filter(({ tag }) => tag !== TAG_FAVS)); .concat(tags.filter(({ tag }) => tag !== TAG_FAVS));
} else { } else {
return tags; return tags;