Make upload list friendlier
This commit is contained in:
parent
a494a230b5
commit
1ff7404f6e
@ -1,6 +1,6 @@
|
|||||||
import { getStorageUploadUrlsNoStore } from '@/platforms/storage/cache';
|
import { getStorageUploadUrlsNoStore } from '@/platforms/storage/cache';
|
||||||
import SiteGrid from '@/components/SiteGrid';
|
import SiteGrid from '@/components/SiteGrid';
|
||||||
import { getUniqueTagsCached } from '@/photo/cache';
|
import { getUniqueTagsCached, getUniqueRecipesCached } from '@/photo/cache';
|
||||||
import AdminUploadsClient from '@/admin/AdminUploadsClient';
|
import AdminUploadsClient from '@/admin/AdminUploadsClient';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import { PATH_ADMIN_PHOTOS } from '@/app/paths';
|
import { PATH_ADMIN_PHOTOS } from '@/app/paths';
|
||||||
@ -10,6 +10,7 @@ export const maxDuration = 60;
|
|||||||
export default async function AdminUploadsPage() {
|
export default async function AdminUploadsPage() {
|
||||||
const urls = await getStorageUploadUrlsNoStore();
|
const urls = await getStorageUploadUrlsNoStore();
|
||||||
const uniqueTags = await getUniqueTagsCached();
|
const uniqueTags = await getUniqueTagsCached();
|
||||||
|
const uniqueRecipes = await getUniqueRecipesCached();
|
||||||
|
|
||||||
if (urls.length === 0) {
|
if (urls.length === 0) {
|
||||||
redirect(PATH_ADMIN_PHOTOS);
|
redirect(PATH_ADMIN_PHOTOS);
|
||||||
@ -20,6 +21,7 @@ export default async function AdminUploadsPage() {
|
|||||||
<AdminUploadsClient {...{
|
<AdminUploadsClient {...{
|
||||||
urls,
|
urls,
|
||||||
uniqueTags,
|
uniqueTags,
|
||||||
|
uniqueRecipes,
|
||||||
}} />}
|
}} />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,7 +7,10 @@ export default function AddButton(
|
|||||||
return (
|
return (
|
||||||
<PathLoaderButton
|
<PathLoaderButton
|
||||||
{...props}
|
{...props}
|
||||||
icon={<BiImageAdd size={18} className="translate-x-[1px]" />}
|
icon={<BiImageAdd
|
||||||
|
size={18}
|
||||||
|
className="translate-x-[1px] translate-y-[1px]"
|
||||||
|
/>}
|
||||||
>
|
>
|
||||||
Add
|
Add
|
||||||
</PathLoaderButton>
|
</PathLoaderButton>
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { StorageListResponse } from '@/platforms/storage';
|
import { StorageListItem, StorageListResponse } from '@/platforms/storage';
|
||||||
import AdminBatchUploadActions from './AdminBatchUploadActions';
|
import AdminBatchUploadActions from './AdminBatchUploadActions';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Tags } from '@/tag';
|
import { Tags } from '@/tag';
|
||||||
import AdminUploadsTable from './AdminUploadsTable';
|
import AdminUploadsTable from './AdminUploadsTable';
|
||||||
|
import { Recipes } from '@/recipe';
|
||||||
|
|
||||||
export type UrlAddStatus = {
|
export type UrlAddStatus = StorageListItem & {
|
||||||
url: string
|
|
||||||
uploadedAt?: Date
|
|
||||||
status?: 'waiting' | 'adding' | 'added'
|
status?: 'waiting' | 'adding' | 'added'
|
||||||
statusMessage?: string
|
statusMessage?: string
|
||||||
progress?: number
|
progress?: number
|
||||||
@ -20,6 +19,7 @@ export default function AdminUploadsClient({
|
|||||||
}: {
|
}: {
|
||||||
urls: StorageListResponse
|
urls: StorageListResponse
|
||||||
uniqueTags?: Tags
|
uniqueTags?: Tags
|
||||||
|
uniqueRecipes?: Recipes
|
||||||
}) {
|
}) {
|
||||||
const [isAdding, setIsAdding] = useState(false);
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
const [urlAddStatuses, setUrlAddStatuses] = useState<UrlAddStatus[]>(urls);
|
const [urlAddStatuses, setUrlAddStatuses] = useState<UrlAddStatus[]>(urls);
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import ImageSmall from '@/components/image/ImageSmall';
|
import ImageSmall from '@/components/image/ImageSmall';
|
||||||
import Spinner from '@/components/Spinner';
|
import Spinner from '@/components/Spinner';
|
||||||
import { getIdFromStorageUrl } from '@/platforms/storage';
|
import { getExtensionFromStorageUrl } from '@/platforms/storage';
|
||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import { FaRegCircleCheck } from 'react-icons/fa6';
|
import { FaRegCircleCheck } from 'react-icons/fa6';
|
||||||
import { pathForAdminUploadUrl } from '@/app/paths';
|
import { pathForAdminUploadUrl } from '@/app/paths';
|
||||||
@ -26,7 +26,7 @@ export default function AdminUploadsTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{urlAddStatuses.map(({ url, status, statusMessage, uploadedAt }) =>
|
{urlAddStatuses.map(({ url, status, statusMessage, uploadedAt, size }) =>
|
||||||
<div key={url}>
|
<div key={url}>
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
'flex items-center gap-2 w-full min-h-8',
|
'flex items-center gap-2 w-full min-h-8',
|
||||||
@ -56,8 +56,10 @@ export default function AdminUploadsTable({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="grow min-w-0">
|
<span className="grow min-w-0">
|
||||||
<div className="overflow-hidden text-ellipsis">
|
<div className="truncate">
|
||||||
{getIdFromStorageUrl(url)}
|
{uploadedAt
|
||||||
|
? <ResponsiveDate date={uploadedAt} />
|
||||||
|
: '—'}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-dim overflow-hidden text-ellipsis">
|
<div className="text-dim overflow-hidden text-ellipsis">
|
||||||
{isAdding || isComplete
|
{isAdding || isComplete
|
||||||
@ -66,9 +68,10 @@ export default function AdminUploadsTable({
|
|||||||
: status === 'adding'
|
: status === 'adding'
|
||||||
? statusMessage ?? 'Adding ...'
|
? statusMessage ?? 'Adding ...'
|
||||||
: 'Waiting'
|
: 'Waiting'
|
||||||
: uploadedAt
|
: size
|
||||||
? <ResponsiveDate date={uploadedAt} />
|
// eslint-disable-next-line max-len
|
||||||
: '—'}
|
? `${size} ${getExtensionFromStorageUrl(url)?.toUpperCase()}`
|
||||||
|
: getExtensionFromStorageUrl(url)?.toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
PutObjectCommand,
|
PutObjectCommand,
|
||||||
} from '@aws-sdk/client-s3';
|
} from '@aws-sdk/client-s3';
|
||||||
import { StorageListResponse, generateStorageId } from '.';
|
import { StorageListResponse, generateStorageId } from '.';
|
||||||
|
import { formatBytesToMB } from '@/utility/number';
|
||||||
|
|
||||||
const AWS_S3_BUCKET = process.env.NEXT_PUBLIC_AWS_S3_BUCKET ?? '';
|
const AWS_S3_BUCKET = process.env.NEXT_PUBLIC_AWS_S3_BUCKET ?? '';
|
||||||
const AWS_S3_REGION = process.env.NEXT_PUBLIC_AWS_S3_REGION ?? '';
|
const AWS_S3_REGION = process.env.NEXT_PUBLIC_AWS_S3_REGION ?? '';
|
||||||
@ -70,10 +71,11 @@ export const awsS3List = async (
|
|||||||
Bucket: AWS_S3_BUCKET,
|
Bucket: AWS_S3_BUCKET,
|
||||||
Prefix,
|
Prefix,
|
||||||
}))
|
}))
|
||||||
.then((data) => data.Contents?.map(({ Key, LastModified }) => ({
|
.then((data) => data.Contents?.map(({ Key, LastModified, Size }) => ({
|
||||||
url: urlForKey(Key),
|
url: urlForKey(Key),
|
||||||
fileName: Key ?? '',
|
fileName: Key ?? '',
|
||||||
uploadedAt: LastModified,
|
uploadedAt: LastModified,
|
||||||
|
size: Size ? formatBytesToMB(Size) : undefined,
|
||||||
})) ?? []);
|
})) ?? []);
|
||||||
|
|
||||||
export const awsS3Delete = async (Key: string) => {
|
export const awsS3Delete = async (Key: string) => {
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
} from '@aws-sdk/client-s3';
|
} from '@aws-sdk/client-s3';
|
||||||
import { StorageListResponse, generateStorageId } from '.';
|
import { StorageListResponse, generateStorageId } from '.';
|
||||||
import { removeUrlProtocol } from '@/utility/url';
|
import { removeUrlProtocol } from '@/utility/url';
|
||||||
|
import { formatBytesToMB } from '@/utility/number';
|
||||||
|
|
||||||
const CLOUDFLARE_R2_BUCKET =
|
const CLOUDFLARE_R2_BUCKET =
|
||||||
process.env.NEXT_PUBLIC_CLOUDFLARE_R2_BUCKET ?? '';
|
process.env.NEXT_PUBLIC_CLOUDFLARE_R2_BUCKET ?? '';
|
||||||
@ -90,10 +91,11 @@ export const cloudflareR2List = async (
|
|||||||
Bucket: CLOUDFLARE_R2_BUCKET,
|
Bucket: CLOUDFLARE_R2_BUCKET,
|
||||||
Prefix,
|
Prefix,
|
||||||
}))
|
}))
|
||||||
.then((data) => data.Contents?.map(({ Key, LastModified }) => ({
|
.then((data) => data.Contents?.map(({ Key, LastModified, Size }) => ({
|
||||||
url: urlForKey(Key),
|
url: urlForKey(Key),
|
||||||
fileName: Key ?? '',
|
fileName: Key ?? '',
|
||||||
uploadedAt: LastModified,
|
uploadedAt: LastModified,
|
||||||
|
size: Size ? formatBytesToMB(Size) : undefined,
|
||||||
})) ?? []);
|
})) ?? []);
|
||||||
|
|
||||||
export const cloudflareR2Delete = async (Key: string) => {
|
export const cloudflareR2Delete = async (Key: string) => {
|
||||||
|
|||||||
@ -33,11 +33,14 @@ import { PATH_API_PRESIGNED_URL } from '@/app/paths';
|
|||||||
|
|
||||||
export const generateStorageId = () => generateNanoid(16);
|
export const generateStorageId = () => generateNanoid(16);
|
||||||
|
|
||||||
export type StorageListResponse = {
|
export type StorageListItem = {
|
||||||
url: string
|
url: string
|
||||||
fileName: string
|
fileName: string
|
||||||
uploadedAt?: Date
|
uploadedAt?: Date
|
||||||
}[];
|
size?: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StorageListResponse = StorageListItem[];
|
||||||
|
|
||||||
export type StorageType =
|
export type StorageType =
|
||||||
'vercel-blob' |
|
'vercel-blob' |
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { PATH_API_VERCEL_BLOB_UPLOAD } from '@/app/paths';
|
import { PATH_API_VERCEL_BLOB_UPLOAD } from '@/app/paths';
|
||||||
import { copy, del, list, put } from '@vercel/blob';
|
import { copy, del, list, put } from '@vercel/blob';
|
||||||
import { upload } from '@vercel/blob/client';
|
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(
|
const VERCEL_BLOB_STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match(
|
||||||
/^vercel_blob_rw_([a-z0-9]+)_[a-z0-9]+$/i,
|
/^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 vercelBlobDelete = (fileName: string) => del(fileName);
|
||||||
|
|
||||||
export const vercelBlobList = (prefix: string) => list({ prefix })
|
export const vercelBlobList = (
|
||||||
.then(({ blobs }) => blobs.map(({ url, uploadedAt }) => ({
|
prefix: string,
|
||||||
|
): Promise<StorageListResponse> => list({ prefix })
|
||||||
|
.then(({ blobs }) => blobs.map(({ url, uploadedAt, size }) => ({
|
||||||
url,
|
url,
|
||||||
fileName: fileNameForStorageUrl(url),
|
fileName: fileNameForStorageUrl(url),
|
||||||
uploadedAt,
|
uploadedAt,
|
||||||
|
size: formatBytesToMB(size),
|
||||||
})));
|
})));
|
||||||
|
|||||||
@ -84,3 +84,6 @@ export const formatNumberToFraction = (number: number) => {
|
|||||||
return `${sign}${integer}${decimalFormatted}`;
|
return `${sign}${integer}${decimalFormatted}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const formatBytesToMB = (bytes: number) =>
|
||||||
|
`${(bytes / 1024 / 1024).toFixed(2)}MB`;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user