Refine batch edit component

This commit is contained in:
Sam Becker 2024-07-16 14:10:52 -05:00
parent e9ead7cc9b
commit d6e6b5ecaf
6 changed files with 134 additions and 68 deletions

View File

@ -1,53 +1,9 @@
'use client';
import { getUniqueTagsCached } from '@/photo/cache';
import AdminBatchEditPanelClient from './AdminBatchEditPanelClient';
import Note from '@/components/Note';
import LoaderButton from '@/components/primitives/LoaderButton';
import SiteGrid from '@/components/SiteGrid';
import { useAppState } from '@/state/AppState';
import { clsx } from 'clsx/lite';
import { IoCloseSharp } from 'react-icons/io5';
import DeleteButton from './DeleteButton';
export default function AdminBatchEditPanel() {
const {
isUserSignedIn,
selectedPhotoIds,
setSelectedPhotoIds,
} = useAppState();
return isUserSignedIn && selectedPhotoIds !== undefined
? <SiteGrid
className="sticky top-0 z-10 mb-5 -mt-2 pt-2"
contentMain={<Note
color="gray"
className={clsx(
'backdrop-blur-lg !border-transparent',
'!text-gray-900 dark:!text-gray-100',
'!bg-gray-100/90 dark:!bg-gray-900/70'
)}
cta={<div className="flex gap-2">
{selectedPhotoIds.length > 0 &&
<>
<LoaderButton>
Tag ...
</LoaderButton>
<DeleteButton />
</>}
<LoaderButton
icon={<IoCloseSharp size={20} className="translate-y-[-1.5px]" />}
onClick={() => setSelectedPhotoIds?.(undefined)}
/>
</div>}
hideIcon
>
{selectedPhotoIds.length === 0
? 'Select photos below'
: <>
{selectedPhotoIds.length}
{selectedPhotoIds.length === 1 ? ' photo' : ' photos'}
{' '}
selected
</>}
</Note>} />
: null;
export default async function AdminBatchEditPanel() {
const existingTags = await getUniqueTagsCached();
return (
<AdminBatchEditPanelClient {...{ existingTags }} />
);
}

View File

@ -0,0 +1,96 @@
'use client';
import Note from '@/components/Note';
import LoaderButton from '@/components/primitives/LoaderButton';
import SiteGrid from '@/components/SiteGrid';
import { useAppState } from '@/state/AppState';
import { clsx } from 'clsx/lite';
import { IoCloseSharp } from 'react-icons/io5';
import DeleteButton from './DeleteButton';
import { useState } from 'react';
import TagInput from '@/components/TagInput';
import { convertTagsForForm, Tags } from '@/tag';
export default function AdminBatchEditPanelClient({
existingTags,
}: {
existingTags: Tags
}) {
const {
isUserSignedIn,
selectedPhotoIds,
setSelectedPhotoIds,
} = useAppState();
const [tags, setTags] = useState<string>();
const isTagging = tags !== undefined;
const photosPlural = selectedPhotoIds?.length === 1 ? 'photo' : 'photos';
const renderPhotoText = () => selectedPhotoIds?.length === 0
? 'Select photos below'
: `${selectedPhotoIds?.length ?? 0} ${photosPlural} selected`;
const renderActions = () => isTagging
? <>
<LoaderButton
className="min-h-[2.5rem]"
onClick={() => setTags(undefined)}
>
Cancel
</LoaderButton>
<LoaderButton
className="min-h-[2.5rem]"
// eslint-disable-next-line max-len
confirmText={`Are you sure you want to apply tags to ${selectedPhotoIds?.length} ${photosPlural}? This action cannot be undone.`}
primary
>
Apply Tags
</LoaderButton>
</>
: <>
{(selectedPhotoIds?.length ?? 0) > 0 &&
<>
<LoaderButton onClick={() => setTags('')}>
Tag ...
</LoaderButton>
<DeleteButton />
</>}
<LoaderButton
icon={<IoCloseSharp size={20} className="translate-y-[-1.5px]" />}
onClick={() => setSelectedPhotoIds?.(undefined)}
/>
</>;
return isUserSignedIn && selectedPhotoIds !== undefined
? <SiteGrid
className="sticky top-0 z-10 mb-5 -mt-2 pt-2"
contentMain={<Note
color="gray"
className={clsx(
'min-h-[3.5rem]',
'backdrop-blur-lg !border-transparent',
'!text-gray-900 dark:!text-gray-100',
'!bg-gray-100/90 dark:!bg-gray-900/70',
)}
padding={isTagging ? 'tight-cta-right-left' : 'tight-cta-right'}
cta={<div className="flex items-center gap-2.5">
{renderActions()}
</div>}
spaceChildren={false}
hideIcon
>
{isTagging
? <TagInput
name="tags"
value={tags}
options={convertTagsForForm(existingTags)}
onChange={setTags}
placeholder={`Tag ${selectedPhotoIds?.length} ${photosPlural} ...`}
/>
: <div className="text-base">
{renderPhotoText()}
</div>}
</Note>} />
: null;
}

View File

@ -12,7 +12,12 @@ export default function Container({
children: ReactNode
className?: string
color?: 'gray' | 'blue' | 'red' | 'yellow'
padding?: 'loose' | 'normal' | 'tight'
padding?:
'loose' |
'normal' |
'tight' |
'tight-cta-right' |
'tight-cta-right-left'
centered?: boolean
spaceChildren?: boolean
} ) {
@ -46,6 +51,8 @@ export default function Container({
case 'loose': return 'p-4 md:p-24';
case 'normal': return 'p-4 md:p-8';
case 'tight': return 'py-1.5 px-2.5';
case 'tight-cta-right': return 'py-1.5 pl-2.5 pr-1.5';
case 'tight-cta-right-left': return 'py-1.5 px-1.5';
}
};

View File

@ -4,32 +4,34 @@ import AnimateItems from './AnimateItems';
import { IoInformationCircleOutline } from 'react-icons/io5';
import { clsx } from 'clsx/lite';
export default function Note({
children,
className,
color = 'blue',
icon,
animate,
cta,
hideIcon,
}: {
export default function Note(props: {
icon?: ReactNode
animate?: boolean
cta?: ReactNode
hideIcon?: boolean
} & ComponentProps<typeof Container>) {
const {
icon,
animate,
cta,
hideIcon,
color = 'blue',
padding,
children,
...rest
} = props;
return (
<AnimateItems
type={animate ? 'bottom' : 'none'}
items={[
<Container
key="Banner"
className={className}
centered={false}
padding="tight"
color={color}
padding={padding ?? (cta ? 'tight-cta-right' : 'tight')}
{...rest}
>
<div className="flex items-center gap-2.5 pb-[1px]">
<div className="flex items-center gap-2.5 w-full">
{!hideIcon &&
<span className={clsx(
'w-5 flex justify-center shrink-0',
@ -44,7 +46,7 @@ export default function Note({
{children}
</span>
{cta &&
<span className="translate-x-1">
<span>
{cta}
</span>}
</div>

View File

@ -260,7 +260,8 @@ export default function TagInput({
className={clsx(
'grow !min-w-0 !p-0 -my-2 text-xl',
'!border-none !ring-transparent',
'placeholder:text-dim',
'placeholder:text-dim placeholder:text-[15px]',
'placeholder:translate-y-[-1.5px]',
)}
size={10}
value={inputText}

View File

@ -10,6 +10,7 @@ export default function LoaderButton(props: {
spinnerColor?: SpinnerColor
styleAs?: 'button' | 'link' | 'link-without-hover'
hideTextOnMobile?: boolean
confirmText?: string
shouldPreventDefault?: boolean
primary?: boolean
} & ButtonHTMLAttributes<HTMLButtonElement>) {
@ -20,6 +21,7 @@ export default function LoaderButton(props: {
spinnerColor,
styleAs = 'button',
hideTextOnMobile = true,
confirmText,
shouldPreventDefault,
primary,
type = 'button',
@ -35,7 +37,9 @@ export default function LoaderButton(props: {
type={type}
onClick={e => {
if (shouldPreventDefault) { e.preventDefault(); }
onClick?.(e);
if (!confirmText || confirm(confirmText)) {
onClick?.(e);
}
}}
className={clsx(
...(styleAs !== 'button'