From c78c6fd7e0b9ef853c6382cdaf1f0612f745ebe4 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Thu, 17 Jul 2025 20:32:09 -0500 Subject: [PATCH] Add sort options to cmd-k menu --- src/app/AppViewSwitcher.tsx | 18 +++---- src/cmdk/CommandKClient.tsx | 95 +++++++++++++++++++++++++++++++------ src/i18n/locales/bd-bn.ts | 16 ++++++- src/i18n/locales/en-us.ts | 16 ++++++- src/i18n/locales/id-id.ts | 16 ++++++- src/i18n/locales/pt-br.ts | 16 ++++++- src/i18n/locales/pt-pt.ts | 16 ++++++- src/i18n/locales/zh-cn.ts | 16 ++++++- src/photo/sort/SortMenu.tsx | 15 +++--- src/photo/sort/path.ts | 16 +++++-- 10 files changed, 196 insertions(+), 44 deletions(-) diff --git a/src/app/AppViewSwitcher.tsx b/src/app/AppViewSwitcher.tsx index 1efc52c8..a919e3b7 100644 --- a/src/app/AppViewSwitcher.tsx +++ b/src/app/AppViewSwitcher.tsx @@ -3,7 +3,6 @@ import SwitcherItem from '@/components/switcher/SwitcherItem'; import IconFull from '@/components/icons/IconFull'; import IconGrid from '@/components/icons/IconGrid'; import { - doesPathOfferSort, PATH_FULL_INFERRED, PATH_GRID_INFERRED, } from '@/app/path'; @@ -23,7 +22,7 @@ import { usePathname } from 'next/navigation'; import { KEY_COMMANDS } from '@/photo/key-commands'; import { useAppText } from '@/i18n/state/client'; import IconSort from '@/components/icons/IconSort'; -import { getSortConfigFromPath } from '@/photo/sort/path'; +import { getSortStateFromPath } from '@/photo/sort/path'; import { motion } from 'framer-motion'; import SortMenu from '@/photo/sort/SortMenu'; import { SWR_KEYS } from '@/swr'; @@ -53,10 +52,11 @@ export default function AppViewSwitcher({ invalidateSwr, } = useAppState(); - const sortConfig = useMemo(() => getSortConfigFromPath(pathname), [pathname]); + const sortConfig = useMemo(() => getSortStateFromPath(pathname), [pathname]); const { sortBy, + doesPathOfferSort, isSortedByDefault, isAscending, pathGrid, @@ -66,7 +66,7 @@ export default function AppViewSwitcher({ const showSortControl = NAV_SORT_CONTROL !== 'none' && - doesPathOfferSort(pathname); + doesPathOfferSort; const hasLoadedRef = useRef(false); useEffect(() => { @@ -194,7 +194,7 @@ export default function AppViewSwitcher({ />} tooltip={{ ...!isSortMenuOpen && SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && { - content: 'Sort', + content: appText.sort.sort, }, }} width="narrow" @@ -210,11 +210,11 @@ export default function AppViewSwitcher({ sort={isAscending ? 'asc' : 'desc'} className="translate-x-[0.5px] translate-y-[1px]" />} - tooltip={{ + tooltip={{...SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && { content: isAscending - ? appText.sort.newest - : appText.sort.oldest, - }} + ? appText.sort.viewNewest + : appText.sort.viewOldest, + }}} width="narrow" noPadding />} diff --git a/src/cmdk/CommandKClient.tsx b/src/cmdk/CommandKClient.tsx index 4a3a6ea7..7d39f86b 100644 --- a/src/cmdk/CommandKClient.tsx +++ b/src/cmdk/CommandKClient.tsx @@ -40,7 +40,7 @@ import Spinner from '../components/Spinner'; import { usePathname, useRouter } from 'next/navigation'; import { useTheme } from 'next-themes'; import { BiDesktop, BiLockAlt, BiMoon, BiSun } from 'react-icons/bi'; -import { IoInvertModeSharp } from 'react-icons/io5'; +import { IoClose, IoInvertModeSharp } from 'react-icons/io5'; import { useAppState } from '@/app/AppState'; import { searchPhotosAction } from '@/photo/actions'; import { RiToolsFill } from 'react-icons/ri'; @@ -86,10 +86,12 @@ import IconFavs from '@/components/icons/IconFavs'; import { useAppText } from '@/i18n/state/client'; import LoaderButton from '@/components/primitives/LoaderButton'; import IconRecents from '@/components/icons/IconRecents'; -import { CgFileDocument } from 'react-icons/cg'; +import { CgClose, CgFileDocument } from 'react-icons/cg'; import { FaRegUserCircle } from 'react-icons/fa'; import { formatDistanceToNow } from 'date-fns'; import IconCheck from '@/components/icons/IconCheck'; +import { getSortStateFromPath } from '@/photo/sort/path'; +import IconSort from '@/components/icons/IconSort'; const DIALOG_TITLE = 'Global Command-K Menu'; const DIALOG_DESCRIPTION = 'For searching photos, views, and settings'; @@ -116,6 +118,11 @@ type CommandKSection = { items: CommandKItem[] } +const renderCheck = (isChecked?: boolean) => + isChecked + ? + : undefined; + const renderToggle = ( label: string, onToggle?: Dispatch>, @@ -123,7 +130,7 @@ const renderToggle = ( ): CommandKItem => ({ label: `Toggle ${label}`, action: () => onToggle?.(prev => !prev), - annotation: isEnabled ? : undefined, + annotation: renderCheck(isEnabled), }); export default function CommandKClient({ @@ -172,6 +179,19 @@ export default function CommandKClient({ setShouldDebugRecipeOverlays, } = useAppState(); + const { + doesPathOfferSort, + isSortedByDefault, + pathNewest, + pathOldest, + pathTakenAt, + pathUploadedAt, + pathClearSort, + isAscending, + isTakenAt, + isUploadedAt, + } = useMemo(() => getSortStateFromPath(pathname), [pathname]); + const appText = useAppText(); const isOpenRef = useRef(isOpen); @@ -209,7 +229,7 @@ export default function CommandKClient({ }, [isWaiting, setIsOpen]); // Raw query values - const [queryLiveRaw, setQueryLive] = useState(''); + const [queryLiveRaw, setQueryLiveRaw] = useState(''); const [queryDebouncedRaw] = useDebounce(queryLiveRaw, 500, { trailing: true }); @@ -289,7 +309,7 @@ export default function CommandKClient({ useEffect(() => { if (!isOpen) { - setQueryLive(''); + setQueryLiveRaw(''); setQueriedSections([]); setIsLoading(false); } @@ -305,6 +325,7 @@ export default function CommandKClient({ return count ? { count, subhead } : undefined; }, [recent, appText]); + // Years only accessible by search const years = useMemo(() => _years.filter(({ year }) => queryLive && year.includes(queryLive)) , [_years, queryLive]); @@ -503,6 +524,40 @@ export default function CommandKClient({ }); } + const sortItems = [{ + label: appText.sort.newestFirst, + path: pathNewest, + annotation: renderCheck(!isAscending), + }, { + label: appText.sort.oldestFirst, + path: pathOldest, + annotation: renderCheck(isAscending), + }, { + label: appText.sort.byTakenAt, + path: pathTakenAt, + annotation: renderCheck(isTakenAt), + }, { + label: appText.sort.byUploadedAt, + path: pathUploadedAt, + annotation: renderCheck(isUploadedAt), + }]; + + if (!isSortedByDefault) { + sortItems.push({ + label: appText.sort.clearSort, + path: pathClearSort, + annotation: , + }); + } + + const sortSection: CommandKSection = { + heading: appText.sort.sort, + accessory: , + items: doesPathOfferSort + ? sortItems + : [], + }; + const pageFull: CommandKItem = { label: GRID_HOMEPAGE_ENABLED ? appText.nav.full @@ -522,13 +577,13 @@ export default function CommandKClient({ : [pageFull, pageGrid]; const sectionPages: CommandKSection = { - heading: 'Pages', + heading: appText.cmdk.pages, accessory: , items: pageItems, }; const adminSection: CommandKSection = { - heading: 'Admin', + heading: appText.nav.admin, accessory: { - setQueryLive(e.currentTarget.value); + value={queryLiveRaw} + onValueChange={value => { + setQueryLiveRaw(value); updateMask(); }} className={clsx( @@ -667,20 +723,30 @@ export default function CommandKClient({ disabled={isPending} /> {isLoading && !isPending - ? - + ? + : setIsOpen?.(false)} + onClick={() => { + if (queryLiveRaw) { + setQueryLiveRaw(''); + updateMask(); + } else { + setIsOpen?.(false); + } + }} > - ESC + {queryLiveRaw + ? + : 'ESC'} } @@ -698,6 +764,7 @@ export default function CommandKClient({ {queriedSections .concat(categorySections) + .concat(sortSection) .concat(sectionPages) .concat(adminSection) .concat(clientSections) diff --git a/src/i18n/locales/bd-bn.ts b/src/i18n/locales/bd-bn.ts index 985336b0..ee609356 100644 --- a/src/i18n/locales/bd-bn.ts +++ b/src/i18n/locales/bd-bn.ts @@ -55,13 +55,25 @@ export const TEXT: I18N = { nextShort: 'পরবর্তী', }, sort: { - newest: 'নতুনতম দেখুন', - oldest: 'পুরাতনতম দেখুন', + sort: 'সাজান', + newest: 'নতুনতম', + oldest: 'পুরাতনতম', + newestFirst: 'নতুনতম প্রথমে', + oldestFirst: 'পুরাতনতম প্রথমে', + viewNewest: 'নতুনতম দেখুন', + viewOldest: 'পুরাতনতম দেখুন', + takenAt: 'তোলা হয়েছে', + byTakenAt: 'তোলার সময় অনুযায়ী', + uploadedAt: 'আপলোড হয়েছে', + byUploadedAt: 'আপলোডের সময় অনুযায়ী', + uploadedAtShort: 'আপলোড', + clearSort: 'সাজানো মুছুন', }, cmdk: { placeholder: 'ছবি, ভিউ, সেটিংস অনুসন্ধান করুন ...', searching: 'অনুসন্ধান হচ্ছে ...', noResults: 'কোনো ফলাফল পাওয়া যায়নি', + pages: 'পৃষ্ঠাসমূহ', }, tooltip: { '35mm': '৩৫মিমি সমতুল্য', diff --git a/src/i18n/locales/en-us.ts b/src/i18n/locales/en-us.ts index e65a26f0..406a7519 100644 --- a/src/i18n/locales/en-us.ts +++ b/src/i18n/locales/en-us.ts @@ -53,13 +53,25 @@ export const TEXT = { nextShort: 'Next', }, sort: { - newest: 'View newest', - oldest: 'View oldest', + sort: 'Sort', + newest: 'Newest', + oldest: 'Oldest', + newestFirst: 'Newest first', + oldestFirst: 'Oldest first', + viewNewest: 'View newest', + viewOldest: 'View oldest', + takenAt: 'Taken at', + byTakenAt: 'By taken at', + uploadedAt: 'Uploaded at', + byUploadedAt: 'By uploaded at', + uploadedAtShort: 'Uploaded', + clearSort: 'Clear sort', }, cmdk: { placeholder: 'Search photos, views, settings ...', searching: 'Searching ...', noResults: 'No results found', + pages: 'Pages', }, tooltip: { '35mm': '35mm Equivalent', diff --git a/src/i18n/locales/id-id.ts b/src/i18n/locales/id-id.ts index 9413ba71..59cc150c 100644 --- a/src/i18n/locales/id-id.ts +++ b/src/i18n/locales/id-id.ts @@ -54,13 +54,25 @@ export const TEXT: I18N = { nextShort: 'Brkt', }, sort: { - newest: 'Lihat terbaru', - oldest: 'Lihat terlama', + sort: 'Urutkan', + newest: 'Terbaru', + oldest: 'Terlama', + newestFirst: 'Terbaru dulu', + oldestFirst: 'Terlama dulu', + viewNewest: 'Lihat terbaru', + viewOldest: 'Lihat terlama', + takenAt: 'Diambil pada', + byTakenAt: 'Berdasarkan waktu pengambilan', + uploadedAt: 'Diunggah pada', + byUploadedAt: 'Berdasarkan waktu unggahan', + uploadedAtShort: 'Diunggah', + clearSort: 'Hapus pengurutan', }, cmdk: { placeholder: 'Cari foto, tampilan, pengaturan ...', searching: 'Mencari ...', noResults: 'Tidak ada hasil ditemukan', + pages: 'Halaman', }, tooltip: { '35mm': 'Setara 35mm', diff --git a/src/i18n/locales/pt-br.ts b/src/i18n/locales/pt-br.ts index 6864a370..9b376984 100644 --- a/src/i18n/locales/pt-br.ts +++ b/src/i18n/locales/pt-br.ts @@ -54,13 +54,25 @@ export const TEXT: I18N = { nextShort: 'Próx', }, sort: { - newest: 'Ver mais recentes', - oldest: 'Ver mais antigas', + sort: 'Ordenar', + newest: 'Mais recentes', + oldest: 'Mais antigas', + newestFirst: 'Mais recentes primeiro', + oldestFirst: 'Mais antigas primeiro', + viewNewest: 'Ver mais recentes', + viewOldest: 'Ver mais antigas', + takenAt: 'Tirado em', + byTakenAt: 'Por data de captura', + uploadedAt: 'Enviado em', + byUploadedAt: 'Por data de envio', + uploadedAtShort: 'Enviado', + clearSort: 'Limpar ordenação', }, cmdk: { placeholder: 'Pesquisar fotos, visualizações, configurações ...', searching: 'Pesquisando ...', noResults: 'Nenhum resultado encontrado', + pages: 'Páginas', }, tooltip: { '35mm': 'Equivalente em 35mm', diff --git a/src/i18n/locales/pt-pt.ts b/src/i18n/locales/pt-pt.ts index e45cc899..11f41421 100644 --- a/src/i18n/locales/pt-pt.ts +++ b/src/i18n/locales/pt-pt.ts @@ -54,13 +54,25 @@ export const TEXT: I18N = { nextShort: 'Próx', }, sort: { - newest: 'Ver mais recentes', - oldest: 'Ver mais antigas', + sort: 'Ordenar', + newest: 'Mais recentes', + oldest: 'Mais antigas', + newestFirst: 'Mais recentes primeiro', + oldestFirst: 'Mais antigas primeiro', + viewNewest: 'Ver mais recentes', + viewOldest: 'Ver mais antigas', + takenAt: 'Tirado em', + byTakenAt: 'Por data de captura', + uploadedAt: 'Enviado em', + byUploadedAt: 'Por data de envio', + uploadedAtShort: 'Enviado', + clearSort: 'Limpar ordenação', }, cmdk: { placeholder: 'Pesquisar fotografias, visualizações, configurações ...', searching: 'A pesquisar ...', noResults: 'Nenhum resultado encontrado', + pages: 'Páginas', }, tooltip: { '35mm': 'Equivalente em 35mm', diff --git a/src/i18n/locales/zh-cn.ts b/src/i18n/locales/zh-cn.ts index 48939a89..ba988f9d 100644 --- a/src/i18n/locales/zh-cn.ts +++ b/src/i18n/locales/zh-cn.ts @@ -54,13 +54,25 @@ export const TEXT: I18N = { nextShort: '下一页', }, sort: { - newest: '查看最新', - oldest: '查看最旧', + sort: '排序', + newest: '最新', + oldest: '最旧', + newestFirst: '最新优先', + oldestFirst: '最旧优先', + viewNewest: '查看最新', + viewOldest: '查看最旧', + takenAt: '拍摄时间', + byTakenAt: '按拍摄时间', + uploadedAt: '上传时间', + byUploadedAt: '按上传时间', + uploadedAtShort: '上传', + clearSort: '清除排序', }, cmdk: { placeholder: '搜索照片、视图、设置...', searching: '搜索中...', noResults: '未找到结果', + pages: '页面', }, tooltip: { '35mm': '35mm 等效焦距', diff --git a/src/photo/sort/SortMenu.tsx b/src/photo/sort/SortMenu.tsx index ecb64853..a9706b95 100644 --- a/src/photo/sort/SortMenu.tsx +++ b/src/photo/sort/SortMenu.tsx @@ -1,8 +1,9 @@ import IconSort from '@/components/icons/IconSort'; import SwitcherItemMenu from '@/components/switcher/SwitcherItemMenu'; -import { getSortConfigFromPath } from './path'; +import { getSortStateFromPath } from './path'; import IconCheck from '@/components/icons/IconCheck'; import { clsx } from 'clsx/lite'; +import { useAppText } from '@/i18n/state/client'; export default function SortMenu({ isOpen, @@ -17,7 +18,9 @@ export default function SortMenu({ }: { isOpen?: boolean setIsOpen?: (isOpen: boolean) => void -} & ReturnType) { +} & ReturnType) { + const appText = useAppText(); + const renderIcon = (isChecked: boolean) => isChecked ? : ; @@ -38,21 +41,21 @@ export default function SortMenu({ />} sections={[{ items: [{ - ...renderLabel('Newest', !isAscending), + ...renderLabel(appText.sort.newest, !isAscending), icon: renderIcon(!isAscending), href: pathNewest, }, { - ...renderLabel('Oldest', isAscending), + ...renderLabel(appText.sort.oldest, isAscending), icon: renderIcon(isAscending), href: pathOldest, }], }, { items: [{ - ...renderLabel('Taken at', isTakenAt), + ...renderLabel(appText.sort.takenAt, isTakenAt), icon: renderIcon(isTakenAt), href: pathTakenAt, }, { - ...renderLabel('Uploaded', isUploadedAt), + ...renderLabel(appText.sort.uploadedAtShort, isUploadedAt), icon: renderIcon(isUploadedAt), href: pathUploadedAt, }], diff --git a/src/photo/sort/path.ts b/src/photo/sort/path.ts index e1e8eefb..e286ffd4 100644 --- a/src/photo/sort/path.ts +++ b/src/photo/sort/path.ts @@ -2,6 +2,7 @@ // to avoid circular dependencies import { + doesPathOfferSort as _doesPathOfferSort, PARAM_SORT_ORDER_NEWEST, PARAM_SORT_ORDER_OLDEST, PARAM_SORT_TYPE_TAKEN_AT, @@ -16,7 +17,7 @@ import { USER_DEFAULT_SORT_WITH_PRIORITY, } from '@/app/config'; -export const getSortByComponents = (sortBy: SortBy): { +const getSortByComponents = (sortBy: SortBy): { sortType: string sortOrder: string } => { @@ -40,7 +41,7 @@ export const getSortByComponents = (sortBy: SortBy): { } }; -const { +export const { sortType: DEFAULT_SORT_TYPE, sortOrder: DEFAULT_SORT_ORDER, } = getSortByComponents(USER_DEFAULT_SORT_BY); @@ -95,7 +96,9 @@ const getPathSortComponents = (pathname: string) => { }; }; -export const getSortConfigFromPath = (pathname: string) => { +export const getSortStateFromPath = (pathname: string) => { + const doesPathOfferSort = _doesPathOfferSort(pathname); + const { gridOrFull: _gridOrFull, sortType, @@ -153,8 +156,14 @@ export const getSortConfigFromPath = (pathname: string) => { const pathUploadedAt = getPath({ sortType: PARAM_SORT_TYPE_UPLOADED_AT, sortOrder }); + // Sort clear + const pathClearSort = _gridOrFull === 'grid' + ? PATH_GRID_INFERRED + : PATH_FULL_INFERRED; + return { sortBy, + doesPathOfferSort, isSortedByDefault, isAscending, isTakenAt, @@ -165,6 +174,7 @@ export const getSortConfigFromPath = (pathname: string) => { pathOldest, pathTakenAt, pathUploadedAt, + pathClearSort, pathSortToggle, }; };