-
+ {title &&
+
}
{children}
-
-
- {shortenUrl(pathShare)}
-
-
+
+
+ {shortenUrl(pathShare)}
+
+ {renderIcon(
+
,
+ () => {
+ navigator.clipboard.writeText(pathShare);
+ toastSuccess('Link to photo copied');
+ },
+ true,
)}
- onClick={() => {
- navigator.clipboard.writeText(pathShare);
- toastSuccess('Link to photo copied');
- }}
- >
-
+ {SHOW_SOCIAL &&
+ renderIcon(
+
,
+ () => window.open(
+ generateXPostText(pathShare, socialText),
+ '_blank',
+ ),
+ )}
diff --git a/src/focal/FocalLengthShareModal.tsx b/src/focal/FocalLengthShareModal.tsx
index 62b02b41..6b421c0f 100644
--- a/src/focal/FocalLengthShareModal.tsx
+++ b/src/focal/FocalLengthShareModal.tsx
@@ -2,6 +2,7 @@ import { absolutePathForFocalLength, pathForFocalLength } from '@/site/paths';
import { Photo, PhotoDateRange } from '../photo';
import ShareModal from '@/components/ShareModal';
import FocalLengthOGTile from './FocalLengthOGTile';
+import { shareTextFocalLength } from '.';
export default function FocalLengthShareModal({
focal,
@@ -16,9 +17,9 @@ export default function FocalLengthShareModal({
}) {
return (
diff --git a/src/focal/index.ts b/src/focal/index.ts
index f3f40bd3..150e3317 100644
--- a/src/focal/index.ts
+++ b/src/focal/index.ts
@@ -27,6 +27,9 @@ export const titleForFocalLength = (
photoQuantityText(explicitCount ?? photos.length),
].join(' ');
+export const shareTextFocalLength = (focal: number) =>
+ `Photos shot at ${formatFocalLength(focal)}`;
+
export const descriptionForFocalLengthPhotos = (
photos: Photo[],
dateBased?: boolean,
diff --git a/src/image-response/CameraImageResponse.tsx b/src/image-response/CameraImageResponse.tsx
index 58eb316b..e698bd0a 100644
--- a/src/image-response/CameraImageResponse.tsx
+++ b/src/image-response/CameraImageResponse.tsx
@@ -33,14 +33,19 @@ export default function CameraImageResponse({
height,
}}
/>
-
-
-
- {formatCameraText(camera)}
-
+ ,
+ }}>
+ {formatCameraText(camera).toLocaleUpperCase()}
);
diff --git a/src/image-response/FilmSimulationImageResponse.tsx b/src/image-response/FilmSimulationImageResponse.tsx
index 5c5c630c..66112674 100644
--- a/src/image-response/FilmSimulationImageResponse.tsx
+++ b/src/image-response/FilmSimulationImageResponse.tsx
@@ -36,15 +36,17 @@ export default function FilmSimulationImageResponse({
height,
}}
/>
-
-
-
- {labelForFilmSimulation(simulation).medium}
-
+ height={height * .081}
+ style={{ transform: `translateY(${height * .001}px)`}}
+ />,
+ }}>
+ {labelForFilmSimulation(simulation).medium.toLocaleUpperCase()}
);
diff --git a/src/image-response/FocalLengthImageResponse.tsx b/src/image-response/FocalLengthImageResponse.tsx
index 498f9cfd..d2435272 100644
--- a/src/image-response/FocalLengthImageResponse.tsx
+++ b/src/image-response/FocalLengthImageResponse.tsx
@@ -32,14 +32,19 @@ export default function FocalLengthImageResponse({
height,
}}
/>
-
-
- {formatFocalLength(focal)}
+ />,
+ }}>
+ {formatFocalLength(focal)}
);
diff --git a/src/image-response/PhotoImageResponse.tsx b/src/image-response/PhotoImageResponse.tsx
index 2175add2..943188a9 100644
--- a/src/image-response/PhotoImageResponse.tsx
+++ b/src/image-response/PhotoImageResponse.tsx
@@ -20,9 +20,16 @@ export default function PhotoImageResponse({
fontFamily: string
isNextImageReady: boolean
}) {
- const model = photo.model
- ? formatCameraModelTextShort(cameraFromPhoto(photo))
- : undefined;
+ const caption = [
+ photo.model
+ ? formatCameraModelTextShort(cameraFromPhoto(photo))
+ : undefined,
+ photo.focalLengthFormatted,
+ photo.fNumberFormatted,
+ photo.isoFormatted,
+ ]
+ .join(' ')
+ .trim();
return (
@@ -33,24 +40,15 @@ export default function PhotoImageResponse({
...OG_TEXT_BOTTOM_ALIGNMENT && { imagePosition: 'top' },
}} />
{shouldShowExifDataForPhoto(photo) &&
-
- {photo.make === 'Apple' &&
- }
- {model &&
-
- {model}
-
}
-
- {photo.focalLengthFormatted}
-
-
- {photo.fNumberFormatted}
-
-
- {photo.isoFormatted}
-
+ },
+ }}>
+ {caption}
}
);
diff --git a/src/image-response/TagImageResponse.tsx b/src/image-response/TagImageResponse.tsx
index d7a7f0e5..d5f11817 100644
--- a/src/image-response/TagImageResponse.tsx
+++ b/src/image-response/TagImageResponse.tsx
@@ -32,21 +32,29 @@ export default function TagImageResponse({
height,
}}
/>
-
- {isTagFavs(tag)
+
: }
- {tag.toUpperCase()}
+ size={height * .06}
+ style={{
+ transform: `translateY(${height * .016}px)`,
+ marginRight: height * .015,
+ }}
+ />,
+ }}>
+ {tag.toLocaleUpperCase()}
);
diff --git a/src/image-response/components/ImageCaption.tsx b/src/image-response/components/ImageCaption.tsx
index 75c1f9f6..498136c3 100644
--- a/src/image-response/components/ImageCaption.tsx
+++ b/src/image-response/components/ImageCaption.tsx
@@ -6,59 +6,50 @@ const GRADIENT_STOPS = 'rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0.7)';
export default function ImageCaption({
height,
fontFamily,
- subhead,
+ icon,
children,
}: {
width: number
height: number
fontFamily: string
- subhead?: ReactNode
+ icon?: ReactNode
children: ReactNode
}) {
+ const paddingEdge = height * .07;
+ const paddingContent = height * .6;
return (
- {subhead &&
-
- {subhead}
-
}
+ {icon}
diff --git a/src/photo/StaggeredOgPhotos.tsx b/src/photo/StaggeredOgPhotos.tsx
index cf00b18d..fa9fd95d 100644
--- a/src/photo/StaggeredOgPhotos.tsx
+++ b/src/photo/StaggeredOgPhotos.tsx
@@ -65,7 +65,7 @@ export default function StaggeredOgPhotos({
onFail={() => recomputeLoadingState({ [photo.id]: 'failed' })}
onVisible={index === photos.length - 1
? onLastPhotoVisible
- :undefined}
+ : undefined}
riseOnHover
/>)}
diff --git a/src/photo/form/index.ts b/src/photo/form/index.ts
index e6df9f75..32db49bd 100644
--- a/src/photo/form/index.ts
+++ b/src/photo/form/index.ts
@@ -7,7 +7,7 @@ import {
generateLocalPostgresString,
} from '@/utility/date';
import { getAspectRatioFromExif, getOffsetFromExif } from '@/utility/exif';
-import { toFixedNumber } from '@/utility/number';
+import { roundToNumber } from '@/utility/number';
import { convertStringToArray } from '@/utility/string';
import { generateNanoid } from '@/utility/nanoid';
import {
@@ -251,7 +251,7 @@ export const convertFormDataToPhotoDbInsert = (
// Convert form strings to arrays
tags: tags.length > 0 ? tags : undefined,
// Convert form strings to numbers
- aspectRatio: toFixedNumber(parseFloat(photoForm.aspectRatio), 6),
+ aspectRatio: roundToNumber(parseFloat(photoForm.aspectRatio), 6),
focalLength: photoForm.focalLength
? parseInt(photoForm.focalLength)
: undefined,
diff --git a/src/simulation/FilmSimulationShareModal.tsx b/src/simulation/FilmSimulationShareModal.tsx
index 3369b82e..c0298c4b 100644
--- a/src/simulation/FilmSimulationShareModal.tsx
+++ b/src/simulation/FilmSimulationShareModal.tsx
@@ -5,7 +5,7 @@ import {
import { Photo, PhotoDateRange } from '../photo';
import ShareModal from '@/components/ShareModal';
import FilmSimulationOGTile from './FilmSimulationOGTile';
-import { FilmSimulation } from '.';
+import { FilmSimulation, shareTextForFilmSimulation } from '.';
export default function FilmSimulationShareModal({
simulation,
@@ -20,9 +20,9 @@ export default function FilmSimulationShareModal({
}) {
return (
diff --git a/src/simulation/index.ts b/src/simulation/index.ts
index bb571b73..51b63dbd 100644
--- a/src/simulation/index.ts
+++ b/src/simulation/index.ts
@@ -40,6 +40,11 @@ export const titleForFilmSimulation = (
photoQuantityText(explicitCount ?? photos.length),
].join(' ');
+export const shareTextForFilmSimulation = (
+ simulation: FilmSimulation,
+) =>
+ `Photos shot on Fujifilm ${labelForFilmSimulation(simulation).large}`;
+
export const descriptionForFilmSimulationPhotos = (
photos: Photo[],
dateBased?: boolean,
diff --git a/src/site/SiteChecklistClient.tsx b/src/site/SiteChecklistClient.tsx
index ebf884a2..5ac61273 100644
--- a/src/site/SiteChecklistClient.tsx
+++ b/src/site/SiteChecklistClient.tsx
@@ -38,6 +38,7 @@ export default function SiteChecklistClient({
hasTitle,
hasDomain,
showRepoLink,
+ showSocial,
showFilmSimulations,
showExifInfo,
isProModeEnabled,
@@ -438,6 +439,17 @@ export default function SiteChecklistClient({
Set environment variable to {'"1"'} to hide footer link:
{renderEnvVars(['NEXT_PUBLIC_HIDE_REPO_LINK'])}
+
+ Set environment variable to {'"1"'} to hide
+ {' '}
+ X button from share modal:
+ {renderEnvVars(['NEXT_PUBLIC_HIDE_SOCIAL'])}
+
0,
hasDomain: (process.env.NEXT_PUBLIC_SITE_DOMAIN ?? '').length > 0,
showRepoLink: SHOW_REPO_LINK,
+ showSocial: SHOW_SOCIAL,
showFilmSimulations: SHOW_FILM_SIMULATIONS,
showExifInfo: SHOW_EXIF_DATA,
isProModeEnabled: PRO_MODE_ENABLED,
diff --git a/src/site/paths.ts b/src/site/paths.ts
index 94a30d3a..c67b0ff3 100644
--- a/src/site/paths.ts
+++ b/src/site/paths.ts
@@ -6,26 +6,27 @@ import { parameterize } from '@/utility/string';
import { TAG_HIDDEN } from '@/tag';
// Core paths
-export const PATH_ROOT = '/';
-export const PATH_GRID = '/grid';
-export const PATH_ADMIN = '/admin';
-export const PATH_API = '/api';
-export const PATH_SIGN_IN = '/sign-in';
-export const PATH_OG = '/og';
+export const PATH_ROOT = '/';
+export const PATH_GRID = '/grid';
+export const PATH_ADMIN = '/admin';
+export const PATH_API = '/api';
+export const PATH_SIGN_IN = '/sign-in';
+export const PATH_OG = '/og';
// Path prefixes
-export const PREFIX_PHOTO = '/p';
-export const PREFIX_TAG = '/tag';
-export const PREFIX_CAMERA = '/shot-on';
-export const PREFIX_FILM_SIMULATION = '/film';
-export const PREFIX_FOCAL_LENGTH = '/focal';
+export const PREFIX_PHOTO = '/p';
+export const PREFIX_TAG = '/tag';
+export const PREFIX_CAMERA = '/shot-on';
+export const PREFIX_FILM_SIMULATION = '/film';
+export const PREFIX_FOCAL_LENGTH = '/focal';
// Dynamic paths
-const PATH_PHOTO_DYNAMIC = `${PREFIX_PHOTO}/[photoId]`;
-const PATH_TAG_DYNAMIC = `${PREFIX_TAG}/[tag]`;
-const PATH_CAMERA_DYNAMIC = `${PREFIX_CAMERA}/[make]/[model]`;
-const PATH_FILM_SIMULATION_DYNAMIC = `${PREFIX_FILM_SIMULATION}/[simulation]`;
-const PATH_FOCAL_LENGTH_DYNAMIC = `${PREFIX_FOCAL_LENGTH}/[focal]`;
+const PATH_PHOTO_DYNAMIC = `${PREFIX_PHOTO}/[photoId]`;
+const PATH_TAG_DYNAMIC = `${PREFIX_TAG}/[tag]`;
+const PATH_CAMERA_DYNAMIC = `${PREFIX_CAMERA}/[make]/[model]`;
+// eslint-disable-next-line max-len
+const PATH_FILM_SIMULATION_DYNAMIC = `${PREFIX_FILM_SIMULATION}/[simulation]`;
+const PATH_FOCAL_LENGTH_DYNAMIC = `${PREFIX_FOCAL_LENGTH}/[focal]`;
// Admin paths
export const PATH_ADMIN_PHOTOS = `${PATH_ADMIN}/photos`;
@@ -34,6 +35,10 @@ export const PATH_ADMIN_TAGS = `${PATH_ADMIN}/tags`;
export const PATH_ADMIN_CONFIGURATION = `${PATH_ADMIN}/configuration`;
export const PATH_ADMIN_BASELINE = `${PATH_ADMIN}/baseline`;
+// Debug paths
+export const PATH_OG_ALL = `${PATH_OG}/all`;
+export const PATH_OG_SAMPLE = `${PATH_OG}/sample`;
+
// API paths
export const PATH_API_STORAGE = `${PATH_API}/storage`;
export const PATH_API_VERCEL_BLOB_UPLOAD = `${PATH_API_STORAGE}/vercel-blob`;
@@ -261,7 +266,7 @@ export const isPathAdminConfiguration = (pathname?: string) =>
export const isPathProtected = (pathname?: string) =>
checkPathPrefix(pathname, PATH_ADMIN) ||
checkPathPrefix(pathname, pathForTag(TAG_HIDDEN)) ||
- pathname === PATH_OG;
+ checkPathPrefix(pathname, PATH_OG);
export const getPathComponents = (pathname = ''): {
photoId?: string
diff --git a/src/tag/TagShareModal.tsx b/src/tag/TagShareModal.tsx
index 5f371090..359ac41e 100644
--- a/src/tag/TagShareModal.tsx
+++ b/src/tag/TagShareModal.tsx
@@ -2,6 +2,7 @@ import { absolutePathForTag, pathForTag } from '@/site/paths';
import { Photo, PhotoDateRange } from '../photo';
import ShareModal from '@/components/ShareModal';
import TagOGTile from './TagOGTile';
+import { shareTextForTag } from '.';
export default function TagShareModal({
tag,
@@ -16,9 +17,9 @@ export default function TagShareModal({
}) {
return (
diff --git a/src/tag/index.ts b/src/tag/index.ts
index fc85ee32..8498a7e2 100644
--- a/src/tag/index.ts
+++ b/src/tag/index.ts
@@ -11,9 +11,9 @@ import {
} from '@/site/paths';
import { capitalizeWords, convertStringToArray } from '@/utility/string';
-// Reserved/virtual tags
-export const TAG_FAVS = 'favs'; // Reserved
-export const TAG_HIDDEN = 'hidden'; // Virtual
+// Reserved tags
+export const TAG_FAVS = 'favs';
+export const TAG_HIDDEN = 'hidden';
export type TagsWithMeta = {
tag: string
@@ -24,10 +24,7 @@ export const formatTag = (tag?: string) =>
capitalizeWords(tag?.replaceAll('-', ' '));
export const doesStringContainReservedTags = (tags?: string) =>
- convertStringToArray(tags)?.some(tag => (
- isTagFavs(tag) ||
- tag.toLowerCase() === TAG_HIDDEN
- ));
+ convertStringToArray(tags)?.some(tag => isTagFavs(tag) || isTagHidden(tag));
export const titleForTag = (
tag: string,
@@ -38,6 +35,9 @@ export const titleForTag = (
photoQuantityText(explicitCount ?? photos.length),
].join(' ');
+export const shareTextForTag = (tag: string) =>
+ isTagFavs(tag) ? 'Favorite photos' : `Photos tagged '${tag}'`;
+
export const sortTags = (
tags: string[],
tagToHide?: string,
@@ -92,6 +92,8 @@ export const isPhotoFav = ({ tags }: Photo) => tags.some(isTagFavs);
export const isPathFavs = (pathname?: string) =>
getPathComponents(pathname).tag === TAG_FAVS;
+export const isTagHidden = (tag: string) => tag.toLowerCase() === TAG_HIDDEN;
+
export const addHiddenToTags = (tags: TagsWithMeta, hiddenPhotosCount = 0) => {
if (hiddenPhotosCount > 0) {
return tags
diff --git a/src/utility/exif.ts b/src/utility/exif.ts
index 60b10288..8760cfe5 100644
--- a/src/utility/exif.ts
+++ b/src/utility/exif.ts
@@ -1,5 +1,5 @@
import { OrientationTypes, type ExifData } from 'ts-exif-parser';
-import { formatNumberToFraction } from './number';
+import { formatNumberToFraction, roundToString } from './number';
const OFFSET_REGEX = /[+-]\d\d:\d\d/;
@@ -32,10 +32,12 @@ export const getAspectRatioFromExif = (data: ExifData): number => {
};
export const formatAperture = (aperture?: number) =>
- aperture ? `ƒ/${aperture}` : undefined;
+ aperture
+ ? `ƒ/${roundToString(aperture)}`
+ : undefined;
export const formatIso = (iso?: number) =>
- iso ? `ISO ${iso}` : undefined;
+ iso ? `ISO ${iso.toLocaleString()}` : undefined;
export const formatExposureTime = (exposureTime = 0) =>
exposureTime > 0
diff --git a/src/utility/number.ts b/src/utility/number.ts
index 2d36bf11..30a3388d 100644
--- a/src/utility/number.ts
+++ b/src/utility/number.ts
@@ -1,11 +1,18 @@
-export const toFixedNumber = (
+export const roundToString = (
number: number,
- digits: number,
- base = 10) => {
- const pow = Math.pow(base ?? 10, digits);
- return Math.round(number * pow) / pow;
+ place = 1,
+ includeZero?: boolean,
+) => {
+ const precision = Math.pow(10, place);
+ const result = Math.round(number * precision) / precision;
+ return includeZero ? result.toFixed(place) : result.toString();
};
+export const roundToNumber = (
+ ...args: Parameters
+) =>
+ parseFloat(roundToString(...args));
+
const gcd = (a: number, b: number): number => {
if (b <= 0.0000001) {
return a;
diff --git a/src/utility/social.ts b/src/utility/social.ts
new file mode 100644
index 00000000..49baefff
--- /dev/null
+++ b/src/utility/social.ts
@@ -0,0 +1,6 @@
+export const generateXPostText = (path: string, text: string) => {
+ const url = new URL('https://x.com/intent/post');
+ url.searchParams.set('text', text);
+ url.searchParams.set('url', path);
+ return url.toString();
+};