Allow admins to select photos from /grid
This commit is contained in:
parent
7f8e2d7a3d
commit
3f0b9e7b27
35
src/admin/AdminBatchEditPanel.tsx
Normal file
35
src/admin/AdminBatchEditPanel.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
'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';
|
||||
|
||||
export default function AdminBatchEditPanel() {
|
||||
const {
|
||||
isUserSignedIn,
|
||||
selectedPhotoIds = [],
|
||||
setSelectedPhotoIds,
|
||||
} = useAppState();
|
||||
|
||||
return isUserSignedIn && selectedPhotoIds.length > 0
|
||||
? <SiteGrid
|
||||
className="mb-5 sticky top-0 z-10 -mt-2 pt-2"
|
||||
contentMain={<Note
|
||||
color="gray"
|
||||
className={clsx(
|
||||
'backdrop-blur-lg !border-transparent',
|
||||
'!bg-gray-200/70 dark:!bg-gray-800/70'
|
||||
)}
|
||||
cta={<LoaderButton
|
||||
onClick={() => setSelectedPhotoIds?.([])}
|
||||
primary
|
||||
>
|
||||
Clear
|
||||
</LoaderButton>}
|
||||
>
|
||||
{selectedPhotoIds.length} photos selected
|
||||
</Note>} />
|
||||
: null;
|
||||
}
|
||||
@ -17,6 +17,7 @@ import Nav from '@/site/Nav';
|
||||
import Footer from '@/site/Footer';
|
||||
import CommandK from '@/site/CommandK';
|
||||
import SwrConfigClient from '../state/SwrConfigClient';
|
||||
import AdminBatchEditPanel from '@/admin/AdminBatchEditPanel';
|
||||
|
||||
import '../site/globals.css';
|
||||
import '../site/sonner.css';
|
||||
@ -84,6 +85,7 @@ export default function RootLayout({
|
||||
'lg:mx-6 lg:mb-6',
|
||||
)}>
|
||||
<Nav siteDomainOrTitle={SITE_DOMAIN_OR_TITLE} />
|
||||
<AdminBatchEditPanel />
|
||||
<div className={clsx(
|
||||
'min-h-[16rem] sm:min-h-[30rem]',
|
||||
'mb-12',
|
||||
|
||||
@ -10,9 +10,11 @@ export default function Note({
|
||||
color = 'blue',
|
||||
icon,
|
||||
animate,
|
||||
cta,
|
||||
}: {
|
||||
icon?: ReactNode
|
||||
animate?: boolean
|
||||
cta?: ReactNode
|
||||
} & ComponentProps<typeof Container>) {
|
||||
return (
|
||||
<AnimateItems
|
||||
@ -35,9 +37,13 @@ export default function Note({
|
||||
className="translate-x-[0.5px] translate-y-[0.5px]"
|
||||
/>}
|
||||
</span>
|
||||
<span className="text-sm">
|
||||
<span className="text-sm grow">
|
||||
{children}
|
||||
</span>
|
||||
{cta &&
|
||||
<span className="translate-x-1">
|
||||
{cta}
|
||||
</span>}
|
||||
</div>
|
||||
</Container>,
|
||||
]}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { Photo } from '.';
|
||||
import PhotoMedium from './PhotoMedium';
|
||||
import { clsx } from 'clsx/lite';
|
||||
@ -5,6 +7,7 @@ import AnimateItems from '@/components/AnimateItems';
|
||||
import { Camera } from '@/camera';
|
||||
import { FilmSimulation } from '@/simulation';
|
||||
import { GRID_ASPECT_RATIO, HIGH_DENSITY_GRID } from '@/site/config';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
|
||||
export default function PhotoGrid({
|
||||
photos,
|
||||
@ -21,6 +24,7 @@ export default function PhotoGrid({
|
||||
staggerOnFirstLoadOnly = true,
|
||||
additionalTile,
|
||||
small,
|
||||
canSelect,
|
||||
onLastPhotoVisible,
|
||||
onAnimationComplete,
|
||||
}: {
|
||||
@ -38,9 +42,16 @@ export default function PhotoGrid({
|
||||
staggerOnFirstLoadOnly?: boolean
|
||||
additionalTile?: JSX.Element
|
||||
small?: boolean
|
||||
canSelect?: boolean
|
||||
onLastPhotoVisible?: () => void
|
||||
onAnimationComplete?: () => void
|
||||
}) {
|
||||
const {
|
||||
isUserSignedIn,
|
||||
selectedPhotoIds = [],
|
||||
setSelectedPhotoIds,
|
||||
} = useAppState();
|
||||
|
||||
return (
|
||||
<AnimateItems
|
||||
className={clsx(
|
||||
@ -60,12 +71,14 @@ export default function PhotoGrid({
|
||||
animateOnFirstLoadOnly={animateOnFirstLoadOnly}
|
||||
staggerOnFirstLoadOnly={staggerOnFirstLoadOnly}
|
||||
onAnimationComplete={onAnimationComplete}
|
||||
items={photos.map((photo, index) =>
|
||||
<div
|
||||
items={photos.map((photo, index) =>{
|
||||
const isSelected = selectedPhotoIds.includes(photo.id);
|
||||
return <div
|
||||
key={photo.id}
|
||||
className={GRID_ASPECT_RATIO !== 0
|
||||
? 'flex relative overflow-hidden'
|
||||
: undefined}
|
||||
className={clsx(
|
||||
GRID_ASPECT_RATIO !== 0 && 'flex relative overflow-hidden',
|
||||
'group',
|
||||
)}
|
||||
style={{
|
||||
...GRID_ASPECT_RATIO !== 0 && {
|
||||
aspectRatio: GRID_ASPECT_RATIO,
|
||||
@ -87,7 +100,37 @@ export default function PhotoGrid({
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
</div>).concat(additionalTile ?? [])}
|
||||
{canSelect && isUserSignedIn &&
|
||||
<>
|
||||
{/* Admin Select Border */}
|
||||
<div className={clsx(
|
||||
'absolute w-full h-full pointer-events-none',
|
||||
)}>
|
||||
<div
|
||||
className={clsx(
|
||||
'w-full h-full border-black dark:border-white',
|
||||
isSelected && 'border-4',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* Admin Select Action */}
|
||||
<div className="absolute top-0 right-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
className={clsx(
|
||||
'absolute top-2 right-2',
|
||||
!isSelected && 'hidden group-hover:block',
|
||||
)}
|
||||
checked={isSelected}
|
||||
onChange={() => setSelectedPhotoIds?.(isSelected
|
||||
? selectedPhotoIds.filter(id => id !== photo.id)
|
||||
: selectedPhotoIds.concat(photo.id),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>}
|
||||
</div>;
|
||||
}).concat(additionalTile ?? [])}
|
||||
itemKeys={photos.map(photo => photo.id)
|
||||
.concat(additionalTile ? ['more'] : [])}
|
||||
/>
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import SiteGrid from '@/components/SiteGrid';
|
||||
import { Photo } from '.';
|
||||
import PhotoGrid from './PhotoGrid';
|
||||
import PhotoGridInfinite from './PhotoGridInfinite';
|
||||
import { Camera } from '@/camera';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import AnimateItems from '@/components/AnimateItems';
|
||||
import { FilmSimulation } from '@/simulation';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { ComponentProps, useCallback, useState } from 'react';
|
||||
|
||||
export default function PhotoGridContainer({
|
||||
cacheKey,
|
||||
@ -21,18 +18,13 @@ export default function PhotoGridContainer({
|
||||
animateOnFirstLoadOnly,
|
||||
header,
|
||||
sidebar,
|
||||
canSelect,
|
||||
}: {
|
||||
cacheKey: string
|
||||
photos: Photo[]
|
||||
count: number
|
||||
tag?: string
|
||||
camera?: Camera
|
||||
simulation?: FilmSimulation
|
||||
focal?: number
|
||||
animateOnFirstLoadOnly?: boolean
|
||||
header?: JSX.Element
|
||||
sidebar?: JSX.Element
|
||||
}) {
|
||||
} & ComponentProps<typeof PhotoGrid>) {
|
||||
const [
|
||||
shouldAnimateDynamicItems,
|
||||
setShouldAnimateDynamicItems,
|
||||
@ -63,6 +55,7 @@ export default function PhotoGridContainer({
|
||||
focal,
|
||||
animateOnFirstLoadOnly,
|
||||
onAnimationComplete,
|
||||
canSelect,
|
||||
}} />
|
||||
{count > initialOffset &&
|
||||
<PhotoGridInfinite {...{
|
||||
@ -74,6 +67,7 @@ export default function PhotoGridContainer({
|
||||
simulation,
|
||||
focal,
|
||||
animateOnFirstLoadOnly,
|
||||
canSelect,
|
||||
}} />}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { Camera } from '@/camera';
|
||||
import { INFINITE_SCROLL_GRID_MULTIPLE } from '.';
|
||||
import InfinitePhotoScroll from './InfinitePhotoScroll';
|
||||
import PhotoGrid from './PhotoGrid';
|
||||
import { FilmSimulation } from '@/simulation';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
export default function PhotoGridInfinite({
|
||||
cacheKey,
|
||||
@ -15,16 +14,11 @@ export default function PhotoGridInfinite({
|
||||
simulation,
|
||||
focal,
|
||||
animateOnFirstLoadOnly,
|
||||
canSelect,
|
||||
}: {
|
||||
cacheKey: string
|
||||
initialOffset: number
|
||||
canStart?: boolean
|
||||
tag?: string
|
||||
camera?: Camera
|
||||
simulation?: FilmSimulation
|
||||
focal?: number
|
||||
animateOnFirstLoadOnly?: boolean
|
||||
}) {
|
||||
} & Omit<ComponentProps<typeof PhotoGrid>, 'photos'>) {
|
||||
return (
|
||||
<InfinitePhotoScroll
|
||||
cacheKey={cacheKey}
|
||||
@ -44,6 +38,7 @@ export default function PhotoGridInfinite({
|
||||
focal,
|
||||
onLastPhotoVisible,
|
||||
animateOnFirstLoadOnly,
|
||||
canSelect,
|
||||
}} />}
|
||||
</InfinitePhotoScroll>
|
||||
);
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { Tags } from '@/tag';
|
||||
import { Photo } from '.';
|
||||
import { Cameras } from '@/camera';
|
||||
@ -5,6 +7,8 @@ import { FilmSimulations } from '@/simulation';
|
||||
import { PATH_GRID } from '@/site/paths';
|
||||
import PhotoGridSidebar from './PhotoGridSidebar';
|
||||
import PhotoGridContainer from './PhotoGridContainer';
|
||||
import { useEffect } from 'react';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
|
||||
export default function PhotoGridPage({
|
||||
photos,
|
||||
@ -19,6 +23,10 @@ export default function PhotoGridPage({
|
||||
cameras: Cameras
|
||||
simulations: FilmSimulations
|
||||
}) {
|
||||
const { setSelectedPhotoIds } = useAppState();
|
||||
|
||||
useEffect(() => () => setSelectedPhotoIds?.([]), [setSelectedPhotoIds]);
|
||||
|
||||
return (
|
||||
<PhotoGridContainer
|
||||
cacheKey={`page-${PATH_GRID}`}
|
||||
@ -32,6 +40,7 @@ export default function PhotoGridPage({
|
||||
photosCount,
|
||||
}} />
|
||||
</div>}
|
||||
canSelect
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -22,6 +22,8 @@ export interface AppStateContext {
|
||||
adminUpdateTimes?: Date[]
|
||||
registerAdminUpdate?: () => void
|
||||
hiddenPhotosCount?: number
|
||||
selectedPhotoIds?: string[]
|
||||
setSelectedPhotoIds?: Dispatch<SetStateAction<string[]>>
|
||||
// DEBUG
|
||||
arePhotosMatted?: boolean
|
||||
setArePhotosMatted?: Dispatch<SetStateAction<boolean>>
|
||||
|
||||
@ -34,6 +34,8 @@ export default function AppStateProvider({
|
||||
useState<Date[]>([]);
|
||||
const [hiddenPhotosCount, setHiddenPhotosCount] =
|
||||
useState(0);
|
||||
const [selectedPhotoIds, setSelectedPhotoIds] =
|
||||
useState<string[]>([]);
|
||||
// DEBUG
|
||||
const [arePhotosMatted, setArePhotosMatted] =
|
||||
useState(MATTE_PHOTOS);
|
||||
@ -92,6 +94,8 @@ export default function AppStateProvider({
|
||||
adminUpdateTimes,
|
||||
registerAdminUpdate,
|
||||
hiddenPhotosCount,
|
||||
selectedPhotoIds,
|
||||
setSelectedPhotoIds,
|
||||
// DEBUG
|
||||
arePhotosMatted,
|
||||
setArePhotosMatted,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user