Merge pull request #105 from sambecker/focal-cmdk
Add focal length to CMD-K menu
This commit is contained in:
commit
668f529ff0
22
package.json
22
package.json
@ -9,28 +9,28 @@
|
||||
"analyze": "ANALYZE=true next build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^0.0.18",
|
||||
"@aws-sdk/client-s3": "3.583.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.583.0",
|
||||
"@ai-sdk/openai": "^0.0.21",
|
||||
"@aws-sdk/client-s3": "3.591.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.591.0",
|
||||
"@next/bundle-analyzer": "14.2.3",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@testing-library/jest-dom": "^6.4.5",
|
||||
"@testing-library/react": "^15.0.7",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^20.12.13",
|
||||
"@types/node": "^20.14.2",
|
||||
"@types/pg": "^8.11.6",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.11.0",
|
||||
"@typescript-eslint/parser": "^7.11.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.12.0",
|
||||
"@typescript-eslint/parser": "^7.12.0",
|
||||
"@upstash/ratelimit": "^1.1.3",
|
||||
"@vercel/analytics": "^1.3.1",
|
||||
"@vercel/blob": "^0.23.3",
|
||||
"@vercel/kv": "^2.0.0",
|
||||
"@vercel/speed-insights": "^1.0.11",
|
||||
"ai": "^3.1.21",
|
||||
"ai": "^3.1.27",
|
||||
"autoprefixer": "10.4.19",
|
||||
"camelcase-keys": "^9.1.3",
|
||||
"clsx": "^2.1.1",
|
||||
@ -39,14 +39,14 @@
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-next": "14.2.3",
|
||||
"exifr": "^7.1.3",
|
||||
"framer-motion": "^11.2.9",
|
||||
"framer-motion": "^11.2.10",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"nanoid": "^5.0.7",
|
||||
"next": "^14.2.3",
|
||||
"next-auth": "5.0.0-beta.18",
|
||||
"next-themes": "^0.3.0",
|
||||
"pg": "^8.11.5",
|
||||
"pg": "^8.12.0",
|
||||
"postcss": "8.4.38",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
@ -54,7 +54,7 @@
|
||||
"sharp": "^0.33.4",
|
||||
"sonner": "^1.4.41",
|
||||
"swr": "^2.2.5",
|
||||
"tailwindcss": "3.4.3",
|
||||
"tailwindcss": "3.4.4",
|
||||
"ts-exif-parser": "^0.2.2",
|
||||
"typescript": "5.4.5",
|
||||
"undici": "^6.18.2",
|
||||
|
||||
851
pnpm-lock.yaml
generated
851
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -91,12 +91,16 @@ export default function CommandKClient({
|
||||
const isOpenRef = useRef(isOpen);
|
||||
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [keyPending, setKeyPending] = useState<string>();
|
||||
const shouldCloseAfterPending = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPending && shouldCloseAfterPending.current) {
|
||||
setIsOpen?.(false);
|
||||
shouldCloseAfterPending.current = false;
|
||||
if (!isPending) {
|
||||
setKeyPending(undefined);
|
||||
if (shouldCloseAfterPending.current) {
|
||||
setIsOpen?.(false);
|
||||
shouldCloseAfterPending.current = false;
|
||||
}
|
||||
}
|
||||
}, [isPending, setIsOpen]);
|
||||
|
||||
@ -312,7 +316,7 @@ export default function CommandKClient({
|
||||
onClose={() => setIsOpen?.(false)}
|
||||
fast
|
||||
>
|
||||
<div className={clsx('space-y-1.5', isPending && 'opacity-30')}>
|
||||
<div className="space-y-1.5">
|
||||
<div className="relative">
|
||||
<Command.Input
|
||||
onChangeCapture={(e) => setQueryLive(e.currentTarget.value)}
|
||||
@ -324,6 +328,7 @@ export default function CommandKClient({
|
||||
'focus:border-gray-200 focus:dark:border-gray-800',
|
||||
'placeholder:text-gray-400/80',
|
||||
'placeholder:dark:text-gray-700',
|
||||
isPending && 'opacity-20',
|
||||
)}
|
||||
placeholder="Search photos, views, settings ..."
|
||||
disabled={isPending}
|
||||
@ -356,6 +361,7 @@ export default function CommandKClient({
|
||||
heading={<div className={clsx(
|
||||
'flex items-center',
|
||||
'px-2',
|
||||
isPending && 'opacity-20',
|
||||
)}>
|
||||
{accessory &&
|
||||
<div className="w-5">{accessory}</div>}
|
||||
@ -379,15 +385,17 @@ export default function CommandKClient({
|
||||
annotationAria,
|
||||
path,
|
||||
action,
|
||||
}) =>
|
||||
<CommandKItem
|
||||
key={`${heading} ${label}`}
|
||||
}) => {
|
||||
const key = `${heading} ${label}`;
|
||||
return <CommandKItem
|
||||
key={key}
|
||||
label={label}
|
||||
value={`${heading} ${label}`}
|
||||
value={key}
|
||||
keywords={keywords}
|
||||
onSelect={() => {
|
||||
if (path) {
|
||||
startTransition(() => {
|
||||
setKeyPending(key);
|
||||
startTransition(async () => {
|
||||
shouldCloseAfterPending.current = true;
|
||||
router.push(path, { scroll: true });
|
||||
});
|
||||
@ -399,8 +407,10 @@ export default function CommandKClient({
|
||||
accessory={accessory}
|
||||
annotation={annotation}
|
||||
annotationAria={annotationAria}
|
||||
showSpinner={Boolean(path)}
|
||||
/>)}
|
||||
loading={key === keyPending}
|
||||
disabled={isPending && key !== keyPending}
|
||||
/>;
|
||||
})}
|
||||
</Command.Group>)}
|
||||
{footer && !queryLive &&
|
||||
<div className="text-center text-dim pt-3 sm:pt-4">
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { Command } from 'cmdk';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import Spinner from '../Spinner';
|
||||
|
||||
export default function CommandKItem({
|
||||
@ -11,7 +11,8 @@ export default function CommandKItem({
|
||||
accessory,
|
||||
annotation,
|
||||
annotationAria,
|
||||
showSpinner,
|
||||
loading,
|
||||
disabled,
|
||||
}: {
|
||||
label: string
|
||||
value: string
|
||||
@ -20,10 +21,9 @@ export default function CommandKItem({
|
||||
accessory?: ReactNode
|
||||
annotation?: ReactNode
|
||||
annotationAria?: string
|
||||
showSpinner?: boolean
|
||||
loading?: boolean
|
||||
disabled?: boolean
|
||||
}) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
return (
|
||||
<Command.Item
|
||||
value={value}
|
||||
@ -32,23 +32,26 @@ export default function CommandKItem({
|
||||
'px-2',
|
||||
accessory ? 'py-2' : 'py-1',
|
||||
'rounded-md cursor-pointer tracking-wide',
|
||||
'data-[selected=true]:bg-gray-100',
|
||||
'data-[selected=true]:dark:bg-gray-900/75',
|
||||
'active:!bg-gray-200/75 active:dark:!bg-gray-800/55',
|
||||
...loading
|
||||
? [
|
||||
'data-[selected=true]:dark:bg-gray-900/50',
|
||||
'data-[selected=true]:bg-gray-100/50',
|
||||
] : [
|
||||
'data-[selected=true]:dark:bg-gray-900/75',
|
||||
'data-[selected=true]:bg-gray-100',
|
||||
],
|
||||
disabled && 'opacity-15',
|
||||
)}
|
||||
onSelect={() => {
|
||||
onSelect?.();
|
||||
if (showSpinner) {
|
||||
setIsLoading(true);
|
||||
}
|
||||
}}
|
||||
onSelect={onSelect}
|
||||
disabled={loading || disabled}
|
||||
>
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
{accessory}
|
||||
<span className="grow text-ellipsis truncate">
|
||||
{label}
|
||||
</span>
|
||||
{annotation && !isLoading &&
|
||||
{annotation && !loading &&
|
||||
<span
|
||||
className="text-dim whitespace-nowrap"
|
||||
aria-label={annotationAria}
|
||||
@ -57,8 +60,8 @@ export default function CommandKItem({
|
||||
{annotation}
|
||||
</span>
|
||||
</span>}
|
||||
{isLoading &&
|
||||
<Spinner color="text" />}
|
||||
{loading &&
|
||||
<Spinner color="dim" />}
|
||||
</div>
|
||||
</Command.Item>
|
||||
);
|
||||
|
||||
@ -9,6 +9,11 @@ import {
|
||||
absolutePathForFocalLengthImage,
|
||||
} from '@/site/paths';
|
||||
|
||||
export type FocalLengths = {
|
||||
focal: number
|
||||
count: number
|
||||
}[]
|
||||
|
||||
export const getFocalLengthFromString = (focalString?: string) => {
|
||||
const focal = focalString?.match(/^([0-9]+)mm/)?.[1];
|
||||
return focal ? parseInt(focal, 10) : 0;
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
getPhotosNearId,
|
||||
getPhotosMostRecentUpdate,
|
||||
getPhotosMeta,
|
||||
getUniqueFocalLengths,
|
||||
} from '@/photo/db/query';
|
||||
import { GetPhotosOptions } from './db';
|
||||
import { parseCachedPhotoDates, parseCachedPhotosDates } from '@/photo';
|
||||
@ -37,6 +38,7 @@ const KEY_PHOTO = 'photo';
|
||||
const KEY_TAGS = 'tags';
|
||||
const KEY_CAMERAS = 'cameras';
|
||||
const KEY_FILM_SIMULATIONS = 'film-simulations';
|
||||
const KEY_FOCAL_LENGTHS = 'focal-lengths';
|
||||
// Type keys
|
||||
const KEY_COUNT = 'count';
|
||||
const KEY_HIDDEN = 'hidden';
|
||||
@ -196,6 +198,12 @@ export const getUniqueFilmSimulationsCached =
|
||||
[KEY_PHOTOS, KEY_FILM_SIMULATIONS],
|
||||
);
|
||||
|
||||
export const getUniqueFocalLengthsCached =
|
||||
unstable_cache(
|
||||
getUniqueFocalLengths,
|
||||
[KEY_PHOTOS, KEY_FOCAL_LENGTHS],
|
||||
);
|
||||
|
||||
// No store
|
||||
|
||||
export const getPhotosNoStore = (...args: Parameters<typeof getPhotos>) => {
|
||||
|
||||
@ -21,6 +21,7 @@ import {
|
||||
getOrderByFromOptions,
|
||||
} from '.';
|
||||
import { getWheresFromOptions } from '.';
|
||||
import { FocalLengths } from '@/focal';
|
||||
|
||||
const createPhotosTable = () =>
|
||||
sql`
|
||||
@ -283,6 +284,20 @@ export const getUniqueFilmSimulations = async () =>
|
||||
})))
|
||||
, 'getUniqueFilmSimulations');
|
||||
|
||||
export const getUniqueFocalLengths = async () =>
|
||||
safelyQueryPhotos(() => sql`
|
||||
SELECT DISTINCT focal_length, COUNT(*)
|
||||
FROM photos
|
||||
WHERE hidden IS NOT TRUE AND focal_length IS NOT NULL
|
||||
GROUP BY focal_length
|
||||
ORDER BY focal_length ASC
|
||||
`.then(({ rows }): FocalLengths => rows
|
||||
.map(({ focal_length, count }) => ({
|
||||
focal: parseInt(focal_length, 10),
|
||||
count: parseInt(count, 10),
|
||||
})))
|
||||
, 'getUniqueFocalLengths');
|
||||
|
||||
export const getPhotos = async (options: GetPhotosOptions = {}) =>
|
||||
safelyQueryPhotos(async () => {
|
||||
const sql = ['SELECT * FROM photos'];
|
||||
|
||||
@ -10,15 +10,18 @@ import {
|
||||
import {
|
||||
pathForCamera,
|
||||
pathForFilmSimulation,
|
||||
pathForFocalLength,
|
||||
} from './paths';
|
||||
import { formatCameraText } from '@/camera';
|
||||
import { photoQuantityText } from '@/photo';
|
||||
import { formatCount, formatCountDescriptive } from '@/utility/string';
|
||||
import { TagsWithMeta } from '@/tag';
|
||||
import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon';
|
||||
import { IoMdCamera } from 'react-icons/io';
|
||||
import { ADMIN_DEBUG_TOOLS_ENABLED, SHOW_FILM_SIMULATIONS } from './config';
|
||||
import { labelForFilmSimulation } from '@/vendors/fujifilm';
|
||||
import { getUniqueFocalLengths } from '@/photo/db/query';
|
||||
import { formatFocalLength } from '@/focal';
|
||||
import { TbCone } from 'react-icons/tb';
|
||||
|
||||
export default async function CommandK() {
|
||||
const [
|
||||
@ -26,15 +29,17 @@ export default async function CommandK() {
|
||||
tags,
|
||||
cameras,
|
||||
filmSimulations,
|
||||
focalLengths,
|
||||
] = await Promise.all([
|
||||
getPhotosMetaCached()
|
||||
.then(({ count }) => count)
|
||||
.catch(() => 0),
|
||||
getUniqueTagsCached().catch(() => [] as TagsWithMeta),
|
||||
getUniqueTagsCached().catch(() => []),
|
||||
getUniqueCamerasCached().catch(() => []),
|
||||
SHOW_FILM_SIMULATIONS
|
||||
? getUniqueFilmSimulationsCached().catch(() => [])
|
||||
: [],
|
||||
getUniqueFocalLengths().catch(() => []),
|
||||
]);
|
||||
|
||||
const SECTION_CAMERAS: CommandKSection = {
|
||||
@ -61,11 +66,25 @@ export default async function CommandK() {
|
||||
})),
|
||||
};
|
||||
|
||||
const SECTION_FOCAL: CommandKSection = {
|
||||
heading: 'Focal Lengths',
|
||||
accessory: <TbCone
|
||||
className="rotate-[270deg] text-[14px]"
|
||||
/>,
|
||||
items: focalLengths.map(({ focal, count }) => ({
|
||||
label: formatFocalLength(focal)!,
|
||||
annotation: formatCount(count),
|
||||
annotationAria: formatCountDescriptive(count),
|
||||
path: pathForFocalLength(focal),
|
||||
})),
|
||||
};
|
||||
|
||||
return <CommandKClient
|
||||
tags={tags}
|
||||
serverSections={[
|
||||
SECTION_CAMERAS,
|
||||
SECTION_FILM,
|
||||
SECTION_FOCAL,
|
||||
]}
|
||||
showDebugTools={ADMIN_DEBUG_TOOLS_ENABLED}
|
||||
footer={photoQuantityText(count, false)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user