Add fujifilm simulations to /grid sidebar
This commit is contained in:
parent
355a700f17
commit
503ef6ca7c
@ -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
|
||||
-
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -9,7 +9,7 @@ export default function FilmPage() {
|
||||
<div key={value}>
|
||||
<PhotoFujifilmSimulation
|
||||
simulation={value}
|
||||
showIconFirst
|
||||
type="icon-first"
|
||||
/>
|
||||
</div>)}
|
||||
</div>
|
||||
|
||||
@ -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
11
src/cache/index.ts
vendored
@ -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),
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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)]}
|
||||
/>}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 &&
|
||||
|
||||
20
src/vendors/fujifilm/PhotoFujifilmSimulation.tsx
vendored
20
src/vendors/fujifilm/PhotoFujifilmSimulation.tsx
vendored
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
5
src/vendors/fujifilm/index.ts
vendored
5
src/vendors/fujifilm/index.ts
vendored
@ -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;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user