diff --git a/.vscode/settings.json b/.vscode/settings.json index ea2c7fe4..637cea4a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "cSpell.words": [ "ABCDEFGHIJKLMNOP", "Acros", + "affordance", "ARROWLEFT", "ARROWRIGHT", "Astia", diff --git a/package.json b/package.json index 420e9543..059a433c 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@typescript-eslint/eslint-plugin": "^7.0.2", "@typescript-eslint/parser": "^7.0.2", "@vercel/analytics": "^1.2.2", - "@vercel/blob": "^0.22.0", + "@vercel/blob": "^0.22.1", "@vercel/postgres": "0.7.2", "@vercel/speed-insights": "^1.0.10", "autoprefixer": "10.4.17", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ead1f382..891daee6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,8 +48,8 @@ dependencies: specifier: ^1.2.2 version: 1.2.2(next@14.1.1-canary.65)(react@18.2.0) '@vercel/blob': - specifier: ^0.22.0 - version: 0.22.0 + specifier: ^0.22.1 + version: 0.22.1 '@vercel/postgres': specifier: 0.7.2 version: 0.7.2 @@ -3259,13 +3259,14 @@ packages: server-only: 0.0.1 dev: false - /@vercel/blob@0.22.0: - resolution: {integrity: sha512-l0o5bN5ih1H1DG29goULMpCzNIoFI3knFYNFwvGN7iZhK9vltCdlDy77AmrFldRP5af02YczUkjSXWLHMrHStg==} + /@vercel/blob@0.22.1: + resolution: {integrity: sha512-LtHmiYAdJhiSAfBP+5hHXtVyqZUND2G+ild/XVY0SOiB46ab7VUrQctwUMGcVx+yZyXZ2lXPT1HvRJtXFnKvHA==} engines: {node: '>=16.14'} dependencies: async-retry: 1.3.3 bytes: 3.1.2 - undici: 5.28.2 + is-buffer: 2.0.5 + undici: 5.28.3 dev: false /@vercel/postgres@0.7.2: @@ -5184,6 +5185,11 @@ packages: has-tostringtag: 1.0.2 dev: false + /is-buffer@2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + dev: false + /is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -7526,8 +7532,8 @@ packages: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} dev: false - /undici@5.28.2: - resolution: {integrity: sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==} + /undici@5.28.3: + resolution: {integrity: sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==} engines: {node: '>=14.0'} dependencies: '@fastify/busboy': 2.1.0 diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7a968f86..7ea6be9b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -97,11 +97,11 @@ export default function RootLayout({ + + + + - - - - ); diff --git a/src/camera/index.ts b/src/camera/index.ts index 555f73b1..c89b4673 100644 --- a/src/camera/index.ts +++ b/src/camera/index.ts @@ -1,4 +1,4 @@ -import { Photo } from '@/photo'; +import type { Photo } from '@/photo'; import { parameterize } from '@/utility/string'; const CAMERA_PLACEHOLDER: Camera = { make: 'Camera', model: 'Model' }; diff --git a/src/components/CommandKClient.tsx b/src/components/CommandKClient.tsx index 91dcd4ba..3a218901 100644 --- a/src/components/CommandKClient.tsx +++ b/src/components/CommandKClient.tsx @@ -1,7 +1,7 @@ 'use client'; import { Command } from 'cmdk'; -import { ReactNode, useEffect, useState } from 'react'; +import { ReactNode, useEffect, useMemo, useRef, useState } from 'react'; import Modal from './Modal'; import { clsx } from 'clsx/lite'; import { useDebounce } from 'use-debounce'; @@ -10,6 +10,7 @@ import { useRouter } from 'next/navigation'; import { useTheme } from 'next-themes'; import { BiDesktop, BiMoon, BiSun } from 'react-icons/bi'; import { IoInvertModeSharp } from 'react-icons/io5'; +import { useAppState } from '@/state'; const LISTENER_KEYDOWN = 'keydown'; const MINIMUM_QUERY_LENGTH = 2; @@ -30,13 +31,31 @@ export type CommandKSection = { export default function CommandKClient({ onQueryChange, sections = [], + footer, }: { onQueryChange?: (query: string) => Promise sections?: CommandKSection[] + footer?: string }) { - const [isOpen, setIsOpen] = useState(false); - const [queryRaw, setQueryRaw] = useState(''); - const [queryDebounced] = useDebounce(queryRaw, 500, { trailing: true }); + const { + isCommandKOpen: isOpen, + setIsCommandKOpen: setIsOpen, + setShouldRespondToKeyboardCommands, + } = useAppState(); + + const isOpenRef = useRef(isOpen); + + // Raw query values + const [queryLiveRaw, setQueryLive] = useState(''); + const [queryDebouncedRaw] = + useDebounce(queryLiveRaw, 500, { trailing: true }); + const isPlaceholderVisible = queryLiveRaw === ''; + + // Parameterized query values + const queryLive = useMemo(() => + queryLiveRaw.trim().toLocaleLowerCase(), [queryLiveRaw]); + const queryDebounced = useMemo(() => + queryDebouncedRaw.trim().toLocaleLowerCase(), [queryDebouncedRaw]); const [isLoading, setIsLoading] = useState(false); const [queriedSections, setQueriedSections] = useState([]); @@ -45,42 +64,55 @@ export default function CommandKClient({ const router = useRouter(); + useEffect(() => { + isOpenRef.current = isOpen; + }, [isOpen]); + useEffect(() => { const down = (e: KeyboardEvent) => { if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); - setIsOpen((open) => !open); + setIsOpen?.((open) => !open); } }; document.addEventListener(LISTENER_KEYDOWN, down); return () => document.removeEventListener(LISTENER_KEYDOWN, down); - }, []); + }, [setIsOpen]); useEffect(() => { if (queryDebounced.length >= MINIMUM_QUERY_LENGTH) { setIsLoading(true); onQueryChange?.(queryDebounced).then(querySections => { - setQueriedSections(querySections); + if (isOpenRef.current) { + setQueriedSections(querySections); + } else { + // Ignore stale requests that come in after dialog is closed + setQueriedSections([]); + } setIsLoading(false); }); } }, [queryDebounced, onQueryChange]); useEffect(() => { - if (queryRaw === '') { - setQueriedSections([]); - } else if (queryRaw.length >= MINIMUM_QUERY_LENGTH) { - setIsLoading(true); - } - }, [queryRaw]); - - useEffect(() => { - if (!isOpen) { - setQueryRaw(''); + if (queryLive === '') { setQueriedSections([]); setIsLoading(false); + } else if (queryLive.length >= MINIMUM_QUERY_LENGTH) { + setIsLoading(true); } - }, [isOpen]); + }, [queryLive]); + + useEffect(() => { + if (isOpen) { + setShouldRespondToKeyboardCommands?.(false); + } else if (!isOpen) { + setQueryLive(''); + setQueriedSections([]); + setIsLoading(false); + setTimeout(() => setShouldRespondToKeyboardCommands?.(true), 500); + } + }, [isOpen, setShouldRespondToKeyboardCommands]); const sectionTheme: CommandKSection = { heading: 'Theme', @@ -114,32 +146,38 @@ export default function CommandKClient({ > setIsOpen(false)} + onClose={() => setIsOpen?.(false)} fast >
setQueryRaw(e.currentTarget.value)} + onChangeCapture={(e) => setQueryLive(e.currentTarget.value)} className={clsx( - 'w-full', + 'w-full !min-w-0', 'focus:ring-0', + isPlaceholderVisible || isLoading && '!pr-8', '!border-gray-200 dark:!border-gray-800', 'focus:border-gray-200 focus:dark:border-gray-800', 'placeholder:text-gray-400/80', 'placeholder:dark:text-gray-700', )} - style={{ paddingRight: '2rem' }} placeholder="Search photos, views, settings ..." /> {isLoading && - + }
- + - {isLoading ? 'Loading ...' : 'No results found'} + {isLoading ? 'Searching ...' : 'No results found'} {queriedSections .concat(sections) @@ -175,8 +213,8 @@ export default function CommandKClient({ action, }) => { - setIsOpen(false); + setIsOpen?.(false); action?.(); if (path) { router.push(path); @@ -209,6 +247,10 @@ export default function CommandKClient({
)} )} + {footer && !queryLive && +
+ {footer} +
}
diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 646b4d84..ac0f7526 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -8,6 +8,7 @@ import { useRouter } from 'next/navigation'; import AnimateItems from './AnimateItems'; import { PATH_ROOT } from '@/site/paths'; import usePrefersReducedMotion from '@/utility/usePrefersReducedMotion'; +import useMetaThemeColor from '@/site/useMetaThemeColor'; export default function Modal({ onClosePath, @@ -38,6 +39,8 @@ export default function Modal({ } }, []); + useMetaThemeColor({ colorLight: '#333' }); + useClickInsideOutside({ htmlElements, onClickOutside: () => { @@ -70,16 +73,16 @@ export default function Modal({ {children} ]} diff --git a/src/components/Switcher.tsx b/src/components/Switcher.tsx index 74acf151..63be9212 100644 --- a/src/components/Switcher.tsx +++ b/src/components/Switcher.tsx @@ -3,17 +3,20 @@ import { clsx } from 'clsx/lite'; export default function Switcher({ children, + type = 'regular', }: { children: ReactNode + type?: 'regular' | 'borderless' }) { return (
{children}
diff --git a/src/components/SwitcherItem.tsx b/src/components/SwitcherItem.tsx index 4e41be1b..1117e2b4 100644 --- a/src/components/SwitcherItem.tsx +++ b/src/components/SwitcherItem.tsx @@ -20,12 +20,14 @@ export default function SwitcherItem({ classNameProp, 'py-0.5 px-1.5', 'cursor-pointer', - 'hover:bg-gray-50 active:bg-gray-100 active:text-gray-400', - // eslint-disable-next-line max-len - 'dark:hover:bg-gray-950 dark:active:bg-gray-900/75 dark:active:text-gray-600', + 'hover:bg-gray-100/60 active:bg-gray-100', + 'dark:hover:bg-gray-900/75 dark:active:bg-gray-900', active ? 'text-black dark:text-white' - : 'text-gray-300 dark:text-gray-700', + : 'text-gray-400 dark:text-gray-600', + active + ? 'hover:text-black hover:dark:text-white' + : 'hover:text-gray-700 dark:hover:text-gray-400', ); const renderIcon = () => noPadding diff --git a/src/image-response/TagImageResponse.tsx b/src/image-response/TagImageResponse.tsx index bae151e2..d7a7f0e5 100644 --- a/src/image-response/TagImageResponse.tsx +++ b/src/image-response/TagImageResponse.tsx @@ -1,9 +1,9 @@ -import { Photo } from '../photo'; +import type { Photo } from '../photo'; import { FaStar, FaTag } from 'react-icons/fa'; import ImageCaption from './components/ImageCaption'; import ImagePhotoGrid from './components/ImagePhotoGrid'; import ImageContainer from './components/ImageContainer'; -import { NextImageSize } from '@/services/next-image'; +import type { NextImageSize } from '@/services/next-image'; import { isTagFavs } from '@/tag'; export default function TagImageResponse({ diff --git a/src/photo/PhotoDetailPage.tsx b/src/photo/PhotoDetailPage.tsx index 6bde1144..ac4e4c3f 100644 --- a/src/photo/PhotoDetailPage.tsx +++ b/src/photo/PhotoDetailPage.tsx @@ -95,20 +95,29 @@ export default function PhotoDetailPage({ tag={tag} animateOnFirstLoadOnly />} - contentSide={
- -
} + contentSide={ + + , + ]} + />} /> ); diff --git a/src/photo/PhotoEscapeHandler.tsx b/src/photo/PhotoEscapeHandler.tsx index 44aa4579..4ba55918 100644 --- a/src/photo/PhotoEscapeHandler.tsx +++ b/src/photo/PhotoEscapeHandler.tsx @@ -1,6 +1,7 @@ 'use client'; import { getEscapePath } from '@/site/paths'; +import { useAppState } from '@/state'; import { useRouter, usePathname } from 'next/navigation'; import { useEffect } from 'react'; @@ -11,17 +12,21 @@ export default function PhotoEscapeHandler() { const pathname = usePathname(); + const { shouldRespondToKeyboardCommands } = useAppState(); + const escapePath = getEscapePath(pathname); useEffect(() => { - const onKeyUp = (e: KeyboardEvent) => { - if (e.key.toUpperCase() === 'ESCAPE' && escapePath) { - router.push(escapePath, { scroll: false }); + if (shouldRespondToKeyboardCommands) { + const onKeyUp = (e: KeyboardEvent) => { + if (e.key.toUpperCase() === 'ESCAPE' && escapePath) { + router.push(escapePath, { scroll: false }); + }; }; - }; - window.addEventListener(LISTENER_KEYUP, onKeyUp); - return () => window.removeEventListener(LISTENER_KEYUP, onKeyUp); - }, [router, escapePath]); + window.addEventListener(LISTENER_KEYUP, onKeyUp); + return () => window.removeEventListener(LISTENER_KEYUP, onKeyUp); + } + }, [shouldRespondToKeyboardCommands, router, escapePath]); return null; } diff --git a/src/photo/PhotoGrid.tsx b/src/photo/PhotoGrid.tsx index fe83b0bb..c8b3c5a9 100644 --- a/src/photo/PhotoGrid.tsx +++ b/src/photo/PhotoGrid.tsx @@ -59,7 +59,7 @@ export default function PhotoGrid({ 'aspect-square', 'overflow-hidden', '[&>*]:flex [&>*]:w-full [&>*]:h-full', - '[&>*>*]:object-cover', + '[&>*>*]:object-cover [&>*>*]:min-h-full', ) : undefined} style={{ diff --git a/src/photo/PhotoLinks.tsx b/src/photo/PhotoLinks.tsx index 39789e62..ad0a04d4 100644 --- a/src/photo/PhotoLinks.tsx +++ b/src/photo/PhotoLinks.tsx @@ -30,40 +30,46 @@ export default function PhotoLinks({ }) { const router = useRouter(); - const { setNextPhotoAnimation } = useAppState(); + const { + setNextPhotoAnimation, + shouldRespondToKeyboardCommands, + } = useAppState(); const previousPhoto = getPreviousPhoto(photo, photos); const nextPhoto = getNextPhoto(photo, photos); useEffect(() => { - const onKeyUp = (e: KeyboardEvent) => { - switch (e.key.toUpperCase()) { - case 'ARROWLEFT': - case 'J': - if (previousPhoto) { - setNextPhotoAnimation?.(ANIMATION_RIGHT); - router.push( - pathForPhoto(previousPhoto, tag, camera, simulation), - { scroll: false }, - ); - } - break; - case 'ARROWRIGHT': - case 'L': - if (nextPhoto) { - setNextPhotoAnimation?.(ANIMATION_LEFT); - router.push( - pathForPhoto(nextPhoto, tag, camera, simulation), - { scroll: false }, - ); - } - break; + if (shouldRespondToKeyboardCommands) { + const onKeyUp = (e: KeyboardEvent) => { + switch (e.key.toUpperCase()) { + case 'ARROWLEFT': + case 'J': + if (previousPhoto) { + setNextPhotoAnimation?.(ANIMATION_RIGHT); + router.push( + pathForPhoto(previousPhoto, tag, camera, simulation), + { scroll: false }, + ); + } + break; + case 'ARROWRIGHT': + case 'L': + if (nextPhoto) { + setNextPhotoAnimation?.(ANIMATION_LEFT); + router.push( + pathForPhoto(nextPhoto, tag, camera, simulation), + { scroll: false }, + ); + } + break; + }; }; - }; - window.addEventListener(LISTENER_KEYUP, onKeyUp); - return () => window.removeEventListener(LISTENER_KEYUP, onKeyUp); + window.addEventListener(LISTENER_KEYUP, onKeyUp); + return () => window.removeEventListener(LISTENER_KEYUP, onKeyUp); + } }, [ router, + shouldRespondToKeyboardCommands, setNextPhotoAnimation, previousPhoto, nextPhoto, diff --git a/src/photo/form/index.ts b/src/photo/form/index.ts index d2ba6a85..c1404a4a 100644 --- a/src/photo/form/index.ts +++ b/src/photo/form/index.ts @@ -155,7 +155,10 @@ export const convertPhotoToFormData = ( export const convertExifToFormData = ( data: ExifData, filmSimulation?: FilmSimulation, -): Record => ({ +): Omit< + Record, + 'takenAt' | 'takenAtNaive' +> => ({ aspectRatio: getAspectRatioFromExif(data).toString(), make: data.tags?.Make, model: data.tags?.Model, @@ -170,15 +173,14 @@ export const convertExifToFormData = ( longitude: !GEO_PRIVACY_ENABLED ? data.tags?.GPSLongitude?.toString() : undefined, filmSimulation, - takenAt: data.tags?.DateTimeOriginal - ? convertTimestampWithOffsetToPostgresString( - data.tags?.DateTimeOriginal, + ...data.tags?.DateTimeOriginal && { + takenAt: convertTimestampWithOffsetToPostgresString( + data.tags.DateTimeOriginal, getOffsetFromExif(data), - ) - : undefined, - takenAtNaive: data.tags?.DateTimeOriginal - ? convertTimestampToNaivePostgresString(data.tags?.DateTimeOriginal) - : undefined, + ), + takenAtNaive: + convertTimestampToNaivePostgresString(data.tags.DateTimeOriginal), + }, }); // PREPARE FORM FOR DB INSERT diff --git a/src/photo/index.ts b/src/photo/index.ts index d3ad3f08..8f0f135c 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -44,8 +44,8 @@ export interface PhotoExif { latitude?: number longitude?: number filmSimulation?: FilmSimulation - takenAt: string - takenAtNaive: string + takenAt?: string + takenAtNaive?: string } // Raw db insert @@ -59,6 +59,8 @@ export interface PhotoDbInsert extends PhotoExif { locationName?: string priorityOrder?: number hidden?: boolean + takenAt: string + takenAtNaive: string } // Raw db response diff --git a/src/site/CommandK.tsx b/src/site/CommandK.tsx index 6a091f4c..68ef8fe4 100644 --- a/src/site/CommandK.tsx +++ b/src/site/CommandK.tsx @@ -1,5 +1,6 @@ import CommandKClient, { CommandKSection } from '@/components/CommandKClient'; import { + getPhotosCountCached, getUniqueCamerasCached, getUniqueFilmSimulationsCached, getUniqueTagsCached, @@ -9,6 +10,7 @@ import { PATH_ADMIN_PHOTOS, PATH_ADMIN_TAGS, PATH_ADMIN_UPLOADS, + PATH_SIGN_IN, pathForCamera, pathForFilmSimulation, pathForPhoto, @@ -17,24 +19,27 @@ import { import { formatCameraText } from '@/camera'; import { authCached } from '@/auth/cache'; import { getPhotos } from '@/services/vercel-postgres'; -import { titleForPhoto } from '@/photo'; +import { photoQuantityText, titleForPhoto } from '@/photo'; import PhotoTiny from '@/photo/PhotoTiny'; import { formatDate } from '@/utility/date'; import { formatCount, formatCountDescriptive } from '@/utility/string'; -import { BiLockAlt } from 'react-icons/bi'; +import { BiLockAlt, BiSolidUser } from 'react-icons/bi'; import { sortTagsObject } from '@/tag'; import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon'; import { FaTag } from 'react-icons/fa'; import { TbPhoto } from 'react-icons/tb'; import { IoMdCamera } from 'react-icons/io'; import { HiDocumentText } from 'react-icons/hi'; +import { signOutAction } from '@/auth/actions'; export default async function CommandK() { const [ + count, tags, cameras, filmSimulations, ] = await Promise.all([ + getPhotosCountCached().catch(() => 0), getUniqueTagsCached().catch(() => []), getUniqueCamerasCached().catch(() => []), getUniqueFilmSimulationsCached().catch(() => []), @@ -42,7 +47,7 @@ export default async function CommandK() { const session = await authCached().catch(() => null); - const showAdminPages = Boolean(session?.user?.email); + const isAdminLoggedIn = Boolean(session?.user?.email); const SECTION_TAGS: CommandKSection = { heading: 'Tags', @@ -72,7 +77,7 @@ export default async function CommandK() { const SECTION_FILM: CommandKSection = { heading: 'Film Simulations', accessory: - + , items: filmSimulations.map(({ simulation, count }) => ({ label: simulation, @@ -91,23 +96,37 @@ export default async function CommandK() { }, { label: 'Grid', path:'/grid', - }] as CommandKSection['items']).concat(showAdminPages ? [{ - label: 'Admin / Photos', - annotation: , - path: PATH_ADMIN_PHOTOS, - }, { - label: 'Admin / Uploads', - annotation: , - path: PATH_ADMIN_UPLOADS, - }, { - label: 'Admin / Tags', - annotation: , - path: PATH_ADMIN_TAGS, - }, { - label: 'Admin / Config', - annotation: , - path: PATH_ADMIN_CONFIGURATION, - }] : []), + }]), + }; + + const SECTION_ADMIN: CommandKSection = { + heading: 'Admin', + accessory: , + items: isAdminLoggedIn + ? [{ + label: 'Manage Photos', + annotation: , + path: PATH_ADMIN_PHOTOS, + }, { + label: 'Manage Uploads', + annotation: , + path: PATH_ADMIN_UPLOADS, + }, { + label: 'Manage Tags', + annotation: , + path: PATH_ADMIN_TAGS, + }, { + label: 'App Config', + annotation: , + path: PATH_ADMIN_CONFIGURATION, + }, { + label: 'Sign Out', + action: signOutAction, + }] + : [{ + label: 'Sign In', + path: PATH_SIGN_IN, + }], }; return { 'use server'; - const photos = (await getPhotos({ title: query })) + const photos = (await getPhotos({ title: query, limit: 10 })) .filter(({ title }) => Boolean(title)); return photos.length > 0 ? [{ @@ -134,5 +154,6 @@ export default async function CommandK() { }] : []; }} + footer={photoQuantityText(count, false)} />; } diff --git a/src/site/IconSearch.tsx b/src/site/IconSearch.tsx new file mode 100644 index 00000000..102f672f --- /dev/null +++ b/src/site/IconSearch.tsx @@ -0,0 +1,27 @@ +/* eslint-disable max-len */ + +const INTRINSIC_WIDTH = 28; +const INTRINSIC_HEIGHT = 24; + +export default function IconSearch({ + width = INTRINSIC_WIDTH, + includeTitle = true, +}: { + width?: number; + includeTitle?: boolean; +}) { + return ( + + {includeTitle && Search ⌘K} + + + + ); +} diff --git a/src/site/IconSets.tsx b/src/site/IconSets.tsx deleted file mode 100644 index 7d26b32d..00000000 --- a/src/site/IconSets.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* eslint-disable max-len */ - -const INTRINSIC_WIDTH = 28; -const INTRINSIC_HEIGHT = 24; - -export default function IconSets({ - width = INTRINSIC_WIDTH, - includeTitle = true, -}: { - width?: number - includeTitle?: boolean -}) { - return ( - - {includeTitle && Photo Sets} - - - - - - - - ); -}; diff --git a/src/site/NavClient.tsx b/src/site/NavClient.tsx index e15e3590..9ff3f75c 100644 --- a/src/site/NavClient.tsx +++ b/src/site/NavClient.tsx @@ -11,7 +11,6 @@ import { isPathAdmin, isPathGrid, isPathProtected, - isPathSets, isPathSignIn, } from '@/site/paths'; import AnimateItems from '../components/AnimateItems'; @@ -40,8 +39,6 @@ export default function NavClient({ return 'full-frame'; } else if (isPathGrid(pathname)) { return 'grid'; - } else if (isPathSets(pathname)) { - return 'sets'; } else if (isPathProtected(pathname)) { return 'admin'; } @@ -62,7 +59,7 @@ export default function NavClient({ 'w-full min-h-[4rem]', 'leading-none', )}> -
+
- } - href="/" - active={currentSelection === 'full-frame'} - noPadding - /> - } - href={PATH_GRID} - active={currentSelection === 'grid'} - noPadding - /> - } - href={PATH_SETS} - active={currentSelection === 'sets'} - noPadding - /> - {showAdmin && +
+ } - href={PATH_ADMIN_PHOTOS} - active={currentSelection === 'admin'} - />} - + icon={} + href="/" + active={currentSelection === 'full-frame'} + noPadding + /> + } + href={PATH_GRID} + active={currentSelection === 'grid'} + noPadding + /> + {showAdmin && + } + href={PATH_ADMIN_PHOTOS} + active={currentSelection === 'admin'} + />} + + + } + onClick={() => setIsCommandKOpen?.(true)} + /> + +
); } diff --git a/src/site/paths.ts b/src/site/paths.ts index e6b699ce..0d500be6 100644 --- a/src/site/paths.ts +++ b/src/site/paths.ts @@ -10,7 +10,6 @@ import { FilmSimulation } from '@/simulation'; // Core paths export const PATH_ROOT = '/'; export const PATH_GRID = '/grid'; -export const PATH_SETS = '/sets'; export const PATH_ADMIN = '/admin'; export const PATH_API = '/api'; export const PATH_SIGN_IN = '/sign-in'; @@ -55,7 +54,6 @@ export const PATHS_ADMIN = [ export const PATHS_TO_CACHE = [ PATH_ROOT, PATH_GRID, - PATH_SETS, PATH_OG, PATH_PHOTO_DYNAMIC, PATH_TAG_DYNAMIC, @@ -236,9 +234,6 @@ export const checkPathPrefix = (pathname = '', prefix: string) => export const isPathGrid = (pathname?: string) => checkPathPrefix(pathname, PATH_GRID); -export const isPathSets = (pathname?: string) => - checkPathPrefix(pathname, PATH_SETS); - export const isPathSignIn = (pathname?: string) => checkPathPrefix(pathname, PATH_SIGN_IN); diff --git a/src/site/useMetaThemeColor.ts b/src/site/useMetaThemeColor.ts new file mode 100644 index 00000000..c2e4b34a --- /dev/null +++ b/src/site/useMetaThemeColor.ts @@ -0,0 +1,28 @@ +import { useTheme } from 'next-themes'; +import { useEffect } from 'react'; + +export default function useMetaThemeColor({ + colorLight, + colorDark, +}: { + colorLight?: string + colorDark?: string +}) { + const { resolvedTheme } = useTheme(); + + const preferredThemeColor = resolvedTheme === 'light' + ? colorLight + : colorDark; + + useEffect(() => { + if (preferredThemeColor) { + // Temporarily create meta tag for overlays, + // which prevents stale headers on theme changes + const meta = document.createElement('meta'); + meta.name = 'theme-color'; + meta.content = preferredThemeColor; + document.getElementsByTagName('head')[0]?.appendChild(meta); + return () => meta.remove(); + } + }, [preferredThemeColor]); +} diff --git a/src/state/AppState.ts b/src/state/AppState.ts index c8b70912..8c303280 100644 --- a/src/state/AppState.ts +++ b/src/state/AppState.ts @@ -1,12 +1,16 @@ -import { createContext, useContext } from 'react'; +import { Dispatch, SetStateAction, createContext, useContext } from 'react'; import { AnimationConfig } from '@/components/AnimateItems'; export interface AppStateContext { previousPathname?: string hasLoaded?: boolean - setHasLoaded?: (hasLoaded: boolean) => void + setHasLoaded?: Dispatch> nextPhotoAnimation?: AnimationConfig - setNextPhotoAnimation?: (animation?: AnimationConfig) => void + setNextPhotoAnimation?: Dispatch> + shouldRespondToKeyboardCommands?: boolean + setShouldRespondToKeyboardCommands?: Dispatch> + isCommandKOpen?: boolean + setIsCommandKOpen?: Dispatch> clearNextPhotoAnimation?: () => void } diff --git a/src/state/AppStateProvider.tsx b/src/state/AppStateProvider.tsx index 827afe63..56f3f6be 100644 --- a/src/state/AppStateProvider.tsx +++ b/src/state/AppStateProvider.tsx @@ -17,6 +17,11 @@ export default function AppStateProvider({ const [nextPhotoAnimation, setNextPhotoAnimation] = useState(); + const [shouldRespondToKeyboardCommands, setShouldRespondToKeyboardCommands] = + useState(true); + + const [isCommandKOpen, setIsCommandKOpen] = useState(false); + useEffect(() => { setHasLoaded?.(true); }, [setHasLoaded]); @@ -29,6 +34,10 @@ export default function AppStateProvider({ setHasLoaded, nextPhotoAnimation, setNextPhotoAnimation, + shouldRespondToKeyboardCommands, + setShouldRespondToKeyboardCommands, + isCommandKOpen, + setIsCommandKOpen, clearNextPhotoAnimation: () => setNextPhotoAnimation?.(undefined), }} >