Refactor site checklist, add secret generator
This commit is contained in:
parent
ed019be284
commit
33ec20d709
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -9,6 +9,7 @@
|
|||||||
"nextjs",
|
"nextjs",
|
||||||
"qaub",
|
"qaub",
|
||||||
"skippable",
|
"skippable",
|
||||||
|
"sonner",
|
||||||
"thephotoblog",
|
"thephotoblog",
|
||||||
"trpc",
|
"trpc",
|
||||||
"WRHGZC",
|
"WRHGZC",
|
||||||
|
|||||||
@ -33,6 +33,7 @@
|
|||||||
"react-icons": "^4.10.1",
|
"react-icons": "^4.10.1",
|
||||||
"react-spinners": "^0.13.8",
|
"react-spinners": "^0.13.8",
|
||||||
"short-uuid": "^4.2.2",
|
"short-uuid": "^4.2.2",
|
||||||
|
"sonner": "^0.7.0",
|
||||||
"tailwindcss": "3.3.3",
|
"tailwindcss": "3.3.3",
|
||||||
"ts-exif-parser": "^0.2.2",
|
"ts-exif-parser": "^0.2.2",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.2.2"
|
||||||
|
|||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@ -29,6 +29,7 @@ specifiers:
|
|||||||
react-icons: ^4.10.1
|
react-icons: ^4.10.1
|
||||||
react-spinners: ^0.13.8
|
react-spinners: ^0.13.8
|
||||||
short-uuid: ^4.2.2
|
short-uuid: ^4.2.2
|
||||||
|
sonner: ^0.7.0
|
||||||
tailwindcss: 3.3.3
|
tailwindcss: 3.3.3
|
||||||
ts-exif-parser: ^0.2.2
|
ts-exif-parser: ^0.2.2
|
||||||
typescript: 5.2.2
|
typescript: 5.2.2
|
||||||
@ -59,6 +60,7 @@ dependencies:
|
|||||||
react-icons: 4.10.1_react@18.2.0
|
react-icons: 4.10.1_react@18.2.0
|
||||||
react-spinners: 0.13.8_biqbaboplfbrettd7655fr4n2y
|
react-spinners: 0.13.8_biqbaboplfbrettd7655fr4n2y
|
||||||
short-uuid: 4.2.2
|
short-uuid: 4.2.2
|
||||||
|
sonner: 0.7.0_biqbaboplfbrettd7655fr4n2y
|
||||||
tailwindcss: 3.3.3
|
tailwindcss: 3.3.3
|
||||||
ts-exif-parser: 0.2.2
|
ts-exif-parser: 0.2.2
|
||||||
typescript: 5.2.2
|
typescript: 5.2.2
|
||||||
@ -3189,6 +3191,16 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/sonner/0.7.0_biqbaboplfbrettd7655fr4n2y:
|
||||||
|
resolution: {integrity: sha512-vAlXCrE6/183yt64ktIUnPv85RmAPYiicl5z35fDDFhWRIUpg7N62TsiIbHjwGuxbVJu/5hYlh92HHImsS27dA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18.0.0
|
||||||
|
react-dom: ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0_react@18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/source-map-js/1.0.2:
|
/source-map-js/1.0.2:
|
||||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import InfoBlock from '@/components/InfoBlock';
|
import InfoBlock from '@/components/InfoBlock';
|
||||||
import SiteGrid from '@/components/SiteGrid';
|
import SiteGrid from '@/components/SiteGrid';
|
||||||
import { SITE_CHECKLIST_STATUS } from '@/site';
|
|
||||||
import SiteChecklist from '@/site/SiteChecklist';
|
import SiteChecklist from '@/site/SiteChecklist';
|
||||||
|
|
||||||
export default function ChecklistPage() {
|
export default async function ChecklistPage() {
|
||||||
return (
|
return (
|
||||||
<SiteGrid
|
<SiteGrid
|
||||||
contentMain={<InfoBlock>
|
contentMain={<InfoBlock>
|
||||||
<SiteChecklist {...SITE_CHECKLIST_STATUS} />
|
<SiteChecklist />
|
||||||
</InfoBlock>}
|
</InfoBlock>}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { BASE_URL, SITE_DESCRIPTION, SITE_TITLE } from '@/site/config';
|
|||||||
import StateProvider from '@/state/AppStateProvider';
|
import StateProvider from '@/state/AppStateProvider';
|
||||||
import ThemeProviderClient from '@/site/ThemeProviderClient';
|
import ThemeProviderClient from '@/site/ThemeProviderClient';
|
||||||
import Nav from '@/components/Nav';
|
import Nav from '@/components/Nav';
|
||||||
|
import ToasterClient from '@/components/ToasterClient';
|
||||||
|
|
||||||
import '../site/globals.css';
|
import '../site/globals.css';
|
||||||
|
|
||||||
@ -76,6 +77,7 @@ export default function RootLayout({
|
|||||||
</StateProvider>
|
</StateProvider>
|
||||||
<Analytics />
|
<Analytics />
|
||||||
</main>
|
</main>
|
||||||
|
<ToasterClient />
|
||||||
</ThemeProviderClient>
|
</ThemeProviderClient>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -10,37 +10,36 @@ export default function SignInForm() {
|
|||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InfoBlock
|
<InfoBlock>
|
||||||
className="space-y-8"
|
<div className="space-y-8">
|
||||||
padding="normal"
|
<div className="space-y-4">
|
||||||
>
|
<FieldSet
|
||||||
<div className="space-y-4">
|
id="email"
|
||||||
<FieldSet
|
label="Admin Email"
|
||||||
id="email"
|
value={email}
|
||||||
label="Admin Email"
|
onChange={setEmail}
|
||||||
value={email}
|
/>
|
||||||
onChange={setEmail}
|
<FieldSet
|
||||||
/>
|
id="password"
|
||||||
<FieldSet
|
label="Admin Password"
|
||||||
id="password"
|
value={password}
|
||||||
label="Admin Password"
|
onChange={setPassword}
|
||||||
value={password}
|
type="password"
|
||||||
onChange={setPassword}
|
/>
|
||||||
type="password"
|
</div>
|
||||||
/>
|
<button
|
||||||
|
onClick={() => signIn(
|
||||||
|
'credentials',
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
callbackUrl: '/admin/photos',
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Sign in
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={() => signIn(
|
|
||||||
'credentials',
|
|
||||||
{
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
callbackUrl: '/admin/photos',
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Sign in
|
|
||||||
</button>
|
|
||||||
</InfoBlock>
|
</InfoBlock>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,3 +55,8 @@ export const {
|
|||||||
signIn: '/sign-in',
|
signIn: '/sign-in',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const generateAuthSecret = () => fetch(
|
||||||
|
'https://generate-secret.vercel.app/32',
|
||||||
|
{ cache: 'no-cache' },
|
||||||
|
).then(res => res.text());
|
||||||
|
|||||||
@ -4,27 +4,33 @@ import { ReactNode } from 'react';
|
|||||||
export default function InfoBlock({
|
export default function InfoBlock({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
padding = 'loose',
|
padding = 'normal',
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
padding?: 'loose' | 'normal';
|
padding?: 'loose' | 'normal' | 'tight';
|
||||||
} ) {
|
} ) {
|
||||||
|
const getPaddingClasses = () => {
|
||||||
|
switch (padding) {
|
||||||
|
case 'loose': return 'p-4 md:p-24';
|
||||||
|
case 'normal': return 'p-4 md:p-8';
|
||||||
|
case 'tight': return 'py-2 px-3';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cc(
|
<div className={cc(
|
||||||
'flex flex-col items-center justify-center',
|
'flex flex-col items-center justify-center',
|
||||||
'px-8 rounded-lg',
|
'rounded-lg border',
|
||||||
padding === 'loose' ? 'py-24' : 'py-8',
|
|
||||||
'border',
|
|
||||||
'bg-gray-50 border-gray-200',
|
'bg-gray-50 border-gray-200',
|
||||||
'dark:bg-gray-900/40 dark:border-gray-800',
|
'dark:bg-gray-900/40 dark:border-gray-800',
|
||||||
'text-center',
|
getPaddingClasses(),
|
||||||
|
className,
|
||||||
)}>
|
)}>
|
||||||
<div className={cc(
|
<div className={cc(
|
||||||
'flex flex-col items-center justify-center',
|
'flex flex-col items-center justify-center',
|
||||||
'space-y-4',
|
'space-y-4',
|
||||||
'text-gray-500 dark:text-gray-400',
|
'text-gray-500 dark:text-gray-400',
|
||||||
className,
|
|
||||||
)}>
|
)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
34
src/components/LoaderIcon.tsx
Normal file
34
src/components/LoaderIcon.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { cc } from '@/utility/css';
|
||||||
|
import Spinner from './Spinner';
|
||||||
|
|
||||||
|
export default function IconButton({
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
isLoading,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
onClick?: () => void
|
||||||
|
isLoading?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<span className="min-w-[1.1rem]">
|
||||||
|
{!isLoading
|
||||||
|
? <span
|
||||||
|
onClick={onClick}
|
||||||
|
className={cc(
|
||||||
|
onClick !== undefined && 'cursor-pointer',
|
||||||
|
'active:opacity-50',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
: <span className={cc(
|
||||||
|
'inline-block translate-x-[2px] translate-y-[1px]',
|
||||||
|
)}>
|
||||||
|
<Spinner size={12} />
|
||||||
|
</span>}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/components/ToasterClient.tsx
Normal file
11
src/components/ToasterClient.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
|
import { Toaster } from 'sonner';
|
||||||
|
|
||||||
|
export default function ToasterClient() {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
return (
|
||||||
|
<Toaster theme={theme as 'system' | 'light' | 'dark'} />
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import InfoBlock from '@/components/InfoBlock';
|
import InfoBlock from '@/components/InfoBlock';
|
||||||
import SiteGrid from '@/components/SiteGrid';
|
import SiteGrid from '@/components/SiteGrid';
|
||||||
import { SITE_CHECKLIST_STATUS } from '@/site';
|
import { CONFIG_CHECKLIST_STATUS } from '@/site/config';
|
||||||
import SiteChecklist from '@/site/SiteChecklist';
|
import SiteChecklist from '@/site/SiteChecklist';
|
||||||
import { cc } from '@/utility/css';
|
import { cc } from '@/utility/css';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { HiOutlinePhotograph } from 'react-icons/hi';
|
import { HiOutlinePhotograph } from 'react-icons/hi';
|
||||||
|
|
||||||
export default function PhotosEmptyState() {
|
export default function PhotosEmptyState() {
|
||||||
const showChecklist = Object.values(SITE_CHECKLIST_STATUS).some(v => !v);
|
const showChecklist = Object.values(CONFIG_CHECKLIST_STATUS).some(v => !v);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SiteGrid
|
<SiteGrid
|
||||||
@ -21,13 +21,11 @@ export default function PhotosEmptyState() {
|
|||||||
'font-bold text-2xl',
|
'font-bold text-2xl',
|
||||||
'text-gray-700 dark:text-gray-200',
|
'text-gray-700 dark:text-gray-200',
|
||||||
)}>
|
)}>
|
||||||
{showChecklist
|
{showChecklist ? 'Finish Setup' : 'Welcome!'}
|
||||||
? 'Finish Setup'
|
|
||||||
: 'Welcome!'}
|
|
||||||
</div>
|
</div>
|
||||||
{showChecklist
|
{showChecklist
|
||||||
? <SiteChecklist {...SITE_CHECKLIST_STATUS} />
|
? <SiteChecklist />
|
||||||
: <div className="max-w-md leading-[1.7]">
|
: <div className="max-w-md leading-[1.7] text-center">
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
1. Visit
|
1. Visit
|
||||||
{' '}
|
{' '}
|
||||||
|
|||||||
@ -1,144 +1,13 @@
|
|||||||
'use client';
|
import { generateAuthSecret } from '@/auth';
|
||||||
|
import SiteChecklistClient from './SiteChecklistClient';
|
||||||
import { useTransition } from 'react';
|
import { CONFIG_CHECKLIST_STATUS } from '@/site/config';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { cc } from '@/utility/css';
|
|
||||||
import SiteChecklistRow from './SiteChecklistRow';
|
|
||||||
import { FiExternalLink } from 'react-icons/fi';
|
|
||||||
|
|
||||||
export default function SiteChecklist({
|
|
||||||
hasTitle,
|
|
||||||
hasDomain,
|
|
||||||
hasPostgres,
|
|
||||||
hasBlob,
|
|
||||||
hasAuth,
|
|
||||||
showRefreshButton,
|
|
||||||
}: {
|
|
||||||
hasTitle: boolean
|
|
||||||
hasDomain: boolean
|
|
||||||
hasPostgres: boolean
|
|
||||||
hasBlob: boolean
|
|
||||||
hasAuth: boolean
|
|
||||||
showRefreshButton?: boolean
|
|
||||||
}) {
|
|
||||||
const router = useRouter();
|
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
const refreshSetupStatus = () => {
|
|
||||||
startTransition(router.refresh);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderLink = (href: string, text: string, external = true) =>
|
|
||||||
<>
|
|
||||||
<a {...{
|
|
||||||
href,
|
|
||||||
...external && { target: '_blank', rel: 'noopener noreferrer' },
|
|
||||||
className: cc(
|
|
||||||
'underline hover:no-underline',
|
|
||||||
),
|
|
||||||
}}>
|
|
||||||
{text}
|
|
||||||
</a>
|
|
||||||
{external &&
|
|
||||||
<>
|
|
||||||
|
|
||||||
<FiExternalLink
|
|
||||||
size={14}
|
|
||||||
className='inline translate-y-[-1.5px]'
|
|
||||||
/>
|
|
||||||
</>}
|
|
||||||
</>;
|
|
||||||
|
|
||||||
const renderEnvVar = (variables: string[]) =>
|
|
||||||
<div className="py-1 space-y-1">
|
|
||||||
{variables.map(variable =>
|
|
||||||
<div key={variable}>
|
|
||||||
<span className={cc(
|
|
||||||
'rounded-sm',
|
|
||||||
'bg-gray-100 text-gray-500',
|
|
||||||
'dark:bg-gray-800 dark:text-gray-400',
|
|
||||||
)}>
|
|
||||||
`{variable}`
|
|
||||||
</span>
|
|
||||||
</div>)}
|
|
||||||
</div>;
|
|
||||||
|
|
||||||
|
export default async function SiteChecklist() {
|
||||||
|
const secret = await generateAuthSecret();
|
||||||
return (
|
return (
|
||||||
<div className={cc(
|
<SiteChecklistClient {...{
|
||||||
'text-sm',
|
...CONFIG_CHECKLIST_STATUS,
|
||||||
'max-w-xl',
|
secret,
|
||||||
'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',
|
|
||||||
)}>
|
|
||||||
<SiteChecklistRow
|
|
||||||
title="Add title"
|
|
||||||
status={hasTitle}
|
|
||||||
isPending={isPending}
|
|
||||||
>
|
|
||||||
Store in environment variable:
|
|
||||||
{renderEnvVar(['NEXT_PUBLIC_SITE_TITLE'])}
|
|
||||||
</SiteChecklistRow>
|
|
||||||
<SiteChecklistRow
|
|
||||||
title="Add domain"
|
|
||||||
status={hasDomain}
|
|
||||||
isPending={isPending}
|
|
||||||
>
|
|
||||||
Store in environment variable:
|
|
||||||
{renderEnvVar(['NEXT_PUBLIC_SITE_DOMAIN'])}
|
|
||||||
</SiteChecklistRow>
|
|
||||||
<SiteChecklistRow
|
|
||||||
title="Setup database"
|
|
||||||
status={hasPostgres}
|
|
||||||
isPending={isPending}
|
|
||||||
>
|
|
||||||
{renderLink(
|
|
||||||
'https://vercel.com/docs/storage/vercel-postgres/quickstart',
|
|
||||||
'Create Vercel Postgres store',
|
|
||||||
)}
|
|
||||||
{' '}
|
|
||||||
and connect to project
|
|
||||||
</SiteChecklistRow>
|
|
||||||
<SiteChecklistRow
|
|
||||||
title="Setup blob store"
|
|
||||||
status={hasBlob}
|
|
||||||
isPending={isPending}
|
|
||||||
>
|
|
||||||
{renderLink(
|
|
||||||
'https://vercel.com/docs/storage/vercel-blob/quickstart',
|
|
||||||
'Create Vercel Blob store',
|
|
||||||
)}
|
|
||||||
{' '}
|
|
||||||
and connect to project
|
|
||||||
</SiteChecklistRow>
|
|
||||||
<SiteChecklistRow
|
|
||||||
title="Setup auth"
|
|
||||||
status={hasAuth}
|
|
||||||
isPending={isPending}
|
|
||||||
>
|
|
||||||
{renderLink(
|
|
||||||
'https://clerk.com/docs/quickstarts/setup-clerk',
|
|
||||||
'Create Clerk account',
|
|
||||||
)}
|
|
||||||
{' '}
|
|
||||||
and add environment variables:
|
|
||||||
{renderEnvVar([
|
|
||||||
'NEXT_PUBLIC_CLERK_SIGN_IN_URL',
|
|
||||||
'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY',
|
|
||||||
'CLERK_SECRET_KEY',
|
|
||||||
'CLERK_ADMIN_USER_ID',
|
|
||||||
])}
|
|
||||||
</SiteChecklistRow>
|
|
||||||
<div className="py-4 space-y-4">
|
|
||||||
<div className="px-8 text-gray-400">
|
|
||||||
Changes to environment variables require a redeploy
|
|
||||||
or reboot of local dev server
|
|
||||||
</div>
|
|
||||||
{showRefreshButton &&
|
|
||||||
<button onClick={refreshSetupStatus}>
|
|
||||||
Check
|
|
||||||
</button>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
186
src/site/SiteChecklistClient.tsx
Normal file
186
src/site/SiteChecklistClient.tsx
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useTransition } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { cc } from '@/utility/css';
|
||||||
|
import SiteChecklistRow from './SiteChecklistRow';
|
||||||
|
import { FiExternalLink } from 'react-icons/fi';
|
||||||
|
import { BiCopy, BiRefresh } from 'react-icons/bi';
|
||||||
|
import IconButton from '@/components/LoaderIcon';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import InfoBlock from '@/components/InfoBlock';
|
||||||
|
|
||||||
|
export default function SiteChecklistClient({
|
||||||
|
hasTitle,
|
||||||
|
hasDomain,
|
||||||
|
hasPostgres,
|
||||||
|
hasBlob,
|
||||||
|
hasAuth,
|
||||||
|
hasAdminUser,
|
||||||
|
showRefreshButton,
|
||||||
|
secret,
|
||||||
|
}: {
|
||||||
|
hasTitle: boolean
|
||||||
|
hasDomain: boolean
|
||||||
|
hasPostgres: boolean
|
||||||
|
hasBlob: boolean
|
||||||
|
hasAuth: boolean
|
||||||
|
hasAdminUser: 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: cc(
|
||||||
|
'underline hover:no-underline',
|
||||||
|
),
|
||||||
|
}}>
|
||||||
|
{text}
|
||||||
|
</a>
|
||||||
|
{external &&
|
||||||
|
<>
|
||||||
|
|
||||||
|
<FiExternalLink
|
||||||
|
size={14}
|
||||||
|
className='inline translate-y-[-1.5px]'
|
||||||
|
/>
|
||||||
|
</>}
|
||||||
|
</>;
|
||||||
|
|
||||||
|
const renderEnvVar = (variable: string) =>
|
||||||
|
<div key={variable}>
|
||||||
|
<span className={cc(
|
||||||
|
'rounded-sm',
|
||||||
|
'bg-gray-100 text-gray-500',
|
||||||
|
'dark:bg-gray-800 dark:text-gray-400',
|
||||||
|
)}>
|
||||||
|
`{variable}`
|
||||||
|
</span>
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
const renderEnvVars = (variables: string[]) =>
|
||||||
|
<div className="py-1 space-y-1">
|
||||||
|
{variables.map(renderEnvVar)}
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-sm max-w-xl space-y-4">
|
||||||
|
<div className={cc(
|
||||||
|
'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',
|
||||||
|
)}>
|
||||||
|
<SiteChecklistRow
|
||||||
|
title="Add title"
|
||||||
|
status={hasTitle}
|
||||||
|
isPending={isPendingPage}
|
||||||
|
>
|
||||||
|
Store in environment variable:
|
||||||
|
{renderEnvVars(['NEXT_PUBLIC_SITE_TITLE'])}
|
||||||
|
</SiteChecklistRow>
|
||||||
|
<SiteChecklistRow
|
||||||
|
title="Add domain"
|
||||||
|
status={hasDomain}
|
||||||
|
isPending={isPendingPage}
|
||||||
|
>
|
||||||
|
Store in environment variable:
|
||||||
|
{renderEnvVars(['NEXT_PUBLIC_SITE_DOMAIN'])}
|
||||||
|
</SiteChecklistRow>
|
||||||
|
<SiteChecklistRow
|
||||||
|
title="Setup database"
|
||||||
|
status={hasPostgres}
|
||||||
|
isPending={isPendingPage}
|
||||||
|
>
|
||||||
|
{renderLink(
|
||||||
|
'https://vercel.com/docs/storage/vercel-postgres/quickstart',
|
||||||
|
'Create Vercel Postgres store',
|
||||||
|
)}
|
||||||
|
{' '}
|
||||||
|
and connect to project
|
||||||
|
</SiteChecklistRow>
|
||||||
|
<SiteChecklistRow
|
||||||
|
title="Setup blob store"
|
||||||
|
status={hasBlob}
|
||||||
|
isPending={isPendingPage}
|
||||||
|
>
|
||||||
|
{renderLink(
|
||||||
|
'https://vercel.com/docs/storage/vercel-blob/quickstart',
|
||||||
|
'Create Vercel Blob store',
|
||||||
|
)}
|
||||||
|
{' '}
|
||||||
|
and connect to project
|
||||||
|
</SiteChecklistRow>
|
||||||
|
<SiteChecklistRow
|
||||||
|
title="Setup auth"
|
||||||
|
status={hasAuth}
|
||||||
|
isPending={isPendingPage}
|
||||||
|
>
|
||||||
|
Store auth secret in environment variable:
|
||||||
|
<InfoBlock className="my-1.5" padding="tight">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span>{secret}</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(secret);
|
||||||
|
toast('Secret copied to clipboard', {
|
||||||
|
duration: 4000,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BiCopy size={16} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={refreshSecret}
|
||||||
|
isLoading={isPendingSecret}
|
||||||
|
>
|
||||||
|
<BiRefresh size={18} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</InfoBlock>
|
||||||
|
{renderEnvVars(['AUTH_SECRET'])}
|
||||||
|
</SiteChecklistRow>
|
||||||
|
<SiteChecklistRow
|
||||||
|
title="Setup admin user"
|
||||||
|
status={hasAdminUser}
|
||||||
|
isPending={isPendingPage}
|
||||||
|
>
|
||||||
|
Store admin email/password
|
||||||
|
{' '}
|
||||||
|
in environment variables:
|
||||||
|
{renderEnvVars([
|
||||||
|
'ADMIN_EMAIL',
|
||||||
|
'ADMIN_PASSWORD',
|
||||||
|
])}
|
||||||
|
</SiteChecklistRow>
|
||||||
|
{showRefreshButton &&
|
||||||
|
<div className="py-4 space-y-4">
|
||||||
|
<button onClick={refreshPage}>
|
||||||
|
Check
|
||||||
|
</button>
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
<div className="px-10 text-gray-500">
|
||||||
|
Changes to environment variables require a redeploy
|
||||||
|
or reboot of local dev server
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -11,3 +11,15 @@ export const SITE_DESCRIPTION = process.env.NEXT_PUBLIC_SITE_DESCRIPTION
|
|||||||
export const BASE_URL = process.env.NODE_ENV === 'production'
|
export const BASE_URL = process.env.NODE_ENV === 'production'
|
||||||
? `https://${SITE_DOMAIN}`
|
? `https://${SITE_DOMAIN}`
|
||||||
: 'http://localhost:3000';
|
: 'http://localhost:3000';
|
||||||
|
|
||||||
|
export const CONFIG_CHECKLIST_STATUS = {
|
||||||
|
hasTitle: (process.env.NEXT_PUBLIC_SITE_TITLE ?? '').length > 0,
|
||||||
|
hasDomain: (process.env.NEXT_PUBLIC_SITE_DOMAIN ?? '').length > 0,
|
||||||
|
hasPostgres: (process.env.POSTGRES_HOST ?? '').length > 0,
|
||||||
|
hasBlob: (process.env.BLOB_READ_WRITE_TOKEN ?? '').length > 0,
|
||||||
|
hasAuth: (process.env.AUTH_SECRET ?? '').length > 0,
|
||||||
|
hasAdminUser: (
|
||||||
|
(process.env.ADMIN_EMAIL ?? '').length > 0 &&
|
||||||
|
(process.env.ADMIN_PASSWORD ?? '').length > 0
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|||||||
@ -23,16 +23,3 @@ const STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match(
|
|||||||
|
|
||||||
export const BLOB_BASE_URL =
|
export const BLOB_BASE_URL =
|
||||||
`https://${STORE_ID}.public.blob.vercel-storage.com`;
|
`https://${STORE_ID}.public.blob.vercel-storage.com`;
|
||||||
|
|
||||||
export const SITE_CHECKLIST_STATUS = {
|
|
||||||
hasTitle: (process.env.NEXT_PUBLIC_SITE_TITLE ?? '').length > 0,
|
|
||||||
hasDomain: (process.env.NEXT_PUBLIC_SITE_DOMAIN ?? '').length > 0,
|
|
||||||
hasPostgres: (process.env.POSTGRES_HOST ?? '').length > 0,
|
|
||||||
hasBlob: (process.env.BLOB_READ_WRITE_TOKEN ?? '').length > 0,
|
|
||||||
hasAuth: (
|
|
||||||
(process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY ?? '').length > 0 &&
|
|
||||||
(process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL ?? '').length > 0 &&
|
|
||||||
(process.env.CLERK_SECRET_KEY ?? '').length > 0 &&
|
|
||||||
(process.env.CLERK_ADMIN_USER_ID ?? '').length > 0
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user