Refine admin tag editor
This commit is contained in:
parent
1fd41462e7
commit
02fbf0a2e0
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -6,6 +6,7 @@
|
|||||||
"exif",
|
"exif",
|
||||||
"hgetall",
|
"hgetall",
|
||||||
"hset",
|
"hset",
|
||||||
|
"Lightbox",
|
||||||
"nanoids",
|
"nanoids",
|
||||||
"nextjs",
|
"nextjs",
|
||||||
"qaub",
|
"qaub",
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import AdminChildPage from '@/components/AdminChildPage';
|
import AdminChildPage from '@/components/AdminChildPage';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import { getPhotosCached } from '@/cache';
|
import { getPhotosCached, getPhotosTagCountCached } from '@/cache';
|
||||||
import TagForm from '@/tag/TagForm';
|
import TagForm from '@/tag/TagForm';
|
||||||
import { PATH_ADMIN, PATH_ADMIN_TAGS } from '@/site/paths';
|
import { PATH_ADMIN, PATH_ADMIN_TAGS, pathForTag } from '@/site/paths';
|
||||||
import { getPhotosTagCount } from '@/services/postgres';
|
|
||||||
import PhotoTag from '@/tag/PhotoTag';
|
import PhotoTag from '@/tag/PhotoTag';
|
||||||
import { photoQuantityText } from '@/photo';
|
import { photoLabelForCount } from '@/photo';
|
||||||
import PhotoGrid from '@/photo/PhotoGrid';
|
import PhotoLightbox from '@/photo/PhotoLightbox';
|
||||||
|
|
||||||
|
const MAX_PHOTO_TO_SHOW = 6;
|
||||||
|
|
||||||
export const runtime = 'edge';
|
export const runtime = 'edge';
|
||||||
|
|
||||||
@ -19,8 +20,8 @@ export default async function PhotoPageEdit({ params: { tag } }: Props) {
|
|||||||
count,
|
count,
|
||||||
photos,
|
photos,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
getPhotosTagCount(tag),
|
getPhotosTagCountCached(tag),
|
||||||
getPhotosCached({ tag }),
|
getPhotosCached({ tag, limit: MAX_PHOTO_TO_SHOW }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (count === 0) { redirect(PATH_ADMIN); }
|
if (count === 0) { redirect(PATH_ADMIN); }
|
||||||
@ -32,18 +33,21 @@ export default async function PhotoPageEdit({ params: { tag } }: Props) {
|
|||||||
breadcrumb={<div className="flex item gap-2">
|
breadcrumb={<div className="flex item gap-2">
|
||||||
<PhotoTag {...{ tag }} />
|
<PhotoTag {...{ tag }} />
|
||||||
<div className="text-dim uppercase">
|
<div className="text-dim uppercase">
|
||||||
{photoQuantityText(count, false)}
|
<span>{count}</span>
|
||||||
|
<span className="hidden xs:inline-block">
|
||||||
|
|
||||||
|
{photoLabelForCount(count)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
>
|
>
|
||||||
<div className="space-y-8">
|
<TagForm {...{ tag, photos }}>
|
||||||
<PhotoGrid
|
<PhotoLightbox
|
||||||
photos={photos.slice(0, 12)}
|
{...{ count, photos }}
|
||||||
animate={false}
|
maxPhotosToShow={MAX_PHOTO_TO_SHOW}
|
||||||
small
|
moreLink={pathForTag(tag)}
|
||||||
/>
|
/>
|
||||||
<TagForm {...{ tag, photos }} />
|
</TagForm>
|
||||||
</div>
|
|
||||||
</AdminChildPage>
|
</AdminChildPage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -20,7 +20,10 @@ function AdminChildPage({
|
|||||||
contentMain={
|
contentMain={
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{backPath &&
|
{backPath &&
|
||||||
<div className="flex gap-3 items-center h-9">
|
<div className={cc(
|
||||||
|
'flex flex-wrap items-center gap-x-1.5 sm:gap-x-3 gap-y-1',
|
||||||
|
'h-9',
|
||||||
|
)}>
|
||||||
<Link
|
<Link
|
||||||
href={backPath}
|
href={backPath}
|
||||||
className="flex gap-1.5 items-center"
|
className="flex gap-1.5 items-center"
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export default function PhotoGrid({
|
|||||||
animateOnFirstLoadOnly,
|
animateOnFirstLoadOnly,
|
||||||
staggerOnFirstLoadOnly = true,
|
staggerOnFirstLoadOnly = true,
|
||||||
showMorePath,
|
showMorePath,
|
||||||
|
additionalTile,
|
||||||
small,
|
small,
|
||||||
}: {
|
}: {
|
||||||
photos: Photo[]
|
photos: Photo[]
|
||||||
@ -26,6 +27,7 @@ export default function PhotoGrid({
|
|||||||
animateOnFirstLoadOnly?: boolean
|
animateOnFirstLoadOnly?: boolean
|
||||||
staggerOnFirstLoadOnly?: boolean
|
staggerOnFirstLoadOnly?: boolean
|
||||||
showMorePath?: string
|
showMorePath?: string
|
||||||
|
additionalTile?: JSX.Element
|
||||||
small?: boolean
|
small?: boolean
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@ -34,7 +36,7 @@ export default function PhotoGrid({
|
|||||||
className={cc(
|
className={cc(
|
||||||
'grid gap-1',
|
'grid gap-1',
|
||||||
small
|
small
|
||||||
? 'grid-cols-4 xs:grid-cols-6'
|
? 'grid-cols-3 xs:grid-cols-6'
|
||||||
: 'grid-cols-2 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4',
|
: 'grid-cols-2 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4',
|
||||||
'items-center',
|
'items-center',
|
||||||
)}
|
)}
|
||||||
@ -51,7 +53,7 @@ export default function PhotoGrid({
|
|||||||
tag={tag}
|
tag={tag}
|
||||||
camera={camera}
|
camera={camera}
|
||||||
selected={photo.id === selectedPhoto?.id}
|
selected={photo.id === selectedPhoto?.id}
|
||||||
/>)}
|
/>).concat(additionalTile ?? [])}
|
||||||
/>
|
/>
|
||||||
{showMorePath &&
|
{showMorePath &&
|
||||||
<MorePhotos path={showMorePath} />}
|
<MorePhotos path={showMorePath} />}
|
||||||
|
|||||||
51
src/photo/PhotoLightbox.tsx
Normal file
51
src/photo/PhotoLightbox.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { cc } from '@/utility/css';
|
||||||
|
import { Photo } from '.';
|
||||||
|
import PhotoGrid from './PhotoGrid';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export default function PhotoLightbox({
|
||||||
|
count,
|
||||||
|
photos,
|
||||||
|
maxPhotosToShow = 6,
|
||||||
|
moreLink,
|
||||||
|
}: {
|
||||||
|
count: number
|
||||||
|
photos: Photo[]
|
||||||
|
maxPhotosToShow?: number
|
||||||
|
moreLink: string
|
||||||
|
}) {
|
||||||
|
const photoCountToShow = maxPhotosToShow < count
|
||||||
|
? maxPhotosToShow - 1
|
||||||
|
: maxPhotosToShow;
|
||||||
|
|
||||||
|
const countNotShown = count - photoCountToShow;
|
||||||
|
|
||||||
|
const showOverageTile = countNotShown > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cc(
|
||||||
|
'border dark:border-gray-800 p-1.5 lg:p-2 rounded-md',
|
||||||
|
'bg-gray-50 dark:bg-gray-950',
|
||||||
|
)}>
|
||||||
|
<PhotoGrid
|
||||||
|
photos={photos.slice(0, photoCountToShow)}
|
||||||
|
animate={false}
|
||||||
|
additionalTile={showOverageTile
|
||||||
|
? <Link
|
||||||
|
href={moreLink}
|
||||||
|
className={cc(
|
||||||
|
'flex flex-col items-center justify-center',
|
||||||
|
'gap-0.5 lg:gap-1',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="text-[1.1rem] lg:text-[1.5rem]">
|
||||||
|
+{countNotShown}
|
||||||
|
</div>
|
||||||
|
<div className="text-dim">More</div>
|
||||||
|
</Link>
|
||||||
|
: undefined}
|
||||||
|
small
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -154,7 +154,7 @@ export const translatePhotoId = (id: string) =>
|
|||||||
export const titleForPhoto = (photo: Photo) =>
|
export const titleForPhoto = (photo: Photo) =>
|
||||||
photo.title || 'Untitled';
|
photo.title || 'Untitled';
|
||||||
|
|
||||||
const photoLabelForCount = (count: number) =>
|
export const photoLabelForCount = (count: number) =>
|
||||||
count === 1 ? 'Photo' : 'Photos';
|
count === 1 ? 'Photo' : 'Photos';
|
||||||
|
|
||||||
export const photoQuantityText = (count: number, includeParentheses = true) =>
|
export const photoQuantityText = (count: number, includeParentheses = true) =>
|
||||||
|
|||||||
@ -4,14 +4,16 @@ import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { PATH_ADMIN_TAGS } from '@/site/paths';
|
import { PATH_ADMIN_TAGS } from '@/site/paths';
|
||||||
import FieldSetWithStatus from '@/components/FieldSetWithStatus';
|
import FieldSetWithStatus from '@/components/FieldSetWithStatus';
|
||||||
import { useMemo, useState } from 'react';
|
import { ReactNode, useMemo, useState } from 'react';
|
||||||
import { renamePhotoTagGloballyAction } from '@/photo/actions';
|
import { renamePhotoTagGloballyAction } from '@/photo/actions';
|
||||||
import { parameterize } from '@/utility/string';
|
import { parameterize } from '@/utility/string';
|
||||||
|
|
||||||
export default function TagForm({
|
export default function TagForm({
|
||||||
tag,
|
tag,
|
||||||
|
children,
|
||||||
}: {
|
}: {
|
||||||
tag: string
|
tag: string
|
||||||
|
children?: ReactNode
|
||||||
}) {
|
}) {
|
||||||
const [updatedTagRaw, setUpdatedTagRaw] = useState(tag);
|
const [updatedTagRaw, setUpdatedTagRaw] = useState(tag);
|
||||||
|
|
||||||
@ -49,6 +51,7 @@ export default function TagForm({
|
|||||||
hidden
|
hidden
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
|
{children}
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Link
|
<Link
|
||||||
className="button"
|
className="button"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user