diff --git a/backend/analytics.php b/backend/analytics.php
index 47547d6..1a19e31 100644
--- a/backend/analytics.php
+++ b/backend/analytics.php
@@ -1,11 +1,17 @@
0,
'todayViews' => 0,
@@ -39,7 +47,7 @@ $data = [
'devices' => ['Desktop' => 0, 'Mobile' => 0]
];
-// Load existing data
+// Load existing analytics JSON data
if (file_exists($dataFile)) {
$content = @file_get_contents($dataFile);
if ($content) {
@@ -50,7 +58,7 @@ if (file_exists($dataFile)) {
}
}
-// Reset daily stats if day changed
+// 4. Daily Reset Logic
$today = date('Y-m-d');
if ($data['lastDate'] !== $today) {
$data['todayViews'] = 0;
@@ -58,7 +66,7 @@ if ($data['lastDate'] !== $today) {
$data['visitors'] = [];
}
-// Localhost / Development check
+// 5. Environment & Rate-Limiting Checks
$referer = $_SERVER['HTTP_REFERER'] ?? '';
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
@@ -71,9 +79,9 @@ $isLocalhost = (
$lastTrackTime = $_SESSION['last_track_time'] ?? 0;
$currentTime = time();
-$isSpam = ($currentTime - $lastTrackTime) < 30;
+$isSpam = ($currentTime - $lastTrackTime) < 30; // 30-second cooldown per session
-// Track view
+// 6. Analytics Tracking Execution
if (isset($_GET['track']) && $_GET['track'] === 'true' && !$isLocalhost && !$isSpam) {
$_SESSION['last_track_time'] = $currentTime;
@@ -81,22 +89,26 @@ if (isset($_GET['track']) && $_GET['track'] === 'true' && !$isLocalhost && !$isS
$data['todayViews']++;
$data['lastPing'] = $currentTime;
+ // Detect coarse device category
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
$isMobile = preg_match('/mobile|android|iphone|ipad|tablet/i', $ua);
$deviceType = $isMobile ? 'Mobile' : 'Desktop';
$data['devices'][$deviceType] = ($data['devices'][$deviceType] ?? 0) + 1;
+ // Hash IP address with daily salt for anonymized unique visitor tracking
$visitorHash = md5($ip . $today);
- if (!in_array($visitorHash, $data['visitors'])) {
+ if (!in_array($visitorHash, $data['visitors'], true)) {
$data['visitors'][] = $visitorHash;
}
+ // Atomic file save to prevent race condition corruption
@file_put_contents($dataFile, json_encode($data), LOCK_EX);
}
-// Calculate server execution time (ms)
+// 7. Calculate Server Execution Latency
$responseTime = round((microtime(true) - $startTime) * 1000, 2);
+// 8. Output Metrics Payload
echo json_encode([
'totalViews' => $data['totalViews'],
'todayViews' => $data['todayViews'],
diff --git a/backend/config.php b/backend/config.php
index 05369cf..2d303c9 100644
--- a/backend/config.php
+++ b/backend/config.php
@@ -1,5 +1,8 @@
'dev@andre-kempf.com',
'from_email' => 'noreply@andre-kempf.com',
diff --git a/backend/sendMail.php b/backend/sendMail.php
index d18a452..7d46e47 100644
--- a/backend/sendMail.php
+++ b/backend/sendMail.php
@@ -1,67 +1,79 @@
website)) {
+ // 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;
}
- // 2. Sanitize inputs & set fallbacks
- $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');
+ // 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');
- // Validate required fields
if (!$email || empty($text)) {
http_response_code(400);
- echo json_encode(["status" => "error", "message" => "Invalid input"]);
+ echo json_encode(["status" => "error", "message" => "Invalid input data"]);
exit;
}
- // 3. Render template
- $emailBody = renderEmailTemplate($name, $email, $text, $params->message ?? '');
- $subject = "{$config['app_name']}: $name";
+ // Render HTML email template
+ $rawMessage = $params->message ?? '';
+ $emailBody = renderEmailTemplate($name, $email, $text, $rawMessage);
+ $subject = "{$config['app_name']}: $name";
- // 4. Set headers
- $headers = [
+ // 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
- $success = mail($config['recipient_email'], $subject, $emailBody, implode("\n", $headers));
+ // 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"]);
+ echo json_encode(["status" => "error", "message" => "Failed to send email"]);
}
break;
- default: // Reject any other HTTP method
- header("Allow: POST", true, 405);
+ // 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;
}
\ No newline at end of file
diff --git a/backend/templates/emailTemplate.php b/backend/templates/emailTemplate.php
index 7f31a0c..c91cc5b 100644
--- a/backend/templates/emailTemplate.php
+++ b/backend/templates/emailTemplate.php
@@ -1,34 +1,52 @@
+
+
// Portfolio Contact Message
-
New message from $name
+ New message from {$safeName}
+
- From: $name ($email)
+ From: {$safeName} ({$safeEmail})
{$formattedText}
+
+
";
diff --git a/src/app/globals.css b/src/app/globals.css
index 11ce580..531244c 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,10 +1,12 @@
@import "tailwindcss";
+/* Theme variable bindings linking Next.js Google fonts to Tailwind utility classes */
@theme {
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
--font-mono: var(--font-geist-mono), ui-monospace, SFMono-Regular, monospace;
}
+/* Global base styles, scrollbar styling, and focus states */
@layer base {
html {
color-scheme: dark;
@@ -19,14 +21,17 @@
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
}
+ /* Global text selection highlight */
::selection {
@apply bg-blue-500/30 text-blue-200;
}
+ /* Accessible keyboard navigation focus indicator */
:focus-visible {
@apply outline-2 outline-offset-2 outline-blue-500 rounded-sm;
}
+ /* Custom Webkit scrollbar styling (Chrome, Safari, Edge) */
::-webkit-scrollbar {
width: 8px;
}
@@ -42,6 +47,7 @@
}
}
+/* Override default browser autofill background styles in form fields */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
diff --git a/src/app/imprint/page.tsx b/src/app/imprint/page.tsx
index 729087f..f47f0d9 100644
--- a/src/app/imprint/page.tsx
+++ b/src/app/imprint/page.tsx
@@ -1,8 +1,12 @@
import Link from "next/link";
+/**
+ * Legal notice page pursuant to German telemedia law (§ 5 DDG & § 18 MStV)
+ */
export default function ImprintPage() {
return (
+ {/* Page Heading & Back Navigation */}
Legal Notice.
@@ -14,7 +18,7 @@ export default function ImprintPage() {
- {/* Information in accordance with § 5 DDG & § 18 MStV */}
+ {/* Publisher Identification (§ 5 DDG & § 18 MStV) */}
@@ -32,7 +36,7 @@ export default function ImprintPage() {
- {/* Contact */}
+ {/* Direct Contact Info */}
@@ -48,7 +52,7 @@ export default function ImprintPage() {
- {/* Disclaimer */}
+ {/* Legal Disclaimer (Content, Links, Copyright) */}
@@ -89,7 +93,7 @@ export default function ImprintPage() {
- {/* Dispute Resolution */}
+ {/* Consumer Dispute Resolution Disclaimer */}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index dae3483..7dd3309 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -4,6 +4,9 @@ import Skills from "../components/Skills";
import Contact from "../components/Contact";
import Portfolio from "../components/Portfolio";
+/**
+ * Main portfolio landing page assembling all key sections in sequence
+ */
export default function Home() {
return (
diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx
index 39be6eb..f4c5a5b 100644
--- a/src/app/privacy/page.tsx
+++ b/src/app/privacy/page.tsx
@@ -1,8 +1,12 @@
import Link from "next/link";
+/**
+ * GDPR privacy policy page outlining data collection, self-hosted analytics, and user rights
+ */
export default function PrivacyPage() {
return (
+ {/* Page Heading & Navigation */}
Privacy Policy.
@@ -14,7 +18,7 @@ export default function PrivacyPage() {
- {/* Overview */}
+ {/* Executive Summary */}
@@ -43,7 +47,7 @@ export default function PrivacyPage() {
- {/* Responsible Party */}
+ {/* Data Controller Information */}
@@ -66,7 +70,7 @@ export default function PrivacyPage() {
- {/* Hosting & Server Log Files */}
+ {/* External Web Hosting Details */}
@@ -85,7 +89,7 @@ export default function PrivacyPage() {
- {/* Server Analytics */}
+ {/* Privacy-Preserving Analytics Principles */}
@@ -123,7 +127,7 @@ export default function PrivacyPage() {
- {/* Your Rights */}
+ {/* Statutory Data Subject Rights */}
diff --git a/src/components/About.tsx b/src/components/About.tsx
index 4b06241..f8deb28 100644
--- a/src/components/About.tsx
+++ b/src/components/About.tsx
@@ -1,10 +1,13 @@
+/**
+ * Section presenting personal background information, education, and key overview facts
+ */
export default function About() {
return (
- {/* Titel */}
+ {/* Section Header */}
// 01. Introduction
@@ -15,7 +18,7 @@ export default function About() {
- {/* Left column */}
+ {/* Personal Bio Paragraphs */}
Hey there! André here—a full-stack developer based in Karlsruhe. A
@@ -52,7 +55,7 @@ export default function About() {
- {/* Right column */}
+ {/* Quick Facts Card */}
- {/* Header Titel */}
+ {/* Card Header */}
//{" "}
@@ -72,7 +75,7 @@ export default function About() {
- {/* List Container */}
+ {/* Key Highlights List */}
-
▸
@@ -110,7 +113,7 @@ export default function About() {
- {/* Footer Quote */}
+ {/* Footer Tagline */}
`Keep it simple, keep it clean.`
diff --git a/src/components/AnalyticsModal.tsx b/src/components/AnalyticsModal.tsx
index 650f4aa..7975de7 100644
--- a/src/components/AnalyticsModal.tsx
+++ b/src/components/AnalyticsModal.tsx
@@ -3,7 +3,9 @@
import { useEffect } from "react";
import { createPortal } from "react-dom";
-/** Analytics metrics payload returned by the custom backend API */
+/**
+ * Analytics metrics payload returned by the custom backend API
+ */
export interface AnalyticsData {
totalViews: number;
todayViews: number;
@@ -22,7 +24,9 @@ interface AnalyticsModalProps {
stats: AnalyticsData;
}
-/** Formats unix timestamps into relative time strings (e.g., "5s ago", "2m ago") */
+/**
+ * Formats unix timestamps into relative time strings (e.g., "5s ago", "2m ago")
+ */
function getTimeAgo(timestamp?: number): string {
if (!timestamp) return "just now";
const seconds = Math.floor((Date.now() - timestamp * 1000) / 1000);
@@ -34,7 +38,9 @@ function getTimeAgo(timestamp?: number): string {
return `${hours}h ago`;
}
-/** Terminal-style modal inspector for viewing live traffic and system health metrics */
+/**
+ * Terminal-style modal inspector for viewing live traffic and system health metric
+ */
export default function AnalyticsModal({
isOpen,
onClose,
diff --git a/src/components/AnalyticsWidget.tsx b/src/components/AnalyticsWidget.tsx
index d44f3fb..5f14f41 100644
--- a/src/components/AnalyticsWidget.tsx
+++ b/src/components/AnalyticsWidget.tsx
@@ -3,7 +3,9 @@
import { useEffect, useState } from "react";
import AnalyticsModal, { AnalyticsData } from "./AnalyticsModal";
-/** Footer badge widget that fetches live metrics and triggers the analytics modal */
+/**
+ * Footer badge widget that fetches live metrics and triggers the analytics modal
+ */
export default function AnalyticsWidget() {
const [stats, setStats] = useState
(null);
const [isOpen, setIsOpen] = useState(false);
diff --git a/src/components/Background.tsx b/src/components/Background.tsx
index 40d70ef..4c133a6 100644
--- a/src/components/Background.tsx
+++ b/src/components/Background.tsx
@@ -1,6 +1,10 @@
+/**
+ * Global ambient background featuring a masked SVG-style grid overlay and subtle glow effect
+ */
export default function Background() {
return (
<>
+ {/* Radial-masked blue grid overlay */}
+ {/* Top ambient glow light */}
>
);
diff --git a/src/components/Contact.tsx b/src/components/Contact.tsx
index 84754f6..ebef5e1 100644
--- a/src/components/Contact.tsx
+++ b/src/components/Contact.tsx
@@ -2,29 +2,36 @@
import { useState } from "react";
+/**
+ * Contact section with interactive form, bot anti-spam protection, and direct email info
+ */
export default function Contact() {
+ // Form input states
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [message, setMessage] = useState("");
const [honeypot, setHoneypot] = useState("");
+ // Submission UI states
const [submitted, setSubmitted] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
+ // Bot detection timestamp (form interaction speed check)
const [formLoadTime] = useState(Date.now());
- const handleSubmit = async (e: React.SubmitEvent) => {
+ // Handles contact form submission with bot verification & PHP backend dispatch
+ const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
- // Bot check
+ // Anti-spam check 1: Submissions faster than 3 seconds are treated as automated bots
const timeTaken = (Date.now() - formLoadTime) / 1000;
-
if (timeTaken < 3) {
setSubmitted(true);
return;
}
+ // Anti-spam check 2: Honeypot field filled out by bots
if (honeypot !== "") {
setSubmitted(true);
return;
@@ -67,7 +74,7 @@ export default function Contact() {
id="contactme"
className="relative py-6 px-4 md:px-6 max-w-5xl mx-auto scroll-mt-15"
>
- {/* Title */}
+ {/* Section Header */}
// 04. Get in Touch
@@ -78,7 +85,7 @@ export default function Contact() {
- {/* Left column */}
+ {/* Left Column: Direct Info & Terminal Card */}
@@ -96,6 +103,7 @@ export default function Contact() {
contribute to your team.
+ {/* Terminal Status Display */}
// Terminal Direct Contact
@@ -111,6 +119,7 @@ export default function Contact() {
+ {/* Email Shortcut */}
// Prefer email?
@@ -121,7 +130,7 @@ export default function Contact() {
- {/* Right column */}
+ {/* Right Column: Contact Form / Success Message */}
{submitted ? (
+ /* Success Message */
✓
Thank you!
- Your message has been sent successfully. I'll get back to you
- as soon as possible!
+ Your message has been sent successfully. I'll get back to
+ you as soon as possible!
) : (
+ /* Form Container */