Add fujifilm simulations to /grid sidebar

This commit is contained in:
Sam Becker 2023-11-05 12:42:00 -06:00
parent 355a700f17
commit 503ef6ca7c
13 changed files with 121 additions and 39 deletions

View File

@ -62,10 +62,11 @@ Installation
### 6. Optional configuration
- Set `NEXT_PUBLIC_HIDE_REPO_LINK = 1` to remove footer link to repo
- Set `NEXT_PUBLIC_PRO_MODE = 1` to enable higher quality image storage
- Set `NEXT_PUBLIC_PUBLIC_API = 1` to enable a public API available at `/api`
- Set `NEXT_PUBLIC_OG_TEXT_ALIGNMENT = BOTTOM` to keep OG image text bottom aligned (default is top)
- `NEXT_PUBLIC_PRO_MODE = 1` enables higher quality image storage
- `NEXT_PUBLIC_PUBLIC_API = 1` enables public API available at `/api`
- `NEXT_PUBLIC_OG_TEXT_ALIGNMENT = BOTTOM` keeps OG image text bottom aligned (default is top)
- `NEXT_PUBLIC_HIDE_REPO_LINK = 1` removes footer link to repo
- `NEXT_PUBLIC_HIDE_FILM_SIMULATIONS = 1` prevents Fujifilm simulations showing up in `/grid` sidebar
FAQ
-

View File

@ -28,7 +28,7 @@ export default function FilmPage() {
</div>
<PhotoFujifilmSimulation
simulation={FILM_SIMULATION_FORM_INPUT_OPTIONS[index].value}
showIconFirst
type="icon-first"
badged={false}
/>
<div className="mt-4 text-dim relative">

View File

@ -9,7 +9,7 @@ export default function FilmPage() {
<div key={value}>
<PhotoFujifilmSimulation
simulation={value}
showIconFirst
type="icon-first"
/>
</div>)}
</div>

View File

@ -2,6 +2,7 @@ import {
getPhotosCached,
getPhotosCountCached,
getUniqueCamerasCached,
getUniqueFilmSimulationsCached,
getUniqueTagsCached,
} from '@/cache';
import SiteGrid from '@/components/SiteGrid';
@ -16,6 +17,7 @@ import {
getPaginationForSearchParams,
} from '@/site/pagination';
import PhotoGridSidebar from '@/photo/PhotoGridSidebar';
import { SHOW_FILM_SIMULATIONS } from '@/site/config';
export const runtime = 'edge';
@ -32,11 +34,13 @@ export default async function GridPage({ searchParams }: PaginationParams) {
photosCount,
tags,
cameras,
simulations,
] = await Promise.all([
getPhotosCached({ limit }),
getPhotosCountCached(),
getUniqueTagsCached(),
getUniqueCamerasCached(),
SHOW_FILM_SIMULATIONS ? getUniqueFilmSimulationsCached() : [],
]);
const showMorePath = photosCount > photos.length
@ -48,7 +52,7 @@ export default async function GridPage({ searchParams }: PaginationParams) {
? <SiteGrid
contentMain={<PhotoGrid {...{ photos, showMorePath }} />}
contentSide={<div className="sticky top-4 space-y-4">
<PhotoGridSidebar {...{ tags, cameras, photosCount }} />
<PhotoGridSidebar {...{ tags, cameras, simulations, photosCount }} />
</div>}
sideHiddenOnMobile
/>

11
src/cache/index.ts vendored
View File

@ -12,6 +12,7 @@ import {
getPhotosTagDateRange,
getPhotosCameraDateRange,
getUniqueTagsHidden,
getUniqueFilmSimulations,
} from '@/services/postgres';
import { parseCachedPhotosDates, parseCachedPhotoDates } from '@/photo';
import { getBlobPhotoUrls, getBlobUploadUrls } from '@/services/blob';
@ -24,6 +25,7 @@ const KEY_PHOTOS_COUNT = `${KEY_PHOTOS}-count`;
const KEY_PHOTOS_DATE_RANGE = `${KEY_PHOTOS}-date-range`;
const KEY_TAGS = 'tags';
const KEY_CAMERAS = 'cameras';
const KEY_FILM_SIMULATIONS = 'film-simulations';
const KEY_BLOB = 'blob';
// Temporary key to clear caches on forked blogs
const KEY_NEW_QUERY = 'new-query';
@ -210,6 +212,15 @@ export const getUniqueCamerasCached: typeof getUniqueCameras = (...args) =>
}
)();
// eslint-disable-next-line max-len
export const getUniqueFilmSimulationsCached: typeof getUniqueFilmSimulations = (...args) =>
unstable_cache(
() => getUniqueFilmSimulations(...args),
[KEY_PHOTOS, KEY_FILM_SIMULATIONS], {
tags: [KEY_PHOTOS, KEY_FILM_SIMULATIONS],
}
)();
export const getBlobUploadUrlsCached: typeof getBlobUploadUrls = (...args) =>
unstable_cache(
() => getBlobUploadUrls(...args),

View File

@ -4,15 +4,18 @@ import { ReactNode } from 'react';
export default function HeaderList({
title,
className,
icon,
items,
}: {
title?: string,
className?: string,
icon?: JSX.Element,
items: ReactNode[]
}) {
return (
<AnimateItems
className={className}
scaleOffset={0.95}
duration={0.5}
staggerDelay={0.05}

View File

@ -6,14 +6,21 @@ import { FaTag } from 'react-icons/fa';
import { IoMdCamera } from 'react-icons/io';
import { photoQuantityText } from '.';
import { Tags } from '@/tag';
import PhotoFujifilmSimulation from
'@/vendors/fujifilm/PhotoFujifilmSimulation';
import { FujifilmSimulations } from '@/vendors/fujifilm';
import PhotoFujifilmSimulationIcon from
'@/vendors/fujifilm/PhotoFujifilmSimulationIcon';
export default function PhotoGridSidebar({
tags,
cameras,
simulations,
photosCount,
}: {
tags: Tags
cameras: Cameras
simulations: FujifilmSimulations
photosCount: number
}) {
return (
@ -44,6 +51,23 @@ export default function PhotoGridSidebar({
hideApple
/>)}
/>}
{simulations.length > 0 && <HeaderList
title="Films"
icon={<PhotoFujifilmSimulationIcon
className="translate-y-[-0.5px]"
/>}
className="space-y-0.5"
items={simulations.map(({ simulation }) =>
<div
key={simulation}
className="translate-x-[-2px]"
>
<PhotoFujifilmSimulation
simulation={simulation}
type="text-only"
/>
</div>)}
/>}
{photosCount > 0 && <HeaderList
items={[photoQuantityText(photosCount, false)]}
/>}

View File

@ -10,6 +10,7 @@ import {
import { Camera, Cameras, createCameraKey } from '@/camera';
import { parameterize } from '@/utility/string';
import { Tags } from '@/tag';
import { FujifilmSimulation, FujifilmSimulations } from '@/vendors/fujifilm';
const PHOTO_DEFAULT_LIMIT = 100;
@ -318,6 +319,18 @@ const sqlGetUniqueCameras = async () => sql`
count: parseInt(count, 10),
})));
const sqlGetUniqueFilmSimulations = async () => sql`
SELECT DISTINCT film_simulation, COUNT(*)
FROM photos
WHERE hidden IS NOT TRUE AND film_simulation IS NOT NULL
GROUP BY film_simulation
ORDER BY film_simulation DESC
`.then(({ rows }): FujifilmSimulations => rows
.map(({ film_simulation, count }) => ({
simulation: film_simulation as FujifilmSimulation,
count: parseInt(count, 10),
})));
export type GetPhotosOptions = {
sortBy?: 'createdAt' | 'takenAt' | 'priority'
limit?: number
@ -422,3 +435,7 @@ export const getPhotosCameraDateRange = (camera: Camera) =>
safelyQueryPhotos(() => sqlGetPhotosCameraDateRange(camera));
export const getPhotosCameraCount = (camera: Camera) =>
safelyQueryPhotos(() => sqlGetPhotosCameraCount(camera));
// FILM SIMULATIONS
export const getUniqueFilmSimulations = () =>
safelyQueryPhotos(sqlGetUniqueFilmSimulations);

View File

@ -17,6 +17,7 @@ import IconButton from '@/components/IconButton';
import InfoBlock from '@/components/InfoBlock';
import Checklist from '@/components/Checklist';
import { toastSuccess } from '@/toast';
import { ConfigChecklistStatus } from './config';
export default function SiteChecklistClient({
hasPostgres,
@ -26,22 +27,13 @@ export default function SiteChecklistClient({
hasTitle,
hasDomain,
showRepoLink,
showFilmSimulations,
isProModeEnabled,
isPublicApiEnabled,
isOgTextBottomAligned,
showRefreshButton,
secret,
}: {
hasPostgres: boolean
hasBlob: boolean
hasAuth: boolean
hasAdminUser: boolean
hasTitle: boolean
hasDomain: boolean
showRepoLink: boolean
isProModeEnabled: boolean
isPublicApiEnabled: boolean
isOgTextBottomAligned: boolean
}: ConfigChecklistStatus & {
showRefreshButton?: boolean
secret: string
}) {
@ -210,15 +202,6 @@ export default function SiteChecklistClient({
title="Settings"
icon={<BiCog size={16} />}
>
<ChecklistRow
title="Show Repo Link"
status={showRepoLink}
isPending={isPendingPage}
optional
>
Set environment variable to {'"1"'} to hide footer link:
{renderEnvVars(['NEXT_PUBLIC_HIDE_REPO_LINK'])}
</ChecklistRow>
<ChecklistRow
title="Pro Mode"
status={isProModeEnabled}
@ -249,6 +232,25 @@ export default function SiteChecklistClient({
keep OG image text bottom aligned (default is top):
{renderEnvVars(['NEXT_PUBLIC_OG_TEXT_ALIGNMENT'])}
</ChecklistRow>
<ChecklistRow
title="Show Repo Link"
status={showRepoLink}
isPending={isPendingPage}
optional
>
Set environment variable to {'"1"'} to hide footer link:
{renderEnvVars(['NEXT_PUBLIC_HIDE_REPO_LINK'])}
</ChecklistRow>
<ChecklistRow
title="Show Fujifilm simulations"
status={showFilmSimulations}
isPending={isPendingPage}
optional
>
Set environment variable to {'"1"'} to prevent
simulations showing up in <code>/grid</code> sidebar:
{renderEnvVars(['NEXT_PUBLIC_HIDE_FILM_SIMULATIONS'])}
</ChecklistRow>
</Checklist>
{showRefreshButton &&
<div className="py-4 space-y-4">

View File

@ -28,11 +28,13 @@ export const BASE_URL = process.env.NODE_ENV === 'production'
? makeUrlAbsolute(SITE_DOMAIN).toLowerCase()
: 'http://localhost:3000';
export const SHOW_REPO_LINK = process.env.NEXT_PUBLIC_HIDE_REPO_LINK !== '1';
export const PRO_MODE_ENABLED = process.env.NEXT_PUBLIC_PRO_MODE === '1';
export const PUBLIC_API_ENABLED = process.env.NEXT_PUBLIC_PUBLIC_API === '1';
export const OG_TEXT_BOTTOM_ALIGNMENT =
(process.env.NEXT_PUBLIC_OG_TEXT_ALIGNMENT ?? '').toUpperCase() === 'BOTTOM';
export const SHOW_REPO_LINK = process.env.NEXT_PUBLIC_HIDE_REPO_LINK !== '1';
export const SHOW_FILM_SIMULATIONS =
process.env.NEXT_PUBLIC_HIDE_FILM_SIMULATIONS !== '1';
export const CONFIG_CHECKLIST_STATUS = {
hasPostgres: (process.env.POSTGRES_HOST ?? '').length > 0,
@ -45,11 +47,14 @@ export const CONFIG_CHECKLIST_STATUS = {
hasTitle: (process.env.NEXT_PUBLIC_SITE_TITLE ?? '').length > 0,
hasDomain: (process.env.NEXT_PUBLIC_SITE_DOMAIN ?? '').length > 0,
showRepoLink: SHOW_REPO_LINK,
showFilmSimulations: SHOW_FILM_SIMULATIONS,
isProModeEnabled: PRO_MODE_ENABLED,
isPublicApiEnabled: PUBLIC_API_ENABLED,
isOgTextBottomAligned: OG_TEXT_BOTTOM_ALIGNMENT,
};
export type ConfigChecklistStatus = typeof CONFIG_CHECKLIST_STATUS;
export const IS_SITE_READY =
CONFIG_CHECKLIST_STATUS.hasPostgres &&
CONFIG_CHECKLIST_STATUS.hasBlob &&

View File

@ -8,11 +8,11 @@ import Badge from '@/components/Badge';
export default function PhotoFujifilmSimulation({
simulation,
showIconFirst,
type = 'icon-last',
badged = true,
}: {
simulation: FujifilmSimulation
showIconFirst?: boolean
type?: 'icon-last' | 'icon-first' | 'icon-only' | 'text-only'
badged?: boolean
}) {
const { small, medium, large } = getLabelForFilmSimulation(simulation);
@ -31,15 +31,17 @@ export default function PhotoFujifilmSimulation({
title={`Film Simulation: ${large}`}
className="inline-flex items-center gap-1"
>
{badged
? <Badge type="secondary" uppercase>{renderContent()}</Badge>
: <span className="uppercase text-medium">{renderContent()}</span>}
<span className={cc(
{type !== 'icon-only' && <>
{badged
? <Badge type="secondary" uppercase>{renderContent()}</Badge>
: <span className="uppercase text-medium">{renderContent()}</span>}
</>}
{type !== 'text-only' && <span className={cc(
'translate-y-[-1.25px] text-extra-dim',
showIconFirst && 'order-first',
type === 'icon-first' && 'order-first',
)}>
<PhotoFujifilmSimulationIcon simulation={simulation} />
</span>
<PhotoFujifilmSimulationIcon {...{ simulation }} />
</span>}
</span>
);
}

View File

@ -6,8 +6,10 @@ import {
export default function PhotoFujifilmSimulationIcon({
simulation,
className,
}: {
simulation: FujifilmSimulation;
simulation?: FujifilmSimulation;
className?: string
}) {
const contentForSimulation = (): JSX.Element => {
switch (simulation) {
@ -124,12 +126,18 @@ export default function PhotoFujifilmSimulationIcon({
<path fillRule="evenodd" clipRule="evenodd" d="M16.25 14H22.5C22.6381 14 22.75 13.8881 22.75 13.75V10.5202C22.75 10.4539 22.7763 10.3903 22.8232 10.3434L25.677 7.48989C25.7238 7.44301 25.7502 7.37942 25.7502 7.31311V4.25C25.7502 4.11193 25.6383 4 25.5002 4H16.25C16.1119 4 16 4.11194 16 4.25002L16.0002 6.49998C16.0002 6.63806 15.8882 6.75 15.7502 6.75H14.7502C14.6121 6.75 14.5002 6.86192 14.5002 6.99999L14.5 11C14.5 11.1381 14.6119 11.25 14.75 11.25H15.75C15.8881 11.25 16 11.3619 16 11.5V13.75C16 13.8881 16.1119 14 16.25 14ZM18.75 5H17V6.5H18.75V5ZM17 11.5H18.75V13H17V11.5ZM21.75 5H20V6.5H21.75V5ZM20 11.5H21.75V13H20V11.5ZM24.75 5H23V6.5H24.75V5Z" fill="currentColor"/>
<path fillRule="evenodd" clipRule="evenodd" d="M5.25 3.49999L2 3.49999C1.86193 3.49999 1.75 3.61192 1.75 3.74999V5.24998C1.75 5.38806 1.86193 5.49998 2 5.49998H3.00001C3.13808 5.49998 3.25001 5.61191 3.25001 5.74999L3.25 12.25C3.25 12.3881 3.13807 12.5 3 12.5H2C1.86193 12.5 1.75 12.6119 1.75 12.75V14.25C1.75 14.3881 1.86193 14.5 2 14.5H14.25C14.3881 14.5 14.5 14.3881 14.5 14.25V12.75C14.5 12.6119 14.3881 12.5 14.25 12.5H13.25C13.1119 12.5 13 12.3881 13 12.25L13 5.75C13 5.61193 13.112 5.5 13.25 5.5H14.25C14.3881 5.5 14.5 5.38807 14.5 5.25V3.74999C14.5 3.61192 14.3881 3.49999 14.25 3.49999L11 3.49999C10.8619 3.49998 10.75 3.38806 10.75 3.24999V1.74998C10.75 1.61191 10.6381 1.49998 10.5 1.49998H5.75C5.61193 1.49998 5.5 1.61191 5.5 1.74998V3.24999C5.5 3.38806 5.38807 3.49998 5.25 3.49999ZM5.33818 13H7.01843V9.898H8.13469L9.40369 13H11.2249L9.79144 9.74525C10.2379 9.58075 10.5748 9.29092 10.8019 8.87575C11.0291 8.46058 11.1427 7.95142 11.1427 7.34825C11.1427 6.57275 10.9508 5.95392 10.5669 5.49175C10.1831 5.02958 9.62302 4.7985 8.88668 4.7985H5.33818V13ZM9.20393 8.33525C9.0786 8.44492 8.90235 8.49975 8.67518 8.49975H7.01843V6.26725H8.67518C8.90235 6.26725 9.0786 6.326 9.20393 6.4435C9.3371 6.55317 9.40369 6.749 9.40369 7.031V7.736C9.40369 8.018 9.3371 8.21775 9.20393 8.33525Z" fill="currentColor"/>
</>;
default: return <>
<path fillRule="evenodd" clipRule="evenodd" d="M1.5 13H8.75001C8.88808 13 9.00001 12.8881 9.00001 12.75V9.52022C9.00001 9.45392 9.02635 9.39033 9.07324 9.34344L11.927 6.48989C11.9739 6.44301 12.0002 6.37942 12.0002 6.31311V3.25C12.0002 3.11193 11.8883 3 11.7502 3H1.5C1.36193 3 1.25 3.11193 1.25 3.25V12.75C1.25 12.8881 1.36193 13 1.5 13ZM4.50001 4H2.75001V5.5H4.50001V4ZM2.75001 10.5H4.50001V12H2.75001V10.5ZM7.50001 4H5.75001V5.5H7.50001V4ZM5.75001 10.5H7.50001V12H5.75001V10.5ZM10.5 4H8.75001V5.5H10.5V4Z" fill="currentColor"/>
</>;
}
};
return (
<svg
aria-description={getLabelForFilmSimulation(simulation).large}
className={className}
aria-description={simulation
? getLabelForFilmSimulation(simulation).large
: 'Film Simulation'}
width="28"
height="16"
viewBox="0 0 28 16"

View File

@ -46,6 +46,11 @@ export type FujifilmSimulation =
FujifilmSimulationFromSaturation |
FujifilmMode;
export type FujifilmSimulations = {
simulation: FujifilmSimulation
count: number
}[]
export const isExifForFujifilm = (data: ExifData) =>
data.tags?.Make === MAKE_FUJIFILM;