Elevate uploads to admin page

This commit is contained in:
Sam Becker 2023-10-10 15:42:58 -05:00
parent cca73eb0d8
commit fbdba04b3c
9 changed files with 136 additions and 71 deletions

66
src/admin/BlobUrls.tsx Normal file
View File

@ -0,0 +1,66 @@
import { Fragment } from 'react';
import AdminGrid from './AdminGrid';
import Link from 'next/link';
import ImageTiny from '@/components/ImageTiny';
import { pathForBlobUrl } from '@/services/blob';
import EditButton from './EditButton';
import FormWithConfirm from '@/components/FormWithConfirm';
import { deleteBlobPhotoAction } from '@/photo/actions';
import DeleteButton from './DeleteButton';
import { cc } from '@/utility/css';
import { pathForAdminUploadUrl } from '@/site/paths';
export default function BlobUrls({
title,
urls,
}: {
title?: string
urls: string[]
}) {
return (
<AdminGrid {...{ title }} >
{urls.map(url => {
const href = pathForAdminUploadUrl(url);
const fileName = url.split('/').pop();
return <Fragment key={url}>
<Link href={href}>
<ImageTiny
alt={`Photo: ${fileName}`}
src={url}
aspectRatio={3.0 / 2.0}
className={cc(
'rounded-sm overflow-hidden',
'border border-gray-200 dark:border-gray-800',
)}
/>
</Link>
<Link
href={href}
className="break-all"
title={url}
>
{pathForBlobUrl(url)}
</Link>
<EditButton href={href} label="Setup" />
<FormWithConfirm
action={deleteBlobPhotoAction}
confirmText="Are you sure you want to delete this upload?"
>
<input
type="hidden"
name="redirectToPhotos"
value={urls.length < 2 ? 'true' : 'false'}
readOnly
/>
<input
type="hidden"
name="url"
value={url}
readOnly
/>
<DeleteButton />
</FormWithConfirm>
</Fragment>;})}
</AdminGrid>
);
}

View File

@ -1,9 +1,14 @@
import AdminNav from '@/admin/AdminNav';
import {
getBlobUploadUrlsCached,
getPhotosCountIncludingHiddenCached,
getUniqueTagsCached,
} from '@/cache';
import { PATH_ADMIN_PHOTOS, PATH_ADMIN_TAGS } from '@/site/paths';
import {
PATH_ADMIN_PHOTOS,
PATH_ADMIN_TAGS,
PATH_ADMIN_UPLOADS,
} from '@/site/paths';
export default async function AdminLayout({
children,
@ -11,28 +16,37 @@ export default async function AdminLayout({
children: React.ReactNode
}) {
const [
photosCount,
tagsCount,
countPhotos,
countUploads,
countTags,
] = await Promise.all([
getPhotosCountIncludingHiddenCached(),
getBlobUploadUrlsCached().then(urls => urls.length),
getUniqueTagsCached().then(tags => tags.length),
]);
const navItemPhotos = {
label: 'Photos',
href: PATH_ADMIN_PHOTOS,
count: photosCount,
count: countPhotos,
};
const navItemUploads = {
label: 'Uploads',
href: PATH_ADMIN_UPLOADS,
count: countUploads,
};
const navItemTags = {
label: 'Tags',
href: PATH_ADMIN_TAGS,
count: tagsCount,
count: countTags,
};
const navItems = tagsCount > 0
? [navItemPhotos, navItemTags]
: [navItemPhotos];
const navItems = [navItemPhotos];
if (countUploads > 0) { navItems.push(navItemUploads); }
if (countTags > 0) { navItems.push(navItemTags); }
return (
<div className="mt-4 space-y-5">

View File

@ -3,16 +3,11 @@ import PhotoUploadInput from '@/photo/PhotoUploadInput';
import Link from 'next/link';
import PhotoTiny from '@/photo/PhotoTiny';
import { cc } from '@/utility/css';
import ImageTiny from '@/components/ImageTiny';
import FormWithConfirm from '@/components/FormWithConfirm';
import SiteGrid from '@/components/SiteGrid';
import {
deletePhotoAction,
deleteBlobPhotoAction,
syncCacheAction,
} from '@/photo/actions';
deletePhotoAction, syncCacheAction } from '@/photo/actions';
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
import { pathForBlobUrl } from '@/services/blob';
import {
pathForAdminPhotos,
pathForPhoto,
@ -22,7 +17,6 @@ import { titleForPhoto } from '@/photo';
import MorePhotos from '@/components/MorePhotos';
import {
getBlobPhotoUrlsCached,
getBlobUploadUrlsCached,
getPhotosCached,
getPhotosCountIncludingHiddenCached,
} from '@/cache';
@ -35,6 +29,7 @@ import {
import AdminGrid from '@/admin/AdminGrid';
import DeleteButton from '@/admin/DeleteButton';
import EditButton from '@/admin/EditButton';
import BlobUrls from '@/admin/BlobUrls';
export const runtime = 'edge';
@ -48,12 +43,10 @@ export default async function AdminTagsPage({
const [
photos,
count,
blobUploadUrls,
blobPhotoUrls,
] = await Promise.all([
getPhotosCached({ includeHidden: true, sortBy: 'createdAt', limit }),
getPhotosCountIncludingHiddenCached(),
getBlobUploadUrlsCached(),
DEBUG_PHOTO_BLOBS ? getBlobPhotoUrlsCached() : [],
]);
@ -78,16 +71,16 @@ export default async function AdminTagsPage({
</SubmitButtonWithStatus>
</form>
</div>
{blobUploadUrls.length > 0 &&
<BlobUrls
blobUrls={blobUploadUrls}
label={`Uploads Files (${blobUploadUrls.length})`}
/>}
{blobPhotoUrls.length > 0 &&
<BlobUrls
blobUrls={blobPhotoUrls}
label={`Photos Files (${blobPhotoUrls.length})`}
/>}
<div className={cc(
'border-b pb-6',
'border-gray-200 dark:border-gray-700',
)}>
<BlobUrls
title={`Photo Blobs (${blobPhotoUrls.length})`}
urls={blobPhotoUrls}
/>
</div>}
<div className="space-y-4">
<AdminGrid>
{photos.map(photo =>
@ -152,45 +145,3 @@ export default async function AdminTagsPage({
/>
);
}
function BlobUrls ({
blobUrls,
label,
}: {
blobUrls: string[],
label: string,
}) {
return <AdminGrid title={label}>
{blobUrls.map(url => {
const href = `/admin/uploads/${encodeURIComponent(url)}`;
const fileName = url.split('/').pop();
return <Fragment key={url}>
<Link href={href}>
<ImageTiny
alt={`Photo: ${fileName}`}
src={url}
aspectRatio={3.0 / 2.0}
className={cc(
'rounded-sm overflow-hidden',
'border border-gray-200 dark:border-gray-800',
)}
/>
</Link>
<Link
href={href}
className="break-all"
title={url}
>
{pathForBlobUrl(url)}
</Link>
<EditButton href={href} label="Setup" />
<FormWithConfirm
action={deleteBlobPhotoAction}
confirmText="Are you sure you want to delete this upload?"
>
<input type="hidden" name="url" value={url} />
<DeleteButton />
</FormWithConfirm>
</Fragment>;})}
</AdminGrid>;
}

View File

@ -2,7 +2,8 @@ import PhotoForm from '@/photo/PhotoForm';
import { ExifParserFactory } from 'ts-exif-parser';
import { convertExifToFormData } from '@/photo/form';
import AdminChildPage from '@/components/AdminChildPage';
import { getExtensionFromBlobUrl } from '@/services/blob';
import { getExtensionFromBlobUrl, getIdFromBlobUrl } from '@/services/blob';
import { PATH_ADMIN_UPLOADS } from '@/site/paths';
interface Params {
params: { uploadPath: string }
@ -27,7 +28,11 @@ export default async function UploadPage({ params: { uploadPath } }: Params) {
}
return (
<AdminChildPage>
<AdminChildPage
backPath={PATH_ADMIN_UPLOADS}
backLabel="Uploads"
breadcrumb={getIdFromBlobUrl(url)}
>
{data
? <PhotoForm
initialPhotoForm={{

View File

@ -0,0 +1,12 @@
import BlobUrls from '@/admin/BlobUrls';
import { getBlobUploadUrlsCached } from '@/cache';
import SiteGrid from '@/components/SiteGrid';
export default async function UploadsPage() {
const blobUrls = await getBlobUploadUrlsCached();
return (
<SiteGrid
contentMain={<BlobUrls urls={blobUrls} />}
/>
);
}

View File

@ -8,6 +8,7 @@ import {
} from '@/services/blob';
import { cc } from '@/utility/css';
import { useRouter } from 'next/navigation';
import { pathForAdminUploadUrl } from '@/site/paths';
export default function PhotoUploadInput() {
const [isUploading, setIsUploading] = useState(false);
@ -38,7 +39,7 @@ export default function PhotoUploadInput() {
// relevant only when a photo isn't added
router.refresh();
// Redirect to photo detail page
router.push(`/admin/uploads/${encodeURIComponent(url)}`);
router.push(pathForAdminUploadUrl(url));
})
.catch(error => {
setIsUploading(false);

View File

@ -88,6 +88,10 @@ export async function deleteBlobPhotoAction(formData: FormData) {
await deleteBlobPhoto(formData.get('url') as string);
revalidateBlobKey();
if (formData.get('redirectToPhotos') === 'true') {
redirect(PATH_ADMIN_PHOTOS);
}
};
export async function syncCacheAction() {

View File

@ -23,12 +23,20 @@ const REGEX_UPLOAD_PATH = new RegExp(
'i',
);
const REGEX_UPLOAD_ID = new RegExp(
`.${PREFIX_UPLOAD}-([a-z0-9]+)\.[a-z]{1,4}$`,
'i',
);
export const pathForBlobUrl = (url: string) =>
url.replace(`${BLOB_BASE_URL}/`, '');
export const getExtensionFromBlobUrl = (url: string) =>
url.match(/.([a-z]{1,4})$/i)?.[1];
export const getIdFromBlobUrl = (url: string) =>
url.match(REGEX_UPLOAD_ID)?.[1];
export const isUploadPathnameValid = (pathname?: string) =>
pathname?.match(REGEX_UPLOAD_PATH);

View File

@ -21,6 +21,7 @@ export const PATH_CHECKLIST = '/checklist';
// Admin paths
export const PATH_ADMIN_PHOTOS = `${PATH_ADMIN}/photos`;
export const PATH_ADMIN_UPLOADS = `${PATH_ADMIN}/uploads`;
export const PATH_ADMIN_TAGS = `${PATH_ADMIN}/tags`;
export const PATH_ADMIN_UPLOAD = `${PATH_ADMIN}/uploads`;
export const PATH_ADMIN_UPLOAD_BLOB = `${PATH_ADMIN_UPLOAD}/blob`;
@ -45,6 +46,9 @@ export const pathForGrid = (next?: number) =>
export const pathForAdminPhotos = (next?: number) =>
pathWithNext(PATH_ADMIN_PHOTOS, next);
export const pathForAdminUploadUrl = (url: string) =>
`${PATH_ADMIN_UPLOADS}/${encodeURIComponent(url)}`;
export const pathForAdminPhotoEdit = (photo: PhotoOrPhotoId) =>
`${PATH_ADMIN_PHOTOS}/${getPhotoId(photo)}/${EDIT}`;