Vercel/src/site/SiteChecklistClient.tsx
2024-05-09 19:09:05 -05:00

496 lines
16 KiB
TypeScript

'use client';
import { ComponentProps, ReactNode, useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { clsx } from 'clsx/lite';
import ChecklistRow from '../components/ChecklistRow';
import { FiExternalLink } from 'react-icons/fi';
import {
BiCog,
BiCopy,
BiData,
BiLockAlt,
BiPencil,
BiRefresh,
} from 'react-icons/bi';
import InfoBlock from '@/components/InfoBlock';
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';
export default function SiteChecklistClient({
hasDatabase,
isPostgresSSLEnabled,
hasVercelPostgres,
hasVercelKV,
hasStorageProvider,
hasVercelBlobStorage,
hasCloudflareR2Storage,
hasAwsS3Storage,
hasMultipleStorageProviders,
currentStorage,
hasAuthSecret,
hasAdminUser,
hasTitle,
hasDomain,
showRepoLink,
showFilmSimulations,
showExifInfo,
isProModeEnabled,
isStaticallyOptimized,
arePagesStaticallyOptimized,
areOGImagesStaticallyOptimized,
arePhotosMatted,
isBlurEnabled,
isGeoPrivacyEnabled,
isPriorityOrderEnabled,
isAiTextGenerationEnabled,
aiTextAutoGeneratedFields,
hasAiTextAutoGeneratedFields,
isPublicApiEnabled,
isOgTextBottomAligned,
gridAspectRatio,
hasGridAspectRatio,
simplifiedView,
showRefreshButton,
secret,
}: ConfigChecklistStatus & {
simplifiedView?: boolean
showRefreshButton?: boolean
secret: string
}) {
const router = useRouter();
const [isPendingPage, startTransitionPage] = useTransition();
const [isPendingSecret, startTransitionSecret] = useTransition();
const refreshPage = () => {
startTransitionPage(router.refresh);
};
const refreshSecret = () => {
startTransitionSecret(router.refresh);
};
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 &&
<>
&nbsp;
<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={() => {
navigator.clipboard.writeText(text);
toastSuccess(`${label} copied to clipboard`);
}}
styleAsLink
/>;
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-wide',
'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="py-0.5">
{variables.map(envVar => renderEnvVar(envVar))}
</div>;
const renderSubStatus = (
type: ComponentProps<typeof StatusIcon>['type'],
label: ReactNode,
iconClassName?: string,
) =>
<div className="flex gap-1 -translate-x-1">
<span className={iconClassName}>
<StatusIcon {...{ type }} />
</span>
<span className="min-w-0">
{label}
</span>
</div>;
return (
<div className="max-w-xl space-y-6 w-full">
<Checklist
title="Storage"
icon={<BiData size={16} />}
>
<ChecklistRow
title="Setup database"
status={hasDatabase}
isPending={isPendingPage}
>
{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
? '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={isPendingPage}
>
{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="Setup auth"
status={hasAuthSecret}
isPending={isPendingPage}
>
Store auth secret in environment variable:
{!hasAuthSecret &&
<div className="overflow-x-auto">
<InfoBlock className="my-1.5 inline-flex" padding="tight">
<div className="flex flex-nowrap items-center gap-4">
<span>{secret}</span>
<div className="flex items-center gap-0.5">
{renderCopyButton('Secret', secret)}
<LoaderButton
icon={<BiRefresh size={18} />}
onClick={refreshSecret}
isLoading={isPendingSecret}
spinnerColor="text"
styleAsLink
/>
</div>
</div>
</InfoBlock>
</div>}
{renderEnvVars(['AUTH_SECRET'])}
</ChecklistRow>
<ChecklistRow
title="Setup admin user"
status={hasAdminUser}
isPending={isPendingPage}
>
Store admin email/password
{' '}
in environment variables:
{renderEnvVars([
'ADMIN_EMAIL',
'ADMIN_PASSWORD',
])}
</ChecklistRow>
</Checklist>
<Checklist
title="Content"
icon={<BiPencil size={16} />}
optional
>
<ChecklistRow
title="Add title"
status={hasTitle}
isPending={isPendingPage}
optional
>
Store in environment variable (used in page titles):
{renderEnvVars(['NEXT_PUBLIC_SITE_TITLE'])}
</ChecklistRow>
<ChecklistRow
title="Add custom domain"
status={hasDomain}
isPending={isPendingPage}
optional
>
Store in environment variable (displayed in top-right nav):
{renderEnvVars(['NEXT_PUBLIC_SITE_DOMAIN'])}
</ChecklistRow>
</Checklist>
{!simplifiedView && <>
<Checklist
title="AI Text Generation"
icon={<HiSparkles />}
experimental
optional
>
<ChecklistRow
title="Add OpenAI Secret Key"
status={isAiTextGenerationEnabled}
isPending={isPendingPage}
optional
>
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="Enable Rate Limiting"
status={hasVercelKV}
isPending={isPendingPage}
optional
>
{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}
isPending={isPendingPage}
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="Pro mode"
status={isProModeEnabled}
isPending={isPendingPage}
optional
>
Set environment variable to {'"1"'} to enable
higher quality image storage:
{renderEnvVars(['NEXT_PUBLIC_PRO_MODE'])}
</ChecklistRow>
<ChecklistRow
title="Static Optimization"
status={isStaticallyOptimized}
isPending={isPendingPage}
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}
isPending={isPendingPage}
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}
isPending={isPendingPage}
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}
isPending={isPendingPage}
optional
>
Set environment variable to {'"1"'} to disable
collection/display of location-based data
{renderEnvVars(['NEXT_PUBLIC_GEO_PRIVACY'])}
</ChecklistRow>
<ChecklistRow
title="Priority order"
status={isPriorityOrderEnabled}
isPending={isPendingPage}
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}
isPending={isPendingPage}
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}
isPending={isPendingPage}
optional
>
Set environment variable to {'"1"'} to hide footer link:
{renderEnvVars(['NEXT_PUBLIC_HIDE_REPO_LINK'])}
</ChecklistRow>
<ChecklistRow
title="Show Fujifilm simulations"
status={showFilmSimulations}
isPending={isPendingPage}
optional
>
Set environment variable to {'"1"'} to prevent
simulations showing up in <code>/grid</code> sidebar:
{renderEnvVars(['NEXT_PUBLIC_HIDE_FILM_SIMULATIONS'])}
</ChecklistRow>
<ChecklistRow
title="Show EXIF data"
status={showExifInfo}
isPending={isPendingPage}
optional
>
Set environment variable to {'"1"'} to hide EXIF data:
{renderEnvVars(['NEXT_PUBLIC_HIDE_EXIF_DATA'])}
</ChecklistRow>
<ChecklistRow
title={`Grid aspect ratio: ${gridAspectRatio}`}
status={hasGridAspectRatio}
isPending={isPendingPage}
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}
isPending={isPendingPage}
optional
>
Set environment variable to {'"BOTTOM"'} to
keep OG image text bottom aligned (default is {'"top"'}):
{renderEnvVars(['NEXT_PUBLIC_OG_TEXT_ALIGNMENT'])}
</ChecklistRow>
</Checklist>
</>}
{showRefreshButton &&
<div className="py-4 space-y-4">
<button onClick={refreshPage}>
Check
</button>
</div>}
<div className="px-11 text-dim">
Changes to environment variables require a redeploy
or reboot of local dev server
</div>
</div>
);
}