Vercel/src/recipe/PhotoRecipeOverlay.tsx
2025-09-07 18:00:52 -05:00

233 lines
6.3 KiB
TypeScript

'use client';
import LoaderButton from '@/components/primitives/LoaderButton';
import PhotoFilm from '@/film/PhotoFilm';
import clsx from 'clsx/lite';
import { ReactNode, RefObject } from 'react';
import { IoCloseCircle } from 'react-icons/io5';
import { motion } from 'framer-motion';
import {
addSign,
formatGrain,
formatNoiseReduction,
formatRecipe,
formatWhiteBalance,
generateRecipeText,
RecipeProps,
} from '.';
import { TbChecklist } from 'react-icons/tb';
import CopyButton from '@/components/CopyButton';
import PhotoRecipe from './PhotoRecipe';
import { useAppText } from '@/i18n/state/client';
import useCategoryCounts from '@/category/useCategoryCounts';
export default function PhotoRecipeOverlay({
ref,
title,
data,
film,
onClose,
isOnPhoto = true,
}: RecipeProps & {
ref?: RefObject<HTMLDivElement | null>
onClose?: () => void
isOnPhoto?: boolean
}) {
const {
dynamicRange,
whiteBalance,
highlight,
shadow,
color,
sharpness,
clarity,
colorChromeEffect,
colorChromeFXBlue,
bwAdjustment,
bwMagentaGreen,
} = data;
const appText = useAppText();
const {
getRecipeCount,
getFilmCount,
} = useCategoryCounts();
const whiteBalanceTypeFormatted = formatWhiteBalance(data);
const renderDataSquare = (
value: ReactNode,
label?: string,
colSpan = 'col-span-4',
) => (
<div className={clsx(
'flex flex-col items-center justify-center gap-0.5 min-w-0',
'rounded-md border',
'border-neutral-200/40',
'bg-neutral-100/30 hover:bg-neutral-100/50',
label && 'p-1',
colSpan,
)}>
<div className="truncate max-w-full tracking-wide">
{typeof value === 'number' ? addSign(value) : value}
</div>
{label && <div className={clsx(
'text-[10px] leading-none tracking-wide font-medium text-black/50',
'uppercase',
)}>
{label}
</div>}
</div>
);
const renderRecipeTitle =
<div className="flex items-center gap-1.5 w-full">
<TbChecklist
size={17}
className="opacity-80 translate-y-[1px]"
/>
<div className={clsx(
'text-[15px] uppercase',
'translate-y-[0.5px] tracking-wide',
'truncate max-w-full',
)}>
{title ? formatRecipe(title) : 'Recipe'}
</div>
</div>;
return (
<motion.div
ref={ref}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className={clsx(
'z-10',
'w-[20rem] p-3 space-y-3',
'scroll-mt-8',
'rounded-[10px]',
isOnPhoto
? 'shadow-2xl'
// Soften shadow to mimic <Modal />
: 'shadow-2xl/20 dark:shadow-2xl/100',
'text-[13.5px] text-black',
'bg-white/70 outline outline-neutral-400/15',
'backdrop-blur-xl saturate-[300%]',
)}
>
<div className={clsx(
'flex items-center gap-2 h-6',
'pl-1.5 pr-0.5',
'translate-y-[0.5px]',
)}>
<div className={clsx(
'grow translate-y-[0.5px]',
title && 'hover:opacity-50 active:opacity-75',
)}>
{title
? <PhotoRecipe
recipe={title}
contrast="frosted"
className={clsx(
'text-[15px]',
'[&>*>*>*>*]:text-black',
'tracking-wide',
)}
countOnHover={getRecipeCount(title)}
/>
: renderRecipeTitle}
</div>
<CopyButton
label={`${title
? `${formatRecipe(title).toLocaleUpperCase()} recipe`
: 'Recipe'}`}
text={generateRecipeText({ title, data, film })}
iconSize={17}
className={clsx(
'translate-y-[-1.5px]',
'text-black/40 active:text-black/75',
'hover:text-black/40',
)}
tooltip={appText.tooltip.recipeCopy}
tooltipColor="frosted"
/>
<span>
<LoaderButton
icon={<IoCloseCircle size={20} />}
onClick={onClose}
className={clsx(
'link p-0 m-0',
'text-black/40 active:text-black/75',
)}
/>
</span>
</div>
<div className="grid grid-cols-12 gap-2">
{/* ROW */}
<div className="col-span-8">
{renderDataSquare(
<div className="flex items-center gap-1.5">
<PhotoFilm
film={film}
contrast="frosted"
className={clsx(
'translate-y-[-0.5px]',
'*:text-black! *:active:text-black!',
'opacity-80 hover:opacity-60 active:opacity-80',
)}
badged={false}
countOnHover={getFilmCount(film)}
/>
</div>,
undefined,
'py-0.5',
)}
</div>
{renderDataSquare(
`DR${dynamicRange.development}`,
undefined,
'col-span-4 py-0.5',
)}
{/* ROW */}
{renderDataSquare(
whiteBalanceTypeFormatted.toUpperCase(),
`R${addSign(whiteBalance?.red)} / B${addSign(whiteBalance?.blue)}`,
'col-span-8',
)}
{renderDataSquare(
formatNoiseReduction(data),
'ISO NR',
'col-span-4',
)}
{/* ROW */}
{renderDataSquare(highlight, 'Highlight', 'col-span-6')}
{renderDataSquare(shadow, 'Shadow', 'col-span-6')}
{/* ROW */}
{renderDataSquare(color, 'Color')}
{renderDataSquare(sharpness, 'Sharpness')}
{renderDataSquare(clarity, 'Clarity')}
{/* ROW */}
{renderDataSquare(
colorChromeEffect?.toLocaleUpperCase() ?? 'N/A',
'Color Chrome',
'col-span-6',
)}
{renderDataSquare(
colorChromeFXBlue?.toLocaleUpperCase() ?? 'N/A',
'FX Blue',
'col-span-6',
)}
{/* ROW */}
{renderDataSquare(
formatGrain(data),
'grain',
'col-span-6',
)}
{renderDataSquare(bwAdjustment ?? 0, 'BW ADJ', 'col-span-3')}
{renderDataSquare(bwMagentaGreen ?? 0, 'BW M/G', 'col-span-3')}
</div>
</motion.div>
);
}