Make Checklist a special case of ScoreCard

This commit is contained in:
Sam Becker 2025-02-18 19:32:24 -06:00
parent 1fa3eaccc3
commit de8bce1bee
11 changed files with 572 additions and 544 deletions

View File

@ -0,0 +1,21 @@
'use client';
import SiteGrid from '@/components/SiteGrid';
import StatusIcon from '@/components/StatusIcon';
import clsx from 'clsx/lite';
export default function ComponentsPage() {
return (
<SiteGrid
contentMain={<div className={clsx(
'flex gap-0.5',
'*:inline-flex *:bg-medium',
)}>
<StatusIcon type="checked" />
<StatusIcon type="missing" />
<StatusIcon type="warning" />
<StatusIcon type="optional" />
</div>}
/>
);
}

View File

@ -114,12 +114,10 @@ export default function AdminAppConfigurationClient({
const renderSubStatus = ( const renderSubStatus = (
type: ComponentProps<typeof StatusIcon>['type'], type: ComponentProps<typeof StatusIcon>['type'],
label: ReactNode, label: ReactNode,
iconClassName?: string, iconClassName = 'translate-y-[3.5px]',
) => ) =>
<div className="flex gap-2 translate-x-[-3px]"> <div className="flex gap-2 translate-x-[-2.5px]">
<span className={iconClassName}> <StatusIcon {...{ type, className: iconClassName }} />
<StatusIcon {...{ type }} />
</span>
<span className="min-w-0"> <span className="min-w-0">
{label} {label}
</span> </span>
@ -132,7 +130,7 @@ export default function AdminAppConfigurationClient({
renderSubStatus( renderSubStatus(
type, type,
renderEnvVars([variable]), renderEnvVars([variable]),
'translate-y-[3px]', 'translate-y-[4.5px]',
); );
const renderError = ({ const renderError = ({
@ -165,498 +163,496 @@ export default function AdminAppConfigurationClient({
return ( return (
<> <>
<div className="space-y-3 -mt-3"> <ChecklistGroup
<ChecklistGroup title="Storage"
title="Storage" icon={<BiData size={16} />}
icon={<BiData size={16} />} >
<ChecklistRow
title={hasDatabase && isAnalyzingConfiguration
? 'Testing database connection'
: 'Setup database'}
status={hasDatabase}
isPending={hasDatabase && isAnalyzingConfiguration}
> >
<ChecklistRow {databaseError && renderError({
title={hasDatabase && isAnalyzingConfiguration connection: { provider: 'Database', error: databaseError},
? 'Testing database connection' })}
: 'Setup database'} {hasVercelPostgres
status={hasDatabase} ? renderSubStatus('checked', 'Vercel Postgres: connected')
isPending={hasDatabase && isAnalyzingConfiguration} : renderSubStatus('optional', <>
> Vercel Postgres:
{databaseError && renderError({
connection: { provider: 'Database', error: databaseError},
})}
{hasVercelPostgres
? renderSubStatus('checked', 'Vercel Postgres: connected')
: renderSubStatus('optional', <>
Vercel Postgres:
{' '}
<AdminLink
// eslint-disable-next-line max-len
href="https://vercel.com/docs/storage/vercel-postgres/quickstart#create-a-postgres-database"
externalIcon
>
create store
</AdminLink>
{' '}
and connect to project
</>)}
{hasDatabase && !hasVercelPostgres &&
renderSubStatus('checked', <>
Postgres-compatible: connected
{' '}
(SSL {isPostgresSslEnabled ? 'enabled' : 'disabled'})
</>)}
</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},
})}
{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>
</>)}
</ChecklistRow>
</ChecklistGroup>
<ChecklistGroup
title="Authentication"
icon={<BiLockAlt size={16} />}
>
<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 />
</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>
</ChecklistGroup>
<ChecklistGroup
title="Content"
icon={<BiPencil size={16} />}
>
<ChecklistRow
title="Configure domain"
status={hasDomain}
showWarning
>
{!hasDomain &&
renderWarning({message:
'Not explicitly setting a domain may cause ' +
'certain features to behave unexpectedly',
})}
Store in environment variable (seen in top-right nav):
{renderEnvVars(['NEXT_PUBLIC_SITE_DOMAIN'])}
</ChecklistRow>
<ChecklistRow
title="Add title"
status={hasTitle}
optional
>
Store in environment variable (seen in browser tab):
{renderEnvVars(['NEXT_PUBLIC_SITE_TITLE'])}
</ChecklistRow>
<ChecklistRow
title="Add description"
status={hasDescription}
optional
>
Store in environment variable (seen in nav, under title):
{renderEnvVars(['NEXT_PUBLIC_SITE_DESCRIPTION'])}
</ChecklistRow>
<ChecklistRow
title="Add about"
status={hasAbout}
optional
>
Store in environment variable (seen in grid sidebar):
{renderEnvVars(['NEXT_PUBLIC_SITE_ABOUT'])}
</ChecklistRow>
</ChecklistGroup>
{!simplifiedView && <>
<ChecklistGroup
title="AI text generation"
titleShort="AI"
icon={<HiSparkles />}
optional
>
<ChecklistRow
title={isAiTextGenerationEnabled && isAnalyzingConfiguration
? 'Testing OpenAI connection'
: 'Add OpenAI secret key'}
status={isAiTextGenerationEnabled}
isPending={isAiTextGenerationEnabled && isAnalyzingConfiguration}
optional
>
{aiError && renderError({
connection: { provider: 'OpenAI', error: aiError},
})}
Store your OpenAI secret key in order to add experimental support
for AI-generated text descriptions and enable an invisible field
called {'"Semantic Description"'} used to support CMD-K search:
{renderEnvVars(['OPENAI_SECRET_KEY'])}
</ChecklistRow>
<ChecklistRow
title={hasRedisStorage && isAnalyzingConfiguration
? 'Testing Redis connection'
: 'Enable rate limiting'}
status={hasRedisStorage}
isPending={hasRedisStorage && isAnalyzingConfiguration}
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
</ChecklistRow>
<ChecklistRow
// eslint-disable-next-line max-len
title={`Auto-generated fields: ${aiTextAutoGeneratedFields.join(', ')}`}
status={hasAiTextAutoGeneratedFields}
optional
>
Comma-separated fields to auto-generate when
uploading photos. Accepted values: title, caption,
tags, description, all, or none
{' '} {' '}
(default: {'"title, tags, semantic"'}): <AdminLink
{renderEnvVars(['AI_TEXT_AUTO_GENERATED_FIELDS'])}
</ChecklistRow>
</ChecklistGroup>
<ChecklistGroup
title="Performance"
icon={<RiSpeedMiniLine size={18} />}
optional
>
<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):
{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 // eslint-disable-next-line max-len
arePhotoCategoryOgImagesStaticallyOptimized ? 'checked' : 'optional', href="https://vercel.com/docs/storage/vercel-postgres/quickstart#create-a-postgres-database"
'NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_CATEGORY_OG_IMAGES', externalIcon
)} >
create store
</AdminLink>
{' '}
and connect to project
</>)}
{hasDatabase && !hasVercelPostgres &&
renderSubStatus('checked', <>
Postgres-compatible: connected
{' '}
(SSL {isPostgresSslEnabled ? 'enabled' : 'disabled'})
</>)}
</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},
})}
{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>
</>)}
</ChecklistRow>
</ChecklistGroup>
<ChecklistGroup
title="Authentication"
icon={<BiLockAlt size={16} />}
>
<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 />
</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>
</ChecklistGroup>
<ChecklistGroup
title="Content"
icon={<BiPencil size={16} />}
>
<ChecklistRow
title="Configure domain"
status={hasDomain}
showWarning
>
{!hasDomain &&
renderWarning({message:
'Not explicitly setting a domain may cause ' +
'certain features to behave unexpectedly',
})}
Store in environment variable (seen in top-right nav):
{renderEnvVars(['NEXT_PUBLIC_SITE_DOMAIN'])}
</ChecklistRow>
<ChecklistRow
title="Add title"
status={hasTitle}
optional
>
Store in environment variable (seen in browser tab):
{renderEnvVars(['NEXT_PUBLIC_SITE_TITLE'])}
</ChecklistRow>
<ChecklistRow
title="Add description"
status={hasDescription}
optional
>
Store in environment variable (seen in nav, under title):
{renderEnvVars(['NEXT_PUBLIC_SITE_DESCRIPTION'])}
</ChecklistRow>
<ChecklistRow
title="Add about"
status={hasAbout}
optional
>
Store in environment variable (seen in grid sidebar):
{renderEnvVars(['NEXT_PUBLIC_SITE_ABOUT'])}
</ChecklistRow>
</ChecklistGroup>
{!simplifiedView && <>
<ChecklistGroup
title="AI text generation"
titleShort="AI"
icon={<HiSparkles />}
optional
>
<ChecklistRow
title={isAiTextGenerationEnabled && isAnalyzingConfiguration
? 'Testing OpenAI connection'
: 'Add OpenAI secret key'}
status={isAiTextGenerationEnabled}
isPending={isAiTextGenerationEnabled && isAnalyzingConfiguration}
optional
>
{aiError && renderError({
connection: { provider: 'OpenAI', error: aiError},
})}
Store your OpenAI secret key in order to add experimental support
for AI-generated text descriptions and enable an invisible field
called {'"Semantic Description"'} used to support CMD-K search:
{renderEnvVars(['OPENAI_SECRET_KEY'])}
</ChecklistRow>
<ChecklistRow
title={hasRedisStorage && isAnalyzingConfiguration
? 'Testing Redis connection'
: 'Enable rate limiting'}
status={hasRedisStorage}
isPending={hasRedisStorage && isAnalyzingConfiguration}
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
</ChecklistRow>
<ChecklistRow
// eslint-disable-next-line max-len
title={`Auto-generated fields: ${aiTextAutoGeneratedFields.join(', ')}`}
status={hasAiTextAutoGeneratedFields}
optional
>
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'])}
</ChecklistRow>
</ChecklistGroup>
<ChecklistGroup
title="Performance"
icon={<RiSpeedMiniLine size={18} />}
optional
>
<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):
{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',
)}
</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>
</ChecklistGroup>
<ChecklistGroup
title="Visual"
icon={<PiPaintBrushHousehold size={19} />}
optional
>
<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:
{renderEnvVars(['NEXT_PUBLIC_MATTE_PHOTOS'])}
</ChecklistRow>
</ChecklistGroup>
<ChecklistGroup
title="Display"
icon={<BiHide size={18} />}
optional
>
<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 social"
status={showSocial}
optional
>
Set environment variable to {'"1"'} to hide
{' '}
X (formerly Twitter) button from share modal:
{renderEnvVars(['NEXT_PUBLIC_HIDE_SOCIAL'])}
</ChecklistRow>
<ChecklistRow
title="Show Fujifilm simulations"
status={showFilmSimulations}
optional
>
Set environment variable to {'"1"'} to prevent
simulations showing up in /grid sidebar and
CMD-K results:
{renderEnvVars(['NEXT_PUBLIC_HIDE_FILM_SIMULATIONS'])}
</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>
</ChecklistGroup>
<ChecklistGroup
title="Grid"
icon={<IoMdGrid size={17} />}
optional
>
<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>
</ChecklistGroup>
<ChecklistGroup
title="Settings"
icon={<HiOutlineCog size={17} className="translate-y-[0.5px]" />}
optional
>
<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="Public API"
status={isPublicApiEnabled}
optional
>
Set environment variable to {'"1"'} to enable
a public API available at <code>/api</code>:
{renderEnvVars(['NEXT_PUBLIC_PUBLIC_API'])}
</ChecklistRow>
<ChecklistRow
title="Priority order"
status={isPriorityOrderEnabled}
optional
>
Set environment variable to {'"1"'} to prevent
priority order photo field affecting photo order:
{renderEnvVars(['NEXT_PUBLIC_IGNORE_PRIORITY_ORDER'])}
</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>
</ChecklistGroup>
{areInternalToolsEnabled &&
<ChecklistGroup
title="Internal"
icon={<CgDebug size={16} />}
optional
>
<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>
<ChecklistRow <ChecklistRow
title="Preserve original uploads" title="DB optimize"
status={areOriginalUploadsPreserved} status={isAdminDbOptimizeEnabled}
optional optional
> >
Set environment variable to {'"1"'} to prevent Set environment variable to {'"1"'} to prevent
image uploads being compressed before storing: homepages from seeding infinite scroll on load:
{renderEnvVars(['NEXT_PUBLIC_PRESERVE_ORIGINAL_UPLOADS'])} {renderEnvVars(['ADMIN_DB_OPTIMIZE'])}
</ChecklistRow> </ChecklistRow>
<ChecklistRow <ChecklistRow
title={`Image quality: ${imageQuality}`} title="SQL debugging"
status={hasImageQuality} status={isAdminSqlDebugEnabled}
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>
</ChecklistGroup>
<ChecklistGroup
title="Visual"
icon={<PiPaintBrushHousehold size={19} />}
optional
>
<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:
{renderEnvVars(['NEXT_PUBLIC_MATTE_PHOTOS'])}
</ChecklistRow>
</ChecklistGroup>
<ChecklistGroup
title="Display"
icon={<BiHide size={18} />}
optional
>
<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 social"
status={showSocial}
optional
>
Set environment variable to {'"1"'} to hide
{' '}
X (formerly Twitter) button from share modal:
{renderEnvVars(['NEXT_PUBLIC_HIDE_SOCIAL'])}
</ChecklistRow>
<ChecklistRow
title="Show Fujifilm simulations"
status={showFilmSimulations}
optional
>
Set environment variable to {'"1"'} to prevent
simulations showing up in /grid sidebar and
CMD-K results:
{renderEnvVars(['NEXT_PUBLIC_HIDE_FILM_SIMULATIONS'])}
</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>
</ChecklistGroup>
<ChecklistGroup
title="Grid"
icon={<IoMdGrid size={17} />}
optional
>
<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>
</ChecklistGroup>
<ChecklistGroup
title="Settings"
icon={<HiOutlineCog size={17} className="translate-y-[0.5px]" />}
optional
>
<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 optional
> >
Set environment variable to {'"1"'} to enable Set environment variable to {'"1"'} to enable
public photo downloads for all visitors: console output for all sql queries:
{renderEnvVars(['NEXT_PUBLIC_ALLOW_PUBLIC_DOWNLOADS'])} {renderEnvVars(['ADMIN_SQL_DEBUG'])}
</ChecklistRow> </ChecklistRow>
<ChecklistRow </ChecklistGroup>}
title="Public API" </>}
status={isPublicApiEnabled}
optional
>
Set environment variable to {'"1"'} to enable
a public API available at <code>/api</code>:
{renderEnvVars(['NEXT_PUBLIC_PUBLIC_API'])}
</ChecklistRow>
<ChecklistRow
title="Priority order"
status={isPriorityOrderEnabled}
optional
>
Set environment variable to {'"1"'} to prevent
priority order photo field affecting photo order:
{renderEnvVars(['NEXT_PUBLIC_IGNORE_PRIORITY_ORDER'])}
</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>
</ChecklistGroup>
{areInternalToolsEnabled &&
<ChecklistGroup
title="Internal"
icon={<CgDebug size={16} />}
optional
>
<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="DB optimize"
status={isAdminDbOptimizeEnabled}
optional
>
Set environment variable to {'"1"'} to prevent
homepages from seeding infinite scroll on load:
{renderEnvVars(['ADMIN_DB_OPTIMIZE'])}
</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>
</ChecklistGroup>}
</>}
</div>
<div className="pl-11 pr-2 sm:pr-11 mt-4 md:mt-7"> <div className="pl-11 pr-2 sm:pr-11 mt-4 md:mt-7">
<div> <div>
Changes to environment variables require a redeploy Changes to environment variables require a redeploy

View File

@ -1,5 +1,6 @@
import Container from '@/components/Container'; import Container from '@/components/Container';
import SiteGrid from '@/components/SiteGrid'; import SiteGrid from '@/components/SiteGrid';
import clsx from 'clsx/lite';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
export default function AdminInfoPage({ export default function AdminInfoPage({
@ -22,7 +23,10 @@ export default function AdminInfoPage({
{accessory} {accessory}
</div> </div>
<Container spaceChildren={false}> <Container spaceChildren={false}>
<div className="max-w-xl w-full"> <div className={clsx(
'max-w-xl w-full',
'space-y-6 md:space-y-8',
)}>
{children} {children}
</div> </div>
</Container> </Container>

View File

@ -120,7 +120,7 @@ export default function AdminAppInsightsClient({
</a>; </a>;
return ( return (
<div className="space-y-6 md:space-y-8"> <>
{(codeMeta || debug) && <> {(codeMeta || debug) && <>
<ScoreCard title="Source code"> <ScoreCard title="Source code">
{codeMeta?.didError {codeMeta?.didError
@ -396,6 +396,6 @@ export default function AdminAppInsightsClient({
content={descriptionWithSpaces} content={descriptionWithSpaces}
/>} />}
</ScoreCard> </ScoreCard>
</div> </>
); );
} }

View File

@ -41,6 +41,7 @@ export const PATH_ADMIN_TAGS = `${PATH_ADMIN}/tags`;
export const PATH_ADMIN_CONFIGURATION = `${PATH_ADMIN}/configuration`; export const PATH_ADMIN_CONFIGURATION = `${PATH_ADMIN}/configuration`;
export const PATH_ADMIN_INSIGHTS = `${PATH_ADMIN}/insights`; export const PATH_ADMIN_INSIGHTS = `${PATH_ADMIN}/insights`;
export const PATH_ADMIN_BASELINE = `${PATH_ADMIN}/baseline`; export const PATH_ADMIN_BASELINE = `${PATH_ADMIN}/baseline`;
export const PATH_ADMIN_COMPONENTS = `${PATH_ADMIN}/components`;
// Debug paths // Debug paths
export const PATH_OG_ALL = `${PATH_OG}/all`; export const PATH_OG_ALL = `${PATH_OG}/all`;
@ -60,6 +61,8 @@ export const PATHS_ADMIN = [
PATH_ADMIN_UPLOADS, PATH_ADMIN_UPLOADS,
PATH_ADMIN_TAGS, PATH_ADMIN_TAGS,
PATH_ADMIN_CONFIGURATION, PATH_ADMIN_CONFIGURATION,
PATH_ADMIN_BASELINE,
PATH_ADMIN_COMPONENTS,
]; ];
export const PATHS_TO_CACHE = [ export const PATHS_TO_CACHE = [

View File

@ -4,6 +4,7 @@ import ExperimentalBadge from './ExperimentalBadge';
import Badge from './Badge'; import Badge from './Badge';
import ResponsiveText from './primitives/ResponsiveText'; import ResponsiveText from './primitives/ResponsiveText';
import { parameterize } from '@/utility/string'; import { parameterize } from '@/utility/string';
import ScoreCard from './ScoreCard';
export default function ChecklistGroup({ export default function ChecklistGroup({
title, title,
@ -23,37 +24,29 @@ export default function ChecklistGroup({
const slug = parameterize(title); const slug = parameterize(title);
return ( return (
<div> <ScoreCard title={<a
<a id={slug}
id={slug} href={`#${slug}`}
href={`#${slug}`} className={clsx(
className={clsx( 'inline-flex items-center',
'inline-flex items-center', 'text-gray-600 dark:text-gray-300',
'text-gray-600 dark:text-gray-300', 'sm:pl-1.5',
'pl-[18px] py-3 text-lg', )}
)} >
> <span className="w-8 sm:w-9 shrink-0">{icon}</span>
<span className="w-7 shrink-0">{icon}</span> <span className="inline-flex flex-wrap items-center gap-y-1 gap-x-1.5">
<span className="inline-flex flex-wrap items-center gap-y-1 gap-x-1.5"> <ResponsiveText shortText={titleShort}>
<ResponsiveText shortText={titleShort}> {title}
{title} </ResponsiveText>
</ResponsiveText> {optional &&
{optional && <Badge type="small" className="translate-y-[0.5px]">
<Badge type="small" className="translate-y-[0.5px]"> Optional
Optional </Badge>}
</Badge>} {experimental &&
{experimental && <ExperimentalBadge className="translate-y-[0.5px]" />}
<ExperimentalBadge className="translate-y-[0.5px]" />} </span>
</span> </a>}>
</a> {children}
<div className={clsx( </ScoreCard>
'bg-white dark:bg-black',
'dark:text-gray-400',
'border border-gray-200 dark:border-gray-800 rounded-md',
'divide-y divide-gray-200 dark:divide-gray-800',
)}>
{children}
</div>
</div>
); );
} }

View File

@ -2,6 +2,7 @@ import { ReactNode } from 'react';
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';
import StatusIcon from './StatusIcon'; import StatusIcon from './StatusIcon';
import ExperimentalBadge from './ExperimentalBadge'; import ExperimentalBadge from './ExperimentalBadge';
import ScoreCardRow from './ScoreCardRow';
export default function ChecklistRow({ export default function ChecklistRow({
title, title,
@ -21,31 +22,28 @@ export default function ChecklistRow({
children: ReactNode children: ReactNode
}) { }) {
return ( return (
<div className={clsx( <ScoreCardRow
'flex gap-2.5', icon={<StatusIcon
'px-4 pt-2 pb-2.5',
)}>
<StatusIcon
type={status type={status
? 'checked' ? 'checked'
: showWarning : showWarning
? 'warning' ? 'warning'
: optional ? 'optional' : 'missing'} : optional ? 'optional' : 'missing'}
loading={isPending} loading={isPending}
/> />}
<div className="flex flex-col min-w-0 grow"> content={<>
<div className={clsx( <div className={clsx(
'flex flex-wrap items-center gap-2 pb-0.5', 'flex flex-wrap items-center gap-2 pb-0.5',
'font-bold dark:text-gray-300', 'font-bold text-main',
)}> )}>
{title} {title}
{experimental && {experimental &&
<ExperimentalBadge className="translate-y-[-0.5px]" />} <ExperimentalBadge className="translate-y-[-0.5px]" />}
</div> </div>
<div className="leading-relaxed"> <div className="leading-relaxed text-medium">
{children} {children}
</div> </div>
</div> </>}
</div> />
); );
} }

View File

@ -6,15 +6,15 @@ export default function ScoreCard({
children, children,
className, className,
}: { }: {
title?: string, title?: ReactNode,
children: ReactNode, children: ReactNode,
className?: string, className?: string,
}) { }) {
return ( return (
<div className="space-y-3"> <div className="space-y-2">
{title && {title &&
<div className={clsx( <div className={clsx(
'pl-[15px]', 'pl-[15px] h-6',
'uppercase font-medium tracking-wider text-[0.8rem]', 'uppercase font-medium tracking-wider text-[0.8rem]',
'text-medium', 'text-medium',
)}> )}>

View File

@ -44,7 +44,7 @@ export default function ScoreCardRow({
<div className="grow space-y-2 py-1.5 w-full overflow-auto"> <div className="grow space-y-2 py-1.5 w-full overflow-auto">
<div className={clsx( <div className={clsx(
'text-main pr-2', 'text-main pr-2',
!isExpanded && 'max-w-full truncate', expandContent && !isExpanded && 'max-w-full truncate',
)}> )}>
{typeof content === 'function' {typeof content === 'function'
? content(isExpanded) ? content(isExpanded)

View File

@ -4,13 +4,16 @@ import {
BiSolidXSquare, BiSolidXSquare,
} from 'react-icons/bi'; } from 'react-icons/bi';
import Spinner from './Spinner'; import Spinner from './Spinner';
import clsx from 'clsx/lite';
export default function StatusIcon({ export default function StatusIcon({
type, type,
loading, loading,
className,
}: { }: {
type: 'checked' | 'missing' | 'warning' | 'optional' type: 'checked' | 'missing' | 'warning' | 'optional'
loading?: boolean loading?: boolean
className?: string
}) { }) {
const getIcon = () => { const getIcon = () => {
switch (type) { switch (type) {
@ -21,13 +24,13 @@ export default function StatusIcon({
/>; />;
case 'missing': case 'missing':
return <BiSolidXSquare return <BiSolidXSquare
size={14} size={14.5}
className="text-red-400 translate-x-[2px] translate-y-[1.5px]" className="text-red-400"
/>; />;
case 'warning': case 'warning':
return <BiSolidXSquare return <BiSolidXSquare
size={14} size={14.5}
className="text-amber-500 translate-x-[2px] translate-y-[1.5px]" className="text-amber-500"
/>; />;
case 'optional': case 'optional':
return <BiSolidCheckboxMinus return <BiSolidCheckboxMinus
@ -38,12 +41,18 @@ export default function StatusIcon({
}; };
return ( return (
<div className="min-w-[1.2rem] pt-[1px]"> <span className={clsx(
'size-[16px]',
'inline-flex items-center justify-center',
className,
)}>
{loading {loading
? <div className="translate-y-0.5"> ? <span className="translate-y-[1px]">
<Spinner size={14} /> <Spinner size={12} />
</div> </span>
: getIcon()} : <span>
</div> {getIcon()}
</span>}
</span>
); );
} }

View File

@ -13,6 +13,7 @@ import {
} from 'react'; } from 'react';
import { import {
PATH_ADMIN_BASELINE, PATH_ADMIN_BASELINE,
PATH_ADMIN_COMPONENTS,
PATH_ADMIN_CONFIGURATION, PATH_ADMIN_CONFIGURATION,
PATH_ADMIN_INSIGHTS, PATH_ADMIN_INSIGHTS,
PATH_ADMIN_PHOTOS, PATH_ADMIN_PHOTOS,
@ -376,6 +377,9 @@ export default function CommandKClient({
? [{ ? [{
label: 'Baseline Overview', label: 'Baseline Overview',
path: PATH_ADMIN_BASELINE, path: PATH_ADMIN_BASELINE,
}, {
label: 'Components Overview',
path: PATH_ADMIN_COMPONENTS,
}] }]
: []) : [])
.concat({ .concat({