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 {
|
||||
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">
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
@ -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={{
|
||||
|
||||
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';
|
||||
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);
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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}`;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user