Merge branch 'main' into static

This commit is contained in:
Sam Becker 2024-01-21 21:19:20 -06:00
commit 5534321e79
12 changed files with 100 additions and 61 deletions

View File

@ -68,6 +68,7 @@ Installation
- `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
- `NEXT_PUBLIC_HIDE_EXIF_DATA = 1` hides EXIF data in photo details and OG images (potentially useful for portfolios, which don't focus on photography)
- `NEXT_PUBLIC_GRID_ASPECT_RATIO = 1.5` sets aspect ratio for grid tiles (defaults to `1`—setting to `0` removes the constraint)
- `NEXT_PUBLIC_OG_TEXT_ALIGNMENT = BOTTOM` keeps OG image text bottom aligned (default is top)
@ -82,24 +83,27 @@ Only one storage adapter—Vercel Blob, Cloudflare R2, or AWS S3—can be used a
- Setup CORS under bucket settings:
```json
[{
"AllowedHeaders": ["*"]
"AllowedOrigins": [
"http://localhost:3000",
"https://{VERCEL_PROJECT_NAME}*.vercel.app",
"{PRODUCTION_DOMAIN}"
],
"AllowedHeaders": ["*"],
"AllowedMethods": [
"GET",
"PUT"
],
"AllowedOrigins": [
"http://localhost:3000",
"https://{VERCEL_PROJECT_NAME}*.vercel.app",
"{PRODUCTION_DOMAIN}"
]
}]
```
- Enable R2.dev subdomain (necessary in order to serve files publicly without a custom domain)
- Store configuration:
- Enable public hosting by doing one of the following:
- Select "Connect Custom Domain" and choose a Cloudflare domain
- OR
- Select "Allow Access" from R2.dev subdomain
- Store public configuration:
- `NEXT_PUBLIC_CLOUDFLARE_R2_BUCKET`: bucket name
- `NEXT_PUBLIC_CLOUDFLARE_R2_ACCOUNT_ID`: account id (found on R2 overview page)
- `NEXT_PUBLIC_CLOUDFLARE_R2_DEV_SUBDOMAIN`: r2.dev subdomain, e.g., "pub-jf90908..."
2. Setup credentials
- `NEXT_PUBLIC_CLOUDFLARE_R2_PUBLIC_DOMAIN`: e.g., either "pub-jf90908...r2.dev" or "custom-domain.com"
2. Setup private credentials
- Create API token by selecting "Manage R2 API Tokens," and clicking "Create API Token"
- Select "Object Read & Write," choose "Apply to specific buckets only," and select the bucket created in Step 1.
- Store credentials (⚠️ _Ensure access keys are not prefixed with `NEXT_PUBLIC`_):
@ -126,10 +130,10 @@ Only one storage adapter—Vercel Blob, Cloudflare R2, or AWS S3—can be used a
"ExposeHeaders": []
}]
```
- Store configuration
- Store public configuration
- `NEXT_PUBLIC_AWS_S3_BUCKET`: bucket name
- `NEXT_PUBLIC_AWS_S3_REGION`: bucket region, e.g., "us-east-1"
2. Setup credentials
2. Setup private credentials
- [Create IAM policy](https://console.aws.amazon.com/iam/home#/policies) using JSON editor:
```json
{

View File

@ -7,9 +7,7 @@ const VERCEL_BLOB_HOSTNAME = VERCEL_BLOB_STORE_ID
: undefined;
const CLOUDFLARE_R2_HOSTNAME =
process.env.NEXT_PUBLIC_CLOUDFLARE_R2_DEV_SUBDOMAIN
? `${process.env.NEXT_PUBLIC_CLOUDFLARE_R2_DEV_SUBDOMAIN}.r2.dev`
: undefined;
process.env.NEXT_PUBLIC_CLOUDFLARE_R2_PUBLIC_DOMAIN;
const AWS_S3_HOSTNAME =
process.env.NEXT_PUBLIC_AWS_S3_BUCKET &&

View File

@ -1,4 +1,9 @@
import { Photo, photoHasCameraData, photoHasExifData, titleForPhoto } from '.';
import {
Photo,
shouldShowCameraDataForPhoto,
shouldShowExifDataForPhoto,
titleForPhoto,
} from '.';
import SiteGrid from '@/components/SiteGrid';
import ImageLarge from '@/components/ImageLarge';
import { clsx } from 'clsx/lite';
@ -92,7 +97,7 @@ export default function PhotoLarge({
{tags.length > 0 &&
<PhotoTags tags={tags} />}
</div>
{showCamera && photoHasCameraData(photo) &&
{showCamera && shouldShowCameraDataForPhoto(photo) &&
<div className="space-y-0.5">
<PhotoCamera
camera={camera}
@ -107,7 +112,7 @@ export default function PhotoLarge({
</div>}
</>)}
{renderMiniGrid(<>
{photoHasExifData(photo) &&
{shouldShowExifDataForPhoto(photo) &&
<ul className="text-medium">
<li>
{photo.focalLengthFormatted}

View File

@ -1,4 +1,4 @@
import { Photo } from '..';
import { Photo, shouldShowExifDataForPhoto } from '..';
import { AiFillApple } from 'react-icons/ai';
import ImageCaption from './components/ImageCaption';
import ImagePhotoGrid from './components/ImagePhotoGrid';
@ -30,25 +30,26 @@ export default function PhotoImageResponse({
height,
...OG_TEXT_BOTTOM_ALIGNMENT && { imagePosition: 'top' },
}} />
<ImageCaption {...{ width, height, fontFamily }}>
{photo.make === 'Apple' &&
{shouldShowExifDataForPhoto(photo) &&
<ImageCaption {...{ width, height, fontFamily }}>
{photo.make === 'Apple' &&
<div style={{ display: 'flex' }}>
<AiFillApple />
</div>}
{model &&
<div style={{ display: 'flex' }}>
{model}
</div>}
<div style={{ display: 'flex' }}>
<AiFillApple />
</div>}
{model &&
{photo.focalLengthFormatted}
</div>
<div style={{ display: 'flex' }}>
{model}
</div>}
<div style={{ display: 'flex' }}>
{photo.focalLengthFormatted}
</div>
<div style={{ display: 'flex' }}>
{photo.fNumberFormatted}
</div>
<div>
{photo.isoFormatted}
</div>
</ImageCaption>
{photo.fNumberFormatted}
</div>
<div>
{photo.isoFormatted}
</div>
</ImageCaption>}
</ImageContainer>
);
};

View File

@ -1,4 +1,5 @@
import { FilmSimulation } from '@/simulation';
import { SHOW_EXIF_DATA } from '@/site/config';
import { ABSOLUTE_PATH_FOR_HOME_IMAGE } from '@/site/paths';
import { formatDateFromPostgresString } from '@/utility/date';
import {
@ -228,14 +229,20 @@ export const dateRangeForPhotos = (
return { start, end, description };
};
export const photoHasCameraData = (photo: Photo) =>
const photoHasCameraData = (photo: Photo) =>
photo.make &&
photo.model;
export const photoHasExifData = (photo: Photo) =>
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);

View File

@ -11,8 +11,10 @@ const AWS_S3_BUCKET = process.env.NEXT_PUBLIC_AWS_S3_BUCKET ?? '';
const AWS_S3_REGION = process.env.NEXT_PUBLIC_AWS_S3_REGION ?? '';
const AWS_S3_ACCESS_KEY = process.env.AWS_S3_ACCESS_KEY ?? '';
const AWS_S3_SECRET_ACCESS_KEY = process.env.AWS_S3_SECRET_ACCESS_KEY ?? '';
export const AWS_S3_BASE_URL =
`https://${AWS_S3_BUCKET}.s3.${AWS_S3_REGION}.amazonaws.com`;
export const AWS_S3_BASE_URL = AWS_S3_BUCKET && AWS_S3_REGION
? `https://${AWS_S3_BUCKET}.s3.${AWS_S3_REGION}.amazonaws.com`
: undefined;
export const awsS3Client = () => new S3Client({
region: AWS_S3_REGION,
@ -25,7 +27,7 @@ export const awsS3Client = () => new S3Client({
const urlForKey = (key?: string) => `${AWS_S3_BASE_URL}/${key}`;
export const isUrlFromAwsS3 = (url: string) =>
url.startsWith(AWS_S3_BASE_URL);
AWS_S3_BASE_URL && url.startsWith(AWS_S3_BASE_URL);
export const awsS3PutObjectCommandForKey = (Key: string) =>
new PutObjectCommand({ Bucket: AWS_S3_BUCKET, Key, ACL: 'public-read' });

View File

@ -11,20 +11,23 @@ const CLOUDFLARE_R2_BUCKET =
process.env.NEXT_PUBLIC_CLOUDFLARE_R2_BUCKET ?? '';
const CLOUDFLARE_R2_ACCOUNT_ID =
process.env.NEXT_PUBLIC_CLOUDFLARE_R2_ACCOUNT_ID ?? '';
const CLOUDFLARE_R2_DEV_SUBDOMAIN =
process.env.NEXT_PUBLIC_CLOUDFLARE_R2_DEV_SUBDOMAIN ?? '';
const CLOUDFLARE_R2_PUBLIC_DOMAIN =
process.env.NEXT_PUBLIC_CLOUDFLARE_R2_PUBLIC_DOMAIN ?? '';
const CLOUDFLARE_R2_ACCESS_KEY =
process.env.CLOUDFLARE_R2_ACCESS_KEY ?? '';
const CLOUDFLARE_R2_SECRET_ACCESS_KEY =
process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY ?? '';
const CLOUDFLARE_R2_ENDPOINT =
`https://${CLOUDFLARE_R2_ACCOUNT_ID}.r2.cloudflarestorage.com`;
const CLOUDFLARE_R2_ENDPOINT = CLOUDFLARE_R2_ACCOUNT_ID
? `https://${CLOUDFLARE_R2_ACCOUNT_ID}.r2.cloudflarestorage.com`
: undefined;
export const CLOUDFLARE_R2_BASE_URL_PUBLIC = CLOUDFLARE_R2_PUBLIC_DOMAIN
? `https://${CLOUDFLARE_R2_PUBLIC_DOMAIN}`
: undefined;
export const CLOUDFLARE_R2_BASE_URL_PRIVATE =
`${CLOUDFLARE_R2_ENDPOINT}/${CLOUDFLARE_R2_BUCKET}`;
export const CLOUDFLARE_R2_BASE_URL_PUBLIC =
`https://${CLOUDFLARE_R2_DEV_SUBDOMAIN}.r2.dev`;
CLOUDFLARE_R2_ENDPOINT && CLOUDFLARE_R2_BUCKET
? `${CLOUDFLARE_R2_ENDPOINT}/${CLOUDFLARE_R2_BUCKET}`
: undefined;
export const cloudflareR2Client = () => new S3Client({
region: 'auto',
@ -39,9 +42,13 @@ const urlForKey = (key?: string, isPublic = true) => isPublic
? `${CLOUDFLARE_R2_BASE_URL_PUBLIC}/${key}`
: `${CLOUDFLARE_R2_BASE_URL_PRIVATE}/${key}`;
export const isUrlFromCloudflareR2 = (url: string) =>
url.startsWith(CLOUDFLARE_R2_BASE_URL_PRIVATE) ||
url.startsWith(CLOUDFLARE_R2_BASE_URL_PUBLIC);
export const isUrlFromCloudflareR2 = (url: string) => (
CLOUDFLARE_R2_BASE_URL_PRIVATE &&
url.startsWith(CLOUDFLARE_R2_BASE_URL_PRIVATE)
) || (
CLOUDFLARE_R2_BASE_URL_PUBLIC &&
url.startsWith(CLOUDFLARE_R2_BASE_URL_PUBLIC)
);
export const cloudflareR2PutObjectCommandForKey = (Key: string) =>
new PutObjectCommand({ Bucket: CLOUDFLARE_R2_BUCKET, Key });

View File

@ -130,7 +130,7 @@ export const convertUploadToPhoto = async (
): Promise<string> => {
const fileName = photoId ? `${PREFIX_PHOTO}-${photoId}` : `${PREFIX_PHOTO}`;
const fileExtension = getExtensionFromStorageUrl(uploadUrl);
const photoUrl = `${fileName}.${fileExtension ?? 'jpg'}`;
const photoPath = `${fileName}.${fileExtension ?? 'jpg'}`;
const storageType = storageTypeFromUrl(uploadUrl);
@ -139,17 +139,17 @@ export const convertUploadToPhoto = async (
// Copy file
switch (storageType) {
case 'vercel-blob':
url = await vercelBlobCopy(uploadUrl, photoUrl, photoId === undefined);
url = await vercelBlobCopy(uploadUrl, photoPath, photoId === undefined);
break;
case 'cloudflare-r2':
url = await cloudflareR2Copy(
getFileNameFromStorageUrl(uploadUrl),
photoUrl,
photoPath,
photoId === undefined,
);
break;
case 'aws-s3':
url = await awsS3Copy(uploadUrl, photoUrl, photoId === undefined);
url = await awsS3Copy(uploadUrl, photoPath, photoId === undefined);
break;
}

View File

@ -6,10 +6,12 @@ const VERCEL_BLOB_STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match(
/^vercel_blob_rw_([a-z0-9]+)_[a-z0-9]+$/i,
)?.[1].toLowerCase();
export const VERCEL_BLOB_BASE_URL =
`https://${VERCEL_BLOB_STORE_ID}.public.blob.vercel-storage.com`;
export const VERCEL_BLOB_BASE_URL = VERCEL_BLOB_STORE_ID
? `https://${VERCEL_BLOB_STORE_ID}.public.blob.vercel-storage.com`
: undefined;
export const isUrlFromVercelBlob = (url: string) =>
VERCEL_BLOB_BASE_URL &&
url.startsWith(VERCEL_BLOB_BASE_URL);
export const vercelBlobUploadFromClient = async (

View File

@ -68,7 +68,7 @@ export default function NavClient({
showAdmin={showAdmin}
/>
</div>
<div className="hidden xs:block">
<div className="hidden xs:block text-right text-balance">
{renderLink(SITE_DOMAIN_OR_TITLE, PATH_ROOT)}
</div>
</div>]

View File

@ -35,6 +35,7 @@ export default function SiteChecklistClient({
hasDomain,
showRepoLink,
showFilmSimulations,
showExifInfo,
isProModeEnabled,
isGeoPrivacyEnabled,
isPriorityOrderEnabled,
@ -146,7 +147,8 @@ export default function SiteChecklistClient({
title={!hasStorage
? 'Setup storage (one of the following)'
: hasMultipleStorageProviders
? `Setup storage (current: ${labelForStorage(currentStorage)})`
// eslint-disable-next-line max-len
? `Setup storage (new uploads go to: ${labelForStorage(currentStorage)})`
: 'Setup storage'}
status={hasStorage}
isPending={isPendingPage}
@ -317,6 +319,15 @@ export default function SiteChecklistClient({
simulations showing up in <code>/grid</code> sidebar:
{renderEnvVars(['NEXT_PUBLIC_HIDE_FILM_SIMULATIONS'])}
</ChecklistRow>
<ChecklistRow
title="Show EXIF data"
status={showExifInfo}
isPending={isPendingPage}
optional
>
Set environment variable to {'"1"'} to hide EXIF data:
{renderEnvVars(['NEXT_PUBLIC_HIDE_EXIF_DATA'])}
</ChecklistRow>
<ChecklistRow
title={`Grid Aspect Ratio: ${gridAspectRatio}`}
status={gridAspectRatio !== 0}

View File

@ -40,7 +40,7 @@ export const HAS_VERCEL_BLOB_STORAGE =
export const HAS_CLOUDFLARE_R2_STORAGE_CLIENT =
(process.env.NEXT_PUBLIC_CLOUDFLARE_R2_BUCKET ?? '').length > 0 &&
(process.env.NEXT_PUBLIC_CLOUDFLARE_R2_ACCOUNT_ID ?? '').length > 0 &&
(process.env.NEXT_PUBLIC_CLOUDFLARE_R2_DEV_SUBDOMAIN ?? '').length > 0;
(process.env.NEXT_PUBLIC_CLOUDFLARE_R2_PUBLIC_DOMAIN ?? '').length > 0;
export const HAS_CLOUDFLARE_R2_STORAGE =
HAS_CLOUDFLARE_R2_STORAGE_CLIENT &&
(process.env.CLOUDFLARE_R2_ACCESS_KEY ?? '').length > 0 &&
@ -83,6 +83,7 @@ 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 =
process.env.NEXT_PUBLIC_HIDE_FILM_SIMULATIONS !== '1';
export const SHOW_EXIF_DATA = process.env.NEXT_PUBLIC_HIDE_EXIF_DATA !== '1';
export const GRID_ASPECT_RATIO = process.env.NEXT_PUBLIC_GRID_ASPECT_RATIO
? parseFloat(process.env.NEXT_PUBLIC_GRID_ASPECT_RATIO)
: 1;
@ -111,6 +112,7 @@ export const CONFIG_CHECKLIST_STATUS = {
hasDomain: (process.env.NEXT_PUBLIC_SITE_DOMAIN ?? '').length > 0,
showRepoLink: SHOW_REPO_LINK,
showFilmSimulations: SHOW_FILM_SIMULATIONS,
showExifInfo: SHOW_EXIF_DATA,
isProModeEnabled: PRO_MODE_ENABLED,
isGeoPrivacyEnabled: GEO_PRIVACY_ENABLED,
isPriorityOrderEnabled: PRIORITY_ORDER_ENABLED,