From 02cfa4ee524b9cc20324aced644f223e018ba168 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Wed, 21 Feb 2024 12:33:31 -0600 Subject: [PATCH] Create cmd-k affordance in nav --- .vscode/settings.json | 1 + src/app/layout.tsx | 6 ++-- src/components/CommandKClient.tsx | 59 ++++++++++++++++++++---------- src/components/Modal.tsx | 8 ++--- src/site/IconSearch.tsx | 27 ++++++++++++++ src/site/IconSets.tsx | 31 ---------------- src/site/NavClient.tsx | 3 -- src/site/ViewSwitcher.tsx | 60 ++++++++++++++++--------------- src/site/paths.ts | 5 --- src/state/AppStateProvider.tsx | 6 +++- src/state/index.ts | 6 ++-- 11 files changed, 117 insertions(+), 95 deletions(-) create mode 100644 src/site/IconSearch.tsx delete mode 100644 src/site/IconSets.tsx 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/src/app/layout.tsx b/src/app/layout.tsx index fc091c62..95f0a523 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,7 +4,7 @@ import { clsx } from 'clsx/lite'; import { IBM_Plex_Mono } from 'next/font/google'; import { Metadata } from 'next'; import { BASE_URL, SITE_DESCRIPTION, SITE_TITLE } from '@/site/config'; -import StateProvider from '@/state/AppStateProvider'; +import AppStateProvider from '@/state/AppStateProvider'; import ThemeProviderClient from '@/site/ThemeProviderClient'; import Nav from '@/site/Nav'; import ToasterWithThemes from '@/toast/ToasterWithThemes'; @@ -73,7 +73,7 @@ export default function RootLayout({ suppressHydrationWarning > - +
- + diff --git a/src/components/CommandKClient.tsx b/src/components/CommandKClient.tsx index 450e89dd..ba53b62f 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, useState } from 'react'; import Modal from './Modal'; import { clsx } from 'clsx/lite'; import { useDebounce } from 'use-debounce'; @@ -10,6 +10,8 @@ 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'; +import { parameterize } from '@/utility/string'; const LISTENER_KEYDOWN = 'keydown'; const MINIMUM_QUERY_LENGTH = 2; @@ -34,9 +36,22 @@ export default function CommandKClient({ onQueryChange?: (query: string) => Promise sections?: CommandKSection[] }) { - const [isOpen, setIsOpen] = useState(false); - const [queryRaw, setQueryRaw] = useState(''); - const [queryDebounced] = useDebounce(queryRaw, 500, { trailing: true }); + const { + isCommandKOpen: isOpen, + setIsCommandKOpen: setIsOpen, + } = useAppState(); + + // Raw query values + const [queryLiveRaw, setQueryLive] = useState(''); + const [queryDebouncedRaw] = + useDebounce(queryLiveRaw, 500, { trailing: true }); + const isPlaceholderVisible = queryLiveRaw === ''; + + // Parameterized query values + const queryLive = useMemo(() => + parameterize(queryLiveRaw), [queryLiveRaw]); + const queryDebounced = useMemo(() => + parameterize(queryDebouncedRaw), [queryDebouncedRaw]); const [isLoading, setIsLoading] = useState(false); const [queriedSections, setQueriedSections] = useState([]); @@ -49,12 +64,12 @@ export default function CommandKClient({ 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) { @@ -67,16 +82,17 @@ export default function CommandKClient({ }, [queryDebounced, onQueryChange]); useEffect(() => { - if (queryRaw === '') { + if (queryLive === '') { setQueriedSections([]); - } else if (queryRaw.length >= MINIMUM_QUERY_LENGTH) { + setIsLoading(false); + } else if (queryLive.length >= MINIMUM_QUERY_LENGTH) { setIsLoading(true); } - }, [queryRaw]); + }, [queryLive]); useEffect(() => { if (!isOpen) { - setQueryRaw(''); + setQueryLive(''); setQueriedSections([]); setIsLoading(false); } @@ -114,30 +130,37 @@ 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 !max-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 ? 'Searching ...' : 'No results found'} @@ -185,7 +208,7 @@ export default function CommandKClient({ 'data-[selected=true]:dark:bg-gray-900/75', )} onSelect={() => { - setIsOpen(false); + setIsOpen?.(false); action?.(); if (path) { router.push(path); diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 646b4d84..12ea5efa 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -57,7 +57,7 @@ export default function Modal({ className={clsx( 'fixed inset-0 z-50 flex justify-center', anchor === 'top' - ? 'items-start pt-4 sm:pt-24' + ? 'items-start pt-4 xs:pt-12 sm:pt-24' : 'items-center', 'bg-black', )} @@ -70,16 +70,16 @@ export default function Modal({ {children}
]} diff --git a/src/site/IconSearch.tsx b/src/site/IconSearch.tsx new file mode 100644 index 00000000..d7ae0ff9 --- /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} + + + + ); +} 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..7bc8b208 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'; } diff --git a/src/site/ViewSwitcher.tsx b/src/site/ViewSwitcher.tsx index 7f7fa149..cb91b9c2 100644 --- a/src/site/ViewSwitcher.tsx +++ b/src/site/ViewSwitcher.tsx @@ -2,9 +2,10 @@ import Switcher from '@/components/Switcher'; import SwitcherItem from '@/components/SwitcherItem'; import IconFullFrame from '@/site/IconFullFrame'; import IconGrid from '@/site/IconGrid'; -import { PATH_ADMIN_PHOTOS, PATH_GRID, PATH_SETS } from '@/site/paths'; +import { PATH_ADMIN_PHOTOS, PATH_GRID } from '@/site/paths'; import { BiLockAlt } from 'react-icons/bi'; -import IconSets from './IconSets'; +import { useAppState } from '@/state'; +import IconSearch from './IconSearch'; export type SwitcherSelection = 'full-frame' | 'grid' | 'sets' | 'admin'; @@ -15,33 +16,36 @@ export default function ViewSwitcher({ currentSelection?: SwitcherSelection showAdmin?: boolean }) { + const { setIsCommandKOpen } = useAppState(); + return ( - - } - 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/state/AppStateProvider.tsx b/src/state/AppStateProvider.tsx index 082df160..7001d824 100644 --- a/src/state/AppStateProvider.tsx +++ b/src/state/AppStateProvider.tsx @@ -5,7 +5,7 @@ import { AppStateContext } from '.'; import { AnimationConfig } from '@/components/AnimateItems'; import usePathnames from '@/utility/usePathnames'; -export default function StateProvider({ +export default function AppStateProvider({ children, }: { children: ReactNode @@ -17,6 +17,8 @@ export default function StateProvider({ const [nextPhotoAnimation, setNextPhotoAnimation] = useState(); + const [isCommandKOpen, setIsCommandKOpen] = useState(false); + useEffect(() => { setHasLoaded?.(true); }, [setHasLoaded]); @@ -28,6 +30,8 @@ export default function StateProvider({ hasLoaded, setHasLoaded, nextPhotoAnimation, + isCommandKOpen, + setIsCommandKOpen, setNextPhotoAnimation, clearNextPhotoAnimation: () => setNextPhotoAnimation?.(undefined), }} diff --git a/src/state/index.ts b/src/state/index.ts index c8b70912..b91f43af 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -1,11 +1,13 @@ -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 + isCommandKOpen?: boolean + setIsCommandKOpen?: Dispatch> setNextPhotoAnimation?: (animation?: AnimationConfig) => void clearNextPhotoAnimation?: () => void }