* Remove unused desktop redirect component * Tweak useEffect/setState interactions * Address more next.js 16 linting * Tweak secret loading * Finish linting setstate/useeffect interactions * Disable ref lint warnings
1023 lines
34 KiB
TypeScript
1023 lines
34 KiB
TypeScript
'use client';
|
|
|
|
import {
|
|
ComponentProps,
|
|
Fragment,
|
|
JSX,
|
|
ReactNode,
|
|
useEffect,
|
|
useState,
|
|
} from 'react';
|
|
import ChecklistRow from '@/components/ChecklistRow';
|
|
import ChecklistGroup from '@/components/ChecklistGroup';
|
|
import { AppConfiguration } from '@/app/config';
|
|
import StatusIcon from '@/components/StatusIcon';
|
|
import { labelForStorage } from '@/platforms/storage';
|
|
import { testConnectionsAction } from '@/admin/actions';
|
|
import ErrorNote from '@/components/ErrorNote';
|
|
import SecretGenerator from '@/app/SecretGenerator';
|
|
import EnvVar from '@/components/EnvVar';
|
|
import AdminLink from '@/admin/AdminLink';
|
|
import ScoreCardContainer from '@/components/ScoreCardContainer';
|
|
import { CATEGORY_KEYS, DEFAULT_CATEGORY_KEYS } from '@/category';
|
|
import {
|
|
AI_AUTO_GENERATED_FIELDS_ALL,
|
|
AI_AUTO_GENERATED_FIELDS_DEFAULT,
|
|
} from '@/photo/ai';
|
|
import clsx from 'clsx/lite';
|
|
import Link from 'next/link';
|
|
import { PATH_FEED_JSON, PATH_RSS_XML } from '@/app/path';
|
|
import { APP_DEFAULT_SORT_BY, DEFAULT_SORT_BY_OPTIONS } from '@/photo/sort';
|
|
import {
|
|
AdminConfigSection,
|
|
ConfigSectionKey,
|
|
getAdminConfigSections,
|
|
} from '.';
|
|
import ColorDot from '@/photo/color/ColorDot';
|
|
import { Oklch } from '@/photo/color/client';
|
|
import { getOrderedKeyListStatus } from '@/utility/key';
|
|
import { DEFAULT_SOCIAL_KEYS, SOCIAL_KEYS } from '@/social';
|
|
import MaskedScroll from '@/components/MaskedScroll';
|
|
import { IoLink } from 'react-icons/io5';
|
|
|
|
export default function AdminAppConfigurationClient({
|
|
// Storage
|
|
hasDatabase,
|
|
isPostgresSslEnabled,
|
|
hasRedisStorage,
|
|
hasStorageProvider,
|
|
hasVercelBlobStorage,
|
|
hasCloudflareR2Storage,
|
|
hasAwsS3Storage,
|
|
hasMinioStorage,
|
|
hasMultipleStorageProviders,
|
|
currentStorage,
|
|
// Auth
|
|
hasAuthSecret,
|
|
hasAdminUser,
|
|
// Content
|
|
locale,
|
|
hasLocale,
|
|
domain,
|
|
hasDomain,
|
|
metaTitle,
|
|
isMetaTitleConfigured,
|
|
metaDescription,
|
|
isMetaDescriptionConfigured,
|
|
navTitle,
|
|
hasNavTitle,
|
|
navCaption,
|
|
hasNavCaption,
|
|
pageAbout,
|
|
hasPageAbout,
|
|
// Performance
|
|
isStaticallyOptimized,
|
|
arePhotosStaticallyOptimized,
|
|
arePhotoOGImagesStaticallyOptimized,
|
|
arePhotoCategoriesStaticallyOptimized,
|
|
arePhotoCategoryOgImagesStaticallyOptimized,
|
|
areOriginalUploadsPreserved,
|
|
hasImageQuality,
|
|
imageQuality,
|
|
isBlurEnabled,
|
|
// AI
|
|
hasOpenaiBaseUrl,
|
|
isAiTextGenerationEnabled,
|
|
aiTextAutoGeneratedFields,
|
|
hasAiTextAutoGeneratedFields,
|
|
// Location services
|
|
hasLocationServices,
|
|
// Categories
|
|
hasCategoryVisibility,
|
|
categoryVisibility,
|
|
showCategoriesOnMobile,
|
|
showCategoryImageHover,
|
|
collapseSidebarCategories,
|
|
hideTagsWithOnePhoto,
|
|
// Sort
|
|
hasDefaultSortBy,
|
|
defaultSortBy,
|
|
hasNavSortControl,
|
|
navSortControl,
|
|
isColorSortEnabled,
|
|
hasColorSortConfiguration,
|
|
colorSortStartingHue,
|
|
colorSortChromaCutoff,
|
|
isSortWithPriority,
|
|
// Display
|
|
showKeyboardShortcutTooltips,
|
|
showExifInfo,
|
|
showZoomControls,
|
|
showTakenAtTimeHidden,
|
|
showRepoLink,
|
|
// Grid
|
|
isGridHomepageEnabled,
|
|
gridAspectRatio,
|
|
hasGridAspectRatio,
|
|
hasHighGridDensity,
|
|
hasGridDensityPreference,
|
|
// Design
|
|
hasDefaultTheme,
|
|
defaultTheme,
|
|
arePhotosMatted,
|
|
arePhotoMatteColorsConfigured,
|
|
matteColor,
|
|
matteColorDark,
|
|
// Settings
|
|
isGeoPrivacyEnabled,
|
|
arePublicDownloadsEnabled,
|
|
hasSocialKeys,
|
|
socialKeys,
|
|
areSiteFeedsEnabled,
|
|
isOgTextBottomAligned,
|
|
// Scripts & Analytics
|
|
hasPageScriptUrls,
|
|
pageScriptUrls,
|
|
// Internal
|
|
areInternalToolsEnabled,
|
|
areAdminDebugToolsEnabled,
|
|
isAdminSqlDebugEnabled,
|
|
// Auth
|
|
secret,
|
|
// Connection status
|
|
databaseError,
|
|
storageError,
|
|
redisError,
|
|
aiError,
|
|
locationError,
|
|
// Component props
|
|
simplifiedView,
|
|
isAnalyzingConfiguration,
|
|
}: AppConfiguration &
|
|
{ secret: string } &
|
|
Partial<Awaited<ReturnType<typeof testConnectionsAction>>> & {
|
|
simplifiedView?: boolean
|
|
isAnalyzingConfiguration?: boolean
|
|
}) {
|
|
const [hasScrolled, setHasScrolled] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const handleScroll = () => { setHasScrolled(true); };
|
|
window.addEventListener('scroll', handleScroll);
|
|
return () => {
|
|
window.removeEventListener('scroll', handleScroll);
|
|
};
|
|
}, []);
|
|
|
|
const renderContent = (content?: ReactNode) => content
|
|
? <div className={clsx(
|
|
'my-1 px-2 py-1',
|
|
'bg-dim rounded-lg',
|
|
)}>
|
|
{content}
|
|
</div>
|
|
: null;
|
|
|
|
const renderEnvVars = (variables: string[]) =>
|
|
<div className="pt-1 flex flex-col gap-0.5">
|
|
{variables.map(variable =>
|
|
<EnvVar key={variable} variable={variable} />)}
|
|
</div>;
|
|
|
|
const renderSubStatus = (
|
|
type: ComponentProps<typeof StatusIcon>['type'],
|
|
label: ReactNode,
|
|
iconClassName = 'translate-y-[3.5px]',
|
|
) =>
|
|
<div className="flex gap-2 translate-x-[-2.5px]">
|
|
<StatusIcon {...{ type, className: iconClassName }} />
|
|
<span className="min-w-0">
|
|
{label}
|
|
</span>
|
|
</div>;
|
|
|
|
const renderSubStatusWithEnvVar = (
|
|
type: ComponentProps<typeof StatusIcon>['type'],
|
|
variable: string,
|
|
) =>
|
|
renderSubStatus(
|
|
type,
|
|
renderEnvVars([variable]),
|
|
'translate-y-[7px]',
|
|
);
|
|
|
|
const renderOrderedKeyList = (
|
|
selectedKeys: string[],
|
|
acceptedKeys: readonly string[],
|
|
) =>
|
|
<div>
|
|
{getOrderedKeyListStatus({ selectedKeys, acceptedKeys })
|
|
.map(({ label, selected }) =>
|
|
<Fragment key={label}>
|
|
{renderSubStatus(
|
|
selected ? 'checked' : 'optional',
|
|
selected
|
|
? label
|
|
: <span className="text-dim">{label}</span>,
|
|
)}
|
|
</Fragment>)}
|
|
</div>;
|
|
|
|
const renderError = ({
|
|
connection,
|
|
message,
|
|
}: {
|
|
connection?: { provider: string, error: string }
|
|
message?: string
|
|
}) =>
|
|
<ErrorNote className="mt-2 mb-3">
|
|
{connection && <>
|
|
{connection.provider} connection error: {`"${connection.error}"`}
|
|
</>}
|
|
{message}
|
|
</ErrorNote>;
|
|
|
|
const renderLink = (href: string, children?: ReactNode) =>
|
|
<Link
|
|
href={href}
|
|
className="underline underline-offset-3 hover:no-underline"
|
|
target="_blank"
|
|
>
|
|
{children || href}
|
|
</Link>;
|
|
|
|
const renderColorDot = (color: Oklch | string, includeTooltip?: boolean) =>
|
|
<ColorDot
|
|
color={color}
|
|
className="size-3! ml-1"
|
|
includeTooltip={includeTooltip}
|
|
/>;
|
|
|
|
const renderCommaSeparatedList = (items: string[]) =>
|
|
<>
|
|
{'"'}
|
|
{items.map((item, index) => <Fragment key={index}>
|
|
{item}{index < items.length - 1 ? <>,​</> : <></>}
|
|
</Fragment>)}
|
|
{'"'}
|
|
</>;
|
|
|
|
const renderGroupContent = (key: ConfigSectionKey): JSX.Element => {
|
|
switch (key) {
|
|
case 'Storage':
|
|
return <>
|
|
<ChecklistRow
|
|
title={hasDatabase && isAnalyzingConfiguration
|
|
? 'Testing database connection'
|
|
: 'Setup database'}
|
|
status={hasDatabase}
|
|
isPending={hasDatabase && isAnalyzingConfiguration}
|
|
>
|
|
{databaseError && renderError({
|
|
connection: { provider: 'Database', error: databaseError},
|
|
})}
|
|
{hasDatabase
|
|
? renderSubStatus(
|
|
'checked',
|
|
// eslint-disable-next-line max-len
|
|
`Postgres: connected${!isPostgresSslEnabled ? ' (SSL disabled)' : ''}`,
|
|
)
|
|
: renderSubStatus('missing', <>
|
|
Postgres:
|
|
{' '}
|
|
<AdminLink
|
|
// eslint-disable-next-line max-len
|
|
href="https://vercel.com/docs/postgres#create-a-postgres-database"
|
|
externalIcon
|
|
>
|
|
create database
|
|
</AdminLink>
|
|
{' '}
|
|
and connect to project
|
|
</>)}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title={
|
|
hasStorageProvider && isAnalyzingConfiguration
|
|
? 'Testing storage connection'
|
|
: !hasStorageProvider
|
|
? 'Setup storage (one of the following)'
|
|
: hasMultipleStorageProviders
|
|
// eslint-disable-next-line max-len
|
|
? `Setup storage (new uploads go to: ${labelForStorage(currentStorage)})`
|
|
: 'Setup storage'}
|
|
status={hasStorageProvider}
|
|
isPending={hasStorageProvider && isAnalyzingConfiguration}
|
|
>
|
|
{storageError && renderError({
|
|
connection: { provider: 'Storage', error: storageError},
|
|
})}
|
|
<div>
|
|
{hasVercelBlobStorage
|
|
? renderSubStatus('checked', 'Vercel Blob: connected')
|
|
: renderSubStatus('optional', <>
|
|
{labelForStorage('vercel-blob')}:
|
|
{' '}
|
|
<AdminLink
|
|
// eslint-disable-next-line max-len
|
|
href="https://vercel.com/docs/storage/vercel-blob/quickstart#create-a-blob-store"
|
|
externalIcon
|
|
>
|
|
create store
|
|
</AdminLink>
|
|
{' '}
|
|
and connect to project
|
|
</>,
|
|
)}
|
|
{hasCloudflareR2Storage
|
|
? renderSubStatus('checked', 'Cloudflare R2: connected')
|
|
: renderSubStatus('optional', <>
|
|
{labelForStorage('cloudflare-r2')}:
|
|
{' '}
|
|
<AdminLink
|
|
// eslint-disable-next-line max-len
|
|
href="https://github.com/sambecker/exif-photo-blog#cloudflare-r2"
|
|
externalIcon
|
|
>
|
|
create/configure bucket
|
|
</AdminLink>
|
|
</>)}
|
|
{hasAwsS3Storage
|
|
? renderSubStatus('checked', 'AWS S3: connected')
|
|
: renderSubStatus('optional', <>
|
|
{labelForStorage('aws-s3')}:
|
|
{' '}
|
|
<AdminLink
|
|
href="https://github.com/sambecker/exif-photo-blog#aws-s3"
|
|
externalIcon
|
|
>
|
|
create/configure bucket
|
|
</AdminLink>
|
|
</>)}
|
|
{hasMinioStorage
|
|
? renderSubStatus('checked', 'MinIO: connected')
|
|
: renderSubStatus('optional', <>
|
|
{labelForStorage('minio')}:
|
|
{' '}
|
|
<AdminLink
|
|
href="https://github.com/sambecker/exif-photo-blog#minio"
|
|
externalIcon
|
|
>
|
|
setup MinIO server
|
|
</AdminLink>
|
|
</>)}
|
|
</div>
|
|
</ChecklistRow>
|
|
</>;
|
|
case 'Authentication':
|
|
return <>
|
|
<ChecklistRow
|
|
title={!hasAuthSecret && isAnalyzingConfiguration
|
|
? 'Generating secret'
|
|
: 'Setup auth'}
|
|
status={hasAuthSecret}
|
|
isPending={!hasAuthSecret && isAnalyzingConfiguration}
|
|
>
|
|
Store auth secret in environment variable:
|
|
{!hasAuthSecret &&
|
|
<div className="overflow-x-auto">
|
|
<SecretGenerator {...{ secret }} />
|
|
</div>}
|
|
{renderEnvVars(['AUTH_SECRET'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Setup admin user"
|
|
status={hasAdminUser}
|
|
>
|
|
Store admin email/password
|
|
{' '}
|
|
in environment variables:
|
|
{renderEnvVars([
|
|
'ADMIN_EMAIL',
|
|
'ADMIN_PASSWORD',
|
|
])}
|
|
</ChecklistRow>
|
|
</>;
|
|
case 'Content':
|
|
return <>
|
|
<ChecklistRow
|
|
title="Configure language"
|
|
status={hasLocale}
|
|
optional
|
|
>
|
|
{renderContent(locale)}
|
|
Store in environment variable
|
|
(check README for
|
|
{' '}
|
|
<AdminLink
|
|
// eslint-disable-next-line max-len
|
|
href="https://github.com/sambecker/exif-photo-blog?tab=readme-ov-file#supported-languages"
|
|
>
|
|
supported languages
|
|
</AdminLink>
|
|
):
|
|
{renderEnvVars(['NEXT_PUBLIC_LOCALE'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Configure domain"
|
|
status={hasDomain}
|
|
>
|
|
{renderContent(domain)}
|
|
Store in environment variable
|
|
(used in explicit share urls, seen in nav if no title is defined):
|
|
{renderEnvVars(['NEXT_PUBLIC_DOMAIN'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Meta title"
|
|
status={isMetaTitleConfigured}
|
|
showWarning
|
|
>
|
|
{renderContent(metaTitle)}
|
|
Store in environment variable
|
|
(seen in search results and browser tab):
|
|
{renderEnvVars(['NEXT_PUBLIC_META_TITLE'])}
|
|
</ChecklistRow>
|
|
{!simplifiedView && <>
|
|
<ChecklistRow
|
|
title="Meta description"
|
|
status={isMetaDescriptionConfigured}
|
|
optional
|
|
>
|
|
{renderContent(metaDescription)}
|
|
Store in environment variable
|
|
(seen in search results):
|
|
{renderEnvVars(['NEXT_PUBLIC_META_DESCRIPTION'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Nav title"
|
|
status={hasNavTitle}
|
|
optional
|
|
>
|
|
{renderContent(navTitle)}
|
|
Store in environment variable (replaces domain in top-right nav):
|
|
{renderEnvVars(['NEXT_PUBLIC_NAV_TITLE'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Nav caption"
|
|
status={hasNavCaption}
|
|
optional
|
|
>
|
|
{hasNavCaption && renderContent(navCaption)}
|
|
Store in environment variable
|
|
(seen in top-right nav, under title):
|
|
{renderEnvVars(['NEXT_PUBLIC_NAV_CAPTION'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Page about"
|
|
status={hasPageAbout}
|
|
optional
|
|
>
|
|
{hasPageAbout && renderContent(pageAbout)}
|
|
Store in environment variable (seen in sidebar):
|
|
{renderEnvVars(['NEXT_PUBLIC_PAGE_ABOUT'])}
|
|
</ChecklistRow>
|
|
</>}
|
|
</>;
|
|
case 'External Services':
|
|
return <>
|
|
<ChecklistRow
|
|
title={hasRedisStorage && isAnalyzingConfiguration
|
|
? 'Testing Redis connection'
|
|
: 'Rate limiting'}
|
|
status={hasRedisStorage}
|
|
isPending={hasRedisStorage && isAnalyzingConfiguration}
|
|
showWarning={isAiTextGenerationEnabled || hasLocationServices}
|
|
optional
|
|
>
|
|
{redisError && renderError({
|
|
connection: { provider: 'Redis', error: redisError},
|
|
})}
|
|
Create Upstash Redis store from storage tab
|
|
on Vercel dashboard and connect to this project
|
|
to enable rate limiting on external services
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title={isAiTextGenerationEnabled && isAnalyzingConfiguration
|
|
? 'Testing OpenAI connection'
|
|
: 'OpenAI'}
|
|
status={isAiTextGenerationEnabled}
|
|
isPending={isAiTextGenerationEnabled && isAnalyzingConfiguration}
|
|
optional
|
|
>
|
|
{aiError && renderError({
|
|
connection: { provider: 'OpenAI', error: aiError},
|
|
})}
|
|
Store OpenAI secret key in order to enable AI-generated
|
|
text descriptions, including an invisible field called
|
|
{' '}
|
|
{'"Semantic Description"'}, which supports CMD-K search
|
|
and image accessibility:
|
|
{renderEnvVars(['OPENAI_SECRET_KEY'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title={hasLocationServices && isAnalyzingConfiguration
|
|
? 'Testing Google Places connection'
|
|
: 'Google Places'}
|
|
status={hasLocationServices}
|
|
isPending={hasLocationServices && isAnalyzingConfiguration}
|
|
optional
|
|
>
|
|
{locationError && renderError({
|
|
connection: { provider: 'Google Places', error: locationError},
|
|
})}
|
|
Store Google Places API key in order to add location meta
|
|
to entities like albums:
|
|
{renderEnvVars(['GOOGLE_PLACES_API_KEY'])}
|
|
</ChecklistRow>
|
|
</>;
|
|
case 'AI Text':
|
|
return <>
|
|
<ChecklistRow
|
|
title={'Auto-generated text fields'}
|
|
status={hasAiTextAutoGeneratedFields}
|
|
optional
|
|
>
|
|
<div>
|
|
{hasAiTextAutoGeneratedFields &&
|
|
AI_AUTO_GENERATED_FIELDS_ALL.map(field =>
|
|
<Fragment key={field}>
|
|
{renderSubStatus(
|
|
aiTextAutoGeneratedFields.includes(field)
|
|
? 'checked'
|
|
: 'optional',
|
|
field,
|
|
)}
|
|
</Fragment>)}
|
|
</div>
|
|
Comma-separated fields to auto-generate when
|
|
uploading photos. Accepted values: title, caption,
|
|
tags, description, all, or none
|
|
{' '}
|
|
(default: {renderCommaSeparatedList(
|
|
AI_AUTO_GENERATED_FIELDS_DEFAULT,
|
|
)}):
|
|
{renderEnvVars(['AI_TEXT_AUTO_GENERATED_FIELDS'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Base URL override (experimental)"
|
|
status={hasOpenaiBaseUrl}
|
|
optional
|
|
>
|
|
Store base URL in environment variable to use
|
|
alternate OpenAI-compatible providers:
|
|
{renderEnvVars(['OPENAI_BASE_URL'])}
|
|
</ChecklistRow>
|
|
</>;
|
|
case 'Performance':
|
|
return <>
|
|
<ChecklistRow
|
|
title="Static optimization"
|
|
status={isStaticallyOptimized}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to make site more responsive
|
|
by enabling static optimization
|
|
(i.e., rendering pages and images at build time):
|
|
<div>
|
|
{renderSubStatusWithEnvVar(
|
|
arePhotosStaticallyOptimized ? 'checked' : 'optional',
|
|
'NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTOS',
|
|
)}
|
|
{renderSubStatusWithEnvVar(
|
|
arePhotoOGImagesStaticallyOptimized ? 'checked' : 'optional',
|
|
'NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_OG_IMAGES',
|
|
)}
|
|
{renderSubStatusWithEnvVar(
|
|
arePhotoCategoriesStaticallyOptimized ? 'checked' : 'optional',
|
|
'NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_CATEGORIES',
|
|
)}
|
|
{renderSubStatusWithEnvVar(
|
|
// eslint-disable-next-line max-len
|
|
arePhotoCategoryOgImagesStaticallyOptimized ? 'checked' : 'optional',
|
|
'NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_CATEGORY_OG_IMAGES',
|
|
)}
|
|
</div>
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Preserve original uploads"
|
|
status={areOriginalUploadsPreserved}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to prevent
|
|
image uploads being compressed before storing:
|
|
{renderEnvVars(['NEXT_PUBLIC_PRESERVE_ORIGINAL_UPLOADS'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title={`Image quality: ${imageQuality}`}
|
|
status={hasImageQuality}
|
|
optional
|
|
>
|
|
Set environment variable from {'"1-100"'}
|
|
{' '}
|
|
to control the quality of large photos
|
|
({'"100"'} represents highest quality/largest size):
|
|
{renderEnvVars(['NEXT_PUBLIC_IMAGE_QUALITY'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Image blur"
|
|
status={isBlurEnabled}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to prevent
|
|
image blur data being stored and displayed:
|
|
{renderEnvVars(['NEXT_PUBLIC_BLUR_DISABLED'])}
|
|
</ChecklistRow>
|
|
</>;
|
|
case 'Categories':
|
|
return <>
|
|
<ChecklistRow
|
|
title="Visibility and ordering"
|
|
status={hasCategoryVisibility}
|
|
optional
|
|
>
|
|
{renderOrderedKeyList(categoryVisibility, CATEGORY_KEYS)}
|
|
<div>
|
|
Configure order and visibility of categories
|
|
(seen in grid sidebar and CMD-K results)
|
|
by storing comma-separated values
|
|
(default: {renderCommaSeparatedList(DEFAULT_CATEGORY_KEYS)}):
|
|
</div>
|
|
{renderEnvVars(['NEXT_PUBLIC_CATEGORY_VISIBILITY'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Show on mobile"
|
|
status={showCategoriesOnMobile}
|
|
optional
|
|
>
|
|
<div className="flex flex-col gap-2">
|
|
<div>
|
|
Set environment variable to {'"1"'} to prevent categories
|
|
displaying on mobile grid view:
|
|
{renderEnvVars(['NEXT_PUBLIC_HIDE_CATEGORIES_ON_MOBILE'])}
|
|
</div>
|
|
</div>
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Show image hovers"
|
|
status={showCategoryImageHover}
|
|
optional
|
|
>
|
|
<div className="flex flex-col gap-2">
|
|
<div>
|
|
Set environment variable to {'"1"'} to prevent images
|
|
displaying when hovering over category links:
|
|
{renderEnvVars(['NEXT_PUBLIC_HIDE_CATEGORY_IMAGE_HOVERS'])}
|
|
</div>
|
|
</div>
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Collapsible sidebar"
|
|
status={collapseSidebarCategories}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to always show
|
|
expanded category content
|
|
{renderEnvVars(['NEXT_PUBLIC_EXHAUSTIVE_SIDEBAR_CATEGORIES'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Hide tags with only 1 photo"
|
|
status={hideTagsWithOnePhoto}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to only show tags
|
|
with 2 or more photos
|
|
{renderEnvVars(['NEXT_PUBLIC_HIDE_TAGS_WITH_ONE_PHOTO'])}
|
|
</ChecklistRow>
|
|
</>;
|
|
case 'Sorting':
|
|
return <>
|
|
<ChecklistRow
|
|
title="Default order"
|
|
status={hasDefaultSortBy}
|
|
optional
|
|
>
|
|
<div>
|
|
{DEFAULT_SORT_BY_OPTIONS
|
|
.map(({sortBy, configKey }) =>
|
|
<Fragment key={ sortBy }>
|
|
{renderSubStatus(
|
|
sortBy === defaultSortBy ? 'checked' : 'optional',
|
|
`${configKey}${sortBy === APP_DEFAULT_SORT_BY
|
|
? ' (default)'
|
|
: ''}`,
|
|
)}
|
|
</Fragment>)}
|
|
</div>
|
|
Change default sort on grid/full homepages
|
|
{renderEnvVars(['NEXT_PUBLIC_DEFAULT_SORT'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title={`Nav sort control: ${navSortControl}`}
|
|
status={hasNavSortControl}
|
|
optional
|
|
>
|
|
Set environment variable to {'"none"'}, {'"toggle"'} (default),
|
|
or {'"menu"'}, to control sort UI on grid/full homepages:
|
|
{renderEnvVars(['NEXT_PUBLIC_NAV_SORT_CONTROL'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Color sort"
|
|
status={isColorSortEnabled}
|
|
experimental
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to enable color-based sorting
|
|
(forces nav sort control to {'"menu,"'} flags photos missing
|
|
color data in admin dashboard)—color identification
|
|
benefits greatly from AI being enabled:
|
|
{renderEnvVars([
|
|
'NEXT_PUBLIC_COLOR_SORT',
|
|
])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Color sort configuration"
|
|
status={hasColorSortConfiguration}
|
|
experimental
|
|
optional
|
|
>
|
|
Configure which colors start first
|
|
(accepts a hue of 0 to 360, default: 80)
|
|
and which are considered sufficiently vibrant
|
|
(accepts a chroma of 0 to 0.37, default: 0.05):
|
|
<div>
|
|
<EnvVar
|
|
variable="NEXT_PUBLIC_COLOR_SORT_STARTING_HUE"
|
|
value={colorSortStartingHue}
|
|
accessory={renderColorDot({
|
|
l: 0.85,
|
|
c: 0.15,
|
|
h: colorSortStartingHue,
|
|
}, false)}
|
|
/>
|
|
<EnvVar
|
|
variable="NEXT_PUBLIC_COLOR_SORT_CHROMA_CUTOFF"
|
|
value={colorSortChromaCutoff}
|
|
/>
|
|
</div>
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Priority-based"
|
|
status={isSortWithPriority}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to take priority field
|
|
into account when sorting photos (enabling may have
|
|
performance consequences):
|
|
{renderEnvVars(['NEXT_PUBLIC_PRIORITY_BASED_SORTING'])}
|
|
</ChecklistRow>
|
|
</>;
|
|
case 'Display':
|
|
return <>
|
|
<ChecklistRow
|
|
title="Show keyboard shortcut tooltips"
|
|
status={showKeyboardShortcutTooltips}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to hide keyboard shortcut
|
|
tooltips in areas like the main nav, and previous/next photo links:
|
|
{renderEnvVars(['NEXT_PUBLIC_HIDE_KEYBOARD_SHORTCUT_TOOLTIPS'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Show EXIF data"
|
|
status={showExifInfo}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to hide EXIF data:
|
|
{renderEnvVars(['NEXT_PUBLIC_HIDE_EXIF_DATA'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Show zoom controls"
|
|
status={showZoomControls}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to hide
|
|
fullscreen photo zoom controls:
|
|
{renderEnvVars(['NEXT_PUBLIC_HIDE_ZOOM_CONTROLS'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Show taken at time"
|
|
status={showTakenAtTimeHidden}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to hide
|
|
taken at time from photo meta:
|
|
{renderEnvVars(['NEXT_PUBLIC_HIDE_TAKEN_AT_TIME'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Show repo link"
|
|
status={showRepoLink}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to hide footer link:
|
|
{renderEnvVars(['NEXT_PUBLIC_HIDE_REPO_LINK'])}
|
|
</ChecklistRow>
|
|
</>;
|
|
case 'Grid':
|
|
return <>
|
|
<ChecklistRow
|
|
title="Grid homepage"
|
|
status={isGridHomepageEnabled}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to show grid layout
|
|
on homepage:
|
|
{renderEnvVars(['NEXT_PUBLIC_GRID_HOMEPAGE'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title={`Grid aspect ratio: ${gridAspectRatio}`}
|
|
status={hasGridAspectRatio}
|
|
optional
|
|
>
|
|
Set environment variable to any number to enforce aspect ratio
|
|
{' '}
|
|
(default is {'"1"'}, i.e., square)—set to {'"0"'} to disable:
|
|
{renderEnvVars(['NEXT_PUBLIC_GRID_ASPECT_RATIO'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title={`Grid density: ${hasHighGridDensity ? 'high' : 'low'}`}
|
|
status={hasGridDensityPreference}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to ensure large thumbnails
|
|
on photo grid views (if not configured, density is based on
|
|
aspect ratio):
|
|
{renderEnvVars(['NEXT_PUBLIC_SHOW_LARGE_THUMBNAILS'])}
|
|
</ChecklistRow>
|
|
</>;
|
|
case 'Design':
|
|
return <>
|
|
<ChecklistRow
|
|
title={`Default theme: ${defaultTheme}`}
|
|
status={hasDefaultTheme}
|
|
optional
|
|
>
|
|
{'Set environment variable to \'light\' or \'dark\''}
|
|
{' '}
|
|
to configure initial theme
|
|
{' '}
|
|
(defaults to {'\'system\''}):
|
|
{renderEnvVars(['NEXT_PUBLIC_DEFAULT_THEME'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Photo matting"
|
|
status={arePhotosMatted}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to constrain the size
|
|
{' '}
|
|
of each photo, and display a surrounding border:
|
|
<div className="pt-1 flex flex-col gap-1">
|
|
<EnvVar variable="NEXT_PUBLIC_MATTE_PHOTOS" />
|
|
</div>
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Custom photo matting colors"
|
|
status={arePhotoMatteColorsConfigured}
|
|
optional
|
|
>
|
|
Set environment variable hex values (e.g., #cccccc)
|
|
to override matte colors:
|
|
<div className="pt-1 flex flex-col gap-1">
|
|
<EnvVar
|
|
variable="NEXT_PUBLIC_MATTE_COLOR"
|
|
accessory={matteColor && renderColorDot(matteColor)}
|
|
/>
|
|
<EnvVar
|
|
variable="NEXT_PUBLIC_MATTE_COLOR_DARK"
|
|
accessory={matteColorDark && renderColorDot(matteColorDark)}
|
|
/>
|
|
</div>
|
|
</ChecklistRow>
|
|
</>;
|
|
case 'Settings':
|
|
return <>
|
|
<ChecklistRow
|
|
title="Geo privacy"
|
|
status={isGeoPrivacyEnabled}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to disable
|
|
collection/display of location-based data:
|
|
{renderEnvVars(['NEXT_PUBLIC_GEO_PRIVACY'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Public downloads"
|
|
status={arePublicDownloadsEnabled}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to enable
|
|
public photo downloads for all visitors:
|
|
{renderEnvVars(['NEXT_PUBLIC_ALLOW_PUBLIC_DOWNLOADS'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Social networks"
|
|
status={hasSocialKeys}
|
|
optional
|
|
>
|
|
{renderOrderedKeyList(socialKeys, SOCIAL_KEYS)}
|
|
<div>
|
|
Configure order and visibility of social networks
|
|
(seen in share modal) by storing comma-separated values
|
|
(accepts {'"all"'} or {'"none"'},
|
|
defaults to {renderCommaSeparatedList(DEFAULT_SOCIAL_KEYS)})
|
|
</div>
|
|
{renderEnvVars(['NEXT_PUBLIC_SOCIAL_NETWORKS'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Site feeds (JSON/RSS)"
|
|
status={areSiteFeedsEnabled}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to enable feeds at
|
|
{' '}
|
|
{renderLink(PATH_FEED_JSON)} and {renderLink(PATH_RSS_XML)}:
|
|
{renderEnvVars(['NEXT_PUBLIC_SITE_FEEDS'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Legacy OG text alignment"
|
|
status={isOgTextBottomAligned}
|
|
optional
|
|
>
|
|
Set environment variable to {'"BOTTOM"'} to
|
|
keep OG image text bottom aligned (default is {'"top"'}):
|
|
{renderEnvVars(['NEXT_PUBLIC_OG_TEXT_ALIGNMENT'])}
|
|
</ChecklistRow>
|
|
</>;
|
|
case 'Scripts & Analytics':
|
|
return <>
|
|
<ChecklistRow
|
|
title="Custom page scripts"
|
|
status={hasPageScriptUrls}
|
|
optional
|
|
>
|
|
{pageScriptUrls.length > 0 &&
|
|
<div className="mt-2 text-xs space-y-1.5">
|
|
{pageScriptUrls.map(url =>
|
|
<MaskedScroll
|
|
key={url}
|
|
className={clsx(
|
|
'inline-flex items-center gap-1',
|
|
'bg-dim rounded-md px-1.5 py-0.5',
|
|
)}
|
|
direction="horizontal"
|
|
>
|
|
<IoLink size={14} className="shrink-0 translate-y-[0.5px]"/>
|
|
<span className="font-medium text-nowrap">
|
|
{url}
|
|
</span>
|
|
</MaskedScroll>)}
|
|
</div>}
|
|
Set environment variable to comma-separated list of URLs
|
|
to be added to the bottom of the body tag via {'"next/script"'}:
|
|
{renderEnvVars(['PAGE_SCRIPT_URLS'])}
|
|
</ChecklistRow>
|
|
</>;
|
|
case 'Internal':
|
|
return <>
|
|
<ChecklistRow
|
|
title="Debug tools"
|
|
status={areAdminDebugToolsEnabled}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to temporarily enable
|
|
features like photo matting, baseline grid, etc.:
|
|
{renderEnvVars(['ADMIN_DEBUG_TOOLS'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="SQL debugging"
|
|
status={isAdminSqlDebugEnabled}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to enable
|
|
console output for all sql queries:
|
|
{renderEnvVars(['ADMIN_SQL_DEBUG'])}
|
|
</ChecklistRow>
|
|
</>;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ScoreCardContainer>
|
|
{getAdminConfigSections(areInternalToolsEnabled, simplifiedView)
|
|
.map((section) => (
|
|
<ChecklistGroup
|
|
key={section.title}
|
|
title={section.title}
|
|
titleShort={(section as AdminConfigSection).titleShort}
|
|
icon={section.icon}
|
|
optional={!section.required}
|
|
updateHashOnVisible={hasScrolled && !simplifiedView}
|
|
>
|
|
{renderGroupContent(section.title)}
|
|
</ChecklistGroup>
|
|
))}
|
|
<div className="pl-11 pr-2 sm:pr-11 mt-4 md:mt-7">
|
|
<div>
|
|
Changes to environment variables require a new deployment
|
|
to take effect
|
|
</div>
|
|
</div>
|
|
</ScoreCardContainer>
|
|
);
|
|
}
|