Integrate dynamic data into admin menu, update cmdk-menu
This commit is contained in:
parent
97d8fef130
commit
ac19ed2215
@ -1,7 +1,7 @@
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import clsx from 'clsx/lite';
|
||||
import { FaCircle } from 'react-icons/fa6';
|
||||
import { LuCog } from 'react-icons/lu';
|
||||
import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
|
||||
|
||||
export default function AdminAppInfoIcon({
|
||||
size = 'large',
|
||||
@ -18,22 +18,15 @@ export default function AdminAppInfoIcon({
|
||||
className,
|
||||
)}>
|
||||
<LuCog
|
||||
size={size === 'large' ? 20 : 18}
|
||||
size={size === 'large' ? 20 : 17}
|
||||
className="inline-flex translate-y-[1px]"
|
||||
aria-label="App Info"
|
||||
/>
|
||||
{insightIndicatorStatus &&
|
||||
<FaCircle
|
||||
size={size === 'large' ? 8 : 7}
|
||||
className={clsx(
|
||||
'absolute',
|
||||
size === 'large'
|
||||
? 'top-[1.5px] right-[0.5px]'
|
||||
: 'top-[1px] right-[-0.5px]',
|
||||
insightIndicatorStatus === 'blue'
|
||||
? 'text-blue-500'
|
||||
: 'text-amber-500',
|
||||
)}
|
||||
<InsightsIndicatorDot
|
||||
size={size}
|
||||
top={size === 'large' ? 1.5 : 1.5}
|
||||
right={size === 'large' ? 0.5 : 1}
|
||||
/>}
|
||||
</span>
|
||||
);
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
PATH_ADMIN_INSIGHTS,
|
||||
PATH_ADMIN_PHOTOS,
|
||||
PATH_ADMIN_TAGS,
|
||||
PATH_ADMIN_UPLOADS,
|
||||
PATH_GRID_INFERRED,
|
||||
} from '@/app/paths';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
@ -17,6 +18,8 @@ import { BiLockAlt } from 'react-icons/bi';
|
||||
import AdminAppInfoIcon from './AdminAppInfoIcon';
|
||||
import { PiSignOutBold } from 'react-icons/pi';
|
||||
import { signOutAndRedirectAction } from '@/auth/actions';
|
||||
import { ComponentProps } from 'react';
|
||||
import { FaRegFolderOpen } from 'react-icons/fa';
|
||||
|
||||
export default function AdminAppMenu({
|
||||
className,
|
||||
@ -26,12 +29,87 @@ export default function AdminAppMenu({
|
||||
buttonClassName?: string
|
||||
}) {
|
||||
const {
|
||||
photosCount,
|
||||
uploadsCount,
|
||||
tagsCount,
|
||||
selectedPhotoIds,
|
||||
setSelectedPhotoIds,
|
||||
} = useAppState();
|
||||
|
||||
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 (
|
||||
<MoreMenu
|
||||
header="Admin menu"
|
||||
@ -45,55 +123,7 @@ export default function AdminAppMenu({
|
||||
'rounded-none focus:outline-none',
|
||||
buttonClassName,
|
||||
)}
|
||||
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,
|
||||
}]}
|
||||
items={items}
|
||||
ariaLabel="Admin Menu"
|
||||
/>
|
||||
);
|
||||
|
||||
@ -7,7 +7,7 @@ import clsx from 'clsx/lite';
|
||||
import ClearCacheButton from '@/admin/ClearCacheButton';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import { FaCircle } from 'react-icons/fa6';
|
||||
import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
|
||||
|
||||
const ADMIN_INFO_PAGES = [{
|
||||
title: 'Insights',
|
||||
@ -65,14 +65,10 @@ export default function AdminInfoPage({
|
||||
{title}
|
||||
</ResponsiveText>
|
||||
{title === 'Insights' && insightIndicatorStatus &&
|
||||
<FaCircle
|
||||
size={6}
|
||||
className={clsx(
|
||||
insightIndicatorStatus === 'blue'
|
||||
? 'text-blue-500'
|
||||
: 'text-amber-500',
|
||||
'absolute top-[4px] right-[-2px]',
|
||||
)}
|
||||
<InsightsIndicatorDot
|
||||
size="small"
|
||||
top={4}
|
||||
right={-2}
|
||||
/>}
|
||||
</LinkWithStatus>)}
|
||||
</div>
|
||||
|
||||
@ -14,20 +14,21 @@ import AdminNavClient from './AdminNavClient';
|
||||
export default async function AdminNav() {
|
||||
const [
|
||||
countPhotos,
|
||||
countUploads,
|
||||
countTags,
|
||||
countUploads,
|
||||
mostRecentPhotoUpdateTime,
|
||||
] = await Promise.all([
|
||||
getPhotosMetaCached({ hidden: 'include' })
|
||||
.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;
|
||||
}),
|
||||
getUniqueTagsCached().then(tags => tags.length).catch(() => 0),
|
||||
getPhotosMostRecentUpdateCached().catch(() => undefined),
|
||||
]);
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ import { MdOutlineFileDownload } from 'react-icons/md';
|
||||
import MoreMenuItem from '@/components/more/MoreMenuItem';
|
||||
import IconGrSync from '@/app/IconGrSync';
|
||||
import { isPhotoOutdated } from '@/photo/outdated';
|
||||
import { FaCircle } from 'react-icons/fa6';
|
||||
import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
|
||||
|
||||
export default function AdminPhotoMenuClient({
|
||||
photo,
|
||||
@ -81,12 +81,13 @@ export default function AdminPhotoMenuClient({
|
||||
hrefDownloadName: downloadFileNameForPhoto(photo),
|
||||
});
|
||||
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>
|
||||
{isPhotoOutdated(photo) &&
|
||||
<FaCircle
|
||||
size={8}
|
||||
className="text-amber-500 translate-y-[1.5px]"
|
||||
<InsightsIndicatorDot
|
||||
colorOverride="yellow"
|
||||
className="translate-y-[1.5px]"
|
||||
/>}
|
||||
</span>,
|
||||
icon: <IconGrSync className="translate-x-[-1px]" />,
|
||||
|
||||
@ -6,6 +6,45 @@ import { testOpenAiConnection } from '@/platforms/openai';
|
||||
import { testDatabaseConnection } from '@/platforms/postgres';
|
||||
import { testStorageConnection } from '@/platforms/storage';
|
||||
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 = (
|
||||
shouldCheck: boolean,
|
||||
|
||||
50
src/admin/insights/InsightsIndicatorDot.tsx
Normal file
50
src/admin/insights/InsightsIndicatorDot.tsx
Normal 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 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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';
|
||||
}
|
||||
});
|
||||
28
src/admin/insights/server.ts
Normal file
28
src/admin/insights/server.ts
Normal 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';
|
||||
}
|
||||
};
|
||||
@ -44,7 +44,7 @@ import { TbPhoto } from 'react-icons/tb';
|
||||
import { getKeywordsForPhoto, titleForPhoto } from '@/photo';
|
||||
import PhotoDate from '@/photo/PhotoDate';
|
||||
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 { FaTag } from 'react-icons/fa';
|
||||
import { formatCount, formatCountDescriptive } from '@/utility/string';
|
||||
@ -52,6 +52,7 @@ import CommandKItem from './CommandKItem';
|
||||
import { GRID_HOMEPAGE_ENABLED } from '@/app/config';
|
||||
import { DialogDescription, DialogTitle } from '@radix-ui/react-dialog';
|
||||
import * as VisuallyHidden from '@radix-ui/react-visually-hidden';
|
||||
import InsightsIndicatorDot from '@/admin/insights/InsightsIndicatorDot';
|
||||
|
||||
const DIALOG_TITLE = 'Global Command-K Menu';
|
||||
const DIALOG_DESCRIPTION = 'For searching photos, views, and settings';
|
||||
@ -102,7 +103,9 @@ export default function CommandKClient({
|
||||
isUserSignedIn,
|
||||
setUserEmail,
|
||||
isCommandKOpen: isOpen,
|
||||
hiddenPhotosCount,
|
||||
photosCountHidden,
|
||||
uploadsCount,
|
||||
tagsCount,
|
||||
selectedPhotoIds,
|
||||
setSelectedPhotoIds,
|
||||
insightIndicatorStatus,
|
||||
@ -228,8 +231,8 @@ export default function CommandKClient({
|
||||
}, [isOpen, setShouldRespondToKeyboardCommands]);
|
||||
|
||||
const tagsIncludingHidden = useMemo(() =>
|
||||
addHiddenToTags(tags, hiddenPhotosCount)
|
||||
, [tags, hiddenPhotosCount]);
|
||||
addHiddenToTags(tags, photosCountHidden)
|
||||
, [tags, photosCountHidden]);
|
||||
|
||||
const SECTION_TAGS: CommandKSection = {
|
||||
heading: 'Tags',
|
||||
@ -336,70 +339,77 @@ export default function CommandKClient({
|
||||
const adminSection: CommandKSection = {
|
||||
heading: 'Admin',
|
||||
accessory: <BiSolidUser size={15} className="translate-x-[-1px]" />,
|
||||
items: isUserSignedIn
|
||||
? ([{
|
||||
label: 'Manage Photos',
|
||||
annotation: <BiLockAlt />,
|
||||
path: PATH_ADMIN_PHOTOS,
|
||||
}, {
|
||||
items: [],
|
||||
};
|
||||
|
||||
if (isUserSignedIn) {
|
||||
adminSection.items.push({
|
||||
label: 'Manage Photos',
|
||||
annotation: <BiLockAlt />,
|
||||
path: PATH_ADMIN_PHOTOS,
|
||||
});
|
||||
if (uploadsCount) {
|
||||
adminSection.items.push({
|
||||
label: 'Manage Uploads',
|
||||
annotation: <BiLockAlt />,
|
||||
path: PATH_ADMIN_UPLOADS,
|
||||
}, {
|
||||
});
|
||||
}
|
||||
if (tagsCount) {
|
||||
adminSection.items.push({
|
||||
label: 'Manage Tags',
|
||||
annotation: <BiLockAlt />,
|
||||
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 />,
|
||||
path: PATH_ADMIN_CONFIGURATION,
|
||||
path: PATH_ADMIN_BASELINE,
|
||||
}, {
|
||||
label: <span className="flex items-center gap-3">
|
||||
App Insights
|
||||
{insightIndicatorStatus && <FaCircle
|
||||
size={8}
|
||||
className={clsx(
|
||||
insightIndicatorStatus === 'blue'
|
||||
? 'text-blue-500'
|
||||
: 'text-amber-500',
|
||||
)}
|
||||
/>}
|
||||
</span>,
|
||||
keywords: ['app insights'],
|
||||
label: 'Components Overview',
|
||||
annotation: <BiLockAlt />,
|
||||
path: PATH_ADMIN_INSIGHTS,
|
||||
}, {
|
||||
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),
|
||||
}] as CommandKItem[])
|
||||
.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,
|
||||
}],
|
||||
};
|
||||
path: PATH_ADMIN_COMPONENTS,
|
||||
});
|
||||
}
|
||||
adminSection.items.push({
|
||||
label: 'Sign Out',
|
||||
action: () => {
|
||||
signOutAndRedirectAction().then(() => setUserEmail?.(undefined));
|
||||
},
|
||||
});
|
||||
} else {
|
||||
adminSection.items.push({
|
||||
label: 'Sign In',
|
||||
path: PATH_SIGN_IN,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Command.Dialog
|
||||
|
||||
@ -14,7 +14,7 @@ export default function MoreMenu({
|
||||
align = 'end',
|
||||
...props
|
||||
}: {
|
||||
items: ComponentProps<typeof MoreMenuItem> []
|
||||
items: ComponentProps<typeof MoreMenuItem>[]
|
||||
icon?: ReactNode
|
||||
header?: ReactNode
|
||||
className?: string
|
||||
|
||||
@ -9,6 +9,8 @@ import { downloadFileFromBrowser } from '@/utility/url';
|
||||
|
||||
export default function MoreMenuItem({
|
||||
label,
|
||||
labelComplex,
|
||||
annotation,
|
||||
icon,
|
||||
href,
|
||||
hrefDownloadName,
|
||||
@ -17,7 +19,9 @@ export default function MoreMenuItem({
|
||||
dismissMenu,
|
||||
shouldPreventDefault = true,
|
||||
}: {
|
||||
label: ReactNode
|
||||
label: string
|
||||
labelComplex?: ReactNode
|
||||
annotation?: string
|
||||
icon?: ReactNode
|
||||
href?: string
|
||||
hrefDownloadName?: string
|
||||
@ -48,7 +52,7 @@ export default function MoreMenuItem({
|
||||
disabled={isLoading}
|
||||
className={clsx(
|
||||
'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',
|
||||
'hover:bg-gray-100/90 active:bg-gray-200/75',
|
||||
'dark:hover:bg-gray-800/60 dark:active:bg-gray-900/80',
|
||||
@ -92,7 +96,11 @@ export default function MoreMenuItem({
|
||||
styleAs="link-without-hover"
|
||||
className="translate-y-[1px]"
|
||||
>
|
||||
{label}
|
||||
{labelComplex ?? label}
|
||||
{annotation &&
|
||||
<span className="text-dim ml-3">
|
||||
{annotation}
|
||||
</span>}
|
||||
</LoaderButton>
|
||||
</DropdownMenu.Item>
|
||||
);
|
||||
|
||||
@ -37,11 +37,11 @@ export default function PhotoGridSidebar({
|
||||
}) {
|
||||
const { start, end } = dateRangeForPhotos(undefined, photosDateRange);
|
||||
|
||||
const { hiddenPhotosCount } = useAppState();
|
||||
const { photosCountHidden } = useAppState();
|
||||
|
||||
const tagsIncludingHidden = useMemo(() =>
|
||||
addHiddenToTags(tags, hiddenPhotosCount)
|
||||
, [tags, hiddenPhotosCount]);
|
||||
addHiddenToTags(tags, photosCountHidden)
|
||||
, [tags, photosCountHidden]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
||||
@ -22,7 +22,6 @@ import { redirect } from 'next/navigation';
|
||||
import { deleteFile } from '@/platforms/storage';
|
||||
import {
|
||||
getPhotosCached,
|
||||
getPhotosMetaCached,
|
||||
revalidateAdminPaths,
|
||||
revalidateAllKeysAndPaths,
|
||||
revalidatePhoto,
|
||||
@ -416,10 +415,6 @@ export const streamAiImageQueryAction = async (
|
||||
export const getImageBlurAction = async (url: string) =>
|
||||
runAuthenticatedAdminServerAction(() => blurImageFromUrl(url));
|
||||
|
||||
export const getPhotosHiddenMetaCachedAction = async () =>
|
||||
runAuthenticatedAdminServerAction(() =>
|
||||
getPhotosMetaCached({ hidden: 'only' }));
|
||||
|
||||
// Public/Private actions
|
||||
|
||||
export const getPhotosAction = async (
|
||||
|
||||
@ -26,7 +26,10 @@ export interface AppStateContext {
|
||||
isUserSignedIn?: boolean
|
||||
adminUpdateTimes?: Date[]
|
||||
registerAdminUpdate?: () => void
|
||||
hiddenPhotosCount?: number
|
||||
photosCount?: number
|
||||
photosCountHidden?: number
|
||||
uploadsCount?: number
|
||||
tagsCount?: number
|
||||
selectedPhotoIds?: string[]
|
||||
setSelectedPhotoIds?: Dispatch<SetStateAction<string[] | undefined>>
|
||||
isPerformingSelectEdit?: boolean
|
||||
|
||||
@ -12,11 +12,10 @@ import {
|
||||
MATTE_PHOTOS,
|
||||
SHOW_ZOOM_CONTROLS,
|
||||
} from '@/app/config';
|
||||
import { getPhotosHiddenMetaCachedAction } from '@/photo/actions';
|
||||
import { ShareModalProps } from '@/share';
|
||||
import { storeTimezoneCookie } from '@/utility/timezone';
|
||||
import { getShouldShowInsightsIndicatorAction } from '@/admin/insights/actions';
|
||||
import { InsightIndicatorStatus } from '@/admin/insights';
|
||||
import { getAdminDataAction } from '@/admin/actions';
|
||||
|
||||
export default function AppStateProvider({
|
||||
children,
|
||||
@ -44,8 +43,14 @@ export default function AppStateProvider({
|
||||
useState<string>();
|
||||
const [adminUpdateTimes, setAdminUpdateTimes] =
|
||||
useState<Date[]>([]);
|
||||
const [hiddenPhotosCount, setHiddenPhotosCount] =
|
||||
useState(0);
|
||||
const [photosCount, setPhotosCount] =
|
||||
useState<number>();
|
||||
const [photosCountHidden, setPhotosCountHidden] =
|
||||
useState<number>();
|
||||
const [uploadsCount, setUploadsCount] =
|
||||
useState<number>();
|
||||
const [tagsCount, setTagsCount] =
|
||||
useState<number>();
|
||||
const [selectedPhotoIds, setSelectedPhotoIds] =
|
||||
useState<string[] | undefined>();
|
||||
const [isPerformingSelectEdit, setIsPerformingSelectEdit] =
|
||||
@ -70,26 +75,37 @@ export default function AppStateProvider({
|
||||
|
||||
const invalidateSwr = useCallback(() => setSwrTimestamp(Date.now()), []);
|
||||
|
||||
const { data, error } = useSWR('getAuth', getAuthAction);
|
||||
const { data: auth, error: authError } = useSWR('getAuth', getAuthAction);
|
||||
useEffect(() => {
|
||||
if (!error) {
|
||||
setUserEmail(data?.user?.email ?? undefined);
|
||||
if (!authError) {
|
||||
setUserEmail(auth?.user?.email ?? undefined);
|
||||
}
|
||||
}, [data, error]);
|
||||
}, [auth, authError]);
|
||||
const isUserSignedIn = Boolean(userEmail);
|
||||
|
||||
const { data: adminData, error: adminError } = useSWR(
|
||||
isUserSignedIn ? 'getAdminData' : null,
|
||||
getAdminDataAction, {
|
||||
refreshInterval: 1000 * 60 * 5,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isUserSignedIn) {
|
||||
const timeout = setTimeout(() =>{
|
||||
getPhotosHiddenMetaCachedAction()
|
||||
.then(({ count }) => setHiddenPhotosCount(count));
|
||||
getShouldShowInsightsIndicatorAction()
|
||||
.then(setInsightIndicatorStatus);
|
||||
}, 100);
|
||||
return () => clearTimeout(timeout);
|
||||
if (adminData) {
|
||||
const timeout = setTimeout(() => {
|
||||
setPhotosCount(adminData.countPhotos);
|
||||
setPhotosCountHidden(adminData.countHiddenPhotos);
|
||||
setUploadsCount(adminData.countUploads);
|
||||
setTagsCount(adminData.countTags);
|
||||
setInsightIndicatorStatus(adminData.shouldShowInsightsIndicator);
|
||||
}, 100);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
} else {
|
||||
setHiddenPhotosCount(0);
|
||||
setPhotosCountHidden(0);
|
||||
}
|
||||
}, [isUserSignedIn]);
|
||||
}, [adminData, adminError, isUserSignedIn]);
|
||||
|
||||
const registerAdminUpdate = useCallback(() =>
|
||||
setAdminUpdateTimes(updates => [...updates, new Date()])
|
||||
@ -125,7 +141,10 @@ export default function AppStateProvider({
|
||||
isUserSignedIn,
|
||||
adminUpdateTimes,
|
||||
registerAdminUpdate,
|
||||
hiddenPhotosCount,
|
||||
photosCount,
|
||||
photosCountHidden,
|
||||
uploadsCount,
|
||||
tagsCount,
|
||||
selectedPhotoIds,
|
||||
setSelectedPhotoIds,
|
||||
isPerformingSelectEdit,
|
||||
|
||||
@ -105,11 +105,11 @@ export const isPathFavs = (pathname?: string) =>
|
||||
|
||||
export const isTagHidden = (tag: string) => tag.toLowerCase() === TAG_HIDDEN;
|
||||
|
||||
export const addHiddenToTags = (tags: Tags, hiddenPhotosCount = 0) => {
|
||||
if (hiddenPhotosCount > 0) {
|
||||
export const addHiddenToTags = (tags: Tags, photosCountHidden = 0) => {
|
||||
if (photosCountHidden > 0) {
|
||||
return tags
|
||||
.filter(({ tag }) => tag === TAG_FAVS)
|
||||
.concat({ tag: TAG_HIDDEN, count: hiddenPhotosCount })
|
||||
.concat({ tag: TAG_HIDDEN, count: photosCountHidden })
|
||||
.concat(tags.filter(({ tag }) => tag !== TAG_FAVS));
|
||||
} else {
|
||||
return tags;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user