From 7dd07aac6ed263bd0c13d325d57c7842c9817b1e Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Fri, 4 Apr 2025 18:09:47 -0500 Subject: [PATCH] Shrink client bundle --- app/api/auth/[...nextauth]/route.ts | 2 +- app/api/storage/presigned-url/[key]/route.ts | 2 +- app/api/storage/vercel-blob/route.ts | 2 +- app/page.tsx | 2 +- app/sign-in/page.tsx | 2 +- middleware.ts | 2 +- src/admin/AdminAppConfigurationClient.tsx | 4 +- src/admin/actions.ts | 2 +- src/admin/insights/AdminAppInsightsClient.tsx | 4 +- src/app/config.ts | 4 +- src/auth/actions.ts | 16 ++-- src/auth/cache.ts | 2 +- src/auth/client.ts | 12 --- src/auth/index.ts | 55 ++----------- src/auth/server.ts | 51 ++++++++++++ src/film/index.tsx | 7 +- src/photo/PhotoGridPage.tsx | 76 +++++------------- src/photo/PhotoGridPageClient.tsx | 56 +++++++++++++ src/photo/PhotoGridSidebar.tsx | 16 ++-- src/photo/actions.ts | 2 +- src/photo/form/index.ts | 51 +----------- src/photo/form/server.ts | 50 ++++++++++++ src/photo/server.ts | 11 ++- src/platforms/fujifilm/index.ts | 80 ------------------- src/platforms/fujifilm/recipe.ts | 2 +- src/platforms/fujifilm/server.ts | 80 +++++++++++++++++++ src/platforms/fujifilm/simulation.ts | 2 +- src/state/AppStateProvider.tsx | 2 +- src/utility/html.ts | 2 +- 29 files changed, 312 insertions(+), 287 deletions(-) delete mode 100644 src/auth/client.ts create mode 100644 src/auth/server.ts create mode 100644 src/photo/PhotoGridPageClient.tsx create mode 100644 src/photo/form/server.ts create mode 100644 src/platforms/fujifilm/server.ts diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index fae49073..0f744da4 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -1 +1 @@ -export { GET, POST } from '@/auth'; +export { GET, POST } from '@/auth/server'; diff --git a/app/api/storage/presigned-url/[key]/route.ts b/app/api/storage/presigned-url/[key]/route.ts index 0856e35c..2896c85c 100644 --- a/app/api/storage/presigned-url/[key]/route.ts +++ b/app/api/storage/presigned-url/[key]/route.ts @@ -1,4 +1,4 @@ -import { auth } from '@/auth'; +import { auth } from '@/auth/server'; import { awsS3Client, awsS3PutObjectCommandForKey, diff --git a/app/api/storage/vercel-blob/route.ts b/app/api/storage/vercel-blob/route.ts index d32fbd08..94a38050 100644 --- a/app/api/storage/vercel-blob/route.ts +++ b/app/api/storage/vercel-blob/route.ts @@ -1,4 +1,4 @@ -import { auth } from '@/auth'; +import { auth } from '@/auth/server'; import { revalidateAdminPaths, revalidatePhotosKey } from '@/photo/cache'; import { ACCEPTED_PHOTO_FILE_TYPES, diff --git a/app/page.tsx b/app/page.tsx index f738529f..5fb773f5 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -9,8 +9,8 @@ import { cache } from 'react'; import { getPhotos, getPhotosMeta } from '@/photo/db/query'; import { GRID_HOMEPAGE_ENABLED } from '@/app/config'; import { getDataForCategories } from '@/category/data'; -import PhotoGridPage from '@/photo/PhotoGridPage'; import PhotoFeedPage from '@/photo/PhotoFeedPage'; +import PhotoGridPage from '@/photo/PhotoGridPage'; export const dynamic = 'force-static'; export const maxDuration = 60; diff --git a/app/sign-in/page.tsx b/app/sign-in/page.tsx index 0bccd823..13ecc9d3 100644 --- a/app/sign-in/page.tsx +++ b/app/sign-in/page.tsx @@ -1,4 +1,4 @@ -import { auth } from '@/auth'; +import { auth } from '@/auth/server'; import SignInForm from '@/auth/SignInForm'; import { PATH_ADMIN, PATH_ROOT } from '@/app/paths'; import { clsx } from 'clsx/lite'; diff --git a/middleware.ts b/middleware.ts index e9c19c68..ba0e2bad 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,4 +1,4 @@ -import { auth } from './src/auth'; +import { auth } from './src/auth/server'; import { NextRequest, NextResponse } from 'next/server'; import type { NextApiRequest, NextApiResponse } from 'next'; import { diff --git a/src/admin/AdminAppConfigurationClient.tsx b/src/admin/AdminAppConfigurationClient.tsx index 7356ed97..c5d33cdb 100644 --- a/src/admin/AdminAppConfigurationClient.tsx +++ b/src/admin/AdminAppConfigurationClient.tsx @@ -14,7 +14,7 @@ import { } from 'react-icons/bi'; import { HiOutlineCog } from 'react-icons/hi'; import ChecklistGroup from '@/components/ChecklistGroup'; -import { ConfigChecklistStatus } from '../app/config'; +import { AppConfiguration } from '../app/config'; import StatusIcon from '@/components/StatusIcon'; import { labelForStorage } from '@/platforms/storage'; import { HiSparkles } from 'react-icons/hi'; @@ -109,7 +109,7 @@ export default function AdminAppConfigurationClient({ // Component props simplifiedView, isAnalyzingConfiguration, -}: ConfigChecklistStatus & +}: AppConfiguration & Partial>> & { simplifiedView?: boolean isAnalyzingConfiguration?: boolean diff --git a/src/admin/actions.ts b/src/admin/actions.ts index e551121c..283a69cb 100644 --- a/src/admin/actions.ts +++ b/src/admin/actions.ts @@ -1,6 +1,6 @@ 'use server'; -import { runAuthenticatedAdminServerAction } from '@/auth'; +import { runAuthenticatedAdminServerAction } from '@/auth/server'; import { testRedisConnection } from '@/platforms/redis'; import { testOpenAiConnection } from '@/platforms/openai'; import { testDatabaseConnection } from '@/platforms/postgres'; diff --git a/src/admin/insights/AdminAppInsightsClient.tsx b/src/admin/insights/AdminAppInsightsClient.tsx index da633400..9801b5ad 100644 --- a/src/admin/insights/AdminAppInsightsClient.tsx +++ b/src/admin/insights/AdminAppInsightsClient.tsx @@ -317,9 +317,7 @@ export default function AdminAppInsightsClient({ />} content="Speed up page load times" expandContent={<> - Improve load times by enabling static optimization - {' '} - on: + Improve load times by enabling static optimization:
{renderLabeledEnvVar( 'Photo pages', diff --git a/src/app/config.ts b/src/app/config.ts index 8ba47615..9713d412 100644 --- a/src/app/config.ts +++ b/src/app/config.ts @@ -399,11 +399,11 @@ export const APP_CONFIGURATION = { commitUrl: VERCEL_GIT_COMMIT_URL, }; -export type ConfigChecklistStatus = typeof APP_CONFIGURATION; - export const IS_SITE_READY = APP_CONFIGURATION.hasDatabase && APP_CONFIGURATION.hasStorageProvider && APP_CONFIGURATION.hasAuthSecret && APP_CONFIGURATION.hasAdminUser; + +export type AppConfiguration = typeof APP_CONFIGURATION; \ No newline at end of file diff --git a/src/auth/actions.ts b/src/auth/actions.ts index e2a61d1f..532a99b5 100644 --- a/src/auth/actions.ts +++ b/src/auth/actions.ts @@ -1,18 +1,20 @@ 'use server'; import { + auth, + signIn, + signOut, +} from '@/auth/server'; +import type { Session } from 'next-auth'; +import { redirect } from 'next/navigation'; +import { + generateAuthSecret, KEY_CALLBACK_URL, KEY_CREDENTIALS_CALLBACK_ROUTE_ERROR_URL, KEY_CREDENTIALS_SIGN_IN_ERROR, KEY_CREDENTIALS_SIGN_IN_ERROR_URL, KEY_CREDENTIALS_SUCCESS, - auth, - generateAuthSecret, - signIn, - signOut, -} from '@/auth'; -import type { Session } from 'next-auth'; -import { redirect } from 'next/navigation'; +} from '.'; export const signInAction = async ( _prevState: string | undefined, diff --git a/src/auth/cache.ts b/src/auth/cache.ts index e1079046..486cf43c 100644 --- a/src/auth/cache.ts +++ b/src/auth/cache.ts @@ -1,4 +1,4 @@ import { cache } from 'react'; -import { auth } from '@/auth'; +import { auth } from '@/auth/server'; export const authCachedSafe = cache(() => auth().catch(() => null)); diff --git a/src/auth/client.ts b/src/auth/client.ts deleted file mode 100644 index 49645be4..00000000 --- a/src/auth/client.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { deleteCookie, getCookie, storeCookie } from '@/utility/cookie'; - -const KEY_AUTH_EMAIL = 'authjs.email'; - -export const storeAuthEmailCookie = (email: string) => - storeCookie(KEY_AUTH_EMAIL, email); - -export const clearAuthEmailCookie = () => - deleteCookie(KEY_AUTH_EMAIL); - -export const hasAuthEmailCookie = () => - Boolean(getCookie(KEY_AUTH_EMAIL)); diff --git a/src/auth/index.ts b/src/auth/index.ts index b1978b19..057dd9ad 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -1,6 +1,4 @@ -import { isPathProtected } from '@/app/paths'; -import NextAuth, { User } from 'next-auth'; -import Credentials from 'next-auth/providers/credentials'; +import { deleteCookie, getCookie, storeCookie } from '@/utility/cookie'; export const KEY_CREDENTIALS_SIGN_IN_ERROR = 'CredentialsSignin'; export const KEY_CREDENTIALS_SIGN_IN_ERROR_URL = @@ -10,53 +8,16 @@ export const KEY_CREDENTIALS_CALLBACK_ROUTE_ERROR_URL = export const KEY_CREDENTIALS_SUCCESS = 'success'; export const KEY_CALLBACK_URL = 'callbackUrl'; -export const { - handlers: { GET, POST }, - signIn, - signOut, - auth, -} = NextAuth({ - providers: [ - Credentials({ - async authorize({ email, password }) { - if ( - process.env.ADMIN_EMAIL && process.env.ADMIN_EMAIL === email && - process.env.ADMIN_PASSWORD && process.env.ADMIN_PASSWORD === password - ) { - const user: User = { email, name: 'Admin User' }; - return user; - } else { - return null; - } - }, - }), - ], - callbacks: { - authorized({ auth, request }) { - const { pathname } = request.nextUrl; +const KEY_AUTH_EMAIL = 'authjs.email'; - const isUrlProtected = isPathProtected(pathname); - const isUserLoggedIn = !!auth?.user; - const isRequestAuthorized = !isUrlProtected || isUserLoggedIn; +export const storeAuthEmailCookie = (email: string) => + storeCookie(KEY_AUTH_EMAIL, email); - return isRequestAuthorized; - }, - }, - pages: { - signIn: '/sign-in', - }, -}); +export const clearAuthEmailCookie = () => + deleteCookie(KEY_AUTH_EMAIL); -export const runAuthenticatedAdminServerAction = async ( - callback: () => T, -): Promise => { - const session = await auth(); - if (session?.user) { - return callback(); - } else { - throw new Error('Unauthorized server action request'); - } -}; +export const hasAuthEmailCookie = () => + Boolean(getCookie(KEY_AUTH_EMAIL)); export const generateAuthSecret = () => fetch( 'https://generate-secret.vercel.app/32', diff --git a/src/auth/server.ts b/src/auth/server.ts new file mode 100644 index 00000000..4a8b9a1b --- /dev/null +++ b/src/auth/server.ts @@ -0,0 +1,51 @@ +import { isPathProtected } from '@/app/paths'; +import NextAuth, { User } from 'next-auth'; +import Credentials from 'next-auth/providers/credentials'; + +export const { + handlers: { GET, POST }, + signIn, + signOut, + auth, +} = NextAuth({ + providers: [ + Credentials({ + async authorize({ email, password }) { + if ( + process.env.ADMIN_EMAIL && process.env.ADMIN_EMAIL === email && + process.env.ADMIN_PASSWORD && process.env.ADMIN_PASSWORD === password + ) { + const user: User = { email, name: 'Admin User' }; + return user; + } else { + return null; + } + }, + }), + ], + callbacks: { + authorized({ auth, request }) { + const { pathname } = request.nextUrl; + + const isUrlProtected = isPathProtected(pathname); + const isUserLoggedIn = !!auth?.user; + const isRequestAuthorized = !isUrlProtected || isUserLoggedIn; + + return isRequestAuthorized; + }, + }, + pages: { + signIn: '/sign-in', + }, +}); + +export const runAuthenticatedAdminServerAction = async ( + callback: () => T, +): Promise => { + const session = await auth(); + if (session?.user) { + return callback(); + } else { + throw new Error('Unauthorized server action request'); + } +}; diff --git a/src/film/index.tsx b/src/film/index.tsx index 341acfb1..36162e8a 100644 --- a/src/film/index.tsx +++ b/src/film/index.tsx @@ -12,8 +12,11 @@ import { FUJIFILM_SIMULATION_FORM_INPUT_OPTIONS, labelForFujifilmSimulation, } from '@/platforms/fujifilm/simulation'; -import { deparameterize, formatCount } from '@/utility/string'; -import { formatCountDescriptive } from '@/utility/string'; +import { + deparameterize, + formatCount, + formatCountDescriptive, +} from '@/utility/string'; import { AnnotatedTag } from '@/photo/form'; import PhotoFilmIcon from './PhotoFilmIcon'; diff --git a/src/photo/PhotoGridPage.tsx b/src/photo/PhotoGridPage.tsx index c68896e7..d99d226a 100644 --- a/src/photo/PhotoGridPage.tsx +++ b/src/photo/PhotoGridPage.tsx @@ -1,58 +1,24 @@ -'use client'; +import { ComponentProps } from 'react'; +import PhotoGridPageClient from './PhotoGridPageClient'; +import { + htmlHasBrParagraphBreaks, + safelyParseFormattedHtml, +} from '@/utility/html'; +import { PAGE_ABOUT } from '@/app/config'; -import { Photo } from '.'; -import { PATH_GRID_INFERRED } from '@/app/paths'; -import PhotoGridSidebar from './PhotoGridSidebar'; -import PhotoGridContainer from './PhotoGridContainer'; -import { useEffect, useRef } from 'react'; -import { useAppState } from '@/state/AppState'; -import clsx from 'clsx/lite'; -import { PhotoSetCategories } from '@/category'; -import useElementHeight from '@/utility/useElementHeight'; -import MaskedScroll from '@/components/MaskedScroll'; +export default function PhotoGridPage( + props: ComponentProps, +) { + const aboutTextSafelyParsedHtml = PAGE_ABOUT + ? safelyParseFormattedHtml(PAGE_ABOUT) + : undefined; + const aboutTextHasBrParagraphBreaks = PAGE_ABOUT + ? htmlHasBrParagraphBreaks(PAGE_ABOUT) + : false; -export default function PhotoGridPage({ - photos, - photosCount, - ...categories -}: PhotoSetCategories & { - photos: Photo[] - photosCount: number -}) { - const ref = useRef(null); - - const { setSelectedPhotoIds } = useAppState(); - - useEffect( - () => () => setSelectedPhotoIds?.(undefined), - [setSelectedPhotoIds], - ); - - const containerHeight = useElementHeight(ref); - - return ( - - - - } - canSelect - /> - ); + return ; } diff --git a/src/photo/PhotoGridPageClient.tsx b/src/photo/PhotoGridPageClient.tsx new file mode 100644 index 00000000..66580b2c --- /dev/null +++ b/src/photo/PhotoGridPageClient.tsx @@ -0,0 +1,56 @@ +'use client'; + +import { Photo } from '.'; +import { PATH_GRID_INFERRED } from '@/app/paths'; +import PhotoGridSidebar from './PhotoGridSidebar'; +import PhotoGridContainer from './PhotoGridContainer'; +import { ComponentProps, useEffect, useRef } from 'react'; +import { useAppState } from '@/state/AppState'; +import clsx from 'clsx/lite'; +import useElementHeight from '@/utility/useElementHeight'; +import MaskedScroll from '@/components/MaskedScroll'; + +export default function PhotoGridPageClient({ + photos, + photosCount, + ...categories +}: ComponentProps & { + photos: Photo[] + photosCount: number +}) { + const ref = useRef(null); + + const { setSelectedPhotoIds } = useAppState(); + + useEffect( + () => () => setSelectedPhotoIds?.(undefined), + [setSelectedPhotoIds], + ); + + const containerHeight = useElementHeight(ref); + + return ( + + + + } + canSelect + /> + ); +} diff --git a/src/photo/PhotoGridSidebar.tsx b/src/photo/PhotoGridSidebar.tsx index 72116450..4fe03c94 100644 --- a/src/photo/PhotoGridSidebar.tsx +++ b/src/photo/PhotoGridSidebar.tsx @@ -10,11 +10,7 @@ import FavsTag from '../tag/FavsTag'; import { useAppState } from '@/state/AppState'; import { useMemo, useRef } from 'react'; import HiddenTag from '@/tag/HiddenTag'; -import { CATEGORY_VISIBILITY, PAGE_ABOUT } from '@/app/config'; -import { - htmlHasBrParagraphBreaks, - safelyParseFormattedHtml, -} from '@/utility/html'; +import { CATEGORY_VISIBILITY } from '@/app/config'; import { clsx } from 'clsx/lite'; import PhotoRecipe from '@/recipe/PhotoRecipe'; import IconCamera from '@/components/icons/IconCamera'; @@ -38,11 +34,15 @@ export default function PhotoGridSidebar({ photosCount, photosDateRange, containerHeight, + aboutTextSafelyParsedHtml, + aboutTextHasBrParagraphBreaks, ...categories }: PhotoSetCategories & { photosCount: number photosDateRange?: PhotoDateRange containerHeight?: number + aboutTextSafelyParsedHtml?: string + aboutTextHasBrParagraphBreaks?: boolean }) { const { cameras, @@ -245,16 +245,16 @@ export default function PhotoGridSidebar({ return (
- {PAGE_ABOUT && ]} />} diff --git a/src/photo/actions.ts b/src/photo/actions.ts index cfeda1c2..6513ba29 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -46,7 +46,7 @@ import { } from './server'; import { TAG_FAVS, isTagFavs } from '@/tag'; import { convertPhotoToPhotoDbInsert, Photo } from '.'; -import { runAuthenticatedAdminServerAction } from '@/auth'; +import { runAuthenticatedAdminServerAction } from '@/auth/server'; import { AiImageQuery, getAiImageQuery } from './ai'; import { streamOpenAiImageQuery } from '@/platforms/openai'; import { diff --git a/src/photo/form/index.ts b/src/photo/form/index.ts index 3b4f1633..fc2349d7 100644 --- a/src/photo/form/index.ts +++ b/src/photo/form/index.ts @@ -1,22 +1,13 @@ -import type { ExifData } from 'ts-exif-parser'; -import { DEFAULT_ASPECT_RATIO, Photo, PhotoDbInsert, PhotoExif } from '..'; +import { DEFAULT_ASPECT_RATIO, Photo, PhotoDbInsert } from '..'; import { - convertTimestampToNaivePostgresString, - convertTimestampWithOffsetToPostgresString, generateLocalNaivePostgresString, generateLocalPostgresString, validationMessageNaivePostgresDateString, validationMessagePostgresDateString, } from '@/utility/date'; -import { - convertApertureValueToFNumber, - getAspectRatioFromExif, - getOffsetFromExif, -} from '@/utility/exif'; import { roundToNumber } from '@/utility/number'; import { convertStringToArray, parameterize } from '@/utility/string'; import { generateNanoid } from '@/utility/nanoid'; -import { GEO_PRIVACY_ENABLED } from '@/app/config'; import { TAG_FAVS, getValidationMessageForTags } from '@/tag'; import { MAKE_FUJIFILM } from '@/platforms/fujifilm'; import { FujifilmRecipe } from '@/platforms/fujifilm/recipe'; @@ -272,46 +263,6 @@ export const convertPhotoToFormData = (photo: Photo): PhotoFormData => { } as PhotoFormData); }; -// CREATE FORM DATA: FROM EXIF - -export const convertExifToFormData = ( - data: ExifData, - film?: FujifilmSimulation, - recipeData?: FujifilmRecipe, -): Omit< - Record, - 'takenAt' | 'takenAtNaive' -> => ({ - aspectRatio: getAspectRatioFromExif(data).toString(), - make: data.tags?.Make, - model: data.tags?.Model, - focalLength: data.tags?.FocalLength?.toString(), - focalLengthIn35MmFormat: data.tags?.FocalLengthIn35mmFormat?.toString(), - lensMake: data.tags?.LensMake, - lensModel: data.tags?.LensModel, - fNumber: ( - data.tags?.FNumber?.toString() || - convertApertureValueToFNumber(data.tags?.ApertureValue) - ), - iso: data.tags?.ISO?.toString() || data.tags?.ISOSpeed?.toString(), - exposureTime: data.tags?.ExposureTime?.toString(), - exposureCompensation: data.tags?.ExposureCompensation?.toString(), - latitude: - !GEO_PRIVACY_ENABLED ? data.tags?.GPSLatitude?.toString() : undefined, - longitude: - !GEO_PRIVACY_ENABLED ? data.tags?.GPSLongitude?.toString() : undefined, - film, - recipeData: JSON.stringify(recipeData), - ...data.tags?.DateTimeOriginal && { - takenAt: convertTimestampWithOffsetToPostgresString( - data.tags.DateTimeOriginal, - getOffsetFromExif(data), - ), - takenAtNaive: - convertTimestampToNaivePostgresString(data.tags.DateTimeOriginal), - }, -}); - // PREPARE FORM FOR DB INSERT export const convertFormDataToPhotoDbInsert = ( diff --git a/src/photo/form/server.ts b/src/photo/form/server.ts new file mode 100644 index 00000000..6d4158b7 --- /dev/null +++ b/src/photo/form/server.ts @@ -0,0 +1,50 @@ +import { + convertApertureValueToFNumber, + getAspectRatioFromExif, +} from '@/utility/exif'; +import { GEO_PRIVACY_ENABLED } from '@/app/config'; +import { convertTimestampWithOffsetToPostgresString } from '@/utility/date'; +import { convertTimestampToNaivePostgresString } from '@/utility/date'; +import { getOffsetFromExif } from '@/utility/exif'; +import { PhotoExif } from '..'; +import { FujifilmRecipe } from '@/platforms/fujifilm/recipe'; +import { FujifilmSimulation } from '@/platforms/fujifilm/simulation'; +import type { ExifData } from 'ts-exif-parser'; + +export const convertExifToFormData = ( + data: ExifData, + film?: FujifilmSimulation, + recipeData?: FujifilmRecipe, +): Omit< + Record, + 'takenAt' | 'takenAtNaive' +> => ({ + aspectRatio: getAspectRatioFromExif(data).toString(), + make: data.tags?.Make, + model: data.tags?.Model, + focalLength: data.tags?.FocalLength?.toString(), + focalLengthIn35MmFormat: data.tags?.FocalLengthIn35mmFormat?.toString(), + lensMake: data.tags?.LensMake, + lensModel: data.tags?.LensModel, + fNumber: ( + data.tags?.FNumber?.toString() || + convertApertureValueToFNumber(data.tags?.ApertureValue) + ), + iso: data.tags?.ISO?.toString() || data.tags?.ISOSpeed?.toString(), + exposureTime: data.tags?.ExposureTime?.toString(), + exposureCompensation: data.tags?.ExposureCompensation?.toString(), + latitude: + !GEO_PRIVACY_ENABLED ? data.tags?.GPSLatitude?.toString() : undefined, + longitude: + !GEO_PRIVACY_ENABLED ? data.tags?.GPSLongitude?.toString() : undefined, + film, + recipeData: JSON.stringify(recipeData), + ...data.tags?.DateTimeOriginal && { + takenAt: convertTimestampWithOffsetToPostgresString( + data.tags.DateTimeOriginal, + getOffsetFromExif(data), + ), + takenAtNaive: + convertTimestampToNaivePostgresString(data.tags.DateTimeOriginal), + }, +}); diff --git a/src/photo/server.ts b/src/photo/server.ts index b09a50fa..086fa867 100644 --- a/src/photo/server.ts +++ b/src/photo/server.ts @@ -2,10 +2,7 @@ import { getExtensionFromStorageUrl, getIdFromStorageUrl, } from '@/platforms/storage'; -import { - convertExifToFormData, - convertFormDataToPhotoDbInsert, -} from '@/photo/form'; +import { convertFormDataToPhotoDbInsert } from '@/photo/form'; import { FujifilmSimulation, getFujifilmSimulationFromMakerNote, @@ -17,7 +14,7 @@ import { GEO_PRIVACY_ENABLED, PRESERVE_ORIGINAL_UPLOADS, } from '@/app/config'; -import { isExifForFujifilm } from '@/platforms/fujifilm'; +import { isExifForFujifilm } from '@/platforms/fujifilm/server'; import { FujifilmRecipe, getFujifilmRecipeFromMakerNote, @@ -27,6 +24,8 @@ import { updateAllMatchingRecipeTitles, } from './db/query'; import { PhotoDbInsert } from '.'; +import { convertExifToFormData } from './form/server'; + const IMAGE_WIDTH_RESIZE = 200; const IMAGE_WIDTH_BLUR = 200; @@ -124,7 +123,7 @@ export const extractImageDataFromBlobPath = async ( url, }, ...generateBlurData && { blurData }, - ...convertExifToFormData(exifData, film, recipe), + ...convertExifToFormData (exifData, film, recipe), }, }, imageResizedBase64, diff --git a/src/platforms/fujifilm/index.ts b/src/platforms/fujifilm/index.ts index 858a7ba2..b4af7160 100644 --- a/src/platforms/fujifilm/index.ts +++ b/src/platforms/fujifilm/index.ts @@ -1,84 +1,4 @@ -// MakerNote tag IDs and values referenced from: -// - github.com/exiftool/exiftool/blob/master/lib/Image/ExifTool/FujiFilm.pm -// - exiftool.org/TagNames/FujiFilm.html - -import type { ExifData } from 'ts-exif-parser'; - export const MAKE_FUJIFILM = 'FUJIFILM'; -// Makernote Offsets -const BYTE_OFFSET_TAG_COUNT = 12; -const BYTE_OFFSET_FIRST_TAG = 14; - -// Tag Offsets -const BYTE_OFFSET_TAG_TYPE = 2; -const BYTE_OFFSET_TAG_SIZE = 4; -const BYTE_OFFSET_TAG_VALUE = 8; - -// Tag Sizes -const BYTES_PER_TAG = 12; -const BYTES_PER_TAG_VALUE = 4; - -export const isExifForFujifilm = (data: ExifData) => - data.tags?.Make?.toLocaleUpperCase() === MAKE_FUJIFILM; - export const isMakeFujifilm = (make?: string) => make?.toLocaleUpperCase() === MAKE_FUJIFILM; - -export const parseFujifilmMakerNote = ( - bytes: Buffer, - sendTagNumbers: (tagId: number, numbers: number[]) => void, -) => { - const tagCount = bytes.readUint16LE(BYTE_OFFSET_TAG_COUNT); - - for (let i = 0; i < tagCount; i++) { - const index = BYTE_OFFSET_FIRST_TAG + i * BYTES_PER_TAG; - - if (index + BYTES_PER_TAG < bytes.length) { - const tagId = bytes.readUInt16LE(index); - const tagType = bytes.readUInt16LE(index + BYTE_OFFSET_TAG_TYPE); - const tagValueSize = bytes.readUInt16LE(index + BYTE_OFFSET_TAG_SIZE); - - const sendNumbersForDataType = ( - parseNumberAtOffset: (offset: number) => number, - sizeInBytes: number, - ) => { - let values: number[] = []; - if (tagValueSize * sizeInBytes <= BYTES_PER_TAG_VALUE) { - // Retrieve values if they fit in tag block - values = Array.from({ length: tagValueSize }, (_, i) => - parseNumberAtOffset( - index + BYTE_OFFSET_TAG_VALUE + i * sizeInBytes, - ), - ); - } else { - // Retrieve outside values if they don't fit in tag block - const offset = bytes.readUint16LE(index + BYTE_OFFSET_TAG_VALUE); - for (let i = 0; i < tagValueSize; i++) { - values.push(parseNumberAtOffset(offset + i * sizeInBytes)); - } - } - sendTagNumbers(tagId, values); - }; - - switch (tagType) { - // Int8 (UInt8 read as Int8 according to spec) - case 1: - sendNumbersForDataType(offset => bytes.readInt8(offset), 1); - break; - // UInt16 - case 3: - sendNumbersForDataType(offset => bytes.readUInt16LE(offset), 2); - break; - // UInt32 - case 4: - sendNumbersForDataType(offset => bytes.readUInt32LE(offset), 4); - break; - // Int32 - case 9: - sendNumbersForDataType(offset => bytes.readInt32LE(offset), 4); - break; - } - } - } -}; diff --git a/src/platforms/fujifilm/recipe.ts b/src/platforms/fujifilm/recipe.ts index aed37a45..edcfeebc 100644 --- a/src/platforms/fujifilm/recipe.ts +++ b/src/platforms/fujifilm/recipe.ts @@ -1,4 +1,4 @@ -import { parseFujifilmMakerNote } from '.'; +import { parseFujifilmMakerNote } from './server'; const TAG_ID_DYNAMIC_RANGE = 0x1400; const TAG_ID_DYNAMIC_RANGE_SETTING = 0x1402; diff --git a/src/platforms/fujifilm/server.ts b/src/platforms/fujifilm/server.ts new file mode 100644 index 00000000..fdec1b36 --- /dev/null +++ b/src/platforms/fujifilm/server.ts @@ -0,0 +1,80 @@ +// MakerNote tag IDs and values referenced from: +// - github.com/exiftool/exiftool/blob/master/lib/Image/ExifTool/FujiFilm.pm +// - exiftool.org/TagNames/FujiFilm.html + +import type { ExifData } from 'ts-exif-parser'; +import { MAKE_FUJIFILM } from '.'; + +// Makernote Offsets +const BYTE_OFFSET_TAG_COUNT = 12; +const BYTE_OFFSET_FIRST_TAG = 14; + +// Tag Offsets +const BYTE_OFFSET_TAG_TYPE = 2; +const BYTE_OFFSET_TAG_SIZE = 4; +const BYTE_OFFSET_TAG_VALUE = 8; + +// Tag Sizes +const BYTES_PER_TAG = 12; +const BYTES_PER_TAG_VALUE = 4; + +export const isExifForFujifilm = (data: ExifData) => + data.tags?.Make?.toLocaleUpperCase() === MAKE_FUJIFILM; + +export const parseFujifilmMakerNote = ( + bytes: Buffer, + sendTagNumbers: (tagId: number, numbers: number[]) => void, +) => { + const tagCount = bytes.readUint16LE(BYTE_OFFSET_TAG_COUNT); + + for (let i = 0; i < tagCount; i++) { + const index = BYTE_OFFSET_FIRST_TAG + i * BYTES_PER_TAG; + + if (index + BYTES_PER_TAG < bytes.length) { + const tagId = bytes.readUInt16LE(index); + const tagType = bytes.readUInt16LE(index + BYTE_OFFSET_TAG_TYPE); + const tagValueSize = bytes.readUInt16LE(index + BYTE_OFFSET_TAG_SIZE); + + const sendNumbersForDataType = ( + parseNumberAtOffset: (offset: number) => number, + sizeInBytes: number, + ) => { + let values: number[] = []; + if (tagValueSize * sizeInBytes <= BYTES_PER_TAG_VALUE) { + // Retrieve values if they fit in tag block + values = Array.from({ length: tagValueSize }, (_, i) => + parseNumberAtOffset( + index + BYTE_OFFSET_TAG_VALUE + i * sizeInBytes, + ), + ); + } else { + // Retrieve outside values if they don't fit in tag block + const offset = bytes.readUint16LE(index + BYTE_OFFSET_TAG_VALUE); + for (let i = 0; i < tagValueSize; i++) { + values.push(parseNumberAtOffset(offset + i * sizeInBytes)); + } + } + sendTagNumbers(tagId, values); + }; + + switch (tagType) { + // Int8 (UInt8 read as Int8 according to spec) + case 1: + sendNumbersForDataType(offset => bytes.readInt8(offset), 1); + break; + // UInt16 + case 3: + sendNumbersForDataType(offset => bytes.readUInt16LE(offset), 2); + break; + // UInt32 + case 4: + sendNumbersForDataType(offset => bytes.readUInt32LE(offset), 4); + break; + // Int32 + case 9: + sendNumbersForDataType(offset => bytes.readInt32LE(offset), 4); + break; + } + } + } +}; diff --git a/src/platforms/fujifilm/simulation.ts b/src/platforms/fujifilm/simulation.ts index 1778bbee..a4ef3b96 100644 --- a/src/platforms/fujifilm/simulation.ts +++ b/src/platforms/fujifilm/simulation.ts @@ -1,4 +1,4 @@ -import { parseFujifilmMakerNote } from '.'; +import { parseFujifilmMakerNote } from './server'; const TAG_ID_SATURATION = 0x1003; const TAG_ID_FILM_MODE = 0x1401; diff --git a/src/state/AppStateProvider.tsx b/src/state/AppStateProvider.tsx index 7a18fd76..db8c9f6e 100644 --- a/src/state/AppStateProvider.tsx +++ b/src/state/AppStateProvider.tsx @@ -19,7 +19,7 @@ import { storeAuthEmailCookie, clearAuthEmailCookie, hasAuthEmailCookie, -} from '@/auth/client'; +} from '@/auth'; import { useRouter, usePathname } from 'next/navigation'; import { isPathAdmin, PATH_ROOT } from '@/app/paths'; import { INITIAL_UPLOAD_STATE, UploadState } from '@/admin/upload'; diff --git a/src/utility/html.ts b/src/utility/html.ts index 9f977a44..0890af77 100644 --- a/src/utility/html.ts +++ b/src/utility/html.ts @@ -9,4 +9,4 @@ export const safelyParseFormattedHtml = (text: string) => // Matches two or more
or
tags in a row export const htmlHasBrParagraphBreaks = (text: string) => - text.match(/(){2}/i); + /(){2}/i.test(text);