1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-04-25 18:15:21 -04:00

refactor contact form server action

This commit is contained in:
Jake Jarvis 2025-04-04 09:31:08 -04:00
parent 3043f4df8c
commit 64f5cd2c39
Signed by: jake
SSH Key Fingerprint: SHA256:nCkvAjYA6XaSPUqc4TfbBQTpzr8Xj7ritg/sGInCdkc
8 changed files with 210 additions and 193 deletions

View File

@ -1,112 +0,0 @@
"use server";
import { headers } from "next/headers";
import * as v from "valibot";
import { Resend } from "resend";
import * as config from "../../lib/config";
const ContactSchema = v.object({
// TODO: replace duplicate error messages with v.message() when released. see:
// https://valibot.dev/api/message/
// https://github.com/fabian-hiller/valibot/blob/main/library/src/methods/message/message.ts
name: v.pipe(v.string("Your name is required."), v.trim(), v.nonEmpty("Your name is required.")),
email: v.pipe(
v.string("Your email address is required."),
v.trim(),
v.nonEmpty("Your email address is required."),
v.email("Invalid email address.")
),
message: v.pipe(
v.string("A message is required."),
v.trim(),
v.nonEmpty("A message is required."),
v.minLength(10, "Your message must be at least 10 characters.")
),
"cf-turnstile-response": v.pipe(
// token wasn't submitted at _all_, most likely a direct POST request by a spam bot
v.string("Shoo, bot."),
// form submitted properly but token was missing, might be a forgetful human
v.nonEmpty("Just do the stinkin CAPTCHA, human! 🤖"),
// very rudimentary length check based on Cloudflare's docs
// https://developers.cloudflare.com/turnstile/troubleshooting/testing/
v.minLength("XXXX.DUMMY.TOKEN.XXXX".length),
// "A Turnstile token can have up to 2048 characters."
// https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
v.maxLength(2048),
v.readonly()
),
});
export type ContactInput = v.InferInput<typeof ContactSchema>;
export type ContactState = {
success: boolean;
message: string;
errors?: v.FlatErrors<typeof ContactSchema>["nested"];
};
export const sendMessage = async (prevState: ContactState, formData: FormData): Promise<ContactState> => {
try {
// TODO: remove after debugging why automated spam entries are causing 500 errors
console.debug("[contact form] received data:", formData);
const data = v.safeParse(ContactSchema, Object.fromEntries(formData));
if (!data.success) {
return {
success: false,
message: "Please make sure all fields are filled in.",
errors: v.flatten(data.issues).nested,
};
}
// validate captcha
const turnstileResponse = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
secret: process.env.TURNSTILE_SECRET_KEY || "1x0000000000000000000000000000000AA",
response: data.output["cf-turnstile-response"],
remoteip: (await headers()).get("x-forwarded-for") || "",
}),
cache: "no-store",
signal: AbortSignal.timeout(5000), // 5 second timeout
});
if (!turnstileResponse || !turnstileResponse.ok) {
throw new Error(`[contact form] turnstile validation failed: ${turnstileResponse.status}`);
}
const turnstileData = (await turnstileResponse.json()) as { success: boolean };
if (!turnstileData.success) {
return {
success: false,
message: "Did you complete the CAPTCHA? (If you're human, that is...)",
};
}
if (!process.env.RESEND_FROM_EMAIL) {
console.warn("[contact form] RESEND_FROM_EMAIL not set, falling back to onboarding@resend.dev.");
}
// send email
const resend = new Resend(process.env.RESEND_API_KEY);
await resend.emails.send({
from: `${data.output.name} <${process.env.RESEND_FROM_EMAIL ?? "onboarding@resend.dev"}>`,
replyTo: `${data.output.name} <${data.output.email}>`,
to: [config.authorEmail],
subject: `[${config.siteName}] Contact Form Submission`,
text: data.output.message,
});
return { success: true, message: "Thanks! You should hear from me soon." };
} catch (error) {
console.error("[contact form] fatal error:", error);
return {
success: false,
message: "Internal server error. Please try again later or shoot me an email.",
};
}
};

View File

@ -24,7 +24,7 @@
border-color: var(--colors-error);
}
.fieldError {
.errorMessage {
font-size: 0.9em;
color: var(--colors-error);
}

View File

@ -6,12 +6,16 @@ import Turnstile from "react-turnstile";
import clsx from "clsx";
import { CheckIcon, XIcon } from "lucide-react";
import Link from "../../components/Link";
import { sendMessage, type ContactInput, type ContactState } from "./actions";
import type { ContactInput, ContactState } from "./schema";
import styles from "./form.module.css";
const ContactForm = () => {
const [formState, formAction, pending] = useActionState<ContactState, FormData>(sendMessage, {
const ContactForm = ({
serverAction,
}: {
serverAction: (state: ContactState, payload: FormData) => Promise<ContactState>;
}) => {
const [formState, formAction, pending] = useActionState<ContactState, FormData>(serverAction, {
success: false,
message: "",
});
@ -36,7 +40,7 @@ const ContactForm = () => {
disabled={pending || formState.success}
className={clsx(styles.input, !pending && formState.errors?.name && styles.invalid)}
/>
{!pending && formState.errors?.name && <span className={styles.fieldError}>{formState.errors.name[0]}</span>}
{!pending && formState.errors?.name && <span className={styles.errorMessage}>{formState.errors.name[0]}</span>}
<input
type="email"
@ -50,7 +54,7 @@ const ContactForm = () => {
disabled={pending || formState.success}
className={clsx(styles.input, !pending && formState.errors?.email && styles.invalid)}
/>
{!pending && formState.errors?.email && <span className={styles.fieldError}>{formState.errors.email[0]}</span>}
{!pending && formState.errors?.email && <span className={styles.errorMessage}>{formState.errors.email[0]}</span>}
<TextareaAutosize
name="message"
@ -64,7 +68,7 @@ const ContactForm = () => {
className={clsx(styles.input, styles.textarea, !pending && formState.errors?.message && styles.invalid)}
/>
{!pending && formState.errors?.message && (
<span className={styles.fieldError}>{formState.errors.message[0]}</span>
<span className={styles.errorMessage}>{formState.errors.message[0]}</span>
)}
<div
@ -106,7 +110,7 @@ const ContactForm = () => {
<Turnstile sitekey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || "1x00000000000000000000AA"} fixedSize />
</div>
{!pending && formState.errors?.["cf-turnstile-response"] && (
<span className={styles.fieldError}>{formState.errors["cf-turnstile-response"][0]}</span>
<span className={styles.errorMessage}>{formState.errors["cf-turnstile-response"][0]}</span>
)}
<div className={styles.actionRow}>

View File

@ -1,7 +1,13 @@
import { headers } from "next/headers";
import * as v from "valibot";
import { Resend } from "resend";
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import ContactForm from "./form";
import { addMetadata } from "../../lib/helpers/metadata";
import * as config from "../../lib/config";
import ContactForm from "./form";
import ContactSchema, { type ContactState } from "./schema";
export const metadata = addMetadata({
title: "Contact Me",
@ -11,6 +17,84 @@ export const metadata = addMetadata({
},
});
const send = async (prevState: ContactState, formData: FormData): Promise<ContactState> => {
"use server";
// TODO: remove after debugging why automated spam entries are causing 500 errors
console.debug("[contact form] received data:", formData);
if (!process.env.RESEND_API_KEY) {
throw new Error("[contact form] 'RESEND_API_KEY' is not set.");
}
try {
const data = v.safeParse(ContactSchema, Object.fromEntries(formData));
if (!data.success) {
return {
success: false,
message: "Please make sure all fields are filled in.",
errors: v.flatten(data.issues).nested,
};
}
// try to get the client IP (for turnstile accuracy, not logging!) but no biggie if we can't
let remoteip;
try {
remoteip = (await headers()).get("x-forwarded-for");
} catch {} // eslint-disable-line no-empty
// validate captcha
const turnstileResponse = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
secret: process.env.TURNSTILE_SECRET_KEY || "1x0000000000000000000000000000000AA",
response: data.output["cf-turnstile-response"],
remoteip,
}),
cache: "no-store",
});
if (!turnstileResponse || !turnstileResponse.ok) {
throw new Error(`[contact form] turnstile validation failed: ${turnstileResponse.status}`);
}
const turnstileData = (await turnstileResponse.json()) as { success: boolean };
if (!turnstileData.success) {
return {
success: false,
message: "Did you complete the CAPTCHA? (If you're human, that is...)",
};
}
if (!process.env.RESEND_FROM_EMAIL) {
// https://resend.com/docs/api-reference/emails/send-email
console.warn("[contact form] 'RESEND_FROM_EMAIL' is not set, falling back to onboarding@resend.dev.");
}
// send email
const resend = new Resend(process.env.RESEND_API_KEY!);
await resend.emails.send({
from: `${data.output.name} <${process.env.RESEND_FROM_EMAIL || "onboarding@resend.dev"}>`,
replyTo: `${data.output.name} <${data.output.email}>`,
to: [config.authorEmail],
subject: `[${config.siteName}] Contact Form Submission`,
text: data.output.message,
});
return { success: true, message: "Thanks! You should hear from me soon." };
} catch (error) {
console.error("[contact form] fatal error:", error);
return {
success: false,
message: "Internal server error. Please try again later or shoot me an email.",
};
}
};
const Page = () => {
return (
<div
@ -43,7 +127,7 @@ const Page = () => {
.
</p>
<ContactForm />
<ContactForm serverAction={send} />
</div>
);
};

43
app/contact/schema.ts Normal file
View File

@ -0,0 +1,43 @@
import * as v from "valibot";
export const ContactSchema = v.object({
// TODO: replace duplicate error messages with v.message() when released. see:
// https://valibot.dev/api/message/
// https://github.com/fabian-hiller/valibot/blob/main/library/src/methods/message/message.ts
name: v.pipe(v.string("Your name is required."), v.trim(), v.nonEmpty("Your name is required.")),
email: v.pipe(
v.string("Your email address is required."),
v.trim(),
v.nonEmpty("Your email address is required."),
v.email("Invalid email address.")
),
message: v.pipe(
v.string("A message is required."),
v.trim(),
v.nonEmpty("A message is required."),
v.minLength(10, "Your message must be at least 10 characters.")
),
"cf-turnstile-response": v.pipe(
// token wasn't submitted at _all_, most likely a direct POST request by a spam bot
v.string("Shoo, bot."),
// form submitted properly but token was missing, might be a forgetful human
v.nonEmpty("Just do the stinkin CAPTCHA, human! 🤖"),
// very rudimentary length check based on Cloudflare's docs
// https://developers.cloudflare.com/turnstile/troubleshooting/testing/
v.minLength("XXXX.DUMMY.TOKEN.XXXX".length),
// "A Turnstile token can have up to 2048 characters."
// https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
v.maxLength(2048),
v.readonly()
),
});
export type ContactInput = v.InferInput<typeof ContactSchema>;
export type ContactState = {
success: boolean;
message: string;
errors?: v.FlatErrors<typeof ContactSchema>["nested"];
};
export default ContactSchema;

View File

@ -23,9 +23,7 @@ const useLocalStorage = <T = string>(
window.localStorage.setItem(key, serializer(initialValue));
return initialValue;
}
} catch (
error // eslint-disable-line @typescript-eslint/no-unused-vars
) {
} catch {
return initialValue;
}
});
@ -51,7 +49,7 @@ const useLocalStorage = <T = string>(
try {
window.localStorage.removeItem(key);
setState(undefined);
} catch (error) {} // eslint-disable-line no-empty, @typescript-eslint/no-unused-vars
} catch {} // eslint-disable-line no-empty
}, [key]);
return [state, set, remove];

View File

@ -23,8 +23,8 @@
"@giscus/react": "^3.1.0",
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"@next/bundle-analyzer": "15.3.0-canary.29",
"@next/mdx": "15.3.0-canary.29",
"@next/bundle-analyzer": "15.3.0-canary.33",
"@next/mdx": "15.3.0-canary.33",
"@octokit/graphql": "^8.2.1",
"@octokit/graphql-schema": "^15.26.0",
"@upstash/redis": "^1.34.6",
@ -38,7 +38,7 @@
"html-entities": "^2.6.0",
"lucide-react": "0.487.0",
"modern-normalize": "^3.0.1",
"next": "15.3.0-canary.29",
"next": "15.3.0-canary.33",
"polished": "^4.3.1",
"prop-types": "^15.8.1",
"react": "19.1.0",
@ -83,7 +83,7 @@
"babel-plugin-react-compiler": "19.0.0-beta-e993439-20250328",
"cross-env": "^7.0.3",
"eslint": "^9.23.0",
"eslint-config-next": "15.3.0-canary.29",
"eslint-config-next": "15.3.0-canary.33",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.31.0",

126
pnpm-lock.yaml generated
View File

@ -27,11 +27,11 @@ importers:
specifier: ^3.1.0
version: 3.1.0(@types/react@19.1.0)(react@19.1.0)
'@next/bundle-analyzer':
specifier: 15.3.0-canary.29
version: 15.3.0-canary.29
specifier: 15.3.0-canary.33
version: 15.3.0-canary.33
'@next/mdx':
specifier: 15.3.0-canary.29
version: 15.3.0-canary.29(@mdx-js/loader@3.1.0(acorn@8.14.1))(@mdx-js/react@3.1.0(@types/react@19.1.0)(react@19.1.0))
specifier: 15.3.0-canary.33
version: 15.3.0-canary.33(@mdx-js/loader@3.1.0(acorn@8.14.1))(@mdx-js/react@3.1.0(@types/react@19.1.0)(react@19.1.0))
'@octokit/graphql':
specifier: ^8.2.1
version: 8.2.1
@ -61,7 +61,7 @@ importers:
version: 4.2.2
geist:
specifier: ^1.3.1
version: 1.3.1(next@15.3.0-canary.29(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-e993439-20250328)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))
version: 1.3.1(next@15.3.0-canary.33(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-e993439-20250328)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))
html-entities:
specifier: ^2.6.0
version: 2.6.0
@ -72,8 +72,8 @@ importers:
specifier: ^3.0.1
version: 3.0.1
next:
specifier: 15.3.0-canary.29
version: 15.3.0-canary.29(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-e993439-20250328)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
specifier: 15.3.0-canary.33
version: 15.3.0-canary.33(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-e993439-20250328)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
polished:
specifier: ^4.3.1
version: 4.3.1
@ -202,8 +202,8 @@ importers:
specifier: ^9.23.0
version: 9.23.0
eslint-config-next:
specifier: 15.3.0-canary.29
version: 15.3.0-canary.29(eslint@9.23.0)(typescript@5.8.2)
specifier: 15.3.0-canary.33
version: 15.3.0-canary.33(eslint@9.23.0)(typescript@5.8.2)
eslint-config-prettier:
specifier: ^10.1.1
version: 10.1.1(eslint@9.23.0)
@ -652,17 +652,17 @@ packages:
'@napi-rs/wasm-runtime@0.2.8':
resolution: {integrity: sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg==}
'@next/bundle-analyzer@15.3.0-canary.29':
resolution: {integrity: sha512-siOmaq4mf5a0ZIVmRLLvxDXPB9u0jgHhatqd7quDIBQ95yuTOYcJiCkHmtPnw8xx0cCdMCEH22HqAfIEB2MTZg==}
'@next/bundle-analyzer@15.3.0-canary.33':
resolution: {integrity: sha512-n27QmTA0/AaoFD90hoC924r/OVMN6pSeUw8mGZkwQhSTh1Ns3thcHkoSP4UbZV2x5clxg4XBuH3qUwk2sMogtQ==}
'@next/env@15.3.0-canary.29':
resolution: {integrity: sha512-9n0tgLzP18NxWsJwJsRz7txV8XcGSYkfwmScxJnOCcbLjjowku74cQ4TVMAZ/vC0kkZdgkVysb0dp77iJoMJMQ==}
'@next/env@15.3.0-canary.33':
resolution: {integrity: sha512-yPuNYRSFLHtulgy5Mge7tEy2GT/SmIcC7ZM9mJRFVtCsjjR6hqqOkLdbu/6cl7qT0x5ADRfpokT5Rf5bTzUumA==}
'@next/eslint-plugin-next@15.3.0-canary.29':
resolution: {integrity: sha512-qn73vyDNsqfgodEd1/UM76LpPTkFKui0d8jQ3AvS3eybS+RCfY5j3WEhdqq5UFBIAitFClscr/sr5U6bRHvpQw==}
'@next/eslint-plugin-next@15.3.0-canary.33':
resolution: {integrity: sha512-Wc9n4UkDUuayQnehXy5AT50YoqpLqmeuM9QGZvQajtjwaQ2u002X0wCrJuSOHemHx0NdiZ72CHpfjbaRPCVZnw==}
'@next/mdx@15.3.0-canary.29':
resolution: {integrity: sha512-ZO8GXzt4IRdUVLPGY22ciLBzbXgDVkM1sM6wt5DnQ2Oi2RGxLmxPSW21z/rABVbStZ0u14csRZO+uN3csWbGug==}
'@next/mdx@15.3.0-canary.33':
resolution: {integrity: sha512-Zjc/0IUcAC3XQZj7NUAnR4XHZ2zH723DCrzDwzuIuIe4cYse8Sdy/a/6bGmFJCUdKzbNewGktIKDdNI8/uaS1g==}
peerDependencies:
'@mdx-js/loader': '>=0.15.0'
'@mdx-js/react': '>=0.15.0'
@ -672,50 +672,50 @@ packages:
'@mdx-js/react':
optional: true
'@next/swc-darwin-arm64@15.3.0-canary.29':
resolution: {integrity: sha512-EqZVuXjBIJ2RTcMhY6fWUndaPli/F5qLkJ2ubDz1Mgjq0voHLvjXr94Qdb6H0pNYgID/TEU2N5GX8LBH7kUafQ==}
'@next/swc-darwin-arm64@15.3.0-canary.33':
resolution: {integrity: sha512-oWIA9x2llzFxrvnz/6ZNmiMb5yrfR6WfRKa28mo+6c4e9r7M0gKFpNd5RbDZF+fF2RTetrv57ze+Cm9KkTUrSg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@15.3.0-canary.29':
resolution: {integrity: sha512-hOneW+Yo+jmV+Y+EVWwZTTV4npJhHF8vEv2L8ETN5vc7HgEV+SUd4kah7n80Mzn2gxChLzY2/OqAVRBMvQ8irQ==}
'@next/swc-darwin-x64@15.3.0-canary.33':
resolution: {integrity: sha512-Yj1S17ww6ga6VCCNbWrA0URQsuT3Xb5dE5hToo7OGf2NOFN9zEVMYbDp6CO4/ugOB4BslR8tzSVyfKyytibdCQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@15.3.0-canary.29':
resolution: {integrity: sha512-hTkjTojPdtI86DIc75QR2spGJ45pe0Pr8PzxvcgUDC4X5nOifQBho+5e7T4OZWYjRfON2Jv7Hrk9EMWi/E6iOw==}
'@next/swc-linux-arm64-gnu@15.3.0-canary.33':
resolution: {integrity: sha512-nkV4cw4w23qzpkgG9ITeLnXvLhV/i4ixy6NGJGCxOsYSoGXA5O6KD+ZEoYSSxRuM7rVVUfu6dO7A+W9s74U63Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@15.3.0-canary.29':
resolution: {integrity: sha512-AxY/VxNrbMIGLiuG2lhHAoi++k/VBvcugj5Oo5Ovk2gN+tKEJRryoffawELBpdm1eAiAOjn2fwQNb7xq4YZIvw==}
'@next/swc-linux-arm64-musl@15.3.0-canary.33':
resolution: {integrity: sha512-Y/se0HzGrooecLp1JWWgODK89te2+akTr88NkYKGW63rGVrJvevXDZ8jJQiSMCwUq+OAWBRoSyrswLI+/9VMOQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-x64-gnu@15.3.0-canary.29':
resolution: {integrity: sha512-KY1rz+M2XQhTOjgbJsXJfuK30m6mI1dMXdwRmTmWPwy+/9q0mtusp3oTi5uK/i40dQCUs3SPCWUFTsf0iqowUg==}
'@next/swc-linux-x64-gnu@15.3.0-canary.33':
resolution: {integrity: sha512-ENGvmcSyu51THeSG9i5vQmRVa6EriDmDVgnUGYm5o/X4lrU/xWNNMfpg9Sp0vjsyX3TCw6DSa5x+oZw8w4lPfw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@15.3.0-canary.29':
resolution: {integrity: sha512-bS92i+EPGq3yj9uME/h8j0rd1h1SyuPGnchS4NyV+GOW34JA78lrCL8NttyFcVhBU180vxn++gP0CroadXRIFg==}
'@next/swc-linux-x64-musl@15.3.0-canary.33':
resolution: {integrity: sha512-3UEL+tjNYKL/OG1hlR69C6S5voe9CXNX60O9Irl7TAZAxozdN+J3PrsfZ07QRduQIze0VORWpAh90LNnqXp1FQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-win32-arm64-msvc@15.3.0-canary.29':
resolution: {integrity: sha512-DI0kO4fsJduROSCNQgwzJmgXh2mjGItHDj3Q3SMyVhFcfTWGtbdD4ZXPkCp/6+T3Rso4WkIUefx+vgWbcP8TLg==}
'@next/swc-win32-arm64-msvc@15.3.0-canary.33':
resolution: {integrity: sha512-FQHFbBi340K4uAR/M7FbzHMnucXsPyuns2PKMy9PLaZKgXiBDkVAurI4FRlIKspG35vddnqybn4K/1BMqW4mWg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@15.3.0-canary.29':
resolution: {integrity: sha512-Xudgxsm4iNyNynZ2ZuMeCxMoPnkkRPgdXg24WwrkYM+kzlZi28sGX2BV7Nc2QNopDh7NRPIgpMgyeabJMJRqbQ==}
'@next/swc-win32-x64-msvc@15.3.0-canary.33':
resolution: {integrity: sha512-sctiGfzVmVMw+7995NzH5UM+kQr7bz0AJN07pVaubiT9ByRhcsp7WLnyWEVs2NYQWzVpHEfNwHs5PbZ4w0C3FQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@ -1534,8 +1534,8 @@ packages:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
eslint-config-next@15.3.0-canary.29:
resolution: {integrity: sha512-s0NPH9ZaNrugNbLGkDaBNokDKMSM8723Wzkj2WyQ4TZD0YSbqbEVqivmTN5GolGyrmDO4oxf7baZOGvhnJvRbw==}
eslint-config-next@15.3.0-canary.33:
resolution: {integrity: sha512-V3/yanFbPrLekEv93yp5WhJhMcE3Y5Ah633N9GZ3ueWAyT/VFd7BO5VRnHZlC9E2eIUSkD0At2OCXqVWr3mfzw==}
peerDependencies:
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
typescript: '>=3.3.1'
@ -2624,8 +2624,8 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
next@15.3.0-canary.29:
resolution: {integrity: sha512-TuZ5SWBVd7Imwvq1d/4YI6suKLFwVHAEZbqRJwI+qC0g4nQlRVQ8v4ZVPpwSYQmDahaJqZvWo6mqQCVcRYeRaA==}
next@15.3.0-canary.33:
resolution: {integrity: sha512-5Yc/W1hqOgibDoxnLiOvKHGId76/F+SrvlbZSw0LHhsmWYat6qAEaxv28vlHxj9OiRBqtrp0Ydsb+6RmYjv6IA==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
@ -4156,48 +4156,48 @@ snapshots:
'@tybys/wasm-util': 0.9.0
optional: true
'@next/bundle-analyzer@15.3.0-canary.29':
'@next/bundle-analyzer@15.3.0-canary.33':
dependencies:
webpack-bundle-analyzer: 4.10.1
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@next/env@15.3.0-canary.29': {}
'@next/env@15.3.0-canary.33': {}
'@next/eslint-plugin-next@15.3.0-canary.29':
'@next/eslint-plugin-next@15.3.0-canary.33':
dependencies:
fast-glob: 3.3.1
'@next/mdx@15.3.0-canary.29(@mdx-js/loader@3.1.0(acorn@8.14.1))(@mdx-js/react@3.1.0(@types/react@19.1.0)(react@19.1.0))':
'@next/mdx@15.3.0-canary.33(@mdx-js/loader@3.1.0(acorn@8.14.1))(@mdx-js/react@3.1.0(@types/react@19.1.0)(react@19.1.0))':
dependencies:
source-map: 0.7.4
optionalDependencies:
'@mdx-js/loader': 3.1.0(acorn@8.14.1)
'@mdx-js/react': 3.1.0(@types/react@19.1.0)(react@19.1.0)
'@next/swc-darwin-arm64@15.3.0-canary.29':
'@next/swc-darwin-arm64@15.3.0-canary.33':
optional: true
'@next/swc-darwin-x64@15.3.0-canary.29':
'@next/swc-darwin-x64@15.3.0-canary.33':
optional: true
'@next/swc-linux-arm64-gnu@15.3.0-canary.29':
'@next/swc-linux-arm64-gnu@15.3.0-canary.33':
optional: true
'@next/swc-linux-arm64-musl@15.3.0-canary.29':
'@next/swc-linux-arm64-musl@15.3.0-canary.33':
optional: true
'@next/swc-linux-x64-gnu@15.3.0-canary.29':
'@next/swc-linux-x64-gnu@15.3.0-canary.33':
optional: true
'@next/swc-linux-x64-musl@15.3.0-canary.29':
'@next/swc-linux-x64-musl@15.3.0-canary.33':
optional: true
'@next/swc-win32-arm64-msvc@15.3.0-canary.29':
'@next/swc-win32-arm64-msvc@15.3.0-canary.33':
optional: true
'@next/swc-win32-x64-msvc@15.3.0-canary.29':
'@next/swc-win32-x64-msvc@15.3.0-canary.33':
optional: true
'@nodelib/fs.scandir@2.1.5':
@ -5112,9 +5112,9 @@ snapshots:
escape-string-regexp@5.0.0: {}
eslint-config-next@15.3.0-canary.29(eslint@9.23.0)(typescript@5.8.2):
eslint-config-next@15.3.0-canary.33(eslint@9.23.0)(typescript@5.8.2):
dependencies:
'@next/eslint-plugin-next': 15.3.0-canary.29
'@next/eslint-plugin-next': 15.3.0-canary.33
'@rushstack/eslint-patch': 1.11.0
'@typescript-eslint/eslint-plugin': 8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0)(typescript@5.8.2)
'@typescript-eslint/parser': 8.29.0(eslint@9.23.0)(typescript@5.8.2)
@ -5531,9 +5531,9 @@ snapshots:
functions-have-names@1.2.3: {}
geist@1.3.1(next@15.3.0-canary.29(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-e993439-20250328)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)):
geist@1.3.1(next@15.3.0-canary.33(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-e993439-20250328)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)):
dependencies:
next: 15.3.0-canary.29(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-e993439-20250328)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
next: 15.3.0-canary.33(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-e993439-20250328)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
gensync@1.0.0-beta.2: {}
@ -6664,9 +6664,9 @@ snapshots:
natural-compare@1.4.0: {}
next@15.3.0-canary.29(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-e993439-20250328)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
next@15.3.0-canary.33(@babel/core@7.26.10)(babel-plugin-react-compiler@19.0.0-beta-e993439-20250328)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@next/env': 15.3.0-canary.29
'@next/env': 15.3.0-canary.33
'@swc/counter': 0.1.3
'@swc/helpers': 0.5.15
busboy: 1.6.0
@ -6676,14 +6676,14 @@ snapshots:
react-dom: 19.1.0(react@19.1.0)
styled-jsx: 5.1.6(@babel/core@7.26.10)(react@19.1.0)
optionalDependencies:
'@next/swc-darwin-arm64': 15.3.0-canary.29
'@next/swc-darwin-x64': 15.3.0-canary.29
'@next/swc-linux-arm64-gnu': 15.3.0-canary.29
'@next/swc-linux-arm64-musl': 15.3.0-canary.29
'@next/swc-linux-x64-gnu': 15.3.0-canary.29
'@next/swc-linux-x64-musl': 15.3.0-canary.29
'@next/swc-win32-arm64-msvc': 15.3.0-canary.29
'@next/swc-win32-x64-msvc': 15.3.0-canary.29
'@next/swc-darwin-arm64': 15.3.0-canary.33
'@next/swc-darwin-x64': 15.3.0-canary.33
'@next/swc-linux-arm64-gnu': 15.3.0-canary.33
'@next/swc-linux-arm64-musl': 15.3.0-canary.33
'@next/swc-linux-x64-gnu': 15.3.0-canary.33
'@next/swc-linux-x64-musl': 15.3.0-canary.33
'@next/swc-win32-arm64-msvc': 15.3.0-canary.33
'@next/swc-win32-x64-msvc': 15.3.0-canary.33
babel-plugin-react-compiler: 19.0.0-beta-e993439-20250328
sharp: 0.33.5
transitivePeerDependencies: