Move batch editing to pure url-based state
This commit is contained in:
parent
91f99508f7
commit
1f499e697e
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -50,12 +50,14 @@
|
||||
"parameterizes",
|
||||
"presigner",
|
||||
"Provia",
|
||||
"pushstate",
|
||||
"qaub",
|
||||
"QRSTUVWXYZ",
|
||||
"ratelimit",
|
||||
"ratelimiter",
|
||||
"Reala",
|
||||
"recents",
|
||||
"replacestate",
|
||||
"skippable",
|
||||
"sonner",
|
||||
"sslmode",
|
||||
|
||||
@ -20,7 +20,6 @@ import Nav from '@/app/Nav';
|
||||
import Footer from '@/app/Footer';
|
||||
import CommandK from '@/cmdk/CommandK';
|
||||
import SwrConfigClient from '@/swr/SwrConfigClient';
|
||||
import AdminBatchEditPanel from '@/admin/AdminBatchEditPanel';
|
||||
import ShareModals from '@/share/ShareModals';
|
||||
import AdminUploadPanel from '@/admin/upload/AdminUploadPanel';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
@ -29,6 +28,8 @@ import ThemeColors from '@/app/ThemeColors';
|
||||
import AppTextProvider from '@/i18n/state/AppTextProvider';
|
||||
import SharedHoverProvider from '@/components/shared-hover/SharedHoverProvider';
|
||||
import { PATH_FEED_JSON, PATH_RSS_XML } from '@/app/path';
|
||||
import SelectPhotosProvider from '@/admin/select/SelectPhotosProvider';
|
||||
import AdminBatchEditPanel from '@/admin/select/AdminBatchEditPanel';
|
||||
|
||||
import '../tailwind.css';
|
||||
|
||||
@ -94,51 +95,53 @@ export default function RootLayout({
|
||||
)}>
|
||||
<AppStateProvider areAdminDebugToolsEnabled={ADMIN_DEBUG_TOOLS_ENABLED}>
|
||||
<AppTextProvider>
|
||||
<ThemeColors />
|
||||
<ThemeProvider attribute="class" defaultTheme={DEFAULT_THEME}>
|
||||
<SwrConfigClient>
|
||||
<SharedHoverProvider>
|
||||
<div className={clsx(
|
||||
'mx-3 mb-3',
|
||||
'lg:mx-6 lg:mb-6',
|
||||
)}>
|
||||
<Nav />
|
||||
<main>
|
||||
<ShareModals />
|
||||
<RecipeModal />
|
||||
<div className={clsx(
|
||||
'min-h-[16rem] sm:min-h-[30rem]',
|
||||
'mb-12',
|
||||
'space-y-5',
|
||||
)}>
|
||||
<AdminUploadPanel
|
||||
shouldResize={!PRESERVE_ORIGINAL_UPLOADS}
|
||||
onLastUpload={async () => {
|
||||
'use server';
|
||||
// Update upload count in admin nav
|
||||
revalidatePath('/admin', 'layout');
|
||||
}}
|
||||
/>
|
||||
<AdminBatchEditPanel
|
||||
onBatchActionComplete={async () => {
|
||||
'use server';
|
||||
// Update upload count in admin nav
|
||||
revalidatePath('/admin', 'layout');
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
<CommandK />
|
||||
</SharedHoverProvider>
|
||||
</SwrConfigClient>
|
||||
<Analytics debug={false} />
|
||||
<SpeedInsights debug={false} />
|
||||
<PhotoEscapeHandler />
|
||||
<ToasterWithThemes />
|
||||
</ThemeProvider>
|
||||
<SelectPhotosProvider>
|
||||
<ThemeColors />
|
||||
<ThemeProvider attribute="class" defaultTheme={DEFAULT_THEME}>
|
||||
<SwrConfigClient>
|
||||
<SharedHoverProvider>
|
||||
<div className={clsx(
|
||||
'mx-3 mb-3',
|
||||
'lg:mx-6 lg:mb-6',
|
||||
)}>
|
||||
<Nav />
|
||||
<main>
|
||||
<ShareModals />
|
||||
<RecipeModal />
|
||||
<div className={clsx(
|
||||
'min-h-[16rem] sm:min-h-[30rem]',
|
||||
'mb-12',
|
||||
'space-y-5',
|
||||
)}>
|
||||
<AdminUploadPanel
|
||||
shouldResize={!PRESERVE_ORIGINAL_UPLOADS}
|
||||
onLastUpload={async () => {
|
||||
'use server';
|
||||
// Update upload count in admin nav
|
||||
revalidatePath('/admin', 'layout');
|
||||
}}
|
||||
/>
|
||||
<AdminBatchEditPanel
|
||||
onBatchActionComplete={async () => {
|
||||
'use server';
|
||||
// Update upload count in admin nav
|
||||
revalidatePath('/admin', 'layout');
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
<CommandK />
|
||||
</SharedHoverProvider>
|
||||
</SwrConfigClient>
|
||||
<Analytics debug={false} />
|
||||
<SpeedInsights debug={false} />
|
||||
<PhotoEscapeHandler />
|
||||
<ToasterWithThemes />
|
||||
</ThemeProvider>
|
||||
</SelectPhotosProvider>
|
||||
</AppTextProvider>
|
||||
</AppStateProvider>
|
||||
</body>
|
||||
|
||||
@ -15,7 +15,7 @@ import { IoArrowDown, IoArrowUp } from 'react-icons/io5';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import AdminAppInfoIcon from './AdminAppInfoIcon';
|
||||
import { signOutAction } from '@/auth/actions';
|
||||
import { ComponentProps, useEffect, useMemo } from 'react';
|
||||
import { ComponentProps, useMemo } from 'react';
|
||||
import useIsKeyBeingPressed from '@/utility/useIsKeyBeingPressed';
|
||||
import IconPhoto from '@/components/icons/IconPhoto';
|
||||
import IconUpload from '@/components/icons/IconUpload';
|
||||
@ -31,8 +31,8 @@ import Spinner from '@/components/Spinner';
|
||||
import { useAppText } from '@/i18n/state/client';
|
||||
import SwitcherItemMenu from '@/components/switcher/SwitcherItemMenu';
|
||||
import { MoreMenuSection } from '@/components/more/MoreMenu';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { FiXSquare } from 'react-icons/fi';
|
||||
import { useSelectPhotosState } from './select/SelectPhotosState';
|
||||
|
||||
export default function AdminAppMenu({
|
||||
isOpen,
|
||||
@ -41,30 +41,24 @@ export default function AdminAppMenu({
|
||||
isOpen?: boolean
|
||||
setIsOpen?: (isOpen: boolean) => void
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
photosCountTotal = 0,
|
||||
photosCountNeedSync = 0,
|
||||
uploadsCount = 0,
|
||||
tagsCount = 0,
|
||||
recipesCount = 0,
|
||||
selectedPhotoIds,
|
||||
isLoadingAdminData,
|
||||
startUpload,
|
||||
setSelectedPhotoIds,
|
||||
refreshAdminData,
|
||||
clearAuthStateAndRedirectIfNecessary,
|
||||
} = useAppState();
|
||||
|
||||
const isSelecting = selectedPhotoIds !== undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelecting) {
|
||||
setSelectedPhotoIds?.(undefined);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname, setSelectedPhotoIds]);
|
||||
const {
|
||||
canCurrentPageSelectPhotos,
|
||||
isSelectingPhotos,
|
||||
startSelectingPhotos,
|
||||
stopSelectingPhotos,
|
||||
} = useSelectPhotosState();
|
||||
|
||||
const appText = useAppText();
|
||||
|
||||
@ -72,9 +66,6 @@ export default function AdminAppMenu({
|
||||
|
||||
const showAppInsightsLink = photosCountTotal > 0 && !isAltPressed;
|
||||
|
||||
const currentPageHasGrid = () =>
|
||||
document.querySelector('[data-photo-grid]') !== null;
|
||||
|
||||
const sectionUpload: MoreMenuSection = useMemo(() => ({ items: [{
|
||||
label: appText.admin.uploadPhotos,
|
||||
icon: <IconUpload
|
||||
@ -157,10 +148,10 @@ export default function AdminAppMenu({
|
||||
}
|
||||
if (photosCountTotal) {
|
||||
items.push({
|
||||
label: isSelecting
|
||||
label: isSelectingPhotos
|
||||
? appText.admin.batchExitEdit
|
||||
: appText.admin.batchEditShort,
|
||||
icon: isSelecting
|
||||
icon: isSelectingPhotos
|
||||
? <FiXSquare
|
||||
size={15}
|
||||
className="translate-x-[-0.75px] translate-y-[0.5px]"
|
||||
@ -169,16 +160,12 @@ export default function AdminAppMenu({
|
||||
size={16}
|
||||
className="translate-x-[-0.5px] translate-y-[0.5px]"
|
||||
/>,
|
||||
...!currentPageHasGrid() && {
|
||||
...!canCurrentPageSelectPhotos && {
|
||||
href: `${PATH_GRID_INFERRED}?batch=true`,
|
||||
},
|
||||
action: () => {
|
||||
if (isSelecting) {
|
||||
setSelectedPhotoIds?.(undefined);
|
||||
} else {
|
||||
setSelectedPhotoIds?.([]);
|
||||
}
|
||||
},
|
||||
action: isSelectingPhotos
|
||||
? stopSelectingPhotos
|
||||
: startSelectingPhotos,
|
||||
});
|
||||
}
|
||||
items.push({
|
||||
@ -197,11 +184,13 @@ export default function AdminAppMenu({
|
||||
return { items };
|
||||
}, [
|
||||
appText,
|
||||
isSelecting,
|
||||
canCurrentPageSelectPhotos,
|
||||
isSelectingPhotos,
|
||||
startSelectingPhotos,
|
||||
stopSelectingPhotos,
|
||||
photosCountNeedSync,
|
||||
photosCountTotal,
|
||||
recipesCount,
|
||||
setSelectedPhotoIds,
|
||||
showAppInsightsLink,
|
||||
tagsCount,
|
||||
uploadsCount,
|
||||
|
||||
@ -3,21 +3,21 @@
|
||||
import Note from '@/components/Note';
|
||||
import LoaderButton from '@/components/primitives/LoaderButton';
|
||||
import AppGrid from '@/components/AppGrid';
|
||||
import { useAppState } from '@/app/AppState';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { IoCloseSharp } from 'react-icons/io5';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { TAG_FAVS, Tags } from '@/tag';
|
||||
import PhotoTagFieldset from './PhotoTagFieldset';
|
||||
import PhotoTagFieldset from '@/admin/PhotoTagFieldset';
|
||||
import { tagMultiplePhotosAction } from '@/photo/actions';
|
||||
import { toastSuccess } from '@/toast';
|
||||
import DeletePhotosButton from './DeletePhotosButton';
|
||||
import DeletePhotosButton from '@/admin/DeletePhotosButton';
|
||||
import { photoQuantityText } from '@/photo';
|
||||
import { FaArrowDown, FaCheck } from 'react-icons/fa6';
|
||||
import ResponsiveText from '@/components/primitives/ResponsiveText';
|
||||
import IconFavs from '@/components/icons/IconFavs';
|
||||
import IconTag from '@/components/icons/IconTag';
|
||||
import { useAppText } from '@/i18n/state/client';
|
||||
import { useSelectPhotosState } from './SelectPhotosState';
|
||||
|
||||
export default function AdminBatchEditPanelClient({
|
||||
uniqueTags,
|
||||
@ -27,12 +27,14 @@ export default function AdminBatchEditPanelClient({
|
||||
const refNote = useRef<HTMLDivElement>(null);
|
||||
|
||||
const {
|
||||
isUserSignedIn,
|
||||
canCurrentPageSelectPhotos,
|
||||
isSelectingPhotos,
|
||||
stopSelectingPhotos,
|
||||
selectedPhotoIds,
|
||||
setSelectedPhotoIds,
|
||||
isPerformingSelectEdit,
|
||||
setIsPerformingSelectEdit,
|
||||
} = useAppState();
|
||||
} = useSelectPhotosState();
|
||||
|
||||
const appText = useAppText();
|
||||
|
||||
@ -41,7 +43,7 @@ export default function AdminBatchEditPanelClient({
|
||||
const isInTagMode = tags !== undefined;
|
||||
|
||||
const resetForm = () => {
|
||||
setSelectedPhotoIds?.(undefined);
|
||||
setSelectedPhotoIds?.([]);
|
||||
setTags(undefined);
|
||||
setTagErrorMessage('');
|
||||
};
|
||||
@ -146,21 +148,20 @@ export default function AdminBatchEditPanelClient({
|
||||
</LoaderButton>
|
||||
<LoaderButton
|
||||
icon={<IoCloseSharp size={19} />}
|
||||
onClick={() => setSelectedPhotoIds?.(undefined)}
|
||||
onClick={stopSelectingPhotos}
|
||||
/>
|
||||
</>;
|
||||
|
||||
const shouldShowPanel =
|
||||
isUserSignedIn &&
|
||||
document.querySelector('[data-photo-grid]') !== null &&
|
||||
selectedPhotoIds !== undefined;
|
||||
isSelectingPhotos &&
|
||||
canCurrentPageSelectPhotos;
|
||||
|
||||
useEffect(() => {
|
||||
// Steal focus from Admin Menu to hide tooltip
|
||||
if (shouldShowPanel) {
|
||||
if (isSelectingPhotos) {
|
||||
refNote.current?.focus();
|
||||
}
|
||||
}, [shouldShowPanel]);
|
||||
}, [isSelectingPhotos]);
|
||||
|
||||
return shouldShowPanel
|
||||
? <AppGrid
|
||||
76
src/admin/select/SelectPhotosProvider.tsx
Normal file
76
src/admin/select/SelectPhotosProvider.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
'use client';
|
||||
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { SelectPhotosContext } from './SelectPhotosState';
|
||||
import { PARAM_SELECT, PATH_GRID_INFERRED } from '@/app/path';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useAppState } from '@/app/AppState';
|
||||
import useClientSearchParams from '@/utility/useClientSearchParams';
|
||||
|
||||
export const DATA_KEY_PHOTO_GRID = 'data-photo-grid';
|
||||
|
||||
export default function SelectPhotosProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const { isUserSignedIn } = useAppState();
|
||||
|
||||
const searchParamsSelect = useClientSearchParams(PARAM_SELECT);
|
||||
|
||||
const [canCurrentPageSelectPhotos, setCanCurrentPageSelectPhotos] =
|
||||
useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setCanCurrentPageSelectPhotos(document
|
||||
.querySelector(`[${DATA_KEY_PHOTO_GRID}]`) !== null);
|
||||
}, [pathname]);
|
||||
|
||||
const isSelectingPhotos = useMemo(() =>
|
||||
isUserSignedIn &&
|
||||
searchParamsSelect === 'true'
|
||||
, [isUserSignedIn, searchParamsSelect]);
|
||||
|
||||
const startSelectingPhotos = useCallback(() => {
|
||||
window.history.pushState(
|
||||
null,
|
||||
'',
|
||||
canCurrentPageSelectPhotos
|
||||
? `${pathname}?${PARAM_SELECT}=true`
|
||||
: `${PATH_GRID_INFERRED}?batch=true`,
|
||||
);
|
||||
dispatchEvent(new Event('pushstate'));
|
||||
}, [canCurrentPageSelectPhotos, pathname]);
|
||||
|
||||
const stopSelectingPhotos = useCallback(() => {
|
||||
window.history.pushState(null, '', pathname);
|
||||
dispatchEvent(new Event('pushstate'));
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSelectingPhotos) { setSelectedPhotoIds([]); }
|
||||
}, [isSelectingPhotos]);
|
||||
|
||||
const [selectedPhotoIds, setSelectedPhotoIds] =
|
||||
useState<string[]>([]);
|
||||
|
||||
const [isPerformingSelectEdit, setIsPerformingSelectEdit] =
|
||||
useState(false);
|
||||
|
||||
return (
|
||||
<SelectPhotosContext.Provider value={{
|
||||
canCurrentPageSelectPhotos,
|
||||
isSelectingPhotos,
|
||||
startSelectingPhotos,
|
||||
stopSelectingPhotos,
|
||||
selectedPhotoIds,
|
||||
setSelectedPhotoIds,
|
||||
isPerformingSelectEdit,
|
||||
setIsPerformingSelectEdit,
|
||||
}}>
|
||||
{children}
|
||||
</SelectPhotosContext.Provider>
|
||||
);
|
||||
}
|
||||
16
src/admin/select/SelectPhotosState.ts
Normal file
16
src/admin/select/SelectPhotosState.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { createContext, Dispatch, SetStateAction, use } from 'react';
|
||||
|
||||
export type SelectPhotosState = {
|
||||
canCurrentPageSelectPhotos?: boolean
|
||||
isSelectingPhotos?: boolean;
|
||||
startSelectingPhotos?: () => void
|
||||
stopSelectingPhotos?: () => void
|
||||
selectedPhotoIds?: string[]
|
||||
setSelectedPhotoIds?: (photoIds: string[]) => void
|
||||
isPerformingSelectEdit?: boolean
|
||||
setIsPerformingSelectEdit?: Dispatch<SetStateAction<boolean>>
|
||||
};
|
||||
|
||||
export const SelectPhotosContext = createContext<SelectPhotosState>({});
|
||||
|
||||
export const useSelectPhotosState = () => use(SelectPhotosContext);
|
||||
@ -53,10 +53,6 @@ export type AppStateContextType = {
|
||||
isLoadingAdminData?: boolean
|
||||
refreshAdminData?: () => void
|
||||
updateAdminData?: (updatedData: Partial<AdminData>) => void
|
||||
selectedPhotoIds?: string[]
|
||||
setSelectedPhotoIds?: Dispatch<SetStateAction<string[] | undefined>>
|
||||
isPerformingSelectEdit?: boolean
|
||||
setIsPerformingSelectEdit?: Dispatch<SetStateAction<boolean>>
|
||||
insightsIndicatorStatus?: InsightsIndicatorStatus
|
||||
// UPLOAD
|
||||
startUpload?: () => Promise<boolean>
|
||||
|
||||
@ -93,13 +93,11 @@ export default function AppStateProvider({
|
||||
useState<string>();
|
||||
const [userEmailEager, setUserEmailEager] =
|
||||
useState<string>();
|
||||
const isUserSignedIn = Boolean(userEmail);
|
||||
const isUserSignedInEager = Boolean(userEmailEager);
|
||||
// ADMIN
|
||||
const [adminUpdateTimes, setAdminUpdateTimes] =
|
||||
useState<Date[]>([]);
|
||||
const [selectedPhotoIds, setSelectedPhotoIds] =
|
||||
useState<string[] | undefined>();
|
||||
const [isPerformingSelectEdit, setIsPerformingSelectEdit] =
|
||||
useState(false);
|
||||
// UPLOAD
|
||||
const uploadInputRef = useRef<HTMLInputElement>(null);
|
||||
const [uploadState, _setUploadState] = useState(INITIAL_UPLOAD_STATE);
|
||||
@ -161,8 +159,6 @@ export default function AppStateProvider({
|
||||
setUserEmail(auth?.user?.email ?? undefined);
|
||||
}
|
||||
}, [auth, authError]);
|
||||
const isUserSignedIn = Boolean(userEmail);
|
||||
const isUserSignedInEager = Boolean(userEmailEager);
|
||||
|
||||
const {
|
||||
data: adminData,
|
||||
@ -260,10 +256,6 @@ export default function AppStateProvider({
|
||||
isLoadingAdminData,
|
||||
refreshAdminData,
|
||||
updateAdminData,
|
||||
selectedPhotoIds,
|
||||
setSelectedPhotoIds,
|
||||
isPerformingSelectEdit,
|
||||
setIsPerformingSelectEdit,
|
||||
// UPLOAD
|
||||
uploadInputRef,
|
||||
startUpload,
|
||||
|
||||
@ -85,6 +85,7 @@ export const PATH_API_PRESIGNED_URL = `${PATH_API_STORAGE}/presigned-url`;
|
||||
const EDIT = 'edit';
|
||||
const IMAGE = 'image';
|
||||
export const PARAM_UPLOAD_TITLE = 'title';
|
||||
export const PARAM_SELECT = 'select';
|
||||
|
||||
// Special characters
|
||||
export const MISSING_FIELD = '-';
|
||||
|
||||
@ -93,6 +93,7 @@ import { formatDistanceToNow } from 'date-fns';
|
||||
import IconCheck from '@/components/icons/IconCheck';
|
||||
import { getSortStateFromPath } from '@/photo/sort/path';
|
||||
import IconSort from '@/components/icons/IconSort';
|
||||
import { useSelectPhotosState } from '@/admin/select/SelectPhotosState';
|
||||
|
||||
const DIALOG_TITLE = 'Global Command-K Menu';
|
||||
const DIALOG_DESCRIPTION = 'For searching photos, views, and settings';
|
||||
@ -161,8 +162,6 @@ export default function CommandKClient({
|
||||
uploadsCount,
|
||||
tagsCount,
|
||||
recipesCount,
|
||||
selectedPhotoIds,
|
||||
setSelectedPhotoIds,
|
||||
insightsIndicatorStatus,
|
||||
isGridHighDensity,
|
||||
areZoomControlsShown,
|
||||
@ -182,6 +181,13 @@ export default function CommandKClient({
|
||||
setShouldDebugRecipeOverlays,
|
||||
} = useAppState();
|
||||
|
||||
const {
|
||||
isSelectingPhotos,
|
||||
startSelectingPhotos,
|
||||
stopSelectingPhotos,
|
||||
selectedPhotoIds,
|
||||
} = useSelectPhotosState();
|
||||
|
||||
const {
|
||||
doesPathOfferSort,
|
||||
isSortedByDefault,
|
||||
@ -645,15 +651,10 @@ export default function CommandKClient({
|
||||
: appText.admin.batchExitEdit,
|
||||
annotation: <IconLock narrow />,
|
||||
action: () => {
|
||||
if (selectedPhotoIds === undefined) {
|
||||
const hasGrid = document.querySelector('[data-photo-grid]') !== null;
|
||||
if (!hasGrid) {
|
||||
router.push(`${PATH_GRID_INFERRED}?batch=true`);
|
||||
return;
|
||||
}
|
||||
setSelectedPhotoIds?.([]);
|
||||
if (!isSelectingPhotos) {
|
||||
startSelectingPhotos?.();
|
||||
} else {
|
||||
setSelectedPhotoIds?.(undefined);
|
||||
stopSelectingPhotos?.();
|
||||
}
|
||||
},
|
||||
}, {
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
|
||||
import { clsx } from 'clsx/lite';
|
||||
import SimpleCheckbox from './primitives/SimpleCheckbox';
|
||||
import { useAppState } from '@/app/AppState';
|
||||
import Spinner from './Spinner';
|
||||
import { useSelectPhotosState } from '@/admin/select/SelectPhotosState';
|
||||
|
||||
export default function SelectTileOverlay({
|
||||
isSelected,
|
||||
@ -12,7 +12,7 @@ export default function SelectTileOverlay({
|
||||
isSelected: boolean
|
||||
onSelectChange: () => void
|
||||
}) {
|
||||
const { isPerformingSelectEdit } = useAppState();
|
||||
const { isPerformingSelectEdit } = useSelectPhotosState();
|
||||
|
||||
return (
|
||||
<div className={clsx(
|
||||
|
||||
@ -8,9 +8,10 @@ import AnimateItems from '@/components/AnimateItems';
|
||||
import { GRID_ASPECT_RATIO } from '@/app/config';
|
||||
import { useAppState } from '@/app/AppState';
|
||||
import SelectTileOverlay from '@/components/SelectTileOverlay';
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { ReactNode } from 'react';
|
||||
import { GRID_GAP_CLASSNAME } from '@/components';
|
||||
import { useSelectPhotosState } from '@/admin/select/SelectPhotosState';
|
||||
import { DATA_KEY_PHOTO_GRID } from '@/admin/select/SelectPhotosProvider';
|
||||
|
||||
export default function PhotoGrid({
|
||||
photos,
|
||||
@ -39,28 +40,17 @@ export default function PhotoGrid({
|
||||
onAnimationComplete?: () => void
|
||||
} & PhotoSetCategory) {
|
||||
const {
|
||||
isUserSignedIn,
|
||||
selectedPhotoIds,
|
||||
setSelectedPhotoIds,
|
||||
isGridHighDensity,
|
||||
} = useAppState();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
|
||||
// Check for batch editing parameter on mount
|
||||
useEffect(() => {
|
||||
if (searchParams.get('batch') === 'true') {
|
||||
setSelectedPhotoIds?.([]);
|
||||
// Clean up the URL parameter
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('batch');
|
||||
router.replace(url.pathname + url.search);
|
||||
}
|
||||
}, [searchParams, setSelectedPhotoIds, router]);
|
||||
const {
|
||||
isSelectingPhotos,
|
||||
selectedPhotoIds,
|
||||
setSelectedPhotoIds,
|
||||
} = useSelectPhotosState();
|
||||
|
||||
return (
|
||||
<div data-photo-grid>
|
||||
<div {...{ [DATA_KEY_PHOTO_GRID]: true }}>
|
||||
<AnimateItems
|
||||
className={clsx(
|
||||
'grid',
|
||||
@ -110,7 +100,7 @@ export default function PhotoGrid({
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
{isUserSignedIn && selectedPhotoIds !== undefined &&
|
||||
{isSelectingPhotos &&
|
||||
<SelectTileOverlay
|
||||
isSelected={isSelected}
|
||||
onSelectChange={() => setSelectedPhotoIds?.(isSelected
|
||||
|
||||
24
src/utility/useClientSearchParams.ts
Normal file
24
src/utility/useClientSearchParams.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export default function useClientSearchParams(
|
||||
paramKey: string,
|
||||
): string | undefined {
|
||||
const [paramValue, setParamValue] = useState<string>();
|
||||
|
||||
const captureParam = useCallback(() => {
|
||||
setParamValue(window.location.search.split(`${paramKey}=`)[1]);
|
||||
}, [paramKey]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('popstate', captureParam);
|
||||
window.addEventListener('pushstate', captureParam);
|
||||
window.addEventListener('replacestate', captureParam);
|
||||
return () => {
|
||||
window.removeEventListener('popstate', captureParam);
|
||||
window.removeEventListener('pushstate', captureParam);
|
||||
window.removeEventListener('replacestate', captureParam);
|
||||
};
|
||||
}, [captureParam]);
|
||||
|
||||
return paramValue;
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user