diff --git a/.astro/types.d.ts b/.astro/types.d.ts index 03d7cc4..24a12e7 100644 --- a/.astro/types.d.ts +++ b/.astro/types.d.ts @@ -1,2 +1,3 @@ /// -/// \ No newline at end of file +/// +/// \ No newline at end of file diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..a60cee3 --- /dev/null +++ b/.env.template @@ -0,0 +1,2 @@ +BREVO_API_KEY= +BREVO_LIST_ID=3 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8f28c35..fc196bd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -5,6 +5,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: ./.github/actions/prepare + - run: cp .env.template .env - run: pnpm build lint: name: Lint @@ -12,6 +13,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: ./.github/actions/prepare + - run: pnpm astro sync - run: pnpm lint lint_knip: name: Lint Knip @@ -47,6 +49,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: ./.github/actions/prepare + - run: pnpm astro sync - run: pnpm tsc name: CI diff --git a/.gitignore b/.gitignore index 8d175bb..a77ba83 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .astro +.env .vercel /dist /node_modules \ No newline at end of file diff --git a/README.md b/README.md index 00640e0..01af889 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,11 @@ pnpm i pnpm dev ``` +### Environment Variables + +To get the newsletter API running, copy `.env.template` to an `.env` file and fill in the Brevo API key value from our password manager. +This is not necessary unless you want to work on the newsletter API. + ## Contributors diff --git a/astro.config.ts b/astro.config.ts index 01d66d3..578680b 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -1,11 +1,17 @@ import vercel from "@astrojs/vercel"; import { konamiEmojiBlast } from "@konami-emoji-blast/astro"; -import { defineConfig } from "astro/config"; +import { defineConfig, envField } from "astro/config"; export default defineConfig({ adapter: vercel({ webAnalytics: { enabled: true }, }), + env: { + schema: { + BREVO_API_KEY: envField.string({ access: "secret", context: "server" }), + BREVO_LIST_ID: envField.number({ access: "public", context: "server" }), + }, + }, image: { layout: "constrained", responsiveStyles: true, diff --git a/cspell.json b/cspell.json index fcc1c85..a05e510 100644 --- a/cspell.json +++ b/cspell.json @@ -16,6 +16,7 @@ "words": [ "astropub", "bootcamps", + "brevo", "d'œuvres", "devries", "dimitri", diff --git a/eslint.config.ts b/eslint.config.ts index 59a3de2..aa019bb 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -63,6 +63,7 @@ export default defineConfig( }, }, rules: { + "n/no-missing-import": "off", "n/no-unpublished-import": "off", // Stylistic concerns that don't interfere with Prettier diff --git a/package.json b/package.json index c885fbf..1b89b5e 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "sharp": "^0.34.5", "temporal-polyfill": "^0.3.0", "uqr": "^0.1.2", - "wifi-share-link": "^0.1.2" + "wifi-share-link": "^0.1.2", + "zod": "^4.3.6" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^4.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1e9dbb..dfd5c37 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: wifi-share-link: specifier: ^0.1.2 version: 0.1.2 + zod: + specifier: ^4.3.6 + version: 4.3.6 devDependencies: '@eslint-community/eslint-plugin-eslint-comments': specifier: ^4.6.0 diff --git a/src/components/Newsletter.astro b/src/components/Newsletter.astro index 85da574..927bfed 100644 --- a/src/components/Newsletter.astro +++ b/src/components/Newsletter.astro @@ -32,11 +32,7 @@ import Heading from "./Heading.astro"; Sign up to receive announcements and important SquiggleConf info. -
+ [response.ok, response.json(), response] as const) + .then((response) => [response.ok, response.text(), response] as const) .then(([ok, dataPromise, response]) => { dataPromise.then((data) => { - console.log({ data }); if (ok) { setMessage("Thanks for signing up!", "happy"); form.reset(); diff --git a/src/pages/api/newsletter.ts b/src/pages/api/newsletter.ts new file mode 100644 index 0000000..539203a --- /dev/null +++ b/src/pages/api/newsletter.ts @@ -0,0 +1,52 @@ +import { APIRoute } from "astro"; +import { BREVO_API_KEY, BREVO_LIST_ID } from "astro:env/server"; +import { z } from "zod"; + +const bodySchema = z.object({ + email: z.email(), +}); + +export const POST: APIRoute = async ({ request }) => { + const formData = Object.fromEntries(await request.formData()); + const body = bodySchema.safeParse(formData); + if (body.error) { + return new Response("Invalid body", { + status: 400, + statusText: body.error.message, + }); + } + + const { email } = body.data; + + try { + const response = await fetch("https://api.brevo.com/v3/contacts", { + body: JSON.stringify({ + email, + listIds: [BREVO_LIST_ID], + }), + headers: { + "api-key": BREVO_API_KEY, + "Content-Type": "application/json", + }, + method: "POST", + }); + + console.log(response); + + return response.ok || isBrevoDuplicateIdentifier(await response.json()) + ? new Response("Thanks for signing up!", { status: 200 }) + : new Response("Error", { status: 400 }); + } catch (error) { + console.error(error); + return new Response("Error", { status: 400 }); + } +}; + +function isBrevoDuplicateIdentifier(json: unknown) { + return ( + typeof json === "object" && + !!json && + "code" in json && + json.code === "duplicate_parameter" + ); +} diff --git a/tsconfig.json b/tsconfig.json index b54dffa..d82be47 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,5 +17,5 @@ "strict": true, "target": "ESNext" }, - "include": ["api", "src"] + "include": ["src"] }