From e547986940445d91f2fe9f27ee7e9bc62f2410d7 Mon Sep 17 00:00:00 2001 From: Chneemann Date: Wed, 29 Jul 2026 08:21:50 +0200 Subject: [PATCH] docs & refactor: add JSDoc/PHPDoc comments and polish component structure --- backend/analytics.php | 32 +++++++++++------ backend/config.php | 3 ++ backend/sendMail.php | 56 +++++++++++++++++------------ backend/templates/emailTemplate.php | 30 ++++++++++++---- src/app/globals.css | 6 ++++ src/app/imprint/page.tsx | 12 ++++--- src/app/page.tsx | 3 ++ src/app/privacy/page.tsx | 14 +++++--- src/components/About.tsx | 15 ++++---- src/components/AnalyticsModal.tsx | 12 +++++-- src/components/AnalyticsWidget.tsx | 4 ++- src/components/Background.tsx | 5 +++ src/components/Contact.tsx | 38 +++++++++++++++----- src/components/Footer.tsx | 9 +++-- src/components/Portfolio.tsx | 29 +++++++++------ src/components/Skills.tsx | 19 ++++++---- src/components/Social.tsx | 6 ++++ 17 files changed, 207 insertions(+), 86 deletions(-) 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}
+
- + ➜ Quick Reply
+
"; 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 */
+ {/* Hidden Honeypot Field for Bot Catching */} + {/* Name Input */}
setName(e.target.value)} @@ -171,12 +185,14 @@ export default function Contact() { />
+ {/* Email Input */}
setEmail(e.target.value)} @@ -185,11 +201,13 @@ export default function Contact() { />
+ {/* Message Input */}