Refine batch edit component
This commit is contained in:
parent
e9ead7cc9b
commit
d6e6b5ecaf
@ -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 }} />
|
||||
);
|
||||
}
|
||||
|
||||
96
src/admin/AdminBatchEditPanelClient.tsx
Normal file
96
src/admin/AdminBatchEditPanelClient.tsx
Normal 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;
|
||||
}
|
||||
@ -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';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user