Add hidden field to photos

This commit is contained in:
Sam Becker 2023-09-25 15:28:41 -05:00
parent 061d3bb03b
commit 53db663a5c
8 changed files with 102 additions and 27 deletions

View File

@ -20,8 +20,9 @@ import {
getBlobPhotoUrlsCached,
getBlobUploadUrlsCached,
getPhotosCached,
getPhotosCountCached,
getPhotosCountIncludingHiddenCached,
} from '@/cache';
import { AiOutlineEyeInvisible } from 'react-icons/ai';
export const runtime = 'edge';
@ -40,8 +41,8 @@ export default async function AdminPage({
blobUploadUrls,
blobPhotoUrls,
] = await Promise.all([
getPhotosCached({ sortBy: 'createdAt', limit }),
getPhotosCountCached(),
getPhotosCached({ includeHidden: true, sortBy: 'createdAt', limit }),
getPhotosCountIncludingHiddenCached(),
getBlobUploadUrlsCached(),
DEBUG_PHOTO_BLOBS ? getBlobPhotoUrlsCached() : [],
]);
@ -81,10 +82,17 @@ export default async function AdminPage({
href={pathForPhoto(photo)}
className="sm:w-[50%] flex items-center gap-2"
>
{photo.title ||
<span className="text-gray-400 dark:text-gray-500">
Untitled
</span>}
<span className={cc(
'inline-flex items-center gap-2',
photo.hidden && 'text-gray-400 dark:text-gray-500',
)}>
<span>{photo.title || 'Untitled'}</span>
{photo.hidden &&
<AiOutlineEyeInvisible
className="translate-y-[0.25px]"
size={16}
/>}
</span>
{photo.priorityOrder !== null &&
<span className={cc(
'text-xs leading-none px-1.5 py-1 rounded-sm',

13
src/cache/index.ts vendored
View File

@ -5,6 +5,7 @@ import {
getPhoto,
getPhotos,
getPhotosCount,
getPhotosCountIncludingHidden,
getUniqueTags,
} from '@/services/postgres';
import { parseCachedPhotosDates, parseCachedPhotoDates } from '@/photo';
@ -25,6 +26,7 @@ const getPhotosCacheTags = (options: GetPhotosOptions = {}) => {
tag,
takenAfterInclusive,
takenBefore,
includeHidden,
} = options;
if (sortBy !== undefined) { tags.push(`sortBy-${sortBy}`); }
@ -35,6 +37,8 @@ const getPhotosCacheTags = (options: GetPhotosOptions = {}) => {
if (takenBefore !== undefined) { tags.push(`takenBefore-${takenBefore.toISOString()}`); }
// eslint-disable-next-line max-len
if (takenAfterInclusive !== undefined) { tags.push(`takenAfterInclusive-${takenAfterInclusive.toISOString()}`); }
// eslint-disable-next-line max-len
if (includeHidden !== undefined) { tags.push(`includeHidden-${includeHidden}`); }
return tags;
};
@ -68,6 +72,15 @@ export const getPhotosCountCached: typeof getPhotosCount = (...args) =>
}
)();
export const getPhotosCountIncludingHiddenCached: typeof getPhotosCount =
(...args) =>
unstable_cache(
() => getPhotosCountIncludingHidden(...args),
[TAG_PHOTOS, TAG_PHOTOS_COUNT], {
tags: [TAG_PHOTOS, TAG_PHOTOS_COUNT],
}
)();
export const getPhotoCached: typeof getPhoto = (...args) =>
unstable_cache(
() => getPhoto(...args),

View File

@ -3,6 +3,7 @@
import { LegacyRef } from 'react';
import { experimental_useFormStatus as useFormStatus } from 'react-dom';
import Spinner from './Spinner';
import { cc } from '@/utility/css';
export default function FieldSetWithStatus({
id,
@ -26,7 +27,7 @@ export default function FieldSetWithStatus({
loading?: boolean
required?: boolean
readOnly?: boolean
type?: 'text' | 'password'
type?: 'text' | 'password' | 'checkbox'
inputRef?: LegacyRef<HTMLInputElement>
}) {
const { pending } = useFormStatus();
@ -34,7 +35,7 @@ export default function FieldSetWithStatus({
return (
<div className="space-y-1">
<label
className="flex gap-2 items-center"
className="flex gap-2 items-center select-none"
htmlFor={id}
>
{label}
@ -57,11 +58,13 @@ export default function FieldSetWithStatus({
name={id}
value={value}
placeholder={placeholder}
onChange={e => onChange?.(e.target.value)}
onChange={e => onChange?.(type === 'checkbox'
? e.target.value ? 'true' : 'false'
: e.target.value)}
type={type}
autoComplete="off"
readOnly={readOnly || pending}
className="w-full"
className={cc(type === 'text' && 'w-full')}
/>
</div>
);

View File

@ -71,10 +71,15 @@ export default function PhotoForm({
action={type === 'create' ? createPhotoAction : updatePhotoAction}
className="space-y-6 pb-12"
>
{FORM_METADATA_ENTRIES.map(([
key,
{ label, note, required, readOnly, hideIfEmpty, loadingMessage },
]) =>
{FORM_METADATA_ENTRIES.map(([key, {
label,
note,
required,
readOnly,
hideIfEmpty,
loadingMessage,
checkbox,
}]) =>
(!hideIfEmpty || formData[key]) &&
<FieldSetWithStatus
key={key}
@ -89,6 +94,7 @@ export default function PhotoForm({
? loadingMessage
: undefined}
loading={loadingMessage && !formData[key] ? true : false}
type={checkbox ? 'checkbox' : undefined}
/>)}
<div className="flex gap-4">
{type === 'edit' &&

View File

@ -19,6 +19,7 @@ type FormMeta = {
hideIfEmpty?: boolean
hideTemporarily?: boolean
loadingMessage?: string
checkbox?: boolean
};
const FORM_METADATA: Record<keyof PhotoFormData, FormMeta> = {
@ -45,6 +46,7 @@ const FORM_METADATA: Record<keyof PhotoFormData, FormMeta> = {
priorityOrder: { label: 'priority order' },
takenAt: { label: 'taken at' },
takenAtNaive: { label: 'taken at (naive)' },
hidden: { label: 'hidden', checkbox: true },
};
export const FORM_METADATA_ENTRIES =
@ -146,5 +148,6 @@ export const convertFormDataToPhoto = (
priorityOrder: photoForm.priorityOrder
? parseFloat(photoForm.priorityOrder)
: undefined,
hidden: photoForm.hidden === 'true',
};
};

View File

@ -43,6 +43,7 @@ export interface PhotoDbInsert extends PhotoExif {
tags?: string[]
locationName?: string
priorityOrder?: number
hidden?: boolean
}
// Raw db response

View File

@ -38,6 +38,7 @@ const sqlCreatePhotosTable = () =>
priority_order REAL,
taken_at TIMESTAMP WITH TIME ZONE NOT NULL,
taken_at_naive VARCHAR(255) NOT NULL,
hidden BOOLEAN,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
)
@ -67,6 +68,7 @@ export const sqlInsertPhoto = (photo: PhotoDbInsert) => {
longitude,
film_simulation,
priority_order,
hidden,
taken_at,
taken_at_naive
)
@ -91,6 +93,7 @@ export const sqlInsertPhoto = (photo: PhotoDbInsert) => {
${photo.longitude},
${photo.filmSimulation},
${photo.priorityOrder},
${photo.hidden},
${photo.takenAt},
${photo.takenAtNaive}
)
@ -119,6 +122,7 @@ export const sqlUpdatePhoto = (photo: PhotoDbInsert) =>
longitude=${photo.longitude},
film_simulation=${photo.filmSimulation},
priority_order=${photo.priorityOrder || null},
hidden=${photo.hidden},
taken_at=${photo.takenAt},
taken_at_naive=${photo.takenAtNaive},
updated_at=${(new Date()).toISOString()}
@ -134,16 +138,28 @@ const sqlGetPhotos = (
) =>
sql<PhotoDb>`
SELECT * FROM photos
WHERE hidden IS NOT TRUE
ORDER BY taken_at DESC
LIMIT ${limit} OFFSET ${offset}
`;
const sqlGetPhotosIncludingHidden = (
limit = PHOTO_DEFAULT_LIMIT,
offset = 0,
) =>
sql<PhotoDb>`
SELECT * FROM photos
ORDER BY created_at DESC
LIMIT ${limit} OFFSET ${offset}
`;
const sqlGetPhotosSortedByCreatedAt = (
limit = PHOTO_DEFAULT_LIMIT,
offset = 0,
) =>
sql<PhotoDb>`
SELECT * FROM photos
WHERE hidden IS NOT TRUE
ORDER BY created_at DESC
LIMIT ${limit} OFFSET ${offset}
`;
@ -154,6 +170,7 @@ const sqlGetPhotosSortedByPriority = (
) =>
sql<PhotoDb>`
SELECT * FROM photos
WHERE hidden IS NOT TRUE
ORDER BY priority_order ASC, taken_at DESC
LIMIT ${limit} OFFSET ${offset}
`;
@ -164,7 +181,9 @@ const sqlGetPhotosByTag = (
tag: string,
) =>
sql<PhotoDb>`
SELECT * FROM photos WHERE ${tag}=ANY(tags)
SELECT * FROM photos
WHERE ${tag}=ANY(tags)
AND hidden IS NOT TRUE
ORDER BY taken_at ASC
LIMIT ${limit} OFFSET ${offset}
`;
@ -176,6 +195,7 @@ const sqlGetPhotosTakenAfterDateInclusive = (
sql<PhotoDb>`
SELECT * FROM photos
WHERE taken_at <= ${takenAt.toISOString()}
AND hidden IS NOT TRUE
ORDER BY taken_at DESC
LIMIT ${limit}
`;
@ -187,6 +207,7 @@ const sqlGetPhotosTakenBeforeDate = (
sql<PhotoDb>`
SELECT * FROM photos
WHERE taken_at > ${takenAt.toISOString()}
AND hidden IS NOT TRUE
ORDER BY taken_at ASC
LIMIT ${limit}
`;
@ -196,10 +217,16 @@ const sqlGetPhoto = (id: string) =>
const sqlGetPhotosCount = async () => sql`
SELECT COUNT(*) FROM photos
WHERE hidden IS NOT TRUE
`.then(({ rows }) => parseInt(rows[0].count, 10));
const sqlGetPhotosCountIncludingHidden = async () => sql`
SELECT COUNT(*) FROM photos
`.then(({ rows }) => parseInt(rows[0].count, 10));
const sqlGetUniqueTags = async () => sql`
SELECT DISTINCT unnest(tags) FROM photos
WHERE hidden IS NOT TRUE
`.then(({ rows }) => rows.map(row => row.unnest as string));
export type GetPhotosOptions = {
@ -209,6 +236,7 @@ export type GetPhotosOptions = {
tag?: string
takenBefore?: Date
takenAfterInclusive?: Date
includeHidden?: boolean
}
const safelyQueryPhotos = async <T>(callback: () => Promise<T>): Promise<T> => {
@ -249,19 +277,25 @@ export const getPhotos = async (options: GetPhotosOptions = {}) => {
tag,
takenBefore,
takenAfterInclusive,
includeHidden,
} = options;
const getPhotosSql = takenBefore
? () => sqlGetPhotosTakenBeforeDate(takenBefore, limit)
: takenAfterInclusive
? () => sqlGetPhotosTakenAfterDateInclusive(takenAfterInclusive, limit)
: tag
? () => sqlGetPhotosByTag(limit, offset, tag)
: sortBy === 'createdAt'
? () => sqlGetPhotosSortedByCreatedAt(limit, offset)
: sortBy === 'priority'
? () => sqlGetPhotosSortedByPriority(limit, offset)
: () => sqlGetPhotos(limit, offset);
let getPhotosSql = () => sqlGetPhotos(limit, offset);
if (includeHidden) {
getPhotosSql = () => sqlGetPhotosIncludingHidden(limit, offset);
} else if (takenBefore) {
getPhotosSql = () => sqlGetPhotosTakenBeforeDate(takenBefore, limit);
} else if (takenAfterInclusive) {
// eslint-disable-next-line max-len
getPhotosSql = () => sqlGetPhotosTakenAfterDateInclusive(takenAfterInclusive, limit);
} else if (tag) {
getPhotosSql = () => sqlGetPhotosByTag(limit, offset, tag);
} else if (sortBy === 'createdAt') {
getPhotosSql = () => sqlGetPhotosSortedByCreatedAt(limit, offset);
} else if (sortBy === 'priority') {
getPhotosSql = () => sqlGetPhotosSortedByPriority(limit, offset);
}
return safelyQueryPhotos(getPhotosSql)
.then(({ rows }) => rows.map(parsePhotoFromDb));
@ -278,4 +312,7 @@ export const getPhoto = async (id: string): Promise<Photo | undefined> => {
export const getPhotosCount = () => safelyQueryPhotos(sqlGetPhotosCount);
export const getPhotosCountIncludingHidden = () =>
safelyQueryPhotos(sqlGetPhotosCountIncludingHidden);
export const getUniqueTags = () => safelyQueryPhotos(sqlGetUniqueTags);

View File

@ -47,6 +47,10 @@
file:active:disabled:bg-white
file:hover:disabled:cursor-not-allowed
}
input[type=checkbox] {
@apply
rounded-md
}
button, .button {
@apply
cursor-pointer