Elevate uploads to admin page
This commit is contained in:
parent
cca73eb0d8
commit
fbdba04b3c
66
src/admin/BlobUrls.tsx
Normal file
66
src/admin/BlobUrls.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,9 +1,14 @@
|
|||||||
import AdminNav from '@/admin/AdminNav';
|
import AdminNav from '@/admin/AdminNav';
|
||||||
import {
|
import {
|
||||||
|
getBlobUploadUrlsCached,
|
||||||
getPhotosCountIncludingHiddenCached,
|
getPhotosCountIncludingHiddenCached,
|
||||||
getUniqueTagsCached,
|
getUniqueTagsCached,
|
||||||
} from '@/cache';
|
} 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({
|
export default async function AdminLayout({
|
||||||
children,
|
children,
|
||||||
@ -11,28 +16,37 @@ export default async function AdminLayout({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const [
|
const [
|
||||||
photosCount,
|
countPhotos,
|
||||||
tagsCount,
|
countUploads,
|
||||||
|
countTags,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
getPhotosCountIncludingHiddenCached(),
|
getPhotosCountIncludingHiddenCached(),
|
||||||
|
getBlobUploadUrlsCached().then(urls => urls.length),
|
||||||
getUniqueTagsCached().then(tags => tags.length),
|
getUniqueTagsCached().then(tags => tags.length),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const navItemPhotos = {
|
const navItemPhotos = {
|
||||||
label: 'Photos',
|
label: 'Photos',
|
||||||
href: PATH_ADMIN_PHOTOS,
|
href: PATH_ADMIN_PHOTOS,
|
||||||
count: photosCount,
|
count: countPhotos,
|
||||||
|
};
|
||||||
|
|
||||||
|
const navItemUploads = {
|
||||||
|
label: 'Uploads',
|
||||||
|
href: PATH_ADMIN_UPLOADS,
|
||||||
|
count: countUploads,
|
||||||
};
|
};
|
||||||
|
|
||||||
const navItemTags = {
|
const navItemTags = {
|
||||||
label: 'Tags',
|
label: 'Tags',
|
||||||
href: PATH_ADMIN_TAGS,
|
href: PATH_ADMIN_TAGS,
|
||||||
count: tagsCount,
|
count: countTags,
|
||||||
};
|
};
|
||||||
|
|
||||||
const navItems = tagsCount > 0
|
const navItems = [navItemPhotos];
|
||||||
? [navItemPhotos, navItemTags]
|
|
||||||
: [navItemPhotos];
|
if (countUploads > 0) { navItems.push(navItemUploads); }
|
||||||
|
if (countTags > 0) { navItems.push(navItemTags); }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 space-y-5">
|
<div className="mt-4 space-y-5">
|
||||||
|
|||||||
@ -3,16 +3,11 @@ import PhotoUploadInput from '@/photo/PhotoUploadInput';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import PhotoTiny from '@/photo/PhotoTiny';
|
import PhotoTiny from '@/photo/PhotoTiny';
|
||||||
import { cc } from '@/utility/css';
|
import { cc } from '@/utility/css';
|
||||||
import ImageTiny from '@/components/ImageTiny';
|
|
||||||
import FormWithConfirm from '@/components/FormWithConfirm';
|
import FormWithConfirm from '@/components/FormWithConfirm';
|
||||||
import SiteGrid from '@/components/SiteGrid';
|
import SiteGrid from '@/components/SiteGrid';
|
||||||
import {
|
import {
|
||||||
deletePhotoAction,
|
deletePhotoAction, syncCacheAction } from '@/photo/actions';
|
||||||
deleteBlobPhotoAction,
|
|
||||||
syncCacheAction,
|
|
||||||
} from '@/photo/actions';
|
|
||||||
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
|
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
|
||||||
import { pathForBlobUrl } from '@/services/blob';
|
|
||||||
import {
|
import {
|
||||||
pathForAdminPhotos,
|
pathForAdminPhotos,
|
||||||
pathForPhoto,
|
pathForPhoto,
|
||||||
@ -22,7 +17,6 @@ import { titleForPhoto } from '@/photo';
|
|||||||
import MorePhotos from '@/components/MorePhotos';
|
import MorePhotos from '@/components/MorePhotos';
|
||||||
import {
|
import {
|
||||||
getBlobPhotoUrlsCached,
|
getBlobPhotoUrlsCached,
|
||||||
getBlobUploadUrlsCached,
|
|
||||||
getPhotosCached,
|
getPhotosCached,
|
||||||
getPhotosCountIncludingHiddenCached,
|
getPhotosCountIncludingHiddenCached,
|
||||||
} from '@/cache';
|
} from '@/cache';
|
||||||
@ -35,6 +29,7 @@ import {
|
|||||||
import AdminGrid from '@/admin/AdminGrid';
|
import AdminGrid from '@/admin/AdminGrid';
|
||||||
import DeleteButton from '@/admin/DeleteButton';
|
import DeleteButton from '@/admin/DeleteButton';
|
||||||
import EditButton from '@/admin/EditButton';
|
import EditButton from '@/admin/EditButton';
|
||||||
|
import BlobUrls from '@/admin/BlobUrls';
|
||||||
|
|
||||||
export const runtime = 'edge';
|
export const runtime = 'edge';
|
||||||
|
|
||||||
@ -48,12 +43,10 @@ export default async function AdminTagsPage({
|
|||||||
const [
|
const [
|
||||||
photos,
|
photos,
|
||||||
count,
|
count,
|
||||||
blobUploadUrls,
|
|
||||||
blobPhotoUrls,
|
blobPhotoUrls,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
getPhotosCached({ includeHidden: true, sortBy: 'createdAt', limit }),
|
getPhotosCached({ includeHidden: true, sortBy: 'createdAt', limit }),
|
||||||
getPhotosCountIncludingHiddenCached(),
|
getPhotosCountIncludingHiddenCached(),
|
||||||
getBlobUploadUrlsCached(),
|
|
||||||
DEBUG_PHOTO_BLOBS ? getBlobPhotoUrlsCached() : [],
|
DEBUG_PHOTO_BLOBS ? getBlobPhotoUrlsCached() : [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -78,16 +71,16 @@ export default async function AdminTagsPage({
|
|||||||
</SubmitButtonWithStatus>
|
</SubmitButtonWithStatus>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{blobUploadUrls.length > 0 &&
|
|
||||||
<BlobUrls
|
|
||||||
blobUrls={blobUploadUrls}
|
|
||||||
label={`Uploads Files (${blobUploadUrls.length})`}
|
|
||||||
/>}
|
|
||||||
{blobPhotoUrls.length > 0 &&
|
{blobPhotoUrls.length > 0 &&
|
||||||
<BlobUrls
|
<div className={cc(
|
||||||
blobUrls={blobPhotoUrls}
|
'border-b pb-6',
|
||||||
label={`Photos Files (${blobPhotoUrls.length})`}
|
'border-gray-200 dark:border-gray-700',
|
||||||
/>}
|
)}>
|
||||||
|
<BlobUrls
|
||||||
|
title={`Photo Blobs (${blobPhotoUrls.length})`}
|
||||||
|
urls={blobPhotoUrls}
|
||||||
|
/>
|
||||||
|
</div>}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<AdminGrid>
|
<AdminGrid>
|
||||||
{photos.map(photo =>
|
{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>;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import PhotoForm from '@/photo/PhotoForm';
|
|||||||
import { ExifParserFactory } from 'ts-exif-parser';
|
import { ExifParserFactory } from 'ts-exif-parser';
|
||||||
import { convertExifToFormData } from '@/photo/form';
|
import { convertExifToFormData } from '@/photo/form';
|
||||||
import AdminChildPage from '@/components/AdminChildPage';
|
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 {
|
interface Params {
|
||||||
params: { uploadPath: string }
|
params: { uploadPath: string }
|
||||||
@ -27,7 +28,11 @@ export default async function UploadPage({ params: { uploadPath } }: Params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminChildPage>
|
<AdminChildPage
|
||||||
|
backPath={PATH_ADMIN_UPLOADS}
|
||||||
|
backLabel="Uploads"
|
||||||
|
breadcrumb={getIdFromBlobUrl(url)}
|
||||||
|
>
|
||||||
{data
|
{data
|
||||||
? <PhotoForm
|
? <PhotoForm
|
||||||
initialPhotoForm={{
|
initialPhotoForm={{
|
||||||
|
|||||||
12
src/app/(auth-state)/admin/uploads/page.tsx
Normal file
12
src/app/(auth-state)/admin/uploads/page.tsx
Normal 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} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ import {
|
|||||||
} from '@/services/blob';
|
} from '@/services/blob';
|
||||||
import { cc } from '@/utility/css';
|
import { cc } from '@/utility/css';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { pathForAdminUploadUrl } from '@/site/paths';
|
||||||
|
|
||||||
export default function PhotoUploadInput() {
|
export default function PhotoUploadInput() {
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
@ -38,7 +39,7 @@ export default function PhotoUploadInput() {
|
|||||||
// relevant only when a photo isn't added
|
// relevant only when a photo isn't added
|
||||||
router.refresh();
|
router.refresh();
|
||||||
// Redirect to photo detail page
|
// Redirect to photo detail page
|
||||||
router.push(`/admin/uploads/${encodeURIComponent(url)}`);
|
router.push(pathForAdminUploadUrl(url));
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
|
|||||||
@ -88,6 +88,10 @@ export async function deleteBlobPhotoAction(formData: FormData) {
|
|||||||
await deleteBlobPhoto(formData.get('url') as string);
|
await deleteBlobPhoto(formData.get('url') as string);
|
||||||
|
|
||||||
revalidateBlobKey();
|
revalidateBlobKey();
|
||||||
|
|
||||||
|
if (formData.get('redirectToPhotos') === 'true') {
|
||||||
|
redirect(PATH_ADMIN_PHOTOS);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function syncCacheAction() {
|
export async function syncCacheAction() {
|
||||||
|
|||||||
@ -23,12 +23,20 @@ const REGEX_UPLOAD_PATH = new RegExp(
|
|||||||
'i',
|
'i',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const REGEX_UPLOAD_ID = new RegExp(
|
||||||
|
`.${PREFIX_UPLOAD}-([a-z0-9]+)\.[a-z]{1,4}$`,
|
||||||
|
'i',
|
||||||
|
);
|
||||||
|
|
||||||
export const pathForBlobUrl = (url: string) =>
|
export const pathForBlobUrl = (url: string) =>
|
||||||
url.replace(`${BLOB_BASE_URL}/`, '');
|
url.replace(`${BLOB_BASE_URL}/`, '');
|
||||||
|
|
||||||
export const getExtensionFromBlobUrl = (url: string) =>
|
export const getExtensionFromBlobUrl = (url: string) =>
|
||||||
url.match(/.([a-z]{1,4})$/i)?.[1];
|
url.match(/.([a-z]{1,4})$/i)?.[1];
|
||||||
|
|
||||||
|
export const getIdFromBlobUrl = (url: string) =>
|
||||||
|
url.match(REGEX_UPLOAD_ID)?.[1];
|
||||||
|
|
||||||
export const isUploadPathnameValid = (pathname?: string) =>
|
export const isUploadPathnameValid = (pathname?: string) =>
|
||||||
pathname?.match(REGEX_UPLOAD_PATH);
|
pathname?.match(REGEX_UPLOAD_PATH);
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,7 @@ export const PATH_CHECKLIST = '/checklist';
|
|||||||
|
|
||||||
// Admin paths
|
// Admin paths
|
||||||
export const PATH_ADMIN_PHOTOS = `${PATH_ADMIN}/photos`;
|
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_TAGS = `${PATH_ADMIN}/tags`;
|
||||||
export const PATH_ADMIN_UPLOAD = `${PATH_ADMIN}/uploads`;
|
export const PATH_ADMIN_UPLOAD = `${PATH_ADMIN}/uploads`;
|
||||||
export const PATH_ADMIN_UPLOAD_BLOB = `${PATH_ADMIN_UPLOAD}/blob`;
|
export const PATH_ADMIN_UPLOAD_BLOB = `${PATH_ADMIN_UPLOAD}/blob`;
|
||||||
@ -45,6 +46,9 @@ export const pathForGrid = (next?: number) =>
|
|||||||
export const pathForAdminPhotos = (next?: number) =>
|
export const pathForAdminPhotos = (next?: number) =>
|
||||||
pathWithNext(PATH_ADMIN_PHOTOS, next);
|
pathWithNext(PATH_ADMIN_PHOTOS, next);
|
||||||
|
|
||||||
|
export const pathForAdminUploadUrl = (url: string) =>
|
||||||
|
`${PATH_ADMIN_UPLOADS}/${encodeURIComponent(url)}`;
|
||||||
|
|
||||||
export const pathForAdminPhotoEdit = (photo: PhotoOrPhotoId) =>
|
export const pathForAdminPhotoEdit = (photo: PhotoOrPhotoId) =>
|
||||||
`${PATH_ADMIN_PHOTOS}/${getPhotoId(photo)}/${EDIT}`;
|
`${PATH_ADMIN_PHOTOS}/${getPhotoId(photo)}/${EDIT}`;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user