diff --git a/src/components/SwitcherItem.tsx b/src/components/SwitcherItem.tsx
index b69b00fc..38eabe07 100644
--- a/src/components/SwitcherItem.tsx
+++ b/src/components/SwitcherItem.tsx
@@ -11,6 +11,7 @@ export default function SwitcherItem({
className: classNameProp,
onClick,
active,
+ isInteractive = true,
noPadding,
prefetch = SHOULD_PREFETCH_ALL_LINKS,
}: {
@@ -20,21 +21,24 @@ export default function SwitcherItem({
className?: string
onClick?: () => void
active?: boolean
+ isInteractive?: boolean
noPadding?: boolean
prefetch?: boolean
}) {
const className = clsx(
- classNameProp,
+ 'flex items-center justify-center',
+ 'w-[42px] h-full',
'py-0.5 px-1.5',
- 'cursor-pointer',
- 'hover:bg-gray-100/60 active:bg-gray-100',
- 'dark:hover:bg-gray-900/75 dark:active:bg-gray-900',
+ isInteractive && 'cursor-pointer',
+ isInteractive && 'hover:bg-gray-100/60 active:bg-gray-100',
+ isInteractive && 'dark:hover:bg-gray-900/75 dark:active:bg-gray-900',
active
? 'text-black dark:text-white'
: 'text-gray-400 dark:text-gray-600',
active
? 'hover:text-black dark:hover:text-white'
: 'hover:text-gray-700 dark:hover:text-gray-400',
+ classNameProp,
);
const renderIcon = () => noPadding
@@ -54,6 +58,8 @@ export default function SwitcherItem({
}}>
{renderIcon()}
- :
{renderIcon()}
+ :
+ {renderIcon()}
+
);
};
diff --git a/src/components/cmdk/CommandKClient.tsx b/src/components/cmdk/CommandKClient.tsx
index 2f5682ea..3bf4bd5d 100644
--- a/src/components/cmdk/CommandKClient.tsx
+++ b/src/components/cmdk/CommandKClient.tsx
@@ -39,12 +39,12 @@ import { searchPhotosAction } from '@/photo/actions';
import { RiToolsFill } from 'react-icons/ri';
import { BiLockAlt, BiSolidUser } from 'react-icons/bi';
import { HiDocumentText } from 'react-icons/hi';
-import { signOutAndRedirectAction } from '@/auth/actions';
+import { signOutAction } from '@/auth/actions';
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';
@@ -100,9 +101,11 @@ export default function CommandKClient({
const {
isUserSignedIn,
- setUserEmail,
+ clearAuthStateAndRedirect,
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,75 @@ export default function CommandKClient({
const adminSection: CommandKSection = {
heading: 'Admin',
accessory:
,
- items: isUserSignedIn
- ? ([{
- label: 'Manage Photos',
- annotation:
,
- path: PATH_ADMIN_PHOTOS,
- }, {
+ items: [],
+ };
+
+ if (isUserSignedIn) {
+ adminSection.items.push({
+ label: 'Manage Photos',
+ annotation:
,
+ path: PATH_ADMIN_PHOTOS,
+ });
+ if (uploadsCount) {
+ adminSection.items.push({
label: 'Manage Uploads',
annotation:
,
path: PATH_ADMIN_UPLOADS,
- }, {
+ });
+ }
+ if (tagsCount) {
+ adminSection.items.push({
label: 'Manage Tags',
annotation:
,
path: PATH_ADMIN_TAGS,
- }, {
- label: 'App Config',
+ });
+ }
+ adminSection.items.push({
+ label:
+ App Insights
+ {insightIndicatorStatus &&
+ }
+ ,
+ keywords: ['app insights'],
+ annotation:
,
+ path: PATH_ADMIN_INSIGHTS,
+ }, {
+ label: 'App Config',
+ annotation:
,
+ path: PATH_ADMIN_CONFIGURATION,
+ }, {
+ label: selectedPhotoIds === undefined
+ ? 'Select Multiple Photos'
+ : 'Exit Select Multiple Photos',
+ annotation:
,
+ path: selectedPhotoIds === undefined
+ ? PATH_GRID_INFERRED
+ : undefined,
+ action: selectedPhotoIds === undefined
+ ? () => setSelectedPhotoIds?.([])
+ : () => setSelectedPhotoIds?.(undefined),
+ });
+ if (showDebugTools) {
+ adminSection.items.push({
+ label: 'Baseline Overview',
annotation:
,
- path: PATH_ADMIN_CONFIGURATION,
+ path: PATH_ADMIN_BASELINE,
}, {
- label:
- App Insights
- {insightIndicatorStatus && }
- ,
- keywords: ['app insights'],
+ label: 'Components Overview',
annotation:
,
- path: PATH_ADMIN_INSIGHTS,
- }, {
- label: selectedPhotoIds === undefined
- ? 'Select Multiple Photos'
- : 'Exit Select Multiple Photos',
- annotation:
,
- 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: () => signOutAction().then(clearAuthStateAndRedirect),
+ });
+ } else {
+ adminSection.items.push({
+ label: 'Sign In',
+ path: PATH_SIGN_IN,
+ });
+ }
return (
[]
+ items: ComponentProps[]
+ icon?: ReactNode
+ header?: ReactNode
className?: string
buttonClassName?: string
ariaLabel: string
-}){
+ onOpen?: () => void
+} & ComponentProps){
+ const [isOpen, setIsOpen] = useState(false);
+
+ const dismissMenu = useCallback(() => {
+ setIsOpen(false);
+ }, [setIsOpen]);
+
+ useEffect(() => {
+ if (isOpen) { onOpen?.(); }
+ }, [isOpen, onOpen]);
+
return (
-
+
-
+ {icon ?? }
-
+ {header &&
+ {header}
+
}
{items.map(props =>
- ,
+ ,
)}
diff --git a/src/components/more/MoreMenuItem.tsx b/src/components/more/MoreMenuItem.tsx
index cc2c904d..fe54671d 100644
--- a/src/components/more/MoreMenuItem.tsx
+++ b/src/components/more/MoreMenuItem.tsx
@@ -2,26 +2,32 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { clsx } from 'clsx/lite';
-import { ReactNode, useState, useTransition } from 'react';
+import { ReactNode, useEffect, useState, useTransition } from 'react';
import LoaderButton from '../primitives/LoaderButton';
import { usePathname, useRouter } from 'next/navigation';
import { downloadFileFromBrowser } from '@/utility/url';
export default function MoreMenuItem({
label,
+ labelComplex,
+ annotation,
icon,
href,
hrefDownloadName,
className,
action,
+ dismissMenu,
shouldPreventDefault = true,
}: {
- label: ReactNode
+ label: string
+ labelComplex?: ReactNode
+ annotation?: string
icon?: ReactNode
href?: string
hrefDownloadName?: string
className?: string
action?: () => Promise | void
+ dismissMenu?: () => void
shouldPreventDefault?: boolean
}) {
const router = useRouter();
@@ -30,38 +36,54 @@ export default function MoreMenuItem({
const [isPending, startTransition] = useTransition();
+ const [transitionDidStart, setTransitionDidStart] = useState(false);
+
const [isLoading, setIsLoading] = useState(false);
+ useEffect(() => {
+ if (transitionDidStart && !isPending) {
+ dismissMenu?.();
+ setTransitionDidStart(false);
+ }
+ }, [isPending, dismissMenu, transitionDidStart]);
+
return (
{
+ onSelect={async e => {
if (shouldPreventDefault) { e.preventDefault(); }
if (action) {
const result = action();
if (result instanceof Promise) {
setIsLoading(true);
- await result.finally(() => setIsLoading(false));
+ await result.finally(() => {
+ setIsLoading(false);
+ dismissMenu?.();
+ });
}
}
if (href && href !== pathname) {
if (hrefDownloadName) {
setIsLoading(true);
downloadFileFromBrowser(href, hrefDownloadName)
- .finally(() => setIsLoading(false));
+ .finally(() => {
+ setIsLoading(false);
+ dismissMenu?.();
+ });
} else {
+ setTransitionDidStart(true);
startTransition(() => router.push(href));
}
}
@@ -74,7 +96,11 @@ export default function MoreMenuItem({
styleAs="link-without-hover"
className="translate-y-[1px]"
>
- {label}
+ {labelComplex ?? label}
+ {annotation &&
+
+ {annotation}
+ }
);
diff --git a/src/photo/PhotoGridSidebar.tsx b/src/photo/PhotoGridSidebar.tsx
index 21c53dae..2288d94a 100644
--- a/src/photo/PhotoGridSidebar.tsx
+++ b/src/photo/PhotoGridSidebar.tsx
@@ -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 (
diff --git a/src/photo/actions.ts b/src/photo/actions.ts
index b7e71a0f..9b741559 100644
--- a/src/photo/actions.ts
+++ b/src/photo/actions.ts
@@ -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 (
diff --git a/src/photo/form/index.ts b/src/photo/form/index.ts
index 62ed6e0b..515c0787 100644
--- a/src/photo/form/index.ts
+++ b/src/photo/form/index.ts
@@ -197,7 +197,6 @@ export const formHasTextContent = ({
// CREATE FORM DATA: FROM PHOTO
export const convertPhotoToFormData = (photo: Photo): PhotoFormData => {
- console.log('convertPhotoToFormData', photo);
const valueForKey = (key: keyof Photo, value: any) => {
switch (key) {
case 'tags':
diff --git a/src/platforms/github.ts b/src/platforms/github.ts
index 8bb89823..8747080a 100644
--- a/src/platforms/github.ts
+++ b/src/platforms/github.ts
@@ -2,7 +2,6 @@ import {
TEMPLATE_REPO_OWNER,
TEMPLATE_REPO_NAME,
TEMPLATE_REPO_BRANCH,
- IS_DEVELOPMENT,
} from '@/app/config';
const DEFAULT_BRANCH = 'main';
@@ -17,7 +16,7 @@ interface RepoParams {
const fetchGitHub = async (
url: string,
- cacheRequest = IS_DEVELOPMENT,
+ cacheRequest = true,
) => {
const data = await fetch(
url,
@@ -133,8 +132,6 @@ export const getGitHubPublicFork = async (): Promise
=> {
};
export const getGitHubMeta = async (params: RepoParams) => {
- console.log('getGitHubMeta', params);
-
const urlOwner = getGitHubUrlOwner(params);
const urlRepo = getGitHubUrlRepo(params);
const urlBranch = getGitHubUrlBranch(params);
diff --git a/src/state/AppState.ts b/src/state/AppState.ts
index ebd1f451..c0091658 100644
--- a/src/state/AppState.ts
+++ b/src/state/AppState.ts
@@ -20,13 +20,20 @@ export interface AppStateContext {
setIsCommandKOpen?: Dispatch>
shareModalProps?: ShareModalProps
setShareModalProps?: Dispatch>
- // ADMIN
+ // AUTH
userEmail?: string
setUserEmail?: Dispatch>
isUserSignedIn?: boolean
+ isUserSignedInEager?: boolean
+ clearAuthStateAndRedirect?: () => void
+ // ADMIN
adminUpdateTimes?: Date[]
registerAdminUpdate?: () => void
- hiddenPhotosCount?: number
+ refreshAdminData?: () => void
+ photosCount?: number
+ photosCountHidden?: number
+ uploadsCount?: number
+ tagsCount?: number
selectedPhotoIds?: string[]
setSelectedPhotoIds?: Dispatch>
isPerformingSelectEdit?: boolean
diff --git a/src/state/AppStateProvider.tsx b/src/state/AppStateProvider.tsx
index 28dc7455..1124cca4 100644
--- a/src/state/AppStateProvider.tsx
+++ b/src/state/AppStateProvider.tsx
@@ -12,11 +12,17 @@ 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';
+import {
+ storeAuthEmailCookie,
+ clearAuthEmailCookie,
+ hasAuthEmailCookie,
+} from '@/auth/client';
+import { useRouter } from 'next/navigation';
+import { PATH_SIGN_IN } from '@/app/paths';
export default function AppStateProvider({
children,
@@ -25,6 +31,8 @@ export default function AppStateProvider({
}) {
const { previousPathname } = usePathnames();
+ const router = useRouter();
+
// CORE
const [hasLoaded, setHasLoaded] =
useState(false);
@@ -42,10 +50,19 @@ export default function AppStateProvider({
// ADMIN
const [userEmail, setUserEmail] =
useState();
+ const [isUserSignedInEager, setIsUserSignedInEager] =
+ useState(false);
+ // ADMIN
const [adminUpdateTimes, setAdminUpdateTimes] =
useState([]);
- const [hiddenPhotosCount, setHiddenPhotosCount] =
- useState(0);
+ const [photosCount, setPhotosCount] =
+ useState();
+ const [photosCountHidden, setPhotosCountHidden] =
+ useState();
+ const [uploadsCount, setUploadsCount] =
+ useState();
+ const [tagsCount, setTagsCount] =
+ useState();
const [selectedPhotoIds, setSelectedPhotoIds] =
useState();
const [isPerformingSelectEdit, setIsPerformingSelectEdit] =
@@ -70,26 +87,43 @@ 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);
+ setIsUserSignedInEager(hasAuthEmailCookie());
+ if (!authError) {
+ setUserEmail(auth?.user?.email ?? undefined);
}
- }, [data, error]);
+ }, [auth, authError]);
const isUserSignedIn = Boolean(userEmail);
+
+ const {
+ data: adminData,
+ error: adminError,
+ mutate: refreshAdminData,
+ } = useSWR(
+ isUserSignedIn ? 'getAdminData' : null,
+ getAdminDataAction, {
+ refreshInterval: 1000 * 60,
+ },
+ );
+
useEffect(() => {
- if (isUserSignedIn) {
- const timeout = setTimeout(() =>{
- getPhotosHiddenMetaCachedAction()
- .then(({ count }) => setHiddenPhotosCount(count));
- getShouldShowInsightsIndicatorAction()
- .then(setInsightIndicatorStatus);
- }, 100);
- return () => clearTimeout(timeout);
+ if (userEmail) {
+ storeAuthEmailCookie(userEmail);
+ 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, userEmail]);
const registerAdminUpdate = useCallback(() =>
setAdminUpdateTimes(updates => [...updates, new Date()])
@@ -100,6 +134,12 @@ export default function AppStateProvider({
storeTimezoneCookie();
}, []);
+ const clearAuthStateAndRedirect = useCallback((shouldRedirect = true) => {
+ setUserEmail(undefined);
+ clearAuthEmailCookie();
+ if (shouldRedirect) { router.push(PATH_SIGN_IN); }
+ }, [router]);
+
return (
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;
diff --git a/src/utility/cookie.ts b/src/utility/cookie.ts
index 1c344c34..124d2134 100644
--- a/src/utility/cookie.ts
+++ b/src/utility/cookie.ts
@@ -5,19 +5,25 @@ export const storeCookie = (
maxAge = 63158400,
sameSite = 'Lax',
) => {
- document.cookie =
- `${name}=${value};Path=${path};Max-Age=${maxAge};SameSite=${sameSite}`;
+ if (typeof document !== 'undefined') {
+ document.cookie =
+ `${name}=${value};Path=${path};Max-Age=${maxAge};SameSite=${sameSite}`;
+ }
};
export const getCookie = (name: string) => {
- const cookie: Record = {};
- document.cookie.split(';').forEach(function(el) {
- const split = el.split('=');
- cookie[split[0].trim()] = split.slice(1).join('=');
- });
- return cookie[name];
+ if (typeof document !== 'undefined') {
+ const cookie: Record = {};
+ document.cookie.split(';').forEach(function(el) {
+ const split = el.split('=');
+ cookie[split[0].trim()] = split.slice(1).join('=');
+ });
+ return cookie[name];
+ }
};
export const deleteCookie = (name: string) => {
- document.cookie = `${name}=;Max-Age=0`;
+ if (typeof document !== 'undefined') {
+ document.cookie = `${name}=;Max-Age=0`;
+ }
};
diff --git a/tailwind.css b/tailwind.css
index 6aa846fe..f1368499 100644
--- a/tailwind.css
+++ b/tailwind.css
@@ -301,6 +301,7 @@
disabled:bg-gray-100 dark:disabled:bg-gray-900
disabled:border-gray-200 disabled:dark:border-gray-700
border-gray-900 dark:border-gray-100
+ hover:border-gray-900 dark:hover:border-gray-100
active:bg-gray-700 active:border-gray-700
active:dark:bg-gray-300 active:dark:border-gray-300
shadow-none