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 { 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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@ -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]" />,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
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 { 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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user