Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This project is a comprehensive full‑stack web app for doing simple, local fil
**PDF Tools:**

- Convert PDF pages to PNG (single page, range, or all pages)
- Footer “Stay Updated” signup validates email and opens a local mailto compose (no server storage)
- Merge multiple PDF files into one document, including files with the same name
- Split a PDF by extracting a page range into a new document
- Convert PDF to DOCX
Expand Down
1 change: 0 additions & 1 deletion backend/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
'reportlab.lib.styles',
'reportlab.platypus',
'pdf2image',
'PIL',
'pdf2docx',
'markdown2',
'bs4',
Expand Down
91 changes: 83 additions & 8 deletions frontend/src/components/Landing/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useState } from "react";
import { Link } from "react-router-dom";
import {
FileText,
Expand All @@ -13,9 +13,52 @@ import {
Instagram
} from "lucide-react";
import { SiX } from "react-icons/si";
import {
buildNewsletterMailto,
isValidEmail,
} from "../../utils/newsletterSignup";

const Footer = () => {
const currentYear = new Date().getFullYear();
const [email, setEmail] = useState("");
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">(
"idle",
);
const [statusMessage, setStatusMessage] = useState("");

const handleSubscribe = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();

if (status === "loading") return;

if (!isValidEmail(email)) {
setStatus("error");
setStatusMessage("Enter a valid email address.");
return;
}

const contactAddress = import.meta.env.VITE_EMAIL;
if (!contactAddress) {
setStatus("error");
setStatusMessage("Newsletter contact email is not configured.");
return;
}

setStatus("loading");
setStatusMessage("Preparing your subscription request...");

try {
// Privacy-first: no backend storage or third-party newsletter API.
// Open the local mail client so the visitor can confirm interest.
window.location.href = buildNewsletterMailto(email, contactAddress);
setStatus("success");
setStatusMessage("Thanks! Confirm the message in your email client.");
setEmail("");
} catch {
setStatus("error");
setStatusMessage("Could not open your email client. Try again.");
}
Comment on lines +50 to +60
};

return (
<footer className="relative z-10 bg-gradient-to-br from-slate-50 via-white to-slate-50 dark:from-gray-900 dark:via-gray-900 dark:to-gray-900 border-t border-slate-200 dark:border-gray-700">
Expand Down Expand Up @@ -135,16 +178,48 @@ const Footer = () => {
{/* Newsletter Signup */}
<div className="mt-6">
<h4 className="text-sm font-semibold text-slate-900 mb-2">Stay Updated</h4>
<div className="flex gap-2">
<input
type="email"
<form className="flex gap-2" onSubmit={handleSubscribe} noValidate>
<input
type="email"
name="email"
value={email}
onChange={(event) => {
setEmail(event.target.value);
if (status !== "idle") {
setStatus("idle");
setStatusMessage("");
}
}}
placeholder="Enter your email"
className="flex-1 px-3 py-2 text-sm border border-slate-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-slate-900 dark:text-slate-100 placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
autoComplete="email"
aria-invalid={status === "error"}
aria-describedby="footer-newsletter-status"
disabled={status === "loading"}
Comment on lines +194 to +197
className="flex-1 px-3 py-2 text-sm border border-slate-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-slate-900 dark:text-slate-100 placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:opacity-60"
/>
<button className="px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white text-sm rounded-lg transition-all duration-300">
Subscribe
<button
type="submit"
disabled={status === "loading"}
className="px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white text-sm rounded-lg transition-all duration-300 disabled:opacity-60 disabled:cursor-not-allowed"
>
{status === "loading" ? "Sending..." : "Subscribe"}
</button>
</div>
</form>
{statusMessage && (
<p
id="footer-newsletter-status"
role="status"
className={`mt-2 text-xs ${
status === "error"
? "text-red-500"
: status === "success"
? "text-green-600"
: "text-slate-500"
}`}
>
{statusMessage}
</p>
)}
</div>
</div>
</div>
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/utils/newsletterSignup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";

import {
buildNewsletterMailto,
isValidEmail,
} from "./newsletterSignup";

describe("newsletterSignup", () => {
it("validates common email shapes", () => {
expect(isValidEmail("user@example.com")).toBe(true);
expect(isValidEmail(" user@example.com ")).toBe(true);
expect(isValidEmail("not-an-email")).toBe(false);
expect(isValidEmail("missing@domain")).toBe(false);
expect(isValidEmail("")).toBe(false);
});

it("builds a mailto link without calling remote services", () => {
expect(buildNewsletterMailto("user@example.com", "hello@pdf.example")).toBe(
"mailto:hello@pdf.example?subject=pdfToPng%20newsletter%20interest&body=Please%20keep%20me%20updated%20about%20pdfToPng.%0A%0AEmail%3A%20user%40example.com%0A",
);
});
});
15 changes: 15 additions & 0 deletions frontend/src/utils/newsletterSignup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

export function isValidEmail(value: string) {
const email = value.trim();
return EMAIL_PATTERN.test(email);
}

export function buildNewsletterMailto(email: string, contactAddress: string) {
const trimmedEmail = email.trim();
const subject = encodeURIComponent("pdfToPng newsletter interest");
const body = encodeURIComponent(
`Please keep me updated about pdfToPng.\n\nEmail: ${trimmedEmail}\n`,
);
return `mailto:${contactAddress}?subject=${subject}&body=${body}`;
}
Loading