diff --git a/client/index.html b/client/index.html index 899f9c9..afe1075 100644 --- a/client/index.html +++ b/client/index.html @@ -10,7 +10,7 @@ "@context": "https://schema.org", "@type": "Notary", "name": "Keystone Notary Group, LLC", - "image": "https://www.keystonenotarygroup.com/assets/logo_1.webp", + "image": "/assets/logo_1.png", "url": "https://www.keystonenotarygroup.com", "telephone": "+1-267-309-9000", "email": "info@keystonenotarygroup.com", @@ -31,8 +31,8 @@ - - + + @@ -58,7 +58,7 @@ "@context": "https://schema.org", "@type": "LocalBusiness", "name": "Keystone Notary Group, LLC", - "image": "https://www.keystonenotarygroup.com/assets/logo_1.webp", + "image": "/assets/logo_1.png", "url": "https://www.keystonenotarygroup.com", "telephone": "+1-267-309-9000", "email": "info@keystonenotarygroup.com", diff --git a/client/public/manifest.webmanifest b/client/public/manifest.webmanifest index 780a58c..b331a41 100644 --- a/client/public/manifest.webmanifest +++ b/client/public/manifest.webmanifest @@ -7,14 +7,14 @@ "theme_color": "#0b0b0d", "icons": [ { - "src": "https://www.keystonenotarygroup.com/assets/icon-192.webp", + "src": "/assets/icon-192.png", "sizes": "192x192", - "type": "image/webp" + "type": "image/png" }, { - "src": "https://www.keystonenotarygroup.com/assets/icon-512.webp", + "src": "/assets/icon-512.png", "sizes": "512x512", - "type": "image/webp" + "type": "image/png" } ] } \ No newline at end of file diff --git a/client/src/modules/App.tsx b/client/src/modules/App.tsx index 271d897..30fccc3 100644 --- a/client/src/modules/App.tsx +++ b/client/src/modules/App.tsx @@ -45,7 +45,7 @@ export default function App(){ diff --git a/client/src/modules/sections/Coverage.tsx b/client/src/modules/sections/Coverage.tsx index 571e5e1..10ece15 100644 --- a/client/src/modules/sections/Coverage.tsx +++ b/client/src/modules/sections/Coverage.tsx @@ -10,7 +10,7 @@ export function Coverage(){ if (!lineRef.current) return const mm = gsap.matchMedia() mm.add("(prefers-reduced-motion: no-preference)", () => { - const length = 800 + const length = lineRef.current?.getTotalLength() || 800 gsap.set(lineRef.current, { strokeDasharray: length, strokeDashoffset: length }) gsap.to(lineRef.current, { strokeDashoffset: 0, diff --git a/client/src/modules/sections/Credentials.tsx b/client/src/modules/sections/Credentials.tsx index 2a6ce0c..de8a652 100644 --- a/client/src/modules/sections/Credentials.tsx +++ b/client/src/modules/sections/Credentials.tsx @@ -28,7 +28,7 @@ export function Credentials(){
Background‑Checked
- NNA Certified Notary Signing Agent badge for 2025 + NNA Certified Notary Signing Agent badge for 2025
NNA Notary Signing Agent — 2025
diff --git a/client/src/modules/sections/ServiceMap.tsx b/client/src/modules/sections/ServiceMap.tsx index a3337ba..a084f46 100644 --- a/client/src/modules/sections/ServiceMap.tsx +++ b/client/src/modules/sections/ServiceMap.tsx @@ -1,6 +1,14 @@ import React, { useEffect, useRef } from 'react' import L from 'leaflet' import 'leaflet/dist/leaflet.css' +import icon from 'leaflet/dist/images/marker-icon.png' +import iconShadow from 'leaflet/dist/images/marker-shadow.png' + +const DefaultIcon = L.icon({ + iconUrl: icon, + shadowUrl: iconShadow, +}) +L.Marker.prototype.options.icon = DefaultIcon const HELLERTOWN = [40.5795, -75.3407] as [number, number] @@ -49,7 +57,7 @@ function feeForMiles(m:number){ fetch('/data/counties.geojson').then(r=>r.json()).then(geo=>{ const layers:any = {} L.geoJSON(geo, { - style: (f:any)=>({ color:'#E5E4E2', weight:1, fillOpacity:0.05 }), + style: () => ({ color:'#E5E4E2', weight:1, fillOpacity:0.05 }), onEachFeature: (f:any, layer:any)=>{ layer.bindTooltip(f.properties.name) layers[f.properties.name] = layer diff --git a/client/src/modules/widgets/ChatWidget.tsx b/client/src/modules/widgets/ChatWidget.tsx index 925b66b..52bed61 100644 --- a/client/src/modules/widgets/ChatWidget.tsx +++ b/client/src/modules/widgets/ChatWidget.tsx @@ -1,9 +1,17 @@ -import React, { useState } from 'react' +import React, { useState, useRef, useEffect } from 'react' export function ChatWidget(){ const [open, setOpen] = useState(false) const [msgs, setMsgs] = useState<{role:'user'|'ai', text:string}[]>([]) const [input, setInput] = useState('') + const inputRef = useRef(null) + + useEffect(() => { + if (open) { + setTimeout(() => inputRef.current?.focus(), 100) + } + }, [open]) + async function send(e: React.FormEvent){ e.preventDefault() if(!input.trim()) return @@ -28,7 +36,7 @@ export function ChatWidget(){ {msgs.map((m,i)=>(

{m.text}

))}
- setInput(e.target.value)} placeholder="Type your question…" required /> + setInput(e.target.value)} placeholder="Type your question…" required />

AI assistant powered by OpenAI. Do not share sensitive info.

diff --git a/server/package.json b/server/package.json index e368386..83ad935 100644 --- a/server/package.json +++ b/server/package.json @@ -18,11 +18,10 @@ "express": "^4.19.2", "express-rate-limit": "^7.3.1", "morgan": "^1.10.0", - "nodemailer": "^6.9.14", "@sendgrid/mail": "^8.1.3", "googleapis": "^140.0.0", "cookie-parser": "^1.4.6", - "multer": "2.0.2", + "multer": "^1.4.5-lts.1", "redis": "^4.6.7" }, "devDependencies": { diff --git a/server/server.js b/server/server.js index 66d33e9..99855c0 100644 --- a/server/server.js +++ b/server/server.js @@ -3,7 +3,6 @@ import express from "express"; import cors from "cors"; import morgan from "morgan"; import rateLimit from "express-rate-limit"; -import nodemailer from "nodemailer"; import cookieParser from "cookie-parser"; import { google } from "googleapis"; // Redis is optional; dynamically import so tests can run without the module @@ -190,17 +189,10 @@ app.use(express.json()); app.use(morgan("tiny")); app.use(cookieParser()); -// DEMO / ZERO-CONFIG MODE: -// - If SMTP isn't set, log emails to console and still return success. -// - If OPENAI_API_KEY is missing, return a helpful canned response. -const ZERO_CONFIG = !process.env.SMTP_HOST || !process.env.OPENAI_API_KEY; - const limiter = rateLimit({ windowMs: 60 * 1000, max: 30 }); app.use("/api/", limiter); // --- Admin magic-link auth & CMS storage --- -const ADMIN_EMAIL = - process.env.ADMIN_EMAIL || process.env.EMAIL_TO || "owner@example.com"; const ADMIN_TOKEN_TTL_MS = 15 * 60 * 1000; const ADMIN_SESSION_TTL_MS = 12 * 60 * 60 * 1000; const REDIS_URL = process.env.REDIS_URL; @@ -309,17 +301,6 @@ app.post("/api/admin/request-magic-link", async (req, res) => { } catch (e) { console.error("Send email failed", e); } - } else if (transporter) { - try { - await transporter.sendMail({ - to: email, - from: process.env.EMAIL_FROM || "no-reply@example.com", - subject: "Your Keystone admin link", - text: `Click to log in: ${url}`, - }); - } catch (e) { - console.error(e); - } } else { console.log("[DEMO] Admin magic link:", url); } @@ -579,40 +560,6 @@ app.get("/api/route", async (req, res) => { return res.status(501).json({ error: "routing provider not configured" }); }); -let transporter = null; -if (process.env.EMAIL_TRANSPORT === "smtp") { - transporter = nodemailer.createTransport({ - host: process.env.SMTP_HOST, - port: Number(process.env.SMTP_PORT || 587), - secure: false, - auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS }, - }); -} - -function buildICS(summary, description) { - const dt = new Date(); - const dtStart = new Date(dt.getTime() + 60 * 60 * 1000); // default start in 1h - const dtEnd = new Date(dtStart.getTime() + 60 * 60 * 1000); // 1h duration - function fmt(d) { - return d.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z"; - } - return [ - "BEGIN:VCALENDAR", - "VERSION:2.0", - "PRODID:-//Keystone Notary Group//EN", - "CALSCALE:GREGORIAN", - "METHOD:PUBLISH", - "BEGIN:VEVENT", - "UID:keystone-" + Date.now() + "@keystone", - "DTSTAMP:" + fmt(new Date()), - "DTSTART:" + fmt(dtStart), - "DTEND:" + fmt(dtEnd), - "SUMMARY:" + summary, - "DESCRIPTION:" + description.replace(/\n/g, "\\n"), - "END:VEVENT", - "END:VCALENDAR", - ].join("\r\n"); -} app.post("/api/contact", async (req, res) => { const { @@ -632,7 +579,7 @@ app.post("/api/contact", async (req, res) => { // reCAPTCHA check when secret is set if (RECAPTCHA_SECRET) { try { - const r = await fetch("https://www.google.com/recaptcha/api/siteverify", { + const r = await global.fetch("https://www.google.com/recaptcha/api/siteverify", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ @@ -709,26 +656,6 @@ ${message} uploads, }); - const html = ` -
- - - -
-

We received your message

-

Thanks for reaching out to Keystone Notary Group, LLC. We'll respond shortly.

-
-

Name: ${name}

-

Email: ${email}

-

Phone: ${phone || "n/a"}

-

Service: ${service || "n/a"}

-
-

Your message:

-
${message}
-

Call us at (267) 309‑9000 or reply to this email.

-
© Keystone Notary Group, LLC · Hellertown, PA
-
`; - if ( SENDGRID_API_KEY && SENDGRID_TEMPLATE_OWNER && @@ -751,35 +678,14 @@ ${message} } catch (err) { console.error("SendGrid error", err); } - } else if (transporter) { - const icalEvent = buildICS( - "Prospective Notary Appointment", - `${name} – ${service || "General"}\nPhone: ${phone || "n/a"}\nEmail: ${email}`, - ); - await transporter.sendMail({ - from: process.env.EMAIL_FROM || "no-reply@example.com", - to: process.env.EMAIL_TO || "owner@example.com", - subject: "Keystone Notary — Contact form", - text, - icalEvent: { content: icalEvent }, - }); } else { console.log("[DEMO] Email to owner would be sent with:\n", text); - } - if (transporter && email) { - const icalEvent = buildICS( - "Prospective Notary Appointment", - `${name} – ${service || "General"}\nPhone: ${phone || "n/a"}\nEmail: ${email}`, - ); - await transporter.sendMail({ - from: process.env.EMAIL_FROM || "no-reply@example.com", - to: email, - subject: "We received your message — Keystone Notary Group", - html, - icalEvent: { content: icalEvent }, - }); - } else if (email) { - console.log("[DEMO] Confirmation email to", email, "with HTML template."); + if (email) + console.log( + "[DEMO] Confirmation email to", + email, + "with HTML template.", + ); } res.json({ ok: true }); } catch (err) { @@ -867,7 +773,7 @@ app.post("/api/chat", async (req, res) => { Location: Hellertown, PA. Services: mobile notary, NNA certified & insured signing agents. Phone: (267) 309-9000. Email: info@keystonenotarygroup.com. Avoid legal advice; suggest contacting us for specifics.`; - const response = await fetch("https://api.openai.com/v1/chat/completions", { + const response = await global.fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { Authorization: `Bearer ${OPENAI_API_KEY}`,