feat: switch contact form handler to formspree
All checks were successful
Deploy Portfolio to VPS / deploy (push) Successful in 50s
All checks were successful
Deploy Portfolio to VPS / deploy (push) Successful in 50s
This commit is contained in:
parent
ed1d2f9bb1
commit
4ed8d309ab
4 changed files with 20 additions and 154 deletions
|
|
@ -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',
|
|
||||||
];
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
";
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { useState } from "react";
|
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() {
|
export default function Contact() {
|
||||||
// Form input states
|
// Form input states
|
||||||
|
|
@ -20,7 +20,7 @@ export default function Contact() {
|
||||||
// Bot detection timestamp (form interaction speed check)
|
// Bot detection timestamp (form interaction speed check)
|
||||||
const [formLoadTime] = useState(Date.now());
|
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>) => {
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
|
@ -41,16 +41,21 @@ export default function Contact() {
|
||||||
setError(false);
|
setError(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
// Direct POST request to Formspree
|
||||||
"https://andre-kempf.com/backend/sendMail.php",
|
const response = await fetch(`https://formspree.io/f/mojgbbzp`, {
|
||||||
{
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ name, email, message, honeypot }),
|
body: JSON.stringify({
|
||||||
},
|
name,
|
||||||
);
|
email,
|
||||||
|
message,
|
||||||
|
_replyto: email,
|
||||||
|
_subject: `Portfolio Contact: ${name}`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setSubmitted(true);
|
setSubmitted(true);
|
||||||
|
|
@ -177,6 +182,7 @@ export default function Contact() {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="name"
|
id="name"
|
||||||
|
name="name"
|
||||||
required
|
required
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
|
@ -193,6 +199,7 @@ export default function Contact() {
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
id="email"
|
id="email"
|
||||||
|
name="email"
|
||||||
required
|
required
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
|
@ -208,6 +215,7 @@ export default function Contact() {
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="message"
|
id="message"
|
||||||
|
name="message"
|
||||||
rows={4}
|
rows={4}
|
||||||
required
|
required
|
||||||
value={message}
|
value={message}
|
||||||
|
|
@ -228,7 +236,7 @@ export default function Contact() {
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
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"}
|
{loading ? "Sending..." : "Send Message"}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue