Refine outdated photo view
This commit is contained in:
parent
141b7e3ed8
commit
48b1751b12
@ -4,15 +4,22 @@ import PhotoUpload from '@/photo/PhotoUpload';
|
||||
import { PATH_ADMIN_PHOTOS } from '@/site/paths';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { FaArrowRight } from 'react-icons/fa';
|
||||
|
||||
export default function AdminCTA() {
|
||||
const { isUserSignedIn } = useAppState();
|
||||
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex justify-center pt-4">
|
||||
{isUserSignedIn
|
||||
? <PhotoUpload showUploadStatus={false} />
|
||||
? <PhotoUpload
|
||||
showUploadStatus={false}
|
||||
isUploading={isUploading}
|
||||
setIsUploading={setIsUploading}
|
||||
/>
|
||||
: <Link
|
||||
href={PATH_ADMIN_PHOTOS}
|
||||
className="button primary"
|
||||
|
||||
86
src/admin/AdminPhotosClient.tsx
Normal file
86
src/admin/AdminPhotosClient.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
|
||||
import PhotoUpload from '@/photo/PhotoUpload';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import SiteGrid from '@/components/SiteGrid';
|
||||
import AdminUploadsTable from '@/admin/AdminUploadsTable';
|
||||
import { AI_TEXT_GENERATION_ENABLED, PRO_MODE_ENABLED } from '@/site/config';
|
||||
import AdminPhotosTable from '@/admin/AdminPhotosTable';
|
||||
import AdminPhotosTableInfinite from '@/admin/AdminPhotosTableInfinite';
|
||||
import PathLoaderButton from '@/components/primitives/PathLoaderButton';
|
||||
import { PATH_ADMIN_OUTDATED } from '@/site/paths';
|
||||
import { Photo } from '@/photo';
|
||||
import { StorageListResponse } from '@/services/storage';
|
||||
import { useState } from 'react';
|
||||
import { FaRegClock } from 'react-icons/fa6';
|
||||
|
||||
export default function AdminPhotosClient({
|
||||
photos,
|
||||
photosCount,
|
||||
photosCountOutdated,
|
||||
onLastPhotoUpload,
|
||||
blobPhotoUrls,
|
||||
infiniteScrollInitial,
|
||||
infiniteScrollMultiple,
|
||||
}: {
|
||||
photos: Photo[]
|
||||
photosCount: number
|
||||
photosCountOutdated: number
|
||||
onLastPhotoUpload: () => Promise<void>
|
||||
blobPhotoUrls: StorageListResponse
|
||||
infiniteScrollInitial: number
|
||||
infiniteScrollMultiple: number
|
||||
}) {
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
return (
|
||||
<SiteGrid
|
||||
contentMain={
|
||||
<div className="space-y-4">
|
||||
<div className="flex">
|
||||
<div className="grow">
|
||||
<PhotoUpload
|
||||
shouldResize={!PRO_MODE_ENABLED}
|
||||
isUploading={isUploading}
|
||||
setIsUploading={setIsUploading}
|
||||
onLastUpload={onLastPhotoUpload}
|
||||
/>
|
||||
</div>
|
||||
{photosCountOutdated > 0 && <PathLoaderButton
|
||||
path={PATH_ADMIN_OUTDATED}
|
||||
icon={<FaRegClock size={15} className="translate-y-[1px]" />}
|
||||
title={`${photosCountOutdated} Outdated Photos`}
|
||||
className={clsx(
|
||||
isUploading && 'hidden md:inline-flex',
|
||||
)}
|
||||
hideTextOnMobile={false}
|
||||
>
|
||||
{photosCountOutdated}
|
||||
</PathLoaderButton>}
|
||||
</div>
|
||||
{!isUploading && blobPhotoUrls.length > 0 &&
|
||||
<div className={clsx(
|
||||
'border-b pb-6',
|
||||
'border-gray-200 dark:border-gray-700',
|
||||
)}>
|
||||
<AdminUploadsTable
|
||||
title={`Photo Blobs (${blobPhotoUrls.length})`}
|
||||
urls={blobPhotoUrls}
|
||||
/>
|
||||
</div>}
|
||||
<div className="space-y-4">
|
||||
<AdminPhotosTable
|
||||
photos={photos}
|
||||
hasAiTextGeneration={AI_TEXT_GENERATION_ENABLED}
|
||||
/>
|
||||
{photosCount > photos.length &&
|
||||
<AdminPhotosTableInfinite
|
||||
initialOffset={infiniteScrollInitial}
|
||||
itemsPerPage={infiniteScrollMultiple}
|
||||
hasAiTextGeneration={AI_TEXT_GENERATION_ENABLED}
|
||||
/>}
|
||||
</div>
|
||||
</div>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -25,11 +25,15 @@ export default function AdminPhotosTable({
|
||||
onLastPhotoVisible,
|
||||
revalidatePhoto,
|
||||
hasAiTextGeneration,
|
||||
canEdit = true,
|
||||
canDelete = true,
|
||||
}: {
|
||||
photos: Photo[],
|
||||
onLastPhotoVisible?: () => void
|
||||
revalidatePhoto?: RevalidatePhoto
|
||||
hasAiTextGeneration?: boolean
|
||||
canEdit?: boolean
|
||||
canDelete?: boolean
|
||||
}) {
|
||||
const { invalidateSwr } = useAppState();
|
||||
|
||||
@ -82,7 +86,8 @@ export default function AdminPhotosTable({
|
||||
'flex flex-nowrap',
|
||||
'gap-2 sm:gap-3 items-center',
|
||||
)}>
|
||||
<EditButton path={pathForAdminPhotoEdit(photo)} />
|
||||
{canEdit &&
|
||||
<EditButton path={pathForAdminPhotoEdit(photo)} />}
|
||||
<PhotoSyncButton
|
||||
action={syncPhotoAction}
|
||||
photoTitle={titleForPhoto(photo)}
|
||||
@ -92,15 +97,16 @@ export default function AdminPhotosTable({
|
||||
shouldConfirm
|
||||
shouldToast
|
||||
/>
|
||||
<FormWithConfirm
|
||||
action={deletePhotoFormAction}
|
||||
confirmText={deleteConfirmationTextForPhoto(photo)}
|
||||
onSubmit={() => revalidatePhoto?.(photo.id, true)}
|
||||
>
|
||||
<input type="hidden" name="id" value={photo.id} />
|
||||
<input type="hidden" name="url" value={photo.url} />
|
||||
<DeleteButton clearLocalState />
|
||||
</FormWithConfirm>
|
||||
{canDelete &&
|
||||
<FormWithConfirm
|
||||
action={deletePhotoFormAction}
|
||||
confirmText={deleteConfirmationTextForPhoto(photo)}
|
||||
onSubmit={() => revalidatePhoto?.(photo.id, true)}
|
||||
>
|
||||
<input type="hidden" name="id" value={photo.id} />
|
||||
<input type="hidden" name="url" value={photo.url} />
|
||||
<DeleteButton clearLocalState />
|
||||
</FormWithConfirm>}
|
||||
</div>
|
||||
</Fragment>)}
|
||||
</AdminTable>
|
||||
|
||||
@ -3,16 +3,18 @@
|
||||
import { PATH_ADMIN_PHOTOS } from '@/site/paths';
|
||||
import InfinitePhotoScroll from '../photo/InfinitePhotoScroll';
|
||||
import AdminPhotosTable from './AdminPhotosTable';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
export default function AdminPhotosTableInfinite({
|
||||
initialOffset,
|
||||
itemsPerPage,
|
||||
hasAiTextGeneration,
|
||||
canEdit,
|
||||
canDelete,
|
||||
}: {
|
||||
initialOffset: number
|
||||
itemsPerPage: number
|
||||
hasAiTextGeneration?: boolean
|
||||
}) {
|
||||
} & Omit<ComponentProps<typeof AdminPhotosTable>, 'photos'>) {
|
||||
return (
|
||||
<InfinitePhotoScroll
|
||||
cacheKey={`page-${PATH_ADMIN_PHOTOS}`}
|
||||
@ -27,6 +29,8 @@ export default function AdminPhotosTableInfinite({
|
||||
onLastPhotoVisible={onLastPhotoVisible}
|
||||
revalidatePhoto={revalidatePhoto}
|
||||
hasAiTextGeneration={hasAiTextGeneration}
|
||||
canEdit={canEdit}
|
||||
canDelete={canDelete}
|
||||
/>}
|
||||
</InfinitePhotoScroll>
|
||||
);
|
||||
|
||||
@ -54,6 +54,8 @@ export default async function AdminPhotosPage() {
|
||||
<AdminPhotosTable
|
||||
photos={photos}
|
||||
hasAiTextGeneration={AI_TEXT_GENERATION_ENABLED}
|
||||
canEdit={false}
|
||||
canDelete={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,19 +1,9 @@
|
||||
import PhotoUpload from '@/photo/PhotoUpload';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import SiteGrid from '@/components/SiteGrid';
|
||||
import AdminUploadsTable from '@/admin/AdminUploadsTable';
|
||||
import { AI_TEXT_GENERATION_ENABLED, PRO_MODE_ENABLED } from '@/site/config';
|
||||
import { getStoragePhotoUrlsNoStore } from '@/services/storage/cache';
|
||||
import { getPhotos } from '@/photo/db/query';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import AdminPhotosTable from '@/admin/AdminPhotosTable';
|
||||
import AdminPhotosTableInfinite from
|
||||
'@/admin/AdminPhotosTableInfinite';
|
||||
import { getPhotosMetaCached } from '@/photo/cache';
|
||||
import { IoInformationCircleOutline } from 'react-icons/io5';
|
||||
import PathLoaderButton from '@/components/primitives/PathLoaderButton';
|
||||
import { PATH_ADMIN_OUTDATED } from '@/site/paths';
|
||||
import { OUTDATED_THRESHOLD } from '@/photo';
|
||||
import AdminPhotosClient from '@/admin/AdminPhotosClient';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
const DEBUG_PHOTO_BLOBS = false;
|
||||
|
||||
@ -47,54 +37,18 @@ export default async function AdminPhotosPage() {
|
||||
]);
|
||||
|
||||
return (
|
||||
<SiteGrid
|
||||
contentMain={
|
||||
<div className="space-y-4">
|
||||
<div className="flex">
|
||||
<div className="grow">
|
||||
<PhotoUpload
|
||||
shouldResize={!PRO_MODE_ENABLED}
|
||||
onLastUpload={async () => {
|
||||
'use server';
|
||||
// Update upload count in admin nav
|
||||
revalidatePath('/admin', 'layout');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{photosCountOutdated > 0 && <PathLoaderButton
|
||||
path={PATH_ADMIN_OUTDATED}
|
||||
icon={<IoInformationCircleOutline
|
||||
size={18}
|
||||
/>}
|
||||
title={`${photosCountOutdated} Outdated Photos`}
|
||||
hideTextOnMobile={false}
|
||||
>
|
||||
{photosCountOutdated}
|
||||
</PathLoaderButton>}
|
||||
</div>
|
||||
{blobPhotoUrls.length > 0 &&
|
||||
<div className={clsx(
|
||||
'border-b pb-6',
|
||||
'border-gray-200 dark:border-gray-700',
|
||||
)}>
|
||||
<AdminUploadsTable
|
||||
title={`Photo Blobs (${blobPhotoUrls.length})`}
|
||||
urls={blobPhotoUrls}
|
||||
/>
|
||||
</div>}
|
||||
<div className="space-y-4">
|
||||
<AdminPhotosTable
|
||||
photos={photos}
|
||||
hasAiTextGeneration={AI_TEXT_GENERATION_ENABLED}
|
||||
/>
|
||||
{photosCount > photos.length &&
|
||||
<AdminPhotosTableInfinite
|
||||
initialOffset={INFINITE_SCROLL_INITIAL_ADMIN_PHOTOS}
|
||||
itemsPerPage={INFINITE_SCROLL_MULTIPLE_ADMIN_PHOTOS}
|
||||
hasAiTextGeneration={AI_TEXT_GENERATION_ENABLED}
|
||||
/>}
|
||||
</div>
|
||||
</div>}
|
||||
/>
|
||||
<AdminPhotosClient {...{
|
||||
photos,
|
||||
photosCount,
|
||||
photosCountOutdated,
|
||||
onLastPhotoUpload: async () => {
|
||||
'use server';
|
||||
// Update upload count in admin nav
|
||||
revalidatePath('/admin', 'layout');
|
||||
},
|
||||
blobPhotoUrls,
|
||||
infiniteScrollInitial: INFINITE_SCROLL_INITIAL_ADMIN_PHOTOS,
|
||||
infiniteScrollMultiple: INFINITE_SCROLL_MULTIPLE_ADMIN_PHOTOS,
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -10,15 +10,18 @@ import { clsx } from 'clsx/lite';
|
||||
export default function PhotoUpload({
|
||||
shouldResize,
|
||||
onLastUpload,
|
||||
isUploading,
|
||||
setIsUploading,
|
||||
showUploadStatus,
|
||||
debug,
|
||||
}: {
|
||||
shouldResize?: boolean
|
||||
onLastUpload?: () => Promise<void>
|
||||
isUploading: boolean
|
||||
setIsUploading: (isUploading: boolean) => void
|
||||
showUploadStatus?: boolean
|
||||
debug?: boolean
|
||||
}) {
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadError, setUploadError] = useState<string>();
|
||||
const [debugDownload, setDebugDownload] = useState<{
|
||||
href: string
|
||||
|
||||
Loading…
Reference in New Issue
Block a user