616 lines
20 KiB
TypeScript
616 lines
20 KiB
TypeScript
'use client';
|
|
|
|
import {
|
|
ComponentProps,
|
|
ReactNode,
|
|
} from 'react';
|
|
import { clsx } from 'clsx/lite';
|
|
import ChecklistRow from '../components/ChecklistRow';
|
|
import { FiExternalLink } from 'react-icons/fi';
|
|
import {
|
|
BiCog,
|
|
BiCopy,
|
|
BiData,
|
|
BiLockAlt,
|
|
BiPencil,
|
|
} from 'react-icons/bi';
|
|
import Container from '@/components/Container';
|
|
import Checklist from '@/components/Checklist';
|
|
import { toastSuccess } from '@/toast';
|
|
import { ConfigChecklistStatus } from './config';
|
|
import StatusIcon from '@/components/StatusIcon';
|
|
import { labelForStorage } from '@/services/storage';
|
|
import { HiSparkles } from 'react-icons/hi';
|
|
import LoaderButton from '@/components/primitives/LoaderButton';
|
|
import { testConnectionsAction } from '@/admin/actions';
|
|
import ErrorNote from '@/components/ErrorNote';
|
|
import Spinner from '@/components/Spinner';
|
|
import WarningNote from '@/components/WarningNote';
|
|
|
|
export default function SiteChecklistClient({
|
|
// Config checklist
|
|
hasDatabase,
|
|
isPostgresSslEnabled,
|
|
hasVercelPostgres,
|
|
hasVercelKv,
|
|
hasStorageProvider,
|
|
hasVercelBlobStorage,
|
|
hasCloudflareR2Storage,
|
|
hasAwsS3Storage,
|
|
hasMultipleStorageProviders,
|
|
currentStorage,
|
|
hasAuthSecret,
|
|
hasAdminUser,
|
|
hasDomain,
|
|
hasTitle,
|
|
hasDescription,
|
|
hasAbout,
|
|
hasDefaultTheme,
|
|
showRepoLink,
|
|
showSocial,
|
|
showFilmSimulations,
|
|
showExifInfo,
|
|
defaultTheme,
|
|
isProModeEnabled,
|
|
isGridHomepageEnabled: isGridFirst,
|
|
isStaticallyOptimized,
|
|
arePagesStaticallyOptimized,
|
|
areOGImagesStaticallyOptimized,
|
|
arePhotosMatted,
|
|
isBlurEnabled,
|
|
isGeoPrivacyEnabled,
|
|
showPhotoTitleFallbackText,
|
|
isPriorityOrderEnabled,
|
|
isAiTextGenerationEnabled,
|
|
aiTextAutoGeneratedFields,
|
|
hasAiTextAutoGeneratedFields,
|
|
isPublicApiEnabled,
|
|
isOgTextBottomAligned,
|
|
gridAspectRatio,
|
|
hasGridAspectRatio,
|
|
// Connection status
|
|
databaseError,
|
|
storageError,
|
|
kvError,
|
|
aiError,
|
|
// Component props
|
|
simplifiedView,
|
|
isTestingConnections,
|
|
secret,
|
|
baseUrl,
|
|
commitSha,
|
|
}: ConfigChecklistStatus &
|
|
Partial<Awaited<ReturnType<typeof testConnectionsAction>>> & {
|
|
simplifiedView?: boolean
|
|
isTestingConnections?: boolean
|
|
secret?: string
|
|
}) {
|
|
const renderLink = (href: string, text: string, external = true) =>
|
|
<>
|
|
<a {...{
|
|
href,
|
|
...external && { target: '_blank', rel: 'noopener noreferrer' },
|
|
className: clsx(
|
|
'underline hover:no-underline',
|
|
),
|
|
}}>
|
|
{text}
|
|
</a>
|
|
{external &&
|
|
<>
|
|
|
|
<FiExternalLink
|
|
size={14}
|
|
className='inline translate-y-[-1.5px]'
|
|
/>
|
|
</>}
|
|
</>;
|
|
|
|
const renderCopyButton = (label: string, text?: string, subtle?: boolean) =>
|
|
<LoaderButton
|
|
icon={<BiCopy size={15} />}
|
|
className={clsx(
|
|
'translate-y-[2px]',
|
|
subtle && 'text-gray-300 dark:text-gray-700',
|
|
)}
|
|
onClick={text
|
|
? () => {
|
|
navigator.clipboard.writeText(text);
|
|
toastSuccess(`${label} copied to clipboard`);
|
|
}
|
|
: undefined}
|
|
styleAs="link"
|
|
disabled={!text}
|
|
/>;
|
|
|
|
const renderEnvVar = (
|
|
variable: string,
|
|
minimal?: boolean,
|
|
) =>
|
|
<div
|
|
key={variable}
|
|
className={clsx(
|
|
'overflow-x-auto overflow-y-hidden',
|
|
minimal && 'inline-flex',
|
|
)}
|
|
>
|
|
<span className="inline-flex items-center gap-1">
|
|
<span className={clsx(
|
|
'text-[11px] font-medium tracking-wider',
|
|
'px-0.5 py-[0.5px]',
|
|
'rounded-[5px]',
|
|
'bg-gray-100 dark:bg-gray-800',
|
|
)}>
|
|
`{variable}`
|
|
</span>
|
|
{!minimal && renderCopyButton(variable, variable, true)}
|
|
</span>
|
|
</div>;
|
|
|
|
const renderEnvVars = (variables: string[]) =>
|
|
<div className="pt-1 space-y-1">
|
|
{variables.map(envVar => renderEnvVar(envVar))}
|
|
</div>;
|
|
|
|
const renderSubStatus = (
|
|
type: ComponentProps<typeof StatusIcon>['type'],
|
|
label: ReactNode,
|
|
iconClassName?: string,
|
|
) =>
|
|
<div className="flex gap-2 translate-x-[-3px]">
|
|
<span className={iconClassName}>
|
|
<StatusIcon {...{ type }} />
|
|
</span>
|
|
<span className="min-w-0">
|
|
{label}
|
|
</span>
|
|
</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 renderWarning = ({
|
|
connection,
|
|
message,
|
|
}: {
|
|
connection?: { provider: string, error: string }
|
|
message?: string
|
|
}) =>
|
|
<WarningNote className="mt-2 mb-3">
|
|
{connection && <>
|
|
{connection.provider} connection error: {`"${connection.error}"`}
|
|
</>}
|
|
{message}
|
|
</WarningNote>;
|
|
|
|
return (
|
|
<div className="max-w-xl w-full">
|
|
<div className="space-y-6">
|
|
<Checklist
|
|
title="Storage"
|
|
icon={<BiData size={16} />}
|
|
>
|
|
<ChecklistRow
|
|
title={hasDatabase && isTestingConnections
|
|
? 'Testing database connection'
|
|
: 'Setup database'}
|
|
status={hasDatabase}
|
|
isPending={hasDatabase && isTestingConnections}
|
|
>
|
|
{databaseError && renderError({
|
|
connection: { provider: 'Database', error: databaseError},
|
|
})}
|
|
{hasVercelPostgres
|
|
? renderSubStatus('checked', 'Vercel Postgres: connected')
|
|
: renderSubStatus('optional', <>
|
|
Vercel Postgres:
|
|
{' '}
|
|
{renderLink(
|
|
// eslint-disable-next-line max-len
|
|
'https://vercel.com/docs/storage/vercel-postgres/quickstart#create-a-postgres-database',
|
|
'create store',
|
|
)}
|
|
{' '}
|
|
and connect to project
|
|
</>)}
|
|
{hasDatabase && !hasVercelPostgres &&
|
|
renderSubStatus('checked', <>
|
|
Postgres-compatible: connected
|
|
{' '}
|
|
(SSL {isPostgresSslEnabled ? 'enabled' : 'disabled'})
|
|
</>)}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title={
|
|
hasStorageProvider && isTestingConnections
|
|
? '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 && isTestingConnections}
|
|
>
|
|
{storageError && renderError({
|
|
connection: { provider: 'Storage', error: storageError},
|
|
})}
|
|
{hasVercelBlobStorage
|
|
? renderSubStatus('checked', 'Vercel Blob: connected')
|
|
: renderSubStatus('optional', <>
|
|
{labelForStorage('vercel-blob')}:
|
|
{' '}
|
|
{renderLink(
|
|
// eslint-disable-next-line max-len
|
|
'https://vercel.com/docs/storage/vercel-blob/quickstart#create-a-blob-store',
|
|
'create store',
|
|
)}
|
|
{' '}
|
|
and connect to project
|
|
</>
|
|
)}
|
|
{hasCloudflareR2Storage
|
|
? renderSubStatus('checked', 'Cloudflare R2: connected')
|
|
: renderSubStatus('optional', <>
|
|
{labelForStorage('cloudflare-r2')}:
|
|
{' '}
|
|
{renderLink(
|
|
'https://github.com/sambecker/exif-photo-blog#cloudflare-r2',
|
|
'create/configure bucket',
|
|
)}
|
|
</>)}
|
|
{hasAwsS3Storage
|
|
? renderSubStatus('checked', 'AWS S3: connected')
|
|
: renderSubStatus('optional', <>
|
|
{labelForStorage('aws-s3')}:
|
|
{' '}
|
|
{renderLink(
|
|
'https://github.com/sambecker/exif-photo-blog#aws-s3',
|
|
'create/configure bucket',
|
|
)}
|
|
</>)}
|
|
</ChecklistRow>
|
|
</Checklist>
|
|
<Checklist
|
|
title="Authentication"
|
|
icon={<BiLockAlt size={16} />}
|
|
>
|
|
<ChecklistRow
|
|
title={!hasAuthSecret && isTestingConnections
|
|
? 'Generating secret'
|
|
: 'Setup auth'}
|
|
status={hasAuthSecret}
|
|
isPending={!hasAuthSecret && isTestingConnections}
|
|
>
|
|
Store auth secret in environment variable:
|
|
{!hasAuthSecret &&
|
|
<div className="overflow-x-auto">
|
|
<Container className="my-1.5 inline-flex" padding="tight">
|
|
<div className={clsx(
|
|
'flex flex-nowrap items-center gap-2 leading-none -mx-1',
|
|
)}>
|
|
{secret ? <span>{secret}</span> : <Spinner />}
|
|
<div
|
|
className="flex items-center gap-0.5 translate-y-[-2px]"
|
|
>
|
|
{renderCopyButton('Secret', secret)}
|
|
</div>
|
|
</div>
|
|
</Container>
|
|
</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>
|
|
</Checklist>
|
|
<Checklist
|
|
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>
|
|
</Checklist>
|
|
{!simplifiedView && <>
|
|
<Checklist
|
|
title="AI text generation"
|
|
titleShort="AI"
|
|
icon={<HiSparkles />}
|
|
experimental
|
|
optional
|
|
>
|
|
<ChecklistRow
|
|
title={isAiTextGenerationEnabled && isTestingConnections
|
|
? 'Testing OpenAI connection'
|
|
: 'Add OpenAI secret key'}
|
|
status={isAiTextGenerationEnabled}
|
|
isPending={isAiTextGenerationEnabled && isTestingConnections}
|
|
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={hasVercelKv && isTestingConnections
|
|
? 'Testing KV connection'
|
|
: 'Enable rate limiting'}
|
|
status={hasVercelKv}
|
|
isPending={hasVercelKv && isTestingConnections}
|
|
optional
|
|
>
|
|
{kvError && renderError({
|
|
connection: { provider: 'Vercel KV', error: kvError},
|
|
})}
|
|
{renderLink(
|
|
// eslint-disable-next-line max-len
|
|
'https://vercel.com/docs/storage/vercel-kv/quickstart#create-a-kv-database',
|
|
'Create Vercel KV store',
|
|
)}
|
|
{' '}
|
|
and connect to project in order 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 is {'"all"'}):
|
|
{renderEnvVars(['AI_TEXT_AUTO_GENERATED_FIELDS'])}
|
|
</ChecklistRow>
|
|
</Checklist>
|
|
<Checklist
|
|
title="Settings"
|
|
icon={<BiCog size={16} />}
|
|
optional
|
|
>
|
|
<ChecklistRow
|
|
title="Grid homepage"
|
|
status={isGridFirst}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to show grid layout
|
|
on homepage:
|
|
{renderEnvVars(['NEXT_PUBLIC_GRID_HOMEPAGE'])}
|
|
</ChecklistRow>
|
|
<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="Pro mode"
|
|
status={isProModeEnabled}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to enable
|
|
higher quality image storage:
|
|
{renderEnvVars(['NEXT_PUBLIC_PRO_MODE'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Static optimization"
|
|
status={isStaticallyOptimized}
|
|
optional
|
|
experimental
|
|
>
|
|
Set environment variable to {'"1"'} to enable static optimization,
|
|
i.e., rendering pages and images at build time:
|
|
{renderSubStatus(
|
|
arePagesStaticallyOptimized ? 'checked' : 'optional',
|
|
renderEnvVars(['NEXT_PUBLIC_STATICALLY_OPTIMIZE_PAGES']),
|
|
'translate-y-[3.5px]',
|
|
)}
|
|
{renderSubStatus(
|
|
areOGImagesStaticallyOptimized ? 'checked' : 'optional',
|
|
renderEnvVars(['NEXT_PUBLIC_STATICALLY_OPTIMIZE_OG_IMAGES']),
|
|
'translate-y-[3.5px]',
|
|
)}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Photo matting"
|
|
status={arePhotosMatted}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to constrain the size
|
|
{' '}
|
|
of each photo, and enable a surrounding border:
|
|
{renderEnvVars(['NEXT_PUBLIC_MATTE_PHOTOS'])}
|
|
</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>
|
|
<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="Show photo title fallback text"
|
|
status={showPhotoTitleFallbackText}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to prevent
|
|
showing {'"Untitled"'} for photos without titles:
|
|
{renderEnvVars(['NEXT_PUBLIC_HIDE_TITLE_FALLBACK_TEXT'])}
|
|
</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="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="Show repo link"
|
|
status={showRepoLink}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to hide footer link:
|
|
{renderEnvVars(['NEXT_PUBLIC_HIDE_REPO_LINK'])}
|
|
</ChecklistRow>
|
|
<ChecklistRow
|
|
title="Show social"
|
|
status={showSocial}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to hide
|
|
{' '}
|
|
X 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 EXIF data"
|
|
status={showExifInfo}
|
|
optional
|
|
>
|
|
Set environment variable to {'"1"'} to hide EXIF data:
|
|
{renderEnvVars(['NEXT_PUBLIC_HIDE_EXIF_DATA'])}
|
|
</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="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>
|
|
</Checklist>
|
|
</>}
|
|
</div>
|
|
<div className="pl-11 pr-2 sm:pr-11 mt-4 md:mt-7">
|
|
<div>
|
|
Changes to environment variables require a redeploy
|
|
or reboot of local dev server
|
|
</div>
|
|
{!simplifiedView &&
|
|
<div className="text-dim before:content-['—']">
|
|
<div className="flex whitespace-nowrap">
|
|
<span className="font-bold">Domain</span>
|
|
|
|
<span className="w-full flex overflow-x-auto">
|
|
{baseUrl || 'Not Defined'}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className="font-bold">Commit</span>
|
|
|
|
{commitSha || 'Not Found'}
|
|
</div>
|
|
</div>}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|