diff --git a/.vscode/settings.json b/.vscode/settings.json index 4a786c5d..2f8184b5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -37,6 +37,7 @@ "mitigations", "nanoids", "nextjs", + "nowrap", "parameterizes", "presigner", "Provia", diff --git a/src/components/LinkWithIconLoader.tsx b/src/components/LinkWithIconLoader.tsx index 39294a11..8cbd236e 100644 --- a/src/components/LinkWithIconLoader.tsx +++ b/src/components/LinkWithIconLoader.tsx @@ -6,7 +6,6 @@ export default function LinkWithIconLoader({ className, icon, loader, - debugLoading, ...props }: Omit, 'children'> & { icon: ReactNode @@ -20,11 +19,11 @@ export default function LinkWithIconLoader({ {({ isLoading }) => <> {icon} - {(isLoading || debugLoading) && diff --git a/src/components/LinkWithStatus.tsx b/src/components/LinkWithStatus.tsx index b3c5c46d..879e445c 100644 --- a/src/components/LinkWithStatus.tsx +++ b/src/components/LinkWithStatus.tsx @@ -17,30 +17,28 @@ const FLICKER_THRESHOLD = 400; // Clear loading status after long duration const MAX_LOADING_DURATION = 15_000; -export type LinkWithStatusProps = Omit< - ComponentProps, 'children' -> & { - loadingClassName?: string - children: ReactNode | ((props: { - isLoading: boolean - }) => ReactNode) - debugLoading?: boolean -} - export default function LinkWithStatus({ loadingClassName, href, className, onClick, children, - debugLoading = false, + isLoading: isLoadingProp = false, + setIsLoading: setIsLoadingProp, ...props -}: LinkWithStatusProps) { +}: Omit, 'children'> & { + children: ReactNode | ((props: { isLoading: boolean }) => ReactNode) + loadingClassName?: string + // For hoisting state to a parent component, e.g., + isLoading?: boolean + setIsLoading?: (isLoading: boolean) => void +}) { const path = usePathname(); const [pathWhenClicked, setPathWhenClicked] = useState(); - const [_isLoading, setIsLoading] = useState(false); - const isLoading = _isLoading || debugLoading; + const [_isLoading, _setIsLoading] = useState(false); + const isLoading = isLoadingProp || _isLoading; + const setIsLoading = setIsLoadingProp || _setIsLoading; const isLoadingStartTime = useRef(undefined); @@ -60,7 +58,7 @@ export default function LinkWithStatus({ const stopLoading = useCallback(() => { setIsLoading(false); setPathWhenClicked(undefined); - }, []); + }, [setIsLoading]); const isVisitingLinkHref = path === href; diff --git a/src/components/primitives/EntityLink.tsx b/src/components/primitives/EntityLink.tsx index f1e77b76..c7dce627 100644 --- a/src/components/primitives/EntityLink.tsx +++ b/src/components/primitives/EntityLink.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ComponentProps, ReactNode } from 'react'; +import { ComponentProps, ReactNode, RefObject, useState } from 'react'; import LabeledIcon, { LabeledIconType } from './LabeledIcon'; import Badge from '../Badge'; import { clsx } from 'clsx/lite'; @@ -9,6 +9,7 @@ import Spinner from '../Spinner'; import ResponsiveText from './ResponsiveText'; export interface EntityLinkExternalProps { + ref?: RefObject type?: LabeledIconType badged?: boolean contrast?: ComponentProps['contrast'] @@ -18,6 +19,7 @@ export interface EntityLinkExternalProps { } export default function EntityLink({ + ref, icon, label, labelSmall, @@ -28,6 +30,7 @@ export default function EntityLink({ href = '', // Make link optional for debugging purposes prefetch, title, + accessory, hoverEntity, truncate = true, className, @@ -42,6 +45,7 @@ export default function EntityLink({ href?: string prefetch?: boolean title?: string + accessory?: ReactNode hoverEntity?: ReactNode truncate?: boolean className?: string @@ -49,6 +53,8 @@ export default function EntityLink({ uppercase?: boolean debug?: boolean } & EntityLinkExternalProps) { + const [isLoading, setIsLoading] = useState(false); + const classForContrast = () => { switch (contrast) { case 'low': @@ -68,61 +74,66 @@ export default function EntityLink({ ; return ( - + - {({ isLoading }) => <> - - {badged - ? - {renderLabel} - - : - {renderLabel} - } - - {!isLoading && hoverEntity !== undefined && - - {hoverEntity} + + {badged + ? + {renderLabel} + + : + {renderLabel} } - {isLoading && - } - } + + {accessory} + {!isLoading && hoverEntity !== undefined && + + {hoverEntity} + } + {isLoading && + } ); } diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 63c96a12..135747c3 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -42,8 +42,6 @@ import { LuExpand } from 'react-icons/lu'; import LoaderButton from '@/components/primitives/LoaderButton'; import Tooltip from '@/components/Tooltip'; import ZoomControls, { ZoomControlsRef } from '@/components/image/ZoomControls'; -import { TbChecklist } from 'react-icons/tb'; -import { IoCloseSharp } from 'react-icons/io5'; import { AnimatePresence } from 'framer-motion'; import useRecipeOverlay from '../recipe/useRecipeOverlay'; import PhotoRecipeOverlay from '@/recipe/PhotoRecipeOverlay'; @@ -105,8 +103,8 @@ export default function PhotoLarge({ onVisible?: () => void }) { const ref = useRef(null); - - const zoomControlsRef = useRef(null); + const refZoomControls = useRef(null); + const refPhotoRecipe = useRef(null); const { areZoomControlsShown, @@ -132,8 +130,7 @@ export default function PhotoLarge({ , []); const refRecipe = useRef(null); - const refRecipeButton = useRef(null); - const refTriggers = useMemo(() => [refRecipeButton], []); + const refTriggers = useMemo(() => [refPhotoRecipe], []); const { shouldShowRecipeOverlay, toggleRecipeOverlay, @@ -155,7 +152,6 @@ export default function PhotoLarge({ const showLensContent = showLens && shouldShowLensDataForPhoto(photo); const showTagsContent = tags.length > 0; const showRecipeContent = showRecipe && shouldShowRecipeDataForPhoto(photo); - const showRecipeButton = shouldShowRecipeDataForPhoto(photo); const showFilmContent = showFilm && shouldShowFilmDataForPhoto(photo); useVisible({ ref, onVisible }); @@ -205,7 +201,7 @@ export default function PhotoLarge({ arePhotosMatted && matteContentWidthForAspectRatio, )}> @@ -334,10 +330,13 @@ export default function PhotoLarge({ } {showRecipeContent && recipeTitle && } {showTagsContent && {photo.isoFormatted}
  • {photo.exposureCompensationFormatted ?? '0ev'}
  • - {(showRecipeButton || showFilmContent) && -
    - {showFilmContent && photo.film && - } - {showRecipeButton && - - - } -
    } + {showFilmContent && photo.film && + } }
    } - onClick={() => zoomControlsRef.current?.open()} + onClick={() => refZoomControls.current?.open()} styleAs="link" className="text-medium translate-y-[0.25px]" hideFocusOutline diff --git a/src/recipe/PhotoRecipe.tsx b/src/recipe/PhotoRecipe.tsx index 94af5e47..e93aa23b 100644 --- a/src/recipe/PhotoRecipe.tsx +++ b/src/recipe/PhotoRecipe.tsx @@ -4,53 +4,43 @@ import EntityLink, { } from '@/components/primitives/EntityLink'; import { formatRecipe } from '.'; import clsx from 'clsx/lite'; -import { RefObject } from 'react'; +import { ComponentProps } from 'react'; import IconRecipe from '@/components/icons/IconRecipe'; +import PhotoRecipeOverlayButton from './PhotoRecipeOverlayButton'; export default function PhotoRecipe({ + ref, recipe, countOnHover, - refButton, - isOpen, - recipeOnClick, + toggleRecipeOverlay, + shouldShowRecipeOverlay, ...props }: { recipe: string - refButton?: RefObject - isOpen?: boolean - recipeOnClick?: () => void countOnHover?: number -} & EntityLinkExternalProps) { +} & Partial> + & EntityLinkExternalProps) { return ( -
    - } - hoverEntity={countOnHover} - /> - {recipeOnClick && - } -
    + } + accessory={toggleRecipeOverlay && + } + hoverEntity={countOnHover} + /> ); } diff --git a/src/recipe/PhotoRecipeOverlayButton.tsx b/src/recipe/PhotoRecipeOverlayButton.tsx new file mode 100644 index 00000000..73c20202 --- /dev/null +++ b/src/recipe/PhotoRecipeOverlayButton.tsx @@ -0,0 +1,45 @@ +'use client'; + +import clsx from 'clsx/lite'; +import { FaPlus } from 'react-icons/fa6'; +import Tooltip from '@/components/Tooltip'; +import { useRef } from 'react'; + +export default function PhotoRecipeOverlayButton({ + className, + toggleRecipeOverlay, + shouldShowRecipeOverlay, +}: { + className?: string + toggleRecipeOverlay: () => void + shouldShowRecipeOverlay?: boolean +}) { + const ref = useRef(null); + + return ( + + + + ); +} diff --git a/src/recipe/RecipeHeader.tsx b/src/recipe/RecipeHeader.tsx index ebbb80e3..06811e77 100644 --- a/src/recipe/RecipeHeader.tsx +++ b/src/recipe/RecipeHeader.tsx @@ -20,7 +20,7 @@ export default function RecipeHeader({ count?: number dateRange?: PhotoDateRange }) { - const { setRecipeModalProps } = useAppState(); + const { recipeModalProps, setRecipeModalProps } = useAppState(); const photo = getPhotoWithRecipeFromPhotos(photos, selectedPhoto); @@ -30,7 +30,8 @@ export default function RecipeHeader({ entity={ ( + shouldShowRecipeOverlay={Boolean(recipeModalProps)} + toggleRecipeOverlay={() => ( photo?.recipeData && photo?.film ) ? setRecipeModalProps?.({ diff --git a/src/recipe/useRecipeOverlay.ts b/src/recipe/useRecipeOverlay.ts index e4e542e3..291a530d 100644 --- a/src/recipe/useRecipeOverlay.ts +++ b/src/recipe/useRecipeOverlay.ts @@ -1,8 +1,3 @@ -import { - getPathComponents, - pathForPhoto, -} from '@/app/paths'; -import { usePathname } from 'next/navigation'; import { RefObject, useCallback, useEffect, useState } from 'react'; import { isElementEntirelyInViewport } from '@/utility/dom'; import useClickInsideOutside from '@/utility/useClickInsideOutside'; @@ -14,53 +9,15 @@ export default function useRecipeOverlay({ ref?: RefObject, refTriggers?: RefObject[], }) { - const pathname = usePathname(); - - const { - photoId, - ...pathComponents - } = getPathComponents(pathname); - const [shouldShowRecipeOverlay, setShouldShowRecipeOverlay] = useState(false); - const setVisibility = useCallback((shouldShow: boolean) => { - if (shouldShow) { - setShouldShowRecipeOverlay(true); - // Only add query param for photo details - if (photoId) { - window.history.pushState( - null, - '', - pathForPhoto({ - photo: photoId, - ...pathComponents, - showRecipe: true, - }), - ); - } - } else { - setShouldShowRecipeOverlay(false); - // Only remove query param for photo details - if (photoId) { - window.history.pushState( - null, - '', - pathForPhoto({ - photo: photoId, - ...pathComponents, - }), - ); - } - } - }, [pathComponents, photoId]); - const showRecipeOverlay = - useCallback(() => setVisibility(true), [setVisibility]); + useCallback(() => setShouldShowRecipeOverlay(true), []); const hideRecipeOverlay = - useCallback(() => setVisibility(false), [setVisibility]); + useCallback(() => setShouldShowRecipeOverlay(false), []); const toggleRecipeOverlay = useCallback(() => - setVisibility(!shouldShowRecipeOverlay), - [setVisibility, shouldShowRecipeOverlay]); + setShouldShowRecipeOverlay(current => !current), + []); useClickInsideOutside({ htmlElements: [ref, ...refTriggers], diff --git a/tailwind.css b/tailwind.css index 964f10eb..0722b691 100644 --- a/tailwind.css +++ b/tailwind.css @@ -101,7 +101,6 @@ @utility text-light { @apply text-gray-100 } - /* Text */ @utility text-main { @apply