Optimize Next.js 16 behavior (#349)
* Remove unused desktop redirect component * Tweak useEffect/setState interactions * Address more next.js 16 linting * Tweak secret loading * Finish linting setstate/useeffect interactions * Disable ref lint warnings
This commit is contained in:
parent
d061051803
commit
dbf55badf6
@ -18,9 +18,7 @@ const eslintConfig = defineConfig([
|
||||
'@stylistic': stylistic,
|
||||
},
|
||||
rules: {
|
||||
// Temporarily disable during Next.js 16 migration
|
||||
'react-hooks/refs': 'off',
|
||||
'react-hooks/set-state-in-effect': 'warn',
|
||||
'@next/next/no-img-element': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
|
||||
21
package.json
21
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "exif-photo-blog",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint .",
|
||||
@ -10,8 +10,8 @@
|
||||
},
|
||||
"packageManager": "pnpm@10.19.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^2.0.53",
|
||||
"@ai-sdk/rsc": "^1.0.79",
|
||||
"@ai-sdk/openai": "^2.0.54",
|
||||
"@ai-sdk/rsc": "^1.0.81",
|
||||
"@aws-sdk/client-s3": "3.917.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.917.0",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
@ -24,7 +24,7 @@
|
||||
"@vercel/analytics": "^1.5.0",
|
||||
"@vercel/blob": "^2.0.0",
|
||||
"@vercel/speed-insights": "^1.2.0",
|
||||
"ai": "^5.0.79",
|
||||
"ai": "^5.0.81",
|
||||
"camelcase-keys": "^10.0.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@ -37,14 +37,14 @@
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"framer-motion": "^12.23.24",
|
||||
"nanoid": "^5.1.6",
|
||||
"next": "15.5.5",
|
||||
"next": "16.0.0",
|
||||
"next-auth": "5.0.0-beta.29",
|
||||
"next-themes": "^0.4.6",
|
||||
"ol": "^10.6.1",
|
||||
"pg": "^8.16.3",
|
||||
"piexifjs": "^1.0.6",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-openlayers": "^10.5.1",
|
||||
"sanitize-html": "^2.17.0",
|
||||
@ -58,8 +58,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@next/bundle-analyzer": "15.5.5",
|
||||
"@next/eslint-plugin-next": "15.5.5",
|
||||
"@next/bundle-analyzer": "16.0.0",
|
||||
"@next/eslint-plugin-next": "16.0.0",
|
||||
"@stylistic/eslint-plugin": "^5.5.0",
|
||||
"@tailwindcss/postcss": "^4.1.16",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
@ -74,8 +74,7 @@
|
||||
"@types/sanitize-html": "^2.16.0",
|
||||
"cross-fetch": "^4.1.0",
|
||||
"eslint": "9.38.0",
|
||||
"eslint-config-next": "15.5.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-config-next": "16.0.0",
|
||||
"jest": "^30.2.0",
|
||||
"jest-environment-jsdom": "^30.2.0",
|
||||
"postcss": "8.5.6",
|
||||
|
||||
724
pnpm-lock.yaml
generated
724
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -10,7 +10,7 @@ import {
|
||||
PREFIX_TAG,
|
||||
} from './src/app/path';
|
||||
|
||||
export default function middleware(req: NextRequest, res:NextResponse) {
|
||||
export function proxy(req: NextRequest, res:NextResponse) {
|
||||
const pathname = req.nextUrl.pathname;
|
||||
|
||||
if (pathname === PATH_ADMIN) {
|
||||
@ -53,11 +53,10 @@ export default function AdminNavClient({
|
||||
useState(areTimesRecent(updateTimes));
|
||||
|
||||
useEffect(() => {
|
||||
// Check every 5 seconds if update times are recent
|
||||
setHasRecentUpdates(areTimesRecent(updateTimes));
|
||||
// Check every 1 second if update times are recent
|
||||
const interval = setInterval(() =>
|
||||
setHasRecentUpdates(areTimesRecent(updateTimes))
|
||||
, 5_000);
|
||||
, 1_000);
|
||||
return () => clearInterval(interval);
|
||||
}, [updateTimes]);
|
||||
|
||||
|
||||
@ -2,16 +2,19 @@ import { Suspense } from 'react';
|
||||
import { APP_CONFIGURATION } from '@/app/config';
|
||||
import AdminAppConfigurationClient from './AdminAppConfigurationClient';
|
||||
import AdminAppConfigurationServer from './AdminAppConfigurationServer';
|
||||
import { generateAuthSecret } from '@/auth';
|
||||
|
||||
export default function AdminAppConfiguration({
|
||||
export default async function AdminAppConfiguration({
|
||||
simplifiedView,
|
||||
}: {
|
||||
simplifiedView?: boolean
|
||||
}) {
|
||||
const secret = await generateAuthSecret();
|
||||
return (
|
||||
<Suspense fallback={<AdminAppConfigurationClient {...{
|
||||
...APP_CONFIGURATION,
|
||||
isAnalyzingConfiguration: true,
|
||||
secret,
|
||||
simplifiedView,
|
||||
}} /> }>
|
||||
<AdminAppConfigurationServer {...{ simplifiedView }} />
|
||||
|
||||
@ -137,6 +137,8 @@ export default function AdminAppConfigurationClient({
|
||||
areInternalToolsEnabled,
|
||||
areAdminDebugToolsEnabled,
|
||||
isAdminSqlDebugEnabled,
|
||||
// Auth
|
||||
secret,
|
||||
// Connection status
|
||||
databaseError,
|
||||
storageError,
|
||||
@ -147,6 +149,7 @@ export default function AdminAppConfigurationClient({
|
||||
simplifiedView,
|
||||
isAnalyzingConfiguration,
|
||||
}: AppConfiguration &
|
||||
{ secret: string } &
|
||||
Partial<Awaited<ReturnType<typeof testConnectionsAction>>> & {
|
||||
simplifiedView?: boolean
|
||||
isAnalyzingConfiguration?: boolean
|
||||
@ -373,7 +376,7 @@ export default function AdminAppConfigurationClient({
|
||||
Store auth secret in environment variable:
|
||||
{!hasAuthSecret &&
|
||||
<div className="overflow-x-auto">
|
||||
<SecretGenerator />
|
||||
<SecretGenerator {...{ secret }} />
|
||||
</div>}
|
||||
{renderEnvVars(['AUTH_SECRET'])}
|
||||
</ChecklistRow>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import AdminAppConfigurationClient from './AdminAppConfigurationClient';
|
||||
import { APP_CONFIGURATION } from '@/app/config';
|
||||
import { testConnectionsAction } from '@/admin/actions';
|
||||
import { generateAuthSecret } from '@/auth';
|
||||
|
||||
export default async function AdminAppConfigurationServer({
|
||||
simplifiedView,
|
||||
@ -9,10 +10,13 @@ export default async function AdminAppConfigurationServer({
|
||||
}) {
|
||||
const connectionErrors = await testConnectionsAction().catch(() => ({}));
|
||||
|
||||
const secret = await generateAuthSecret();
|
||||
|
||||
return (
|
||||
<AdminAppConfigurationClient {...{
|
||||
...APP_CONFIGURATION,
|
||||
...connectionErrors,
|
||||
secret,
|
||||
simplifiedView,
|
||||
}} />
|
||||
);
|
||||
|
||||
@ -22,7 +22,11 @@ export default function SelectPhotosProvider({
|
||||
|
||||
const { isUserSignedIn } = useAppState();
|
||||
|
||||
const searchParamsSelect = useClientSearchParams(PARAM_SELECT);
|
||||
const searchParamsSelect = useClientSearchParams(
|
||||
PARAM_SELECT,
|
||||
// Only scan urls when admin is signed in
|
||||
isUserSignedIn,
|
||||
);
|
||||
|
||||
const [canCurrentPageSelectPhotos, setCanCurrentPageSelectPhotos] =
|
||||
useState(false);
|
||||
@ -36,9 +40,12 @@ export default function SelectPhotosProvider({
|
||||
, []);
|
||||
|
||||
useEffect(() => {
|
||||
const doesPageHavePhotoGrids = getPhotoGridElements().length > 0;
|
||||
setCanCurrentPageSelectPhotos(doesPageHavePhotoGrids);
|
||||
}, [pathname, getPhotoGridElements]);
|
||||
if (isUserSignedIn) {
|
||||
const doesPageHavePhotoGrids = getPhotoGridElements().length > 0;
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setCanCurrentPageSelectPhotos(doesPageHavePhotoGrids);
|
||||
}
|
||||
}, [pathname, isUserSignedIn, getPhotoGridElements]);
|
||||
|
||||
const isSelectingPhotos = useMemo(() =>
|
||||
isUserSignedIn &&
|
||||
@ -66,6 +73,7 @@ export default function SelectPhotosProvider({
|
||||
photoGrids[0]?.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setSelectedPhotoIds([]);
|
||||
}
|
||||
}, [isSelectingPhotos, getPhotoGridElements]);
|
||||
|
||||
@ -18,7 +18,6 @@ import { SWRKey } from '@/swr';
|
||||
|
||||
export type AppStateContextType = {
|
||||
// CORE
|
||||
hasLoaded?: boolean
|
||||
hasLoadedWithAnimations?: boolean
|
||||
invalidateSwr?: (key?: SWRKey, revalidate?: boolean) => void
|
||||
nextPhotoAnimation?: AnimationConfig
|
||||
@ -31,6 +30,7 @@ export type AppStateContextType = {
|
||||
typeof getCountsForCategoriesCachedAction
|
||||
>>
|
||||
// ENVIRONMENT
|
||||
timezone?: string
|
||||
supportsHover?: boolean
|
||||
// MODAL
|
||||
isCommandKOpen?: boolean
|
||||
|
||||
@ -54,8 +54,6 @@ export default function AppStateProvider({
|
||||
const pathname = usePathname();
|
||||
|
||||
// CORE
|
||||
const [hasLoaded, setHasLoaded] =
|
||||
useState(false);
|
||||
const [hasLoadedWithAnimations, setHasLoadedWithAnimations] =
|
||||
useState(false);
|
||||
const [nextPhotoAnimation, _setNextPhotoAnimation] =
|
||||
@ -80,6 +78,7 @@ export default function AppStateProvider({
|
||||
const [shouldRespondToKeyboardCommands, setShouldRespondToKeyboardCommands] =
|
||||
useState(true);
|
||||
// ENVIRONMENT
|
||||
const [timezone, setTimezone] = useState<string>();
|
||||
const supportsHover = useSupportsHover();
|
||||
// MODAL
|
||||
const [isCommandKOpen, setIsCommandKOpen] =
|
||||
@ -118,9 +117,11 @@ export default function AppStateProvider({
|
||||
useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setHasLoaded(true);
|
||||
storeTimezoneCookie();
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setUserEmailEager(getAuthEmailCookie());
|
||||
// Capture backup timezone on client
|
||||
setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||
if (IS_PRODUCTION) { warmRedisAction(); }
|
||||
const timeout = setTimeout(() => {
|
||||
setHasLoadedWithAnimations(true);
|
||||
@ -152,6 +153,7 @@ export default function AppStateProvider({
|
||||
} = useSWR(SWR_KEYS.GET_AUTH, getAuthAction);
|
||||
useEffect(() => {
|
||||
if (auth === null || authError) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setUserEmail(undefined);
|
||||
setUserEmailEager(undefined);
|
||||
clearAuthEmailCookie();
|
||||
@ -222,7 +224,6 @@ export default function AppStateProvider({
|
||||
<AppStateContext.Provider
|
||||
value={{
|
||||
// CORE
|
||||
hasLoaded,
|
||||
hasLoadedWithAnimations,
|
||||
invalidateSwr,
|
||||
nextPhotoAnimation,
|
||||
@ -233,6 +234,7 @@ export default function AppStateProvider({
|
||||
setShouldRespondToKeyboardCommands,
|
||||
categoriesWithCounts,
|
||||
// ENVIRONMENT
|
||||
timezone,
|
||||
supportsHover,
|
||||
// MODAL
|
||||
isCommandKOpen,
|
||||
|
||||
@ -4,13 +4,17 @@ import { clsx } from 'clsx/lite';
|
||||
import Container from '@/components/Container';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import CopyButton from '@/components/CopyButton';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { generateAuthSecretAction } from '@/auth/actions';
|
||||
import { BiRefresh } from 'react-icons/bi';
|
||||
|
||||
export default function SecretGenerator() {
|
||||
export default function SecretGenerator({
|
||||
secret: secretFromProps,
|
||||
}: {
|
||||
secret: string
|
||||
}) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [secret, setSecret] = useState('');
|
||||
const [secret, setSecret] = useState(secretFromProps);
|
||||
|
||||
const getSecret = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
@ -19,10 +23,6 @@ export default function SecretGenerator() {
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getSecret();
|
||||
}, [getSecret]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Container className="my-1.5 inline-flex" padding="tight">
|
||||
|
||||
@ -1,27 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import Switcher from '@/components/switcher/Switcher';
|
||||
import SwitcherItem from '@/components/switcher/SwitcherItem';
|
||||
import { BiDesktop, BiMoon, BiSun } from 'react-icons/bi';
|
||||
import { useAppText } from '@/i18n/state/client';
|
||||
import { useAppState } from './AppState';
|
||||
|
||||
export default function ThemeSwitcher () {
|
||||
const { hasLoadedWithAnimations } = useAppState();
|
||||
|
||||
const appText = useAppText();
|
||||
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
// useEffect only runs on the client, so now we can safely show the UI
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Switcher
|
||||
// Apply offset due to outline strategy
|
||||
@ -30,19 +22,19 @@ export default function ThemeSwitcher () {
|
||||
<SwitcherItem
|
||||
icon={<BiDesktop size={16} />}
|
||||
onClick={() => setTheme('system')}
|
||||
active={theme === 'system'}
|
||||
active={hasLoadedWithAnimations && theme === 'system'}
|
||||
tooltip={{ content: appText.theme.system }}
|
||||
/>
|
||||
<SwitcherItem
|
||||
icon={<BiSun size={18} />}
|
||||
onClick={() => setTheme('light')}
|
||||
active={theme === 'light'}
|
||||
active={hasLoadedWithAnimations && theme === 'light'}
|
||||
tooltip={{ content: appText.theme.light }}
|
||||
/>
|
||||
<SwitcherItem
|
||||
icon={<BiMoon size={16} />}
|
||||
onClick={() => setTheme('dark')}
|
||||
active={theme === 'dark'}
|
||||
active={hasLoadedWithAnimations && theme === 'dark'}
|
||||
tooltip={{ content: appText.theme.dark }}
|
||||
/>
|
||||
</Switcher>
|
||||
|
||||
@ -46,7 +46,7 @@ function AnimateItems({
|
||||
onAnimationComplete,
|
||||
}: Props) {
|
||||
const {
|
||||
hasLoaded,
|
||||
hasLoadedWithAnimations,
|
||||
nextPhotoAnimation,
|
||||
getNextPhotoAnimationId,
|
||||
clearNextPhotoAnimation,
|
||||
@ -56,14 +56,13 @@ function AnimateItems({
|
||||
|
||||
const prefersReducedMotion = usePrefersReducedMotion();
|
||||
|
||||
const hasLoadedInitial = useRef(hasLoaded);
|
||||
const nextPhotoAnimationInitial = useRef(nextPhotoAnimation);
|
||||
|
||||
const shouldAnimate = type !== 'none' &&
|
||||
!prefersReducedMotion &&
|
||||
!(animateOnFirstLoadOnly && hasLoadedInitial.current);
|
||||
!(animateOnFirstLoadOnly && hasLoadedWithAnimations);
|
||||
const shouldStagger =
|
||||
!(staggerOnFirstLoadOnly && hasLoadedInitial.current);
|
||||
!(staggerOnFirstLoadOnly && hasLoadedWithAnimations);
|
||||
|
||||
const typeResolved = animateFromAppState
|
||||
? (nextPhotoAnimationInitial.current?.type ?? type)
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import useIsDesktop from '@/utility/useIsDesktop';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function RedirectOnDesktop({
|
||||
redirectPath,
|
||||
shouldPrefetchRedirect = true,
|
||||
}: {
|
||||
redirectPath: string
|
||||
shouldPrefetchRedirect?: boolean
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
const isDesktop = useIsDesktop();
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldPrefetchRedirect) {
|
||||
router.prefetch(redirectPath);
|
||||
}
|
||||
}, [router, shouldPrefetchRedirect, redirectPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDesktop && pathname !== redirectPath) {
|
||||
router.push(redirectPath);
|
||||
}
|
||||
}, [router, isDesktop, pathname, redirectPath]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useAppState } from '@/app/AppState';
|
||||
import { formatDate } from '@/utility/date';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function ResponsiveDate({
|
||||
date,
|
||||
@ -15,13 +15,9 @@ export default function ResponsiveDate({
|
||||
className?: string
|
||||
titleLabel?: string
|
||||
} & Parameters<typeof formatDate>[0]) {
|
||||
const [timezone, setTimezone] = useState(timezoneFromProps);
|
||||
const { timezone: timezoneFromState } = useAppState();
|
||||
|
||||
useEffect(() => {
|
||||
if (!timezoneFromProps) {
|
||||
setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||
}
|
||||
}, [timezoneFromProps]);
|
||||
const timezone = timezoneFromProps ?? timezoneFromState;
|
||||
|
||||
const showPlaceholder = timezone === undefined;
|
||||
|
||||
|
||||
@ -50,6 +50,7 @@ export default function SelectMenu({
|
||||
|
||||
useEffect(() => {
|
||||
if (readOnly) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, [readOnly]);
|
||||
|
||||
@ -193,6 +193,7 @@ export default function TagInput({
|
||||
useEffect(() => {
|
||||
if (inputText) {
|
||||
if (inputText.includes(',')) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
addOptions(inputText.split(','));
|
||||
} else {
|
||||
setShouldShowMenu(true);
|
||||
|
||||
@ -37,6 +37,7 @@ export default function ImageWithFallback({
|
||||
!ref.current?.complete ||
|
||||
(ref.current?.naturalWidth ?? 0) === 0
|
||||
) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setFadeFallbackTransition(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@ -29,6 +29,7 @@ export default function OGLoaderImage({
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current?.complete) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setLoadingState('loading');
|
||||
}
|
||||
}, [path]);
|
||||
|
||||
@ -34,6 +34,7 @@ export default function PathLoaderButton({
|
||||
}, loaderDelay);
|
||||
return () => clearTimeout(timeout);
|
||||
} else {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setShouldShowLoader(false);
|
||||
}
|
||||
}, [isPending, loaderDelay]);
|
||||
|
||||
@ -5,8 +5,8 @@ import Spinner from '../Spinner';
|
||||
import LinkWithIconLoader from '../LinkWithIconLoader';
|
||||
import Tooltip from '../Tooltip';
|
||||
|
||||
const WIDTH_CLASS = 'w-[42px]';
|
||||
const WIDTH_CLASS_NARROW = 'w-[36px]';
|
||||
const WIDTH_CLASS = 'w-[42px]';
|
||||
const WIDTH_CLASS_NARROW = 'w-[36px]';
|
||||
|
||||
export default function SwitcherItem({
|
||||
icon,
|
||||
|
||||
@ -17,6 +17,7 @@ export default function useTitleCaptionAiImageQuery(
|
||||
const [caption, setCaption] = useState('');
|
||||
useEffect(() => {
|
||||
const { title, caption } = parseTitleAndCaption(text);
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setTitle(title);
|
||||
setCaption(caption);
|
||||
}, [text]);
|
||||
|
||||
@ -102,31 +102,31 @@ const getPhotosCacheKeys = (options: PhotoQueryOptions = {}) => {
|
||||
};
|
||||
|
||||
export const revalidatePhotosKey = () =>
|
||||
revalidateTag(KEY_PHOTOS);
|
||||
revalidateTag(KEY_PHOTOS, 'max');
|
||||
|
||||
export const revalidateAlbumsKey = () =>
|
||||
revalidateTag(KEY_ALBUMS);
|
||||
revalidateTag(KEY_ALBUMS, 'max');
|
||||
|
||||
export const revalidateTagsKey = () =>
|
||||
revalidateTag(KEY_TAGS);
|
||||
revalidateTag(KEY_TAGS, 'max');
|
||||
|
||||
export const revalidateRecipesKey = () =>
|
||||
revalidateTag(KEY_RECIPES);
|
||||
revalidateTag(KEY_RECIPES, 'max');
|
||||
|
||||
export const revalidateCamerasKey = () =>
|
||||
revalidateTag(KEY_CAMERAS);
|
||||
revalidateTag(KEY_CAMERAS, 'max');
|
||||
|
||||
export const revalidateLensesKey = () =>
|
||||
revalidateTag(KEY_LENSES);
|
||||
revalidateTag(KEY_LENSES, 'max');
|
||||
|
||||
export const revalidateFilmsKey = () =>
|
||||
revalidateTag(KEY_FILMS);
|
||||
revalidateTag(KEY_FILMS, 'max');
|
||||
|
||||
export const revalidateFocalLengthsKey = () =>
|
||||
revalidateTag(KEY_FOCAL_LENGTHS);
|
||||
revalidateTag(KEY_FOCAL_LENGTHS, 'max');
|
||||
|
||||
export const revalidateYearsKey = () =>
|
||||
revalidateTag(KEY_YEARS);
|
||||
revalidateTag(KEY_YEARS, 'max');
|
||||
|
||||
export const revalidateAllKeys = () => {
|
||||
revalidatePhotosKey();
|
||||
@ -151,7 +151,7 @@ export const revalidateAllKeysAndPaths = () => {
|
||||
|
||||
export const revalidatePhoto = (photoId: string) => {
|
||||
// Tags
|
||||
revalidateTag(photoId);
|
||||
revalidateTag(photoId, 'max');
|
||||
revalidateYearsKey();
|
||||
revalidateCamerasKey();
|
||||
revalidateLensesKey();
|
||||
|
||||
@ -24,6 +24,7 @@ export default function ApplyRecipeTitleGloballyCheckbox({
|
||||
|
||||
useEffect(() => {
|
||||
if (recipeTitle && hasRecipeTitleChanged && recipeData && film) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setMatchingPhotosCount(undefined);
|
||||
getPhotosNeedingRecipeTitleCountAction(recipeData, film, photoId)
|
||||
.then(setMatchingPhotosCount);
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
'use client';
|
||||
|
||||
import {
|
||||
@ -371,7 +372,7 @@ export default function PhotoForm({
|
||||
blurCompatibilityLevel="none"
|
||||
width={thumbnailDimensions.width}
|
||||
height={thumbnailDimensions.height}
|
||||
priority
|
||||
preload
|
||||
/>;
|
||||
|
||||
return (
|
||||
|
||||
@ -31,6 +31,7 @@ export default function PlaceInput({
|
||||
|
||||
useEffect(() => {
|
||||
if (inputTextDebounced) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setIsLoadingPlaces(true);
|
||||
getPlaceAutoCompleteAction(inputTextDebounced)
|
||||
.then(options => {
|
||||
|
||||
@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export default function useClientSearchParams(
|
||||
paramKey: string,
|
||||
enableScanning = true,
|
||||
): string | undefined {
|
||||
const pathname = usePathname();
|
||||
|
||||
@ -23,7 +24,10 @@ export default function useClientSearchParams(
|
||||
};
|
||||
}, [captureParam]);
|
||||
|
||||
useEffect(captureParam, [captureParam, pathname]);
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
if (enableScanning) { captureParam(); }
|
||||
}, [pathname, captureParam, enableScanning]);
|
||||
|
||||
return paramValue;
|
||||
};
|
||||
|
||||
@ -17,9 +17,8 @@ export default function useHash() {
|
||||
|
||||
// Needed to capture non-request-initiated hash changes
|
||||
const params = useSearchParams();
|
||||
useEffect(() => {
|
||||
storeHash();
|
||||
}, [params, storeHash]);
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
useEffect(storeHash, [params, storeHash]);
|
||||
|
||||
const updateWindowHash = useCallback((hash: string) => {
|
||||
window.history.replaceState(null, '', `#${hash}`);
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function useIsDesktop() {
|
||||
const [isDesktop, setIsDesktop] = useState<boolean>();
|
||||
|
||||
useEffect(() => {
|
||||
const breakpointMd = getComputedStyle(document.body)
|
||||
.getPropertyValue('--breakpoint-md');
|
||||
const mql = window.matchMedia(`(min-width: ${breakpointMd})`);
|
||||
setIsDesktop(mql.matches);
|
||||
|
||||
const eventHandler = (event: MediaQueryListEvent) =>
|
||||
setIsDesktop(event.matches);
|
||||
|
||||
mql.addEventListener('change', eventHandler);
|
||||
return () => mql.removeEventListener('change', eventHandler);
|
||||
}, []);
|
||||
|
||||
return isDesktop;
|
||||
};
|
||||
@ -5,14 +5,16 @@ export default function useSupportsHover() {
|
||||
? window.matchMedia('(hover: hover)')
|
||||
: undefined);
|
||||
|
||||
const [supportsHover, setSupportsHover] =
|
||||
useState<boolean>(mqlRef.current?.matches ?? false);
|
||||
const [supportsHover, setSupportsHover] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (e: MediaQueryListEvent) => setSupportsHover(e.matches);
|
||||
const mql = mqlRef.current;
|
||||
mql?.addEventListener('change', listener);
|
||||
return () => mql?.removeEventListener('change', listener);
|
||||
if (mql) {
|
||||
setSupportsHover(mql.matches);
|
||||
const listener = (e: MediaQueryListEvent) => setSupportsHover(e.matches);
|
||||
mql.addEventListener('change', listener);
|
||||
return () => mql?.removeEventListener('change', listener);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return supportsHover;
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user