'use client'; import { ComponentProps, Fragment, ReactNode, } from 'react'; import ChecklistRow from '../components/ChecklistRow'; import { BiData, BiHide, BiLockAlt, BiPencil, } from 'react-icons/bi'; import { HiOutlineCog, HiSparkles } from 'react-icons/hi'; 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 { RiSpeedMiniLine } from 'react-icons/ri'; import SecretGenerator from '../app/SecretGenerator'; import { PiPaintBrushHousehold } from 'react-icons/pi'; import { IoMdGrid } from 'react-icons/io'; import { CgDebug } from 'react-icons/cg'; import EnvVar from '@/components/EnvVar'; import AdminLink from './AdminLink'; import ScoreCardContainer from '@/components/ScoreCardContainer'; import { DEFAULT_CATEGORY_KEYS, getHiddenCategories } from '@/category'; import { AI_AUTO_GENERATED_FIELDS_ALL } from '@/photo/ai'; import clsx from 'clsx/lite'; import Link from 'next/link'; import { PATH_FEED_JSON, PATH_RSS_XML } from '@/app/paths'; import { FaRegFolderClosed } from 'react-icons/fa6'; import IconSort from '@/components/icons/IconSort'; import { APP_DEFAULT_SORT_BY, SORT_BY_OPTIONS } from '@/photo/db/sort'; export default function AdminAppConfigurationClient({ // Storage hasDatabase, isPostgresSslEnabled, hasVercelPostgres, hasRedisStorage, hasStorageProvider, hasVercelBlobStorage, hasCloudflareR2Storage, hasAwsS3Storage, hasMultipleStorageProviders, currentStorage, // Auth hasAuthSecret, hasAdminUser, // Content locale, hasLocale, domain, hasDomain, metaTitle, isMetaTitleConfigured, metaDescription, isMetaDescriptionConfigured, navTitle, hasNavTitle, navCaption, hasNavCaption, pageAbout, hasPageAbout, // AI hasOpenaiBaseUrl, isAiTextGenerationEnabled, aiTextAutoGeneratedFields, hasAiTextAutoGeneratedFields, // Performance isStaticallyOptimized, arePhotosStaticallyOptimized, arePhotoOGImagesStaticallyOptimized, arePhotoCategoriesStaticallyOptimized, arePhotoCategoryOgImagesStaticallyOptimized, areOriginalUploadsPreserved, hasImageQuality, imageQuality, isBlurEnabled, // Categories hasCategoryVisibility, categoryVisibility, showCategoryImageHover, collapseSidebarCategories, hideTagsWithOnePhoto, // Sort hasDefaultSortBy, defaultSortBy, isSortWithPriority, showSortControl, // Display showKeyboardShortcutTooltips, showExifInfo, showZoomControls, showTakenAtTimeHidden, showSocial, showRepoLink, // Grid isGridHomepageEnabled, gridAspectRatio, hasGridAspectRatio, hasHighGridDensity, hasGridDensityPreference, // Design hasDefaultTheme, defaultTheme, arePhotosMatted, arePhotoMatteColorsConfigured, matteColor, matteColorDark, // Settings isGeoPrivacyEnabled, arePublicDownloadsEnabled, areSiteFeedsEnabled, isOgTextBottomAligned, // Internal areInternalToolsEnabled, areAdminDebugToolsEnabled, isAdminDbOptimizeEnabled, isAdminSqlDebugEnabled, // Connection status databaseError, storageError, redisError, aiError, // Component props simplifiedView, isAnalyzingConfiguration, }: AppConfiguration & Partial>> & { simplifiedView?: boolean isAnalyzingConfiguration?: boolean }) { const renderContent = (content?: ReactNode) => content ?
{content}
: null; const renderEnvVars = (variables: string[]) =>
{variables.map(variable => )}
; const renderSubStatus = ( type: ComponentProps['type'], label: ReactNode, iconClassName = 'translate-y-[3.5px]', ) =>
{label}
; const renderSubStatusWithEnvVar = ( type: ComponentProps['type'], variable: string, ) => renderSubStatus( type, renderEnvVars([variable]), 'translate-y-[7px]', ); const renderError = ({ connection, message, }: { connection?: { provider: string, error: string } message?: string }) => {connection && <> {connection.provider} connection error: {`"${connection.error}"`} } {message} ; const renderLink = (href: string, children?: ReactNode) => {children || href} ; return ( } > {databaseError && renderError({ connection: { provider: 'Database', error: databaseError}, })} {hasVercelPostgres ? renderSubStatus('checked', 'Vercel Postgres: connected') : renderSubStatus('optional', <> Vercel Postgres: {' '} create store {' '} and connect to project )} {hasDatabase && !hasVercelPostgres && renderSubStatus('checked', <> Postgres-compatible: connected {' '} (SSL {isPostgresSslEnabled ? 'enabled' : 'disabled'}) )} {storageError && renderError({ connection: { provider: 'Storage', error: storageError}, })}
{hasVercelBlobStorage ? renderSubStatus('checked', 'Vercel Blob: connected') : renderSubStatus('optional', <> {labelForStorage('vercel-blob')}: {' '} create store {' '} and connect to project , )} {hasCloudflareR2Storage ? renderSubStatus('checked', 'Cloudflare R2: connected') : renderSubStatus('optional', <> {labelForStorage('cloudflare-r2')}: {' '} create/configure bucket )} {hasAwsS3Storage ? renderSubStatus('checked', 'AWS S3: connected') : renderSubStatus('optional', <> {labelForStorage('aws-s3')}: {' '} create/configure bucket )}
} > Store auth secret in environment variable: {!hasAuthSecret &&
} {renderEnvVars(['AUTH_SECRET'])}
Store admin email/password {' '} in environment variables: {renderEnvVars([ 'ADMIN_EMAIL', 'ADMIN_PASSWORD', ])}
} > {renderContent(locale)} Store in environment variable (check README for {' '} supported languages ): {renderEnvVars(['NEXT_PUBLIC_LOCALE'])} {renderContent(domain)} Store in environment variable (used in explicit share urls, seen in nav if no title is defined): {renderEnvVars(['NEXT_PUBLIC_DOMAIN'])} {renderContent(metaTitle)} Store in environment variable (seen in search results and browser tab): {renderEnvVars(['NEXT_PUBLIC_META_TITLE'])} {!simplifiedView && <> {renderContent(metaDescription)} Store in environment variable (seen in search results): {renderEnvVars(['NEXT_PUBLIC_META_DESCRIPTION'])} {renderContent(navTitle)} Store in environment variable (replaces domain in top-right nav): {renderEnvVars(['NEXT_PUBLIC_NAV_TITLE'])} {hasNavCaption && renderContent(navCaption)} Store in environment variable (seen in top-right nav, under title): {renderEnvVars(['NEXT_PUBLIC_NAV_CAPTION'])} {hasPageAbout && renderContent(pageAbout)} Store in environment variable (seen in sidebar): {renderEnvVars(['NEXT_PUBLIC_PAGE_ABOUT'])} } {!simplifiedView && <> } optional > {aiError && renderError({ connection: { provider: 'OpenAI', error: aiError}, })} Store your OpenAI secret key in order to enable AI-generated text descriptions and optionally leverage an invisible field called {'"Semantic Description"'} used to support CMD-K search and improve accessibility: {renderEnvVars(['OPENAI_SECRET_KEY'])}
{hasAiTextAutoGeneratedFields && AI_AUTO_GENERATED_FIELDS_ALL.map(field => {renderSubStatus( aiTextAutoGeneratedFields.includes(field) ? 'checked' : 'optional', field, )} )}
Comma-separated fields to auto-generate when uploading photos. Accepted values: title, caption, tags, description, all, or none {' '} (default: {'"title,tags,semantic"'}): {renderEnvVars(['AI_TEXT_AUTO_GENERATED_FIELDS'])}
{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 Store base URL in environment variable to use alternate OpenAI-compatible providers: {renderEnvVars(['OPENAI_BASE_URL'])}
} optional > Set environment variable to {'"1"'} to make site more responsive by enabling static optimization (i.e., rendering pages and images at build time):
{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', )}
Set environment variable to {'"1"'} to prevent image uploads being compressed before storing: {renderEnvVars(['NEXT_PUBLIC_PRESERVE_ORIGINAL_UPLOADS'])} Set environment variable from {'"1-100"'} {' '} to control the quality of large photos ({'"100"'} represents highest quality/largest size): {renderEnvVars(['NEXT_PUBLIC_IMAGE_QUALITY'])} Set environment variable to {'"1"'} to prevent image blur data being stored and displayed: {renderEnvVars(['NEXT_PUBLIC_BLUR_DISABLED'])}
} optional >
{categoryVisibility.map((category, index) => {renderSubStatus( 'checked', <> {index + 1} {'.'} {category} , )} )} {getHiddenCategories(categoryVisibility) .map(category => {renderSubStatus( 'optional', {'* '} {category} , )} )}
Configure order and visibility of categories (seen in grid sidebar and CMD-K results) by storing comma-separated values (default: {`"${DEFAULT_CATEGORY_KEYS.join(',')}"`}): {renderEnvVars(['NEXT_PUBLIC_CATEGORY_VISIBILITY'])}
Set environment variable to {'"1"'} to prevent images displaying when hovering over category links: {renderEnvVars(['NEXT_PUBLIC_HIDE_CATEGORY_IMAGE_HOVERS'])}
Set environment variable to {'"1"'} to always show expanded category content {renderEnvVars(['NEXT_PUBLIC_EXHAUSTIVE_SIDEBAR_CATEGORIES'])} Set environment variable to {'"1"'} to only show tags with 2 or more photos {renderEnvVars(['NEXT_PUBLIC_HIDE_TAGS_WITH_ONE_PHOTO'])}
} optional >
{SORT_BY_OPTIONS.map(({sortBy, string }) => {renderSubStatus( sortBy === defaultSortBy ? 'checked' : 'optional', `${string}${sortBy === APP_DEFAULT_SORT_BY ? ' (default)' : ''}`, )} )}
Change default sort on grid/full homepages {renderEnvVars(['NEXT_PUBLIC_DEFAULT_SORT'])}
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'])} Set environment variable to {'"1"'} to show sort control in desktop nav on grid/full homepages: {renderEnvVars(['NEXT_PUBLIC_SHOW_SORT_CONTROL'])}
} 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'])} Set environment variable to {'"1"'} to hide EXIF data: {renderEnvVars(['NEXT_PUBLIC_HIDE_EXIF_DATA'])} Set environment variable to {'"1"'} to hide fullscreen photo zoom controls: {renderEnvVars(['NEXT_PUBLIC_HIDE_ZOOM_CONTROLS'])} Set environment variable to {'"1"'} to hide taken at time from photo meta: {renderEnvVars(['NEXT_PUBLIC_HIDE_TAKEN_AT_TIME'])} Set environment variable to {'"1"'} to hide {' '} X (formerly Twitter) button from share modal: {renderEnvVars(['NEXT_PUBLIC_HIDE_SOCIAL'])} Set environment variable to {'"1"'} to hide footer link: {renderEnvVars(['NEXT_PUBLIC_HIDE_REPO_LINK'])} } optional > Set environment variable to {'"1"'} to show grid layout on homepage: {renderEnvVars(['NEXT_PUBLIC_GRID_HOMEPAGE'])} 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'])} 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'])} } optional > {'Set environment variable to \'light\' or \'dark\''} {' '} to configure initial theme {' '} (defaults to {'\'system\''}): {renderEnvVars(['NEXT_PUBLIC_DEFAULT_THEME'])} Set environment variable to {'"1"'} to constrain the size {' '} of each photo, and display a surrounding border:
Set environment variable hex values (e.g., #cccccc) to override matte colors:
} /> } />
} optional > Set environment variable to {'"1"'} to disable collection/display of location-based data: {renderEnvVars(['NEXT_PUBLIC_GEO_PRIVACY'])} Set environment variable to {'"1"'} to enable public photo downloads for all visitors: {renderEnvVars(['NEXT_PUBLIC_ALLOW_PUBLIC_DOWNLOADS'])} Set environment variable to {'"1"'} to enable feeds at {' '} {renderLink(PATH_FEED_JSON)} and {renderLink(PATH_RSS_XML)}: {renderEnvVars(['NEXT_PUBLIC_SITE_FEEDS'])} Set environment variable to {'"BOTTOM"'} to keep OG image text bottom aligned (default is {'"top"'}): {renderEnvVars(['NEXT_PUBLIC_OG_TEXT_ALIGNMENT'])} {areInternalToolsEnabled && } optional > Set environment variable to {'"1"'} to temporarily enable features like photo matting, baseline grid, etc.: {renderEnvVars(['ADMIN_DEBUG_TOOLS'])} Set environment variable to {'"1"'} to prevent homepages from seeding infinite scroll on load: {renderEnvVars(['ADMIN_DB_OPTIMIZE'])} Set environment variable to {'"1"'} to enable console output for all sql queries: {renderEnvVars(['ADMIN_SQL_DEBUG'])} } }
Changes to environment variables require a redeploy or reboot of local dev server
); }