Introduce timestamp-based swr invalidation
This commit is contained in:
parent
11878f807c
commit
edb4df83b8
103
src/admin/AdminPhotoTable.tsx
Normal file
103
src/admin/AdminPhotoTable.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
|
||||
import { Photo, deleteConfirmationTextForPhoto, titleForPhoto } from '@/photo';
|
||||
import AdminTable from './AdminTable';
|
||||
import { Fragment } from 'react';
|
||||
import PhotoTiny from '@/photo/PhotoTiny';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { pathForAdminPhotoEdit, pathForPhoto } from '@/site/paths';
|
||||
import Link from 'next/link';
|
||||
import { AiOutlineEyeInvisible } from 'react-icons/ai';
|
||||
import PhotoDate from '@/photo/PhotoDate';
|
||||
import FormWithConfirm from '@/components/FormWithConfirm';
|
||||
import EditButton from './EditButton';
|
||||
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
|
||||
import IconGrSync from '@/site/IconGrSync';
|
||||
import DeleteButton from './DeleteButton';
|
||||
import {
|
||||
deletePhotoFormAction,
|
||||
syncPhotoExifDataAction,
|
||||
} from '@/photo/actions';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
|
||||
export default function AdminPhotoTable({
|
||||
photos,
|
||||
}: {
|
||||
photos: Photo[],
|
||||
}) {
|
||||
const { invalidateSwr } = useAppState();
|
||||
|
||||
return (
|
||||
<AdminTable>
|
||||
{photos.map(photo =>
|
||||
<Fragment key={photo.id}>
|
||||
<PhotoTiny photo={photo} />
|
||||
<div className="flex flex-col lg:flex-row">
|
||||
<Link
|
||||
key={photo.id}
|
||||
href={pathForPhoto(photo)}
|
||||
className="lg:w-[50%] flex items-center gap-2"
|
||||
prefetch={false}
|
||||
>
|
||||
<span className={clsx(
|
||||
'inline-flex items-center gap-2',
|
||||
photo.hidden && 'text-dim',
|
||||
)}>
|
||||
<span>{photo.title || 'Untitled'}</span>
|
||||
{photo.hidden &&
|
||||
<AiOutlineEyeInvisible
|
||||
className="translate-y-[0.25px]"
|
||||
size={16}
|
||||
/>}
|
||||
</span>
|
||||
{photo.priorityOrder !== null &&
|
||||
<span className={clsx(
|
||||
'text-xs leading-none px-1.5 py-1 rounded-sm',
|
||||
'dark:text-gray-300',
|
||||
'bg-gray-100 dark:bg-gray-800',
|
||||
)}>
|
||||
{photo.priorityOrder}
|
||||
</span>}
|
||||
</Link>
|
||||
<div className={clsx(
|
||||
'lg:w-[50%] uppercase',
|
||||
'text-dim',
|
||||
)}>
|
||||
<PhotoDate {...{ photo }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx(
|
||||
'flex flex-nowrap',
|
||||
'gap-2 sm:gap-3 items-center',
|
||||
)}>
|
||||
<EditButton href={pathForAdminPhotoEdit(photo)} />
|
||||
<FormWithConfirm
|
||||
action={syncPhotoExifDataAction}
|
||||
confirmText={
|
||||
'Are you sure you want to overwrite EXIF data ' +
|
||||
`for "${titleForPhoto(photo)}" from source file? ` +
|
||||
'This action cannot be undone.'
|
||||
}
|
||||
>
|
||||
<input type="hidden" name="id" value={photo.id} />
|
||||
<SubmitButtonWithStatus
|
||||
icon={<IconGrSync className="translate-y-[-0.5px]" />}
|
||||
onFormSubmitToastMessage={`
|
||||
"${titleForPhoto(photo)}" EXIF data synced
|
||||
`}
|
||||
onFormSubmit={invalidateSwr}
|
||||
/>
|
||||
</FormWithConfirm>
|
||||
<FormWithConfirm
|
||||
action={deletePhotoFormAction}
|
||||
confirmText={deleteConfirmationTextForPhoto(photo)}
|
||||
>
|
||||
<input type="hidden" name="id" value={photo.id} />
|
||||
<input type="hidden" name="url" value={photo.url} />
|
||||
<DeleteButton onFormSubmit={invalidateSwr} />
|
||||
</FormWithConfirm>
|
||||
</div>
|
||||
</Fragment>)}
|
||||
</AdminTable>
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export default function AdminGrid ({
|
||||
export default function AdminTable ({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
40
src/admin/AdminTagBadge.tsx
Normal file
40
src/admin/AdminTagBadge.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import PhotoTag from '@/tag/PhotoTag';
|
||||
import { photoLabelForCount } from '@/photo';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import FavsTag from '@/tag/FavsTag';
|
||||
import { isTagFavs } from '@/tag';
|
||||
import Badge from '@/components/Badge';
|
||||
|
||||
export default function AdminTagBadge({
|
||||
tag,
|
||||
count,
|
||||
hideBadge,
|
||||
}: {
|
||||
tag: string,
|
||||
count: number,
|
||||
hideBadge?: boolean,
|
||||
}) {
|
||||
const renderBadgeContent = () =>
|
||||
<div className={clsx(
|
||||
'inline-flex items-center gap-2',
|
||||
// Fix nested EntityLink-in-Badge quirk for tags
|
||||
'[&>*>*:first-child]:items-center',
|
||||
)}>
|
||||
{isTagFavs(tag)
|
||||
? <FavsTag />
|
||||
: <PhotoTag {...{ tag }} />}
|
||||
<div className="text-dim uppercase">
|
||||
<span>{count}</span>
|
||||
<span className="hidden xs:inline-block">
|
||||
|
||||
{photoLabelForCount(count)}
|
||||
</span>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
return (
|
||||
hideBadge
|
||||
? renderBadgeContent()
|
||||
: <Badge>{renderBadgeContent()}</Badge>
|
||||
);
|
||||
}
|
||||
48
src/admin/AdminTagTable.tsx
Normal file
48
src/admin/AdminTagTable.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import FormWithConfirm from '@/components/FormWithConfirm';
|
||||
import { deletePhotoTagGloballyAction } from '@/photo/actions';
|
||||
import AdminTable from '@/admin/AdminTable';
|
||||
import { Fragment } from 'react';
|
||||
import DeleteButton from '@/admin/DeleteButton';
|
||||
import { photoQuantityText } from '@/photo';
|
||||
import { TagsWithMeta, formatTag, sortTagsObject } from '@/tag';
|
||||
import EditButton from '@/admin/EditButton';
|
||||
import { pathForAdminTagEdit } from '@/site/paths';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import AdminTagBadge from './AdminTagBadge';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
|
||||
export default function AdminTagTable({
|
||||
tags,
|
||||
}: {
|
||||
tags: TagsWithMeta
|
||||
}) {
|
||||
const { invalidateSwr } = useAppState();
|
||||
|
||||
return (
|
||||
<AdminTable>
|
||||
{sortTagsObject(tags).map(({ tag, count }) =>
|
||||
<Fragment key={tag}>
|
||||
<div className="pr-2 col-span-2">
|
||||
<AdminTagBadge {...{ tag, count }} />
|
||||
</div>
|
||||
<div className={clsx(
|
||||
'flex flex-nowrap',
|
||||
'gap-2 sm:gap-3 items-center',
|
||||
)}>
|
||||
<EditButton href={pathForAdminTagEdit(tag)} />
|
||||
<FormWithConfirm
|
||||
action={deletePhotoTagGloballyAction}
|
||||
confirmText={
|
||||
// eslint-disable-next-line max-len
|
||||
`Are you sure you want to remove "${formatTag(tag)}" from ${photoQuantityText(count, false).toLowerCase()}?`}
|
||||
>
|
||||
<input type="hidden" name="tag" value={tag} />
|
||||
<DeleteButton onFormSubmit={invalidateSwr} />
|
||||
</FormWithConfirm>
|
||||
</div>
|
||||
</Fragment>)}
|
||||
</AdminTable>
|
||||
);
|
||||
}
|
||||
@ -1,9 +1,13 @@
|
||||
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { ComponentProps } from 'react';
|
||||
import { BiTrash } from 'react-icons/bi';
|
||||
|
||||
export default function DeleteButton () {
|
||||
export default function DeleteButton (
|
||||
props: ComponentProps<typeof SubmitButtonWithStatus>
|
||||
) {
|
||||
return <SubmitButtonWithStatus
|
||||
{...props}
|
||||
title="Delete"
|
||||
icon={<BiTrash size={16} className="translate-y-[-1.5px]" />}
|
||||
spinnerColor="text"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Fragment } from 'react';
|
||||
import AdminGrid from './AdminGrid';
|
||||
import AdminTable from './AdminTable';
|
||||
import Link from 'next/link';
|
||||
import ImageTiny from '@/components/ImageTiny';
|
||||
import { StorageListResponse, fileNameForStorageUrl } from '@/services/storage';
|
||||
@ -19,7 +19,7 @@ export default function StorageUrls({
|
||||
urls: StorageListResponse
|
||||
}) {
|
||||
return (
|
||||
<AdminGrid {...{ title }} >
|
||||
<AdminTable {...{ title }} >
|
||||
{urls.map(({ url, uploadedAt }) => {
|
||||
const addUploadPath = pathForAdminUploadUrl(url);
|
||||
const uploadFileName = fileNameForStorageUrl(url);
|
||||
@ -70,6 +70,6 @@ export default function StorageUrls({
|
||||
</FormWithConfirm>
|
||||
</div>
|
||||
</Fragment>;})}
|
||||
</AdminGrid>
|
||||
</AdminTable>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,39 +1,20 @@
|
||||
import { Fragment } from 'react';
|
||||
import PhotoUpload from '@/photo/PhotoUpload';
|
||||
import Link from 'next/link';
|
||||
import PhotoTiny from '@/photo/PhotoTiny';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import FormWithConfirm from '@/components/FormWithConfirm';
|
||||
import SiteGrid from '@/components/SiteGrid';
|
||||
import {
|
||||
deletePhotoFormAction,
|
||||
syncPhotoExifDataAction,
|
||||
} from '@/photo/actions';
|
||||
import {
|
||||
pathForAdminPhotos,
|
||||
pathForPhoto,
|
||||
pathForAdminPhotoEdit,
|
||||
} from '@/site/paths';
|
||||
import { deleteConfirmationTextForPhoto, titleForPhoto } from '@/photo';
|
||||
import { pathForAdminPhotos } from '@/site/paths';
|
||||
import { getPhotosCountIncludingHiddenCached } from '@/photo/cache';
|
||||
import { AiOutlineEyeInvisible } from 'react-icons/ai';
|
||||
import {
|
||||
PaginationParams,
|
||||
getPaginationFromSearchParams,
|
||||
} from '@/site/pagination';
|
||||
import AdminGrid from '@/admin/AdminGrid';
|
||||
import DeleteButton from '@/admin/DeleteButton';
|
||||
import EditButton from '@/admin/EditButton';
|
||||
import StorageUrls from '@/admin/StorageUrls';
|
||||
import { PRO_MODE_ENABLED } from '@/site/config';
|
||||
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
|
||||
import IconGrSync from '@/site/IconGrSync';
|
||||
import { getStoragePhotoUrlsNoStore } from '@/services/storage/cache';
|
||||
import MoreComponentsFromSearchParams from
|
||||
'@/components/MoreComponentsFromSearchParams';
|
||||
import { getPhotos } from '@/services/vercel-postgres';
|
||||
import PhotoDate from '@/photo/PhotoDate';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import AdminPhotoTable from '@/admin/AdminPhotoTable';
|
||||
|
||||
const DEBUG_PHOTO_BLOBS = false;
|
||||
|
||||
@ -77,76 +58,7 @@ export default async function AdminPhotosPage({
|
||||
/>
|
||||
</div>}
|
||||
<div className="space-y-4">
|
||||
<AdminGrid>
|
||||
{photos.map(photo =>
|
||||
<Fragment key={photo.id}>
|
||||
<PhotoTiny photo={photo} />
|
||||
<div className="flex flex-col lg:flex-row">
|
||||
<Link
|
||||
key={photo.id}
|
||||
href={pathForPhoto(photo)}
|
||||
className="lg:w-[50%] flex items-center gap-2"
|
||||
prefetch={false}
|
||||
>
|
||||
<span className={clsx(
|
||||
'inline-flex items-center gap-2',
|
||||
photo.hidden && 'text-dim',
|
||||
)}>
|
||||
<span>{photo.title || 'Untitled'}</span>
|
||||
{photo.hidden &&
|
||||
<AiOutlineEyeInvisible
|
||||
className="translate-y-[0.25px]"
|
||||
size={16}
|
||||
/>}
|
||||
</span>
|
||||
{photo.priorityOrder !== null &&
|
||||
<span className={clsx(
|
||||
'text-xs leading-none px-1.5 py-1 rounded-sm',
|
||||
'dark:text-gray-300',
|
||||
'bg-gray-100 dark:bg-gray-800',
|
||||
)}>
|
||||
{photo.priorityOrder}
|
||||
</span>}
|
||||
</Link>
|
||||
<div className={clsx(
|
||||
'lg:w-[50%] uppercase',
|
||||
'text-dim',
|
||||
)}>
|
||||
<PhotoDate {...{ photo }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx(
|
||||
'flex flex-nowrap',
|
||||
'gap-2 sm:gap-3 items-center',
|
||||
)}>
|
||||
<EditButton href={pathForAdminPhotoEdit(photo)} />
|
||||
<FormWithConfirm
|
||||
action={syncPhotoExifDataAction}
|
||||
confirmText={
|
||||
'Are you sure you want to overwrite EXIF data ' +
|
||||
`for "${titleForPhoto(photo)}" from source file? ` +
|
||||
'This action cannot be undone.'
|
||||
}
|
||||
>
|
||||
<input type="hidden" name="id" value={photo.id} />
|
||||
<SubmitButtonWithStatus
|
||||
icon={<IconGrSync className="translate-y-[-0.5px]" />}
|
||||
onFormSubmitToastMessage={`
|
||||
"${titleForPhoto(photo)}" EXIF data synced
|
||||
`}
|
||||
/>
|
||||
</FormWithConfirm>
|
||||
<FormWithConfirm
|
||||
action={deletePhotoFormAction}
|
||||
confirmText={deleteConfirmationTextForPhoto(photo)}
|
||||
>
|
||||
<input type="hidden" name="id" value={photo.id} />
|
||||
<input type="hidden" name="url" value={photo.url} />
|
||||
<DeleteButton />
|
||||
</FormWithConfirm>
|
||||
</div>
|
||||
</Fragment>)}
|
||||
</AdminGrid>
|
||||
<AdminPhotoTable photos={photos} />
|
||||
{showMorePhotos &&
|
||||
<MoreComponentsFromSearchParams
|
||||
label="More photos"
|
||||
|
||||
@ -3,13 +3,9 @@ import { redirect } from 'next/navigation';
|
||||
import { getPhotosCached } from '@/photo/cache';
|
||||
import TagForm from '@/tag/TagForm';
|
||||
import { PATH_ADMIN, PATH_ADMIN_TAGS, pathForTag } from '@/site/paths';
|
||||
import PhotoTag from '@/tag/PhotoTag';
|
||||
import { photoLabelForCount } from '@/photo';
|
||||
import PhotoLightbox from '@/photo/PhotoLightbox';
|
||||
import FavsTag from '@/tag/FavsTag';
|
||||
import { isTagFavs } from '@/tag';
|
||||
import { getPhotosTagMeta } from '@/services/vercel-postgres';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import AdminTagBadge from '@/admin/AdminTagBadge';
|
||||
|
||||
const MAX_PHOTO_TO_SHOW = 6;
|
||||
|
||||
@ -36,22 +32,7 @@ export default async function PhotoPageEdit({
|
||||
<AdminChildPage
|
||||
backPath={PATH_ADMIN_TAGS}
|
||||
backLabel="Tags"
|
||||
breadcrumb={<div className={clsx(
|
||||
'flex items-center gap-2',
|
||||
// Fix nested EntityLink-in-Badge quirk for tags
|
||||
'[&>*>*:first-child]:items-center',
|
||||
)}>
|
||||
{isTagFavs(tag)
|
||||
? <FavsTag />
|
||||
: <PhotoTag {...{ tag }} />}
|
||||
<div className="text-dim uppercase">
|
||||
<span>{count}</span>
|
||||
<span className="hidden xs:inline-block">
|
||||
|
||||
{photoLabelForCount(count)}
|
||||
</span>
|
||||
</div>
|
||||
</div>}
|
||||
breadcrumb={<AdminTagBadge {...{ tag, count, hideBadge: true }} />}
|
||||
>
|
||||
<TagForm {...{ tag, photos }}>
|
||||
<PhotoLightbox
|
||||
|
||||
@ -1,17 +1,6 @@
|
||||
import FormWithConfirm from '@/components/FormWithConfirm';
|
||||
import AdminTagTable from '@/admin/AdminTagTable';
|
||||
import SiteGrid from '@/components/SiteGrid';
|
||||
import { deletePhotoTagGloballyAction } from '@/photo/actions';
|
||||
import AdminGrid from '@/admin/AdminGrid';
|
||||
import { Fragment } from 'react';
|
||||
import DeleteButton from '@/admin/DeleteButton';
|
||||
import { photoQuantityText } from '@/photo';
|
||||
import { getUniqueTagsHiddenCached } from '@/photo/cache';
|
||||
import PhotoTag from '@/tag/PhotoTag';
|
||||
import { formatTag, isTagFavs, sortTagsObject } from '@/tag';
|
||||
import EditButton from '@/admin/EditButton';
|
||||
import { pathForAdminTagEdit } from '@/site/paths';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import FavsTag from '@/tag/FavsTag';
|
||||
|
||||
export default async function AdminTagsPage() {
|
||||
const tags = await getUniqueTagsHiddenCached();
|
||||
@ -21,34 +10,7 @@ export default async function AdminTagsPage() {
|
||||
contentMain={
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<AdminGrid>
|
||||
{sortTagsObject(tags).map(({ tag, count }) =>
|
||||
<Fragment key={tag}>
|
||||
<div className="pr-2 -translate-y-0.5">
|
||||
{isTagFavs(tag)
|
||||
? <FavsTag prefetch={false} />
|
||||
: <PhotoTag {...{ tag, prefetch: false }} />}
|
||||
</div>
|
||||
<div className="text-dim uppercase">
|
||||
{photoQuantityText(count, false)}
|
||||
</div>
|
||||
<div className={clsx(
|
||||
'flex flex-nowrap',
|
||||
'gap-2 sm:gap-3 items-center',
|
||||
)}>
|
||||
<EditButton href={pathForAdminTagEdit(tag)} />
|
||||
<FormWithConfirm
|
||||
action={deletePhotoTagGloballyAction}
|
||||
confirmText={
|
||||
// eslint-disable-next-line max-len
|
||||
`Are you sure you want to remove "${formatTag(tag)}" from ${photoQuantityText(count, false).toLowerCase()}?`}
|
||||
>
|
||||
<input type="hidden" name="tag" value={tag} />
|
||||
<DeleteButton />
|
||||
</FormWithConfirm>
|
||||
</div>
|
||||
</Fragment>)}
|
||||
</AdminGrid>
|
||||
<AdminTagTable {...{ tags }} />
|
||||
</div>
|
||||
</div>}
|
||||
/>
|
||||
|
||||
@ -38,7 +38,6 @@ export default async function GridPage() {
|
||||
<PhotoGrid {...{ photos, photoPriority: true }} />
|
||||
{photos.length >= INFINITE_SCROLL_MULTIPLE_GRID &&
|
||||
<InfinitePhotoScroll
|
||||
swrKey={photos[0].id}
|
||||
type='grid'
|
||||
initialOffset={INFINITE_SCROLL_MULTIPLE_GRID}
|
||||
itemsPerPage={INFINITE_SCROLL_MULTIPLE_GRID}
|
||||
|
||||
@ -33,7 +33,6 @@ export default async function HomePage() {
|
||||
<PhotosLarge {...{ photos }} />
|
||||
{photos.length >= INFINITE_SCROLL_MULTIPLE_HOME &&
|
||||
<InfinitePhotoScroll
|
||||
swrKey={photos[0].id}
|
||||
type="full-frame"
|
||||
initialOffset={INFINITE_SCROLL_MULTIPLE_HOME}
|
||||
itemsPerPage={INFINITE_SCROLL_MULTIPLE_HOME}
|
||||
|
||||
@ -35,12 +35,10 @@ export default function SubmitButtonWithStatus({
|
||||
const pendingPrevious = useRef(pending);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
pendingPrevious.current &&
|
||||
!pending &&
|
||||
onFormSubmitToastMessage
|
||||
) {
|
||||
toastSuccess(onFormSubmitToastMessage);
|
||||
if (pending && !pendingPrevious.current) {
|
||||
if (onFormSubmitToastMessage) {
|
||||
toastSuccess(onFormSubmitToastMessage);
|
||||
}
|
||||
onFormSubmit?.();
|
||||
}
|
||||
pendingPrevious.current = pending;
|
||||
|
||||
@ -10,6 +10,7 @@ import { getPhotosAction } from '@/photo/actions';
|
||||
import { Photo } from '.';
|
||||
import PhotoGrid from './PhotoGrid';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
|
||||
export type RevalidatePhoto = (
|
||||
photoId: string,
|
||||
@ -17,14 +18,12 @@ export type RevalidatePhoto = (
|
||||
) => Promise<any>;
|
||||
|
||||
export default function InfinitePhotoScroll({
|
||||
swrKey,
|
||||
type = 'full-frame',
|
||||
initialOffset = 0,
|
||||
itemsPerPage = 12,
|
||||
prefetch = true,
|
||||
triggerOnView = true,
|
||||
}: {
|
||||
swrKey: string
|
||||
type?: 'full-frame' | 'grid'
|
||||
initialOffset?: number
|
||||
itemsPerPage?: number
|
||||
@ -32,9 +31,15 @@ export default function InfinitePhotoScroll({
|
||||
triggerOnView?: boolean
|
||||
debug?: boolean
|
||||
}) {
|
||||
const key = `${swrKey}-${type}`;
|
||||
const { swrTimestamp } = useAppState();
|
||||
|
||||
const buttonContainerRef = useRef<HTMLDivElement>(null);
|
||||
const key = `${swrTimestamp}-${type}`;
|
||||
|
||||
const keyGenerator = useCallback(
|
||||
(size: number, prev: Photo[]) => prev && prev.length === 0
|
||||
? null
|
||||
: [key, size]
|
||||
, [key]);
|
||||
|
||||
const fetcher = useCallback(([_key, size]: [string, number]) =>
|
||||
getPhotosAction(
|
||||
@ -45,13 +50,13 @@ export default function InfinitePhotoScroll({
|
||||
|
||||
const { data, isLoading, isValidating, error, mutate, size, setSize } =
|
||||
useSwrInfinite<Photo[]>(
|
||||
(size: number, prev: []) => prev && prev.length === 0
|
||||
? null
|
||||
: [key, size],
|
||||
keyGenerator,
|
||||
fetcher,
|
||||
{ revalidateFirstPage: false },
|
||||
);
|
||||
|
||||
const buttonContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isLoadingOrValidating = isLoading || isValidating;
|
||||
|
||||
const isFinished = useMemo(() =>
|
||||
|
||||
@ -72,7 +72,8 @@ export default function PhotoGrid({
|
||||
priority: photoPriority,
|
||||
}} />
|
||||
</div>).concat(additionalTile ?? [])}
|
||||
itemKeys={photos.map(photo => photo.id)}
|
||||
itemKeys={photos.map(photo => photo.id)
|
||||
.concat(additionalTile ? ['more'] : [])}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -29,7 +29,7 @@ import Spinner from '@/components/Spinner';
|
||||
import { getNextImageUrlForRequest } from '@/services/next-image';
|
||||
import useDelay from '@/utility/useDelay';
|
||||
import usePreventNavigation from '@/utility/usePreventNavigation';
|
||||
import useSwrClear from '@/state/useSwrClear';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
|
||||
const THUMBNAIL_SIZE = 300;
|
||||
|
||||
@ -63,6 +63,8 @@ export default function PhotoForm({
|
||||
useState<string>();
|
||||
const [hasBlurData, setHasBlurData] = useState(false);
|
||||
|
||||
const { invalidateSwr } = useAppState();
|
||||
|
||||
const changedFormKeys = useMemo(() =>
|
||||
getChangedFormFields(initialPhotoForm, formData),
|
||||
[initialPhotoForm, formData]);
|
||||
@ -215,8 +217,6 @@ export default function PhotoForm({
|
||||
}
|
||||
};
|
||||
|
||||
const clearSwr = useSwrClear();
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-[38rem] relative">
|
||||
{debugBlur && blurError &&
|
||||
@ -369,7 +369,14 @@ export default function PhotoForm({
|
||||
<SubmitButtonWithStatus
|
||||
disabled={!canFormBeSubmitted}
|
||||
onFormStatusChange={onFormStatusChange}
|
||||
onSubmit={clearSwr}
|
||||
onFormSubmitToastMessage={type === 'edit'
|
||||
? formData.title
|
||||
? `"${formData.title}" updated`
|
||||
: 'Photo updated'
|
||||
: formData.title
|
||||
? `"${formData.title}" created`
|
||||
: 'Photo created'}
|
||||
onFormSubmit={invalidateSwr}
|
||||
primary
|
||||
>
|
||||
{type === 'create' ? 'Create' : 'Update'}
|
||||
|
||||
@ -8,6 +8,7 @@ import { SITE_DOMAIN_OR_TITLE } from '@/site/config';
|
||||
import ViewSwitcher, { SwitcherSelection } from '@/site/ViewSwitcher';
|
||||
import {
|
||||
PATH_ROOT,
|
||||
isPathAdmin,
|
||||
isPathGrid,
|
||||
isPathProtected,
|
||||
isPathSignIn,
|
||||
@ -15,11 +16,7 @@ import {
|
||||
import AnimateItems from '../components/AnimateItems';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
|
||||
export default function Nav({
|
||||
animate = true,
|
||||
}: {
|
||||
animate?: boolean,
|
||||
}) {
|
||||
export default function Nav() {
|
||||
const pathname = usePathname();
|
||||
|
||||
const { isUserSignedIn } = useAppState();
|
||||
@ -49,7 +46,7 @@ export default function Nav({
|
||||
contentMain={
|
||||
<AnimateItems
|
||||
animateOnFirstLoadOnly
|
||||
type={animate ? 'bottom' : 'none'}
|
||||
type={!isPathAdmin(pathname) ? 'bottom' : 'none'}
|
||||
distanceOffset={10}
|
||||
items={showNav
|
||||
? [<div
|
||||
|
||||
@ -4,6 +4,8 @@ import { AnimationConfig } from '@/components/AnimateItems';
|
||||
export interface AppStateContext {
|
||||
previousPathname?: string
|
||||
hasLoaded?: boolean
|
||||
swrTimestamp?: number
|
||||
invalidateSwr?: () => void
|
||||
userEmail?: string
|
||||
setUserEmail?: Dispatch<SetStateAction<string | undefined>>
|
||||
isUserSignedIn?: boolean
|
||||
|
||||
@ -15,6 +15,8 @@ export default function AppStateProvider({
|
||||
|
||||
const [hasLoaded, setHasLoaded] =
|
||||
useState(false);
|
||||
const [swrTimestamp, setSwrTimestamp] =
|
||||
useState(Date.now());
|
||||
const [userEmail, setUserEmail] =
|
||||
useState<string>();
|
||||
const [nextPhotoAnimation, setNextPhotoAnimation] =
|
||||
@ -26,6 +28,8 @@ export default function AppStateProvider({
|
||||
const [shouldShowBaselineGrid, setShouldShowBaselineGrid] =
|
||||
useState(false);
|
||||
|
||||
const invalidateSwr = useCallback(() => setSwrTimestamp(Date.now()), []);
|
||||
|
||||
const captureUser = useCallback(() =>
|
||||
getCurrentUser().then(user => setUserEmail?.(user?.email ?? undefined))
|
||||
, []);
|
||||
@ -40,6 +44,8 @@ export default function AppStateProvider({
|
||||
value={{
|
||||
previousPathname,
|
||||
hasLoaded,
|
||||
swrTimestamp,
|
||||
invalidateSwr,
|
||||
setHasLoaded,
|
||||
isUserSignedIn: userEmail !== undefined,
|
||||
userEmail,
|
||||
|
||||
@ -7,6 +7,7 @@ import FieldSetWithStatus from '@/components/FieldSetWithStatus';
|
||||
import { ReactNode, useMemo, useState } from 'react';
|
||||
import { renamePhotoTagGloballyAction } from '@/photo/actions';
|
||||
import { parameterize } from '@/utility/string';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
|
||||
export default function TagForm({
|
||||
tag,
|
||||
@ -15,6 +16,8 @@ export default function TagForm({
|
||||
tag: string
|
||||
children?: ReactNode
|
||||
}) {
|
||||
const { invalidateSwr } = useAppState();
|
||||
|
||||
const [updatedTagRaw, setUpdatedTagRaw] = useState(tag);
|
||||
|
||||
const updatedTag = useMemo(() =>
|
||||
@ -61,6 +64,7 @@ export default function TagForm({
|
||||
</Link>
|
||||
<SubmitButtonWithStatus
|
||||
disabled={!isFormValid}
|
||||
onFormSubmit={invalidateSwr}
|
||||
>
|
||||
Update
|
||||
</SubmitButtonWithStatus>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user