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]} - )} -
+
+