use standard email/SMTP for contact form instead of airtable API

This commit is contained in:
2024-02-27 17:34:48 -05:00
parent 2de3914449
commit 4bef13f4ab
11 changed files with 102 additions and 110 deletions
+3 -4
View File
@@ -1,10 +1,9 @@
# absolutely required: # absolutely required:
NEXT_PUBLIC_BASE_URL= NEXT_PUBLIC_BASE_URL=
DATABASE_URL=
GH_PUBLIC_TOKEN= GH_PUBLIC_TOKEN=
POSTGRES_URL=
POSTGRES_URL_NON_POOLING=
# optional (but not really): # optional (but not really):
AIRTABLE_API_KEY=
AIRTABLE_BASE=
HCAPTCHA_SECRET_KEY= HCAPTCHA_SECRET_KEY=
MAILGUN_SMTP_USER=
MAILGUN_SMTP_PASS=
+1 -1
View File
@@ -5,7 +5,7 @@
[![Licensed under CC-BY-4.0](https://img.shields.io/badge/license-CC--BY--4.0-fb7828?logo=creative-commons&logoColor=white)](LICENSE) [![Licensed under CC-BY-4.0](https://img.shields.io/badge/license-CC--BY--4.0-fb7828?logo=creative-commons&logoColor=white)](LICENSE)
[![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) [![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)
My humble abode on the World Wide Web, created and deployed using [Next.js](https://nextjs.org/), [Stitches](https://stitches.dev/), [Prisma](https://www.prisma.io/), [Vercel](https://vercel.com/), [Supabase](https://supabase.com/), [and more](https://jarv.is/humans.txt). My humble abode on the World Wide Web, created and deployed using [Next.js](https://nextjs.org/), [Stitches](https://stitches.dev/), [Prisma](https://www.prisma.io/), [Vercel](https://vercel.com/), [PlanetScale](https://planetscale.com/), [and more](https://jarv.is/humans.txt).
I keep an ongoing list of [post ideas](https://github.com/jakejarvis/jarv.is/issues/1) and [coding to-dos](https://github.com/jakejarvis/jarv.is/issues/714) as issues in this repo. Outside contributions, improvements, and/or corrections are welcome too! I keep an ongoing list of [post ideas](https://github.com/jakejarvis/jarv.is/issues/1) and [coding to-dos](https://github.com/jakejarvis/jarv.is/issues/714) as issues in this repo. Outside contributions, improvements, and/or corrections are welcome too!
+1 -4
View File
@@ -4,9 +4,6 @@
// https://nextjs.org/blog/next-9-1-7#new-built-in-polyfills-fetch-url-and-objectassign // https://nextjs.org/blog/next-9-1-7#new-built-in-polyfills-fetch-url-and-objectassign
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const fetcher = async (input: RequestInfo, init?: RequestInit): Promise<any> => { const fetcher = <T = any>(...args: Parameters<typeof fetch>): Promise<T> => fetch(...args).then((res) => res.json());
const res = await fetch(input, init);
return res.json();
};
export default fetcher; export default fetcher;
+6 -4
View File
@@ -22,7 +22,7 @@
"@hcaptcha/react-hcaptcha": "^1.10.1", "@hcaptcha/react-hcaptcha": "^1.10.1",
"@novnc/novnc": "1.4.0", "@novnc/novnc": "1.4.0",
"@octokit/graphql": "^7.0.2", "@octokit/graphql": "^7.0.2",
"@octokit/graphql-schema": "^14.57.0", "@octokit/graphql-schema": "^14.58.0",
"@prisma/client": "^5.10.2", "@prisma/client": "^5.10.2",
"@react-spring/web": "^9.7.3", "@react-spring/web": "^9.7.3",
"@stitches/react": "1.3.1-1", "@stitches/react": "1.3.1-1",
@@ -39,6 +39,7 @@
"next": "14.1.0", "next": "14.1.0",
"next-mdx-remote": "^4.4.1", "next-mdx-remote": "^4.4.1",
"next-seo": "^6.5.0", "next-seo": "^6.5.0",
"nodemailer": "^6.9.10",
"obj-str": "^1.1.0", "obj-str": "^1.1.0",
"p-map": "^7.0.1", "p-map": "^7.0.1",
"p-memoize": "^7.1.1", "p-memoize": "^7.1.1",
@@ -74,9 +75,10 @@
"@jakejarvis/eslint-config": "^3.1.0", "@jakejarvis/eslint-config": "^3.1.0",
"@types/comma-number": "^2.1.2", "@types/comma-number": "^2.1.2",
"@types/node": "^20.11.20", "@types/node": "^20.11.20",
"@types/nodemailer": "^6.4.14",
"@types/novnc__novnc": "^1.3.4", "@types/novnc__novnc": "^1.3.4",
"@types/prop-types": "^15.7.11", "@types/prop-types": "^15.7.11",
"@types/react": "^18.2.58", "@types/react": "^18.2.60",
"@types/react-dom": "^18.2.19", "@types/react-dom": "^18.2.19",
"@types/react-is": "^18.2.4", "@types/react-is": "^18.2.4",
"@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/eslint-plugin": "^7.1.0",
@@ -111,9 +113,9 @@
"eslint" "eslint"
] ]
}, },
"packageManager": "pnpm@8.15.3", "packageManager": "pnpm@8.15.4",
"volta": { "volta": {
"node": "20.11.1", "node": "20.11.1",
"pnpm": "8.15.3" "pnpm": "8.15.4"
} }
} }
+45 -57
View File
@@ -1,29 +1,17 @@
import { NextResponse } from "next/server"; import nodemailer from "nodemailer";
import queryString from "query-string"; import queryString from "query-string";
import { siteDomain, hcaptchaSiteKey } from "../../lib/config"; import fetcher from "../../lib/helpers/fetcher";
import type { NextRequest } from "next/server"; import { siteDomain, authorEmail, hcaptchaSiteKey } from "../../lib/config";
import type { NextApiHandler } from "next";
// fallback to dummy secret for testing: https://docs.hcaptcha.com/#integration-testing-test-keys const handler: NextApiHandler = async (req, res) => {
const HCAPTCHA_SITE_KEY = hcaptchaSiteKey || "10000000-ffff-ffff-ffff-000000000001";
const HCAPTCHA_SECRET_KEY = process.env.HCAPTCHA_SECRET_KEY || "0x0000000000000000000000000000000000000000";
const HCAPTCHA_API_ENDPOINT = "https://hcaptcha.com/siteverify";
const { AIRTABLE_API_KEY, AIRTABLE_BASE } = process.env;
const AIRTABLE_API_ENDPOINT = "https://api.airtable.com/v0/";
export const config = {
runtime: "edge",
};
// eslint-disable-next-line import/no-anonymous-default-export
export default async (req: NextRequest) => {
// redirect GET requests to this endpoint to the contact form itself // redirect GET requests to this endpoint to the contact form itself
if (req.method === "GET") { if (req.method === "GET") {
return NextResponse.redirect(`${process.env.NEXT_PUBLIC_BASE_URL || `https://${siteDomain}`}/contact/`); return res.redirect(`${process.env.NEXT_PUBLIC_BASE_URL || `https://${siteDomain}`}/contact/`);
} }
// possible weirdness? https://github.com/orgs/vercel/discussions/78#discussioncomment-5089059 // possible weirdness? https://github.com/orgs/vercel/discussions/78#discussioncomment-5089059
const data = await req.json(); const data = req.body;
// these are both backups to client-side validations just in case someone squeezes through without them. the codes // these are both backups to client-side validations just in case someone squeezes through without them. the codes
// are identical so they're caught in the same fashion. // are identical so they're caught in the same fashion.
@@ -36,61 +24,61 @@ export default async (req: NextRequest) => {
throw new Error("INVALID_CAPTCHA"); throw new Error("INVALID_CAPTCHA");
} }
// sent directly to airtable
const airtableResult = await sendToAirtable({
Name: data.name,
Email: data.email,
Message: data.message,
});
// throw an internal error, not user's fault // throw an internal error, not user's fault
if (airtableResult !== true) { if (!(await sendMessage(data))) {
throw new Error("AIRTABLE_API_ERROR"); throw new Error("NODEMAILER_ERROR");
} }
// disable caching on both ends. see:
// https://vercel.com/docs/concepts/functions/edge-functions/edge-caching
res.setHeader("Cache-Control", "private, no-cache, no-store, must-revalidate");
// success! let the client know // success! let the client know
return NextResponse.json( return res.status(201).json({ success: true });
{ success: true },
{
status: 201,
headers: {
// disable caching on both ends. see:
// https://vercel.com/docs/concepts/functions/edge-functions/edge-caching
"Cache-Control": "private, no-cache, no-store, must-revalidate",
},
}
);
}; };
const validateCaptcha = async (formResponse: unknown): Promise<unknown> => { const validateCaptcha = async (formResponse: unknown): Promise<unknown> => {
const response = await fetch(HCAPTCHA_API_ENDPOINT, { const response = await fetcher("https://hcaptcha.com/siteverify", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
}, },
body: queryString.stringify({ body: queryString.stringify({
response: formResponse, response: formResponse,
sitekey: HCAPTCHA_SITE_KEY, // fallback to dummy secret for testing: https://docs.hcaptcha.com/#integration-testing-test-keys
secret: HCAPTCHA_SECRET_KEY, sitekey: hcaptchaSiteKey || "10000000-ffff-ffff-ffff-000000000001",
secret: process.env.HCAPTCHA_SECRET_KEY || "0x0000000000000000000000000000000000000000",
}), }),
}); });
const result = await response.json(); return response?.success;
return result.success;
}; };
const sendToAirtable = async (data: unknown): Promise<boolean> => { const sendMessage = async (data: Record<string, unknown>): Promise<boolean> => {
const response = await fetch(`${AIRTABLE_API_ENDPOINT}${AIRTABLE_BASE}/Messages`, { try {
method: "POST", const transporter = nodemailer.createTransport({
headers: { service: "mailgun",
Authorization: `Bearer ${AIRTABLE_API_KEY}`, auth: {
"Content-Type": "application/json", user: process.env.MAILGUN_SMTP_USER,
}, pass: process.env.MAILGUN_SMTP_PASS,
body: JSON.stringify({ },
fields: data, });
}),
});
return response.ok; await transporter.sendMail({
from: `${data.name} <${process.env.MAILGUN_SMTP_USER}>`,
sender: `nodemailer <${process.env.MAILGUN_SMTP_USER}>`,
replyTo: `${data.name} <${data.email}>`,
to: `<${authorEmail}>`,
subject: `[${siteDomain}] Contact Form Submission`,
// TODO: add markdown parsing as promised on the form.
text: `${data.message}`,
});
} catch (error) {
console.error(error);
return false;
}
return true;
}; };
export default handler;
+1 -1
View File
@@ -48,7 +48,7 @@ const Privacy = () => {
<p> <p>
A very simple hit counter on each blog post tallies an aggregate number of pageviews (i.e.{" "} A very simple hit counter on each blog post tallies an aggregate number of pageviews (i.e.{" "}
<CodeInline>hits = hits + 1</CodeInline>) in a <Link href="https://supabase.com/">Supabase Postgres</Link>{" "} <CodeInline>hits = hits + 1</CodeInline>) in a <Link href="https://planetscale.com/">PlanetScale</Link>{" "}
database. Individual views and identifying (or non-identifying) details are{" "} database. Individual views and identifying (or non-identifying) details are{" "}
<strong>never stored or logged</strong>. <strong>never stored or logged</strong>.
</p> </p>
+40 -23
View File
@@ -18,8 +18,8 @@ dependencies:
specifier: ^7.0.2 specifier: ^7.0.2
version: 7.0.2 version: 7.0.2
'@octokit/graphql-schema': '@octokit/graphql-schema':
specifier: ^14.57.0 specifier: ^14.58.0
version: 14.57.0 version: 14.58.0
'@prisma/client': '@prisma/client':
specifier: ^5.10.2 specifier: ^5.10.2
version: 5.10.2(prisma@5.10.2) version: 5.10.2(prisma@5.10.2)
@@ -68,6 +68,9 @@ dependencies:
next-seo: next-seo:
specifier: ^6.5.0 specifier: ^6.5.0
version: 6.5.0(next@14.1.0)(react-dom@18.2.0)(react@18.2.0) version: 6.5.0(next@14.1.0)(react-dom@18.2.0)(react@18.2.0)
nodemailer:
specifier: ^6.9.10
version: 6.9.10
obj-str: obj-str:
specifier: ^1.1.0 specifier: ^1.1.0
version: 1.1.0 version: 1.1.0
@@ -103,7 +106,7 @@ dependencies:
version: 5.0.1(react@18.2.0) version: 5.0.1(react@18.2.0)
react-innertext: react-innertext:
specifier: ^1.1.5 specifier: ^1.1.5
version: 1.1.5(@types/react@18.2.58)(react@18.2.0) version: 1.1.5(@types/react@18.2.60)(react@18.2.0)
react-intersection-observer: react-intersection-observer:
specifier: ^9.8.1 specifier: ^9.8.1
version: 9.8.1(react-dom@18.2.0)(react@18.2.0) version: 9.8.1(react-dom@18.2.0)(react@18.2.0)
@@ -115,7 +118,7 @@ dependencies:
version: 2.14.1(react@18.2.0) version: 2.14.1(react@18.2.0)
react-textarea-autosize: react-textarea-autosize:
specifier: ^8.5.3 specifier: ^8.5.3
version: 8.5.3(@types/react@18.2.58)(react@18.2.0) version: 8.5.3(@types/react@18.2.60)(react@18.2.0)
react-tweet: react-tweet:
specifier: ^3.2.0 specifier: ^3.2.0
version: 3.2.0(react-dom@18.2.0)(react@18.2.0) version: 3.2.0(react-dom@18.2.0)(react@18.2.0)
@@ -174,6 +177,9 @@ devDependencies:
'@types/node': '@types/node':
specifier: ^20.11.20 specifier: ^20.11.20
version: 20.11.20 version: 20.11.20
'@types/nodemailer':
specifier: ^6.4.14
version: 6.4.14
'@types/novnc__novnc': '@types/novnc__novnc':
specifier: ^1.3.4 specifier: ^1.3.4
version: 1.3.4 version: 1.3.4
@@ -181,8 +187,8 @@ devDependencies:
specifier: ^15.7.11 specifier: ^15.7.11
version: 15.7.11 version: 15.7.11
'@types/react': '@types/react':
specifier: ^18.2.58 specifier: ^18.2.60
version: 18.2.58 version: 18.2.60
'@types/react-dom': '@types/react-dom':
specifier: ^18.2.19 specifier: ^18.2.19
version: 18.2.19 version: 18.2.19
@@ -604,7 +610,7 @@ packages:
react: '>=16' react: '>=16'
dependencies: dependencies:
'@types/mdx': 2.0.8 '@types/mdx': 2.0.8
'@types/react': 18.2.58 '@types/react': 18.2.60
react: 18.2.0 react: 18.2.0
dev: false dev: false
@@ -758,8 +764,8 @@ packages:
universal-user-agent: 6.0.1 universal-user-agent: 6.0.1
dev: false dev: false
/@octokit/graphql-schema@14.57.0: /@octokit/graphql-schema@14.58.0:
resolution: {integrity: sha512-DvUIxJkfboiQEOme4DOpF4+EtOh1qbYffm/hbcFdqAQzWFPV19Nkul9t/XnyKRPFyy26Nwf8QesCOeYw8os5rQ==} resolution: {integrity: sha512-89QSUV1Dgxzq90wqkv0Nmw7jHfFCAQ4K/fjp5ezvDEHqFFzMCn25TBQlm38WB8ams+hGxInRDbITCP0n7GTGlg==}
dependencies: dependencies:
graphql: 16.8.1 graphql: 16.8.1
graphql-tag: 2.12.6(graphql@16.8.1) graphql-tag: 2.12.6(graphql@16.8.1)
@@ -969,7 +975,7 @@ packages:
/@types/hoist-non-react-statics@3.3.2: /@types/hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-YIQtIg4PKr7ZyqNPZObpxfHsHEmuB8dXCxd6qVcGuQVDK2bpsF7bYNnBJ4Nn7giuACZg+WewExgrtAJ3XnA4Xw==} resolution: {integrity: sha512-YIQtIg4PKr7ZyqNPZObpxfHsHEmuB8dXCxd6qVcGuQVDK2bpsF7bYNnBJ4Nn7giuACZg+WewExgrtAJ3XnA4Xw==}
dependencies: dependencies:
'@types/react': 18.2.58 '@types/react': 18.2.60
hoist-non-react-statics: 3.3.2 hoist-non-react-statics: 3.3.2
dev: false dev: false
@@ -1021,6 +1027,12 @@ packages:
dependencies: dependencies:
undici-types: 5.26.5 undici-types: 5.26.5
/@types/nodemailer@6.4.14:
resolution: {integrity: sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==}
dependencies:
'@types/node': 20.11.20
dev: true
/@types/novnc__novnc@1.3.4: /@types/novnc__novnc@1.3.4:
resolution: {integrity: sha512-JseVFIc9bm6IV2TkHi/oL4tr3anYeOw2BXX44HgqdRnRCaimz/nfZDWCyylK/OsJIfLzcchLLf2pxevYMNXFbQ==} resolution: {integrity: sha512-JseVFIc9bm6IV2TkHi/oL4tr3anYeOw2BXX44HgqdRnRCaimz/nfZDWCyylK/OsJIfLzcchLLf2pxevYMNXFbQ==}
dev: true dev: true
@@ -1035,17 +1047,17 @@ packages:
/@types/react-dom@18.2.19: /@types/react-dom@18.2.19:
resolution: {integrity: sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==} resolution: {integrity: sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==}
dependencies: dependencies:
'@types/react': 18.2.58 '@types/react': 18.2.60
dev: true dev: true
/@types/react-is@18.2.4: /@types/react-is@18.2.4:
resolution: {integrity: sha512-wBc7HgmbCcrvw0fZjxbgz/xrrlZKzEqmABBMeSvpTvdm25u6KI6xdIi9pRE2G0C1Lw5ETFdcn4UbYZ4/rpqUYw==} resolution: {integrity: sha512-wBc7HgmbCcrvw0fZjxbgz/xrrlZKzEqmABBMeSvpTvdm25u6KI6xdIi9pRE2G0C1Lw5ETFdcn4UbYZ4/rpqUYw==}
dependencies: dependencies:
'@types/react': 18.2.58 '@types/react': 18.2.60
dev: true dev: true
/@types/react@18.2.58: /@types/react@18.2.60:
resolution: {integrity: sha512-TaGvMNhxvG2Q0K0aYxiKfNDS5m5ZsoIBBbtfUorxdH4NGSXIlYvZxLJI+9Dd3KjeB3780bciLyAb7ylO8pLhPw==} resolution: {integrity: sha512-dfiPj9+k20jJrLGOu9Nf6eqxm2EyJRrq2NvwOFsfbb7sFExZ9WELPs67UImHj3Ayxg8ruTtKtNnbjaF8olPq0A==}
dependencies: dependencies:
'@types/prop-types': 15.7.11 '@types/prop-types': 15.7.11
'@types/scheduler': 0.16.4 '@types/scheduler': 0.16.4
@@ -4516,6 +4528,11 @@ packages:
'@types/nlcst': 1.0.4 '@types/nlcst': 1.0.4
dev: false dev: false
/nodemailer@6.9.10:
resolution: {integrity: sha512-qtoKfGFhvIFW5kLfrkw2R6Nm6Ur4LNUMykyqu6n9BRKJuyQrqEGwdXXUAbwWEKt33dlWUGXb7rzmJP/p4+O+CA==}
engines: {node: '>=6.0.0'}
dev: false
/nopt@7.2.0: /nopt@7.2.0:
resolution: {integrity: sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==} resolution: {integrity: sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -4905,13 +4922,13 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false dev: false
/react-innertext@1.1.5(@types/react@18.2.58)(react@18.2.0): /react-innertext@1.1.5(@types/react@18.2.60)(react@18.2.0):
resolution: {integrity: sha512-PWAqdqhxhHIv80dT9znP2KvS+hfkbRovFp4zFYHFFlOoQLRiawIic81gKb3U1wEyJZgMwgs3JoLtwryASRWP3Q==} resolution: {integrity: sha512-PWAqdqhxhHIv80dT9znP2KvS+hfkbRovFp4zFYHFFlOoQLRiawIic81gKb3U1wEyJZgMwgs3JoLtwryASRWP3Q==}
peerDependencies: peerDependencies:
'@types/react': '>=0.0.0 <=99' '@types/react': '>=0.0.0 <=99'
react: '>=0.0.0 <=99' react: '>=0.0.0 <=99'
dependencies: dependencies:
'@types/react': 18.2.58 '@types/react': 18.2.60
react: 18.2.0 react: 18.2.0
dev: false dev: false
@@ -4948,7 +4965,7 @@ packages:
react-fast-compare: 3.2.2 react-fast-compare: 3.2.2
dev: false dev: false
/react-textarea-autosize@8.5.3(@types/react@18.2.58)(react@18.2.0): /react-textarea-autosize@8.5.3(@types/react@18.2.60)(react@18.2.0):
resolution: {integrity: sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==} resolution: {integrity: sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
peerDependencies: peerDependencies:
@@ -4957,7 +4974,7 @@ packages:
'@babel/runtime': 7.23.1 '@babel/runtime': 7.23.1
react: 18.2.0 react: 18.2.0
use-composed-ref: 1.3.0(react@18.2.0) use-composed-ref: 1.3.0(react@18.2.0)
use-latest: 1.2.1(@types/react@18.2.58)(react@18.2.0) use-latest: 1.2.1(@types/react@18.2.60)(react@18.2.0)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/react' - '@types/react'
dev: false dev: false
@@ -5997,7 +6014,7 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false dev: false
/use-isomorphic-layout-effect@1.1.2(@types/react@18.2.58)(react@18.2.0): /use-isomorphic-layout-effect@1.1.2(@types/react@18.2.60)(react@18.2.0):
resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==} resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==}
peerDependencies: peerDependencies:
'@types/react': '*' '@types/react': '*'
@@ -6006,11 +6023,11 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
dependencies: dependencies:
'@types/react': 18.2.58 '@types/react': 18.2.60
react: 18.2.0 react: 18.2.0
dev: false dev: false
/use-latest@1.2.1(@types/react@18.2.58)(react@18.2.0): /use-latest@1.2.1(@types/react@18.2.60)(react@18.2.0):
resolution: {integrity: sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==} resolution: {integrity: sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==}
peerDependencies: peerDependencies:
'@types/react': '*' '@types/react': '*'
@@ -6019,9 +6036,9 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
dependencies: dependencies:
'@types/react': 18.2.58 '@types/react': 18.2.60
react: 18.2.0 react: 18.2.0
use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.58)(react@18.2.0) use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.60)(react@18.2.0)
dev: false dev: false
/use-sync-external-store@1.2.0(react@18.2.0): /use-sync-external-store@1.2.0(react@18.2.0):
@@ -1,7 +0,0 @@
-- CreateTable
CREATE TABLE "hits" (
"slug" VARCHAR(128) NOT NULL,
"hits" INTEGER NOT NULL DEFAULT 1,
CONSTRAINT "hits_pkey" PRIMARY KEY ("slug")
);
-3
View File
@@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
+4 -5
View File
@@ -1,12 +1,11 @@
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
} }
datasource db { datasource db {
provider = "postgresql" provider = "mysql"
url = env("POSTGRES_URL") url = env("DATABASE_URL")
directUrl = env("POSTGRES_URL_NON_POOLING") relationMode = "prisma"
} }
model hits { model hits {
+1 -1
View File
@@ -32,7 +32,7 @@
- Vercel - Vercel
- Stitches - Stitches
- Prisma - Prisma
- Supabase - PlanetScale
- Giscus - Giscus
- Fathom Analytics - Fathom Analytics
- ...and more: https://jarv.is/uses/ - ...and more: https://jarv.is/uses/