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}`;