feat: switch contact form handler to formspree
All checks were successful
Deploy Portfolio to VPS / deploy (push) Successful in 50s

This commit is contained in:
Chneemann 2026-07-31 08:38:44 +02:00
parent ed1d2f9bb1
commit 4ed8d309ab
No known key found for this signature in database
4 changed files with 20 additions and 154 deletions

View file

@ -1,10 +0,0 @@
<?php
/**
* Global mail configuration options for portfolio contact processing
*/
return [
'recipient_email' => 'dev@andre-kempf.com',
'from_email' => 'noreply@andre-kempf.com',
'app_name' => 'Portfolio Contact',
];

View file

@ -1,79 +0,0 @@
<?php
/**
* Endpoint for processing contact form submissions and sending notification emails via PHP mail().
*/
$config = require_once __DIR__ . '/config.php';
require_once __DIR__ . '/templates/emailTemplate.php';
switch ($_SERVER['REQUEST_METHOD']) {
// 1. Handle CORS Preflight Requests
case "OPTIONS":
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
http_response_code(200);
exit;
// 2. Process Contact Form Submission
case "POST":
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
// Read raw JSON payload from frontend
$json = file_get_contents('php://input');
$params = json_decode($json);
// Honeypot check: Catch bot submissions (matches frontend 'honeypot' or 'website_hp')
$honeypot = $params->honeypot ?? $params->website_hp ?? $params->website ?? '';
if (!empty($honeypot)) {
// Silently discard bot submission with 200 OK
http_response_code(200);
echo json_encode(["status" => "success"]);
exit;
}
// Sanitize & validate incoming fields
$email = filter_var($params->email ?? '', FILTER_VALIDATE_EMAIL);
$name = htmlspecialchars(trim($params->name ?? 'Anonymous'), ENT_QUOTES, 'UTF-8');
$text = htmlspecialchars(trim($params->message ?? ''), ENT_QUOTES, 'UTF-8');
if (!$email || empty($text)) {
http_response_code(400);
echo json_encode(["status" => "error", "message" => "Invalid input data"]);
exit;
}
// Render HTML email template
$rawMessage = $params->message ?? '';
$emailBody = renderEmailTemplate($name, $email, $text, $rawMessage);
$subject = "{$config['app_name']}: $name";
// Construct standard MIME headers
$headers = [
'MIME-Version: 1.0',
'Content-type: text/html; charset=utf-8',
"From: {$config['from_email']}",
"Reply-To: $email"
];
// Send email via PHP native mailer
$success = mail($config['recipient_email'], $subject, $emailBody, implode("\r\n", $headers));
if ($success) {
http_response_code(200);
echo json_encode(["status" => "success"]);
} else {
http_response_code(500);
echo json_encode(["status" => "error", "message" => "Failed to send email"]);
}
break;
// 3. Reject Unsupported HTTP Methods
default:
header("Allow: POST, OPTIONS", true, 405);
http_response_code(405);
echo json_encode(["status" => "error", "message" => "Method Not Allowed"]);
exit;
}

View file

@ -1,53 +0,0 @@
<?php
/**
* Renders an HTML email template for portfolio contact submissions with dark theme styling.
*
* @param string $name Name of the sender
* @param string $email Email address of the sender
* @param string $text Message content from the contact form
* @param string $paramsMessage Plain text copy for mailto pre-filled body
* @return string HTML email content
*/
function renderEmailTemplate(string $name, string $email, string $text, string $paramsMessage): string {
// Sanitize input values for HTML output safety
$safeName = htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
$safeEmail = htmlspecialchars($email, ENT_QUOTES, 'UTF-8');
// Pre-fill reply parameters using rawurlencode (%20 for spaces)
$replySubject = rawurlencode("Re: Portfolio Contact from $name");
$replyBody = rawurlencode("\n\n---\nOriginal message from $name ($email):\n" . $paramsMessage);
$mailtoLink = "mailto:" . rawurlencode($email) . "?subject=$replySubject&body=$replyBody";
// Clean message body and format line breaks for HTML rendering
$formattedText = nl2br(htmlspecialchars(trim($text), ENT_QUOTES, 'UTF-8'));
return "
<div style='width: 100%; height: 100%; background-color: #fff; font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;'>
<div style='background-color: #020617; color: #f8fafc; padding: 28px; max-width: 800px; margin: 0 auto; border: 1px solid #1e293b; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5);'>
<!-- Email Header -->
<div style='margin-bottom: 20px;'>
<span style='font-family: monospace; color: #3b82f6; font-size: 12px;'>// Portfolio Contact Message</span>
<h2 style='margin: 6px 0 0 0; color: #ffffff; font-size: 20px;'>New message from {$safeName}</h2>
</div>
<!-- Message Card -->
<div style='background-color: #0f172a; border: 1px solid #1e293b; padding: 18px; border-radius: 8px; margin-bottom: 24px;'>
<div style='font-family: monospace; font-size: 12px; color: #64748b; margin-bottom: 12px;'>
<span style='color: #3b82f6;'>From:</span> {$safeName} ({$safeEmail})
</div>
<div style='font-size: 14px; line-height: 1.6; color: #cbd5e1;'>{$formattedText}</div>
</div>
<!-- Quick Reply CTA -->
<div style='border-top: 1px solid #1e293b; padding-top: 20px; text-align: center;'>
<a href='{$mailtoLink}' style='display: inline-block; padding: 12px 24px; background-color: #2563eb; color: #ffffff; font-weight: 500; text-decoration: none; border-radius: 8px; font-size: 13px; font-family: monospace;'>
Quick Reply
</a>
</div>
</div>
</div>
";
}

View file

@ -3,7 +3,7 @@
import { useState } from "react";
/**
* Contact section with interactive form, bot anti-spam protection, and direct email info
* Contact section with interactive form, bot anti-spam protection, and Formspree integration
*/
export default function Contact() {
// Form input states
@ -20,7 +20,7 @@ export default function Contact() {
// Bot detection timestamp (form interaction speed check)
const [formLoadTime] = useState(Date.now());
// Handles contact form submission with bot verification & PHP backend dispatch
// Handles contact form submission with bot verification & Formspree dispatch
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
@ -41,16 +41,21 @@ export default function Contact() {
setError(false);
try {
const response = await fetch(
"https://andre-kempf.com/backend/sendMail.php",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name, email, message, honeypot }),
// Direct POST request to Formspree
const response = await fetch(`https://formspree.io/f/mojgbbzp`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
);
body: JSON.stringify({
name,
email,
message,
_replyto: email,
_subject: `Portfolio Contact: ${name}`,
}),
});
if (response.ok) {
setSubmitted(true);
@ -177,6 +182,7 @@ export default function Contact() {
<input
type="text"
id="name"
name="name"
required
value={name}
onChange={(e) => setName(e.target.value)}
@ -193,6 +199,7 @@ export default function Contact() {
<input
type="email"
id="email"
name="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
@ -208,6 +215,7 @@ export default function Contact() {
</label>
<textarea
id="message"
name="message"
rows={4}
required
value={message}
@ -228,7 +236,7 @@ export default function Contact() {
<button
type="submit"
disabled={loading}
className="w-full px-6 py-3 bg-slate-900 border border-slate-800 hover:border-slate-700 hover:bg-slate-850 rounded-lg transition-all hover:-translate-y-0.5 duration-200 cursor-pointer disabled:opacity-50"
className="w-full px-6 py-3 bg-slate-900 border border-slate-800 hover:border-slate-700 hover:bg-slate-850 rounded-lg transition-all hover:-translate-y-0.5 duration-200 cursor-pointer disabled:opacity-50 text-slate-100 text-sm font-medium"
>
{loading ? "Sending..." : "Send Message"}
</button>