Merge pull request #28 from sambecker/universal-tags
Universal Photo Set Links
This commit is contained in:
commit
35d4a89955
18
package.json
18
package.json
@ -9,26 +9,26 @@
|
||||
"analyze": "ANALYZE=true next build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.470.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.470.0",
|
||||
"@aws-sdk/client-s3": "3.474.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.474.0",
|
||||
"@next/bundle-analyzer": "14.0.4",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "^20.10.4",
|
||||
"@types/react": "18.2.43",
|
||||
"@types/react-dom": "18.2.17",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.2",
|
||||
"@typescript-eslint/parser": "^6.13.2",
|
||||
"@types/react": "18.2.45",
|
||||
"@types/react-dom": "18.2.18",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"@vercel/analytics": "^1.1.1",
|
||||
"@vercel/blob": "^0.16.0",
|
||||
"@vercel/blob": "^0.16.1",
|
||||
"@vercel/postgres": "0.5.1",
|
||||
"@vercel/speed-insights": "^1.0.1",
|
||||
"@vercel/speed-insights": "^1.0.2",
|
||||
"autoprefixer": "10.4.16",
|
||||
"camelcase-keys": "^9.1.2",
|
||||
"date-fns": "^2.30.0",
|
||||
"eslint": "8.55.0",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-next": "14.0.4",
|
||||
"exifr": "^7.1.3",
|
||||
"framer-motion": "^10.16.16",
|
||||
|
||||
2089
pnpm-lock.yaml
generated
2089
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -52,7 +52,12 @@ export default async function GridPage({ searchParams }: PaginationParams) {
|
||||
? <SiteGrid
|
||||
contentMain={<PhotoGrid {...{ photos, showMorePath }} />}
|
||||
contentSide={<div className="sticky top-4 space-y-4">
|
||||
<PhotoGridSidebar {...{ tags, cameras, simulations, photosCount }} />
|
||||
<PhotoGridSidebar {...{
|
||||
tags,
|
||||
cameras,
|
||||
simulations,
|
||||
photosCount,
|
||||
}} />
|
||||
</div>}
|
||||
sideHiddenOnMobile
|
||||
/>
|
||||
|
||||
7
src/cache/index.ts
vendored
7
src/cache/index.ts
vendored
@ -20,6 +20,7 @@ import {
|
||||
getUniqueFilmSimulations,
|
||||
getPhotosFilmSimulationDateRange,
|
||||
getPhotosFilmSimulationCount,
|
||||
getPhotosDateRange,
|
||||
} from '@/services/vercel-postgres';
|
||||
import { parseCachedPhotoDates, parseCachedPhotosDates } from '@/photo';
|
||||
import { getBlobPhotoUrls, getBlobUploadUrls } from '@/services/blob';
|
||||
@ -118,6 +119,12 @@ export const getPhotosCached = (
|
||||
[KEY_PHOTOS, ...getPhotosCacheKeys(...args)],
|
||||
)(...args).then(parseCachedPhotosDates);
|
||||
|
||||
export const getPhotosDateRangeCached =
|
||||
unstable_cache(
|
||||
getPhotosDateRange,
|
||||
[KEY_PHOTOS, KEY_DATE_RANGE],
|
||||
);
|
||||
|
||||
export const getPhotosCountCached =
|
||||
unstable_cache(
|
||||
getPhotosCount,
|
||||
|
||||
@ -21,7 +21,7 @@ export default function CameraHeader({
|
||||
const camera = cameraFromPhoto(photos[0], cameraProp);
|
||||
return (
|
||||
<PhotoSetHeader
|
||||
entity={<PhotoCamera {...{ camera }} />}
|
||||
entity={<PhotoCamera {...{ camera }} hideAppleIcon />}
|
||||
entityVerb="Photo"
|
||||
entityDescription={
|
||||
descriptionForCameraPhotos(photos, undefined, count, dateRange)}
|
||||
|
||||
@ -1,56 +1,52 @@
|
||||
import { AiFillApple } from 'react-icons/ai';
|
||||
import { cc } from '@/utility/css';
|
||||
import Link from 'next/link';
|
||||
import { pathForCamera } from '@/site/paths';
|
||||
import { IoMdCamera } from 'react-icons/io';
|
||||
import { Camera } from '.';
|
||||
import EntityLink, { EntityLinkExternalProps } from '@/components/EntityLink';
|
||||
import { cc } from '@/utility/css';
|
||||
|
||||
export default function PhotoCamera({
|
||||
camera,
|
||||
showIcon = true,
|
||||
hideApple = true,
|
||||
hideAppleIcon,
|
||||
type = 'icon-first',
|
||||
badged,
|
||||
dim,
|
||||
countOnHover,
|
||||
}: {
|
||||
camera: Camera
|
||||
showIcon?: boolean
|
||||
hideApple?: boolean
|
||||
hideAppleIcon?: boolean
|
||||
countOnHover?: number
|
||||
}) {
|
||||
} & EntityLinkExternalProps) {
|
||||
const isCameraApple = camera.make?.toLowerCase() === 'apple';
|
||||
const showAppleIcon = !hideAppleIcon && isCameraApple;
|
||||
|
||||
return (
|
||||
<span className="group">
|
||||
<Link
|
||||
href={pathForCamera(camera)}
|
||||
className={cc(
|
||||
'inline-flex items-center self-start',
|
||||
'uppercase',
|
||||
'hover:text-gray-900 dark:hover:text-gray-100',
|
||||
)}
|
||||
>
|
||||
{showIcon && <>
|
||||
<IoMdCamera
|
||||
size={13}
|
||||
className="text-icon translate-y-[-0.25px]"
|
||||
/>
|
||||
|
||||
</>}
|
||||
{!(hideApple && camera.make?.toLowerCase() === 'apple') &&
|
||||
<>
|
||||
{camera.make?.toLowerCase() === 'apple'
|
||||
? <AiFillApple
|
||||
title="Apple"
|
||||
className="text-icon translate-y-[-0.5px]"
|
||||
size={14}
|
||||
/>
|
||||
: camera.make}
|
||||
|
||||
</>}
|
||||
<EntityLink
|
||||
label={<>
|
||||
{!isCameraApple && <>{camera.make} </>}
|
||||
{camera.model}
|
||||
</Link>
|
||||
{countOnHover !== undefined &&
|
||||
<span className="hidden group-hover:inline">
|
||||
{' '}
|
||||
{countOnHover}
|
||||
</span>}
|
||||
</span>
|
||||
</>}
|
||||
href={pathForCamera(camera)}
|
||||
icon={showAppleIcon
|
||||
? <AiFillApple
|
||||
title="Apple"
|
||||
className={cc(
|
||||
'text-icon',
|
||||
'translate-x-[-2.5px] translate-y-[2px]',
|
||||
)}
|
||||
size={15}
|
||||
/>
|
||||
: <IoMdCamera
|
||||
size={13}
|
||||
className={cc(
|
||||
'text-icon',
|
||||
'translate-x-[-1px] translate-y-[3.5px]',
|
||||
)}
|
||||
/>}
|
||||
type={showAppleIcon && isCameraApple ? 'icon-first' : type}
|
||||
badged={badged}
|
||||
dim={dim}
|
||||
hoverEntity={countOnHover}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -11,16 +11,16 @@ export default function Badge({
|
||||
uppercase?: boolean
|
||||
interactive?: boolean
|
||||
}) {
|
||||
const coreStyles = () => {
|
||||
const stylesForType = () => {
|
||||
switch (type) {
|
||||
case 'primary': return cc(
|
||||
'px-1.5 py-[0.3rem] leading-none rounded-md',
|
||||
'px-1.5 py-[0.3rem] rounded-md',
|
||||
'bg-gray-100/80 dark:bg-gray-900/80',
|
||||
'border border-gray-200/60 dark:border-gray-800/75'
|
||||
);
|
||||
case 'secondary': return cc(
|
||||
'px-[0.3rem] py-1 leading-none rounded-[0.25rem]',
|
||||
'bg-gray-100 dark:bg-gray-800/60',
|
||||
'px-[0.3rem] py-1 rounded-[0.25rem]',
|
||||
'bg-gray-300/30 dark:bg-gray-700/50',
|
||||
'text-medium',
|
||||
'font-medium text-[0.7rem]',
|
||||
interactive && 'hover:text-gray-900 dark:hover:text-gray-100',
|
||||
@ -30,7 +30,8 @@ export default function Badge({
|
||||
};
|
||||
return (
|
||||
<span className={cc(
|
||||
coreStyles(),
|
||||
'leading-none',
|
||||
stylesForType(),
|
||||
uppercase && 'uppercase tracking-wider',
|
||||
)}>
|
||||
{children}
|
||||
|
||||
77
src/components/EntityLink.tsx
Normal file
77
src/components/EntityLink.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import Link from 'next/link';
|
||||
import { ReactNode } from 'react';
|
||||
import Badge from './Badge';
|
||||
import { cc } from '@/utility/css';
|
||||
|
||||
export interface EntityLinkExternalProps {
|
||||
type?: 'icon-last' | 'icon-first' | 'icon-only' | 'text-only'
|
||||
badged?: boolean
|
||||
dim?: boolean
|
||||
}
|
||||
|
||||
export default function EntityLink({
|
||||
label,
|
||||
labelSmall,
|
||||
href,
|
||||
icon,
|
||||
title,
|
||||
type = 'icon-first',
|
||||
badged,
|
||||
hoverEntity,
|
||||
dim,
|
||||
}: {
|
||||
label: ReactNode
|
||||
labelSmall?: ReactNode
|
||||
href: string
|
||||
icon?: ReactNode
|
||||
title?: string
|
||||
hoverEntity?: ReactNode
|
||||
} & EntityLinkExternalProps) {
|
||||
const renderLabel = () => <>
|
||||
<span className="xs:hidden">
|
||||
{labelSmall ?? label}
|
||||
</span>
|
||||
<span className="hidden xs:inline-block">
|
||||
{label}
|
||||
</span>
|
||||
</>;
|
||||
|
||||
return (
|
||||
<span className="group inline-flex items-center gap-2 overflow-hidden">
|
||||
<Link
|
||||
href={href}
|
||||
title={title}
|
||||
className={cc(
|
||||
'inline-flex gap-[0.23rem]',
|
||||
!badged && 'text-main hover:text-gray-900 dark:hover:text-gray-100',
|
||||
dim && 'text-dim',
|
||||
)}
|
||||
>
|
||||
{type !== 'icon-only' && <>
|
||||
{badged
|
||||
? <span className="h-6 inline-flex items-center">
|
||||
<Badge type="secondary" uppercase interactive>
|
||||
{renderLabel()}
|
||||
</Badge>
|
||||
</span>
|
||||
: <span className="uppercase">
|
||||
{renderLabel()}
|
||||
</span>}
|
||||
</>}
|
||||
{icon && type !== 'text-only' &&
|
||||
<span className={cc(
|
||||
'flex-shrink-0',
|
||||
'text-dim inline-flex min-w-[0.9rem]',
|
||||
type === 'icon-first' && 'order-first',
|
||||
badged && 'translate-y-[4px]',
|
||||
)}>
|
||||
{icon}
|
||||
</span>}
|
||||
</Link>
|
||||
{hoverEntity !== undefined &&
|
||||
<span className="hidden group-hover:inline">
|
||||
{hoverEntity}
|
||||
</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@ -20,20 +20,21 @@ export default function HeaderList({
|
||||
duration={0.5}
|
||||
staggerDelay={0.05}
|
||||
items={(title || icon
|
||||
? [
|
||||
<div key="header" className={cc(
|
||||
? [<div
|
||||
key="header"
|
||||
className={cc(
|
||||
'text-gray-900',
|
||||
'dark:text-gray-100',
|
||||
'flex items-center mb-0.5',
|
||||
'flex items-center mb-0.5 gap-1',
|
||||
'uppercase',
|
||||
)}>
|
||||
{icon &&
|
||||
<span className="w-[17px]">
|
||||
{icon}
|
||||
</span>}
|
||||
{title}
|
||||
</div>,
|
||||
]
|
||||
)}
|
||||
>
|
||||
{icon &&
|
||||
<span className="w-[1rem]">
|
||||
{icon}
|
||||
</span>}
|
||||
{title}
|
||||
</div>]
|
||||
:[] as ReactNode[]
|
||||
).concat(items)}
|
||||
classNameItem="text-dim uppercase"
|
||||
|
||||
@ -5,9 +5,7 @@ import AnimateItems from '@/components/AnimateItems';
|
||||
import { Camera } from '@/camera';
|
||||
import MorePhotos from '@/photo/MorePhotos';
|
||||
import { FilmSimulation } from '@/simulation';
|
||||
import { GRID_ASPECT_RATIO } from '@/site/config';
|
||||
|
||||
const HIGH_DENSITY = GRID_ASPECT_RATIO <= 1;
|
||||
import { GRID_ASPECT_RATIO, HIGH_DENSITY_GRID } from '@/site/config';
|
||||
|
||||
export default function PhotoGrid({
|
||||
photos,
|
||||
@ -43,7 +41,7 @@ export default function PhotoGrid({
|
||||
'grid gap-0.5 sm:gap-1',
|
||||
small
|
||||
? 'grid-cols-3 xs:grid-cols-6'
|
||||
: HIGH_DENSITY
|
||||
: HIGH_DENSITY_GRID
|
||||
? 'grid-cols-2 xs:grid-cols-4 lg:grid-cols-5'
|
||||
: 'grid-cols-2 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4',
|
||||
'items-center',
|
||||
|
||||
@ -4,7 +4,7 @@ import HeaderList from '@/components/HeaderList';
|
||||
import PhotoTag from '@/tag/PhotoTag';
|
||||
import { FaTag } from 'react-icons/fa';
|
||||
import { IoMdCamera } from 'react-icons/io';
|
||||
import { photoQuantityText } from '.';
|
||||
import { PhotoDateRange, dateRangeForPhotos, photoQuantityText } from '.';
|
||||
import { Tags } from '@/tag';
|
||||
import PhotoFilmSimulation from
|
||||
'@/simulation/PhotoFilmSimulation';
|
||||
@ -17,12 +17,16 @@ export default function PhotoGridSidebar({
|
||||
cameras,
|
||||
simulations,
|
||||
photosCount,
|
||||
photosDateRange,
|
||||
}: {
|
||||
tags: Tags
|
||||
cameras: Cameras
|
||||
simulations: FilmSimulations
|
||||
photosCount: number
|
||||
photosDateRange?: PhotoDateRange
|
||||
}) {
|
||||
const { start, end } = dateRangeForPhotos(undefined, photosDateRange);
|
||||
|
||||
return (
|
||||
<>
|
||||
{tags.length > 0 && <HeaderList
|
||||
@ -32,8 +36,9 @@ export default function PhotoGridSidebar({
|
||||
<PhotoTag
|
||||
key={tag}
|
||||
tag={tag}
|
||||
showIcon={false}
|
||||
type="text-only"
|
||||
countOnHover={count}
|
||||
badged
|
||||
/>)}
|
||||
/>}
|
||||
{cameras.length > 0 && <HeaderList
|
||||
@ -48,15 +53,16 @@ export default function PhotoGridSidebar({
|
||||
<PhotoCamera
|
||||
key={cameraKey}
|
||||
camera={camera}
|
||||
showIcon={false}
|
||||
type="text-only"
|
||||
countOnHover={count}
|
||||
hideApple
|
||||
hideAppleIcon
|
||||
badged
|
||||
/>)}
|
||||
/>}
|
||||
{simulations.length > 0 && <HeaderList
|
||||
title="Films"
|
||||
icon={<PhotoFilmSimulationIcon
|
||||
className="translate-y-[-0.5px]"
|
||||
className="translate-y-[0.5px]"
|
||||
/>}
|
||||
items={simulations
|
||||
.sort(sortFilmSimulationsWithCount)
|
||||
@ -72,9 +78,16 @@ export default function PhotoGridSidebar({
|
||||
/>
|
||||
</div>)}
|
||||
/>}
|
||||
{photosCount > 0 && <HeaderList
|
||||
items={[photoQuantityText(photosCount, false)]}
|
||||
/>}
|
||||
{photosCount > 0 && start
|
||||
? <HeaderList
|
||||
title={photoQuantityText(photosCount, false)}
|
||||
items={start === end
|
||||
? [start]
|
||||
: [`${end} –`, start]}
|
||||
/>
|
||||
: <HeaderList
|
||||
items={[photoQuantityText(photosCount, false)]}
|
||||
/>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -61,6 +61,7 @@ export default function PhotoLarge({
|
||||
/>}
|
||||
contentSide={
|
||||
<div className={cc(
|
||||
'leading-snug',
|
||||
'sticky top-4 self-start',
|
||||
'grid grid-cols-2 md:grid-cols-1',
|
||||
'gap-x-0.5 sm:gap-x-1',
|
||||
@ -83,8 +84,7 @@ export default function PhotoLarge({
|
||||
<div className="space-y-0.5">
|
||||
<PhotoCamera
|
||||
camera={camera}
|
||||
showIcon={false}
|
||||
hideApple={false}
|
||||
type="text-only"
|
||||
/>
|
||||
{showSimulation && photo.filmSimulation &&
|
||||
<div className="translate-x-[-0.3rem]">
|
||||
|
||||
@ -3,6 +3,7 @@ import { Photo, PhotoDateRange, dateRangeForPhotos } from '.';
|
||||
import ShareButton from '@/components/ShareButton';
|
||||
import AnimateItems from '@/components/AnimateItems';
|
||||
import { ReactNode } from 'react';
|
||||
import { HIGH_DENSITY_GRID } from '@/site/config';
|
||||
|
||||
export default function PhotoSetHeader({
|
||||
entity,
|
||||
@ -37,14 +38,23 @@ export default function PhotoSetHeader({
|
||||
items={[<div
|
||||
key="PhotosHeader"
|
||||
className={cc(
|
||||
'flex flex-col gap-y-0.5',
|
||||
'xs:grid grid-cols-2 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4',
|
||||
'grid gap-0.5 sm:gap-1 items-start',
|
||||
HIGH_DENSITY_GRID
|
||||
? 'xs:grid-cols-2 sm:grid-cols-4 lg:grid-cols-5'
|
||||
: 'xs:grid-cols-2 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4',
|
||||
)}>
|
||||
{entity}
|
||||
<span className={cc(
|
||||
'inline-flex',
|
||||
HIGH_DENSITY_GRID && 'sm:col-span-2',
|
||||
)}>
|
||||
{entity}
|
||||
</span>
|
||||
<span className={cc(
|
||||
'inline-flex gap-2 items-center self-start',
|
||||
'uppercase text-dim',
|
||||
'sm:col-span-2 md:col-span-1 lg:col-span-2',
|
||||
HIGH_DENSITY_GRID
|
||||
? 'lg:col-span-2'
|
||||
: 'sm:col-span-2 md:col-span-1 lg:col-span-2',
|
||||
)}>
|
||||
{selectedPhotoIndex !== undefined
|
||||
// eslint-disable-next-line max-len
|
||||
|
||||
@ -194,22 +194,28 @@ const sortPhotosByDate = (
|
||||
: a.takenAt.getTime() - b.takenAt.getTime());
|
||||
|
||||
export const dateRangeForPhotos = (
|
||||
photos: Photo[],
|
||||
photos: Photo[] = [],
|
||||
explicitDateRange?: PhotoDateRange,
|
||||
) => {
|
||||
const photosSorted = sortPhotosByDate(photos);
|
||||
let start = '';
|
||||
let end = '';
|
||||
let description = '';
|
||||
|
||||
if (explicitDateRange || photos.length > 0) {
|
||||
const photosSorted = sortPhotosByDate(photos);
|
||||
start = formatDateFromPostgresString(
|
||||
explicitDateRange?.start ?? photosSorted[photos.length - 1].takenAtNaive,
|
||||
true,
|
||||
);
|
||||
end = formatDateFromPostgresString(
|
||||
explicitDateRange?.end ?? photosSorted[0].takenAtNaive,
|
||||
true
|
||||
);
|
||||
description = start === end
|
||||
? start
|
||||
: `${start}–${end}`;
|
||||
}
|
||||
|
||||
const start = formatDateFromPostgresString(
|
||||
explicitDateRange?.start ?? photosSorted[photos.length - 1].takenAtNaive,
|
||||
true,
|
||||
);
|
||||
const end = formatDateFromPostgresString(
|
||||
explicitDateRange?.end ?? photosSorted[0].takenAtNaive,
|
||||
true
|
||||
);
|
||||
const description = start === end
|
||||
? start
|
||||
: `${start}–${end}`;
|
||||
return { start, end, description };
|
||||
};
|
||||
|
||||
|
||||
@ -288,6 +288,12 @@ const sqlGetPhotosFilmSimulationCount = async (
|
||||
hidden IS NOT TRUE
|
||||
`.then(({ rows }) => parseInt(rows[0].count, 10));
|
||||
|
||||
const sqlGetPhotosDateRange = async () => sql`
|
||||
SELECT MIN(taken_at_naive) as start, MAX(taken_at_naive) as end
|
||||
FROM photos
|
||||
WHERE hidden IS NOT TRUE
|
||||
`.then(({ rows }) => rows[0] as PhotoDateRange);
|
||||
|
||||
const sqlGetPhotosTagDateRange = async (tag: string) => sql`
|
||||
SELECT MIN(taken_at_naive) as start, MAX(taken_at_naive) as end
|
||||
FROM photos
|
||||
@ -442,6 +448,8 @@ export const getPhoto = async (id: string): Promise<Photo | undefined> => {
|
||||
.then(({ rows }) => rows.map(parsePhotoFromDb))
|
||||
.then(photos => photos.length > 0 ? photos[0] : undefined);
|
||||
};
|
||||
export const getPhotosDateRange = () =>
|
||||
safelyQueryPhotos(sqlGetPhotosDateRange);
|
||||
export const getPhotosCount = () =>
|
||||
safelyQueryPhotos(sqlGetPhotosCount);
|
||||
export const getPhotosCountIncludingHidden = () =>
|
||||
|
||||
@ -1,58 +1,35 @@
|
||||
import { cc } from '@/utility/css';
|
||||
import { labelForFilmSimulation } from '@/vendors/fujifilm';
|
||||
import PhotoFilmSimulationIcon from './PhotoFilmSimulationIcon';
|
||||
import Badge from '@/components/Badge';
|
||||
import Link from 'next/link';
|
||||
import { pathForFilmSimulation } from '@/site/paths';
|
||||
import { FilmSimulation } from '.';
|
||||
import EntityLink, { EntityLinkExternalProps } from '@/components/EntityLink';
|
||||
|
||||
export default function PhotoFilmSimulation({
|
||||
simulation,
|
||||
type = 'icon-last',
|
||||
badged = true,
|
||||
dim,
|
||||
countOnHover,
|
||||
}: {
|
||||
simulation: FilmSimulation
|
||||
type?: 'icon-last' | 'icon-first' | 'icon-only' | 'text-only'
|
||||
badged?: boolean
|
||||
countOnHover?: number
|
||||
}) {
|
||||
} & EntityLinkExternalProps) {
|
||||
const { small, medium, large } = labelForFilmSimulation(simulation);
|
||||
|
||||
const renderContent = () => <>
|
||||
<span className="xs:hidden">
|
||||
{small}
|
||||
</span>
|
||||
<span className="hidden xs:inline-block">
|
||||
{medium}
|
||||
</span>
|
||||
</>;
|
||||
|
||||
return (
|
||||
<span className="group h-6 inline-flex items-center gap-2">
|
||||
<Link
|
||||
href={pathForFilmSimulation(simulation)}
|
||||
title={`Film Simulation: ${large}`}
|
||||
className="inline-flex items-center gap-1"
|
||||
>
|
||||
{type !== 'icon-only' && <>
|
||||
{badged
|
||||
? <Badge type="secondary" uppercase interactive>
|
||||
{renderContent()}
|
||||
</Badge>
|
||||
: <span className="uppercase text-medium">{renderContent()}</span>}
|
||||
</>}
|
||||
{type !== 'text-only' && <span className={cc(
|
||||
'translate-y-[-1px] text-dim',
|
||||
type === 'icon-first' && 'order-first',
|
||||
)}>
|
||||
<PhotoFilmSimulationIcon {...{ simulation }} />
|
||||
</span>}
|
||||
</Link>
|
||||
{countOnHover !== undefined &&
|
||||
<span className="hidden group-hover:inline">
|
||||
{countOnHover}
|
||||
</span>}
|
||||
</span>
|
||||
<EntityLink
|
||||
label={medium}
|
||||
labelSmall={small}
|
||||
href={pathForFilmSimulation(simulation)}
|
||||
icon={<PhotoFilmSimulationIcon
|
||||
simulation={simulation}
|
||||
className="translate-y-[-1px]"
|
||||
/>}
|
||||
title={`Film Simulation: ${large}`}
|
||||
type={type}
|
||||
badged={badged}
|
||||
dim={dim}
|
||||
hoverEntity={countOnHover}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -59,6 +59,8 @@ export const GRID_ASPECT_RATIO = process.env.NEXT_PUBLIC_GRID_ASPECT_RATIO
|
||||
export const OG_TEXT_BOTTOM_ALIGNMENT =
|
||||
(process.env.NEXT_PUBLIC_OG_TEXT_ALIGNMENT ?? '').toUpperCase() === 'BOTTOM';
|
||||
|
||||
export const HIGH_DENSITY_GRID = GRID_ASPECT_RATIO <= 1;
|
||||
|
||||
export const CONFIG_CHECKLIST_STATUS = {
|
||||
hasPostgres: (process.env.POSTGRES_HOST ?? '').length > 0,
|
||||
hasBlob: HAS_VERCEL_BLOB || HAS_AWS_S3_STORAGE,
|
||||
|
||||
@ -1,44 +1,30 @@
|
||||
import Link from 'next/link';
|
||||
import { pathForTag } from '@/site/paths';
|
||||
import { FaTag } from 'react-icons/fa';
|
||||
import { cc } from '@/utility/css';
|
||||
import { formatTag } from '.';
|
||||
import EntityLink, { EntityLinkExternalProps } from '@/components/EntityLink';
|
||||
|
||||
export default function PhotoTag({
|
||||
tag,
|
||||
showIcon = true,
|
||||
type,
|
||||
badged,
|
||||
dim,
|
||||
countOnHover,
|
||||
}: {
|
||||
tag: string
|
||||
showIcon?: boolean
|
||||
countOnHover?: number
|
||||
}) {
|
||||
} & EntityLinkExternalProps) {
|
||||
return (
|
||||
<span className="group">
|
||||
<Link
|
||||
href={pathForTag(tag)}
|
||||
className={cc(
|
||||
'inline-flex items-center gap-x-1.5 self-start',
|
||||
'hover:text-gray-900 dark:hover:text-gray-100',
|
||||
)}
|
||||
>
|
||||
{showIcon &&
|
||||
<FaTag
|
||||
size={11}
|
||||
className={cc(
|
||||
'flex-shrink-0',
|
||||
'text-icon translate-y-[0.5px]',
|
||||
)}
|
||||
/>}
|
||||
<span className="uppercase">
|
||||
{formatTag(tag)}
|
||||
</span>
|
||||
</Link>
|
||||
{countOnHover !== undefined &&
|
||||
<span className="hidden group-hover:inline">
|
||||
{' '}
|
||||
{countOnHover}
|
||||
</span>}
|
||||
</span>
|
||||
<EntityLink
|
||||
label={formatTag(tag)}
|
||||
href={pathForTag(tag)}
|
||||
icon={<FaTag
|
||||
size={11}
|
||||
className="text-icon translate-y-[5px]"
|
||||
/>}
|
||||
type={type}
|
||||
badged={badged}
|
||||
dim={dim}
|
||||
hoverEntity={countOnHover}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user