Sitemaps (#260)
* Track last modified date for category queries * Remove unused hidden tags queries * Add tags to sitemap * Calculate sitemap validation * Add remaining categories to sitemap.xml * Add photos to sitemap.xml * Finalize sitemap metadata * Guard against missing dates in sitemap.xml
This commit is contained in:
parent
29da584311
commit
311d7a77af
111
app/sitemap.ts
Normal file
111
app/sitemap.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import type { MetadataRoute } from 'next';
|
||||||
|
import { getDataForCategoriesCached } from '@/category/cache';
|
||||||
|
import {
|
||||||
|
absolutePathForCamera,
|
||||||
|
absolutePathForFilm,
|
||||||
|
absolutePathForFocalLength,
|
||||||
|
absolutePathForLens,
|
||||||
|
absolutePathForPhoto,
|
||||||
|
absolutePathForRecipe,
|
||||||
|
absolutePathForTag,
|
||||||
|
} from '@/app/paths';
|
||||||
|
import { isTagFavs } from '@/tag';
|
||||||
|
import { BASE_URL, GRID_HOMEPAGE_ENABLED } from '@/app/config';
|
||||||
|
import { getPhotoIdsAndUpdatedAt } from '@/photo/db/query';
|
||||||
|
|
||||||
|
// Cache for 24 hours
|
||||||
|
export const revalidate = 86_400;
|
||||||
|
|
||||||
|
const PRIORITY_HOME = 1;
|
||||||
|
const PRIORITY_HOME_VIEW = 0.9;
|
||||||
|
const PRIORITY_CATEGORY_SPECIAL = 0.8;
|
||||||
|
const PRIORITY_CATEGORY = 0.7;
|
||||||
|
const PRIORITY_PHOTO = 0.5;
|
||||||
|
|
||||||
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||||
|
const [
|
||||||
|
{
|
||||||
|
cameras,
|
||||||
|
lenses,
|
||||||
|
tags,
|
||||||
|
recipes,
|
||||||
|
films,
|
||||||
|
focalLengths,
|
||||||
|
},
|
||||||
|
photos,
|
||||||
|
] = await Promise.all([
|
||||||
|
getDataForCategoriesCached(),
|
||||||
|
getPhotoIdsAndUpdatedAt(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const lastModifiedSite = [
|
||||||
|
...cameras.map(({ lastModified }) => lastModified),
|
||||||
|
...lenses.map(({ lastModified }) => lastModified),
|
||||||
|
...tags.map(({ lastModified }) => lastModified),
|
||||||
|
...recipes.map(({ lastModified }) => lastModified),
|
||||||
|
...films.map(({ lastModified }) => lastModified),
|
||||||
|
...focalLengths.map(({ lastModified }) => lastModified),
|
||||||
|
...photos.map(({ updatedAt }) => updatedAt),
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((a, b) => b.getTime() - a.getTime())[0];
|
||||||
|
|
||||||
|
return [
|
||||||
|
// Homepage
|
||||||
|
{
|
||||||
|
url: BASE_URL!,
|
||||||
|
priority: PRIORITY_HOME,
|
||||||
|
lastModified: lastModifiedSite,
|
||||||
|
},
|
||||||
|
// Grid or Feed
|
||||||
|
{
|
||||||
|
url: GRID_HOMEPAGE_ENABLED ? `${BASE_URL}/feed` : `${BASE_URL}/grid`,
|
||||||
|
priority: PRIORITY_HOME_VIEW,
|
||||||
|
lastModified: lastModifiedSite,
|
||||||
|
},
|
||||||
|
// Cameras
|
||||||
|
...cameras.map(({ camera, lastModified }) => ({
|
||||||
|
url: absolutePathForCamera(camera),
|
||||||
|
priority: PRIORITY_CATEGORY,
|
||||||
|
lastModified,
|
||||||
|
})),
|
||||||
|
// Lenses
|
||||||
|
...lenses.map(({ lens, lastModified }) => ({
|
||||||
|
url: absolutePathForLens(lens),
|
||||||
|
priority: PRIORITY_CATEGORY,
|
||||||
|
lastModified,
|
||||||
|
})),
|
||||||
|
// Tags
|
||||||
|
...tags.map(({ tag, lastModified }) => ({
|
||||||
|
url: absolutePathForTag(tag),
|
||||||
|
priority: isTagFavs(tag)
|
||||||
|
? PRIORITY_CATEGORY_SPECIAL
|
||||||
|
: PRIORITY_CATEGORY,
|
||||||
|
lastModified,
|
||||||
|
})),
|
||||||
|
// Recipes
|
||||||
|
...recipes.map(({ recipe, lastModified }) => ({
|
||||||
|
url: absolutePathForRecipe(recipe),
|
||||||
|
priority: PRIORITY_CATEGORY,
|
||||||
|
lastModified,
|
||||||
|
})),
|
||||||
|
// Films
|
||||||
|
...films.map(({ film, lastModified }) => ({
|
||||||
|
url: absolutePathForFilm(film),
|
||||||
|
priority: PRIORITY_CATEGORY,
|
||||||
|
lastModified,
|
||||||
|
})),
|
||||||
|
// Focal Lengths
|
||||||
|
...focalLengths.map(({ focal, lastModified }) => ({
|
||||||
|
url: absolutePathForFocalLength(focal),
|
||||||
|
priority: PRIORITY_CATEGORY,
|
||||||
|
lastModified,
|
||||||
|
})),
|
||||||
|
// Photos
|
||||||
|
...photos.map(({ id, updatedAt }) => ({
|
||||||
|
url: absolutePathForPhoto({ photo: id }),
|
||||||
|
priority: PRIORITY_PHOTO,
|
||||||
|
lastModified: updatedAt,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { CategoryQueryMeta } from '@/category';
|
||||||
import type { Photo } from '@/photo';
|
import type { Photo } from '@/photo';
|
||||||
import { isCameraMakeApple } from '@/platforms/apple';
|
import { isCameraMakeApple } from '@/platforms/apple';
|
||||||
import { formatSonyModel, isMakeSony } from '@/platforms/sony';
|
import { formatSonyModel, isMakeSony } from '@/platforms/sony';
|
||||||
@ -18,13 +19,12 @@ export interface PhotoCameraProps {
|
|||||||
params: Promise<Camera & { photoId: string }>
|
params: Promise<Camera & { photoId: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CameraWithCount = {
|
export type CameraWithMeta = {
|
||||||
cameraKey: string
|
cameraKey: string
|
||||||
camera: Camera
|
camera: Camera
|
||||||
count: number
|
} & CategoryQueryMeta;
|
||||||
}
|
|
||||||
|
|
||||||
export type Cameras = CameraWithCount[];
|
export type Cameras = CameraWithMeta[];
|
||||||
|
|
||||||
// Support keys for make-only and model-only camera queries
|
// Support keys for make-only and model-only camera queries
|
||||||
export const createCameraKey = ({ make, model }: Partial<Camera>) =>
|
export const createCameraKey = ({ make, model }: Partial<Camera>) =>
|
||||||
@ -42,8 +42,8 @@ export const formatCameraParams = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const sortCamerasWithCount = (
|
export const sortCamerasWithCount = (
|
||||||
a: CameraWithCount,
|
a: CameraWithMeta,
|
||||||
b: CameraWithCount,
|
b: CameraWithMeta,
|
||||||
) => {
|
) => {
|
||||||
const aText = formatCameraText(a.camera);
|
const aText = formatCameraText(a.camera);
|
||||||
const bText = formatCameraText(b.camera);
|
const bText = formatCameraText(b.camera);
|
||||||
|
|||||||
@ -28,6 +28,11 @@ export const DEFAULT_CATEGORY_KEYS: CategoryKeys = [
|
|||||||
'films',
|
'films',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export interface CategoryQueryMeta {
|
||||||
|
count: number
|
||||||
|
lastModified: Date
|
||||||
|
}
|
||||||
|
|
||||||
export const getHiddenCategories = (keys: CategoryKeys): CategoryKeys =>
|
export const getHiddenCategories = (keys: CategoryKeys): CategoryKeys =>
|
||||||
CATEGORY_KEYS.filter(key => !keys.includes(key));
|
CATEGORY_KEYS.filter(key => !keys.includes(key));
|
||||||
|
|
||||||
|
|||||||
@ -20,13 +20,11 @@ import {
|
|||||||
import { AnnotatedTag } from '@/photo/form';
|
import { AnnotatedTag } from '@/photo/form';
|
||||||
import PhotoFilmIcon from './PhotoFilmIcon';
|
import PhotoFilmIcon from './PhotoFilmIcon';
|
||||||
import { AppTextState } from '@/i18n/state';
|
import { AppTextState } from '@/i18n/state';
|
||||||
|
import { CategoryQueryMeta } from '@/category';
|
||||||
|
|
||||||
export type FilmWithCount = {
|
export type FilmWithMeta = { film: string } & CategoryQueryMeta
|
||||||
film: string
|
|
||||||
count: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Films = FilmWithCount[]
|
export type Films = FilmWithMeta[]
|
||||||
|
|
||||||
export const labelForFilm = (film: string) => {
|
export const labelForFilm = (film: string) => {
|
||||||
// Use Fujifilm simulation text when recognized
|
// Use Fujifilm simulation text when recognized
|
||||||
@ -48,8 +46,8 @@ export const sortFilms = (
|
|||||||
) => films.sort(sortFilmsWithCount);
|
) => films.sort(sortFilmsWithCount);
|
||||||
|
|
||||||
export const sortFilmsWithCount = (
|
export const sortFilmsWithCount = (
|
||||||
a: FilmWithCount,
|
a: FilmWithMeta,
|
||||||
b: FilmWithCount,
|
b: FilmWithMeta,
|
||||||
) => {
|
) => {
|
||||||
const aLabel = labelForFilm(a.film).large;
|
const aLabel = labelForFilm(a.film).large;
|
||||||
const bLabel = labelForFilm(b.film).large;
|
const bLabel = labelForFilm(b.film).large;
|
||||||
|
|||||||
@ -9,11 +9,11 @@ import {
|
|||||||
absolutePathForFocalLengthImage,
|
absolutePathForFocalLengthImage,
|
||||||
} from '@/app/paths';
|
} from '@/app/paths';
|
||||||
import { AppTextState } from '@/i18n/state';
|
import { AppTextState } from '@/i18n/state';
|
||||||
|
import { CategoryQueryMeta } from '@/category';
|
||||||
|
|
||||||
export type FocalLengths = {
|
type FocalLengthWithMeta = { focal: number } & CategoryQueryMeta;
|
||||||
focal: number
|
|
||||||
count: number
|
export type FocalLengths = FocalLengthWithMeta[];
|
||||||
}[]
|
|
||||||
|
|
||||||
export const getFocalLengthFromString = (focalString?: string) => {
|
export const getFocalLengthFromString = (focalString?: string) => {
|
||||||
const focal = focalString?.match(/^([0-9]+)mm/)?.[1];
|
const focal = focalString?.match(/^([0-9]+)mm/)?.[1];
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { parameterize } from '@/utility/string';
|
|||||||
import { formatAppleLensText, isLensApple } from '../platforms/apple';
|
import { formatAppleLensText, isLensApple } from '../platforms/apple';
|
||||||
import { MISSING_FIELD } from '@/app/paths';
|
import { MISSING_FIELD } from '@/app/paths';
|
||||||
import { formatGoogleLensText, isLensGoogle } from '../platforms/google';
|
import { formatGoogleLensText, isLensGoogle } from '../platforms/google';
|
||||||
|
import { CategoryQueryMeta } from '@/category';
|
||||||
|
|
||||||
const LENS_PLACEHOLDER: Lens = { make: 'Lens', model: 'Model' };
|
const LENS_PLACEHOLDER: Lens = { make: 'Lens', model: 'Model' };
|
||||||
|
|
||||||
@ -21,13 +22,12 @@ export interface LensPhotoProps {
|
|||||||
params: Promise<LensWithPhotoId>
|
params: Promise<LensWithPhotoId>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LensWithCount = {
|
export type LensWithMeta = {
|
||||||
lensKey: string
|
lensKey: string
|
||||||
lens: Lens
|
lens: Lens
|
||||||
count: number
|
} & CategoryQueryMeta;
|
||||||
}
|
|
||||||
|
|
||||||
export type Lenses = LensWithCount[];
|
export type Lenses = LensWithMeta[];
|
||||||
|
|
||||||
export const getLensFromParams = async (
|
export const getLensFromParams = async (
|
||||||
params: Promise<Lens>,
|
params: Promise<Lens>,
|
||||||
@ -71,8 +71,8 @@ export const formatLensParams = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const sortLensesWithCount = (
|
export const sortLensesWithCount = (
|
||||||
a: LensWithCount,
|
a: LensWithMeta,
|
||||||
b: LensWithCount,
|
b: LensWithMeta,
|
||||||
) => {
|
) => {
|
||||||
const aText = formatLensText(a.lens);
|
const aText = formatLensText(a.lens);
|
||||||
const bText = formatLensText(b.lens);
|
const bText = formatLensText(b.lens);
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import {
|
|||||||
getPhotos,
|
getPhotos,
|
||||||
getUniqueCameras,
|
getUniqueCameras,
|
||||||
getUniqueTags,
|
getUniqueTags,
|
||||||
getUniqueTagsHidden,
|
|
||||||
getUniqueFilms,
|
getUniqueFilms,
|
||||||
getPhotosNearId,
|
getPhotosNearId,
|
||||||
getPhotosMostRecentUpdate,
|
getPhotosMostRecentUpdate,
|
||||||
@ -50,7 +49,6 @@ const KEY_RECIPES = 'recipes';
|
|||||||
const KEY_FOCAL_LENGTHS = 'focal-lengths';
|
const KEY_FOCAL_LENGTHS = 'focal-lengths';
|
||||||
// Type keys
|
// Type keys
|
||||||
const KEY_COUNT = 'count';
|
const KEY_COUNT = 'count';
|
||||||
const KEY_HIDDEN = 'hidden';
|
|
||||||
const KEY_DATE_RANGE = 'date-range';
|
const KEY_DATE_RANGE = 'date-range';
|
||||||
|
|
||||||
const getPhotosCacheKeyForOption = (
|
const getPhotosCacheKeyForOption = (
|
||||||
@ -211,12 +209,6 @@ export const getUniqueTagsCached =
|
|||||||
[KEY_PHOTOS, KEY_TAGS],
|
[KEY_PHOTOS, KEY_TAGS],
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getUniqueTagsHiddenCached =
|
|
||||||
unstable_cache(
|
|
||||||
getUniqueTagsHidden,
|
|
||||||
[KEY_PHOTOS, KEY_TAGS, KEY_HIDDEN],
|
|
||||||
);
|
|
||||||
|
|
||||||
export const getUniqueCamerasCached =
|
export const getUniqueCamerasCached =
|
||||||
unstable_cache(
|
unstable_cache(
|
||||||
getUniqueCameras,
|
getUniqueCameras,
|
||||||
|
|||||||
@ -323,73 +323,75 @@ export const getPhotosMostRecentUpdate = async () =>
|
|||||||
|
|
||||||
export const getUniqueTags = async () =>
|
export const getUniqueTags = async () =>
|
||||||
safelyQueryPhotos(() => sql`
|
safelyQueryPhotos(() => sql`
|
||||||
SELECT DISTINCT unnest(tags) as tag, COUNT(*)
|
SELECT DISTINCT unnest(tags) as tag,
|
||||||
|
COUNT(*),
|
||||||
|
MAX(updated_at) as last_modified
|
||||||
FROM photos
|
FROM photos
|
||||||
WHERE hidden IS NOT TRUE
|
WHERE hidden IS NOT TRUE
|
||||||
GROUP BY tag
|
GROUP BY tag
|
||||||
ORDER BY tag ASC
|
ORDER BY tag ASC
|
||||||
`.then(({ rows }): Tags => rows.map(({ tag, count }) => ({
|
`.then(({ rows }): Tags => rows.map(({ tag, count, last_modified }) => ({
|
||||||
tag: tag as string,
|
tag: tag as string,
|
||||||
count: parseInt(count, 10),
|
count: parseInt(count, 10),
|
||||||
|
lastModified: last_modified as Date,
|
||||||
})))
|
})))
|
||||||
, 'getUniqueTags');
|
, 'getUniqueTags');
|
||||||
|
|
||||||
export const getUniqueTagsHidden = async () =>
|
|
||||||
safelyQueryPhotos(() => sql`
|
|
||||||
SELECT DISTINCT unnest(tags) as tag, COUNT(*)
|
|
||||||
FROM photos
|
|
||||||
GROUP BY tag
|
|
||||||
ORDER BY tag ASC
|
|
||||||
`.then(({ rows }): Tags => rows.map(({ tag, count }) => ({
|
|
||||||
tag: tag as string,
|
|
||||||
count: parseInt(count, 10),
|
|
||||||
})))
|
|
||||||
, 'getUniqueTagsHidden');
|
|
||||||
|
|
||||||
export const getUniqueCameras = async () =>
|
export const getUniqueCameras = async () =>
|
||||||
safelyQueryPhotos(() => sql`
|
safelyQueryPhotos(() => sql`
|
||||||
SELECT DISTINCT make||' '||model as camera, make, model, COUNT(*)
|
SELECT DISTINCT make||' '||model as camera, make, model,
|
||||||
|
COUNT(*),
|
||||||
|
MAX(updated_at) as last_modified
|
||||||
FROM photos
|
FROM photos
|
||||||
WHERE hidden IS NOT TRUE
|
WHERE hidden IS NOT TRUE
|
||||||
AND trim(make) <> ''
|
AND trim(make) <> ''
|
||||||
AND trim(model) <> ''
|
AND trim(model) <> ''
|
||||||
GROUP BY make, model
|
GROUP BY make, model
|
||||||
ORDER BY camera ASC
|
ORDER BY camera ASC
|
||||||
`.then(({ rows }): Cameras => rows.map(({ make, model, count }) => ({
|
`.then(({ rows }): Cameras => rows.map(({
|
||||||
|
make, model, count, last_modified,
|
||||||
|
}) => ({
|
||||||
cameraKey: createCameraKey({ make, model }),
|
cameraKey: createCameraKey({ make, model }),
|
||||||
camera: { make, model },
|
camera: { make, model },
|
||||||
count: parseInt(count, 10),
|
count: parseInt(count, 10),
|
||||||
|
lastModified: last_modified as Date,
|
||||||
})))
|
})))
|
||||||
, 'getUniqueCameras');
|
, 'getUniqueCameras');
|
||||||
|
|
||||||
export const getUniqueLenses = async () =>
|
export const getUniqueLenses = async () =>
|
||||||
safelyQueryPhotos(() => sql`
|
safelyQueryPhotos(() => sql`
|
||||||
SELECT DISTINCT lens_make||' '||lens_model as lens,
|
SELECT DISTINCT lens_make||' '||lens_model as lens,
|
||||||
lens_make, lens_model, COUNT(*)
|
lens_make, lens_model,
|
||||||
|
COUNT(*),
|
||||||
|
MAX(updated_at) as last_modified
|
||||||
FROM photos
|
FROM photos
|
||||||
WHERE hidden IS NOT TRUE
|
WHERE hidden IS NOT TRUE
|
||||||
AND trim(lens_model) <> ''
|
AND trim(lens_model) <> ''
|
||||||
GROUP BY lens_make, lens_model
|
GROUP BY lens_make, lens_model
|
||||||
ORDER BY lens ASC
|
ORDER BY lens ASC
|
||||||
`.then(({ rows }): Lenses => rows
|
`.then(({ rows }): Lenses => rows
|
||||||
.map(({ lens_make: make, lens_model: model, count }) => ({
|
.map(({ lens_make: make, lens_model: model, count, last_modified }) => ({
|
||||||
lensKey: createLensKey({ make, model }),
|
lensKey: createLensKey({ make, model }),
|
||||||
lens: { make, model },
|
lens: { make, model },
|
||||||
count: parseInt(count, 10),
|
count: parseInt(count, 10),
|
||||||
|
lastModified: last_modified as Date,
|
||||||
})))
|
})))
|
||||||
, 'getUniqueLenses');
|
, 'getUniqueLenses');
|
||||||
|
|
||||||
export const getUniqueRecipes = async () =>
|
export const getUniqueRecipes = async () =>
|
||||||
safelyQueryPhotos(() => sql`
|
safelyQueryPhotos(() => sql`
|
||||||
SELECT DISTINCT recipe_title, COUNT(*)
|
SELECT DISTINCT recipe_title,
|
||||||
|
COUNT(*),
|
||||||
|
MAX(updated_at) as last_modified
|
||||||
FROM photos
|
FROM photos
|
||||||
WHERE hidden IS NOT TRUE AND recipe_title IS NOT NULL
|
WHERE hidden IS NOT TRUE AND recipe_title IS NOT NULL
|
||||||
GROUP BY recipe_title
|
GROUP BY recipe_title
|
||||||
ORDER BY recipe_title ASC
|
ORDER BY recipe_title ASC
|
||||||
`.then(({ rows }): Recipes => rows
|
`.then(({ rows }): Recipes => rows
|
||||||
.map(({ recipe_title, count }) => ({
|
.map(({ recipe_title, count, last_modified }) => ({
|
||||||
recipe: recipe_title,
|
recipe: recipe_title,
|
||||||
count: parseInt(count, 10),
|
count: parseInt(count, 10),
|
||||||
|
lastModified: last_modified as Date,
|
||||||
})))
|
})))
|
||||||
, 'getUniqueRecipes');
|
, 'getUniqueRecipes');
|
||||||
|
|
||||||
@ -438,29 +440,35 @@ export const updateAllMatchingRecipeTitles = (
|
|||||||
|
|
||||||
export const getUniqueFilms = async () =>
|
export const getUniqueFilms = async () =>
|
||||||
safelyQueryPhotos(() => sql`
|
safelyQueryPhotos(() => sql`
|
||||||
SELECT DISTINCT film, COUNT(*)
|
SELECT DISTINCT film,
|
||||||
|
COUNT(*),
|
||||||
|
MAX(updated_at) as last_modified
|
||||||
FROM photos
|
FROM photos
|
||||||
WHERE hidden IS NOT TRUE AND film IS NOT NULL
|
WHERE hidden IS NOT TRUE AND film IS NOT NULL
|
||||||
GROUP BY film
|
GROUP BY film
|
||||||
ORDER BY film ASC
|
ORDER BY film ASC
|
||||||
`.then(({ rows }): Films => rows
|
`.then(({ rows }): Films => rows
|
||||||
.map(({ film, count }) => ({
|
.map(({ film, count, last_modified }) => ({
|
||||||
film,
|
film,
|
||||||
count: parseInt(count, 10),
|
count: parseInt(count, 10),
|
||||||
|
lastModified: last_modified as Date,
|
||||||
})))
|
})))
|
||||||
, 'getUniqueFilms');
|
, 'getUniqueFilms');
|
||||||
|
|
||||||
export const getUniqueFocalLengths = async () =>
|
export const getUniqueFocalLengths = async () =>
|
||||||
safelyQueryPhotos(() => sql`
|
safelyQueryPhotos(() => sql`
|
||||||
SELECT DISTINCT focal_length, COUNT(*)
|
SELECT DISTINCT focal_length,
|
||||||
|
COUNT(*),
|
||||||
|
MAX(updated_at) as last_modified
|
||||||
FROM photos
|
FROM photos
|
||||||
WHERE hidden IS NOT TRUE AND focal_length IS NOT NULL
|
WHERE hidden IS NOT TRUE AND focal_length IS NOT NULL
|
||||||
GROUP BY focal_length
|
GROUP BY focal_length
|
||||||
ORDER BY focal_length ASC
|
ORDER BY focal_length ASC
|
||||||
`.then(({ rows }): FocalLengths => rows
|
`.then(({ rows }): FocalLengths => rows
|
||||||
.map(({ focal_length, count }) => ({
|
.map(({ focal_length, count, last_modified }) => ({
|
||||||
focal: parseInt(focal_length, 10),
|
focal: parseInt(focal_length, 10),
|
||||||
count: parseInt(count, 10),
|
count: parseInt(count, 10),
|
||||||
|
lastModified: last_modified as Date,
|
||||||
})))
|
})))
|
||||||
, 'getUniqueFocalLengths');
|
, 'getUniqueFocalLengths');
|
||||||
|
|
||||||
@ -562,6 +570,13 @@ export const getPublicPhotoIds = async ({ limit }: { limit?: number }) =>
|
|||||||
.then(({ rows }) => rows.map(({ id }) => id as string))
|
.then(({ rows }) => rows.map(({ id }) => id as string))
|
||||||
, 'getPublicPhotoIds');
|
, 'getPublicPhotoIds');
|
||||||
|
|
||||||
|
export const getPhotoIdsAndUpdatedAt = async () =>
|
||||||
|
safelyQueryPhotos(() =>
|
||||||
|
sql`SELECT id, updated_at FROM photos WHERE hidden IS NOT TRUE`
|
||||||
|
.then(({ rows }) => rows.map(({ id, updated_at }) =>
|
||||||
|
({ id: id as string, updatedAt: updated_at as Date })))
|
||||||
|
, 'getPhotoIdsAndUpdatedAt');
|
||||||
|
|
||||||
export const getPhoto = async (
|
export const getPhoto = async (
|
||||||
id: string,
|
id: string,
|
||||||
includeHidden?: boolean,
|
includeHidden?: boolean,
|
||||||
|
|||||||
@ -9,13 +9,11 @@ import {
|
|||||||
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||||
import { labelForFilm } from '@/film';
|
import { labelForFilm } from '@/film';
|
||||||
import { AppTextState } from '@/i18n/state';
|
import { AppTextState } from '@/i18n/state';
|
||||||
|
import { CategoryQueryMeta } from '@/category';
|
||||||
|
|
||||||
export type RecipeWithCount = {
|
export type RecipeWithMeta = { recipe: string } & CategoryQueryMeta
|
||||||
recipe: string
|
|
||||||
count: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Recipes = RecipeWithCount[]
|
export type Recipes = RecipeWithMeta[]
|
||||||
|
|
||||||
export interface RecipeProps {
|
export interface RecipeProps {
|
||||||
title?: string
|
title?: string
|
||||||
|
|||||||
@ -15,17 +15,16 @@ import {
|
|||||||
formatCount,
|
formatCount,
|
||||||
formatCountDescriptive,
|
formatCountDescriptive,
|
||||||
} from '@/utility/string';
|
} from '@/utility/string';
|
||||||
import { sortCategoryByCount } from '@/category';
|
import { CategoryQueryMeta, sortCategoryByCount } from '@/category';
|
||||||
import { AppTextState } from '@/i18n/state';
|
import { AppTextState } from '@/i18n/state';
|
||||||
|
|
||||||
// Reserved tags
|
// Reserved tags
|
||||||
export const TAG_FAVS = 'favs';
|
export const TAG_FAVS = 'favs';
|
||||||
export const TAG_HIDDEN = 'hidden';
|
export const TAG_HIDDEN = 'hidden';
|
||||||
|
|
||||||
export type Tags = {
|
type TagWithMeta = { tag: string } & CategoryQueryMeta;
|
||||||
tag: string
|
|
||||||
count: number
|
export type Tags = TagWithMeta[]
|
||||||
}[]
|
|
||||||
|
|
||||||
export const formatTag = (tag?: string) =>
|
export const formatTag = (tag?: string) =>
|
||||||
capitalizeWords(tag?.replaceAll('-', ' '));
|
capitalizeWords(tag?.replaceAll('-', ' '));
|
||||||
@ -138,11 +137,19 @@ export const isPathFavs = (pathname?: string) =>
|
|||||||
|
|
||||||
export const isTagHidden = (tag: string) => tag.toLowerCase() === TAG_HIDDEN;
|
export const isTagHidden = (tag: string) => tag.toLowerCase() === TAG_HIDDEN;
|
||||||
|
|
||||||
export const addHiddenToTags = (tags: Tags, photosCountHidden = 0) =>
|
export const addHiddenToTags = (
|
||||||
photosCountHidden > 0
|
tags: Tags,
|
||||||
|
countHidden = 0,
|
||||||
|
lastModifiedHidden = new Date(),
|
||||||
|
) =>
|
||||||
|
countHidden > 0
|
||||||
? tags
|
? tags
|
||||||
.filter(({ tag }) => tag === TAG_FAVS)
|
.filter(({ tag }) => tag === TAG_FAVS)
|
||||||
.concat({ tag: TAG_HIDDEN, count: photosCountHidden })
|
.concat({
|
||||||
|
tag: TAG_HIDDEN,
|
||||||
|
count: countHidden,
|
||||||
|
lastModified: lastModifiedHidden,
|
||||||
|
})
|
||||||
.concat(tags
|
.concat(tags
|
||||||
.filter(({ tag }) => tag !== TAG_FAVS)
|
.filter(({ tag }) => tag !== TAG_FAVS)
|
||||||
.sort(sortCategoryByCount),
|
.sort(sortCategoryByCount),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user