From fbdba04b3c52cc2030c911bf83b29eb38afcd033 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 10 Oct 2023 15:42:58 -0500 Subject: [PATCH] Elevate uploads to admin page --- src/admin/BlobUrls.tsx | 66 +++++++++++++++++ src/app/(auth-state)/admin/layout.tsx | 30 +++++--- src/app/(auth-state)/admin/photos/page.tsx | 71 +++---------------- .../admin/uploads/[uploadPath]/page.tsx | 9 ++- src/app/(auth-state)/admin/uploads/page.tsx | 12 ++++ src/photo/PhotoUploadInput.tsx | 3 +- src/photo/actions.ts | 4 ++ src/services/blob.ts | 8 +++ src/site/paths.ts | 4 ++ 9 files changed, 136 insertions(+), 71 deletions(-) create mode 100644 src/admin/BlobUrls.tsx create mode 100644 src/app/(auth-state)/admin/uploads/page.tsx diff --git a/src/admin/BlobUrls.tsx b/src/admin/BlobUrls.tsx new file mode 100644 index 00000000..fdeab93c --- /dev/null +++ b/src/admin/BlobUrls.tsx @@ -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 ( + + {urls.map(url => { + const href = pathForAdminUploadUrl(url); + const fileName = url.split('/').pop(); + return + + + + + {pathForBlobUrl(url)} + + + + + + + + ;})} + + ); +} diff --git a/src/app/(auth-state)/admin/layout.tsx b/src/app/(auth-state)/admin/layout.tsx index 679b9f82..81fb5acb 100644 --- a/src/app/(auth-state)/admin/layout.tsx +++ b/src/app/(auth-state)/admin/layout.tsx @@ -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 (
diff --git a/src/app/(auth-state)/admin/photos/page.tsx b/src/app/(auth-state)/admin/photos/page.tsx index 919669d1..6051a40f 100644 --- a/src/app/(auth-state)/admin/photos/page.tsx +++ b/src/app/(auth-state)/admin/photos/page.tsx @@ -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({
- {blobUploadUrls.length > 0 && - } {blobPhotoUrls.length > 0 && - } +
+ +
}
{photos.map(photo => @@ -152,45 +145,3 @@ export default async function AdminTagsPage({ /> ); } - -function BlobUrls ({ - blobUrls, - label, -}: { - blobUrls: string[], - label: string, -}) { - return - {blobUrls.map(url => { - const href = `/admin/uploads/${encodeURIComponent(url)}`; - const fileName = url.split('/').pop(); - return - - - - - {pathForBlobUrl(url)} - - - - - - - ;})} - ; -} diff --git a/src/app/(auth-state)/admin/uploads/[uploadPath]/page.tsx b/src/app/(auth-state)/admin/uploads/[uploadPath]/page.tsx index 7b4ffc89..a4ea30e5 100644 --- a/src/app/(auth-state)/admin/uploads/[uploadPath]/page.tsx +++ b/src/app/(auth-state)/admin/uploads/[uploadPath]/page.tsx @@ -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 ( - + {data ? } + /> + ); +} diff --git a/src/photo/PhotoUploadInput.tsx b/src/photo/PhotoUploadInput.tsx index 6d64edd9..a09dd022 100644 --- a/src/photo/PhotoUploadInput.tsx +++ b/src/photo/PhotoUploadInput.tsx @@ -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); diff --git a/src/photo/actions.ts b/src/photo/actions.ts index 0034fe19..87abaf5e 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -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() { diff --git a/src/services/blob.ts b/src/services/blob.ts index a34d83a0..d83a363e 100644 --- a/src/services/blob.ts +++ b/src/services/blob.ts @@ -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); diff --git a/src/site/paths.ts b/src/site/paths.ts index 312d451e..93d55c25 100644 --- a/src/site/paths.ts +++ b/src/site/paths.ts @@ -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}`;