Vercel/src/photo/index.ts
2024-02-22 23:01:41 -06:00

256 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { FilmSimulation } from '@/simulation';
import { HIGH_DENSITY_GRID, SHOW_EXIF_DATA } from '@/site/config';
import { ABSOLUTE_PATH_FOR_HOME_IMAGE } from '@/site/paths';
import { formatDateFromPostgresString } from '@/utility/date';
import {
formatAperture,
formatIso,
formatExposureCompensation,
formatExposureTime,
formatFocalLength,
} from '@/utility/exif';
import camelcaseKeys from 'camelcase-keys';
import type { Metadata } from 'next';
export const GENERATE_STATIC_PARAMS_LIMIT = 1000;
export const INFINITE_SCROLL_MULTIPLE_ROOT =
process.env.NODE_ENV === 'development' ? 2 : 12;
export const INFINITE_SCROLL_MULTIPLE_GRID = HIGH_DENSITY_GRID
? process.env.NODE_ENV === 'development' ? 4 : 20
: process.env.NODE_ENV === 'development' ? 4 : 24;
export const GRID_THUMBNAILS_TO_SHOW_MAX = 12;
export const ACCEPTED_PHOTO_FILE_TYPES = [
'image/jpg',
'image/jpeg',
'image/png',
];
export const MAX_PHOTO_UPLOAD_SIZE_IN_BYTES = 50_000_000;
// Core EXIF data
export interface PhotoExif {
aspectRatio: number
make?: string
model?: string
focalLength?: number
focalLengthIn35MmFormat?: number
fNumber?: number
iso?: number
exposureTime?: number
exposureCompensation?: number
latitude?: number
longitude?: number
filmSimulation?: FilmSimulation
takenAt?: string
takenAtNaive?: string
}
// Raw db insert
export interface PhotoDbInsert extends PhotoExif {
id: string
url: string
extension: string
blurData?: string
title?: string
tags?: string[]
locationName?: string
priorityOrder?: number
hidden?: boolean
takenAt: string
takenAtNaive: string
}
// Raw db response
export interface PhotoDb extends Omit<PhotoDbInsert, 'takenAt' | 'tags'> {
updatedAt: Date
createdAt: Date
takenAt: Date
tags: string[]
}
// Parsed db response
export interface Photo extends PhotoDb {
focalLengthFormatted?: string
focalLengthIn35MmFormatFormatted?: string
fNumberFormatted?: string
isoFormatted?: string
exposureTimeFormatted?: string
exposureCompensationFormatted?: string
takenAtNaiveFormatted: string
}
export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => {
const photoDb = camelcaseKeys(
photoDbRaw as unknown as Record<string, unknown>
) as unknown as PhotoDb;
return {
...photoDb,
tags: photoDb.tags ?? [],
focalLengthFormatted:
formatFocalLength(photoDb.focalLength),
focalLengthIn35MmFormatFormatted:
formatFocalLength(photoDb.focalLengthIn35MmFormat),
fNumberFormatted:
formatAperture(photoDb.fNumber),
isoFormatted:
formatIso(photoDb.iso),
exposureTimeFormatted:
formatExposureTime(photoDb.exposureTime),
exposureCompensationFormatted:
formatExposureCompensation(photoDb.exposureCompensation),
takenAtNaiveFormatted:
formatDateFromPostgresString(photoDb.takenAtNaive),
};
};
export const parseCachedPhotoDates = (photo: Photo) => ({
...photo,
takenAt: new Date(photo.takenAt),
updatedAt: new Date(photo.updatedAt),
createdAt: new Date(photo.createdAt),
} as Photo);
export const parseCachedPhotosDates = (photos: Photo[]) =>
photos.map(parseCachedPhotoDates);
export const convertPhotoToPhotoDbInsert = (
photo: Photo,
): PhotoDbInsert => ({
...photo,
takenAt: photo.takenAt.toISOString(),
});
export const photoStatsAsString = (photo: Photo) => [
photo.model,
photo.focalLengthFormatted,
photo.fNumberFormatted,
photo.isoFormatted,
].join(' ');
export const descriptionForPhoto = (photo: Photo) =>
photo.takenAtNaiveFormatted?.toUpperCase();
export const getPreviousPhoto = (photo: Photo, photos: Photo[]) => {
const index = photos.findIndex(p => p.id === photo.id);
return index > 0
? photos[index - 1]
: undefined;
};
export const getNextPhoto = (photo: Photo, photos: Photo[]) => {
const index = photos.findIndex(p => p.id === photo.id);
return index < photos.length - 1
? photos[index + 1]
: undefined;
};
export const generateOgImageMetaForPhotos = (photos: Photo[]): Metadata => {
if (photos.length > 0) {
return {
openGraph: {
images: ABSOLUTE_PATH_FOR_HOME_IMAGE,
},
twitter: {
card: 'summary_large_image',
images: ABSOLUTE_PATH_FOR_HOME_IMAGE,
},
};
} else {
// If there are no photos, refrain from showing an OG image
return {};
}
};
const PHOTO_ID_FORWARDING_TABLE: Record<string, string> = JSON.parse(
process.env.PHOTO_ID_FORWARDING_TABLE || '{}'
);
export const translatePhotoId = (id: string) =>
PHOTO_ID_FORWARDING_TABLE[id] || id;
export const titleForPhoto = (photo: Photo) =>
photo.title || 'Untitled';
export const photoLabelForCount = (count: number) =>
count === 1 ? 'Photo' : 'Photos';
export const photoQuantityText = (count: number, includeParentheses = true) =>
includeParentheses
? `(${count} ${photoLabelForCount(count)})`
: `${count} ${photoLabelForCount(count)}`;
export const deleteConfirmationTextForPhoto = (photo: Photo) =>
`Are you sure you want to delete "${titleForPhoto(photo)}?"`;
export type PhotoDateRange = { start: string, end: string };
export const descriptionForPhotoSet = (
photos:Photo[],
descriptor?: string,
dateBased?: boolean,
explicitCount?: number,
explicitDateRange?: PhotoDateRange,
) =>
dateBased
? dateRangeForPhotos(photos, explicitDateRange).description.toUpperCase()
: [
explicitCount ?? photos.length,
descriptor,
photoLabelForCount(explicitCount ?? photos.length),
].join(' ');
const sortPhotosByDate = (
photos: Photo[],
order: 'ASC' | 'DESC' = 'DESC'
) =>
[...photos].sort((a, b) => order === 'DESC'
? b.takenAt.getTime() - a.takenAt.getTime()
: a.takenAt.getTime() - b.takenAt.getTime());
export const dateRangeForPhotos = (
photos: Photo[] = [],
explicitDateRange?: PhotoDateRange,
) => {
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}`;
}
return { start, end, description };
};
const photoHasCameraData = (photo: Photo) =>
photo.make &&
photo.model;
const photoHasExifData = (photo: Photo) =>
photo.focalLength ||
photo.focalLengthIn35MmFormat ||
photo.fNumberFormatted ||
photo.isoFormatted ||
photo.exposureTimeFormatted ||
photo.exposureCompensationFormatted;
export const shouldShowCameraDataForPhoto = (photo: Photo) =>
SHOW_EXIF_DATA && photoHasCameraData(photo);
export const shouldShowExifDataForPhoto = (photo: Photo) =>
SHOW_EXIF_DATA && photoHasExifData(photo);