Add core recipe page components
This commit is contained in:
parent
0565eb93a5
commit
2b93dd750f
@ -1,6 +1,6 @@
|
|||||||
import SiteGrid from '@/components/SiteGrid';
|
import SiteGrid from '@/components/SiteGrid';
|
||||||
import { getPhoto, getPhotos } from '@/photo/db/query';
|
import { getPhoto, getPhotos } from '@/photo/db/query';
|
||||||
import PhotoRecipeOverlay from '@/photo/PhotoRecipeOverlay';
|
import PhotoRecipeOverlay from '@/recipe/PhotoRecipeOverlay';
|
||||||
|
|
||||||
export default async function AdminRecipePage({
|
export default async function AdminRecipePage({
|
||||||
params,
|
params,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { getPhoto, getPhotos } from '@/photo/db/query';
|
import { getPhoto, getPhotos } from '@/photo/db/query';
|
||||||
import PhotoRecipeOverlay from '@/photo/PhotoRecipeOverlay';
|
import PhotoRecipeOverlay from '@/recipe/PhotoRecipeOverlay';
|
||||||
|
|
||||||
export default async function AdminRecipePage() {
|
export default async function AdminRecipePage() {
|
||||||
const [
|
const [
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import AdminBatchEditPanel from '@/admin/AdminBatchEditPanel';
|
|||||||
import ShareModals from '@/share/ShareModals';
|
import ShareModals from '@/share/ShareModals';
|
||||||
import AdminUploadPanel from '@/admin/upload/AdminUploadPanel';
|
import AdminUploadPanel from '@/admin/upload/AdminUploadPanel';
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
|
import RecipeModal from '@/recipe/RecipeModal';
|
||||||
|
|
||||||
import '../tailwind.css';
|
import '../tailwind.css';
|
||||||
|
|
||||||
@ -86,6 +87,7 @@ export default function RootLayout({
|
|||||||
)}>
|
)}>
|
||||||
<Nav siteDomainOrTitle={SITE_DOMAIN_OR_TITLE} />
|
<Nav siteDomainOrTitle={SITE_DOMAIN_OR_TITLE} />
|
||||||
<ShareModals />
|
<ShareModals />
|
||||||
|
<RecipeModal />
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
'min-h-[16rem] sm:min-h-[30rem]',
|
'min-h-[16rem] sm:min-h-[30rem]',
|
||||||
'mb-12',
|
'mb-12',
|
||||||
|
|||||||
87
app/recipe/[recipe]/page.tsx
Normal file
87
app/recipe/[recipe]/page.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo';
|
||||||
|
import { getUniqueTags } from '@/photo/db/query';
|
||||||
|
import { IS_PRODUCTION } from '@/app/config';
|
||||||
|
import { STATICALLY_OPTIMIZED_PHOTO_CATEGORIES } from '@/app/config';
|
||||||
|
import { PATH_ROOT } from '@/app/paths';
|
||||||
|
import { getPhotosTagDataCached } from '@/tag/data';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import { cache } from 'react';
|
||||||
|
import { convertRecipeToTag, generateMetaForRecipe } from '@/recipe';
|
||||||
|
import RecipeOverview from '@/recipe/RecipeOverview';
|
||||||
|
|
||||||
|
const getPhotosTagDataCachedCached = cache((tag: string) =>
|
||||||
|
getPhotosTagDataCached({ tag, limit: INFINITE_SCROLL_GRID_INITIAL}));
|
||||||
|
|
||||||
|
export let generateStaticParams:
|
||||||
|
(() => Promise<{ recipe: string }[]>) | undefined = undefined;
|
||||||
|
|
||||||
|
if (STATICALLY_OPTIMIZED_PHOTO_CATEGORIES && IS_PRODUCTION) {
|
||||||
|
generateStaticParams = async () => {
|
||||||
|
const tags = await getUniqueTags();
|
||||||
|
return tags
|
||||||
|
.filter(({ tag }) => tag.startsWith('recipe'))
|
||||||
|
.map(({ tag }) => ({ recipe: tag.replace(/^recipe-?/i, '')}));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecipeProps {
|
||||||
|
params: Promise<{ recipe: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params,
|
||||||
|
}: RecipeProps): Promise<Metadata> {
|
||||||
|
const { recipe: recipeFromParams } = await params;
|
||||||
|
|
||||||
|
const recipe = decodeURIComponent(recipeFromParams);
|
||||||
|
|
||||||
|
const [
|
||||||
|
photos,
|
||||||
|
{ count, dateRange },
|
||||||
|
] = await getPhotosTagDataCachedCached(convertRecipeToTag(recipe));
|
||||||
|
|
||||||
|
if (photos.length === 0) { return {}; }
|
||||||
|
|
||||||
|
const {
|
||||||
|
url,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
images,
|
||||||
|
} = generateMetaForRecipe(recipe, photos, count, dateRange);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
openGraph: {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
images,
|
||||||
|
url,
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
images,
|
||||||
|
description,
|
||||||
|
card: 'summary_large_image',
|
||||||
|
},
|
||||||
|
description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function RecipePage({
|
||||||
|
params,
|
||||||
|
}:RecipeProps) {
|
||||||
|
const { recipe: recipeFromParams } = await params;
|
||||||
|
|
||||||
|
const recipe = decodeURIComponent(recipeFromParams);
|
||||||
|
|
||||||
|
const [
|
||||||
|
photos,
|
||||||
|
{ count, dateRange },
|
||||||
|
] = await getPhotosTagDataCachedCached(convertRecipeToTag(recipe));
|
||||||
|
|
||||||
|
if (photos.length === 0) { redirect(PATH_ROOT); }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RecipeOverview {...{ recipe, photos, count, dateRange }} />
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -24,6 +24,7 @@ export const PREFIX_TAG = '/tag';
|
|||||||
export const PREFIX_CAMERA = '/shot-on';
|
export const PREFIX_CAMERA = '/shot-on';
|
||||||
export const PREFIX_FILM_SIMULATION = '/film';
|
export const PREFIX_FILM_SIMULATION = '/film';
|
||||||
export const PREFIX_FOCAL_LENGTH = '/focal';
|
export const PREFIX_FOCAL_LENGTH = '/focal';
|
||||||
|
export const PREFIX_RECIPE = '/recipe';
|
||||||
|
|
||||||
// Dynamic paths
|
// Dynamic paths
|
||||||
const PATH_PHOTO_DYNAMIC = `${PREFIX_PHOTO}/[photoId]`;
|
const PATH_PHOTO_DYNAMIC = `${PREFIX_PHOTO}/[photoId]`;
|
||||||
@ -32,6 +33,7 @@ const PATH_CAMERA_DYNAMIC = `${PREFIX_CAMERA}/[make]/[model]`;
|
|||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
const PATH_FILM_SIMULATION_DYNAMIC = `${PREFIX_FILM_SIMULATION}/[simulation]`;
|
const PATH_FILM_SIMULATION_DYNAMIC = `${PREFIX_FILM_SIMULATION}/[simulation]`;
|
||||||
const PATH_FOCAL_LENGTH_DYNAMIC = `${PREFIX_FOCAL_LENGTH}/[focal]`;
|
const PATH_FOCAL_LENGTH_DYNAMIC = `${PREFIX_FOCAL_LENGTH}/[focal]`;
|
||||||
|
const PATH_RECIPE_DYNAMIC = `${PREFIX_RECIPE}/[recipe]`;
|
||||||
|
|
||||||
// Search params
|
// Search params
|
||||||
export const SEARCH_PARAM_SHOW = 'show';
|
export const SEARCH_PARAM_SHOW = 'show';
|
||||||
@ -80,6 +82,7 @@ export const PATHS_TO_CACHE = [
|
|||||||
PATH_CAMERA_DYNAMIC,
|
PATH_CAMERA_DYNAMIC,
|
||||||
PATH_FILM_SIMULATION_DYNAMIC,
|
PATH_FILM_SIMULATION_DYNAMIC,
|
||||||
PATH_FOCAL_LENGTH_DYNAMIC,
|
PATH_FOCAL_LENGTH_DYNAMIC,
|
||||||
|
PATH_RECIPE_DYNAMIC,
|
||||||
...PATHS_ADMIN,
|
...PATHS_ADMIN,
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -110,6 +113,7 @@ export const pathForPhoto = ({
|
|||||||
camera,
|
camera,
|
||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
|
recipe,
|
||||||
showRecipe,
|
showRecipe,
|
||||||
}: PhotoPathParams) => {
|
}: PhotoPathParams) => {
|
||||||
const path = typeof photo !== 'string' && photo.hidden
|
const path = typeof photo !== 'string' && photo.hidden
|
||||||
@ -122,6 +126,8 @@ export const pathForPhoto = ({
|
|||||||
? `${pathForFilmSimulation(simulation)}/${getPhotoId(photo)}`
|
? `${pathForFilmSimulation(simulation)}/${getPhotoId(photo)}`
|
||||||
: focal
|
: focal
|
||||||
? `${pathForFocalLength(focal)}/${getPhotoId(photo)}`
|
? `${pathForFocalLength(focal)}/${getPhotoId(photo)}`
|
||||||
|
: recipe
|
||||||
|
? `${pathForRecipe(recipe)}/${getPhotoId(photo)}`
|
||||||
: `${PREFIX_PHOTO}/${getPhotoId(photo)}`;
|
: `${PREFIX_PHOTO}/${getPhotoId(photo)}`;
|
||||||
return showRecipe
|
return showRecipe
|
||||||
? `${path}?${SEARCH_PARAM_SHOW}=${SEARCH_PARAM_SHOW_RECIPE}`
|
? `${path}?${SEARCH_PARAM_SHOW}=${SEARCH_PARAM_SHOW_RECIPE}`
|
||||||
@ -140,6 +146,9 @@ export const pathForFilmSimulation = (simulation: FilmSimulation) =>
|
|||||||
export const pathForFocalLength = (focal: number) =>
|
export const pathForFocalLength = (focal: number) =>
|
||||||
`${PREFIX_FOCAL_LENGTH}/${focal}mm`;
|
`${PREFIX_FOCAL_LENGTH}/${focal}mm`;
|
||||||
|
|
||||||
|
export const pathForRecipe = (recipe: string) =>
|
||||||
|
`${PREFIX_RECIPE}/${recipe}`;
|
||||||
|
|
||||||
export const absolutePathForPhoto = (params: PhotoPathParams) =>
|
export const absolutePathForPhoto = (params: PhotoPathParams) =>
|
||||||
`${BASE_URL}${pathForPhoto(params)}`;
|
`${BASE_URL}${pathForPhoto(params)}`;
|
||||||
|
|
||||||
@ -152,6 +161,9 @@ export const absolutePathForCamera= (camera: Camera) =>
|
|||||||
export const absolutePathForFilmSimulation = (simulation: FilmSimulation) =>
|
export const absolutePathForFilmSimulation = (simulation: FilmSimulation) =>
|
||||||
`${BASE_URL}${pathForFilmSimulation(simulation)}`;
|
`${BASE_URL}${pathForFilmSimulation(simulation)}`;
|
||||||
|
|
||||||
|
export const absolutePathForRecipe = (recipe: string) =>
|
||||||
|
`${BASE_URL}${pathForRecipe(recipe)}`;
|
||||||
|
|
||||||
export const absolutePathForFocalLength = (focal: number) =>
|
export const absolutePathForFocalLength = (focal: number) =>
|
||||||
`${BASE_URL}${pathForFocalLength(focal)}`;
|
`${BASE_URL}${pathForFocalLength(focal)}`;
|
||||||
|
|
||||||
@ -168,6 +180,9 @@ export const absolutePathForFilmSimulationImage =
|
|||||||
(simulation: FilmSimulation) =>
|
(simulation: FilmSimulation) =>
|
||||||
`${absolutePathForFilmSimulation(simulation)}/image`;
|
`${absolutePathForFilmSimulation(simulation)}/image`;
|
||||||
|
|
||||||
|
export const absolutePathForRecipeImage = (recipe: string) =>
|
||||||
|
`${absolutePathForRecipe(recipe)}/image`;
|
||||||
|
|
||||||
export const absolutePathForFocalLengthImage =
|
export const absolutePathForFocalLengthImage =
|
||||||
(focal: number) =>
|
(focal: number) =>
|
||||||
`${absolutePathForFocalLength(focal)}/image`;
|
`${absolutePathForFocalLength(focal)}/image`;
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export default function PhotoCamera({
|
|||||||
contrast,
|
contrast,
|
||||||
prefetch,
|
prefetch,
|
||||||
countOnHover,
|
countOnHover,
|
||||||
|
className,
|
||||||
}: {
|
}: {
|
||||||
camera: Camera
|
camera: Camera
|
||||||
hideAppleIcon?: boolean
|
hideAppleIcon?: boolean
|
||||||
@ -37,6 +38,7 @@ export default function PhotoCamera({
|
|||||||
className="translate-x-[-0.5px]"
|
className="translate-x-[-0.5px]"
|
||||||
/>}
|
/>}
|
||||||
type={type}
|
type={type}
|
||||||
|
className={className}
|
||||||
badged={badged}
|
badged={badged}
|
||||||
contrast={contrast}
|
contrast={contrast}
|
||||||
prefetch={prefetch}
|
prefetch={prefetch}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export default function Modal({
|
|||||||
onClose,
|
onClose,
|
||||||
className,
|
className,
|
||||||
anchor = 'center',
|
anchor = 'center',
|
||||||
|
container = true,
|
||||||
children,
|
children,
|
||||||
fast,
|
fast,
|
||||||
}: {
|
}: {
|
||||||
@ -23,6 +24,7 @@ export default function Modal({
|
|||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
className?: string
|
className?: string
|
||||||
anchor?: 'top' | 'center'
|
anchor?: 'top' | 'center'
|
||||||
|
container?: boolean
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
fast?: boolean
|
fast?: boolean
|
||||||
}) {
|
}) {
|
||||||
@ -80,11 +82,11 @@ export default function Modal({
|
|||||||
ref={contentRef}
|
ref={contentRef}
|
||||||
key="modalContent"
|
key="modalContent"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-[calc(100vw-1.5rem)] sm:w-[min(540px,90vw)]',
|
container && 'w-[calc(100vw-1.5rem)] sm:w-[min(540px,90vw)]',
|
||||||
'p-3 rounded-lg',
|
container && 'p-3 rounded-lg',
|
||||||
'md:p-4 md:rounded-xl',
|
container && 'md:p-4 md:rounded-xl',
|
||||||
|
container && 'dark:border dark:border-gray-800',
|
||||||
'bg-white dark:bg-black',
|
'bg-white dark:bg-black',
|
||||||
'dark:border dark:border-gray-800',
|
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export interface EntityLinkExternalProps {
|
|||||||
badged?: boolean
|
badged?: boolean
|
||||||
contrast?: ComponentProps<typeof Badge>['contrast']
|
contrast?: ComponentProps<typeof Badge>['contrast']
|
||||||
prefetch?: boolean
|
prefetch?: boolean
|
||||||
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EntityLink({
|
export default function EntityLink({
|
||||||
@ -65,13 +66,13 @@ export default function EntityLink({
|
|||||||
</>;
|
</>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="group inline-flex w-full">
|
<span className={clsx(
|
||||||
|
'group inline-flex max-w-full overflow-hidden',
|
||||||
|
className,
|
||||||
|
)}>
|
||||||
<LinkWithStatus
|
<LinkWithStatus
|
||||||
href={href}
|
href={href}
|
||||||
className={clsx(
|
className="inline-flex items-center gap-2 max-w-full"
|
||||||
'inline-flex items-center gap-2',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{({ isLoading }) => <>
|
{({ isLoading }) => <>
|
||||||
<LabeledIcon {...{
|
<LabeledIcon {...{
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export default function PhotoFocalLength({
|
|||||||
contrast,
|
contrast,
|
||||||
prefetch,
|
prefetch,
|
||||||
countOnHover,
|
countOnHover,
|
||||||
|
className,
|
||||||
}: {
|
}: {
|
||||||
focal: number
|
focal: number
|
||||||
countOnHover?: number
|
countOnHover?: number
|
||||||
@ -22,6 +23,7 @@ export default function PhotoFocalLength({
|
|||||||
href={pathForFocalLength(focal)}
|
href={pathForFocalLength(focal)}
|
||||||
icon={<TbCone className="rotate-[270deg]" />}
|
icon={<TbCone className="rotate-[270deg]" />}
|
||||||
type={type}
|
type={type}
|
||||||
|
className={className}
|
||||||
badged={badged}
|
badged={badged}
|
||||||
contrast={contrast}
|
contrast={contrast}
|
||||||
prefetch={prefetch}
|
prefetch={prefetch}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export default function PhotoGrid({
|
|||||||
camera,
|
camera,
|
||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
|
recipe,
|
||||||
photoPriority,
|
photoPriority,
|
||||||
fast,
|
fast,
|
||||||
animate = true,
|
animate = true,
|
||||||
@ -94,6 +95,7 @@ export default function PhotoGrid({
|
|||||||
camera,
|
camera,
|
||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
|
recipe,
|
||||||
selected: photo.id === selectedPhoto?.id,
|
selected: photo.id === selectedPhoto?.id,
|
||||||
priority: photoPriority,
|
priority: photoPriority,
|
||||||
onVisible: index === photos.length - 1
|
onVisible: index === photos.length - 1
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export default function PhotoGridContainer({
|
|||||||
camera,
|
camera,
|
||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
|
recipe,
|
||||||
animateOnFirstLoadOnly,
|
animateOnFirstLoadOnly,
|
||||||
header,
|
header,
|
||||||
sidebar,
|
sidebar,
|
||||||
@ -53,6 +54,7 @@ export default function PhotoGridContainer({
|
|||||||
camera,
|
camera,
|
||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
|
recipe,
|
||||||
animateOnFirstLoadOnly,
|
animateOnFirstLoadOnly,
|
||||||
onAnimationComplete,
|
onAnimationComplete,
|
||||||
canSelect,
|
canSelect,
|
||||||
@ -66,6 +68,7 @@ export default function PhotoGridContainer({
|
|||||||
camera,
|
camera,
|
||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
|
recipe,
|
||||||
animateOnFirstLoadOnly,
|
animateOnFirstLoadOnly,
|
||||||
canSelect,
|
canSelect,
|
||||||
}} />}
|
}} />}
|
||||||
|
|||||||
@ -38,11 +38,11 @@ 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 PhotoRecipe from './PhotoRecipe';
|
|
||||||
import { TbChecklist } from 'react-icons/tb';
|
import { TbChecklist } from 'react-icons/tb';
|
||||||
import { IoCloseSharp } from 'react-icons/io5';
|
import { IoCloseSharp } from 'react-icons/io5';
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import useRecipeState from './useRecipeState';
|
import useRecipeState from '../recipe/useRecipeState';
|
||||||
|
import PhotoRecipeGrid from '@/recipe/PhotoRecipeGrid';
|
||||||
|
|
||||||
export default function PhotoLarge({
|
export default function PhotoLarge({
|
||||||
photo,
|
photo,
|
||||||
@ -187,7 +187,7 @@ export default function PhotoLarge({
|
|||||||
{(shouldShowRecipe || shouldDebugRecipeOverlays) &&
|
{(shouldShowRecipe || shouldDebugRecipeOverlays) &&
|
||||||
photo.fujifilmRecipe &&
|
photo.fujifilmRecipe &&
|
||||||
photo.filmSimulation &&
|
photo.filmSimulation &&
|
||||||
<PhotoRecipe
|
<PhotoRecipeGrid
|
||||||
ref={refRecipe}
|
ref={refRecipe}
|
||||||
recipe={photo.fujifilmRecipe}
|
recipe={photo.fujifilmRecipe}
|
||||||
simulation={photo.filmSimulation}
|
simulation={photo.filmSimulation}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export default function PhotoLink({
|
|||||||
camera,
|
camera,
|
||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
|
recipe,
|
||||||
scroll,
|
scroll,
|
||||||
prefetch,
|
prefetch,
|
||||||
nextPhotoAnimation,
|
nextPhotoAnimation,
|
||||||
@ -32,7 +33,7 @@ export default function PhotoLink({
|
|||||||
return (
|
return (
|
||||||
photo
|
photo
|
||||||
? <Link
|
? <Link
|
||||||
href={pathForPhoto({ photo, tag, camera, simulation, focal })}
|
href={pathForPhoto({ photo, tag, camera, simulation, focal, recipe })}
|
||||||
prefetch={prefetch}
|
prefetch={prefetch}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (nextPhotoAnimation) {
|
if (nextPhotoAnimation) {
|
||||||
|
|||||||
@ -21,6 +21,7 @@ export default function PhotoMedium({
|
|||||||
camera,
|
camera,
|
||||||
simulation,
|
simulation,
|
||||||
focal,
|
focal,
|
||||||
|
recipe,
|
||||||
selected,
|
selected,
|
||||||
priority,
|
priority,
|
||||||
prefetch = SHOULD_PREFETCH_ALL_LINKS,
|
prefetch = SHOULD_PREFETCH_ALL_LINKS,
|
||||||
@ -41,7 +42,7 @@ export default function PhotoMedium({
|
|||||||
return (
|
return (
|
||||||
<LinkWithStatus
|
<LinkWithStatus
|
||||||
ref={ref}
|
ref={ref}
|
||||||
href={pathForPhoto({ photo, tag, camera, simulation, focal })}
|
href={pathForPhoto({ photo, tag, camera, simulation, focal, recipe })}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'active:brightness-75',
|
'active:brightness-75',
|
||||||
selected && 'brightness-50',
|
selected && 'brightness-50',
|
||||||
|
|||||||
@ -113,6 +113,7 @@ export interface PhotoSetCategory {
|
|||||||
camera?: Camera
|
camera?: Camera
|
||||||
simulation?: FilmSimulation
|
simulation?: FilmSimulation
|
||||||
focal?: number
|
focal?: number
|
||||||
|
recipe?: string
|
||||||
lens?: Lens // Unimplemented as a set
|
lens?: Lens // Unimplemented as a set
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
48
src/recipe/PhotoRecipe.tsx
Normal file
48
src/recipe/PhotoRecipe.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { pathForTag } from '@/app/paths';
|
||||||
|
import EntityLink, {
|
||||||
|
EntityLinkExternalProps,
|
||||||
|
} from '@/components/primitives/EntityLink';
|
||||||
|
import { TbChecklist } from 'react-icons/tb';
|
||||||
|
import { formatRecipe } from '.';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
export default function PhotoRecipe({
|
||||||
|
recipe,
|
||||||
|
type,
|
||||||
|
badged,
|
||||||
|
contrast,
|
||||||
|
prefetch,
|
||||||
|
countOnHover,
|
||||||
|
className,
|
||||||
|
recipeOnClick,
|
||||||
|
}: {
|
||||||
|
recipe: string
|
||||||
|
recipeOnClick?: () => void
|
||||||
|
countOnHover?: number
|
||||||
|
} & EntityLinkExternalProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full gap-2 h-[20.5px]">
|
||||||
|
<EntityLink
|
||||||
|
title="Recipe"
|
||||||
|
label={formatRecipe(recipe)}
|
||||||
|
href={pathForTag(recipe)}
|
||||||
|
icon={<TbChecklist size={16} />}
|
||||||
|
className={className}
|
||||||
|
type={type}
|
||||||
|
badged={badged}
|
||||||
|
contrast={contrast}
|
||||||
|
prefetch={prefetch}
|
||||||
|
hoverEntity={countOnHover}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={recipeOnClick}
|
||||||
|
className={clsx(
|
||||||
|
'px-1! py-0!',
|
||||||
|
'text-[11px] text-medium tracking-wider',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
OPEN
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,17 +1,16 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import LoaderButton from '@/components/primitives/LoaderButton';
|
import LoaderButton from '@/components/primitives/LoaderButton';
|
||||||
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
|
||||||
import { FilmSimulation } from '@/simulation';
|
|
||||||
import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
|
import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
|
||||||
import clsx from 'clsx/lite';
|
import clsx from 'clsx/lite';
|
||||||
import { ReactNode, RefObject } from 'react';
|
import { ReactNode, RefObject } from 'react';
|
||||||
import { IoCloseCircle } from 'react-icons/io5';
|
import { IoCloseCircle } from 'react-icons/io5';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
import { RecipeProps } from '.';
|
||||||
|
|
||||||
const addSign = (value = 0) => value < 0 ? value : `+${value}`;
|
const addSign = (value = 0) => value < 0 ? value : `+${value}`;
|
||||||
|
|
||||||
export default function PhotoRecipe({
|
export default function PhotoRecipeGrid({
|
||||||
ref,
|
ref,
|
||||||
recipe: {
|
recipe: {
|
||||||
dynamicRange,
|
dynamicRange,
|
||||||
@ -33,12 +32,8 @@ export default function PhotoRecipe({
|
|||||||
iso,
|
iso,
|
||||||
exposure,
|
exposure,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: RecipeProps & {
|
||||||
ref?: RefObject<HTMLDivElement | null>
|
ref?: RefObject<HTMLDivElement | null>
|
||||||
recipe: FujifilmRecipe
|
|
||||||
simulation: FilmSimulation
|
|
||||||
iso?: string
|
|
||||||
exposure?: string
|
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
}) {
|
}) {
|
||||||
const whiteBalanceTypeFormatted =
|
const whiteBalanceTypeFormatted =
|
||||||
150
src/recipe/PhotoRecipeOGTile.tsx
Normal file
150
src/recipe/PhotoRecipeOGTile.tsx
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||||
|
import { FilmSimulation } from '@/simulation';
|
||||||
|
import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
|
||||||
|
import clsx from 'clsx/lite';
|
||||||
|
import { ReactNode, RefObject } from 'react';
|
||||||
|
|
||||||
|
const addSign = (value = 0) => value < 0 ? value : `+${value}`;
|
||||||
|
|
||||||
|
export default function PhotoRecipeOGTile({
|
||||||
|
recipe: {
|
||||||
|
dynamicRange,
|
||||||
|
whiteBalance,
|
||||||
|
highISONoiseReduction,
|
||||||
|
noiseReductionBasic,
|
||||||
|
highlight,
|
||||||
|
shadow,
|
||||||
|
color,
|
||||||
|
sharpness,
|
||||||
|
clarity,
|
||||||
|
colorChromeEffect,
|
||||||
|
colorChromeFXBlue,
|
||||||
|
grainEffect,
|
||||||
|
bwAdjustment,
|
||||||
|
bwMagentaGreen,
|
||||||
|
},
|
||||||
|
simulation,
|
||||||
|
iso,
|
||||||
|
exposure,
|
||||||
|
}: {
|
||||||
|
ref?: RefObject<HTMLDivElement | null>
|
||||||
|
recipe: FujifilmRecipe
|
||||||
|
simulation: FilmSimulation
|
||||||
|
iso?: string
|
||||||
|
exposure?: string
|
||||||
|
onClose?: () => void
|
||||||
|
}) {
|
||||||
|
const whiteBalanceTypeFormatted =
|
||||||
|
whiteBalance.type === 'kelvin' && whiteBalance.colorTemperature
|
||||||
|
? `${whiteBalance.colorTemperature}K`
|
||||||
|
: whiteBalance.type
|
||||||
|
.replace(/auto./i, '')
|
||||||
|
.replaceAll('-', ' ');
|
||||||
|
|
||||||
|
const renderRow = (children: ReactNode, className?: string) =>
|
||||||
|
<div className={clsx(
|
||||||
|
'flex gap-2 *:w-full *:grow',
|
||||||
|
className,
|
||||||
|
)}>
|
||||||
|
{children}
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
const renderDataSquare = (
|
||||||
|
value: ReactNode,
|
||||||
|
label?: string,
|
||||||
|
className?: string,
|
||||||
|
) => (
|
||||||
|
<div className={clsx(
|
||||||
|
'flex flex-col items-center justify-center gap-0.5 rounded-md min-w-0',
|
||||||
|
'rounded-md border',
|
||||||
|
'border-transparent',
|
||||||
|
'bg-neutral-100/60',
|
||||||
|
label && 'p-1',
|
||||||
|
className,
|
||||||
|
)}>
|
||||||
|
<div className="truncate max-w-full tracking-wide text-lg">
|
||||||
|
{typeof value === 'number' ? addSign(value) : value}
|
||||||
|
</div>
|
||||||
|
{label && <div className={clsx(
|
||||||
|
'text-[11px] leading-none tracking-wide font-medium text-black/50',
|
||||||
|
'uppercase',
|
||||||
|
)}>
|
||||||
|
{label}
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'flex z-10',
|
||||||
|
'w-[37rem] p-10 aspect-video',
|
||||||
|
'text-[13.5px] text-black',
|
||||||
|
'bg-white/50',
|
||||||
|
'backdrop-blur-xl saturate-[300%]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
{renderRow(<>
|
||||||
|
<div className={clsx(
|
||||||
|
'flex',
|
||||||
|
'text-lg leading-none text-black truncate',
|
||||||
|
)}>
|
||||||
|
KODAK PORTRA 500
|
||||||
|
</div>
|
||||||
|
<PhotoFilmSimulation
|
||||||
|
contrast="frosted"
|
||||||
|
simulation={simulation}
|
||||||
|
className="w-auto! grow-0!"
|
||||||
|
/>
|
||||||
|
</>, 'flex items-center gap-4')}
|
||||||
|
{renderRow(<>
|
||||||
|
{renderDataSquare(`DR${dynamicRange.development}`)}
|
||||||
|
{renderDataSquare(iso)}
|
||||||
|
{renderDataSquare(exposure ?? '0ev')}
|
||||||
|
</>)}
|
||||||
|
{renderRow(<>
|
||||||
|
{renderDataSquare(
|
||||||
|
whiteBalanceTypeFormatted.toUpperCase(),
|
||||||
|
`R${addSign(whiteBalance?.red)} / B${addSign(whiteBalance?.blue)}`,
|
||||||
|
)}
|
||||||
|
{renderDataSquare(
|
||||||
|
highISONoiseReduction ?? noiseReductionBasic ?? 'OFF',
|
||||||
|
'ISO NR',
|
||||||
|
)}
|
||||||
|
{renderDataSquare(highlight, 'Highlight')}
|
||||||
|
{renderDataSquare(shadow, 'Shadow')}
|
||||||
|
</>)}
|
||||||
|
{renderRow(<>
|
||||||
|
{renderDataSquare(color, 'Color')}
|
||||||
|
{renderDataSquare(sharpness, 'Sharpness')}
|
||||||
|
{renderDataSquare(clarity, 'Clarity')}
|
||||||
|
</>)}
|
||||||
|
{renderRow(<>
|
||||||
|
{renderDataSquare(
|
||||||
|
colorChromeEffect?.toLocaleUpperCase() ?? 'N/A',
|
||||||
|
'Color Chrome',
|
||||||
|
)}
|
||||||
|
{renderDataSquare(
|
||||||
|
colorChromeFXBlue?.toLocaleUpperCase() ?? 'N/A',
|
||||||
|
'FX Blue',
|
||||||
|
)}
|
||||||
|
</>)}
|
||||||
|
{renderRow(<>
|
||||||
|
{renderDataSquare(
|
||||||
|
grainEffect.roughness.toLocaleUpperCase(),
|
||||||
|
grainEffect.size === 'large'
|
||||||
|
? 'Large Grain'
|
||||||
|
: grainEffect.size === 'small'
|
||||||
|
? 'Small Grain'
|
||||||
|
: 'Grain',
|
||||||
|
)}
|
||||||
|
{renderDataSquare(bwAdjustment ?? 0, 'BW ADJ')}
|
||||||
|
{renderDataSquare(bwMagentaGreen ?? 0, 'BW M/G')}
|
||||||
|
</>)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -3,8 +3,8 @@
|
|||||||
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||||
import clsx from 'clsx/lite';
|
import clsx from 'clsx/lite';
|
||||||
import ImageLarge from '@/components/image/ImageLarge';
|
import ImageLarge from '@/components/image/ImageLarge';
|
||||||
import PhotoRecipe from './PhotoRecipe';
|
import PhotoRecipeGrid from './PhotoRecipeGrid';
|
||||||
import { Photo } from '.';
|
import { Photo } from '../photo';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
export default function PhotoRecipeOverlay({
|
export default function PhotoRecipeOverlay({
|
||||||
photos,
|
photos,
|
||||||
@ -40,7 +40,7 @@ export default function PhotoRecipeOverlay({
|
|||||||
'absolute inset-0 w-full h-full',
|
'absolute inset-0 w-full h-full',
|
||||||
'flex items-center justify-center',
|
'flex items-center justify-center',
|
||||||
)}>
|
)}>
|
||||||
<PhotoRecipe {...{
|
<PhotoRecipeGrid {...{
|
||||||
recipe,
|
recipe,
|
||||||
simulation: photo.filmSimulation ?? 'provia',
|
simulation: photo.filmSimulation ?? 'provia',
|
||||||
exposure: photo.exposureCompensationFormatted ?? '+0ev',
|
exposure: photo.exposureCompensationFormatted ?? '+0ev',
|
||||||
54
src/recipe/RecipeHeader.tsx
Normal file
54
src/recipe/RecipeHeader.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Photo, PhotoDateRange } from '@/photo';
|
||||||
|
import { descriptionForTaggedPhotos } from '../tag';
|
||||||
|
import PhotoHeader from '@/photo/PhotoHeader';
|
||||||
|
import PhotoRecipe from './PhotoRecipe';
|
||||||
|
import { useAppState } from '@/state/AppState';
|
||||||
|
export default function RecipeHeader({
|
||||||
|
recipe,
|
||||||
|
photos,
|
||||||
|
selectedPhoto,
|
||||||
|
indexNumber,
|
||||||
|
count,
|
||||||
|
dateRange,
|
||||||
|
}: {
|
||||||
|
recipe: string
|
||||||
|
photos: Photo[]
|
||||||
|
selectedPhoto?: Photo
|
||||||
|
indexNumber?: number
|
||||||
|
count?: number
|
||||||
|
dateRange?: PhotoDateRange
|
||||||
|
}) {
|
||||||
|
const { setRecipeModalProps } = useAppState();
|
||||||
|
|
||||||
|
const photo = photos.find(({ filmSimulation, fujifilmRecipe }) =>
|
||||||
|
fujifilmRecipe && filmSimulation);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PhotoHeader
|
||||||
|
tag={recipe}
|
||||||
|
entity={<PhotoRecipe
|
||||||
|
recipe={recipe}
|
||||||
|
contrast="high"
|
||||||
|
recipeOnClick={() => (
|
||||||
|
photo?.fujifilmRecipe &&
|
||||||
|
photo?.filmSimulation
|
||||||
|
) ? setRecipeModalProps?.({
|
||||||
|
simulation: photo.filmSimulation,
|
||||||
|
recipe: photo.fujifilmRecipe,
|
||||||
|
iso: photo.isoFormatted,
|
||||||
|
exposure: photo.exposureTimeFormatted,
|
||||||
|
})
|
||||||
|
: undefined}
|
||||||
|
/>}
|
||||||
|
entityDescription={descriptionForTaggedPhotos(photos, undefined, count)}
|
||||||
|
photos={photos}
|
||||||
|
selectedPhoto={selectedPhoto}
|
||||||
|
indexNumber={indexNumber}
|
||||||
|
count={count}
|
||||||
|
dateRange={dateRange}
|
||||||
|
includeShareButton
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
src/recipe/RecipeModal.tsx
Normal file
21
src/recipe/RecipeModal.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Modal from '@/components/Modal';
|
||||||
|
import { useAppState } from '@/state/AppState';
|
||||||
|
import PhotoRecipeOGTile from './PhotoRecipeOGTile';
|
||||||
|
|
||||||
|
export default function ShareModals() {
|
||||||
|
const {
|
||||||
|
recipeModalProps,
|
||||||
|
setRecipeModalProps,
|
||||||
|
} = useAppState();
|
||||||
|
|
||||||
|
if (recipeModalProps) {
|
||||||
|
return <Modal
|
||||||
|
onClose={() => setRecipeModalProps?.(undefined)}
|
||||||
|
container={false}
|
||||||
|
>
|
||||||
|
<PhotoRecipeOGTile {...recipeModalProps}/>
|
||||||
|
</Modal>;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/recipe/RecipeOverview.tsx
Normal file
33
src/recipe/RecipeOverview.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Photo, PhotoDateRange } from '@/photo';
|
||||||
|
import PhotoGridContainer from '@/photo/PhotoGridContainer';
|
||||||
|
import RecipeHeader from './RecipeHeader';
|
||||||
|
|
||||||
|
export default function RecipeOverview({
|
||||||
|
recipe,
|
||||||
|
photos,
|
||||||
|
count,
|
||||||
|
dateRange,
|
||||||
|
animateOnFirstLoadOnly,
|
||||||
|
}: {
|
||||||
|
recipe: string,
|
||||||
|
photos: Photo[],
|
||||||
|
count: number,
|
||||||
|
dateRange?: PhotoDateRange,
|
||||||
|
animateOnFirstLoadOnly?: boolean,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<PhotoGridContainer {...{
|
||||||
|
cacheKey: `recipe-${recipe}`,
|
||||||
|
photos,
|
||||||
|
count,
|
||||||
|
recipe,
|
||||||
|
header: <RecipeHeader {...{
|
||||||
|
recipe,
|
||||||
|
photos,
|
||||||
|
count,
|
||||||
|
dateRange,
|
||||||
|
}} />,
|
||||||
|
animateOnFirstLoadOnly,
|
||||||
|
}} />
|
||||||
|
);
|
||||||
|
}
|
||||||
66
src/recipe/index.ts
Normal file
66
src/recipe/index.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { absolutePathForRecipe, absolutePathForRecipeImage } from '@/app/paths';
|
||||||
|
import { Photo, photoQuantityText } from '@/photo';
|
||||||
|
import { PhotoDateRange } from '@/photo';
|
||||||
|
import {
|
||||||
|
descriptionForTaggedPhotos,
|
||||||
|
isTagFavs,
|
||||||
|
isTagHidden,
|
||||||
|
Tags,
|
||||||
|
} from '../tag';
|
||||||
|
import { convertStringToArray, parameterize } from '@/utility/string';
|
||||||
|
import { capitalizeWords } from '@/utility/string';
|
||||||
|
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||||
|
import { FilmSimulation } from '@/simulation';
|
||||||
|
|
||||||
|
const KEY_RECIPE = 'recipe';
|
||||||
|
|
||||||
|
export interface RecipeProps {
|
||||||
|
recipe: FujifilmRecipe
|
||||||
|
simulation: FilmSimulation
|
||||||
|
iso?: string
|
||||||
|
exposure?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const convertTagsToRecipes = (tags: Tags) =>
|
||||||
|
tags.filter(({ tag }) => tag.startsWith(KEY_RECIPE))
|
||||||
|
.map(({ tag }) => convertTagToRecipe(tag));
|
||||||
|
|
||||||
|
export const convertRecipeToTag = (recipe: string) =>
|
||||||
|
`${KEY_RECIPE}-${parameterize(recipe)}`;
|
||||||
|
|
||||||
|
export const convertTagToRecipe = (tag: string) =>
|
||||||
|
tag.replace(new RegExp(`^${KEY_RECIPE}-?`), '');
|
||||||
|
|
||||||
|
export const formatRecipe = (recipe?: string) =>
|
||||||
|
capitalizeWords(recipe?.replaceAll('-', ' '));
|
||||||
|
|
||||||
|
export const getValidationMessageForTags = (tags?: string) => {
|
||||||
|
const reservedTags = (convertStringToArray(tags) ?? [])
|
||||||
|
.filter(tag => isTagFavs(tag) || isTagHidden(tag))
|
||||||
|
.map(tag => tag.toLocaleUpperCase());
|
||||||
|
return reservedTags.length
|
||||||
|
? `Reserved tags: ${reservedTags.join(', ').toLocaleLowerCase()}`
|
||||||
|
: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const titleForRecipe = (
|
||||||
|
recipe: string,
|
||||||
|
photos:Photo[] = [],
|
||||||
|
explicitCount?: number,
|
||||||
|
) => [
|
||||||
|
`Recipe: ${formatRecipe(recipe)}`,
|
||||||
|
photoQuantityText(explicitCount ?? photos.length),
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
|
export const generateMetaForRecipe = (
|
||||||
|
recipe: string,
|
||||||
|
photos: Photo[],
|
||||||
|
explicitCount?: number,
|
||||||
|
explicitDateRange?: PhotoDateRange,
|
||||||
|
) => ({
|
||||||
|
url: absolutePathForRecipe(recipe),
|
||||||
|
title: titleForRecipe(recipe, photos, explicitCount),
|
||||||
|
description:
|
||||||
|
descriptionForTaggedPhotos(photos, true, explicitCount, explicitDateRange),
|
||||||
|
images: absolutePathForRecipeImage(recipe),
|
||||||
|
});
|
||||||
@ -15,11 +15,11 @@ export default function PhotoFilmSimulation({
|
|||||||
contrast = 'low',
|
contrast = 'low',
|
||||||
prefetch,
|
prefetch,
|
||||||
countOnHover,
|
countOnHover,
|
||||||
|
className,
|
||||||
}: {
|
}: {
|
||||||
simulation: FilmSimulation
|
simulation: FilmSimulation
|
||||||
countOnHover?: number
|
countOnHover?: number
|
||||||
recipe?: FujifilmRecipe
|
recipe?: FujifilmRecipe
|
||||||
className?: string
|
|
||||||
} & EntityLinkExternalProps) {
|
} & EntityLinkExternalProps) {
|
||||||
const { small, medium, large } = labelForFilmSimulation(simulation);
|
const { small, medium, large } = labelForFilmSimulation(simulation);
|
||||||
|
|
||||||
@ -34,6 +34,7 @@ export default function PhotoFilmSimulation({
|
|||||||
/>}
|
/>}
|
||||||
title={`Film Simulation: ${large}`}
|
title={`Film Simulation: ${large}`}
|
||||||
type={type}
|
type={type}
|
||||||
|
className={className}
|
||||||
badged={badged}
|
badged={badged}
|
||||||
contrast={contrast}
|
contrast={contrast}
|
||||||
prefetch={prefetch}
|
prefetch={prefetch}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { ShareModalProps } from '@/share';
|
|||||||
import { InsightsIndicatorStatus } from '@/admin/insights';
|
import { InsightsIndicatorStatus } from '@/admin/insights';
|
||||||
import { INITIAL_UPLOAD_STATE, UploadState } from '@/admin/upload';
|
import { INITIAL_UPLOAD_STATE, UploadState } from '@/admin/upload';
|
||||||
import { AdminData } from '@/admin/actions';
|
import { AdminData } from '@/admin/actions';
|
||||||
|
import { RecipeProps } from '@/recipe';
|
||||||
|
|
||||||
export type AppStateContext = {
|
export type AppStateContext = {
|
||||||
// CORE
|
// CORE
|
||||||
@ -34,6 +35,8 @@ export type AppStateContext = {
|
|||||||
setIsCommandKOpen?: Dispatch<SetStateAction<boolean>>
|
setIsCommandKOpen?: Dispatch<SetStateAction<boolean>>
|
||||||
shareModalProps?: ShareModalProps
|
shareModalProps?: ShareModalProps
|
||||||
setShareModalProps?: Dispatch<SetStateAction<ShareModalProps | undefined>>
|
setShareModalProps?: Dispatch<SetStateAction<ShareModalProps | undefined>>
|
||||||
|
recipeModalProps?: RecipeProps
|
||||||
|
setRecipeModalProps?: Dispatch<SetStateAction<RecipeProps | undefined>>
|
||||||
// AUTH
|
// AUTH
|
||||||
userEmail?: string
|
userEmail?: string
|
||||||
setUserEmail?: Dispatch<SetStateAction<string | undefined>>
|
setUserEmail?: Dispatch<SetStateAction<string | undefined>>
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import {
|
|||||||
import { useRouter, usePathname } from 'next/navigation';
|
import { useRouter, usePathname } from 'next/navigation';
|
||||||
import { isPathAdmin, PATH_SIGN_IN } from '@/app/paths';
|
import { isPathAdmin, PATH_SIGN_IN } from '@/app/paths';
|
||||||
import { INITIAL_UPLOAD_STATE, UploadState } from '@/admin/upload';
|
import { INITIAL_UPLOAD_STATE, UploadState } from '@/admin/upload';
|
||||||
|
import { RecipeProps } from '@/recipe';
|
||||||
|
|
||||||
export default function AppStateProvider({
|
export default function AppStateProvider({
|
||||||
children,
|
children,
|
||||||
@ -53,6 +54,8 @@ export default function AppStateProvider({
|
|||||||
useState(false);
|
useState(false);
|
||||||
const [shareModalProps, setShareModalProps] =
|
const [shareModalProps, setShareModalProps] =
|
||||||
useState<ShareModalProps>();
|
useState<ShareModalProps>();
|
||||||
|
const [recipeModalProps, setRecipeModalProps] =
|
||||||
|
useState<RecipeProps>();
|
||||||
// AUTH
|
// AUTH
|
||||||
const [userEmail, setUserEmail] =
|
const [userEmail, setUserEmail] =
|
||||||
useState<string>();
|
useState<string>();
|
||||||
@ -170,6 +173,8 @@ export default function AppStateProvider({
|
|||||||
setIsCommandKOpen,
|
setIsCommandKOpen,
|
||||||
shareModalProps,
|
shareModalProps,
|
||||||
setShareModalProps,
|
setShareModalProps,
|
||||||
|
recipeModalProps,
|
||||||
|
setRecipeModalProps,
|
||||||
// AUTH
|
// AUTH
|
||||||
userEmail,
|
userEmail,
|
||||||
setUserEmail,
|
setUserEmail,
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export default function FavsTag({
|
|||||||
contrast,
|
contrast,
|
||||||
prefetch,
|
prefetch,
|
||||||
countOnHover,
|
countOnHover,
|
||||||
|
className,
|
||||||
}: {
|
}: {
|
||||||
countOnHover?: number
|
countOnHover?: number
|
||||||
} & EntityLinkExternalProps) {
|
} & EntityLinkExternalProps) {
|
||||||
@ -36,6 +37,7 @@ export default function FavsTag({
|
|||||||
)}
|
)}
|
||||||
/>}
|
/>}
|
||||||
type={type}
|
type={type}
|
||||||
|
className={className}
|
||||||
hoverEntity={countOnHover}
|
hoverEntity={countOnHover}
|
||||||
badged={badged}
|
badged={badged}
|
||||||
contrast={contrast}
|
contrast={contrast}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export default function HiddenTag({
|
|||||||
contrast,
|
contrast,
|
||||||
prefetch,
|
prefetch,
|
||||||
countOnHover,
|
countOnHover,
|
||||||
|
className,
|
||||||
}: {
|
}: {
|
||||||
countOnHover?: number
|
countOnHover?: number
|
||||||
} & EntityLinkExternalProps) {
|
} & EntityLinkExternalProps) {
|
||||||
@ -28,6 +29,7 @@ export default function HiddenTag({
|
|||||||
href={pathForTag(TAG_HIDDEN)}
|
href={pathForTag(TAG_HIDDEN)}
|
||||||
icon={!badged && <AiOutlineEyeInvisible size={16} />}
|
icon={!badged && <AiOutlineEyeInvisible size={16} />}
|
||||||
type={type}
|
type={type}
|
||||||
|
className={className}
|
||||||
hoverEntity={countOnHover}
|
hoverEntity={countOnHover}
|
||||||
badged={badged}
|
badged={badged}
|
||||||
contrast={contrast}
|
contrast={contrast}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export default function PhotoTag({
|
|||||||
contrast,
|
contrast,
|
||||||
prefetch,
|
prefetch,
|
||||||
countOnHover,
|
countOnHover,
|
||||||
|
className,
|
||||||
}: {
|
}: {
|
||||||
tag: string
|
tag: string
|
||||||
countOnHover?: number
|
countOnHover?: number
|
||||||
@ -25,6 +26,7 @@ export default function PhotoTag({
|
|||||||
className="translate-y-[0.5px]"
|
className="translate-y-[0.5px]"
|
||||||
/>}
|
/>}
|
||||||
type={type}
|
type={type}
|
||||||
|
className={className}
|
||||||
badged={badged}
|
badged={badged}
|
||||||
contrast={contrast}
|
contrast={contrast}
|
||||||
prefetch={prefetch}
|
prefetch={prefetch}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user