Streamline recipe overlay interactions
This commit is contained in:
parent
ae8a38f462
commit
d1689371a2
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -37,6 +37,7 @@
|
|||||||
"mitigations",
|
"mitigations",
|
||||||
"nanoids",
|
"nanoids",
|
||||||
"nextjs",
|
"nextjs",
|
||||||
|
"nowrap",
|
||||||
"parameterizes",
|
"parameterizes",
|
||||||
"presigner",
|
"presigner",
|
||||||
"Provia",
|
"Provia",
|
||||||
|
|||||||
@ -6,7 +6,6 @@ export default function LinkWithIconLoader({
|
|||||||
className,
|
className,
|
||||||
icon,
|
icon,
|
||||||
loader,
|
loader,
|
||||||
debugLoading,
|
|
||||||
...props
|
...props
|
||||||
}: Omit<ComponentProps<typeof LinkWithStatus>, 'children'> & {
|
}: Omit<ComponentProps<typeof LinkWithStatus>, 'children'> & {
|
||||||
icon: ReactNode
|
icon: ReactNode
|
||||||
@ -20,11 +19,11 @@ export default function LinkWithIconLoader({
|
|||||||
{({ isLoading }) => <>
|
{({ isLoading }) => <>
|
||||||
<span className={clsx(
|
<span className={clsx(
|
||||||
'flex transition-opacity',
|
'flex transition-opacity',
|
||||||
isLoading || debugLoading ? 'opacity-0' : 'opacity-100',
|
isLoading ? 'opacity-0' : 'opacity-100',
|
||||||
)}>
|
)}>
|
||||||
{icon}
|
{icon}
|
||||||
</span>
|
</span>
|
||||||
{(isLoading || debugLoading) && <span className={clsx(
|
{isLoading && <span className={clsx(
|
||||||
'absolute inset-0',
|
'absolute inset-0',
|
||||||
'flex items-center justify-center',
|
'flex items-center justify-center',
|
||||||
)}>
|
)}>
|
||||||
|
|||||||
@ -17,30 +17,28 @@ const FLICKER_THRESHOLD = 400;
|
|||||||
// Clear loading status after long duration
|
// Clear loading status after long duration
|
||||||
const MAX_LOADING_DURATION = 15_000;
|
const MAX_LOADING_DURATION = 15_000;
|
||||||
|
|
||||||
export type LinkWithStatusProps = Omit<
|
|
||||||
ComponentProps<typeof Link>, 'children'
|
|
||||||
> & {
|
|
||||||
loadingClassName?: string
|
|
||||||
children: ReactNode | ((props: {
|
|
||||||
isLoading: boolean
|
|
||||||
}) => ReactNode)
|
|
||||||
debugLoading?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LinkWithStatus({
|
export default function LinkWithStatus({
|
||||||
loadingClassName,
|
loadingClassName,
|
||||||
href,
|
href,
|
||||||
className,
|
className,
|
||||||
onClick,
|
onClick,
|
||||||
children,
|
children,
|
||||||
debugLoading = false,
|
isLoading: isLoadingProp = false,
|
||||||
|
setIsLoading: setIsLoadingProp,
|
||||||
...props
|
...props
|
||||||
}: LinkWithStatusProps) {
|
}: Omit<ComponentProps<typeof Link>, 'children'> & {
|
||||||
|
children: ReactNode | ((props: { isLoading: boolean }) => ReactNode)
|
||||||
|
loadingClassName?: string
|
||||||
|
// For hoisting state to a parent component, e.g., <EntityLink />
|
||||||
|
isLoading?: boolean
|
||||||
|
setIsLoading?: (isLoading: boolean) => void
|
||||||
|
}) {
|
||||||
const path = usePathname();
|
const path = usePathname();
|
||||||
|
|
||||||
const [pathWhenClicked, setPathWhenClicked] = useState<string>();
|
const [pathWhenClicked, setPathWhenClicked] = useState<string>();
|
||||||
const [_isLoading, setIsLoading] = useState(false);
|
const [_isLoading, _setIsLoading] = useState(false);
|
||||||
const isLoading = _isLoading || debugLoading;
|
const isLoading = isLoadingProp || _isLoading;
|
||||||
|
const setIsLoading = setIsLoadingProp || _setIsLoading;
|
||||||
|
|
||||||
const isLoadingStartTime = useRef<number | undefined>(undefined);
|
const isLoadingStartTime = useRef<number | undefined>(undefined);
|
||||||
|
|
||||||
@ -60,7 +58,7 @@ export default function LinkWithStatus({
|
|||||||
const stopLoading = useCallback(() => {
|
const stopLoading = useCallback(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setPathWhenClicked(undefined);
|
setPathWhenClicked(undefined);
|
||||||
}, []);
|
}, [setIsLoading]);
|
||||||
|
|
||||||
const isVisitingLinkHref = path === href;
|
const isVisitingLinkHref = path === href;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ComponentProps, ReactNode } from 'react';
|
import { ComponentProps, ReactNode, RefObject, useState } from 'react';
|
||||||
import LabeledIcon, { LabeledIconType } from './LabeledIcon';
|
import LabeledIcon, { LabeledIconType } from './LabeledIcon';
|
||||||
import Badge from '../Badge';
|
import Badge from '../Badge';
|
||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
@ -9,6 +9,7 @@ import Spinner from '../Spinner';
|
|||||||
import ResponsiveText from './ResponsiveText';
|
import ResponsiveText from './ResponsiveText';
|
||||||
|
|
||||||
export interface EntityLinkExternalProps {
|
export interface EntityLinkExternalProps {
|
||||||
|
ref?: RefObject<HTMLSpanElement | null>
|
||||||
type?: LabeledIconType
|
type?: LabeledIconType
|
||||||
badged?: boolean
|
badged?: boolean
|
||||||
contrast?: ComponentProps<typeof Badge>['contrast']
|
contrast?: ComponentProps<typeof Badge>['contrast']
|
||||||
@ -18,6 +19,7 @@ export interface EntityLinkExternalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function EntityLink({
|
export default function EntityLink({
|
||||||
|
ref,
|
||||||
icon,
|
icon,
|
||||||
label,
|
label,
|
||||||
labelSmall,
|
labelSmall,
|
||||||
@ -28,6 +30,7 @@ export default function EntityLink({
|
|||||||
href = '', // Make link optional for debugging purposes
|
href = '', // Make link optional for debugging purposes
|
||||||
prefetch,
|
prefetch,
|
||||||
title,
|
title,
|
||||||
|
accessory,
|
||||||
hoverEntity,
|
hoverEntity,
|
||||||
truncate = true,
|
truncate = true,
|
||||||
className,
|
className,
|
||||||
@ -42,6 +45,7 @@ export default function EntityLink({
|
|||||||
href?: string
|
href?: string
|
||||||
prefetch?: boolean
|
prefetch?: boolean
|
||||||
title?: string
|
title?: string
|
||||||
|
accessory?: ReactNode
|
||||||
hoverEntity?: ReactNode
|
hoverEntity?: ReactNode
|
||||||
truncate?: boolean
|
truncate?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
@ -49,6 +53,8 @@ export default function EntityLink({
|
|||||||
uppercase?: boolean
|
uppercase?: boolean
|
||||||
debug?: boolean
|
debug?: boolean
|
||||||
} & EntityLinkExternalProps) {
|
} & EntityLinkExternalProps) {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const classForContrast = () => {
|
const classForContrast = () => {
|
||||||
switch (contrast) {
|
switch (contrast) {
|
||||||
case 'low':
|
case 'low':
|
||||||
@ -68,61 +74,66 @@ export default function EntityLink({
|
|||||||
</ResponsiveText>;
|
</ResponsiveText>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={clsx(
|
<span
|
||||||
'group inline-flex max-w-full overflow-hidden select-none',
|
ref={ref}
|
||||||
className,
|
className={clsx(
|
||||||
)}>
|
'inline-flex items-center gap-2',
|
||||||
|
'max-w-full overflow-hidden select-none',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
<LinkWithStatus
|
<LinkWithStatus
|
||||||
href={href}
|
href={href}
|
||||||
className="inline-flex items-center gap-2 max-w-full"
|
className="peer inline-flex items-center gap-2 max-w-full truncate"
|
||||||
|
isLoading={isLoading}
|
||||||
|
setIsLoading={setIsLoading}
|
||||||
>
|
>
|
||||||
{({ isLoading }) => <>
|
<LabeledIcon {...{
|
||||||
<LabeledIcon {...{
|
icon,
|
||||||
icon,
|
iconWide,
|
||||||
iconWide,
|
href,
|
||||||
href,
|
prefetch,
|
||||||
prefetch,
|
title,
|
||||||
title,
|
type,
|
||||||
type,
|
uppercase,
|
||||||
uppercase,
|
className: clsx(
|
||||||
className: clsx(
|
classForContrast(),
|
||||||
classForContrast(),
|
href && !badged && 'hover:text-gray-900 dark:hover:text-gray-100',
|
||||||
href && !badged && 'hover:text-gray-900 dark:hover:text-gray-100',
|
classNameIcon,
|
||||||
classNameIcon,
|
),
|
||||||
),
|
classNameIcon: 'text-dim',
|
||||||
classNameIcon: 'text-dim',
|
debug,
|
||||||
debug,
|
}}>
|
||||||
}}>
|
{badged
|
||||||
{badged
|
? <Badge
|
||||||
? <Badge
|
type="small"
|
||||||
type="small"
|
contrast={contrast}
|
||||||
contrast={contrast}
|
className='translate-y-[-0.5px]'
|
||||||
className='translate-y-[-0.5px]'
|
uppercase
|
||||||
uppercase
|
interactive
|
||||||
interactive
|
>
|
||||||
>
|
{renderLabel}
|
||||||
{renderLabel}
|
</Badge>
|
||||||
</Badge>
|
: <span className={clsx(
|
||||||
: <span className={clsx(
|
truncate && 'inline-flex max-w-full *:truncate',
|
||||||
truncate && 'inline-flex max-w-full *:truncate',
|
)}>
|
||||||
)}>
|
{renderLabel}
|
||||||
{renderLabel}
|
|
||||||
</span>}
|
|
||||||
</LabeledIcon>
|
|
||||||
{!isLoading && hoverEntity !== undefined &&
|
|
||||||
<span className="hidden group-hover:inline text-dim">
|
|
||||||
{hoverEntity}
|
|
||||||
</span>}
|
</span>}
|
||||||
{isLoading &&
|
</LabeledIcon>
|
||||||
<Spinner
|
|
||||||
className={clsx(
|
|
||||||
badged && 'translate-y-[0.5px]',
|
|
||||||
contrast === 'frosted' && 'text-neutral-500',
|
|
||||||
)}
|
|
||||||
color={contrast === 'frosted' ? 'text' : undefined}
|
|
||||||
/>}
|
|
||||||
</>}
|
|
||||||
</LinkWithStatus>
|
</LinkWithStatus>
|
||||||
|
{accessory}
|
||||||
|
{!isLoading && hoverEntity !== undefined &&
|
||||||
|
<span className="hidden peer-hover:inline text-dim">
|
||||||
|
{hoverEntity}
|
||||||
|
</span>}
|
||||||
|
{isLoading &&
|
||||||
|
<Spinner
|
||||||
|
className={clsx(
|
||||||
|
badged && 'translate-y-[0.5px]',
|
||||||
|
contrast === 'frosted' && 'text-neutral-500',
|
||||||
|
)}
|
||||||
|
color={contrast === 'frosted' ? 'text' : undefined}
|
||||||
|
/>}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,8 +42,6 @@ import { LuExpand } from 'react-icons/lu';
|
|||||||
import LoaderButton from '@/components/primitives/LoaderButton';
|
import LoaderButton from '@/components/primitives/LoaderButton';
|
||||||
import Tooltip from '@/components/Tooltip';
|
import Tooltip from '@/components/Tooltip';
|
||||||
import ZoomControls, { ZoomControlsRef } from '@/components/image/ZoomControls';
|
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 { AnimatePresence } from 'framer-motion';
|
||||||
import useRecipeOverlay from '../recipe/useRecipeOverlay';
|
import useRecipeOverlay from '../recipe/useRecipeOverlay';
|
||||||
import PhotoRecipeOverlay from '@/recipe/PhotoRecipeOverlay';
|
import PhotoRecipeOverlay from '@/recipe/PhotoRecipeOverlay';
|
||||||
@ -105,8 +103,8 @@ export default function PhotoLarge({
|
|||||||
onVisible?: () => void
|
onVisible?: () => void
|
||||||
}) {
|
}) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const refZoomControls = useRef<ZoomControlsRef>(null);
|
||||||
const zoomControlsRef = useRef<ZoomControlsRef>(null);
|
const refPhotoRecipe = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
areZoomControlsShown,
|
areZoomControlsShown,
|
||||||
@ -132,8 +130,7 @@ export default function PhotoLarge({
|
|||||||
, []);
|
, []);
|
||||||
|
|
||||||
const refRecipe = useRef<HTMLDivElement>(null);
|
const refRecipe = useRef<HTMLDivElement>(null);
|
||||||
const refRecipeButton = useRef<HTMLButtonElement>(null);
|
const refTriggers = useMemo(() => [refPhotoRecipe], []);
|
||||||
const refTriggers = useMemo(() => [refRecipeButton], []);
|
|
||||||
const {
|
const {
|
||||||
shouldShowRecipeOverlay,
|
shouldShowRecipeOverlay,
|
||||||
toggleRecipeOverlay,
|
toggleRecipeOverlay,
|
||||||
@ -155,7 +152,6 @@ export default function PhotoLarge({
|
|||||||
const showLensContent = showLens && shouldShowLensDataForPhoto(photo);
|
const showLensContent = showLens && shouldShowLensDataForPhoto(photo);
|
||||||
const showTagsContent = tags.length > 0;
|
const showTagsContent = tags.length > 0;
|
||||||
const showRecipeContent = showRecipe && shouldShowRecipeDataForPhoto(photo);
|
const showRecipeContent = showRecipe && shouldShowRecipeDataForPhoto(photo);
|
||||||
const showRecipeButton = shouldShowRecipeDataForPhoto(photo);
|
|
||||||
const showFilmContent = showFilm && shouldShowFilmDataForPhoto(photo);
|
const showFilmContent = showFilm && shouldShowFilmDataForPhoto(photo);
|
||||||
|
|
||||||
useVisible({ ref, onVisible });
|
useVisible({ ref, onVisible });
|
||||||
@ -205,7 +201,7 @@ export default function PhotoLarge({
|
|||||||
arePhotosMatted && matteContentWidthForAspectRatio,
|
arePhotosMatted && matteContentWidthForAspectRatio,
|
||||||
)}>
|
)}>
|
||||||
<ZoomControls
|
<ZoomControls
|
||||||
ref={zoomControlsRef}
|
ref={refZoomControls}
|
||||||
selectImageElement={selectZoomImageElement}
|
selectImageElement={selectZoomImageElement}
|
||||||
{...{ isEnabled: showZoomControls, shouldZoomOnFKeydown }}
|
{...{ isEnabled: showZoomControls, shouldZoomOnFKeydown }}
|
||||||
>
|
>
|
||||||
@ -334,10 +330,13 @@ export default function PhotoLarge({
|
|||||||
</div>}
|
</div>}
|
||||||
{showRecipeContent && recipeTitle &&
|
{showRecipeContent && recipeTitle &&
|
||||||
<PhotoRecipe
|
<PhotoRecipe
|
||||||
|
ref={refPhotoRecipe}
|
||||||
recipe={recipeTitle}
|
recipe={recipeTitle}
|
||||||
contrast="medium"
|
contrast="medium"
|
||||||
prefetch={prefetchRelatedLinks}
|
prefetch={prefetchRelatedLinks}
|
||||||
countOnHover={recipeCount}
|
countOnHover={recipeCount}
|
||||||
|
toggleRecipeOverlay={toggleRecipeOverlay}
|
||||||
|
shouldShowRecipeOverlay={shouldShowRecipeOverlay}
|
||||||
/>}
|
/>}
|
||||||
{showTagsContent &&
|
{showTagsContent &&
|
||||||
<PhotoTags
|
<PhotoTags
|
||||||
@ -397,41 +396,12 @@ export default function PhotoLarge({
|
|||||||
<li>{photo.isoFormatted}</li>
|
<li>{photo.isoFormatted}</li>
|
||||||
<li>{photo.exposureCompensationFormatted ?? '0ev'}</li>
|
<li>{photo.exposureCompensationFormatted ?? '0ev'}</li>
|
||||||
</ul>
|
</ul>
|
||||||
{(showRecipeButton || showFilmContent) &&
|
{showFilmContent && photo.film &&
|
||||||
<div className="flex items-center gap-2 *:w-auto">
|
<PhotoFilm
|
||||||
{showFilmContent && photo.film &&
|
film={photo.film}
|
||||||
<PhotoFilm
|
prefetch={prefetchRelatedLinks}
|
||||||
film={photo.film}
|
countOnHover={filmCount}
|
||||||
prefetch={prefetchRelatedLinks}
|
/>}
|
||||||
countOnHover={filmCount}
|
|
||||||
/>}
|
|
||||||
{showRecipeButton &&
|
|
||||||
<Tooltip content="Fujifilm Recipe">
|
|
||||||
<button
|
|
||||||
ref={refRecipeButton}
|
|
||||||
title="Fujifilm Recipe"
|
|
||||||
onClick={() => {
|
|
||||||
toggleRecipeOverlay();
|
|
||||||
// Avoid unexpected tooltip trigger
|
|
||||||
refRecipeButton.current?.blur();
|
|
||||||
}}
|
|
||||||
className={clsx(
|
|
||||||
'text-medium',
|
|
||||||
'border-medium rounded-md',
|
|
||||||
'px-[4px] py-[2.5px] my-[-3px]',
|
|
||||||
'translate-y-[2px]',
|
|
||||||
'hover:bg-dim active:bg-main',
|
|
||||||
!showFilm && 'translate-x-[-2px]',
|
|
||||||
)}>
|
|
||||||
{shouldShowRecipeOverlay
|
|
||||||
? <IoCloseSharp size={15} />
|
|
||||||
: <TbChecklist
|
|
||||||
className="translate-x-[0.5px]"
|
|
||||||
size={15}
|
|
||||||
/>}
|
|
||||||
</button>
|
|
||||||
</Tooltip>}
|
|
||||||
</div>}
|
|
||||||
</>}
|
</>}
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
'flex gap-x-3 gap-y-baseline',
|
'flex gap-x-3 gap-y-baseline',
|
||||||
@ -458,7 +428,7 @@ export default function PhotoLarge({
|
|||||||
<LoaderButton
|
<LoaderButton
|
||||||
title="Open Image Viewer"
|
title="Open Image Viewer"
|
||||||
icon={<LuExpand size={15} />}
|
icon={<LuExpand size={15} />}
|
||||||
onClick={() => zoomControlsRef.current?.open()}
|
onClick={() => refZoomControls.current?.open()}
|
||||||
styleAs="link"
|
styleAs="link"
|
||||||
className="text-medium translate-y-[0.25px]"
|
className="text-medium translate-y-[0.25px]"
|
||||||
hideFocusOutline
|
hideFocusOutline
|
||||||
|
|||||||
@ -4,53 +4,43 @@ import EntityLink, {
|
|||||||
} from '@/components/primitives/EntityLink';
|
} from '@/components/primitives/EntityLink';
|
||||||
import { formatRecipe } from '.';
|
import { formatRecipe } from '.';
|
||||||
import clsx from 'clsx/lite';
|
import clsx from 'clsx/lite';
|
||||||
import { RefObject } from 'react';
|
import { ComponentProps } from 'react';
|
||||||
import IconRecipe from '@/components/icons/IconRecipe';
|
import IconRecipe from '@/components/icons/IconRecipe';
|
||||||
|
import PhotoRecipeOverlayButton from './PhotoRecipeOverlayButton';
|
||||||
|
|
||||||
export default function PhotoRecipe({
|
export default function PhotoRecipe({
|
||||||
|
ref,
|
||||||
recipe,
|
recipe,
|
||||||
countOnHover,
|
countOnHover,
|
||||||
refButton,
|
toggleRecipeOverlay,
|
||||||
isOpen,
|
shouldShowRecipeOverlay,
|
||||||
recipeOnClick,
|
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
recipe: string
|
recipe: string
|
||||||
refButton?: RefObject<HTMLButtonElement | null>
|
|
||||||
isOpen?: boolean
|
|
||||||
recipeOnClick?: () => void
|
|
||||||
countOnHover?: number
|
countOnHover?: number
|
||||||
} & EntityLinkExternalProps) {
|
} & Partial<ComponentProps<typeof PhotoRecipeOverlayButton>>
|
||||||
|
& EntityLinkExternalProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full gap-2">
|
<EntityLink
|
||||||
<EntityLink
|
{...props}
|
||||||
{...props}
|
ref={ref}
|
||||||
title="Recipe"
|
title="Recipe"
|
||||||
label={formatRecipe(recipe)}
|
label={formatRecipe(recipe)}
|
||||||
href={pathForRecipe(recipe)}
|
href={pathForRecipe(recipe)}
|
||||||
icon={<IconRecipe
|
icon={<IconRecipe
|
||||||
size={16}
|
size={16}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
props.badged
|
props.badged
|
||||||
? 'translate-x-[-1px] translate-y-[0.5px]'
|
? 'translate-x-[-1px] translate-y-[0.5px]'
|
||||||
: 'translate-y-[-0.5px]',
|
: 'translate-y-[-0.5px]',
|
||||||
)}
|
)}
|
||||||
/>}
|
/>}
|
||||||
hoverEntity={countOnHover}
|
accessory={toggleRecipeOverlay &&
|
||||||
/>
|
<PhotoRecipeOverlayButton {...{
|
||||||
{recipeOnClick &&
|
toggleRecipeOverlay,
|
||||||
<button
|
shouldShowRecipeOverlay,
|
||||||
ref={refButton}
|
}} />}
|
||||||
onClick={recipeOnClick}
|
hoverEntity={countOnHover}
|
||||||
className={clsx(
|
/>
|
||||||
'self-start',
|
|
||||||
'px-1 py-0.5',
|
|
||||||
'text-[10px] text-main font-medium tracking-wider',
|
|
||||||
'translate-y-[0.5px]',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isOpen ? 'CLOSE' : 'RECIPE'}
|
|
||||||
</button>}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
45
src/recipe/PhotoRecipeOverlayButton.tsx
Normal file
45
src/recipe/PhotoRecipeOverlayButton.tsx
Normal file
@ -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<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content="Recipe Info">
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
onClick={() => {
|
||||||
|
toggleRecipeOverlay?.();
|
||||||
|
// Avoid unexpected tooltip trigger
|
||||||
|
ref.current?.blur();
|
||||||
|
}}
|
||||||
|
className={clsx(
|
||||||
|
'text-medium',
|
||||||
|
'border-medium rounded-md shadow-none',
|
||||||
|
'px-[3px] py-[3px] my-[-3px]',
|
||||||
|
'hover:bg-extra-dim active:bg-dim',
|
||||||
|
className,
|
||||||
|
)}>
|
||||||
|
<FaPlus
|
||||||
|
className={clsx(
|
||||||
|
'transition-transform',
|
||||||
|
shouldShowRecipeOverlay && 'rotate-45',
|
||||||
|
)}
|
||||||
|
size={10}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -20,7 +20,7 @@ export default function RecipeHeader({
|
|||||||
count?: number
|
count?: number
|
||||||
dateRange?: PhotoDateRange
|
dateRange?: PhotoDateRange
|
||||||
}) {
|
}) {
|
||||||
const { setRecipeModalProps } = useAppState();
|
const { recipeModalProps, setRecipeModalProps } = useAppState();
|
||||||
|
|
||||||
const photo = getPhotoWithRecipeFromPhotos(photos, selectedPhoto);
|
const photo = getPhotoWithRecipeFromPhotos(photos, selectedPhoto);
|
||||||
|
|
||||||
@ -30,7 +30,8 @@ export default function RecipeHeader({
|
|||||||
entity={<PhotoRecipe
|
entity={<PhotoRecipe
|
||||||
recipe={recipe}
|
recipe={recipe}
|
||||||
contrast="high"
|
contrast="high"
|
||||||
recipeOnClick={() => (
|
shouldShowRecipeOverlay={Boolean(recipeModalProps)}
|
||||||
|
toggleRecipeOverlay={() => (
|
||||||
photo?.recipeData &&
|
photo?.recipeData &&
|
||||||
photo?.film
|
photo?.film
|
||||||
) ? setRecipeModalProps?.({
|
) ? setRecipeModalProps?.({
|
||||||
|
|||||||
@ -1,8 +1,3 @@
|
|||||||
import {
|
|
||||||
getPathComponents,
|
|
||||||
pathForPhoto,
|
|
||||||
} from '@/app/paths';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { RefObject, useCallback, useEffect, useState } from 'react';
|
import { RefObject, useCallback, useEffect, useState } from 'react';
|
||||||
import { isElementEntirelyInViewport } from '@/utility/dom';
|
import { isElementEntirelyInViewport } from '@/utility/dom';
|
||||||
import useClickInsideOutside from '@/utility/useClickInsideOutside';
|
import useClickInsideOutside from '@/utility/useClickInsideOutside';
|
||||||
@ -14,53 +9,15 @@ export default function useRecipeOverlay({
|
|||||||
ref?: RefObject<HTMLElement | null>,
|
ref?: RefObject<HTMLElement | null>,
|
||||||
refTriggers?: RefObject<HTMLElement | null>[],
|
refTriggers?: RefObject<HTMLElement | null>[],
|
||||||
}) {
|
}) {
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
|
||||||
photoId,
|
|
||||||
...pathComponents
|
|
||||||
} = getPathComponents(pathname);
|
|
||||||
|
|
||||||
const [shouldShowRecipeOverlay, setShouldShowRecipeOverlay] = useState(false);
|
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 =
|
const showRecipeOverlay =
|
||||||
useCallback(() => setVisibility(true), [setVisibility]);
|
useCallback(() => setShouldShowRecipeOverlay(true), []);
|
||||||
const hideRecipeOverlay =
|
const hideRecipeOverlay =
|
||||||
useCallback(() => setVisibility(false), [setVisibility]);
|
useCallback(() => setShouldShowRecipeOverlay(false), []);
|
||||||
const toggleRecipeOverlay = useCallback(() =>
|
const toggleRecipeOverlay = useCallback(() =>
|
||||||
setVisibility(!shouldShowRecipeOverlay),
|
setShouldShowRecipeOverlay(current => !current),
|
||||||
[setVisibility, shouldShowRecipeOverlay]);
|
[]);
|
||||||
|
|
||||||
useClickInsideOutside({
|
useClickInsideOutside({
|
||||||
htmlElements: [ref, ...refTriggers],
|
htmlElements: [ref, ...refTriggers],
|
||||||
|
|||||||
@ -101,7 +101,6 @@
|
|||||||
@utility text-light {
|
@utility text-light {
|
||||||
@apply text-gray-100
|
@apply text-gray-100
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Text */
|
/* Text */
|
||||||
@utility text-main {
|
@utility text-main {
|
||||||
@apply
|
@apply
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user