Merge pull request #30 from sambecker/priority-order
Introduce global priority order
This commit is contained in:
commit
57d77809dc
@ -64,6 +64,7 @@ Installation
|
||||
|
||||
- `NEXT_PUBLIC_PRO_MODE = 1` enables higher quality image storage
|
||||
- `NEXT_PUBLIC_GEO_PRIVACY = 1` disables collection/display of location-based data
|
||||
- `NEXT_PUBLIC_IGNORE_PRIORITY_ORDER = 1` prevents `priority_order` field affecting photo order
|
||||
- `NEXT_PUBLIC_PUBLIC_API = 1` enables public API available at `/api`
|
||||
- `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
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
absolutePathForPhotoImage,
|
||||
} from '@/site/paths';
|
||||
import PhotoDetailPage from '@/photo/PhotoDetailPage';
|
||||
import { getPhotoCached, getPhotosCached } from '@/cache';
|
||||
import { getPhotoCached, getPhotosNearIdCached } from '@/cache';
|
||||
|
||||
interface PhotoProps {
|
||||
params: { photoId: string }
|
||||
@ -50,31 +50,29 @@ export async function generateMetadata({
|
||||
export default async function PhotoPage({
|
||||
params: { photoId },
|
||||
children,
|
||||
}:
|
||||
PhotoProps & { children: React.ReactNode }) {
|
||||
const photo = await getPhotoCached(photoId);
|
||||
}: PhotoProps & { children: React.ReactNode }) {
|
||||
const photos = await getPhotosNearIdCached(
|
||||
photoId,
|
||||
GRID_THUMBNAILS_TO_SHOW_MAX + 2,
|
||||
);
|
||||
|
||||
const photo = photos.find(p => p.id === photoId);
|
||||
|
||||
if (!photo) { redirect(PATH_ROOT); }
|
||||
|
||||
const [
|
||||
photosBefore,
|
||||
photosAfter,
|
||||
] = await Promise.all([
|
||||
getPhotosCached({ takenBefore: photo.takenAt, limit: 1 }),
|
||||
getPhotosCached({
|
||||
takenAfterInclusive: photo.takenAt,
|
||||
limit: GRID_THUMBNAILS_TO_SHOW_MAX + 1,
|
||||
}),
|
||||
]);
|
||||
|
||||
const photos = photosBefore.concat(photosAfter);
|
||||
|
||||
const isPhotoFirst = photos.findIndex(p => p.id === photoId) === 0;
|
||||
|
||||
return <>
|
||||
{children}
|
||||
<PhotoDetailPage
|
||||
photo={photo}
|
||||
photos={photos}
|
||||
photosGrid={photosAfter.slice(1)}
|
||||
photosGrid={photos.slice(
|
||||
isPhotoFirst ? 1 : 2,
|
||||
isPhotoFirst
|
||||
? GRID_THUMBNAILS_TO_SHOW_MAX + 1
|
||||
: GRID_THUMBNAILS_TO_SHOW_MAX + 2,
|
||||
)}
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
|
||||
32
src/cache/index.ts
vendored
32
src/cache/index.ts
vendored
@ -21,6 +21,7 @@ import {
|
||||
getPhotosFilmSimulationDateRange,
|
||||
getPhotosFilmSimulationCount,
|
||||
getPhotosDateRange,
|
||||
getPhotosNearId,
|
||||
} from '@/services/vercel-postgres';
|
||||
import { parseCachedPhotoDates, parseCachedPhotosDates } from '@/photo';
|
||||
import { getBlobPhotoUrls, getBlobUploadUrls } from '@/services/blob';
|
||||
@ -45,26 +46,20 @@ const getPhotosCacheKeyForOption = (
|
||||
option: keyof GetPhotosOptions,
|
||||
): string | null => {
|
||||
switch (option) {
|
||||
// Primitive keys
|
||||
case 'sortBy':
|
||||
case 'limit':
|
||||
case 'tag':
|
||||
case 'simulation':
|
||||
case 'includeHidden': {
|
||||
const value = options[option];
|
||||
return value ? `${option}-${value}` : null;
|
||||
}
|
||||
// Date keys
|
||||
case 'takenBefore':
|
||||
case 'takenAfterInclusive': {
|
||||
const value = options[option];
|
||||
return value ? `${option}-${value.toISOString()}` : null;
|
||||
}
|
||||
// Complex keys
|
||||
case 'camera': {
|
||||
const value = options[option];
|
||||
return value ? `${option}-${createCameraKey(value)}` : null;
|
||||
}
|
||||
case 'takenBefore':
|
||||
case 'takenAfterInclusive': {
|
||||
const value = options[option];
|
||||
return value ? `${option}-${value.toISOString()}` : null;
|
||||
}
|
||||
// Primitive keys
|
||||
default:
|
||||
const value = options[option];
|
||||
return value !== undefined ? `${option}-${value}` : null;
|
||||
}
|
||||
};
|
||||
|
||||
@ -119,6 +114,13 @@ export const getPhotosCached = (
|
||||
[KEY_PHOTOS, ...getPhotosCacheKeys(...args)],
|
||||
)(...args).then(parseCachedPhotosDates);
|
||||
|
||||
export const getPhotosNearIdCached = (
|
||||
...args: Parameters<typeof getPhotosNearId>
|
||||
) => unstable_cache(
|
||||
getPhotosNearId,
|
||||
[KEY_PHOTOS],
|
||||
)(...args).then(parseCachedPhotosDates);
|
||||
|
||||
export const getPhotosDateRangeCached =
|
||||
unstable_cache(
|
||||
getPhotosDateRange,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { sql } from '@vercel/postgres';
|
||||
import { db, sql } from '@vercel/postgres';
|
||||
import {
|
||||
PhotoDb,
|
||||
PhotoDbInsert,
|
||||
@ -11,6 +11,7 @@ import { Camera, Cameras, createCameraKey } from '@/camera';
|
||||
import { parameterize } from '@/utility/string';
|
||||
import { Tags } from '@/tag';
|
||||
import { FilmSimulation, FilmSimulations } from '@/simulation';
|
||||
import { PRIORITY_ORDER_ENABLED } from '@/site/config';
|
||||
|
||||
const PHOTO_DEFAULT_LIMIT = 100;
|
||||
|
||||
@ -151,109 +152,6 @@ export const sqlRenamePhotoTagGlobally = (tag: string, updatedTag: string) =>
|
||||
export const sqlDeletePhoto = (id: string) =>
|
||||
sql`DELETE FROM photos WHERE id=${id}`;
|
||||
|
||||
const sqlGetPhotos = (
|
||||
limit = PHOTO_DEFAULT_LIMIT,
|
||||
offset = 0,
|
||||
) =>
|
||||
sql<PhotoDb>`
|
||||
SELECT * FROM photos
|
||||
WHERE hidden IS NOT TRUE
|
||||
ORDER BY taken_at DESC
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
`;
|
||||
|
||||
const sqlGetPhotosIncludingHidden = (
|
||||
limit = PHOTO_DEFAULT_LIMIT,
|
||||
offset = 0,
|
||||
) =>
|
||||
sql<PhotoDb>`
|
||||
SELECT * FROM photos
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
`;
|
||||
|
||||
const sqlGetPhotosSortedByCreatedAt = (
|
||||
limit = PHOTO_DEFAULT_LIMIT,
|
||||
offset = 0,
|
||||
) =>
|
||||
sql<PhotoDb>`
|
||||
SELECT * FROM photos
|
||||
WHERE hidden IS NOT TRUE
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
`;
|
||||
|
||||
const sqlGetPhotosSortedByPriority = (
|
||||
limit = PHOTO_DEFAULT_LIMIT,
|
||||
offset = 0,
|
||||
) =>
|
||||
sql<PhotoDb>`
|
||||
SELECT * FROM photos
|
||||
WHERE hidden IS NOT TRUE
|
||||
ORDER BY priority_order ASC, taken_at DESC
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
`;
|
||||
|
||||
const sqlGetPhotosByTag = (
|
||||
limit = PHOTO_DEFAULT_LIMIT,
|
||||
tag: string,
|
||||
) =>
|
||||
sql<PhotoDb>`
|
||||
SELECT * FROM photos
|
||||
WHERE ${tag}=ANY(tags)
|
||||
AND hidden IS NOT TRUE
|
||||
ORDER BY taken_at DESC
|
||||
LIMIT ${limit}
|
||||
`;
|
||||
|
||||
const sqlGetPhotosByCamera = async (
|
||||
limit = PHOTO_DEFAULT_LIMIT,
|
||||
make: string,
|
||||
model: string,
|
||||
) => sql<PhotoDb>`
|
||||
SELECT * FROM photos
|
||||
WHERE
|
||||
LOWER(make)=${parameterize(make)} AND
|
||||
LOWER(REPLACE(model, ' ', '-'))=${parameterize(model)}
|
||||
ORDER BY taken_at DESC
|
||||
LIMIT ${limit}
|
||||
`;
|
||||
|
||||
const sqlGetPhotosBySimulation = async (
|
||||
limit = PHOTO_DEFAULT_LIMIT,
|
||||
simulation: FilmSimulation,
|
||||
) => sql<PhotoDb>`
|
||||
SELECT * FROM photos
|
||||
WHERE film_simulation=${simulation}
|
||||
AND hidden IS NOT TRUE
|
||||
ORDER BY taken_at DESC
|
||||
LIMIT ${limit}
|
||||
`;
|
||||
|
||||
const sqlGetPhotosTakenAfterDateInclusive = (
|
||||
takenAt: Date,
|
||||
limit?: number,
|
||||
) =>
|
||||
sql<PhotoDb>`
|
||||
SELECT * FROM photos
|
||||
WHERE taken_at <= ${takenAt.toISOString()}
|
||||
AND hidden IS NOT TRUE
|
||||
ORDER BY taken_at DESC
|
||||
LIMIT ${limit}
|
||||
`;
|
||||
|
||||
const sqlGetPhotosTakenBeforeDate = (
|
||||
takenAt: Date,
|
||||
limit?: number,
|
||||
) =>
|
||||
sql<PhotoDb>`
|
||||
SELECT * FROM photos
|
||||
WHERE taken_at > ${takenAt.toISOString()}
|
||||
AND hidden IS NOT TRUE
|
||||
ORDER BY taken_at ASC
|
||||
LIMIT ${limit}
|
||||
`;
|
||||
|
||||
const sqlGetPhoto = (id: string) =>
|
||||
sql<PhotoDb>`SELECT * FROM photos WHERE id=${id} LIMIT 1`;
|
||||
|
||||
@ -367,6 +265,7 @@ const sqlGetUniqueFilmSimulations = async () => sql`
|
||||
export type GetPhotosOptions = {
|
||||
sortBy?: 'createdAt' | 'takenAt' | 'priority'
|
||||
limit?: number
|
||||
offset?: number
|
||||
tag?: string
|
||||
camera?: Camera
|
||||
simulation?: FilmSimulation
|
||||
@ -403,11 +302,11 @@ const safelyQueryPhotos = async <T>(callback: () => Promise<T>): Promise<T> => {
|
||||
return result;
|
||||
};
|
||||
|
||||
// PHOTOS
|
||||
export const getPhotos = async (options: GetPhotosOptions = {}) => {
|
||||
const {
|
||||
sortBy = 'takenAt',
|
||||
limit,
|
||||
sortBy = PRIORITY_ORDER_ENABLED ? 'priority' : 'takenAt',
|
||||
limit = PHOTO_DEFAULT_LIMIT,
|
||||
offset = 0,
|
||||
tag,
|
||||
camera,
|
||||
simulation,
|
||||
@ -416,30 +315,95 @@ export const getPhotos = async (options: GetPhotosOptions = {}) => {
|
||||
includeHidden,
|
||||
} = options;
|
||||
|
||||
let getPhotosSql = () => sqlGetPhotos(limit);
|
||||
let sql = ['SELECT * FROM photos'];
|
||||
let values = [] as (string | number)[];
|
||||
let valueIndex = 1;
|
||||
|
||||
if (includeHidden) {
|
||||
getPhotosSql = () => sqlGetPhotosIncludingHidden(limit);
|
||||
} else if (takenBefore) {
|
||||
getPhotosSql = () => sqlGetPhotosTakenBeforeDate(takenBefore, limit);
|
||||
} else if (takenAfterInclusive) {
|
||||
// eslint-disable-next-line max-len
|
||||
getPhotosSql = () => sqlGetPhotosTakenAfterDateInclusive(takenAfterInclusive, limit);
|
||||
} else if (tag) {
|
||||
getPhotosSql = () => sqlGetPhotosByTag(limit, tag);
|
||||
} else if (camera) {
|
||||
getPhotosSql = () => sqlGetPhotosByCamera(limit, camera.make, camera.model);
|
||||
} else if (simulation) {
|
||||
getPhotosSql = () => sqlGetPhotosBySimulation(limit, simulation);
|
||||
} else if (sortBy === 'createdAt') {
|
||||
getPhotosSql = () => sqlGetPhotosSortedByCreatedAt(limit);
|
||||
} else if (sortBy === 'priority') {
|
||||
getPhotosSql = () => sqlGetPhotosSortedByPriority(limit);
|
||||
// WHERE
|
||||
let wheres = [] as string[];
|
||||
if (!includeHidden) {
|
||||
wheres.push('hidden IS NOT TRUE');
|
||||
}
|
||||
if (takenBefore) {
|
||||
wheres.push(`taken_at > $${valueIndex++}`);
|
||||
values.push(takenBefore.toISOString());
|
||||
}
|
||||
if (takenAfterInclusive) {
|
||||
wheres.push(`taken_at <= $${valueIndex++}`);
|
||||
values.push(takenAfterInclusive.toISOString());
|
||||
}
|
||||
if (tag) {
|
||||
wheres.push(`$${valueIndex++}=ANY(tags)`);
|
||||
values.push(tag);
|
||||
}
|
||||
if (camera) {
|
||||
wheres.push(`LOWER(make)=$${valueIndex++}`);
|
||||
wheres.push(`LOWER(REPLACE(model, ' ', '-'))=$${valueIndex++}`);
|
||||
values.push(parameterize(camera.make));
|
||||
values.push(parameterize(camera.model));
|
||||
}
|
||||
if (simulation) {
|
||||
wheres.push(`film_simulation=$${valueIndex++}`);
|
||||
values.push(simulation);
|
||||
}
|
||||
if (wheres.length > 0) {
|
||||
sql.push(`WHERE ${wheres.join(' AND ')}`);
|
||||
}
|
||||
|
||||
return safelyQueryPhotos(getPhotosSql)
|
||||
// ORDER BY
|
||||
switch (sortBy) {
|
||||
case 'createdAt':
|
||||
sql.push('ORDER BY created_at DESC');
|
||||
break;
|
||||
case 'takenAt':
|
||||
sql.push('ORDER BY taken_at DESC');
|
||||
break;
|
||||
case 'priority':
|
||||
sql.push('ORDER BY priority_order ASC, taken_at DESC');
|
||||
break;
|
||||
}
|
||||
|
||||
// LIMIT + OFFSET
|
||||
sql.push(`LIMIT $${valueIndex++} OFFSET $${valueIndex++}`);
|
||||
values.push(limit, offset);
|
||||
|
||||
return safelyQueryPhotos(async () => {
|
||||
const client = await db.connect();
|
||||
return client.query(sql.join(' '), values);
|
||||
})
|
||||
.then(({ rows }) => rows.map(parsePhotoFromDb));
|
||||
};
|
||||
|
||||
export const getPhotosNearId = async (
|
||||
id: string,
|
||||
limit: number,
|
||||
) => {
|
||||
const orderBy = PRIORITY_ORDER_ENABLED
|
||||
? 'ORDER BY priority_order ASC, taken_at DESC'
|
||||
: 'ORDER BY taken_at DESC';
|
||||
|
||||
return safelyQueryPhotos(async () => {
|
||||
const client = await db.connect();
|
||||
return client.query(
|
||||
`
|
||||
WITH twi AS (
|
||||
SELECT *, row_number()
|
||||
OVER (${orderBy}) as row_number
|
||||
FROM photos
|
||||
WHERE hidden IS NOT TRUE
|
||||
),
|
||||
current AS (SELECT row_number FROM twi WHERE id = $1)
|
||||
SELECT twi.*
|
||||
FROM twi, current
|
||||
WHERE twi.row_number >= current.row_number - 1
|
||||
LIMIT $2
|
||||
`,
|
||||
[id, limit]
|
||||
);
|
||||
})
|
||||
.then(({ rows }) => rows.map(parsePhotoFromDb));
|
||||
};
|
||||
|
||||
export const getPhoto = async (id: string): Promise<Photo | undefined> => {
|
||||
// Check for photo id forwarding
|
||||
// and convert short ids to uuids
|
||||
|
||||
@ -33,6 +33,7 @@ export default function SiteChecklistClient({
|
||||
showFilmSimulations,
|
||||
isProModeEnabled,
|
||||
isGeoPrivacyEnabled,
|
||||
isPriorityOrderEnabled,
|
||||
isPublicApiEnabled,
|
||||
isOgTextBottomAligned,
|
||||
showRefreshButton,
|
||||
@ -256,6 +257,16 @@ export default function SiteChecklistClient({
|
||||
collection/display of location-based data
|
||||
{renderEnvVars(['NEXT_PUBLIC_GEO_PRIVACY'])}
|
||||
</ChecklistRow>
|
||||
<ChecklistRow
|
||||
title="Priority Order"
|
||||
status={isPriorityOrderEnabled}
|
||||
isPending={isPendingPage}
|
||||
optional
|
||||
>
|
||||
Set environment variable to {'"1"'} to prevent
|
||||
priority order photo field affecting photo order
|
||||
{renderEnvVars(['NEXT_PUBLIC_IGNORE_PRIORITY_ORDER'])}
|
||||
</ChecklistRow>
|
||||
<ChecklistRow
|
||||
title="Public API"
|
||||
status={isPublicApiEnabled}
|
||||
|
||||
@ -49,6 +49,8 @@ export const HAS_AWS_S3_STORAGE =
|
||||
|
||||
export const PRO_MODE_ENABLED = process.env.NEXT_PUBLIC_PRO_MODE === '1';
|
||||
export const GEO_PRIVACY_ENABLED = process.env.NEXT_PUBLIC_GEO_PRIVACY === '1';
|
||||
export const PRIORITY_ORDER_ENABLED =
|
||||
process.env.NEXT_PUBLIC_IGNORE_PRIORITY_ORDER !== '1';
|
||||
export const PUBLIC_API_ENABLED = process.env.NEXT_PUBLIC_PUBLIC_API === '1';
|
||||
export const SHOW_REPO_LINK = process.env.NEXT_PUBLIC_HIDE_REPO_LINK !== '1';
|
||||
export const SHOW_FILM_SIMULATIONS =
|
||||
@ -77,6 +79,7 @@ export const CONFIG_CHECKLIST_STATUS = {
|
||||
showFilmSimulations: SHOW_FILM_SIMULATIONS,
|
||||
isProModeEnabled: PRO_MODE_ENABLED,
|
||||
isGeoPrivacyEnabled: GEO_PRIVACY_ENABLED,
|
||||
isPriorityOrderEnabled: PRIORITY_ORDER_ENABLED,
|
||||
isPublicApiEnabled: PUBLIC_API_ENABLED,
|
||||
isOgTextBottomAligned: OG_TEXT_BOTTOM_ALIGNMENT,
|
||||
gridAspectRatio: GRID_ASPECT_RATIO,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user