diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..1c14150 --- /dev/null +++ b/.env.sample @@ -0,0 +1,9 @@ +# SMTP Config +SMTP_HOST= +SMTP_PORT= +SMTP_USER= +SMTP_PASS= +MAIL_TO= + +# Public +VITE_CONTACT_ENDPOINT= \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9a91606..157e827 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,2 @@ -* @AlejandroLuisHC \ No newline at end of file +* @AlejandroLuisHC +* @david-bardina \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 25b374b..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,35 +0,0 @@ -version: 2 -updates: - # Enable version updates for npm (pnpm) - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "weekly" - day: "monday" - time: "09:00" - open-pull-requests-limit: 5 - commit-message: - prefix: "deps" - include: "scope" - reviewers: - - "alher" - labels: - - "dependencies" - - "automated" - - # Enable version updates for GitHub Actions - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - day: "monday" - time: "09:00" - open-pull-requests-limit: 3 - commit-message: - prefix: "ci" - include: "scope" - reviewers: - - "alher" - labels: - - "github-actions" - - "automated" diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1b475d6..c1fdbdd 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -4,6 +4,8 @@ ``` blocknroll/ +├── api/ # Serverless functions API (email sending) +├── emails/ # Email templates ├── public/ # Static assets ├── src/ │ ├── assets/ # Images, docs, and other assets @@ -109,6 +111,8 @@ const ROUTES = { ### Path Aliases Available: - `@/` → `src/` +- `@api/` → `api/` +- `@emails/` → `emails/` - `@components/` → `src/components/` - `@pages/` → `src/pages/` - `@layouts/` → `src/layouts/` @@ -151,14 +155,3 @@ const ROUTES = { - Automated testing - Build verification - Coverage reporting - -## 🔄 Migration Benefits - -The new architecture provides: - -1. **Better separation** of concerns -2. **Easier navigation** for new developers -3. **Scalable routing** for multi-page apps -4. **Consistent layouts** across pages -5. **Cleaner imports** with path aliases -6. **Future-proof** structure for growth diff --git a/CHANGELOG.md b/CHANGELOG.md index 1660b39..c2a617a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [0.6.0] (2025-08-14) + +### Features + +- Add better contact form with email sending service: + - Add email templates + - Use nodemailer and maileroo to send emails + - Configure vercel serverless functions to manage email sending + ## [0.5.0] (2025-07-25) ### Features diff --git a/README.md b/README.md index 7d31c75..8fbf09e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A modern and welcoming landing page built with React and TypeScript. ## 🏐 About This Project -This is a responsive, multilingual website template designed specifically for Block n' Roll beach volleyball club. +This is a responsive, multilingual website designed specifically for Block n' Roll beach volleyball club. ### Key Features @@ -12,7 +12,7 @@ This is a responsive, multilingual website template designed specifically for Bl - **Multilingual Support**: Built-in internationalization (Spanish/English/Catalan) - **Service Showcase**: Flexible sections for training programs and pricing - **Dynamic Gallery**: Future Instagram integration with smart fallback to sample images -- **Contact Section**: User-friendly contact and inquiry section +- **Contact Section**: User-friendly contact and inquiry section with email sending service - **Performance Optimized**: Fast loading with modern build tools ## 🚀 Technologies Used @@ -23,6 +23,10 @@ This is a responsive, multilingual website template designed specifically for Bl - **Bootstrap** - CSS framework for responsive design - **react-i18next** - Internationalization library - **Vitest** - Testing framework +- **Vercel** - Serverless functions and deployment +- **Nodemailer** - Email sending library +- **Maileroo** - SMTP provider +- **React Email** - Email templates ## 📦 Installation @@ -52,7 +56,7 @@ This is a responsive, multilingual website template designed specifically for Bl 4. **Open your browser** Navigate to `http://localhost:3000` -## 🏗️ Project Structure +## 🏗️ Main Project Structure ``` src/ @@ -142,6 +146,12 @@ The gallery should automatically display Instagram posts when configured: - ✅ Manual refresh capability - ✅ Redirect to post/account +## 📧 Email Sending + +Emails are sent from a Serverless Function (`api/send-email.js`) using Nodemailer over SMTP. + +Note: Works with any SMTP provider (e.g., Maileroo, Gmail, etc.) by setting the variables accordingly. + ## 🧪 Testing The project includes comprehensive testing: @@ -159,7 +169,6 @@ npm run test:coverage ## 🎯 To-Do List - [ ] Instagram app integration -- [ ] Booking/contact system - [ ] SEO optimization - [ ] Blog functionality - [ ] Payment integration (?) diff --git a/api/send-email.js b/api/send-email.js new file mode 100644 index 0000000..22df9df --- /dev/null +++ b/api/send-email.js @@ -0,0 +1,109 @@ +import React from "react"; +import nodemailer from "nodemailer"; +import { render } from "@react-email/render"; +import ContactEmail from "../emails/ContactEmail.js"; + +export default async function handler(req, res) { + if (req.method !== "POST") { + res.status(405).json({ ok: false, error: "Method Not Allowed" }); + return; + } + + try { + const smtpHost = process.env.SMTP_HOST; + const smtpPort = Number(process.env.SMTP_PORT || 587); + const smtpUser = process.env.SMTP_USER; + const smtpPass = process.env.SMTP_PASS; + const mailTo = process.env.MAIL_TO; + + if (!smtpHost || !smtpUser || !smtpPass || !mailTo) { + res.status(500).json({ ok: false, error: "Missing SMTP configuration", code: "MISSING_SMTP_CONFIG" }); + return; + } + + const { name, email, phone, message, meta } = (req.body || {}); + + if (!message || !name) { + res.status(400).json({ ok: false, error: "Missing message or name", code: "BAD_REQUEST" }); + return; + } + + const transporter = nodemailer.createTransport({ + host: smtpHost, + port: smtpPort, + secure: smtpPort === 465, + auth: { user: smtpUser, pass: smtpPass }, + }); + + const inquiryType = (meta?.inquiryType === "talk" ? "talk" : "join"); + const subject = `BnR Web - ${inquiryType === "join" ? "Join" : "Info"} - ${name || "Web user"} - ${email || ""}`; + + const summaryRows = [ + ["Tipo de consulta", inquiryType === "join" ? "Apuntarse a entrenamientos" : "Información"], + ["Nombre", name || "-"], + ["Email", email || "-"], + ]; + if (phone) summaryRows.push(["Teléfono", phone]); + + const detailsRows = []; + // Format availability keys like "mon_18_1930" to human-readable Spanish labels + const formatAvailabilityLabel = (key) => { + if (typeof key !== "string") return String(key); + const [dayKey, ...rest] = key.split("_"); + const slotKey = rest.join("_"); + const dayMap = { mon: "Lun", tue: "Mar", wed: "Mié", thu: "Jue", fri: "Vie" }; + const slotMap = { "18_1930": "18:00-19:30", "1930_21": "19:30-21:00", "21_2230": "21:00-22:30" }; + const day = dayMap[dayKey] || dayKey; + const slot = slotMap[slotKey] || slotKey?.replace("_", ":")?.replace("_", ":"); + return slot ? `${day} ${slot}` : day; + }; + if (inquiryType === "join") { + if (meta?.players != null) detailsRows.push(["Número de jugadores", String(meta.players)]); + if (meta?.level) detailsRows.push(["Nivel estimado", String(meta.level)]); + if (meta?.packageType) detailsRows.push(["Paquete", String(meta.packageType) === "one_per_week" ? "Una vez por semana" : String(meta.packageType) === "two_per_week" ? "Dos veces por semana" : "Privados"]); + if (Array.isArray(meta?.availability)) { + const formatted = meta.availability.map(formatAvailabilityLabel); + detailsRows.push(["Disponibilidad", formatted.length ? formatted : ["Sin preferencia"]]); + } + } + + let html; + try { + html = await render( + React.createElement(ContactEmail, { + title: "New contact message", + summary: summaryRows, + details: detailsRows.length ? detailsRows : undefined, + message, + }) + ); + } catch (renderError) { + console.error("render error:", renderError); + const esc = (s) => String(s).replace(/&/g, "&").replace(//g, ">"); + const line = (k, v) => (v ? `

${esc(k)}: ${esc(v)}

` : ""); + const details = detailsRows.map(([k, v]) => line(k, v)).join(""); + html = ` +

New contact message

+ ${summaryRows.map(([k, v]) => line(k, v)).join("")} + ${details} +
+
${esc(message)}
+ `; + } + + const info = await transporter.sendMail({ + from: smtpUser, + to: mailTo, + replyTo: email || undefined, + subject, + html, + }); + + res.status(200).json({ ok: true, id: info.messageId }); + } catch (error) { + console.error("send-email error:", error); + res.status(500).json({ ok: false, error: "Email send failed", code: "SEND_FAILED" }); + } +} + + diff --git a/api/send-email.test.js b/api/send-email.test.js new file mode 100644 index 0000000..830a438 --- /dev/null +++ b/api/send-email.test.js @@ -0,0 +1,115 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import handler from "./send-email.js"; +import { render } from "@react-email/render"; + +// Hoisted placeholders so they can be referenced inside vi.mock factories +const { sendMailMock, createTransportMock } = vi.hoisted(() => ({ + sendMailMock: vi.fn(), + createTransportMock: vi.fn(), +})); + +vi.mock("nodemailer", () => ({ + default: { + createTransport: (...args) => { + createTransportMock(...args); + return { sendMail: sendMailMock }; + }, + }, +})); + +vi.mock("@react-email/render", () => ({ + render: vi.fn(async () => "ok"), +})); + +const createRes = () => { + const res = { + status: vi.fn().mockReturnThis(), + json: vi.fn(), + }; + return res; +}; + +describe("api/send-email", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.stubEnv("SMTP_HOST", "smtp.example.com"); + vi.stubEnv("SMTP_PORT", "587"); + vi.stubEnv("SMTP_USER", "user@example.com"); + vi.stubEnv("SMTP_PASS", "password"); + vi.stubEnv("MAIL_TO", "dest@example.com"); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("returns 405 for non-POST methods", async () => { + const req = { method: "GET" }; + const res = createRes(); + await handler(req, res); + expect(res.status).toHaveBeenCalledWith(405); + expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ ok: false })); + }); + + it("returns 500 when SMTP config is missing", async () => { + vi.unstubAllEnvs(); // simulate missing env + const req = { method: "POST", body: { name: "John", message: "Hi" } }; + const res = createRes(); + await handler(req, res); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ code: "MISSING_SMTP_CONFIG" }) + ); + }); + + it("returns 400 when required fields are missing", async () => { + const req = { method: "POST", body: { name: "", message: "" } }; + const res = createRes(); + await handler(req, res); + expect(res.status).toHaveBeenCalledWith(400); + }); + + it("sends email and formats availability labels to Spanish day/time", async () => { + const req = { + method: "POST", + body: { + name: "Jane", + email: "jane@example.com", + message: "Hello world", + meta: { + inquiryType: "join", + availability: ["mon_18_1930", "wed_1930_21", "thu_21_2230"], + }, + }, + }; + const res = createRes(); + await handler(req, res); + + // Response + expect(res.status).toHaveBeenCalledWith(200); + expect(sendMailMock).toHaveBeenCalled(); + + // Render call and props + const element = render.mock.calls[0][0]; + expect(element).toBeTruthy(); + const props = element.props || {}; + expect(Array.isArray(props.details)).toBe(true); + const availabilityRow = props.details.find((row) => row[0] === "Disponibilidad"); + expect(availabilityRow).toBeTruthy(); + expect(Array.isArray(availabilityRow[1])).toBe(true); + expect(availabilityRow[1]).toEqual(["Lun 18:00-19:30", "Mié 19:30-21:00", "Jue 21:00-22:30"]); + }); + + it("falls back to simple HTML when render throws", async () => { + render.mockImplementationOnce(async () => { + throw new Error("render boom"); + }); + + const req = { method: "POST", body: { name: "John", email: "john@example.com", message: "Hi" } }; + const res = createRes(); + await handler(req, res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(sendMailMock).toHaveBeenCalled(); + }); +}); diff --git a/emails/ContactEmail.js b/emails/ContactEmail.js new file mode 100644 index 0000000..fd124c6 --- /dev/null +++ b/emails/ContactEmail.js @@ -0,0 +1,148 @@ +import React from "react"; +import { + Html, + Head, + Preview, + Body, + Container, + Section, + Text, + Heading, + Hr, + Row, + Column, +} from "@react-email/components"; + +const ContactEmail = ({ title, summary, details, message }) => + React.createElement( + Html, + null, + React.createElement(Head, null), + React.createElement(Preview, null, title), + React.createElement( + Body, + { style: { backgroundColor: "#f6f9fc", fontFamily: "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif" } }, + React.createElement( + Container, + { style: { margin: "0 auto", padding: "24px", maxWidth: "640px" } }, + React.createElement( + Section, + { style: { backgroundColor: "#ffffff", border: "1px solid #e5e7eb", borderRadius: 12, padding: 24 } }, + React.createElement(Heading, { as: "h2", style: { margin: 0, marginBottom: 16, color: "#111827" } }, title), + React.createElement( + Section, + null, + ...summary.map(([k, v], idx) => + React.createElement(Row, { + key: k, + style: { backgroundColor: idx % 2 ? "#f9fafb" : "transparent", padding: "10px 0", borderBottom: "1px solid #f1f5f9" }, + children: [ + React.createElement( + Column, + { style: { width: 180 } }, + React.createElement(Text, { style: { margin: 0, fontWeight: 600, color: "#6b7280" } }, k) + ), + React.createElement( + Column, + null, + Array.isArray(v) + ? React.createElement( + "div", + null, + ...v.map((item, i) => + React.createElement( + "span", + { + key: i, + style: { + display: "inline-block", + backgroundColor: "#eef2ff", + color: "#3730a3", + padding: "4px 10px", + borderRadius: 9999, + fontSize: 12, + marginRight: 6, + marginBottom: 6, + }, + }, + item + ) + ) + ) + : React.createElement(Text, { style: { margin: 0, color: "#111827" } }, v || "-") + ), + ], + }) + ) + ), + details && details.length + ? React.createElement( + Section, + null, + React.createElement(Hr, null), + React.createElement(Heading, { as: "h3", style: { margin: 0, marginBottom: 8, fontSize: 16, color: "#111827" } }, "Detalles"), + ...(details || []).map(([k, v], idx) => + React.createElement(Row, { + key: k, + style: { backgroundColor: idx % 2 ? "#f9fafb" : "transparent", padding: "10px 0", borderBottom: "1px solid #f1f5f9" }, + children: [ + React.createElement( + Column, + { style: { width: 180 } }, + React.createElement(Text, { style: { margin: 0, fontWeight: 600, color: "#6b7280" } }, k) + ), + React.createElement( + Column, + null, + Array.isArray(v) + ? React.createElement( + "div", + null, + ...v.map((item, i) => + React.createElement( + "span", + { + key: i, + style: { + display: "inline-block", + backgroundColor: "#eef2ff", + color: "#3730a3", + padding: "4px 10px", + borderRadius: 9999, + fontSize: 12, + marginRight: 6, + marginBottom: 6, + }, + }, + item + ) + ) + ) + : React.createElement(Text, { style: { margin: 0, color: "#111827" } }, v || "-") + ), + ], + }) + ) + ) + : null, + message + ? React.createElement( + React.Fragment, + null, + React.createElement(Hr, null), + React.createElement( + Section, + null, + React.createElement(Heading, { as: "h3", style: { margin: 0, marginBottom: 8, fontSize: 16, color: "#111827" } }, "Mensaje"), + React.createElement(Text, { style: { whiteSpace: "pre-wrap", margin: 0 } }, message) + ) + ) + : null + ) + ) + ) + ); + +export default ContactEmail; + + diff --git a/package.json b/package.json index 4a4e4b2..3ff8b4f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "blocknroll", "private": true, - "version": "0.5.0", + "version": "0.6.0", "description": "Block n' Roll - Club de Voleibol de Playa en San Sebastián", "type": "module", "author": "Alejandro L. Herrero ", @@ -22,11 +22,14 @@ "test:cover": "vitest run --coverage" }, "dependencies": { + "@react-email/components": "^0.5.0", + "@react-email/render": "^1.2.0", "bootstrap": "^5.3.2", "flag-icons": "^7.5.0", "i18next": "^25.2.1", "i18next-browser-languagedetector": "^8.2.0", "lucide-react": "^0.522.0", + "nodemailer": "^7.0.5", "react": "^19.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.5.3", @@ -34,15 +37,15 @@ }, "devDependencies": { "@eslint/js": "^9.25.0", - "@testing-library/jest-dom": "^6.6.3", + "@testing-library/jest-dom": "^6.6.4", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", - "@vitejs/plugin-react": "^4.4.1", + "@vitejs/plugin-react": "^5.0.0", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", - "eslint": "^9.25.0", + "eslint": "^9.33.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9486853..fa23268 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + '@react-email/components': + specifier: ^0.5.0 + version: 0.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-email/render': + specifier: ^1.2.0 + version: 1.2.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) bootstrap: specifier: ^5.3.2 version: 5.3.7(@popperjs/core@2.11.8) @@ -23,6 +29,9 @@ importers: lucide-react: specifier: ^0.522.0 version: 0.522.0(react@19.1.0) + nodemailer: + specifier: ^7.0.5 + version: 7.0.5 react: specifier: ^19.1.0 version: 19.1.0 @@ -40,8 +49,8 @@ importers: specifier: ^9.25.0 version: 9.29.0 '@testing-library/jest-dom': - specifier: ^6.6.3 - version: 6.6.3 + specifier: ^6.6.4 + version: 6.6.4 '@testing-library/react': specifier: ^16.3.0 version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -55,8 +64,8 @@ importers: specifier: ^19.1.2 version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': - specifier: ^4.4.1 - version: 4.6.0(vite@6.3.5) + specifier: ^5.0.0 + version: 5.0.0(vite@6.3.5) '@vitest/coverage-v8': specifier: ^3.2.4 version: 3.2.4(vitest@3.2.4) @@ -64,14 +73,14 @@ importers: specifier: ^3.2.4 version: 3.2.4(vitest@3.2.4) eslint: - specifier: ^9.25.0 - version: 9.29.0 + specifier: ^9.33.0 + version: 9.33.0 eslint-plugin-react-hooks: specifier: ^5.2.0 - version: 5.2.0(eslint@9.29.0) + version: 5.2.0(eslint@9.33.0) eslint-plugin-react-refresh: specifier: ^0.4.19 - version: 0.4.20(eslint@9.29.0) + version: 0.4.20(eslint@9.33.0) globals: specifier: ^16.0.0 version: 16.2.0 @@ -83,7 +92,7 @@ importers: version: 5.8.3 typescript-eslint: specifier: ^8.30.1 - version: 8.35.0(eslint@9.29.0)(typescript@5.8.3) + version: 8.35.0(eslint@9.33.0)(typescript@5.8.3) vite: specifier: ^6.3.5 version: 6.3.5 @@ -111,18 +120,22 @@ packages: resolution: {integrity: sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==} engines: {node: '>=6.9.0'} - '@babel/core@7.27.4': - resolution: {integrity: sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==} + '@babel/core@7.28.0': + resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} engines: {node: '>=6.9.0'} - '@babel/generator@7.27.5': - resolution: {integrity: sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==} + '@babel/generator@7.28.0': + resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} engines: {node: '>=6.9.0'} '@babel/helper-compilation-targets@7.27.2': resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.27.1': resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} @@ -158,6 +171,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.28.0': + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-transform-react-jsx-self@7.27.1': resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} engines: {node: '>=6.9.0'} @@ -178,14 +196,18 @@ packages: resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.27.4': - resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==} + '@babel/traverse@7.28.0': + resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} engines: {node: '>=6.9.0'} '@babel/types@7.27.6': resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==} engines: {node: '>=6.9.0'} + '@babel/types@7.28.2': + resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -378,20 +400,16 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.20.1': - resolution: {integrity: sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/config-helpers@0.2.3': - resolution: {integrity: sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==} + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.14.0': - resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} + '@eslint/config-helpers@0.3.1': + resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.15.0': - resolution: {integrity: sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==} + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/eslintrc@3.3.1': @@ -402,12 +420,16 @@ packages: resolution: {integrity: sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@9.33.0': + resolution: {integrity: sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/object-schema@2.1.6': resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.3.2': - resolution: {integrity: sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==} + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@humanfs/core@0.19.1': @@ -438,6 +460,9 @@ packages: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -456,6 +481,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.30': + resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -478,8 +506,133 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@rolldown/pluginutils@1.0.0-beta.19': - resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==} + '@react-email/body@0.1.0': + resolution: {integrity: sha512-o1bcSAmDYNNHECbkeyceCVPGmVsYvT+O3sSO/Ct7apKUu3JphTi31hu+0Nwqr/pgV5QFqdoT5vdS3SW5DJFHgQ==} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/button@0.2.0': + resolution: {integrity: sha512-8i+v6cMxr2emz4ihCrRiYJPp2/sdYsNNsBzXStlcA+/B9Umpm5Jj3WJKYpgTPM+aeyiqlG/MMI1AucnBm4f1oQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/code-block@0.1.0': + resolution: {integrity: sha512-jSpHFsgqnQXxDIssE4gvmdtFncaFQz5D6e22BnVjcCPk/udK+0A9jRwGFEG8JD2si9ZXBmU4WsuqQEczuZn4ww==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/code-inline@0.0.5': + resolution: {integrity: sha512-MmAsOzdJpzsnY2cZoPHFPk6uDO/Ncpb4Kh1hAt9UZc1xOW3fIzpe1Pi9y9p6wwUmpaeeDalJxAxH6/fnTquinA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/column@0.0.13': + resolution: {integrity: sha512-Lqq17l7ShzJG/d3b1w/+lVO+gp2FM05ZUo/nW0rjxB8xBICXOVv6PqjDnn3FXKssvhO5qAV20lHM6S+spRhEwQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/components@0.5.0': + resolution: {integrity: sha512-esRbP+yMmSkNP9hcpiy2RwpDnvSmlxJcJ1HHbzSwlACGlCHTap+ma344QovvzhpVRhMccyWemdClLG822UvVpQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/container@0.0.15': + resolution: {integrity: sha512-Qo2IQo0ru2kZq47REmHW3iXjAQaKu4tpeq/M8m1zHIVwKduL2vYOBQWbC2oDnMtWPmkBjej6XxgtZByxM6cCFg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/font@0.0.9': + resolution: {integrity: sha512-4zjq23oT9APXkerqeslPH3OZWuh5X4crHK6nx82mVHV2SrLba8+8dPEnWbaACWTNjOCbcLIzaC9unk7Wq2MIXw==} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/head@0.0.12': + resolution: {integrity: sha512-X2Ii6dDFMF+D4niNwMAHbTkeCjlYYnMsd7edXOsi0JByxt9wNyZ9EnhFiBoQdqkE+SMDcu8TlNNttMrf5sJeMA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/heading@0.0.15': + resolution: {integrity: sha512-xF2GqsvBrp/HbRHWEfOgSfRFX+Q8I5KBEIG5+Lv3Vb2R/NYr0s8A5JhHHGf2pWBMJdbP4B2WHgj/VUrhy8dkIg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/hr@0.0.11': + resolution: {integrity: sha512-S1gZHVhwOsd1Iad5IFhpfICwNPMGPJidG/Uysy1AwmspyoAP5a4Iw3OWEpINFdgh9MHladbxcLKO2AJO+cA9Lw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/html@0.0.11': + resolution: {integrity: sha512-qJhbOQy5VW5qzU74AimjAR9FRFQfrMa7dn4gkEXKMB/S9xZN8e1yC1uA9C15jkXI/PzmJ0muDIWmFwatm5/+VA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/img@0.0.11': + resolution: {integrity: sha512-aGc8Y6U5C3igoMaqAJKsCpkbm1XjguQ09Acd+YcTKwjnC2+0w3yGUJkjWB2vTx4tN8dCqQCXO8FmdJpMfOA9EQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/link@0.0.12': + resolution: {integrity: sha512-vF+xxQk2fGS1CN7UPQDbzvcBGfffr+GjTPNiWM38fhBfsLv6A/YUfaqxWlmL7zLzVmo0K2cvvV9wxlSyNba1aQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/markdown@0.0.15': + resolution: {integrity: sha512-UQA9pVm5sbflgtg3EX3FquUP4aMBzmLReLbGJ6DZQZnAskBF36aI56cRykDq1o+1jT+CKIK1CducPYziaXliag==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/preview@0.0.13': + resolution: {integrity: sha512-F7j9FJ0JN/A4d7yr+aw28p4uX7VLWs7hTHtLo7WRyw4G+Lit6Zucq4UWKRxJC8lpsUdzVmG7aBJnKOT+urqs/w==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/render@1.2.0': + resolution: {integrity: sha512-5fpbV16VYR9Fmk8t7xiwPNAjxjdI8XzVtlx9J9OkhOsIHdr2s5DwAj8/MXzWa9qRYJyLirQ/l7rBSjjgyRAomw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/row@0.0.12': + resolution: {integrity: sha512-HkCdnEjvK3o+n0y0tZKXYhIXUNPDx+2vq1dJTmqappVHXS5tXS6W5JOPZr5j+eoZ8gY3PShI2LWj5rWF7ZEtIQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/section@0.0.16': + resolution: {integrity: sha512-FjqF9xQ8FoeUZYKSdt8sMIKvoT9XF8BrzhT3xiFKdEMwYNbsDflcjfErJe3jb7Wj/es/lKTbV5QR1dnLzGpL3w==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/tailwind@1.2.2': + resolution: {integrity: sha512-heO9Khaqxm6Ulm6p7HQ9h01oiiLRrZuuEQuYds/O7Iyp3c58sMVHZGIxiRXO/kSs857NZQycpjewEVKF3jhNTw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/text@0.1.5': + resolution: {integrity: sha512-o5PNHFSE085VMXayxH+SJ1LSOtGsTv+RpNKnTiJDrJUwoBu77G3PlKOsZZQHCNyD28WsQpl9v2WcJLbQudqwPg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@rolldown/pluginutils@1.0.0-beta.30': + resolution: {integrity: sha512-whXaSoNUFiyDAjkUF8OBpOm77Szdbk5lGNqFe6CbVbJFrhCCPinCbRA3NjawwlNHla1No7xvXXh+CpSxnPfUEw==} '@rollup/rollup-android-arm-eabi@4.44.0': resolution: {integrity: sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==} @@ -581,12 +734,15 @@ packages: cpu: [x64] os: [win32] + '@selderee/plugin-htmlparser2@0.11.0': + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} - '@testing-library/jest-dom@6.6.3': - resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} + '@testing-library/jest-dom@6.6.4': + resolution: {integrity: sha512-xDXgLjVunjHqczScfkCJ9iyjdNOVHvvCdqHSSxwM9L0l/wHkTRum67SDc020uAlCoqktJplgO2AAQeLP1wgqDQ==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} '@testing-library/react@16.3.0': @@ -704,11 +860,11 @@ packages: resolution: {integrity: sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitejs/plugin-react@4.6.0': - resolution: {integrity: sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==} - engines: {node: ^14.18.0 || >=16.0.0} + '@vitejs/plugin-react@5.0.0': + resolution: {integrity: sha512-Jx9JfsTa05bYkS9xo0hkofp2dCmp1blrKjw9JONs5BTHOvJCgLbaPSuZLGSVJW6u2qe0tc4eevY0+gSNNi0YCw==} + engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 '@vitest/coverage-v8@3.2.4': resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==} @@ -845,10 +1001,6 @@ packages: resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} engines: {node: '>=12'} - chalk@3.0.0: - resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} - engines: {node: '>=8'} - chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -911,6 +1063,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -921,6 +1077,19 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -933,6 +1102,10 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} @@ -976,8 +1149,8 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.29.0: - resolution: {integrity: sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==} + eslint@9.33.0: + resolution: {integrity: sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -1013,6 +1186,9 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + fast-deep-equal@2.0.1: + resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1087,10 +1263,6 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -1116,6 +1288,13 @@ packages: html-parse-stringify@3.0.1: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1241,6 +1420,9 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -1283,6 +1465,16 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + marked@7.0.4: + resolution: {integrity: sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==} + engines: {node: '>= 16'} + hasBin: true + + md-to-react-email@5.0.5: + resolution: {integrity: sha512-OvAXqwq57uOk+WZqFFNCMZz8yDp8BD3WazW1wAKHUrPbbdr89K9DWS6JXY09vd9xNdPNeurI8DU/X4flcfaD8A==} + peerDependencies: + react: ^18.0 || ^19.0 + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1324,6 +1516,10 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + nodemailer@7.0.5: + resolution: {integrity: sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==} + engines: {node: '>=6.0.0'} + nwsapi@2.2.20: resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} @@ -1349,6 +1545,9 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1368,6 +1567,9 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} + peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1387,10 +1589,19 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1422,6 +1633,9 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-promise-suspense@0.3.4: + resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==} + react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -1480,6 +1694,9 @@ packages: scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -1805,18 +2022,18 @@ snapshots: '@babel/compat-data@7.27.5': {} - '@babel/core@7.27.4': + '@babel/core@7.28.0': dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.5 + '@babel/generator': 7.28.0 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) '@babel/helpers': 7.27.6 - '@babel/parser': 7.27.5 + '@babel/parser': 7.28.0 '@babel/template': 7.27.2 - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.6 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.2 convert-source-map: 2.0.0 debug: 4.4.1 gensync: 1.0.0-beta.2 @@ -1825,12 +2042,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.27.5': + '@babel/generator@7.28.0': dependencies: - '@babel/parser': 7.27.5 - '@babel/types': 7.27.6 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.2 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 jsesc: 3.1.0 '@babel/helper-compilation-targets@7.27.2': @@ -1841,19 +2058,21 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-globals@7.28.0': {} + '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.6 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.4)': + '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.28.0 '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.27.4 + '@babel/traverse': 7.28.0 transitivePeerDependencies: - supports-color @@ -1868,20 +2087,24 @@ snapshots: '@babel/helpers@7.27.6': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.27.6 + '@babel/types': 7.28.2 '@babel/parser@7.27.5': dependencies: '@babel/types': 7.27.6 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.4)': + '@babel/parser@7.28.0': dependencies: - '@babel/core': 7.27.4 + '@babel/types': 7.28.2 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.4)': + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 '@babel/runtime@7.27.6': {} @@ -1889,18 +2112,18 @@ snapshots: '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.27.5 - '@babel/types': 7.27.6 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.2 - '@babel/traverse@7.27.4': + '@babel/traverse@7.28.0': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.5 - '@babel/parser': 7.27.5 + '@babel/generator': 7.28.0 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.0 '@babel/template': 7.27.2 - '@babel/types': 7.27.6 + '@babel/types': 7.28.2 debug: 4.4.1 - globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -1909,6 +2132,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@babel/types@7.28.2': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@bcoe/v8-coverage@1.0.2': {} '@csstools/color-helpers@5.0.2': {} @@ -2006,14 +2234,14 @@ snapshots: '@esbuild/win32-x64@0.25.5': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.29.0)': + '@eslint-community/eslint-utils@4.7.0(eslint@9.33.0)': dependencies: - eslint: 9.29.0 + eslint: 9.33.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/config-array@0.20.1': + '@eslint/config-array@0.21.0': dependencies: '@eslint/object-schema': 2.1.6 debug: 4.4.1 @@ -2021,13 +2249,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.2.3': {} + '@eslint/config-helpers@0.3.1': {} - '@eslint/core@0.14.0': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/core@0.15.0': + '@eslint/core@0.15.2': dependencies: '@types/json-schema': 7.0.15 @@ -2047,11 +2271,13 @@ snapshots: '@eslint/js@9.29.0': {} + '@eslint/js@9.33.0': {} + '@eslint/object-schema@2.1.6': {} - '@eslint/plugin-kit@0.3.2': + '@eslint/plugin-kit@0.3.5': dependencies: - '@eslint/core': 0.15.0 + '@eslint/core': 0.15.2 levn: 0.4.1 '@humanfs/core@0.19.1': {} @@ -2078,6 +2304,11 @@ snapshots: '@istanbuljs/schema@0.1.3': {} + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -2095,6 +2326,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.30': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2114,7 +2350,119 @@ snapshots: '@popperjs/core@2.11.8': {} - '@rolldown/pluginutils@1.0.0-beta.19': {} + '@react-email/body@0.1.0(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@react-email/button@0.2.0(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@react-email/code-block@0.1.0(react@19.1.0)': + dependencies: + prismjs: 1.30.0 + react: 19.1.0 + + '@react-email/code-inline@0.0.5(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@react-email/column@0.0.13(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@react-email/components@0.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@react-email/body': 0.1.0(react@19.1.0) + '@react-email/button': 0.2.0(react@19.1.0) + '@react-email/code-block': 0.1.0(react@19.1.0) + '@react-email/code-inline': 0.0.5(react@19.1.0) + '@react-email/column': 0.0.13(react@19.1.0) + '@react-email/container': 0.0.15(react@19.1.0) + '@react-email/font': 0.0.9(react@19.1.0) + '@react-email/head': 0.0.12(react@19.1.0) + '@react-email/heading': 0.0.15(react@19.1.0) + '@react-email/hr': 0.0.11(react@19.1.0) + '@react-email/html': 0.0.11(react@19.1.0) + '@react-email/img': 0.0.11(react@19.1.0) + '@react-email/link': 0.0.12(react@19.1.0) + '@react-email/markdown': 0.0.15(react@19.1.0) + '@react-email/preview': 0.0.13(react@19.1.0) + '@react-email/render': 1.2.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-email/row': 0.0.12(react@19.1.0) + '@react-email/section': 0.0.16(react@19.1.0) + '@react-email/tailwind': 1.2.2(react@19.1.0) + '@react-email/text': 0.1.5(react@19.1.0) + react: 19.1.0 + transitivePeerDependencies: + - react-dom + + '@react-email/container@0.0.15(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@react-email/font@0.0.9(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@react-email/head@0.0.12(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@react-email/heading@0.0.15(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@react-email/hr@0.0.11(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@react-email/html@0.0.11(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@react-email/img@0.0.11(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@react-email/link@0.0.12(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@react-email/markdown@0.0.15(react@19.1.0)': + dependencies: + md-to-react-email: 5.0.5(react@19.1.0) + react: 19.1.0 + + '@react-email/preview@0.0.13(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@react-email/render@1.2.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + html-to-text: 9.0.5 + prettier: 3.6.2 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-promise-suspense: 0.3.4 + + '@react-email/row@0.0.12(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@react-email/section@0.0.16(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@react-email/tailwind@1.2.2(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@react-email/text@0.1.5(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@rolldown/pluginutils@1.0.0-beta.30': {} '@rollup/rollup-android-arm-eabi@4.44.0': optional: true @@ -2176,6 +2524,11 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.44.0': optional: true + '@selderee/plugin-htmlparser2@0.11.0': + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.27.1 @@ -2187,14 +2540,14 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.6.3': + '@testing-library/jest-dom@6.6.4': dependencies: '@adobe/css-tools': 4.4.3 aria-query: 5.3.2 - chalk: 3.0.0 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 lodash: 4.17.21 + picocolors: 1.1.1 redent: 3.0.0 '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': @@ -2252,15 +2605,15 @@ snapshots: dependencies: csstype: 3.1.3 - '@typescript-eslint/eslint-plugin@8.35.0(@typescript-eslint/parser@8.35.0(eslint@9.29.0)(typescript@5.8.3))(eslint@9.29.0)(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.35.0(@typescript-eslint/parser@8.35.0(eslint@9.33.0)(typescript@5.8.3))(eslint@9.33.0)(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.35.0(eslint@9.29.0)(typescript@5.8.3) + '@typescript-eslint/parser': 8.35.0(eslint@9.33.0)(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.35.0 - '@typescript-eslint/type-utils': 8.35.0(eslint@9.29.0)(typescript@5.8.3) - '@typescript-eslint/utils': 8.35.0(eslint@9.29.0)(typescript@5.8.3) + '@typescript-eslint/type-utils': 8.35.0(eslint@9.33.0)(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.0(eslint@9.33.0)(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.35.0 - eslint: 9.29.0 + eslint: 9.33.0 graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -2269,14 +2622,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.35.0(eslint@9.29.0)(typescript@5.8.3)': + '@typescript-eslint/parser@8.35.0(eslint@9.33.0)(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.35.0 '@typescript-eslint/types': 8.35.0 '@typescript-eslint/typescript-estree': 8.35.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.35.0 debug: 4.4.1 - eslint: 9.29.0 + eslint: 9.33.0 typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -2299,12 +2652,12 @@ snapshots: dependencies: typescript: 5.8.3 - '@typescript-eslint/type-utils@8.35.0(eslint@9.29.0)(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.35.0(eslint@9.33.0)(typescript@5.8.3)': dependencies: '@typescript-eslint/typescript-estree': 8.35.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.35.0(eslint@9.29.0)(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.0(eslint@9.33.0)(typescript@5.8.3) debug: 4.4.1 - eslint: 9.29.0 + eslint: 9.33.0 ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: @@ -2328,13 +2681,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.35.0(eslint@9.29.0)(typescript@5.8.3)': + '@typescript-eslint/utils@8.35.0(eslint@9.33.0)(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0) '@typescript-eslint/scope-manager': 8.35.0 '@typescript-eslint/types': 8.35.0 '@typescript-eslint/typescript-estree': 8.35.0(typescript@5.8.3) - eslint: 9.29.0 + eslint: 9.33.0 typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -2344,12 +2697,12 @@ snapshots: '@typescript-eslint/types': 8.35.0 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-react@4.6.0(vite@6.3.5)': + '@vitejs/plugin-react@5.0.0(vite@6.3.5)': dependencies: - '@babel/core': 7.27.4 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.4) - '@rolldown/pluginutils': 1.0.0-beta.19 + '@babel/core': 7.28.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0) + '@rolldown/pluginutils': 1.0.0-beta.30 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 vite: 6.3.5 @@ -2511,11 +2864,6 @@ snapshots: loupe: 3.1.4 pathval: 2.0.1 - chalk@3.0.0: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -2565,12 +2913,32 @@ snapshots: deep-is@0.1.4: {} + deepmerge@4.3.1: {} + dequal@2.0.3: {} dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + eastasianwidth@0.2.0: {} electron-to-chromium@1.5.172: {} @@ -2579,6 +2947,8 @@ snapshots: emoji-regex@9.2.2: {} + entities@4.5.0: {} + entities@6.0.1: {} es-module-lexer@1.7.0: {} @@ -2615,13 +2985,13 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-react-hooks@5.2.0(eslint@9.29.0): + eslint-plugin-react-hooks@5.2.0(eslint@9.33.0): dependencies: - eslint: 9.29.0 + eslint: 9.33.0 - eslint-plugin-react-refresh@0.4.20(eslint@9.29.0): + eslint-plugin-react-refresh@0.4.20(eslint@9.33.0): dependencies: - eslint: 9.29.0 + eslint: 9.33.0 eslint-scope@8.4.0: dependencies: @@ -2632,16 +3002,16 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.29.0: + eslint@9.33.0: dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0) '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.20.1 - '@eslint/config-helpers': 0.2.3 - '@eslint/core': 0.14.0 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.1 + '@eslint/core': 0.15.2 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.29.0 - '@eslint/plugin-kit': 0.3.2 + '@eslint/js': 9.33.0 + '@eslint/plugin-kit': 0.3.5 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 @@ -2696,6 +3066,8 @@ snapshots: expect-type@1.2.2: {} + fast-deep-equal@2.0.1: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -2769,8 +3141,6 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - globals@11.12.0: {} - globals@14.0.0: {} globals@16.2.0: {} @@ -2789,6 +3159,21 @@ snapshots: dependencies: void-elements: 3.1.0 + html-to-text@9.0.5: + dependencies: + '@selderee/plugin-htmlparser2': 0.11.0 + deepmerge: 4.3.1 + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + selderee: 0.11.0 + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 @@ -2920,6 +3305,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + leac@0.6.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -2961,6 +3348,13 @@ snapshots: dependencies: semver: 7.7.2 + marked@7.0.4: {} + + md-to-react-email@5.0.5(react@19.1.0): + dependencies: + marked: 7.0.4 + react: 19.1.0 + merge2@1.4.1: {} micromatch@4.0.8: @@ -2990,6 +3384,8 @@ snapshots: node-releases@2.0.19: {} + nodemailer@7.0.5: {} + nwsapi@2.2.20: {} optionator@0.9.4: @@ -3019,6 +3415,11 @@ snapshots: dependencies: entities: 6.0.1 + parseley@0.12.1: + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + path-exists@4.0.0: {} path-key@3.1.1: {} @@ -3032,6 +3433,8 @@ snapshots: pathval@2.0.1: {} + peberminta@0.9.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -3046,12 +3449,16 @@ snapshots: prelude-ls@1.2.1: {} + prettier@3.6.2: {} + pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 ansi-styles: 5.2.0 react-is: 17.0.2 + prismjs@1.30.0: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -3073,6 +3480,10 @@ snapshots: react-is@17.0.2: {} + react-promise-suspense@0.3.4: + dependencies: + fast-deep-equal: 2.0.1 + react-refresh@0.17.0: {} react-router-dom@7.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): @@ -3140,6 +3551,10 @@ snapshots: scheduler@0.26.0: {} + selderee@0.11.0: + dependencies: + parseley: 0.12.1 + semver@6.3.1: {} semver@7.7.2: {} @@ -3253,12 +3668,12 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.35.0(eslint@9.29.0)(typescript@5.8.3): + typescript-eslint@8.35.0(eslint@9.33.0)(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.35.0(@typescript-eslint/parser@8.35.0(eslint@9.29.0)(typescript@5.8.3))(eslint@9.29.0)(typescript@5.8.3) - '@typescript-eslint/parser': 8.35.0(eslint@9.29.0)(typescript@5.8.3) - '@typescript-eslint/utils': 8.35.0(eslint@9.29.0)(typescript@5.8.3) - eslint: 9.29.0 + '@typescript-eslint/eslint-plugin': 8.35.0(@typescript-eslint/parser@8.35.0(eslint@9.33.0)(typescript@5.8.3))(eslint@9.33.0)(typescript@5.8.3) + '@typescript-eslint/parser': 8.35.0(eslint@9.33.0)(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.0(eslint@9.33.0)(typescript@5.8.3) + eslint: 9.33.0 typescript: 5.8.3 transitivePeerDependencies: - supports-color diff --git a/src/assets/img/Logo-official-no-text.png b/public/logo-official-no-text.png similarity index 100% rename from src/assets/img/Logo-official-no-text.png rename to public/logo-official-no-text.png diff --git a/src/assets/img/net-couple-blurry-gpt.png b/src/assets/img/net-couple-blurry-gpt.png deleted file mode 100644 index c7c44f1..0000000 Binary files a/src/assets/img/net-couple-blurry-gpt.png and /dev/null differ diff --git a/src/components/Contact.test.tsx b/src/components/Contact.test.tsx index 0751ef1..2eec163 100644 --- a/src/components/Contact.test.tsx +++ b/src/components/Contact.test.tsx @@ -13,13 +13,6 @@ vi.mock("react-i18next", () => ({ "contact.subtitle": "Ready to start your beach volleyball journey?", "contact.form.title": "Send us a message", "contact.form.name": "Full Name", - "contact.form.email": "Email Address", - "contact.form.phone": "Phone Number", - "contact.form.program": "Program Interest", - "contact.form.programs.basic": "Basic Training", - "contact.form.programs.competitive": "Competitive", - "contact.form.programs.elite": "Elite Program", - "contact.form.programs.other": "Other", "contact.form.message": "Message", "contact.form.send": "Send Message", "contact.info.title": "Contact Information", @@ -52,9 +45,6 @@ vi.mock("../hooks/useContactForm", () => ({ useContactForm: () => ({ formData: { name: "", - email: "", - phone: "", - program: "basic", message: "", }, handleSubmit: mockHandleSubmit, @@ -98,14 +88,4 @@ describe("Contact Component - Business Logic Tests", () => { // The actual submission is handled by the useContactForm hook, // which is tested separately. This tests the integration exists. }); - - it("includes program selection options", () => { - render(); - - // Test that all program options are available (business logic) - expect(screen.getByText("Basic Training")).toBeInTheDocument(); - expect(screen.getByText("Competitive")).toBeInTheDocument(); - expect(screen.getByText("Elite Program")).toBeInTheDocument(); - expect(screen.getByText("Other")).toBeInTheDocument(); - }); }); diff --git a/src/components/Contact.tsx b/src/components/Contact.tsx index 8672242..e8cc5f8 100644 --- a/src/components/Contact.tsx +++ b/src/components/Contact.tsx @@ -1,15 +1,8 @@ import { useTranslation } from "react-i18next"; import { MapPin, Phone, Mail, Clock, Send } from "lucide-react"; -import { ContactInfo, FormField, ModernCard } from "./ui"; +import { ContactInfo, FormField, ModernCard, AvailabilityGrid } from "./ui"; import { useContactForm } from "../hooks/useContactForm"; -// Types -interface ProgramOption { - value: string; - label: string; -} - -// Sub-components const ContactSectionHeader = () => { const { t } = useTranslation(); @@ -25,18 +18,11 @@ const ContactSectionHeader = () => { const ContactFormSection = () => { const { t } = useTranslation(); - const { formData, handleSubmit, handleChange } = useContactForm(); - - const programOptions: ProgramOption[] = [ - { value: "basic", label: t("contact.form.programs.basic") }, - { value: "competitive", label: t("contact.form.programs.competitive") }, - { value: "elite", label: t("contact.form.programs.elite") }, - { value: "other", label: t("contact.form.programs.other") }, - ]; + const { formData, handleSubmit, handleChange, toggleAvailability, status, submitError, isValid } = useContactForm(); return (
- +
@@ -45,78 +31,132 @@ const ContactFormSection = () => {

{t("contact.form.title")}

-

- Get in touch with our team +

+ {t("contact.form.description")}

+ + -
-
- -
-
- -
-
- + {formData.inquiryType === "join" && ( + <> + ({ value: String(n), label: String(n) }))} + className="mb-4" + /> + + + + + + {formData.packageType !== "private" && ( +
+ + +
+ )} + + )} + + {submitError && ( +
+ {submitError} +
+ )} +
diff --git a/src/components/ui/AvailabilityGrid.tsx b/src/components/ui/AvailabilityGrid.tsx new file mode 100644 index 0000000..379c295 --- /dev/null +++ b/src/components/ui/AvailabilityGrid.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; + +interface AvailabilityGridProps { + selected: string[]; + onToggle: (slotKey: string) => void; +} + +const dayKeys = ["mon", "tue", "wed", "thu", "fri"] as const; +const slotKeys = ["18_1930", "1930_21", "21_2230"] as const; +type DayKey = typeof dayKeys[number]; +type SlotKey = typeof slotKeys[number]; + +const AvailabilityGrid: React.FC = ({ selected, onToggle }) => { + const { t } = useTranslation(); + const isChecked = (dayKey: DayKey, slotKey: SlotKey) => { + const key = `${dayKey}_${slotKey}`; + return selected.includes(key); + }; + + const handleChange = (dayKey: DayKey, slotKey: SlotKey) => () => { + const key = `${dayKey}_${slotKey}`; + onToggle(key); + }; + + return ( +
+ + + + + {dayKeys.map((key) => ( + + ))} + + + + {slotKeys.map((slotKey) => ( + + + {dayKeys.map((dayKey) => { + const checked = isChecked(dayKey, slotKey); + return ( + + ); + })} + + ))} + +
{t(`contact.form.availability.days.${key}`)}
{t(`contact.form.availability.slots.${slotKey}`)} + +
+
+ ); +}; + +export default AvailabilityGrid; + + diff --git a/src/components/ui/ContactInfo.tsx b/src/components/ui/ContactInfo.tsx index dedce25..bff4ad7 100644 --- a/src/components/ui/ContactInfo.tsx +++ b/src/components/ui/ContactInfo.tsx @@ -16,7 +16,7 @@ const ContactInfo = ({ return (
diff --git a/src/components/ui/FormField.tsx b/src/components/ui/FormField.tsx index 6381514..5cc480a 100644 --- a/src/components/ui/FormField.tsx +++ b/src/components/ui/FormField.tsx @@ -1,3 +1,4 @@ +import type React from "react"; import type { ChangeEvent } from "react"; interface FormFieldProps { @@ -13,6 +14,9 @@ interface FormFieldProps { options?: { value: string; label: string }[]; rows?: number; className?: string; + pattern?: string; + inputMode?: React.HTMLAttributes["inputMode"]; + invalidFeedback?: string; } const FormField = ({ @@ -26,6 +30,9 @@ const FormField = ({ options, rows = 4, className = "", + pattern, + inputMode, + invalidFeedback, }: FormFieldProps) => { const baseClasses = type === "select" ? "form-select" : "form-control"; @@ -33,45 +40,62 @@ const FormField = ({ switch (type) { case "textarea": return ( -