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:
parent
3043f4df8c
commit
64f5cd2c39
@ -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.",
|
||||
};
|
||||
}
|
||||
};
|
@ -24,7 +24,7 @@
|
||||
border-color: var(--colors-error);
|
||||
}
|
||||
|
||||
.fieldError {
|
||||
.errorMessage {
|
||||
font-size: 0.9em;
|
||||
color: var(--colors-error);
|
||||
}
|
||||
|
@ -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}>
|
||||
|
@ -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
43
app/contact/schema.ts
Normal 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;
|
@ -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];
|
||||
|
@ -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
126
pnpm-lock.yaml
generated
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user