Add hidden to sidebar and cmd-k menu

This commit is contained in:
Sam Becker 2024-05-12 18:20:12 -05:00
parent 33469a60ee
commit 9c9541977f
9 changed files with 158 additions and 75 deletions

View File

@ -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
/>
<div className="space-y-6">
<Banner animate>
Only authenticated admins can see hidden photos.
Only visible to authenticated admins
</Banner>
<PhotoGrid {...{ photos }} />
</div>
</div>}
/>
);

View File

@ -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)

View File

@ -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,21 +31,39 @@ 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
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
/>
: <PhotoTag
/>;
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"
@ -48,7 +71,9 @@ export default function PhotoGridSidebar({
prefetch={false}
contrast="low"
badged
/>)}
/>;
}
})}
/>}
{cameras.length > 0 && <HeaderList
title="Cameras"

View File

@ -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 }))

View File

@ -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,
]}

View File

@ -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>({});

View File

@ -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}

View File

@ -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)}

View File

@ -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;
}
};