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";
|
||||
|
||||
/**
|
||||
* 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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue