Add guidance around sync buttons

This commit is contained in:
Sam Becker 2025-03-07 08:39:40 -08:00
parent 333f1b99d7
commit 21ed815cba
5 changed files with 78 additions and 55 deletions

View File

@ -6,7 +6,10 @@ import { revalidatePath } from 'next/cache';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import { TIMEZONE_COOKIE_NAME } from '@/utility/timezone'; import { TIMEZONE_COOKIE_NAME } from '@/utility/timezone';
import { getOutdatedPhotosCount } from '@/photo/db/query'; import { getOutdatedPhotosCount } from '@/photo/db/query';
import { PRESERVE_ORIGINAL_UPLOADS } from '@/app/config'; import {
AI_TEXT_GENERATION_ENABLED,
PRESERVE_ORIGINAL_UPLOADS,
} from '@/app/config';
export const maxDuration = 60; export const maxDuration = 60;
@ -45,6 +48,7 @@ export default async function AdminPhotosPage() {
photosCount, photosCount,
photosCountOutdated, photosCountOutdated,
shouldResize: !PRESERVE_ORIGINAL_UPLOADS, shouldResize: !PRESERVE_ORIGINAL_UPLOADS,
hasAiTextGeneration: AI_TEXT_GENERATION_ENABLED,
onLastUpload: async () => { onLastUpload: async () => {
'use server'; 'use server';
// Update upload count in admin nav // Update upload count in admin nav

View File

@ -2,9 +2,6 @@
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';
import SiteGrid from '@/components/SiteGrid'; import SiteGrid from '@/components/SiteGrid';
import {
AI_TEXT_GENERATION_ENABLED,
} from '@/app/config';
import AdminPhotosTable from '@/admin/AdminPhotosTable'; import AdminPhotosTable from '@/admin/AdminPhotosTable';
import AdminPhotosTableInfinite from '@/admin/AdminPhotosTableInfinite'; import AdminPhotosTableInfinite from '@/admin/AdminPhotosTableInfinite';
import PathLoaderButton from '@/components/primitives/PathLoaderButton'; import PathLoaderButton from '@/components/primitives/PathLoaderButton';
@ -23,6 +20,7 @@ export default function AdminPhotosClient({
photosCountOutdated, photosCountOutdated,
blobPhotoUrls, blobPhotoUrls,
shouldResize, shouldResize,
hasAiTextGeneration,
onLastUpload, onLastUpload,
infiniteScrollInitial, infiniteScrollInitial,
infiniteScrollMultiple, infiniteScrollMultiple,
@ -33,6 +31,7 @@ export default function AdminPhotosClient({
photosCountOutdated: number photosCountOutdated: number
blobPhotoUrls: StorageListResponse blobPhotoUrls: StorageListResponse
shouldResize: boolean shouldResize: boolean
hasAiTextGeneration: boolean
onLastUpload: () => Promise<void> onLastUpload: () => Promise<void>
infiniteScrollInitial: number infiniteScrollInitial: number
infiniteScrollMultiple: number infiniteScrollMultiple: number
@ -89,14 +88,14 @@ export default function AdminPhotosClient({
<div className="space-y-[6px] sm:space-y-[10px]"> <div className="space-y-[6px] sm:space-y-[10px]">
<AdminPhotosTable <AdminPhotosTable
photos={photos} photos={photos}
hasAiTextGeneration={AI_TEXT_GENERATION_ENABLED} hasAiTextGeneration={hasAiTextGeneration}
timezone={timezone} timezone={timezone}
/> />
{photosCount > photos.length && {photosCount > photos.length &&
<AdminPhotosTableInfinite <AdminPhotosTableInfinite
initialOffset={infiniteScrollInitial} initialOffset={infiniteScrollInitial}
itemsPerPage={infiniteScrollMultiple} itemsPerPage={infiniteScrollMultiple}
hasAiTextGeneration={AI_TEXT_GENERATION_ENABLED} hasAiTextGeneration={hasAiTextGeneration}
timezone={timezone} timezone={timezone}
/>} />}
</div> </div>

View File

@ -2,6 +2,7 @@
import LoaderButton from '@/components/primitives/LoaderButton'; import LoaderButton from '@/components/primitives/LoaderButton';
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus'; import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
import Tooltip from '@/components/Tooltip';
import { getExifDataAction } from '@/photo/actions'; import { getExifDataAction } from '@/photo/actions';
import { PhotoFormData } from '@/photo/form'; import { PhotoFormData } from '@/photo/form';
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';
@ -18,23 +19,27 @@ export default function ExifCaptureButton({
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
return ( return (
<LoaderButton <Tooltip
title="Update photo from original file" content="Refresh form with EXIF data from original file"
isLoading={isLoading} supportMobile={false}
onClick={() => {
setIsLoading(true);
getExifDataAction(photoUrl)
.then(onSync)
.finally(() => setIsLoading(false));
}}
icon={<LuDatabaseBackup
size={16}
className={clsx(
'translate-y-[0.5px] translate-x-[0.5px]',
'sm:translate-x-[-0.5px]',
)} />}
> >
EXIF <LoaderButton
</LoaderButton> isLoading={isLoading}
onClick={() => {
setIsLoading(true);
getExifDataAction(photoUrl)
.then(onSync)
.finally(() => setIsLoading(false));
}}
icon={<LuDatabaseBackup
size={16}
className={clsx(
'translate-y-[0.5px] translate-x-[0.5px]',
'sm:translate-x-[-0.5px]',
)} />}
>
EXIF
</LoaderButton>
</Tooltip>
); );
} }

View File

@ -3,6 +3,7 @@ import { syncPhotoAction } from '@/photo/actions';
import IconGrSync from '@/app/IconGrSync'; import IconGrSync from '@/app/IconGrSync';
import { toastSuccess } from '@/toast'; import { toastSuccess } from '@/toast';
import { ComponentProps, useState } from 'react'; import { ComponentProps, useState } from 'react';
import Tooltip from '@/components/Tooltip';
export default function PhotoSyncButton({ export default function PhotoSyncButton({
photoId, photoId,
@ -33,29 +34,33 @@ export default function PhotoSyncButton({
confirmText.push('This action cannot be undone.'); confirmText.push('This action cannot be undone.');
return ( return (
<LoaderButton <Tooltip
title="Update photo from original file" content="Regenerate photo data"
className={className} supportMobile={false}
icon={<IconGrSync >
className="translate-y-[0.5px] translate-x-[0.5px]" <LoaderButton
/>} className={className}
onClick={() => { icon={<IconGrSync
if (!shouldConfirm || window.confirm(confirmText.join(' '))) { className="translate-y-[0.5px] translate-x-[0.5px]"
setIsSyncing(true); />}
syncPhotoAction(photoId) onClick={() => {
.then(() => { if (!shouldConfirm || window.confirm(confirmText.join(' '))) {
onSyncComplete?.(); setIsSyncing(true);
if (shouldToast) { syncPhotoAction(photoId)
toastSuccess(photoTitle .then(() => {
? `"${photoTitle}" data synced` onSyncComplete?.();
: 'Data synced'); if (shouldToast) {
} toastSuccess(photoTitle
}) ? `"${photoTitle}" data synced`
.finally(() => setIsSyncing(false)); : 'Data synced');
} }
}} })
isLoading={isSyncing || isSyncingExternal} .finally(() => setIsSyncing(false));
disabled={disabled} }
/> }}
isLoading={isSyncing || isSyncingExternal}
disabled={disabled}
/>
</Tooltip>
); );
} }

View File

@ -10,14 +10,16 @@ import useClickInsideOutside from '@/utility/useClickInsideOutside';
export default function TooltipPrimitive({ export default function TooltipPrimitive({
content, content,
className, className,
classNameTrigger, classNameTrigger: classNameTriggerProp,
sideOffset = 10, sideOffset = 10,
supportMobile = true,
children, children,
}: { }: {
content?: ReactNode content?: ReactNode
className?: string className?: string
classNameTrigger?: string classNameTrigger?: string
sideOffset?: number sideOffset?: number
supportMobile?: boolean
children: ReactNode children: ReactNode
}) { }) {
const refTrigger = useRef<HTMLButtonElement>(null); const refTrigger = useRef<HTMLButtonElement>(null);
@ -27,6 +29,8 @@ export default function TooltipPrimitive({
const supportsHover = useSupportsHover(); const supportsHover = useSupportsHover();
const includeButton = supportMobile && !supportsHover;
useClickInsideOutside({ useClickInsideOutside({
htmlElements: [refTrigger, refContent], htmlElements: [refTrigger, refContent],
onClickOutside: () => { onClickOutside: () => {
@ -34,17 +38,23 @@ export default function TooltipPrimitive({
}, },
}); });
const classNameTrigger = clsx('link cursor-default', classNameTriggerProp);
return ( return (
<Tooltip.Provider delayDuration={100}> <Tooltip.Provider delayDuration={100}>
<Tooltip.Root open={!supportsHover ? isOpen : undefined}> <Tooltip.Root open={!supportsHover ? isOpen : undefined}>
<Tooltip.Trigger asChild> <Tooltip.Trigger asChild>
<button {includeButton
ref={refTrigger} ? <button
onClick={!supportsHover ? () => setIsOpen(!isOpen) : undefined} ref={refTrigger}
className={clsx('link cursor-default', classNameTrigger)} onClick={() => setIsOpen(!isOpen)}
> className={classNameTrigger}
{children} >
</button> {children}
</button>
: <span className={classNameTrigger}>
{children}
</span>}
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Portal > <Tooltip.Portal >
<Tooltip.Content <Tooltip.Content