Add guidance around sync buttons
This commit is contained in:
parent
333f1b99d7
commit
21ed815cba
@ -6,7 +6,10 @@ import { revalidatePath } from 'next/cache';
|
||||
import { cookies } from 'next/headers';
|
||||
import { TIMEZONE_COOKIE_NAME } from '@/utility/timezone';
|
||||
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;
|
||||
|
||||
@ -45,6 +48,7 @@ export default async function AdminPhotosPage() {
|
||||
photosCount,
|
||||
photosCountOutdated,
|
||||
shouldResize: !PRESERVE_ORIGINAL_UPLOADS,
|
||||
hasAiTextGeneration: AI_TEXT_GENERATION_ENABLED,
|
||||
onLastUpload: async () => {
|
||||
'use server';
|
||||
// Update upload count in admin nav
|
||||
|
||||
@ -2,9 +2,6 @@
|
||||
|
||||
import { clsx } from 'clsx/lite';
|
||||
import SiteGrid from '@/components/SiteGrid';
|
||||
import {
|
||||
AI_TEXT_GENERATION_ENABLED,
|
||||
} from '@/app/config';
|
||||
import AdminPhotosTable from '@/admin/AdminPhotosTable';
|
||||
import AdminPhotosTableInfinite from '@/admin/AdminPhotosTableInfinite';
|
||||
import PathLoaderButton from '@/components/primitives/PathLoaderButton';
|
||||
@ -23,6 +20,7 @@ export default function AdminPhotosClient({
|
||||
photosCountOutdated,
|
||||
blobPhotoUrls,
|
||||
shouldResize,
|
||||
hasAiTextGeneration,
|
||||
onLastUpload,
|
||||
infiniteScrollInitial,
|
||||
infiniteScrollMultiple,
|
||||
@ -33,6 +31,7 @@ export default function AdminPhotosClient({
|
||||
photosCountOutdated: number
|
||||
blobPhotoUrls: StorageListResponse
|
||||
shouldResize: boolean
|
||||
hasAiTextGeneration: boolean
|
||||
onLastUpload: () => Promise<void>
|
||||
infiniteScrollInitial: number
|
||||
infiniteScrollMultiple: number
|
||||
@ -89,14 +88,14 @@ export default function AdminPhotosClient({
|
||||
<div className="space-y-[6px] sm:space-y-[10px]">
|
||||
<AdminPhotosTable
|
||||
photos={photos}
|
||||
hasAiTextGeneration={AI_TEXT_GENERATION_ENABLED}
|
||||
hasAiTextGeneration={hasAiTextGeneration}
|
||||
timezone={timezone}
|
||||
/>
|
||||
{photosCount > photos.length &&
|
||||
<AdminPhotosTableInfinite
|
||||
initialOffset={infiniteScrollInitial}
|
||||
itemsPerPage={infiniteScrollMultiple}
|
||||
hasAiTextGeneration={AI_TEXT_GENERATION_ENABLED}
|
||||
hasAiTextGeneration={hasAiTextGeneration}
|
||||
timezone={timezone}
|
||||
/>}
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import LoaderButton from '@/components/primitives/LoaderButton';
|
||||
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
|
||||
import Tooltip from '@/components/Tooltip';
|
||||
import { getExifDataAction } from '@/photo/actions';
|
||||
import { PhotoFormData } from '@/photo/form';
|
||||
import { clsx } from 'clsx/lite';
|
||||
@ -18,23 +19,27 @@ export default function ExifCaptureButton({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
return (
|
||||
<LoaderButton
|
||||
title="Update photo from original file"
|
||||
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]',
|
||||
)} />}
|
||||
<Tooltip
|
||||
content="Refresh form with EXIF data from original file"
|
||||
supportMobile={false}
|
||||
>
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { syncPhotoAction } from '@/photo/actions';
|
||||
import IconGrSync from '@/app/IconGrSync';
|
||||
import { toastSuccess } from '@/toast';
|
||||
import { ComponentProps, useState } from 'react';
|
||||
import Tooltip from '@/components/Tooltip';
|
||||
|
||||
export default function PhotoSyncButton({
|
||||
photoId,
|
||||
@ -33,29 +34,33 @@ export default function PhotoSyncButton({
|
||||
confirmText.push('This action cannot be undone.');
|
||||
|
||||
return (
|
||||
<LoaderButton
|
||||
title="Update photo from original file"
|
||||
className={className}
|
||||
icon={<IconGrSync
|
||||
className="translate-y-[0.5px] translate-x-[0.5px]"
|
||||
/>}
|
||||
onClick={() => {
|
||||
if (!shouldConfirm || window.confirm(confirmText.join(' '))) {
|
||||
setIsSyncing(true);
|
||||
syncPhotoAction(photoId)
|
||||
.then(() => {
|
||||
onSyncComplete?.();
|
||||
if (shouldToast) {
|
||||
toastSuccess(photoTitle
|
||||
? `"${photoTitle}" data synced`
|
||||
: 'Data synced');
|
||||
}
|
||||
})
|
||||
.finally(() => setIsSyncing(false));
|
||||
}
|
||||
}}
|
||||
isLoading={isSyncing || isSyncingExternal}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Tooltip
|
||||
content="Regenerate photo data"
|
||||
supportMobile={false}
|
||||
>
|
||||
<LoaderButton
|
||||
className={className}
|
||||
icon={<IconGrSync
|
||||
className="translate-y-[0.5px] translate-x-[0.5px]"
|
||||
/>}
|
||||
onClick={() => {
|
||||
if (!shouldConfirm || window.confirm(confirmText.join(' '))) {
|
||||
setIsSyncing(true);
|
||||
syncPhotoAction(photoId)
|
||||
.then(() => {
|
||||
onSyncComplete?.();
|
||||
if (shouldToast) {
|
||||
toastSuccess(photoTitle
|
||||
? `"${photoTitle}" data synced`
|
||||
: 'Data synced');
|
||||
}
|
||||
})
|
||||
.finally(() => setIsSyncing(false));
|
||||
}
|
||||
}}
|
||||
isLoading={isSyncing || isSyncingExternal}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@ -10,14 +10,16 @@ import useClickInsideOutside from '@/utility/useClickInsideOutside';
|
||||
export default function TooltipPrimitive({
|
||||
content,
|
||||
className,
|
||||
classNameTrigger,
|
||||
classNameTrigger: classNameTriggerProp,
|
||||
sideOffset = 10,
|
||||
supportMobile = true,
|
||||
children,
|
||||
}: {
|
||||
content?: ReactNode
|
||||
className?: string
|
||||
classNameTrigger?: string
|
||||
sideOffset?: number
|
||||
supportMobile?: boolean
|
||||
children: ReactNode
|
||||
}) {
|
||||
const refTrigger = useRef<HTMLButtonElement>(null);
|
||||
@ -27,6 +29,8 @@ export default function TooltipPrimitive({
|
||||
|
||||
const supportsHover = useSupportsHover();
|
||||
|
||||
const includeButton = supportMobile && !supportsHover;
|
||||
|
||||
useClickInsideOutside({
|
||||
htmlElements: [refTrigger, refContent],
|
||||
onClickOutside: () => {
|
||||
@ -34,17 +38,23 @@ export default function TooltipPrimitive({
|
||||
},
|
||||
});
|
||||
|
||||
const classNameTrigger = clsx('link cursor-default', classNameTriggerProp);
|
||||
|
||||
return (
|
||||
<Tooltip.Provider delayDuration={100}>
|
||||
<Tooltip.Root open={!supportsHover ? isOpen : undefined}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
ref={refTrigger}
|
||||
onClick={!supportsHover ? () => setIsOpen(!isOpen) : undefined}
|
||||
className={clsx('link cursor-default', classNameTrigger)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
{includeButton
|
||||
? <button
|
||||
ref={refTrigger}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={classNameTrigger}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
: <span className={classNameTrigger}>
|
||||
{children}
|
||||
</span>}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal >
|
||||
<Tooltip.Content
|
||||
|
||||
Loading…
Reference in New Issue
Block a user