Merge branch 'main' into static
This commit is contained in:
commit
5534321e79
28
README.md
28
README.md
@ -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
|
||||
{
|
||||
|
||||
@ -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 &&
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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' });
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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>]
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user