jdc-react-mailer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,160 @@
1
+ // src/handler/mailer.ts
2
+ import nodemailer from "nodemailer";
3
+ function defaultEmailTemplate(payload) {
4
+ const lines = [
5
+ "<h2>New contact form submission</h2>",
6
+ '<table style="border-collapse: collapse; font-family: sans-serif;">'
7
+ ];
8
+ if (payload.name) {
9
+ lines.push(`<tr><td style="padding: 6px 12px 6px 0; font-weight: bold;">Name</td><td style="padding: 6px 0;">${escapeHtml(payload.name)}</td></tr>`);
10
+ }
11
+ lines.push(`<tr><td style="padding: 6px 12px 6px 0; font-weight: bold;">Email</td><td style="padding: 6px 0;">${escapeHtml(payload.email)}</td></tr>`);
12
+ if (payload.phone) {
13
+ lines.push(`<tr><td style="padding: 6px 12px 6px 0; font-weight: bold;">Phone</td><td style="padding: 6px 0;">${escapeHtml(payload.phone)}</td></tr>`);
14
+ }
15
+ if (payload.subject) {
16
+ lines.push(`<tr><td style="padding: 6px 12px 6px 0; font-weight: bold;">Subject</td><td style="padding: 6px 0;">${escapeHtml(payload.subject)}</td></tr>`);
17
+ }
18
+ lines.push(`<tr><td style="padding: 6px 12px 6px 0; font-weight: bold; vertical-align: top;">Message</td><td style="padding: 6px 0;">${escapeHtml(payload.message).replace(/\n/g, "<br>")}</td></tr>`);
19
+ lines.push("</table>");
20
+ return lines.join("\n");
21
+ }
22
+ function escapeHtml(s) {
23
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
24
+ }
25
+ function createTransport(smtp) {
26
+ return nodemailer.createTransport(smtp);
27
+ }
28
+ function sendMail(transport, config, payload, htmlBody) {
29
+ const subject = payload.subject || "Contact form submission";
30
+ return transport.sendMail({
31
+ from: config.from,
32
+ to: config.to,
33
+ replyTo: payload.email,
34
+ subject,
35
+ html: htmlBody,
36
+ text: `Name: ${payload.name ?? "\u2014"}
37
+ Email: ${payload.email}
38
+ Phone: ${payload.phone ?? "\u2014"}
39
+ Subject: ${payload.subject ?? "\u2014"}
40
+
41
+ Message:
42
+ ${payload.message}`
43
+ });
44
+ }
45
+ function defaultTemplate(payload) {
46
+ return defaultEmailTemplate(payload);
47
+ }
48
+
49
+ // src/handler/validate.ts
50
+ var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
51
+ function parseBody(body) {
52
+ try {
53
+ return JSON.parse(body);
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+ function validatePayload(raw) {
59
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
60
+ return { ok: false, message: "Invalid payload" };
61
+ }
62
+ const o = raw;
63
+ const email = typeof o.email === "string" ? o.email.trim() : "";
64
+ const message = typeof o.message === "string" ? o.message.trim() : "";
65
+ const name = typeof o.name === "string" ? o.name.trim() : void 0;
66
+ const subject = typeof o.subject === "string" ? o.subject.trim() : void 0;
67
+ const website = typeof o.website === "string" ? o.website.trim() : "";
68
+ if (!email) return { ok: false, message: "Email is required" };
69
+ if (!EMAIL_REGEX.test(email)) return { ok: false, message: "Invalid email address" };
70
+ if (!message) return { ok: false, message: "Message is required" };
71
+ if (website) return { ok: false, message: "Invalid submission" };
72
+ return {
73
+ ok: true,
74
+ payload: { email, message, name, subject }
75
+ };
76
+ }
77
+
78
+ // src/handler/index.ts
79
+ function getClientIp(request) {
80
+ const forwarded = request.headers.get("x-forwarded-for");
81
+ if (forwarded) return forwarded.split(",")[0]?.trim() ?? "unknown";
82
+ const realIp = request.headers.get("x-real-ip");
83
+ if (realIp) return realIp;
84
+ return "unknown";
85
+ }
86
+ function createMailerHandler(config) {
87
+ const transport = createTransport(config.smtp);
88
+ const template = config.emailTemplate ?? defaultTemplate;
89
+ const rateLimitMap = /* @__PURE__ */ new Map();
90
+ const rateLimit = config.rateLimit ?? 0;
91
+ const windowMs = 6e4;
92
+ function checkRateLimit(ip) {
93
+ if (rateLimit <= 0) return true;
94
+ const now = Date.now();
95
+ const entry = rateLimitMap.get(ip);
96
+ if (!entry) {
97
+ rateLimitMap.set(ip, { count: 1, resetAt: now + windowMs });
98
+ return true;
99
+ }
100
+ if (now > entry.resetAt) {
101
+ rateLimitMap.set(ip, { count: 1, resetAt: now + windowMs });
102
+ return true;
103
+ }
104
+ entry.count += 1;
105
+ return entry.count <= rateLimit;
106
+ }
107
+ async function POST(request) {
108
+ const origin = request.headers.get("origin") ?? "";
109
+ const allowedOrigins = config.allowedOrigins;
110
+ if (allowedOrigins && allowedOrigins.length > 0 && origin && !allowedOrigins.includes(origin)) {
111
+ return new Response(
112
+ JSON.stringify({ success: false, message: "Forbidden" }),
113
+ { status: 403, headers: { "Content-Type": "application/json" } }
114
+ );
115
+ }
116
+ const ip = getClientIp(request);
117
+ if (!checkRateLimit(ip)) {
118
+ return new Response(
119
+ JSON.stringify({ success: false, message: "Too many requests" }),
120
+ { status: 429, headers: { "Content-Type": "application/json" } }
121
+ );
122
+ }
123
+ let body;
124
+ try {
125
+ body = await request.text();
126
+ } catch {
127
+ return new Response(
128
+ JSON.stringify({ success: false, message: "Bad request" }),
129
+ { status: 400, headers: { "Content-Type": "application/json" } }
130
+ );
131
+ }
132
+ const raw = parseBody(body);
133
+ const validated = validatePayload(raw);
134
+ if (!validated.ok) {
135
+ return new Response(
136
+ JSON.stringify({ success: false, message: validated.message }),
137
+ { status: 400, headers: { "Content-Type": "application/json" } }
138
+ );
139
+ }
140
+ try {
141
+ const html = template(validated.payload);
142
+ await sendMail(transport, { from: config.from, to: config.to }, validated.payload, html);
143
+ return new Response(
144
+ JSON.stringify({ success: true }),
145
+ { status: 200, headers: { "Content-Type": "application/json" } }
146
+ );
147
+ } catch (err) {
148
+ const message = err instanceof Error ? err.message : "Failed to send email";
149
+ return new Response(
150
+ JSON.stringify({ success: false, message }),
151
+ { status: 500, headers: { "Content-Type": "application/json" } }
152
+ );
153
+ }
154
+ }
155
+ return { POST };
156
+ }
157
+ export {
158
+ createMailerHandler
159
+ };
160
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/handler/mailer.ts","../../src/handler/validate.ts","../../src/handler/index.ts"],"sourcesContent":["import nodemailer from 'nodemailer';\nimport type { Transporter } from 'nodemailer';\nimport type { SmtpConfig, ContactPayload } from './types';\n\nfunction defaultEmailTemplate(payload: ContactPayload): string {\n const lines: string[] = [\n '<h2>New contact form submission</h2>',\n '<table style=\"border-collapse: collapse; font-family: sans-serif;\">',\n ];\n if (payload.name) {\n lines.push(`<tr><td style=\"padding: 6px 12px 6px 0; font-weight: bold;\">Name</td><td style=\"padding: 6px 0;\">${escapeHtml(payload.name)}</td></tr>`);\n }\n lines.push(`<tr><td style=\"padding: 6px 12px 6px 0; font-weight: bold;\">Email</td><td style=\"padding: 6px 0;\">${escapeHtml(payload.email)}</td></tr>`);\n if (payload.phone) {\n lines.push(`<tr><td style=\"padding: 6px 12px 6px 0; font-weight: bold;\">Phone</td><td style=\"padding: 6px 0;\">${escapeHtml(payload.phone)}</td></tr>`);\n }\n if (payload.subject) {\n lines.push(`<tr><td style=\"padding: 6px 12px 6px 0; font-weight: bold;\">Subject</td><td style=\"padding: 6px 0;\">${escapeHtml(payload.subject)}</td></tr>`);\n }\n lines.push(`<tr><td style=\"padding: 6px 12px 6px 0; font-weight: bold; vertical-align: top;\">Message</td><td style=\"padding: 6px 0;\">${escapeHtml(payload.message).replace(/\\n/g, '<br>')}</td></tr>`);\n lines.push('</table>');\n return lines.join('\\n');\n}\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n}\n\nexport function createTransport(smtp: SmtpConfig): Transporter {\n return nodemailer.createTransport(smtp);\n}\n\nexport function sendMail(\n transport: Transporter,\n config: { from: string; to: string },\n payload: ContactPayload,\n htmlBody: string\n): Promise<void> {\n const subject = payload.subject || 'Contact form submission';\n return transport.sendMail({\n from: config.from,\n to: config.to,\n replyTo: payload.email,\n subject,\n html: htmlBody,\n text: `Name: ${payload.name ?? '—'}\\nEmail: ${payload.email}\\nPhone: ${payload.phone ?? '—'}\\nSubject: ${payload.subject ?? '—'}\\n\\nMessage:\\n${payload.message}`,\n }) as Promise<void>;\n}\n\nexport function defaultTemplate(payload: ContactPayload): string {\n return defaultEmailTemplate(payload);\n}\n","import type { ContactPayload } from './types';\n\nconst EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\nexport function parseBody(body: string): unknown {\n try {\n return JSON.parse(body) as unknown;\n } catch {\n return null;\n }\n}\n\nexport function validatePayload(\n raw: unknown\n): { ok: true; payload: ContactPayload } | { ok: false; message: string } {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n return { ok: false, message: 'Invalid payload' };\n }\n const o = raw as Record<string, unknown>;\n const email = typeof o.email === 'string' ? o.email.trim() : '';\n const message = typeof o.message === 'string' ? o.message.trim() : '';\n const name = typeof o.name === 'string' ? o.name.trim() : undefined;\n const subject = typeof o.subject === 'string' ? o.subject.trim() : undefined;\n const website = typeof o.website === 'string' ? o.website.trim() : '';\n\n if (!email) return { ok: false, message: 'Email is required' };\n if (!EMAIL_REGEX.test(email)) return { ok: false, message: 'Invalid email address' };\n if (!message) return { ok: false, message: 'Message is required' };\n if (website) return { ok: false, message: 'Invalid submission' };\n\n return {\n ok: true,\n payload: { email, message, name, subject },\n };\n}\n","import type { NextRequest } from 'next/server';\nimport type { MailerHandlerConfig } from './types';\nimport { createTransport, sendMail, defaultTemplate } from './mailer';\nimport { parseBody, validatePayload } from './validate';\n\nfunction getClientIp(request: NextRequest): string {\n const forwarded = request.headers.get('x-forwarded-for');\n if (forwarded) return forwarded.split(',')[0]?.trim() ?? 'unknown';\n const realIp = request.headers.get('x-real-ip');\n if (realIp) return realIp;\n return 'unknown';\n}\n\nexport function createMailerHandler(config: MailerHandlerConfig) {\n const transport = createTransport(config.smtp);\n const template = config.emailTemplate ?? defaultTemplate;\n\n const rateLimitMap = new Map<string, { count: number; resetAt: number }>();\n const rateLimit = config.rateLimit ?? 0;\n const windowMs = 60_000;\n\n function checkRateLimit(ip: string): boolean {\n if (rateLimit <= 0) return true;\n const now = Date.now();\n const entry = rateLimitMap.get(ip);\n if (!entry) {\n rateLimitMap.set(ip, { count: 1, resetAt: now + windowMs });\n return true;\n }\n if (now > entry.resetAt) {\n rateLimitMap.set(ip, { count: 1, resetAt: now + windowMs });\n return true;\n }\n entry.count += 1;\n return entry.count <= rateLimit;\n }\n\n async function POST(request: NextRequest) {\n const origin = request.headers.get('origin') ?? '';\n const allowedOrigins = config.allowedOrigins;\n if (allowedOrigins && allowedOrigins.length > 0 && origin && !allowedOrigins.includes(origin)) {\n return new Response(\n JSON.stringify({ success: false, message: 'Forbidden' }),\n { status: 403, headers: { 'Content-Type': 'application/json' } }\n );\n }\n\n const ip = getClientIp(request);\n if (!checkRateLimit(ip)) {\n return new Response(\n JSON.stringify({ success: false, message: 'Too many requests' }),\n { status: 429, headers: { 'Content-Type': 'application/json' } }\n );\n }\n\n let body: string;\n try {\n body = await request.text();\n } catch {\n return new Response(\n JSON.stringify({ success: false, message: 'Bad request' }),\n { status: 400, headers: { 'Content-Type': 'application/json' } }\n );\n }\n\n const raw = parseBody(body);\n const validated = validatePayload(raw);\n if (!validated.ok) {\n return new Response(\n JSON.stringify({ success: false, message: validated.message }),\n { status: 400, headers: { 'Content-Type': 'application/json' } }\n );\n }\n\n try {\n const html = template(validated.payload);\n await sendMail(transport, { from: config.from, to: config.to }, validated.payload, html);\n return new Response(\n JSON.stringify({ success: true }),\n { status: 200, headers: { 'Content-Type': 'application/json' } }\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to send email';\n return new Response(\n JSON.stringify({ success: false, message }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n );\n }\n }\n\n return { POST };\n}\n\nexport type { MailerHandlerConfig, ContactPayload, SmtpConfig } from './types';\n"],"mappings":"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","names":[]}
package/dist/index.cjs ADDED
@@ -0,0 +1,368 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ ContactForm: () => ContactForm
24
+ });
25
+ module.exports = __toCommonJS(src_exports);
26
+
27
+ // src/hooks/useContactForm.ts
28
+ var import_react = require("react");
29
+ var DEFAULT_FIELDS = ["name", "email", "subject", "message"];
30
+ var DEFAULT_LABELS = {
31
+ name: "Name",
32
+ email: "Email",
33
+ phone: "Phone (optional)",
34
+ subject: "Subject",
35
+ message: "Message"
36
+ };
37
+ var HONEPOT_NAME = "website";
38
+ function isValidEmail(value) {
39
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
40
+ }
41
+ function isValidPhone(value) {
42
+ return /^[0-9()\-\s+.ext]+$/.test(value.trim());
43
+ }
44
+ function getFieldErrors(payload, fields) {
45
+ const errors = {};
46
+ if (fields.includes("name") && payload.name !== void 0) {
47
+ const v = String(payload.name).trim();
48
+ if (!v) errors.name = "Name is required";
49
+ }
50
+ if (fields.includes("email")) {
51
+ const v = String(payload.email ?? "").trim();
52
+ if (!v) errors.email = "Email is required";
53
+ else if (!isValidEmail(v)) errors.email = "Please enter a valid email address";
54
+ }
55
+ if (fields.includes("phone") && payload.phone) {
56
+ const v = String(payload.phone).trim();
57
+ if (v && !isValidPhone(v)) errors.phone = "Please enter a valid phone number";
58
+ }
59
+ if (fields.includes("subject") && payload.subject !== void 0) {
60
+ const v = String(payload.subject).trim();
61
+ if (!v) errors.subject = "Subject is required";
62
+ }
63
+ if (fields.includes("message")) {
64
+ const v = String(payload.message ?? "").trim();
65
+ if (!v) errors.message = "Message is required";
66
+ }
67
+ return errors;
68
+ }
69
+ function useContactForm({
70
+ endpoint,
71
+ fields = DEFAULT_FIELDS,
72
+ onSuccess,
73
+ onError
74
+ }) {
75
+ const [values, setValues] = (0, import_react.useState)({});
76
+ const [errors, setErrors] = (0, import_react.useState)({});
77
+ const [submitState, setSubmitState] = (0, import_react.useState)("idle");
78
+ const [errorMessage, setErrorMessage] = (0, import_react.useState)(null);
79
+ const setValue = (0, import_react.useCallback)(
80
+ (field, value) => {
81
+ setValues((prev) => ({ ...prev, [field]: value }));
82
+ if (errors[field]) {
83
+ setErrors((prev) => {
84
+ const next = { ...prev };
85
+ delete next[field];
86
+ return next;
87
+ });
88
+ }
89
+ },
90
+ [errors]
91
+ );
92
+ const validate = (0, import_react.useCallback)(() => {
93
+ const payload = {
94
+ email: values.email ?? "",
95
+ message: values.message ?? "",
96
+ name: values.name,
97
+ phone: values.phone,
98
+ subject: values.subject
99
+ };
100
+ const nextErrors = getFieldErrors(payload, fields);
101
+ setErrors(nextErrors);
102
+ return Object.keys(nextErrors).length === 0;
103
+ }, [values, fields]);
104
+ const reset = (0, import_react.useCallback)(() => {
105
+ setValues({});
106
+ setErrors({});
107
+ setSubmitState("idle");
108
+ setErrorMessage(null);
109
+ }, []);
110
+ const submit = (0, import_react.useCallback)(
111
+ async (e) => {
112
+ e.preventDefault();
113
+ const payload = {
114
+ email: (values.email ?? "").trim(),
115
+ message: (values.message ?? "").trim(),
116
+ name: fields.includes("name") ? (values.name ?? "").trim() : void 0,
117
+ phone: fields.includes("phone") ? (values.phone ?? "").trim() || void 0 : void 0,
118
+ subject: fields.includes("subject") ? (values.subject ?? "").trim() : void 0,
119
+ website: (values.website ?? "").trim()
120
+ };
121
+ if (!validate()) return;
122
+ if (payload.website) {
123
+ setValues({});
124
+ setErrors({});
125
+ setSubmitState("success");
126
+ setErrorMessage(null);
127
+ onSuccess?.(payload);
128
+ return;
129
+ }
130
+ setSubmitState("loading");
131
+ setErrorMessage(null);
132
+ try {
133
+ const res = await fetch(endpoint, {
134
+ method: "POST",
135
+ headers: { "Content-Type": "application/json" },
136
+ body: JSON.stringify(payload)
137
+ });
138
+ const data = await res.json().catch(() => ({}));
139
+ if (!res.ok) {
140
+ const msg = typeof data?.message === "string" ? data.message : res.statusText || "Something went wrong";
141
+ throw new Error(msg);
142
+ }
143
+ setValues({});
144
+ setErrors({});
145
+ setSubmitState("success");
146
+ setErrorMessage(null);
147
+ onSuccess?.(payload);
148
+ } catch (err) {
149
+ const error = err instanceof Error ? err : new Error("Submission failed");
150
+ setSubmitState("error");
151
+ setErrorMessage(error.message);
152
+ onError?.(error);
153
+ }
154
+ },
155
+ [endpoint, fields, values, validate, onSuccess, onError, reset]
156
+ );
157
+ return {
158
+ values,
159
+ errors,
160
+ submitState,
161
+ errorMessage,
162
+ setValue,
163
+ setErrorMessage,
164
+ validate,
165
+ submit,
166
+ reset
167
+ };
168
+ }
169
+
170
+ // src/components/ContactForm/ContactForm.tsx
171
+ var import_jsx_runtime = require("react/jsx-runtime");
172
+ var DEFAULT_FIELDS2 = ["name", "email", "subject", "message"];
173
+ var THEME_KEYS = {
174
+ primary: "--jdcm-primary",
175
+ primaryHover: "--jdcm-primary-hover",
176
+ bg: "--jdcm-bg",
177
+ surface: "--jdcm-surface",
178
+ border: "--jdcm-border",
179
+ text: "--jdcm-text",
180
+ muted: "--jdcm-muted",
181
+ error: "--jdcm-error",
182
+ success: "--jdcm-success",
183
+ radius: "--jdcm-radius",
184
+ spacing: "--jdcm-spacing",
185
+ fontFamily: "--jdcm-font"
186
+ };
187
+ function themeToStyle(theme) {
188
+ const style = {};
189
+ for (const [key, value] of Object.entries(theme)) {
190
+ const cssVar = THEME_KEYS[key];
191
+ if (cssVar && value != null) style[cssVar] = value;
192
+ }
193
+ return style;
194
+ }
195
+ function ContactForm({
196
+ endpoint,
197
+ fields = DEFAULT_FIELDS2,
198
+ labels = {},
199
+ placeholders = {},
200
+ theme,
201
+ successMessage = "Thanks! I'll be in touch.",
202
+ onSuccess,
203
+ onError,
204
+ className,
205
+ submitLabel = "Send message"
206
+ }) {
207
+ const {
208
+ values,
209
+ errors,
210
+ submitState,
211
+ errorMessage,
212
+ setValue,
213
+ setErrorMessage,
214
+ submit
215
+ } = useContactForm({ endpoint, fields, onSuccess, onError });
216
+ const rootClassName = ["jdcm-root", className].filter(Boolean).join(" ");
217
+ const rootStyle = theme ? themeToStyle(theme) : void 0;
218
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
219
+ "div",
220
+ {
221
+ className: rootClassName,
222
+ style: rootStyle,
223
+ ...theme ? { "data-theme-set": "true" } : {},
224
+ role: "region",
225
+ "aria-label": "Contact form",
226
+ children: [
227
+ submitState === "success" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "jdcm-success-msg", role: "status", children: successMessage }),
228
+ submitState === "error" && errorMessage && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "jdcm-error-banner", role: "alert", children: [
229
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: errorMessage }),
230
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
231
+ "button",
232
+ {
233
+ type: "button",
234
+ onClick: () => setErrorMessage(null),
235
+ "aria-label": "Dismiss error",
236
+ children: "Dismiss"
237
+ }
238
+ )
239
+ ] }),
240
+ submitState !== "success" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: "jdcm-form", onSubmit: submit, noValidate: true, children: [
241
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "jdcm-field jdcm-hp", "aria-hidden": "true", children: [
242
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "jdcm-website", children: "Website" }),
243
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
244
+ "input",
245
+ {
246
+ id: "jdcm-website",
247
+ type: "text",
248
+ name: HONEPOT_NAME,
249
+ tabIndex: -1,
250
+ autoComplete: "off",
251
+ value: values.website ?? "",
252
+ onChange: (e) => setValue(HONEPOT_NAME, e.target.value)
253
+ }
254
+ )
255
+ ] }),
256
+ fields.includes("name") && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "jdcm-field", children: [
257
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "jdcm-name", children: labels.name ?? DEFAULT_LABELS.name }),
258
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
259
+ "input",
260
+ {
261
+ id: "jdcm-name",
262
+ type: "text",
263
+ name: "name",
264
+ autoComplete: "name",
265
+ placeholder: placeholders.name,
266
+ value: values.name ?? "",
267
+ onChange: (e) => setValue("name", e.target.value),
268
+ "aria-invalid": Boolean(errors.name),
269
+ "aria-describedby": errors.name ? "jdcm-name-err" : void 0
270
+ }
271
+ ),
272
+ errors.name && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { id: "jdcm-name-err", className: "jdcm-error-msg", children: errors.name })
273
+ ] }),
274
+ fields.includes("email") && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "jdcm-field", children: [
275
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "jdcm-email", children: labels.email ?? DEFAULT_LABELS.email }),
276
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
277
+ "input",
278
+ {
279
+ id: "jdcm-email",
280
+ type: "email",
281
+ name: "email",
282
+ required: true,
283
+ autoComplete: "email",
284
+ placeholder: placeholders.email,
285
+ value: values.email ?? "",
286
+ onChange: (e) => setValue("email", e.target.value),
287
+ "aria-invalid": Boolean(errors.email),
288
+ "aria-describedby": errors.email ? "jdcm-email-err" : void 0
289
+ }
290
+ ),
291
+ errors.email && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { id: "jdcm-email-err", className: "jdcm-error-msg", children: errors.email })
292
+ ] }),
293
+ fields.includes("phone") && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "jdcm-field", children: [
294
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "jdcm-phone", children: labels.phone ?? DEFAULT_LABELS.phone }),
295
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
296
+ "input",
297
+ {
298
+ id: "jdcm-phone",
299
+ type: "tel",
300
+ name: "phone",
301
+ autoComplete: "tel",
302
+ placeholder: placeholders.phone,
303
+ value: values.phone ?? "",
304
+ onChange: (e) => setValue("phone", e.target.value),
305
+ "aria-invalid": Boolean(errors.phone),
306
+ "aria-describedby": errors.phone ? "jdcm-phone-err" : void 0
307
+ }
308
+ ),
309
+ errors.phone && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { id: "jdcm-phone-err", className: "jdcm-error-msg", children: errors.phone })
310
+ ] }),
311
+ fields.includes("subject") && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "jdcm-field", children: [
312
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "jdcm-subject", children: labels.subject ?? DEFAULT_LABELS.subject }),
313
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
314
+ "input",
315
+ {
316
+ id: "jdcm-subject",
317
+ type: "text",
318
+ name: "subject",
319
+ autoComplete: "off",
320
+ placeholder: placeholders.subject,
321
+ value: values.subject ?? "",
322
+ onChange: (e) => setValue("subject", e.target.value),
323
+ "aria-invalid": Boolean(errors.subject),
324
+ "aria-describedby": errors.subject ? "jdcm-subject-err" : void 0
325
+ }
326
+ ),
327
+ errors.subject && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { id: "jdcm-subject-err", className: "jdcm-error-msg", children: errors.subject })
328
+ ] }),
329
+ fields.includes("message") && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "jdcm-field", children: [
330
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "jdcm-message", children: labels.message ?? DEFAULT_LABELS.message }),
331
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
332
+ "textarea",
333
+ {
334
+ id: "jdcm-message",
335
+ name: "message",
336
+ required: true,
337
+ placeholder: placeholders.message,
338
+ value: values.message ?? "",
339
+ onChange: (e) => setValue("message", e.target.value),
340
+ "aria-invalid": Boolean(errors.message),
341
+ "aria-describedby": errors.message ? "jdcm-message-err" : void 0
342
+ }
343
+ ),
344
+ errors.message && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { id: "jdcm-message-err", className: "jdcm-error-msg", children: errors.message })
345
+ ] }),
346
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
347
+ "button",
348
+ {
349
+ type: "submit",
350
+ className: "jdcm-submit",
351
+ disabled: submitState === "loading",
352
+ "aria-busy": submitState === "loading",
353
+ children: [
354
+ submitState === "loading" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "jdcm-spinner", "aria-hidden": "true" }),
355
+ submitState === "loading" ? "Sending\u2026" : submitLabel
356
+ ]
357
+ }
358
+ )
359
+ ] })
360
+ ]
361
+ }
362
+ );
363
+ }
364
+ // Annotate the CommonJS export names for ESM import in node:
365
+ 0 && (module.exports = {
366
+ ContactForm
367
+ });
368
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/hooks/useContactForm.ts","../src/components/ContactForm/ContactForm.tsx"],"sourcesContent":["export { ContactForm } from './components/ContactForm';\nexport type {\n ContactFormProps,\n FormFieldName,\n FormFieldsConfig,\n ThemeOverrides,\n ContactFormPayload,\n SubmitState,\n} from './components/ContactForm/types';\n","import { useState, useCallback } from 'react';\nimport type {\n FormFieldName,\n FormFieldsConfig,\n ContactFormPayload,\n SubmitState,\n} from '../components/ContactForm/types';\n\nconst DEFAULT_FIELDS: FormFieldsConfig = ['name', 'email', 'subject', 'message'];\n\nconst DEFAULT_LABELS: Record<FormFieldName, string> = {\n name: 'Name',\n email: 'Email',\n phone: 'Phone (optional)',\n subject: 'Subject',\n message: 'Message',\n};\n\nconst HONEPOT_NAME = 'website';\n\nfunction isValidEmail(value: string): boolean {\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value.trim());\n}\n\nfunction isValidPhone(value: string): boolean {\n return /^[0-9()\\-\\s+.ext]+$/.test(value.trim());\n}\n\nfunction getFieldErrors(\n payload: Partial<ContactFormPayload>,\n fields: FormFieldsConfig\n): Partial<Record<FormFieldName, string>> {\n const errors: Partial<Record<FormFieldName, string>> = {};\n\n if (fields.includes('name') && payload.name !== undefined) {\n const v = String(payload.name).trim();\n if (!v) errors.name = 'Name is required';\n }\n\n if (fields.includes('email')) {\n const v = String(payload.email ?? '').trim();\n if (!v) errors.email = 'Email is required';\n else if (!isValidEmail(v)) errors.email = 'Please enter a valid email address';\n }\n\n if (fields.includes('phone') && payload.phone) {\n const v = String(payload.phone).trim();\n if (v && !isValidPhone(v)) errors.phone = 'Please enter a valid phone number';\n }\n\n if (fields.includes('subject') && payload.subject !== undefined) {\n const v = String(payload.subject).trim();\n if (!v) errors.subject = 'Subject is required';\n }\n\n if (fields.includes('message')) {\n const v = String(payload.message ?? '').trim();\n if (!v) errors.message = 'Message is required';\n }\n\n return errors;\n}\n\nexport interface UseContactFormOptions {\n endpoint: string;\n fields?: FormFieldsConfig;\n onSuccess?: (data: ContactFormPayload) => void;\n onError?: (error: Error) => void;\n}\n\nexport interface UseContactFormReturn {\n values: Partial<ContactFormPayload>;\n errors: Partial<Record<FormFieldName, string>>;\n submitState: SubmitState;\n errorMessage: string | null;\n setValue: (field: FormFieldName | typeof HONEPOT_NAME, value: string) => void;\n setErrorMessage: (msg: string | null) => void;\n validate: () => boolean;\n submit: (e: React.FormEvent) => Promise<void>;\n reset: () => void;\n}\n\nexport function useContactForm({\n endpoint,\n fields = DEFAULT_FIELDS,\n onSuccess,\n onError,\n}: UseContactFormOptions): UseContactFormReturn {\n const [values, setValues] = useState<Partial<ContactFormPayload>>({});\n const [errors, setErrors] = useState<Partial<Record<FormFieldName, string>>>({});\n const [submitState, setSubmitState] = useState<SubmitState>('idle');\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n\n const setValue = useCallback(\n (field: FormFieldName | typeof HONEPOT_NAME, value: string) => {\n setValues((prev) => ({ ...prev, [field]: value }));\n if (errors[field as FormFieldName]) {\n setErrors((prev) => {\n const next = { ...prev };\n delete next[field as FormFieldName];\n return next;\n });\n }\n },\n [errors]\n );\n\n const validate = useCallback((): boolean => {\n const payload: Partial<ContactFormPayload> = {\n email: values.email ?? '',\n message: values.message ?? '',\n name: values.name,\n phone: values.phone,\n subject: values.subject,\n };\n const nextErrors = getFieldErrors(payload, fields);\n setErrors(nextErrors);\n return Object.keys(nextErrors).length === 0;\n }, [values, fields]);\n\n const reset = useCallback(() => {\n setValues({});\n setErrors({});\n setSubmitState('idle');\n setErrorMessage(null);\n }, []);\n\n const submit = useCallback(\n async (e: React.FormEvent) => {\n e.preventDefault();\n\n const payload: Partial<ContactFormPayload> = {\n email: (values.email ?? '').trim(),\n message: (values.message ?? '').trim(),\n name: fields.includes('name') ? (values.name ?? '').trim() : undefined,\n phone: fields.includes('phone') ? (values.phone ?? '').trim() || undefined : undefined,\n subject: fields.includes('subject') ? (values.subject ?? '').trim() : undefined,\n website: (values.website ?? '').trim(),\n };\n\n if (!validate()) return;\n\n if (payload.website) {\n setValues({});\n setErrors({});\n setSubmitState('success');\n setErrorMessage(null);\n onSuccess?.(payload as ContactFormPayload);\n return;\n }\n\n setSubmitState('loading');\n setErrorMessage(null);\n\n try {\n const res = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n });\n\n const data = await res.json().catch(() => ({}));\n\n if (!res.ok) {\n const msg =\n typeof data?.message === 'string'\n ? data.message\n : res.statusText || 'Something went wrong';\n throw new Error(msg);\n }\n\n setValues({});\n setErrors({});\n setSubmitState('success');\n setErrorMessage(null);\n onSuccess?.(payload as ContactFormPayload);\n } catch (err) {\n const error = err instanceof Error ? err : new Error('Submission failed');\n setSubmitState('error');\n setErrorMessage(error.message);\n onError?.(error);\n }\n },\n [endpoint, fields, values, validate, onSuccess, onError, reset]\n );\n return {\n values,\n errors,\n submitState,\n errorMessage,\n setValue,\n setErrorMessage,\n validate,\n submit,\n reset,\n };\n}\n\nexport { DEFAULT_LABELS, HONEPOT_NAME };\n","import React from 'react';\nimport { useContactForm, DEFAULT_LABELS, HONEPOT_NAME } from '../../hooks/useContactForm';\nimport type { ContactFormProps, FormFieldName, ThemeOverrides } from './types';\n/* Consumer imports: import 'jdc-react-mailer/style.css' */\n\nconst DEFAULT_FIELDS: FormFieldName[] = ['name', 'email', 'subject', 'message'];\n\nconst THEME_KEYS: Record<keyof ThemeOverrides, string> = {\n primary: '--jdcm-primary',\n primaryHover: '--jdcm-primary-hover',\n bg: '--jdcm-bg',\n surface: '--jdcm-surface',\n border: '--jdcm-border',\n text: '--jdcm-text',\n muted: '--jdcm-muted',\n error: '--jdcm-error',\n success: '--jdcm-success',\n radius: '--jdcm-radius',\n spacing: '--jdcm-spacing',\n fontFamily: '--jdcm-font',\n};\n\nfunction themeToStyle(theme: ThemeOverrides): React.CSSProperties {\n const style: React.CSSProperties = {};\n for (const [key, value] of Object.entries(theme)) {\n const cssVar = THEME_KEYS[key as keyof ThemeOverrides];\n if (cssVar && value != null) style[cssVar as keyof React.CSSProperties] = value;\n }\n return style;\n}\n\nexport function ContactForm({\n endpoint,\n fields = DEFAULT_FIELDS,\n labels = {},\n placeholders = {},\n theme,\n successMessage = \"Thanks! I'll be in touch.\",\n onSuccess,\n onError,\n className,\n submitLabel = 'Send message',\n}: ContactFormProps) {\n const {\n values,\n errors,\n submitState,\n errorMessage,\n setValue,\n setErrorMessage,\n submit,\n } = useContactForm({ endpoint, fields, onSuccess, onError });\n\n const rootClassName = ['jdcm-root', className].filter(Boolean).join(' ');\n const rootStyle = theme ? themeToStyle(theme) : undefined;\n\n return (\n <div\n className={rootClassName}\n style={rootStyle}\n {...(theme ? { 'data-theme-set': 'true' } : {})}\n role=\"region\"\n aria-label=\"Contact form\"\n >\n {submitState === 'success' && (\n <p className=\"jdcm-success-msg\" role=\"status\">\n {successMessage}\n </p>\n )}\n\n {submitState === 'error' && errorMessage && (\n <div className=\"jdcm-error-banner\" role=\"alert\">\n <span>{errorMessage}</span>\n <button\n type=\"button\"\n onClick={() => setErrorMessage(null)}\n aria-label=\"Dismiss error\"\n >\n Dismiss\n </button>\n </div>\n )}\n\n {submitState !== 'success' && (\n <form className=\"jdcm-form\" onSubmit={submit} noValidate>\n {/* Honeypot */}\n <div className=\"jdcm-field jdcm-hp\" aria-hidden=\"true\">\n <label htmlFor=\"jdcm-website\">Website</label>\n <input\n id=\"jdcm-website\"\n type=\"text\"\n name={HONEPOT_NAME}\n tabIndex={-1}\n autoComplete=\"off\"\n value={values.website ?? ''}\n onChange={(e) => setValue(HONEPOT_NAME, e.target.value)}\n />\n </div>\n\n {fields.includes('name') && (\n <div className=\"jdcm-field\">\n <label htmlFor=\"jdcm-name\">\n {labels.name ?? DEFAULT_LABELS.name}\n </label>\n <input\n id=\"jdcm-name\"\n type=\"text\"\n name=\"name\"\n autoComplete=\"name\"\n placeholder={placeholders.name}\n value={values.name ?? ''}\n onChange={(e) => setValue('name', e.target.value)}\n aria-invalid={Boolean(errors.name)}\n aria-describedby={errors.name ? 'jdcm-name-err' : undefined}\n />\n {errors.name && (\n <span id=\"jdcm-name-err\" className=\"jdcm-error-msg\">\n {errors.name}\n </span>\n )}\n </div>\n )}\n\n {fields.includes('email') && (\n <div className=\"jdcm-field\">\n <label htmlFor=\"jdcm-email\">\n {labels.email ?? DEFAULT_LABELS.email}\n </label>\n <input\n id=\"jdcm-email\"\n type=\"email\"\n name=\"email\"\n required\n autoComplete=\"email\"\n placeholder={placeholders.email}\n value={values.email ?? ''}\n onChange={(e) => setValue('email', e.target.value)}\n aria-invalid={Boolean(errors.email)}\n aria-describedby={errors.email ? 'jdcm-email-err' : undefined}\n />\n {errors.email && (\n <span id=\"jdcm-email-err\" className=\"jdcm-error-msg\">\n {errors.email}\n </span>\n )}\n </div>\n )}\n\n {fields.includes('phone') && (\n <div className=\"jdcm-field\">\n <label htmlFor=\"jdcm-phone\">\n {labels.phone ?? DEFAULT_LABELS.phone}\n </label>\n <input\n id=\"jdcm-phone\"\n type=\"tel\"\n name=\"phone\"\n autoComplete=\"tel\"\n placeholder={placeholders.phone}\n value={values.phone ?? ''}\n onChange={(e) => setValue('phone', e.target.value)}\n aria-invalid={Boolean(errors.phone)}\n aria-describedby={errors.phone ? 'jdcm-phone-err' : undefined}\n />\n {errors.phone && (\n <span id=\"jdcm-phone-err\" className=\"jdcm-error-msg\">\n {errors.phone}\n </span>\n )}\n </div>\n )}\n\n {fields.includes('subject') && (\n <div className=\"jdcm-field\">\n <label htmlFor=\"jdcm-subject\">\n {labels.subject ?? DEFAULT_LABELS.subject}\n </label>\n <input\n id=\"jdcm-subject\"\n type=\"text\"\n name=\"subject\"\n autoComplete=\"off\"\n placeholder={placeholders.subject}\n value={values.subject ?? ''}\n onChange={(e) => setValue('subject', e.target.value)}\n aria-invalid={Boolean(errors.subject)}\n aria-describedby={errors.subject ? 'jdcm-subject-err' : undefined}\n />\n {errors.subject && (\n <span id=\"jdcm-subject-err\" className=\"jdcm-error-msg\">\n {errors.subject}\n </span>\n )}\n </div>\n )}\n\n {fields.includes('message') && (\n <div className=\"jdcm-field\">\n <label htmlFor=\"jdcm-message\">\n {labels.message ?? DEFAULT_LABELS.message}\n </label>\n <textarea\n id=\"jdcm-message\"\n name=\"message\"\n required\n placeholder={placeholders.message}\n value={values.message ?? ''}\n onChange={(e) => setValue('message', e.target.value)}\n aria-invalid={Boolean(errors.message)}\n aria-describedby={errors.message ? 'jdcm-message-err' : undefined}\n />\n {errors.message && (\n <span id=\"jdcm-message-err\" className=\"jdcm-error-msg\">\n {errors.message}\n </span>\n )}\n </div>\n )}\n\n <button\n type=\"submit\"\n className=\"jdcm-submit\"\n disabled={submitState === 'loading'}\n aria-busy={submitState === 'loading'}\n >\n {submitState === 'loading' && (\n <span className=\"jdcm-spinner\" aria-hidden=\"true\" />\n )}\n {submitState === 'loading' ? 'Sending…' : submitLabel}\n </button>\n </form>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["DEFAULT_FIELDS"]}