{
setIsLoading(true);
@@ -19,10 +23,6 @@ export default function SecretGenerator() {
.finally(() => setIsLoading(false));
}, []);
- useEffect(() => {
- getSecret();
- }, [getSecret]);
-
return (
diff --git a/src/app/ThemeSwitcher.tsx b/src/app/ThemeSwitcher.tsx
index b1869b62..9e019dd6 100644
--- a/src/app/ThemeSwitcher.tsx
+++ b/src/app/ThemeSwitcher.tsx
@@ -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 (
}
onClick={() => setTheme('system')}
- active={theme === 'system'}
+ active={hasLoadedWithAnimations && theme === 'system'}
tooltip={{ content: appText.theme.system }}
/>
}
onClick={() => setTheme('light')}
- active={theme === 'light'}
+ active={hasLoadedWithAnimations && theme === 'light'}
tooltip={{ content: appText.theme.light }}
/>
}
onClick={() => setTheme('dark')}
- active={theme === 'dark'}
+ active={hasLoadedWithAnimations && theme === 'dark'}
tooltip={{ content: appText.theme.dark }}
/>
diff --git a/src/components/AnimateItems.tsx b/src/components/AnimateItems.tsx
index 07466e6c..c55dfbee 100644
--- a/src/components/AnimateItems.tsx
+++ b/src/components/AnimateItems.tsx
@@ -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)
diff --git a/src/components/RedirectOnDesktop.tsx b/src/components/RedirectOnDesktop.tsx
deleted file mode 100644
index 42704c7d..00000000
--- a/src/components/RedirectOnDesktop.tsx
+++ /dev/null
@@ -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;
-}
diff --git a/src/components/ResponsiveDate.tsx b/src/components/ResponsiveDate.tsx
index 371ac467..1e44d4db 100644
--- a/src/components/ResponsiveDate.tsx
+++ b/src/components/ResponsiveDate.tsx
@@ -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[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;
diff --git a/src/components/SelectMenu.tsx b/src/components/SelectMenu.tsx
index 863d7fcd..c4172f66 100644
--- a/src/components/SelectMenu.tsx
+++ b/src/components/SelectMenu.tsx
@@ -50,6 +50,7 @@ export default function SelectMenu({
useEffect(() => {
if (readOnly) {
+ // eslint-disable-next-line react-hooks/set-state-in-effect
setIsOpen(false);
}
}, [readOnly]);
diff --git a/src/components/TagInput.tsx b/src/components/TagInput.tsx
index 4e0ecfce..05a87e42 100644
--- a/src/components/TagInput.tsx
+++ b/src/components/TagInput.tsx
@@ -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);
diff --git a/src/components/image/ImageWithFallback.tsx b/src/components/image/ImageWithFallback.tsx
index ec02fadf..d1936cf6 100644
--- a/src/components/image/ImageWithFallback.tsx
+++ b/src/components/image/ImageWithFallback.tsx
@@ -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);
}
}, []);
diff --git a/src/components/og/OGLoaderImage.tsx b/src/components/og/OGLoaderImage.tsx
index 9412325b..d4c1d238 100644
--- a/src/components/og/OGLoaderImage.tsx
+++ b/src/components/og/OGLoaderImage.tsx
@@ -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]);
diff --git a/src/components/primitives/PathLoaderButton.tsx b/src/components/primitives/PathLoaderButton.tsx
index 0851d3e0..29735158 100644
--- a/src/components/primitives/PathLoaderButton.tsx
+++ b/src/components/primitives/PathLoaderButton.tsx
@@ -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]);
diff --git a/src/components/switcher/SwitcherItem.tsx b/src/components/switcher/SwitcherItem.tsx
index 92c94aa0..13b8cd65 100644
--- a/src/components/switcher/SwitcherItem.tsx
+++ b/src/components/switcher/SwitcherItem.tsx
@@ -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,
diff --git a/src/photo/ai/useTitleCaptionAiImageQuery.ts b/src/photo/ai/useTitleCaptionAiImageQuery.ts
index f99483ec..3f77582e 100644
--- a/src/photo/ai/useTitleCaptionAiImageQuery.ts
+++ b/src/photo/ai/useTitleCaptionAiImageQuery.ts
@@ -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]);
diff --git a/src/photo/cache.ts b/src/photo/cache.ts
index a459c86f..1222ff4a 100644
--- a/src/photo/cache.ts
+++ b/src/photo/cache.ts
@@ -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();
diff --git a/src/photo/form/ApplyRecipesGloballyCheckbox.tsx b/src/photo/form/ApplyRecipesGloballyCheckbox.tsx
index 93f44e22..1a683a6c 100644
--- a/src/photo/form/ApplyRecipesGloballyCheckbox.tsx
+++ b/src/photo/form/ApplyRecipesGloballyCheckbox.tsx
@@ -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);
diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx
index 2af6846b..29bbbcb6 100644
--- a/src/photo/form/PhotoForm.tsx
+++ b/src/photo/form/PhotoForm.tsx
@@ -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 (
diff --git a/src/place/PlaceInput.tsx b/src/place/PlaceInput.tsx
index cf451a7b..8acc4686 100644
--- a/src/place/PlaceInput.tsx
+++ b/src/place/PlaceInput.tsx
@@ -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 => {
diff --git a/src/utility/useClientSearchParams.ts b/src/utility/useClientSearchParams.ts
index b08474da..c112aebd 100644
--- a/src/utility/useClientSearchParams.ts
+++ b/src/utility/useClientSearchParams.ts
@@ -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;
};
diff --git a/src/utility/useHash.ts b/src/utility/useHash.ts
index 07558889..cabc263d 100644
--- a/src/utility/useHash.ts
+++ b/src/utility/useHash.ts
@@ -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}`);
diff --git a/src/utility/useIsDesktop.ts b/src/utility/useIsDesktop.ts
deleted file mode 100644
index 2e58ba89..00000000
--- a/src/utility/useIsDesktop.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { useEffect, useState } from 'react';
-
-export default function useIsDesktop() {
- const [isDesktop, setIsDesktop] = useState();
-
- 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;
-};
diff --git a/src/utility/useSupportsHover.ts b/src/utility/useSupportsHover.ts
index 0454e178..e777c891 100644
--- a/src/utility/useSupportsHover.ts
+++ b/src/utility/useSupportsHover.ts
@@ -5,14 +5,16 @@ export default function useSupportsHover() {
? window.matchMedia('(hover: hover)')
: undefined);
- const [supportsHover, setSupportsHover] =
- useState(mqlRef.current?.matches ?? false);
+ const [supportsHover, setSupportsHover] = useState(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;
diff --git a/tsconfig.json b/tsconfig.json
index fcfb38cf..56206137 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -15,7 +15,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
- "jsx": "preserve",
+ "jsx": "react-jsx",
"incremental": true,
"plugins": [
{