Add hidden to sidebar and cmd-k menu
This commit is contained in:
parent
33469a60ee
commit
9c9541977f
@ -50,7 +50,7 @@ export default async function HiddenTagPage() {
|
||||
]);
|
||||
return (
|
||||
<SiteGrid
|
||||
contentMain={<div className="space-y-8 mt-4">
|
||||
contentMain={<div className="space-y-4 mt-4">
|
||||
<AnimateItems
|
||||
type="bottom"
|
||||
items={[<HiddenHeader
|
||||
@ -59,10 +59,12 @@ export default async function HiddenTagPage() {
|
||||
/>]}
|
||||
animateOnFirstLoadOnly
|
||||
/>
|
||||
<Banner animate>
|
||||
Only authenticated admins can see hidden photos.
|
||||
</Banner>
|
||||
<PhotoGrid {...{ photos }} />
|
||||
<div className="space-y-6">
|
||||
<Banner animate>
|
||||
Only visible to authenticated admins
|
||||
</Banner>
|
||||
<PhotoGrid {...{ photos }} />
|
||||
</div>
|
||||
</div>}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
PATH_ADMIN_UPLOADS,
|
||||
PATH_SIGN_IN,
|
||||
pathForPhoto,
|
||||
pathForTag,
|
||||
} from '../site/paths';
|
||||
import Modal from './Modal';
|
||||
import { clsx } from 'clsx/lite';
|
||||
@ -37,6 +38,9 @@ import { getKeywordsForPhoto, titleForPhoto } from '@/photo';
|
||||
import PhotoDate from '@/photo/PhotoDate';
|
||||
import PhotoTiny from '@/photo/PhotoTiny';
|
||||
import { FaCheck } from 'react-icons/fa6';
|
||||
import { TagsWithMeta, addHiddenToTags } from '@/tag';
|
||||
import { FaTag } from 'react-icons/fa';
|
||||
import { formatCount, formatCountDescriptive } from '@/utility/string';
|
||||
|
||||
const LISTENER_KEYDOWN = 'keydown';
|
||||
const MINIMUM_QUERY_LENGTH = 2;
|
||||
@ -44,9 +48,9 @@ const MINIMUM_QUERY_LENGTH = 2;
|
||||
type CommandKItem = {
|
||||
label: string
|
||||
keywords?: string[]
|
||||
accessory?: ReactNode
|
||||
annotation?: ReactNode
|
||||
annotationAria?: string
|
||||
accessory?: ReactNode
|
||||
path?: string
|
||||
action?: () => void | Promise<void>
|
||||
}
|
||||
@ -58,10 +62,12 @@ export type CommandKSection = {
|
||||
}
|
||||
|
||||
export default function CommandKClient({
|
||||
tags,
|
||||
serverSections = [],
|
||||
showDebugTools,
|
||||
footer,
|
||||
}: {
|
||||
tags: TagsWithMeta
|
||||
serverSections?: CommandKSection[]
|
||||
showDebugTools?: boolean
|
||||
footer?: string
|
||||
@ -70,6 +76,7 @@ export default function CommandKClient({
|
||||
isUserSignedIn,
|
||||
setUserEmail,
|
||||
isCommandKOpen: isOpen,
|
||||
hiddenPhotosCount,
|
||||
arePhotosMatted,
|
||||
shouldShowBaselineGrid,
|
||||
shouldDebugBlur,
|
||||
@ -173,6 +180,24 @@ export default function CommandKClient({
|
||||
}
|
||||
}, [isOpen, setShouldRespondToKeyboardCommands]);
|
||||
|
||||
const tagsIncludingHidden = useMemo(() =>
|
||||
addHiddenToTags(tags, hiddenPhotosCount)
|
||||
, [tags, hiddenPhotosCount]);
|
||||
|
||||
const SECTION_TAGS: CommandKSection = {
|
||||
heading: 'Tags',
|
||||
accessory: <FaTag
|
||||
size={10}
|
||||
className="translate-x-[1px] translate-y-[0.75px]"
|
||||
/>,
|
||||
items: tagsIncludingHidden.map(({ tag, count }) => ({
|
||||
label: tag,
|
||||
annotation: formatCount(count),
|
||||
annotationAria: formatCountDescriptive(count),
|
||||
path: pathForTag(tag),
|
||||
})),
|
||||
};
|
||||
|
||||
const clientSections: CommandKSection[] = [{
|
||||
heading: 'Theme',
|
||||
accessory: <IoInvertModeSharp
|
||||
@ -316,6 +341,7 @@ export default function CommandKClient({
|
||||
{isLoading ? 'Searching ...' : 'No results found'}
|
||||
</Command.Empty>
|
||||
{queriedSections
|
||||
.concat(SECTION_TAGS)
|
||||
.concat(serverSections)
|
||||
.concat(sectionPages)
|
||||
.concat(adminSection)
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { Cameras, sortCamerasWithCount } from '@/camera';
|
||||
import PhotoCamera from '@/camera/PhotoCamera';
|
||||
import HeaderList from '@/components/HeaderList';
|
||||
@ -5,11 +7,14 @@ import PhotoTag from '@/tag/PhotoTag';
|
||||
import { FaTag } from 'react-icons/fa';
|
||||
import { IoMdCamera } from 'react-icons/io';
|
||||
import { PhotoDateRange, dateRangeForPhotos, photoQuantityText } from '.';
|
||||
import { TAG_FAVS, TagsWithMeta } from '@/tag';
|
||||
import { TAG_FAVS, TAG_HIDDEN, TagsWithMeta, addHiddenToTags } from '@/tag';
|
||||
import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
|
||||
import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon';
|
||||
import { FilmSimulations, sortFilmSimulationsWithCount } from '@/simulation';
|
||||
import FavsTag from '../tag/FavsTag';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import { useMemo } from 'react';
|
||||
import HiddenTag from '@/tag/HiddenTag';
|
||||
|
||||
export default function PhotoGridSidebar({
|
||||
tags,
|
||||
@ -26,29 +31,49 @@ export default function PhotoGridSidebar({
|
||||
}) {
|
||||
const { start, end } = dateRangeForPhotos(undefined, photosDateRange);
|
||||
|
||||
const { hiddenPhotosCount } = useAppState();
|
||||
|
||||
const tagsIncludingHidden = useMemo(() =>
|
||||
addHiddenToTags(tags, hiddenPhotosCount)
|
||||
, [tags, hiddenPhotosCount]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{tags.length > 0 && <HeaderList
|
||||
title='Tags'
|
||||
icon={<FaTag size={12} className="text-icon" />}
|
||||
items={tags.map(({ tag, count }) => tag === TAG_FAVS
|
||||
? <FavsTag
|
||||
key={TAG_FAVS}
|
||||
countOnHover={count}
|
||||
type="icon-last"
|
||||
prefetch={false}
|
||||
contrast="low"
|
||||
badged
|
||||
/>
|
||||
: <PhotoTag
|
||||
key={tag}
|
||||
tag={tag}
|
||||
type="text-only"
|
||||
countOnHover={count}
|
||||
prefetch={false}
|
||||
contrast="low"
|
||||
badged
|
||||
/>)}
|
||||
items={tagsIncludingHidden.map(({ tag, count }) => {
|
||||
switch (tag) {
|
||||
case TAG_FAVS:
|
||||
return <FavsTag
|
||||
key={TAG_FAVS}
|
||||
countOnHover={count}
|
||||
type="icon-last"
|
||||
prefetch={false}
|
||||
contrast="low"
|
||||
badged
|
||||
/>;
|
||||
case TAG_HIDDEN:
|
||||
return <HiddenTag
|
||||
key={TAG_HIDDEN}
|
||||
countOnHover={count}
|
||||
type="icon-last"
|
||||
prefetch={false}
|
||||
contrast="low"
|
||||
badged
|
||||
/>;
|
||||
default:
|
||||
return <PhotoTag
|
||||
key={tag}
|
||||
tag={tag}
|
||||
type="text-only"
|
||||
countOnHover={count}
|
||||
prefetch={false}
|
||||
contrast="low"
|
||||
badged
|
||||
/>;
|
||||
}
|
||||
})}
|
||||
/>}
|
||||
{cameras.length > 0 && <HeaderList
|
||||
title="Cameras"
|
||||
|
||||
@ -22,6 +22,7 @@ import {
|
||||
} from '@/services/storage';
|
||||
import {
|
||||
getPhotosCachedCached,
|
||||
getPhotosTagHiddenMetaCached,
|
||||
revalidateAdminPaths,
|
||||
revalidateAllKeysAndPaths,
|
||||
revalidatePhoto,
|
||||
@ -197,21 +198,30 @@ export const streamAiImageQueryAction = async (
|
||||
export const getImageBlurAction = async (url: string) =>
|
||||
safelyRunAdminServerAction(() => blurImageFromUrl(url));
|
||||
|
||||
// Public actions
|
||||
export const getPhotosTagHiddenMetaCachedAction = async () =>
|
||||
safelyRunAdminServerAction(getPhotosTagHiddenMetaCached);
|
||||
|
||||
// Public/Private actions
|
||||
|
||||
export const getPhotosAction = async (
|
||||
offset: number,
|
||||
limit: number,
|
||||
hidden?: GetPhotosOptions['hidden'],
|
||||
) =>
|
||||
getPhotos({ offset, hidden, limit });
|
||||
) => (hidden === 'include' || hidden === 'only')
|
||||
? safelyRunAdminServerAction(() =>
|
||||
getPhotos({ offset, hidden, limit }))
|
||||
: getPhotos({ offset, hidden, limit });
|
||||
|
||||
export const getPhotosCachedAction = async (
|
||||
offset: number,
|
||||
limit: number,
|
||||
hidden?: GetPhotosOptions['hidden'],
|
||||
) =>
|
||||
getPhotosCachedCached({ offset, hidden, limit });
|
||||
) => (hidden === 'include' || hidden === 'only')
|
||||
? safelyRunAdminServerAction(() =>
|
||||
getPhotosCachedCached({ offset, hidden, limit }))
|
||||
: getPhotosCachedCached({ offset, hidden, limit });
|
||||
|
||||
// Public actions
|
||||
|
||||
export const queryPhotosByTitleAction = async (query: string) =>
|
||||
(await getPhotos({ query, limit: 10 }))
|
||||
|
||||
@ -8,14 +8,12 @@ import {
|
||||
import {
|
||||
pathForCamera,
|
||||
pathForFilmSimulation,
|
||||
pathForTag,
|
||||
} from './paths';
|
||||
import { formatCameraText } from '@/camera';
|
||||
import { photoQuantityText } from '@/photo';
|
||||
import { formatCount, formatCountDescriptive } from '@/utility/string';
|
||||
import { sortTagsObject } from '@/tag';
|
||||
import { TagsWithMeta } from '@/tag';
|
||||
import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon';
|
||||
import { FaTag } from 'react-icons/fa';
|
||||
import { IoMdCamera } from 'react-icons/io';
|
||||
import { ADMIN_DEBUG_TOOLS_ENABLED } from './config';
|
||||
|
||||
@ -27,25 +25,11 @@ export default async function CommandK() {
|
||||
filmSimulations,
|
||||
] = await Promise.all([
|
||||
getPhotosCountCached().catch(() => 0),
|
||||
getUniqueTagsCached().catch(() => []),
|
||||
getUniqueTagsCached().catch(() => [] as TagsWithMeta),
|
||||
getUniqueCamerasCached().catch(() => []),
|
||||
getUniqueFilmSimulationsCached().catch(() => []),
|
||||
]);
|
||||
|
||||
const SECTION_TAGS: CommandKSection = {
|
||||
heading: 'Tags',
|
||||
accessory: <FaTag
|
||||
size={10}
|
||||
className="translate-x-[1px] translate-y-[0.75px]"
|
||||
/>,
|
||||
items: sortTagsObject(tags).map(({ tag, count }) => ({
|
||||
label: tag,
|
||||
annotation: formatCount(count),
|
||||
annotationAria: formatCountDescriptive(count),
|
||||
path: pathForTag(tag),
|
||||
})),
|
||||
};
|
||||
|
||||
const SECTION_CAMERAS: CommandKSection = {
|
||||
heading: 'Cameras',
|
||||
accessory: <IoMdCamera />,
|
||||
@ -71,8 +55,8 @@ export default async function CommandK() {
|
||||
};
|
||||
|
||||
return <CommandKClient
|
||||
tags={tags}
|
||||
serverSections={[
|
||||
SECTION_TAGS,
|
||||
SECTION_CAMERAS,
|
||||
SECTION_FILM,
|
||||
]}
|
||||
|
||||
@ -2,29 +2,33 @@ import { Dispatch, SetStateAction, createContext, useContext } from 'react';
|
||||
import { AnimationConfig } from '@/components/AnimateItems';
|
||||
|
||||
export interface AppStateContext {
|
||||
// CORE
|
||||
previousPathname?: string
|
||||
hasLoaded?: boolean
|
||||
arePhotosMatted?: boolean
|
||||
setArePhotosMatted?: Dispatch<SetStateAction<boolean>>
|
||||
setHasLoaded?: Dispatch<SetStateAction<boolean>>
|
||||
swrTimestamp?: number
|
||||
invalidateSwr?: () => void
|
||||
userEmail?: string
|
||||
setUserEmail?: Dispatch<SetStateAction<string | undefined>>
|
||||
isUserSignedIn?: boolean
|
||||
setHasLoaded?: Dispatch<SetStateAction<boolean>>
|
||||
nextPhotoAnimation?: AnimationConfig
|
||||
setNextPhotoAnimation?: Dispatch<SetStateAction<AnimationConfig | undefined>>
|
||||
clearNextPhotoAnimation?: () => void
|
||||
shouldRespondToKeyboardCommands?: boolean
|
||||
setShouldRespondToKeyboardCommands?: Dispatch<SetStateAction<boolean>>
|
||||
isCommandKOpen?: boolean
|
||||
setIsCommandKOpen?: Dispatch<SetStateAction<boolean>>
|
||||
// ADMIN
|
||||
userEmail?: string
|
||||
setUserEmail?: Dispatch<SetStateAction<string | undefined>>
|
||||
isUserSignedIn?: boolean
|
||||
adminUpdateTimes?: Date[]
|
||||
registerAdminUpdate?: () => void
|
||||
shouldShowBaselineGrid?: boolean
|
||||
setShouldShowBaselineGrid?: Dispatch<SetStateAction<boolean>>
|
||||
hiddenPhotosCount?: number
|
||||
// DEBUG
|
||||
arePhotosMatted?: boolean
|
||||
setArePhotosMatted?: Dispatch<SetStateAction<boolean>>
|
||||
shouldDebugBlur?: boolean
|
||||
setShouldDebugBlur?: Dispatch<SetStateAction<boolean>>
|
||||
clearNextPhotoAnimation?: () => void
|
||||
shouldShowBaselineGrid?: boolean
|
||||
setShouldShowBaselineGrid?: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
export const AppStateContext = createContext<AppStateContext>({});
|
||||
|
||||
@ -7,6 +7,7 @@ import usePathnames from '@/utility/usePathnames';
|
||||
import { getAuthAction, logClientAuthUpdate } from '@/auth/actions';
|
||||
import useSWR from 'swr';
|
||||
import { MATTE_PHOTOS } from '@/site/config';
|
||||
import { getPhotosTagHiddenMetaCachedAction } from '@/photo/actions';
|
||||
|
||||
export default function AppStateProvider({
|
||||
children,
|
||||
@ -15,25 +16,31 @@ export default function AppStateProvider({
|
||||
}) {
|
||||
const { previousPathname } = usePathnames();
|
||||
|
||||
// CORE
|
||||
const [hasLoaded, setHasLoaded] =
|
||||
useState(false);
|
||||
const [arePhotosMatted, setArePhotosMatted] =
|
||||
useState(MATTE_PHOTOS);
|
||||
const [swrTimestamp, setSwrTimestamp] =
|
||||
useState(Date.now());
|
||||
const [userEmail, setUserEmail] =
|
||||
useState<string>();
|
||||
const [nextPhotoAnimation, setNextPhotoAnimation] =
|
||||
useState<AnimationConfig>();
|
||||
const [shouldRespondToKeyboardCommands, setShouldRespondToKeyboardCommands] =
|
||||
useState(true);
|
||||
const [isCommandKOpen, setIsCommandKOpen] =
|
||||
useState(false);
|
||||
const [adminUpdateTimes, setAdminUpdateTimes] = useState<Date[]>([]);
|
||||
const [shouldShowBaselineGrid, setShouldShowBaselineGrid] =
|
||||
useState(false);
|
||||
// ADMIN
|
||||
const [userEmail, setUserEmail] =
|
||||
useState<string>();
|
||||
const [adminUpdateTimes, setAdminUpdateTimes] =
|
||||
useState<Date[]>([]);
|
||||
const [hiddenPhotosCount, setHiddenPhotosCount] =
|
||||
useState(0);
|
||||
// DEBUG
|
||||
const [arePhotosMatted, setArePhotosMatted] =
|
||||
useState(MATTE_PHOTOS);
|
||||
const [shouldDebugBlur, setShouldDebugBlur] =
|
||||
useState(false);
|
||||
const [shouldShowBaselineGrid, setShouldShowBaselineGrid] =
|
||||
useState(false);
|
||||
|
||||
const invalidateSwr = useCallback(() => setSwrTimestamp(Date.now()), []);
|
||||
|
||||
@ -42,6 +49,13 @@ export default function AppStateProvider({
|
||||
setUserEmail(data?.user?.email ?? undefined);
|
||||
logClientAuthUpdate(data);
|
||||
}, [data]);
|
||||
const isUserSignedIn = userEmail !== undefined;
|
||||
useEffect(() => {
|
||||
if (isUserSignedIn) {
|
||||
getPhotosTagHiddenMetaCachedAction().then(({ count }) =>
|
||||
setHiddenPhotosCount(count));
|
||||
}
|
||||
}, [isUserSignedIn]);
|
||||
|
||||
const registerAdminUpdate = useCallback(() =>
|
||||
setAdminUpdateTimes(updates => [...updates, new Date()])
|
||||
@ -54,29 +68,33 @@ export default function AppStateProvider({
|
||||
return (
|
||||
<AppStateContext.Provider
|
||||
value={{
|
||||
// CORE
|
||||
previousPathname,
|
||||
hasLoaded,
|
||||
arePhotosMatted,
|
||||
setArePhotosMatted,
|
||||
setHasLoaded,
|
||||
swrTimestamp,
|
||||
invalidateSwr,
|
||||
setHasLoaded,
|
||||
isUserSignedIn: userEmail !== undefined,
|
||||
userEmail,
|
||||
setUserEmail,
|
||||
nextPhotoAnimation,
|
||||
setNextPhotoAnimation,
|
||||
clearNextPhotoAnimation: () => setNextPhotoAnimation?.(undefined),
|
||||
shouldRespondToKeyboardCommands,
|
||||
setShouldRespondToKeyboardCommands,
|
||||
isCommandKOpen,
|
||||
setIsCommandKOpen,
|
||||
// ADMIN
|
||||
userEmail,
|
||||
setUserEmail,
|
||||
isUserSignedIn,
|
||||
adminUpdateTimes,
|
||||
registerAdminUpdate,
|
||||
shouldShowBaselineGrid,
|
||||
shouldDebugBlur,
|
||||
hiddenPhotosCount,
|
||||
// DEBUG
|
||||
arePhotosMatted,
|
||||
setArePhotosMatted,
|
||||
setShouldDebugBlur,
|
||||
setShouldShowBaselineGrid,
|
||||
clearNextPhotoAnimation: () => setNextPhotoAnimation?.(undefined),
|
||||
shouldShowBaselineGrid,
|
||||
shouldDebugBlur,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -19,7 +19,10 @@ export default function HiddenTag({
|
||||
label={badged
|
||||
? <span className="inline-flex gap-1">
|
||||
{TAG_HIDDEN}
|
||||
<AiOutlineEyeInvisible size={14} />
|
||||
<AiOutlineEyeInvisible
|
||||
size={13}
|
||||
className="translate-y-[-1.5px]"
|
||||
/>
|
||||
</span>
|
||||
: TAG_HIDDEN}
|
||||
href={pathForTag(TAG_HIDDEN)}
|
||||
|
||||
@ -91,3 +91,14 @@ export const isPhotoFav = ({ tags }: Photo) => tags.some(isTagFavs);
|
||||
|
||||
export const isPathFavs = (pathname?: string) =>
|
||||
getPathComponents(pathname).tag === TAG_FAVS;
|
||||
|
||||
export const addHiddenToTags = (tags: TagsWithMeta, hiddenPhotosCount = 0) => {
|
||||
if (hiddenPhotosCount > 0) {
|
||||
return tags
|
||||
.filter(({ tag }) => tag === TAG_FAVS)
|
||||
.concat({ tag: TAG_HIDDEN, count: hiddenPhotosCount })
|
||||
.concat(tags.filter(({ tag }) => tag !== TAG_FAVS));
|
||||
} else {
|
||||
return tags;
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user