Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -31,8 +31,8 @@
<meta property="og:description" content="NNA certified & insured notary signing agents. We come to you.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://www.keystonenotarygroup.com">
<meta property="og:image" content="https://www.keystonenotarygroup.com/assets/icon-512.webp">
<meta name="twitter:image" content="https://www.keystonenotarygroup.com/assets/icon-512.webp">
<meta property="og:image" content="/assets/icon-512.png">
<meta name="twitter:image" content="/assets/icon-512.png">
<meta name="twitter:card" content="summary_large_image">

<!-- Optional analytics & reCAPTCHA -->
Expand All @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions client/public/manifest.webmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
2 changes: 1 addition & 1 deletion client/src/modules/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default function App(){
</main>
<footer className="max-w-6xl mx-auto px-4 py-10 text-muted flex items-center justify-between">
<p>© {new Date().getFullYear()} Keystone Notary Group, LLC — Hellertown, PA</p>
<a href="#hero" className="border border-white/10 rounded px-2 py-1">▲ Top</a>
<a href="#hero" aria-label="Back to top" className="border border-white/10 rounded px-2 py-1">▲ Top</a>
</footer>
<ChatWidget/>
</>
Expand Down
2 changes: 1 addition & 1 deletion client/src/modules/sections/Coverage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion client/src/modules/sections/Credentials.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function Credentials(){
<div className="pane rounded-2xl border border-white/15 bg-white/5 h-[22vh] grid place-items-center text-4xl font-extrabold">Background‑Checked</div>
</div>
<figure className="mt-8 opacity-90">
<img src="https://www.keystonenotarygroup.com/assets/nna_badge.webp" alt="NNA Certified Notary Signing Agent badge for 2025" className="h-24 w-auto mx-auto"/>
<img src="/assets/nna_badge.png" alt="NNA Certified Notary Signing Agent badge for 2025" className="h-24 w-auto mx-auto"/>
<figcaption className="text-center text-muted mt-2">NNA Notary Signing Agent — 2025</figcaption>
</figure>
</section>
Expand Down
10 changes: 9 additions & 1 deletion client/src/modules/sections/ServiceMap.tsx
Original file line number Diff line number Diff line change
@@ -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]

Expand Down Expand Up @@ -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
Expand Down
12 changes: 10 additions & 2 deletions client/src/modules/widgets/ChatWidget.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>(null)

useEffect(() => {
if (open) {
setTimeout(() => inputRef.current?.focus(), 100)
}
}, [open])

async function send(e: React.FormEvent){
e.preventDefault()
if(!input.trim()) return
Expand All @@ -28,7 +36,7 @@ export function ChatWidget(){
{msgs.map((m,i)=>(<p key={i} className={m.role==='user'?'text-right':'text-left text-platinum'}>{m.text}</p>))}
</div>
<form onSubmit={send} className="flex gap-2">
<input className="flex-1 rounded border border-white/15 bg-black/40 p-2" value={input} onChange={e=>setInput(e.target.value)} placeholder="Type your question…" required />
<input ref={inputRef} className="flex-1 rounded border border-white/15 bg-black/40 p-2" value={input} onChange={e=>setInput(e.target.value)} placeholder="Type your question…" required />
<button className="bg-platinum text-black font-extrabold rounded-lg shadow-glass px-3">Send</button>
</form>
<p className="text-center text-muted text-xs m-0">AI assistant powered by OpenAI. Do not share sensitive info.</p>
Expand Down
3 changes: 1 addition & 2 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
110 changes: 8 additions & 102 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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({
Expand Down Expand Up @@ -709,26 +656,6 @@ ${message}
uploads,
});

const html = `
<div style="font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;background:#0b0b0d;color:#f2f2f2;padding:24px">
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" style="max-width:640px;margin:0 auto;background:#121217;border:1px solid rgba(255,255,255,.08);border-radius:12px">
<tr><td style="padding:24px">
<h1 style="margin:0 0 8px;font-size:22px;color:#E5E4E2">We received your message</h1>
<p style="margin:0 0 16px;color:#a1a1a1">Thanks for reaching out to Keystone Notary Group, LLC. We'll respond shortly.</p>
<div style="padding:12px 16px;border:1px solid rgba(255,255,255,.1);border-radius:8px;background:#0f1013">
<p style="margin:0 0 6px"><strong>Name:</strong> ${name}</p>
<p style="margin:0 0 6px"><strong>Email:</strong> ${email}</p>
<p style="margin:0 0 6px"><strong>Phone:</strong> ${phone || "n/a"}</p>
<p style="margin:0"><strong>Service:</strong> ${service || "n/a"}</p>
</div>
<p style="margin:16px 0 8px"><strong>Your message:</strong></p>
<pre style="white-space:pre-wrap;background:#0b0b0d;border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:12px">${message}</pre>
<p style="margin:16px 0;color:#a1a1a1">Call us at <a href="tel:+12673099000" style="color:#E5E4E2;text-decoration:none">(267) 309‑9000</a> or reply to this email.</p>
</td></tr>
<tr><td style="padding:16px;border-top:1px solid rgba(255,255,255,.08);text-align:center;color:#a1a1a1">© Keystone Notary Group, LLC · Hellertown, PA</td></tr>
</table>
</div>`;

if (
SENDGRID_API_KEY &&
SENDGRID_TEMPLATE_OWNER &&
Expand All @@ -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) {
Expand Down Expand Up @@ -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}`,
Expand Down
Loading