From 5058382f717e14ca0230f23c7f0d6093fe8685ad Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Fri, 2 May 2025 22:04:26 -0400 Subject: [PATCH] Tailwind redesign (#2387) --- .devcontainer/devcontainer.json | 11 +- .npmrc | 1 - .prettierignore | 1 + .prettierrc.mjs | 7 +- .stylelintrc.mjs | 19 - .vscode/extensions.json | 8 + .vscode/settings.json | 16 + README.md | 2 +- app/api/hits/route.ts | 6 +- app/birthday/page.tsx | 8 +- app/cli/page.mdx | 6 +- app/contact/action.ts | 6 +- app/contact/form.module.css | 84 - app/contact/form.tsx | 193 +- app/contact/page.tsx | 38 +- app/feed.atom/route.ts | 2 +- app/feed.xml/route.ts | 2 +- app/fonts.ts | 4 +- app/globals.css | 354 ++- app/hillary/page.tsx | 26 +- app/layout.module.css | 26 - app/layout.tsx | 72 +- app/leo/page.tsx | 24 +- app/license/page.mdx | 4 +- app/manifest.ts | 10 +- app/not-found.tsx | 16 +- app/notes/[slug]/opengraph-image.tsx | 14 +- app/notes/[slug]/page.module.css | 77 - app/notes/[slug]/page.tsx | 86 +- app/notes/page.module.css | 52 - app/notes/page.tsx | 30 +- app/page.module.css | 84 - app/page.tsx | 184 +- app/previously/page.mdx | 30 +- app/privacy/page.mdx | 4 +- .../activity-calendar.tsx} | 2 +- app/projects/github.ts | 2 +- app/projects/page.module.css | 94 - app/projects/page.tsx | 94 +- app/robots.ts | 2 +- app/sitemap.ts | 4 +- app/themes.css | 37 - app/uses/page.mdx | 9 +- app/zip/page.mdx | 80 + app/zip/page.tsx | 173 -- components.json | 21 + components/Blockquote/Blockquote.module.css | 6 - components/Blockquote/Blockquote.tsx | 12 - components/Blockquote/index.ts | 2 - components/Code/Code.module.css | 96 - components/Code/Code.tsx | 35 - components/Code/index.ts | 2 - components/CodePen/index.ts | 2 - components/Comments/index.ts | 2 - components/CopyButton/index.ts | 2 - components/Footer/Footer.module.css | 88 - components/Footer/Footer.tsx | 65 - components/Footer/index.ts | 2 - components/Gist/index.ts | 2 - components/Header/Header.module.css | 83 - components/Header/Header.tsx | 37 - components/Header/index.ts | 2 - components/Heading/Heading.module.css | 50 - components/Heading/Heading.tsx | 35 - components/Heading/index.ts | 2 - components/HeadingAnchor/HeadingAnchor.tsx | 17 - components/HeadingAnchor/index.ts | 2 - .../HorizontalRule/HorizontalRule.module.css | 7 - components/HorizontalRule/HorizontalRule.tsx | 12 - components/HorizontalRule/index.ts | 2 - components/Image/Image.module.css | 17 - components/Image/Image.tsx | 30 - components/Image/index.ts | 2 - components/Link/Link.module.css | 25 - components/Link/index.ts | 2 - components/List/List.module.css | 8 - components/List/List.tsx | 18 - components/List/index.ts | 2 - components/Loading/Loading.module.css | 22 - components/Loading/index.ts | 2 - components/Menu/Menu.module.css | 36 - components/Menu/Menu.tsx | 46 - components/Menu/index.ts | 2 - components/MenuItem/MenuItem.module.css | 43 - components/MenuItem/index.ts | 2 - components/PageTitle/PageTitle.module.css | 15 - components/PageTitle/PageTitle.tsx | 21 - components/PageTitle/index.ts | 2 - components/RelativeTime/index.ts | 2 - components/SkipNav/SkipNav.module.css | 29 - components/SkipNav/SkipNav.tsx | 17 - components/SkipNav/index.ts | 2 - components/ThemeToggle/ThemeToggle.module.css | 26 - components/ThemeToggle/index.ts | 2 - components/Time/index.ts | 2 - components/Tweet/Tweet.module.css | 8 - components/Tweet/index.ts | 2 - components/Video/Video.module.css | 7 - components/Video/index.ts | 2 - components/YouTube/index.ts | 2 - components/button.tsx | 20 + components/code-block.tsx | 52 + .../{Comments/Comments.tsx => comments.tsx} | 2 +- .../CopyButton.tsx => copy-button.tsx} | 16 +- components/{CountUp/index.ts => count-up.ts} | 0 components/image.tsx | 27 + components/input.tsx | 18 + components/{Link/Link.tsx => link.tsx} | 34 +- .../{Loading/Loading.tsx => loading.tsx} | 8 +- components/page-title.tsx | 22 + .../RelativeTime.tsx => relative-time.tsx} | 8 +- components/textarea.tsx | 18 + components/third-party/bluesky.tsx | 39 + .../CodePen.tsx => third-party/codepen.tsx} | 0 .../{Gist/Gist.tsx => third-party/gist.tsx} | 6 +- .../Tweet.tsx => third-party/tweet.tsx} | 14 +- .../YouTube.tsx => third-party/youtube.tsx} | 0 components/{Time/Time.tsx => time.tsx} | 2 +- components/ui/footer.tsx | 62 + components/ui/header.tsx | 41 + .../MenuItem.tsx => ui/menu-item.tsx} | 18 +- components/ui/menu.tsx | 37 + components/ui/skip-nav.tsx | 19 + .../ui/theme-context.tsx | 6 +- .../ThemeToggle.tsx => ui/theme-toggle.tsx} | 16 +- components/{Video/Video.tsx => video.tsx} | 6 +- .../view-counter.tsx | 14 +- eslint.config.mjs | 1 + hooks/index.ts | 4 - hooks/useHasMounted.ts | 13 - hooks/useLocalStorage.ts | 58 - hooks/useMediaQuery.ts | 41 - hooks/useTheme.ts | 15 - lib/config/author.ts | 16 + lib/config/constants.ts | 4 +- lib/config/index.ts | 23 - lib/config/menu.ts | 2 +- lib/config/site.ts | 10 + lib/helpers/build-feed.ts | 21 +- lib/helpers/mdx/heading-anchor.ts | 29 + lib/helpers/mdx/recma.ts | 1 - lib/helpers/mdx/rehype.ts | 4 +- lib/helpers/metadata.ts | 22 +- lib/helpers/posts.ts | 6 +- lib/redis.ts | 7 - lib/utils.ts | 6 + mdx-components.ts | 35 +- next.config.ts | 25 +- notes/coronavirus-open-source/index.mdx | 11 +- notes/css-waving-hand-emoji/index.mdx | 2 +- notes/dark-mode/index.mdx | 9 +- .../index.mdx | 2 +- .../how-to-pull-request-fork-github/index.mdx | 2 +- notes/my-first-code/index.mdx | 12 +- .../index.mdx | 2 +- notes/shodan-search-queries/index.mdx | 150 +- package.json | 56 +- pnpm-lock.yaml | 2178 ++++++++++------- postcss.config.mjs | 7 + public/humans.txt | 1 + renovate.json | 21 +- tsconfig.json | 6 +- 162 files changed, 2739 insertions(+), 3554 deletions(-) delete mode 100644 .stylelintrc.mjs create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json delete mode 100644 app/contact/form.module.css delete mode 100644 app/layout.module.css delete mode 100644 app/notes/[slug]/page.module.css delete mode 100644 app/notes/page.module.css delete mode 100644 app/page.module.css rename app/projects/{calendar.tsx => components/activity-calendar.tsx} (95%) delete mode 100644 app/projects/page.module.css delete mode 100644 app/themes.css create mode 100644 app/zip/page.mdx delete mode 100644 app/zip/page.tsx create mode 100644 components.json delete mode 100644 components/Blockquote/Blockquote.module.css delete mode 100644 components/Blockquote/Blockquote.tsx delete mode 100644 components/Blockquote/index.ts delete mode 100644 components/Code/Code.module.css delete mode 100644 components/Code/Code.tsx delete mode 100644 components/Code/index.ts delete mode 100644 components/CodePen/index.ts delete mode 100644 components/Comments/index.ts delete mode 100644 components/CopyButton/index.ts delete mode 100644 components/Footer/Footer.module.css delete mode 100644 components/Footer/Footer.tsx delete mode 100644 components/Footer/index.ts delete mode 100644 components/Gist/index.ts delete mode 100644 components/Header/Header.module.css delete mode 100644 components/Header/Header.tsx delete mode 100644 components/Header/index.ts delete mode 100644 components/Heading/Heading.module.css delete mode 100644 components/Heading/Heading.tsx delete mode 100644 components/Heading/index.ts delete mode 100644 components/HeadingAnchor/HeadingAnchor.tsx delete mode 100644 components/HeadingAnchor/index.ts delete mode 100644 components/HorizontalRule/HorizontalRule.module.css delete mode 100644 components/HorizontalRule/HorizontalRule.tsx delete mode 100644 components/HorizontalRule/index.ts delete mode 100644 components/Image/Image.module.css delete mode 100644 components/Image/Image.tsx delete mode 100644 components/Image/index.ts delete mode 100644 components/Link/Link.module.css delete mode 100644 components/Link/index.ts delete mode 100644 components/List/List.module.css delete mode 100644 components/List/List.tsx delete mode 100644 components/List/index.ts delete mode 100644 components/Loading/Loading.module.css delete mode 100644 components/Loading/index.ts delete mode 100644 components/Menu/Menu.module.css delete mode 100644 components/Menu/Menu.tsx delete mode 100644 components/Menu/index.ts delete mode 100644 components/MenuItem/MenuItem.module.css delete mode 100644 components/MenuItem/index.ts delete mode 100644 components/PageTitle/PageTitle.module.css delete mode 100644 components/PageTitle/PageTitle.tsx delete mode 100644 components/PageTitle/index.ts delete mode 100644 components/RelativeTime/index.ts delete mode 100644 components/SkipNav/SkipNav.module.css delete mode 100644 components/SkipNav/SkipNav.tsx delete mode 100644 components/SkipNav/index.ts delete mode 100644 components/ThemeToggle/ThemeToggle.module.css delete mode 100644 components/ThemeToggle/index.ts delete mode 100644 components/Time/index.ts delete mode 100644 components/Tweet/Tweet.module.css delete mode 100644 components/Tweet/index.ts delete mode 100644 components/Video/Video.module.css delete mode 100644 components/Video/index.ts delete mode 100644 components/YouTube/index.ts create mode 100644 components/button.tsx create mode 100644 components/code-block.tsx rename components/{Comments/Comments.tsx => comments.tsx} (96%) rename components/{CopyButton/CopyButton.tsx => copy-button.tsx} (74%) rename components/{CountUp/index.ts => count-up.ts} (100%) create mode 100644 components/image.tsx create mode 100644 components/input.tsx rename components/{Link/Link.tsx => link.tsx} (58%) rename components/{Loading/Loading.tsx => loading.tsx} (88%) create mode 100644 components/page-title.tsx rename components/{RelativeTime/RelativeTime.tsx => relative-time.tsx} (80%) create mode 100644 components/textarea.tsx create mode 100644 components/third-party/bluesky.tsx rename components/{CodePen/CodePen.tsx => third-party/codepen.tsx} (100%) rename components/{Gist/Gist.tsx => third-party/gist.tsx} (91%) rename components/{Tweet/Tweet.tsx => third-party/tweet.tsx} (73%) rename components/{YouTube/YouTube.tsx => third-party/youtube.tsx} (100%) rename components/{Time/Time.tsx => time.tsx} (95%) create mode 100644 components/ui/footer.tsx create mode 100644 components/ui/header.tsx rename components/{MenuItem/MenuItem.tsx => ui/menu-item.tsx} (55%) create mode 100644 components/ui/menu.tsx create mode 100644 components/ui/skip-nav.tsx rename contexts/ThemeContext.tsx => components/ui/theme-context.tsx (92%) rename components/{ThemeToggle/ThemeToggle.tsx => ui/theme-toggle.tsx} (51%) rename components/{Video/Video.tsx => video.tsx} (90%) rename app/notes/[slug]/counter.tsx => components/view-counter.tsx (73%) delete mode 100644 hooks/index.ts delete mode 100644 hooks/useHasMounted.ts delete mode 100644 hooks/useLocalStorage.ts delete mode 100644 hooks/useMediaQuery.ts delete mode 100644 hooks/useTheme.ts create mode 100644 lib/config/author.ts delete mode 100644 lib/config/index.ts create mode 100644 lib/config/site.ts create mode 100644 lib/helpers/mdx/heading-anchor.ts delete mode 100644 lib/helpers/mdx/recma.ts delete mode 100644 lib/redis.ts create mode 100644 lib/utils.ts create mode 100644 postcss.config.mjs diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e298a1ba..bc720ea2 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -16,15 +16,18 @@ "git.fetchOnPull": true, "git.rebaseWhenSync": true, "telemetry.telemetryLevel": "off", + "javascript.updateImportsOnFileMove.enabled": "always", "typescript.preferences.importModuleSpecifierEnding": "minimal", - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.surveys.enabled": false, + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.tsserver.log": "off", + "typescript.updateImportsOnFileMove.enabled": "always" }, "extensions": [ + "bradlc.vscode-tailwindcss", "dbaeumer.vscode-eslint", - "EditorConfig.EditorConfig", - "unifiedjs.vscode-mdx", "esbenp.prettier-vscode", - "stylelint.vscode-stylelint" + "unifiedjs.vscode-mdx" ] } }, diff --git a/.npmrc b/.npmrc index 6f07069f..d93a75a6 100644 --- a/.npmrc +++ b/.npmrc @@ -1,3 +1,2 @@ public-hoist-pattern[]=*eslint* public-hoist-pattern[]=*prettier* -public-hoist-pattern[]=*stylelint* diff --git a/.prettierignore b/.prettierignore index c15a8cad..b87eae66 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,3 +10,4 @@ pnpm-lock.yaml # other public/ .devcontainer/devcontainer.json +.vscode/ diff --git a/.prettierrc.mjs b/.prettierrc.mjs index 49b1fa75..1be2c63a 100644 --- a/.prettierrc.mjs +++ b/.prettierrc.mjs @@ -1,12 +1,13 @@ /** @type {import("prettier").Config} */ const config = { - singleQuote: false, + plugins: ["prettier-plugin-tailwindcss"], jsxSingleQuote: false, printWidth: 120, - tabWidth: 2, - useTabs: false, quoteProps: "as-needed", + singleQuote: false, + tabWidth: 2, trailingComma: "es5", + useTabs: false, }; export default config; diff --git a/.stylelintrc.mjs b/.stylelintrc.mjs deleted file mode 100644 index dd849dc0..00000000 --- a/.stylelintrc.mjs +++ /dev/null @@ -1,19 +0,0 @@ -/* eslint-disable import/no-anonymous-default-export */ - -/** @type {import("stylelint").Config} */ -export default { - extends: ["stylelint-config-standard", "stylelint-config-css-modules", "stylelint-prettier/recommended"], - rules: { - "selector-class-pattern": null, - "custom-property-pattern": null, - "media-feature-range-notation": null, - "rule-empty-line-before": [ - "always-multi-line", - { - except: ["after-single-line-comment"], - ignore: ["inside-block"], - }, - ], - "color-hex-length": "long", - }, -}; diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..2229ccd0 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "bradlc.vscode-tailwindcss", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "unifiedjs.vscode-mdx" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..fe47dfc4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "editor.tabSize": 2, + "editor.rulers": [ + 120 + ], + "files.associations": { + "*.css": "tailwindcss", + "*.mdx": "markdown" + }, + "javascript.updateImportsOnFileMove.enabled": "always", + "typescript.preferences.importModuleSpecifierEnding": "minimal", + "typescript.surveys.enabled": false, + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.tsserver.log": "off", + "typescript.updateImportsOnFileMove.enabled": "always" +} diff --git a/README.md b/README.md index e5df2871..96ce4057 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![GitHub repo size](https://img.shields.io/github/repo-size/jakejarvis/jarv.is?color=009cdf&label=repo%20size&logo=git&logoColor=white)](https://github.com/jakejarvis/jarv.is) [![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fjarv.is%2Fapi%2Fhits&query=%24.total.hits&logo=googleanalytics&logoColor=white&label=hits&color=salmon&cacheSeconds=1800)](https://jarv.is/api/hits) -My humble abode on the World Wide Web, created and deployed using [Next.js](https://nextjs.org/), [Vercel](https://vercel.com/), [Upstash Redis](https://upstash.com/), [Giscus](https://giscus.app/), [and more](https://jarv.is/humans.txt). +My humble abode on the World Wide Web, created and deployed using [Next.js](https://nextjs.org/), [Tailwind CSS](https://github.com/user-attachments/assets/dfe99976-c73d-46f1-8a50-f26338463ad8), [Upstash](https://upstash.com/), [Giscus](https://giscus.app/), [and more](https://jarv.is/humans.txt). ## 🕹️ Getting Started diff --git a/app/api/hits/route.ts b/app/api/hits/route.ts index 51ef4f25..7a3302b8 100644 --- a/app/api/hits/route.ts +++ b/app/api/hits/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; import { unstable_cache as cache } from "next/cache"; -import redis from "../../../lib/redis"; +import { kv } from "@vercel/kv"; // cache response from the db const getData = cache( @@ -14,7 +14,7 @@ const getData = cache( }>; }> => { // get all keys (aka slugs) - const slugs = await redis.scan(0, { + const slugs = await kv.scan(0, { match: "hits:*", type: "string", // set an arbitrary yet generous upper limit, just in case... @@ -22,7 +22,7 @@ const getData = cache( }); // get the value (number of hits) for each key (the slug of the page) - const values = await redis.mget(...slugs[1]); + const values = await kv.mget(...slugs[1]); // pair the slugs with their hit values const pages = slugs[1].map((slug, index) => ({ diff --git a/app/birthday/page.tsx b/app/birthday/page.tsx index 96d30a29..f0fa9d0f 100644 --- a/app/birthday/page.tsx +++ b/app/birthday/page.tsx @@ -1,8 +1,8 @@ -import { env } from "../../lib/env"; +import { env } from "@/lib/env"; import { JsonLd } from "react-schemaorg"; -import PageTitle from "../../components/PageTitle"; -import Video from "../../components/Video"; -import { createMetadata } from "../../lib/helpers/metadata"; +import PageTitle from "@/components/page-title"; +import Video from "@/components/video"; +import { createMetadata } from "@/lib/helpers/metadata"; import type { VideoObject } from "schema-dts"; import thumbnail from "./thumbnail.png"; diff --git a/app/cli/page.mdx b/app/cli/page.mdx index fce13a10..9e654f7b 100644 --- a/app/cli/page.mdx +++ b/app/cli/page.mdx @@ -1,6 +1,6 @@ -import PageTitle from "../../components/PageTitle"; -import Comments from "../../components/Comments"; -import { createMetadata } from "../../lib/helpers/metadata"; +import PageTitle from "@/components/page-title"; +import Comments from "@/components/comments"; +import { createMetadata } from "@/lib/helpers/metadata"; export const metadata = createMetadata({ title: "CLI", diff --git a/app/contact/action.ts b/app/contact/action.ts index 14085bed..11377ea2 100644 --- a/app/contact/action.ts +++ b/app/contact/action.ts @@ -1,10 +1,10 @@ "use server"; -import { env } from "../../lib/env"; +import { env } from "@/lib/env"; import { headers } from "next/headers"; import * as v from "valibot"; import { Resend } from "resend"; -import * as config from "../../lib/config"; +import siteConfig from "@/lib/config/site"; const ContactSchema = v.object({ // TODO: replace duplicate error messages with v.message() when released. see: @@ -101,7 +101,7 @@ export const send = async (state: ContactState, payload: FormData): Promise`, replyTo: `${data.output.name} <${data.output.email}>`, to: [env.RESEND_TO_EMAIL], - subject: `[${config.siteName}] Contact Form Submission`, + subject: `[${siteConfig.name}] Contact Form Submission`, text: data.output.message, }); diff --git a/app/contact/form.module.css b/app/contact/form.module.css deleted file mode 100644 index 743e9db3..00000000 --- a/app/contact/form.module.css +++ /dev/null @@ -1,84 +0,0 @@ -.input { - width: 100%; - padding: 0.8em; - margin: 0.6em 0; - border: 2px solid var(--colors-light); - border-radius: 0.6em; - color: var(--colors-text); - background-color: var(--colors-super-duper-light); -} - -.input:focus { - outline: none; - border-color: var(--colors-link); -} - -.input.textarea { - margin-bottom: 0; - line-height: 1.5; - min-height: calc(5 * 1.5rem + 1.5em); /* avoid layout shift when textarea-autosize loads */ - resize: vertical; -} - -.input.invalid { - border-color: var(--colors-error); -} - -.errorMessage { - font-size: 0.9em; - color: var(--colors-error); -} - -.actionRow { - display: flex; - align-items: center; - margin-top: 0.6em; - min-height: 3.75em; -} - -.submitButton { - display: flex; - align-items: center; - height: 3.25em; - padding: 1em 1.25em; - margin-right: 1.5em; - border: 0; - border-radius: 0.6em; - cursor: pointer; - user-select: none; - font-weight: 500; - color: var(--colors-text); - background-color: var(--colors-kinda-light); -} - -.submitButton:hover, -.submitButton:focus-visible { - color: var(--colors-super-duper-light); - background-color: var(--colors-link); -} - -.submitIcon { - width: 1.3em; - height: 1.3em; - margin-right: 0.5em; -} - -.result { - display: flex; - align-items: center; - font-weight: 600; -} - -.result.success { - color: var(--colors-success); -} - -.result.error { - color: var(--colors-error); -} - -.resultIcon { - width: 1.3em; - height: 1.3em; - margin-right: 0.25em; -} diff --git a/app/contact/form.tsx b/app/contact/form.tsx index 28ed5910..15dc3899 100644 --- a/app/contact/form.tsx +++ b/app/contact/form.tsx @@ -1,17 +1,17 @@ "use client"; -import { env } from "../../lib/env"; +import { env } from "@/lib/env"; import { useActionState, useState } from "react"; -import TextareaAutosize from "react-textarea-autosize"; import Turnstile from "react-turnstile"; -import clsx from "clsx"; -import { SendIcon, LoaderIcon, CheckIcon, XIcon } from "lucide-react"; -import Link from "../../components/Link"; +import { SendIcon, Loader2Icon, CheckIcon, XIcon } from "lucide-react"; +import Link from "@/components/link"; +import Input from "@/components/input"; +import Textarea from "@/components/textarea"; +import Button from "@/components/button"; +import { cn } from "@/lib/utils"; import { send, type ContactState, type ContactInput } from "./action"; -import styles from "./form.module.css"; - const ContactForm = () => { const [formState, formAction, pending] = useActionState(send, { success: false, @@ -26,112 +26,115 @@ const ContactForm = () => { }); return ( -
- { - setFormFields({ ...formFields, name: e.target.value }); - }} - disabled={pending || formState.success} - className={clsx(styles.input, !pending && formState.errors?.name && styles.invalid)} - /> - {!pending && formState.errors?.name && {formState.errors.name[0]}} - - { - setFormFields({ ...formFields, email: e.target.value }); - }} - disabled={pending || formState.success} - className={clsx(styles.input, !pending && formState.errors?.email && styles.invalid)} - /> - {!pending && formState.errors?.email && {formState.errors.email[0]}} - - { - setFormFields({ ...formFields, message: e.target.value }); - }} - disabled={pending || formState.success} - className={clsx(styles.input, styles.textarea, !pending && formState.errors?.message && styles.invalid)} - /> - {!pending && formState.errors?.message && ( - {formState.errors.message[0]} - )} - -
- +
+ { + setFormFields({ ...formFields, name: e.target.value }); }} - xmlns="http://www.w3.org/2000/svg" - > - - {" "} - Basic{" "} - - Markdown syntax - {" "} - is allowed here, e.g.: **bold**, _italics_, [ - - links - - ](https://jarv.is), and `code`. + disabled={pending || formState.success} + className={cn(!pending && formState.errors?.name && "border-destructive")} + /> + {!pending && formState.errors?.name && ( + {formState.errors.name[0]} + )}
-
- +
+ { + setFormFields({ ...formFields, email: e.target.value }); + }} + disabled={pending || formState.success} + className={cn(!pending && formState.errors?.email && "border-destructive")} + /> + {!pending && formState.errors?.email && ( + {formState.errors.email[0]} + )}
- {!pending && formState.errors?.["cf-turnstile-response"] && ( - {formState.errors["cf-turnstile-response"][0]} - )} -
+
+