From 33ec20d70925296be74a615a8de9672be13a335a Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Wed, 6 Sep 2023 18:05:29 -0500 Subject: [PATCH] Refactor site checklist, add secret generator --- .vscode/settings.json | 1 + package.json | 1 + pnpm-lock.yaml | 12 ++ src/app/(auth-state)/checklist/page.tsx | 5 +- src/app/layout.tsx | 2 + src/auth/SignInForm.tsx | 59 ++++---- src/auth/index.ts | 5 + src/components/InfoBlock.tsx | 20 ++- src/components/LoaderIcon.tsx | 34 +++++ src/components/ToasterClient.tsx | 11 ++ src/photo/PhotosEmptyState.tsx | 12 +- src/site/SiteChecklist.tsx | 151 ++----------------- src/site/SiteChecklistClient.tsx | 186 ++++++++++++++++++++++++ src/site/config.ts | 12 ++ src/site/index.ts | 13 -- 15 files changed, 323 insertions(+), 201 deletions(-) create mode 100644 src/components/LoaderIcon.tsx create mode 100644 src/components/ToasterClient.tsx create mode 100644 src/site/SiteChecklistClient.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 6fda0a0a..52fcf281 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,7 @@ "nextjs", "qaub", "skippable", + "sonner", "thephotoblog", "trpc", "WRHGZC", diff --git a/package.json b/package.json index a2679774..69feffbb 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "react-icons": "^4.10.1", "react-spinners": "^0.13.8", "short-uuid": "^4.2.2", + "sonner": "^0.7.0", "tailwindcss": "3.3.3", "ts-exif-parser": "^0.2.2", "typescript": "5.2.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72d40e65..8daf91ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,7 @@ specifiers: react-icons: ^4.10.1 react-spinners: ^0.13.8 short-uuid: ^4.2.2 + sonner: ^0.7.0 tailwindcss: 3.3.3 ts-exif-parser: ^0.2.2 typescript: 5.2.2 @@ -59,6 +60,7 @@ dependencies: react-icons: 4.10.1_react@18.2.0 react-spinners: 0.13.8_biqbaboplfbrettd7655fr4n2y short-uuid: 4.2.2 + sonner: 0.7.0_biqbaboplfbrettd7655fr4n2y tailwindcss: 3.3.3 ts-exif-parser: 0.2.2 typescript: 5.2.2 @@ -3189,6 +3191,16 @@ packages: engines: {node: '>=8'} 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: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} diff --git a/src/app/(auth-state)/checklist/page.tsx b/src/app/(auth-state)/checklist/page.tsx index 0e074d62..c69d9d0b 100644 --- a/src/app/(auth-state)/checklist/page.tsx +++ b/src/app/(auth-state)/checklist/page.tsx @@ -1,13 +1,12 @@ import InfoBlock from '@/components/InfoBlock'; import SiteGrid from '@/components/SiteGrid'; -import { SITE_CHECKLIST_STATUS } from '@/site'; import SiteChecklist from '@/site/SiteChecklist'; -export default function ChecklistPage() { +export default async function ChecklistPage() { return ( - + } /> ); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index fe548955..79ce5152 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,6 +6,7 @@ import { BASE_URL, SITE_DESCRIPTION, SITE_TITLE } from '@/site/config'; import StateProvider from '@/state/AppStateProvider'; import ThemeProviderClient from '@/site/ThemeProviderClient'; import Nav from '@/components/Nav'; +import ToasterClient from '@/components/ToasterClient'; import '../site/globals.css'; @@ -76,6 +77,7 @@ export default function RootLayout({ + diff --git a/src/auth/SignInForm.tsx b/src/auth/SignInForm.tsx index b622f385..c5ccaf18 100644 --- a/src/auth/SignInForm.tsx +++ b/src/auth/SignInForm.tsx @@ -10,37 +10,36 @@ export default function SignInForm() { const [password, setPassword] = useState(''); return ( - -
-
-
+ +
+
+
+
+
+
-
); } diff --git a/src/auth/index.ts b/src/auth/index.ts index f8d2cdda..77f6e1a6 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -55,3 +55,8 @@ export const { signIn: '/sign-in', }, }); + +export const generateAuthSecret = () => fetch( + 'https://generate-secret.vercel.app/32', + { cache: 'no-cache' }, +).then(res => res.text()); diff --git a/src/components/InfoBlock.tsx b/src/components/InfoBlock.tsx index 2386dd70..cad3c539 100644 --- a/src/components/InfoBlock.tsx +++ b/src/components/InfoBlock.tsx @@ -4,27 +4,33 @@ import { ReactNode } from 'react'; export default function InfoBlock({ children, className, - padding = 'loose', + padding = 'normal', }: { children: ReactNode 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 (
{children}
diff --git a/src/components/LoaderIcon.tsx b/src/components/LoaderIcon.tsx new file mode 100644 index 00000000..f447bb39 --- /dev/null +++ b/src/components/LoaderIcon.tsx @@ -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 ( + + {!isLoading + ? + {children} + + : + + } + + ); +} diff --git a/src/components/ToasterClient.tsx b/src/components/ToasterClient.tsx new file mode 100644 index 00000000..157c21d6 --- /dev/null +++ b/src/components/ToasterClient.tsx @@ -0,0 +1,11 @@ +'use client'; + +import { useTheme } from 'next-themes'; +import { Toaster } from 'sonner'; + +export default function ToasterClient() { + const { theme } = useTheme(); + return ( + + ); +} \ No newline at end of file diff --git a/src/photo/PhotosEmptyState.tsx b/src/photo/PhotosEmptyState.tsx index d844b9e5..b3f471b7 100644 --- a/src/photo/PhotosEmptyState.tsx +++ b/src/photo/PhotosEmptyState.tsx @@ -1,13 +1,13 @@ import InfoBlock from '@/components/InfoBlock'; import SiteGrid from '@/components/SiteGrid'; -import { SITE_CHECKLIST_STATUS } from '@/site'; +import { CONFIG_CHECKLIST_STATUS } from '@/site/config'; import SiteChecklist from '@/site/SiteChecklist'; import { cc } from '@/utility/css'; import Link from 'next/link'; import { HiOutlinePhotograph } from 'react-icons/hi'; 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 ( - {showChecklist - ? 'Finish Setup' - : 'Welcome!'} + {showChecklist ? 'Finish Setup' : 'Welcome!'}
{showChecklist - ? - :
+ ? + :
1. Visit {' '} diff --git a/src/site/SiteChecklist.tsx b/src/site/SiteChecklist.tsx index 88dd7db0..6dd042b6 100644 --- a/src/site/SiteChecklist.tsx +++ b/src/site/SiteChecklist.tsx @@ -1,144 +1,13 @@ -'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'; - -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) => - <> - - {text} - - {external && - <> -   - - } - ; - - const renderEnvVar = (variables: string[]) => -
- {variables.map(variable => -
- - `{variable}` - -
)} -
; +import { generateAuthSecret } from '@/auth'; +import SiteChecklistClient from './SiteChecklistClient'; +import { CONFIG_CHECKLIST_STATUS } from '@/site/config'; +export default async function SiteChecklist() { + const secret = await generateAuthSecret(); return ( -
- - Store in environment variable: - {renderEnvVar(['NEXT_PUBLIC_SITE_TITLE'])} - - - Store in environment variable: - {renderEnvVar(['NEXT_PUBLIC_SITE_DOMAIN'])} - - - {renderLink( - 'https://vercel.com/docs/storage/vercel-postgres/quickstart', - 'Create Vercel Postgres store', - )} - {' '} - and connect to project - - - {renderLink( - 'https://vercel.com/docs/storage/vercel-blob/quickstart', - 'Create Vercel Blob store', - )} - {' '} - and connect to project - - - {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', - ])} - -
-
- Changes to environment variables require a redeploy - or reboot of local dev server -
- {showRefreshButton && - } -
-
+ ); -} \ No newline at end of file +} diff --git a/src/site/SiteChecklistClient.tsx b/src/site/SiteChecklistClient.tsx new file mode 100644 index 00000000..5999daef --- /dev/null +++ b/src/site/SiteChecklistClient.tsx @@ -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) => + <> + + {text} + + {external && + <> +   + + } + ; + + const renderEnvVar = (variable: string) => +
+ + `{variable}` + +
; + + const renderEnvVars = (variables: string[]) => +
+ {variables.map(renderEnvVar)} +
; + + return ( +
+
+ + Store in environment variable: + {renderEnvVars(['NEXT_PUBLIC_SITE_TITLE'])} + + + Store in environment variable: + {renderEnvVars(['NEXT_PUBLIC_SITE_DOMAIN'])} + + + {renderLink( + 'https://vercel.com/docs/storage/vercel-postgres/quickstart', + 'Create Vercel Postgres store', + )} + {' '} + and connect to project + + + {renderLink( + 'https://vercel.com/docs/storage/vercel-blob/quickstart', + 'Create Vercel Blob store', + )} + {' '} + and connect to project + + + Store auth secret in environment variable: + +
+ {secret} +
+ { + navigator.clipboard.writeText(secret); + toast('Secret copied to clipboard', { + duration: 4000, + }); + }} + > + + + + + +
+
+
+ {renderEnvVars(['AUTH_SECRET'])} +
+ + Store admin email/password + {' '} + in environment variables: + {renderEnvVars([ + 'ADMIN_EMAIL', + 'ADMIN_PASSWORD', + ])} + + {showRefreshButton && +
+ +
} +
+
+ Changes to environment variables require a redeploy + or reboot of local dev server +
+
+ ); +} \ No newline at end of file diff --git a/src/site/config.ts b/src/site/config.ts index c7178747..76772b2b 100644 --- a/src/site/config.ts +++ b/src/site/config.ts @@ -11,3 +11,15 @@ export const SITE_DESCRIPTION = process.env.NEXT_PUBLIC_SITE_DESCRIPTION export const BASE_URL = process.env.NODE_ENV === 'production' ? `https://${SITE_DOMAIN}` : '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 + ), +}; diff --git a/src/site/index.ts b/src/site/index.ts index 149bc374..6406851c 100644 --- a/src/site/index.ts +++ b/src/site/index.ts @@ -23,16 +23,3 @@ const STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match( export const BLOB_BASE_URL = `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 - ), -};