diff --git a/app/admin/uploads/page.tsx b/app/admin/uploads/page.tsx index 68727e31..a4d48fe6 100644 --- a/app/admin/uploads/page.tsx +++ b/app/admin/uploads/page.tsx @@ -1,6 +1,6 @@ import { getStorageUploadUrlsNoStore } from '@/platforms/storage/cache'; import SiteGrid from '@/components/SiteGrid'; -import { getUniqueTagsCached } from '@/photo/cache'; +import { getUniqueTagsCached, getUniqueRecipesCached } from '@/photo/cache'; import AdminUploadsClient from '@/admin/AdminUploadsClient'; import { redirect } from 'next/navigation'; import { PATH_ADMIN_PHOTOS } from '@/app/paths'; @@ -10,6 +10,7 @@ export const maxDuration = 60; export default async function AdminUploadsPage() { const urls = await getStorageUploadUrlsNoStore(); const uniqueTags = await getUniqueTagsCached(); + const uniqueRecipes = await getUniqueRecipesCached(); if (urls.length === 0) { redirect(PATH_ADMIN_PHOTOS); @@ -20,6 +21,7 @@ export default async function AdminUploadsPage() { } /> ); diff --git a/src/admin/AddButton.tsx b/src/admin/AddButton.tsx index 7feb9b4b..8f426100 100644 --- a/src/admin/AddButton.tsx +++ b/src/admin/AddButton.tsx @@ -7,7 +7,10 @@ export default function AddButton( return ( } + icon={} > Add diff --git a/src/admin/AdminUploadsClient.tsx b/src/admin/AdminUploadsClient.tsx index 062e4257..cc3cccb6 100644 --- a/src/admin/AdminUploadsClient.tsx +++ b/src/admin/AdminUploadsClient.tsx @@ -1,14 +1,13 @@ 'use client'; -import { StorageListResponse } from '@/platforms/storage'; +import { StorageListItem, StorageListResponse } from '@/platforms/storage'; import AdminBatchUploadActions from './AdminBatchUploadActions'; import { useMemo, useState } from 'react'; import { Tags } from '@/tag'; import AdminUploadsTable from './AdminUploadsTable'; +import { Recipes } from '@/recipe'; -export type UrlAddStatus = { - url: string - uploadedAt?: Date +export type UrlAddStatus = StorageListItem & { status?: 'waiting' | 'adding' | 'added' statusMessage?: string progress?: number @@ -20,6 +19,7 @@ export default function AdminUploadsClient({ }: { urls: StorageListResponse uniqueTags?: Tags + uniqueRecipes?: Recipes }) { const [isAdding, setIsAdding] = useState(false); const [urlAddStatuses, setUrlAddStatuses] = useState(urls); diff --git a/src/admin/AdminUploadsTable.tsx b/src/admin/AdminUploadsTable.tsx index 54a1375b..091dfe98 100644 --- a/src/admin/AdminUploadsTable.tsx +++ b/src/admin/AdminUploadsTable.tsx @@ -2,7 +2,7 @@ import ImageSmall from '@/components/image/ImageSmall'; import Spinner from '@/components/Spinner'; -import { getIdFromStorageUrl } from '@/platforms/storage'; +import { getExtensionFromStorageUrl } from '@/platforms/storage'; import { clsx } from 'clsx/lite'; import { FaRegCircleCheck } from 'react-icons/fa6'; import { pathForAdminUploadUrl } from '@/app/paths'; @@ -26,7 +26,7 @@ export default function AdminUploadsTable({ return (
- {urlAddStatuses.map(({ url, status, statusMessage, uploadedAt }) => + {urlAddStatuses.map(({ url, status, statusMessage, uploadedAt, size }) =>
-
- {getIdFromStorageUrl(url)} +
+ {uploadedAt + ? + : '—'}
{isAdding || isComplete @@ -66,9 +68,10 @@ export default function AdminUploadsTable({ : status === 'adding' ? statusMessage ?? 'Adding ...' : 'Waiting' - : uploadedAt - ? - : '—'} + : size + // eslint-disable-next-line max-len + ? `${size} ${getExtensionFromStorageUrl(url)?.toUpperCase()}` + : getExtensionFromStorageUrl(url)?.toUpperCase()}
diff --git a/src/platforms/storage/aws-s3.ts b/src/platforms/storage/aws-s3.ts index 63884a68..af418912 100644 --- a/src/platforms/storage/aws-s3.ts +++ b/src/platforms/storage/aws-s3.ts @@ -6,6 +6,7 @@ import { PutObjectCommand, } from '@aws-sdk/client-s3'; import { StorageListResponse, generateStorageId } from '.'; +import { formatBytesToMB } from '@/utility/number'; const AWS_S3_BUCKET = process.env.NEXT_PUBLIC_AWS_S3_BUCKET ?? ''; const AWS_S3_REGION = process.env.NEXT_PUBLIC_AWS_S3_REGION ?? ''; @@ -70,10 +71,11 @@ export const awsS3List = async ( Bucket: AWS_S3_BUCKET, Prefix, })) - .then((data) => data.Contents?.map(({ Key, LastModified }) => ({ + .then((data) => data.Contents?.map(({ Key, LastModified, Size }) => ({ url: urlForKey(Key), fileName: Key ?? '', uploadedAt: LastModified, + size: Size ? formatBytesToMB(Size) : undefined, })) ?? []); export const awsS3Delete = async (Key: string) => { diff --git a/src/platforms/storage/cloudflare-r2.ts b/src/platforms/storage/cloudflare-r2.ts index 6276a47a..4d2d83b3 100644 --- a/src/platforms/storage/cloudflare-r2.ts +++ b/src/platforms/storage/cloudflare-r2.ts @@ -7,6 +7,7 @@ import { } from '@aws-sdk/client-s3'; import { StorageListResponse, generateStorageId } from '.'; import { removeUrlProtocol } from '@/utility/url'; +import { formatBytesToMB } from '@/utility/number'; const CLOUDFLARE_R2_BUCKET = process.env.NEXT_PUBLIC_CLOUDFLARE_R2_BUCKET ?? ''; @@ -90,10 +91,11 @@ export const cloudflareR2List = async ( Bucket: CLOUDFLARE_R2_BUCKET, Prefix, })) - .then((data) => data.Contents?.map(({ Key, LastModified }) => ({ + .then((data) => data.Contents?.map(({ Key, LastModified, Size }) => ({ url: urlForKey(Key), fileName: Key ?? '', uploadedAt: LastModified, + size: Size ? formatBytesToMB(Size) : undefined, })) ?? []); export const cloudflareR2Delete = async (Key: string) => { diff --git a/src/platforms/storage/index.ts b/src/platforms/storage/index.ts index 63af85d4..f581ceba 100644 --- a/src/platforms/storage/index.ts +++ b/src/platforms/storage/index.ts @@ -33,11 +33,14 @@ import { PATH_API_PRESIGNED_URL } from '@/app/paths'; export const generateStorageId = () => generateNanoid(16); -export type StorageListResponse = { +export type StorageListItem = { url: string fileName: string uploadedAt?: Date -}[]; + size?: string +}; + +export type StorageListResponse = StorageListItem[]; export type StorageType = 'vercel-blob' | diff --git a/src/platforms/storage/vercel-blob.ts b/src/platforms/storage/vercel-blob.ts index 2fdc49d8..69289211 100644 --- a/src/platforms/storage/vercel-blob.ts +++ b/src/platforms/storage/vercel-blob.ts @@ -1,7 +1,8 @@ import { PATH_API_VERCEL_BLOB_UPLOAD } from '@/app/paths'; import { copy, del, list, put } from '@vercel/blob'; import { upload } from '@vercel/blob/client'; -import { fileNameForStorageUrl } from '.'; +import { fileNameForStorageUrl, StorageListResponse } from '.'; +import { formatBytesToMB } from '@/utility/number'; const VERCEL_BLOB_STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match( /^vercel_blob_rw_([a-z0-9]+)_[a-z0-9]+$/i, @@ -56,9 +57,12 @@ export const vercelBlobCopy = ( export const vercelBlobDelete = (fileName: string) => del(fileName); -export const vercelBlobList = (prefix: string) => list({ prefix }) - .then(({ blobs }) => blobs.map(({ url, uploadedAt }) => ({ +export const vercelBlobList = ( + prefix: string, +): Promise => list({ prefix }) + .then(({ blobs }) => blobs.map(({ url, uploadedAt, size }) => ({ url, fileName: fileNameForStorageUrl(url), uploadedAt, + size: formatBytesToMB(size), }))); diff --git a/src/utility/number.ts b/src/utility/number.ts index 30a3388d..6c2ac38c 100644 --- a/src/utility/number.ts +++ b/src/utility/number.ts @@ -84,3 +84,6 @@ export const formatNumberToFraction = (number: number) => { return `${sign}${integer}${decimalFormatted}`; } }; + +export const formatBytesToMB = (bytes: number) => + `${(bytes / 1024 / 1024).toFixed(2)}MB`;