Compare commits

..

2 Commits

40 changed files with 3794 additions and 774 deletions

View File

@ -1,9 +0,0 @@
# AuthType Basic
# AuthName "Geschützter Bereich"
# AuthUserFile /FSST/Website/.htpasswd
# Require valid-user
#
# # .htpasswd vor Auslieferung schützen
# <Files ".htpasswd">
# Require all denied
# </Files>

View File

@ -1 +0,0 @@
FSST:$apr1$HhgwKWOh$AtczxeE9BzXsBLaQLDKB20

37
.idea/php.xml generated
View File

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCSFixerOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCodeSnifferOptionsConfiguration">
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpCodeSniffer">
<phpcs_settings>
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="b74b3486-711a-42ad-bf18-c51cc1addaa5" timeout="30000" />
</phpcs_settings>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="8.2">
<option name="suggestChangeDefaultLanguageLevel" value="false" />
</component>
<component name="PhpStan">
<PhpStan_settings>
<phpstan_by_interpreter asDefaultInterpreter="true" interpreter_id="b74b3486-711a-42ad-bf18-c51cc1addaa5" timeout="60000" />
</PhpStan_settings>
</component>
<component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="Psalm">
<Psalm_settings>
<psalm_fixer_by_interpreter asDefaultInterpreter="true" interpreter_id="b74b3486-711a-42ad-bf18-c51cc1addaa5" timeout="60000" />
</Psalm_settings>
</component>
<component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" />
</component>
</project>

100
404.php
View File

@ -1,27 +1,53 @@
<?php
// Wichtig: echten 404-Status setzen
// !!! Funktioniert nur auf geizkragen.fabianschieder.com (Weil im VirtualHost configuriert) !!!
/**
* @file 404.php
* @brief Fehlerseite für nicht gefundene Dokumente (HTTP 404 Not Found)
*
* @details Diese Datei wird aufgerufen, wenn ein Benutzer eine URL aufruft,
* die auf dem Server nicht existiert. Sie sendet den korrekten HTTP-Statuscode 404 an den Browser,
* liest die angeforderte URI aus und bietet dem Benutzer benutzerfreundliche Möglichkeiten,
* zurück zur Startseite oder zur vorherigen Seite zu navigieren. Ein ansprechendes Design
* mit Glitch-Effekten macht die Fehlerseite visuell interessant.
*
* @author Fabian Schieder / Geizkragen Team
* @version 1.0
*/
// Wichtig: Echten 404-Status setzen, damit Suchmaschinen und Browser erkennen, dass die Seite nicht existiert.
// !!! Funktioniert in der Produktion nur zuverlässig (z.B. auf geizkragen.fabianschieder.com),
// wenn die Fehlerseite im VirtualHost oder der .htaccess entsprechend für 404-Fehler konfiguriert ist !!!
http_response_code(404);
// Request-Daten absichern
// Request-Daten absichern, um XSS (Cross-Site Scripting) Angriffe bei der Ausgabe zu verhindern.
// htmlspecialchars wandelt spezielle Zeichen (<, >, &, ", ') in ungefährliche HTML-Entitäten um.
$requestUri = htmlspecialchars($_SERVER['REQUEST_URI'] ?? '', ENT_QUOTES, 'UTF-8');
$method = htmlspecialchars($_SERVER['REQUEST_METHOD'] ?? '', ENT_QUOTES, 'UTF-8');
// Optional: einfaches Logging
// Optional: Einfaches Logging der 404-Fehler für administrative Zwecke.
// Schreibt die IP-Adresse des Clients, die HTTP-Methode und die angeforderte fehlerhafte URI in das PHP Error-Log.
error_log("[404] " . ($_SERVER['REMOTE_ADDR'] ?? '') . " $method $requestUri");
?>
<!DOCTYPE html>
<html lang="de">
<head>
<!-- Metadaten zur Zeichenkodierung und für ein responsives Layout -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Einbindung des Favicons -->
<link rel="icon" href="/assets/images/favicon.ico" sizes="any">
<!-- Einbindung des globalen Stylesheets -->
<link rel="stylesheet" href="/style.css">
<!-- Titel der Seite, der im Browser-Tab angezeigt wird -->
<title>404 Seite nicht gefunden | Geizkragen</title>
</head>
<body>
<!-- Hauptinhaltsbereich der Fehlerseite -->
<main>
<!-- Container für die Zentrierung und Ausrichtung der Inhalte -->
<div class="container" style="
display: flex;
flex-direction: column;
@ -31,12 +57,22 @@ error_log("[404] " . ($_SERVER['REMOTE_ADDR'] ?? '') . " $method $requestUri");
text-align: center;
padding-block: 4rem;
">
<!-- 404 Glitch-Zahl -->
<!--
404 Glitch-Zahl:
Ein dekoratives Element, das die Zahl 404 groß und mit einem visuellen Störungs-Effekt (Glitch) darstellt.
Das Attribut aria-hidden="true" versteckt es vor Screenreadern, da es rein dekorativ ist.
-->
<div class="error-code" aria-hidden="true">404</div>
<!-- Card -->
<!--
Error Card:
Eine visuelle Karte, die den Text, das Icon und die Navigations-Aktionen strukturiert zusammenfasst.
-->
<div class="error-card">
<!-- Icon-Bereich in der Fehlerkarte -->
<div class="error-card__icon">
<!-- SVG-Icon: Ein durchgestrichener Kreis zur symbolischen Darstellung eines Fehlers -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" width="28" height="28">
<circle cx="11" cy="11" r="8"/>
@ -45,14 +81,20 @@ error_log("[404] " . ($_SERVER['REMOTE_ADDR'] ?? '') . " $method $requestUri");
</svg>
</div>
<!-- Überschrift für die Fehlerkarte -->
<h1 class="error-card__title">Seite nicht gefunden</h1>
<!-- Erklärender Text für den Benutzer -->
<p class="error-card__text">
Die Seite, die du suchst, existiert leider nicht oder wurde verschoben.
</p>
<!-- Ausgabe des angeforderten Pfads, der zum Fehler geführt hat -->
<div class="error-card__path"><?php echo $requestUri; ?></div>
<!-- Container für die Navigationsbuttons -->
<div class="error-card__actions">
<!-- Button: Zurück zur Startseite -->
<a href="/index.php" class="btn btn--primary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" width="18" height="18">
@ -61,6 +103,8 @@ error_log("[404] " . ($_SERVER['REMOTE_ADDR'] ?? '') . " $method $requestUri");
</svg>
Zur Startseite
</a>
<!-- Button: Zurück zur vorherigen Seite im Browser-Verlauf -->
<button onclick="history.back()" class="btn btn--ghost">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" width="18" height="18">
@ -74,24 +118,36 @@ error_log("[404] " . ($_SERVER['REMOTE_ADDR'] ?? '') . " $method $requestUri");
</div>
</main>
<!-- Inlined CSS-Bereich für das spezifische Styling der 404-Komponenten -->
<style>
/**
* @brief Styling für die 404 Glitch-Zahl
* @details Definiert eine große, animierte Textdarstellung mit einem Farbgradienten.
*/
/* ── 404 Glitch-Zahl ── */
.error-code {
font-size: clamp(7rem, 18vw, 12rem);
font-weight: 900;
line-height: 1;
letter-spacing: -0.04em;
/* Lineares Farbverlaufs-Hintergrundbild für den Text */
background: linear-gradient(135deg, var(--color-primary), #4f46e5, var(--color-accent));
background-size: 200% 200%;
/* Wendet den Hintergrund nur auf den Text an (Webkit und Standard) */
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
/* Animation zur Verschiebung des Gradienten */
animation: gradientShift 4s ease-in-out infinite;
margin-bottom: 1.5rem;
position: relative;
display: inline-block;
}
/**
* @brief Pseudo-Elemente für den Glitch-Tearing-Effekt
* @details Erstellt Kopien des Textes, die beschnitten und verschoben werden.
*/
.error-code::before,
.error-code::after {
content: '404';
@ -104,16 +160,19 @@ error_log("[404] " . ($_SERVER['REMOTE_ADDR'] ?? '') . " $method $requestUri");
background-clip: text;
}
/* Oberer Teil des Glitches */
.error-code::before {
clip-path: inset(0 0 65% 0);
animation: glitch1 3s infinite linear alternate-reverse;
}
/* Unterer Teil des Glitches */
.error-code::after {
clip-path: inset(65% 0 0 0);
animation: glitch2 3s infinite linear alternate-reverse;
}
/** Keyframes-Animationen für den wackeligen Glitch-Effekt */
@keyframes glitch1 {
0%,92% { transform: translate(0); }
93% { transform: translate(-6px,-2px); }
@ -130,6 +189,9 @@ error_log("[404] " . ($_SERVER['REMOTE_ADDR'] ?? '') . " $method $requestUri");
94%,100%{ transform: translate(0); }
}
/**
* @brief Styling für die Error-Karten-Komponente (Glassmorphism-Effekt)
*/
/* ── Card ── */
.error-card {
background: rgba(31, 41, 55, 0.55);
@ -144,6 +206,7 @@ error_log("[404] " . ($_SERVER['REMOTE_ADDR'] ?? '') . " $method $requestUri");
animation: fadeInUp 0.6s ease-out 0.1s both;
}
/* Styling für den Kreis mit dem Icon in der Karte */
.error-card__icon {
width: 56px;
height: 56px;
@ -157,6 +220,7 @@ error_log("[404] " . ($_SERVER['REMOTE_ADDR'] ?? '') . " $method $requestUri");
animation: pulse-glow 3s ease-in-out infinite;
}
/* Typografie für den Karten-Titel */
.error-card__title {
font-size: 1.4rem;
font-weight: 800;
@ -164,6 +228,7 @@ error_log("[404] " . ($_SERVER['REMOTE_ADDR'] ?? '') . " $method $requestUri");
margin-bottom: 0.6rem;
}
/* Typografie für den Info-Text */
.error-card__text {
color: var(--text-muted);
font-size: 0.95rem;
@ -171,6 +236,7 @@ error_log("[404] " . ($_SERVER['REMOTE_ADDR'] ?? '') . " $method $requestUri");
margin-bottom: 0.25rem;
}
/* Darstellung des Pfad-Textes im Monospace-Look */
.error-card__path {
display: inline-block;
background: var(--color-primary-soft);
@ -186,14 +252,18 @@ error_log("[404] " . ($_SERVER['REMOTE_ADDR'] ?? '') . " $method $requestUri");
max-width: 100%;
}
/**
* @brief Layout und Styling der Aktions-Buttons
*/
/* ── Buttons ── */
.error-card__actions {
display: flex;
gap: 0.75rem;
gap: 0.75rem; /* Abstand zwischen den Buttons */
justify-content: center;
flex-wrap: wrap;
flex-wrap: wrap; /* Erlaubt Umbruch bei kleinen Bildschirmen */
}
/* Basis-Klasse für Buttons mit Flex-Eigenschaften und Transitionen */
.btn {
display: inline-flex;
align-items: center;
@ -209,39 +279,50 @@ error_log("[404] " . ($_SERVER['REMOTE_ADDR'] ?? '') . " $method $requestUri");
transition: all var(--transition-smooth);
}
/* Haupt-Button (Primäraktion) */
.btn--primary {
background: var(--color-primary);
color: #fff;
box-shadow: 0 4px 14px var(--color-primary-glow);
}
/* Hover-Zustand des Haupt-Buttons */
.btn--primary:hover {
background: var(--color-primary-hover);
transform: translateY(-2px);
transform: translateY(-2px); /* Hebe-Effekt */
box-shadow: 0 8px 24px var(--color-primary-glow);
color: #fff;
}
/* Sekundärer Ghost-Button (Transparenter Hintergrund) */
.btn--ghost {
background: rgba(255,255,255,0.04);
color: var(--text-muted);
border: 1px solid var(--border-subtle);
}
/* Hover-Zustand des Ghost-Buttons */
.btn--ghost:hover {
background: rgba(255,255,255,0.09);
color: var(--text-primary);
transform: translateY(-2px);
}
/**
* @brief Responsive Anpassungen (Media Queries)
* @details Passt das Layout auf kleineren Bildschirmen (z.B. Mobilgeräten) an.
*/
/* ── Responsive ── */
@media (max-width: 480px) {
/* Verringerte Polsterung der Karte für kleine Screens */
.error-card {
padding: 1.8rem 1.25rem 1.5rem;
}
/* Stapeln der Buttons untereinander */
.error-card__actions {
flex-direction: column;
}
/* Buttons füllen die gesamte Breite aus und zentrieren ihren Inhalt */
.btn {
justify-content: center;
width: 100%;
@ -251,3 +332,4 @@ error_log("[404] " . ($_SERVER['REMOTE_ADDR'] ?? '') . " $method $requestUri");
</body>
</html>

250
README.md
View File

@ -1,250 +0,0 @@
# Geizkragen Preisvergleichs- und Produktplattform
## Kurzüberblick
Geizkragen ist eine webbasierte Anwendung zur Produktsuche und zum Preisvergleich. Nutzer können Produkte durchsuchen, vergleichen und in einer persönlichen Wunschliste verwalten.
Das Projekt dient als technische Demonstration einer klassischen PHP-basierten Webanwendung mit Datenbankanbindung.
---
## Funktionsumfang
* Produktsuche (inkl. API-Endpunkt)
* Vergleich mehrerer Produkte
* Wunschliste (benutzergebunden)
* Benutzerregistrierung und Login
* Benutzerkonto-Verwaltung
* Administrationsbereich zur Benutzerverwaltung
* Hinzufügen neuer Produkte
* Produktdetailseiten
* Einfache statistische Auswertungen
* Grundlegendes Empfehlungssystem
---
## Projektstruktur
```bash
geizkragen/
├── index.php
├── productpage.php
├── compare.php
├── wunschliste.php
├── login.php
├── register.php
├── account.php
├── admin_users.php
├── api/
│ └── search_products.php
├── assets/
│ ├── css/
│ ├── images/
│ └── icons/
├── header.php
├── footer.php
├── style.css
├── stats.json
└── .htaccess
```
---
## Installation und Setup
### Voraussetzungen
* Webserver (z. B. Apache)
* PHP (empfohlen ≥ 7.x)
* MySQL oder MariaDB
---
### Installation
```bash
git clone <repository-url>
cd geizkragen
```
Oder manuell:
Projekt in das Webserver-Verzeichnis kopieren, z. B.:
```
C:\xampp\htdocs\geizkragen
```
---
### Datenbankeinrichtung
1. Neue Datenbank erstellen
2. Tabellen entsprechend dem Schema anlegen
3. Zugangsdaten in den PHP-Dateien konfigurieren
---
### Start
Webserver starten und im Browser öffnen:
```
http://localhost/geizkragen
```
---
## API
### Produktsuche
```
GET /api/search_products.php?q=Suchbegriff
```
Antwort:
* JSON mit passenden Produkten
---
## Technische Architektur
### Backend
* PHP
* Serverseitige Verarbeitung
* Teilweise API-Struktur
### Frontend
* HTML5
* CSS
* Klassisches serverseitiges Rendering
### Datenhaltung
* MySQL / MariaDB
* Ergänzend JSON (Statistiken)
---
## Sicherheit und Hinweis zur öffentlichen Nutzung
Dieses Repository ist öffentlich zugänglich.
Der aktuelle Stand ist als Lern- bzw. Entwicklungsprojekt zu verstehen und **nicht für den produktiven Einsatz im Internet vorgesehen**.
Bekannte Einschränkungen:
* Sicherheitsmechanismen sind nicht vollständig implementiert
* Authentifizierung und Session-Handling sollten überprüft werden
* Eingabevalidierung und Schutz gegen SQL-Injection sind teilweise unzureichend
* Keine Garantie für Datenschutz oder Datensicherheit
**Empfehlung:**
Vor einem produktiven Einsatz müssen grundlegende Sicherheitsmaßnahmen implementiert werden.
---
## Empfohlene Sicherheitsmaßnahmen
* Verwendung von Prepared Statements (PDO/MySQLi)
* Passwort-Hashing mit `password_hash()`
* CSRF-Schutz für Formulare
* Strikte Validierung und Sanitizing aller Eingaben
* Sichere Session-Verwaltung (Session-Regeneration)
* Fehlerausgaben im Produktivbetrieb deaktivieren
---
## Erweiterungsmöglichkeiten
* Einführung eines MVC-Patterns
* Trennung von Backend und Frontend (API-first)
* Nutzung eines modernen Frontend-Frameworks
* Verbesserung der Benutzeroberfläche (Responsive Design)
* Erweiterung des Empfehlungssystems
* Automatisierte Tests
---
## Entwicklungsumgebung
Empfohlen:
* PhpStorm oder Visual Studio Code
* MySQL Workbench
* XAMPP oder vergleichbare lokale Serverumgebung
---
## Lizenz
Derzeit ist keine Lizenz definiert.
Ohne Lizenz ist die Nutzung, Veränderung und Weitergabe rechtlich eingeschränkt.
Empfehlung:
Eine Open-Source-Lizenz wie MIT oder Apache 2.0 hinzufügen.
---
## Autor
Fabian Schieder
Paul Eisenbock
---
## Fachliche Einschätzung
Das Projekt ist ein solides Beispiel für eine klassische PHP-Webanwendung mit Datenbankanbindung.
Für Lernzwecke ist die Struktur sinnvoll, jedoch fehlen klare architektonische Trennungen und sicherheitsrelevante Mechanismen.
Für eine Weiterentwicklung sind insbesondere folgende Punkte entscheidend:
* Strukturierung (MVC oder ähnliche Muster)
* Absicherung der Datenbankzugriffe
* Saubere Trennung von Logik und Darstellung
* Skalierbarkeit durch API-basierte Architektur
---
## Typische Fehlerquellen und Best Practices
### Häufige Probleme
* SQL-Injection durch direkte Queries
* Fehlendes Passwort-Hashing
* Vermischung von PHP und HTML
* Unsichere Session-Verwaltung
* Fehlende Eingabevalidierung
### Best Practices
* Prepared Statements konsequent verwenden
* Zentrale Konfigurationsdateien nutzen
* Logging und Fehlerhandling implementieren
* Code modularisieren
* Sicherheitskonzepte früh integrieren
---
## Typischer Ablauf
1. Benutzer sucht ein Produkt
2. Ergebnisse werden angezeigt
3. Produktdetailseite wird geöffnet
4. Optional: Vergleich oder Speicherung in Wunschliste
---
## Zusammenfassung
* PHP-basierte Preisvergleichsplattform
* Klassische serverseitige Architektur
* Öffentliche Repository mit Lerncharakter
* Sicherheit derzeit nicht ausreichend für Produktion
* Gute Basis für Erweiterung und Refactoring

View File

@ -1,36 +1,97 @@
<?php
/**
* @file account.php
* @brief Profil- und Kontoverwaltungsseite des Benutzers.
*
* @details Diese Datei stellt die Benutzeroberfläche für das Benutzerprofil dar.
* Sie überprüft, ob eine aktive Benutzersitzung vorliegt, ruft die Benutzerdaten
* (wie Anzeigename, E-Mail und Profilbild) aus der Datenbank ab und zeigt diese an.
* Zudem bietet sie Funktionen zum Hochladen eines neuen Profilbildes, Verlinken zu
* administrativen oder benutzerspezifischen Aktionen (wie Wunschliste und Produktverwaltung)
* sowie die Möglichkeit, sich vom System abzumelden.
*
* @author Geizkragen-Projekt
* @date 2026-04-03
*/
/**
* @brief Einbinden der Bootstrap-Datei.
*
* Beinhaltet Konfigurationen, Konstanten und grundlegende Funktionen (wie z. B. DB-Verbindung),
* die für den gesamten Ablauf erforderlich sind.
*/
require_once __DIR__ . '/lib/bootstrap.php';
/**
* @brief Überprüfung der Benutzersitzung.
*
* Wenn die Session-Variable 'user_id' nicht gesetzt ist, wird der Benutzer sofort
* auf die Login-Seite weitergeleitet.
*/
if (empty($_SESSION['user_id'])) {
header('Location: login.php');
exit();
}
/**
* @var int $userId Die ID des aktuell angemeldeten Benutzers, sicher als Integer gecastet.
*/
$userId = (int)$_SESSION['user_id'];
/**
* @var mysqli $conn Das aktive Datenbankverbindungsobjekt.
*/
$conn = db_connect();
/**
* @brief Vorbereiten der SQL-Abfrage zur Ermittlung der Benutzerdaten.
*
* Holt die userID, den Anzeigenamen, die E-Mail-Adresse und den Pfad zum Profilbild
* aus der Tabelle 'users' für den gerade angemeldeten Benutzer.
*
* @var mysqli_stmt|false $stmt Das vorbereitete Statement.
*/
$stmt = $conn->prepare('SELECT userID, displayName, email, profilePicture FROM users WHERE userID = ? LIMIT 1');
if (!$stmt) {
// Bei einem Fehler beim Vorbereiten der Abfrage wird ein HTTP 500 Fehler gesendet und die Ausführung gestoppt.
http_response_code(500);
die('Datenbankfehler');
}
/**
* @brief Ausführen des Statements mit der Benutzer-ID.
*/
$stmt->bind_param('i', $userId);
$stmt->execute();
/**
* @var mysqli_result|false $result Das Ergebnis der Datenbankabfrage.
*/
$result = $stmt->get_result();
/**
* @brief Auswertung des Abfrageergebnisses.
*
* Wenn ein Datensatz gefunden wurde, wird das assoziative Array in $user gespeichert, ansonsten ist $user null.
*
* @var array|null $user Enthält die Benutzerdaten oder null, wenn kein Benutzer gefunden wurde.
*/
if ($result) {
$user = mysqli_fetch_assoc($result);
} else {
$user = null;
}
// Schließen des Statements und der Datenbankverbindung zur Ressourcenfreigabe.
$stmt->close();
$conn->close();
/**
* @brief Validierung der gefundenen Benutzerdaten.
*
* Falls kein gültiger Benutzer gefunden wurde (z.B. wenn der Benutzer zwischenzeitlich aus der DB gelöscht wurde),
* wird die aktuelle Sitzung zerstört und der Nutzer zum Login-Bildschirm umgeleitet.
*/
if (!$user) {
session_unset();
session_destroy();
@ -38,12 +99,25 @@ if (!$user) {
exit();
}
/**
* @brief Einbinden des HTML-Headers.
*
* Lädt den allgemeinen Kopfbereich der Webseite, inklusive CSS-Referenzen und Navigation.
*/
include 'header.php';
?>
<!--
@brief Hauptcontainer für die Account-Ansicht.
Definiert die Struktur für Profilanzeige und Einstellungen.
-->
<main class="auth" role="main">
<section class="account" aria-label="Account Bereich">
<!--
@brief Erfolgs- und Fehlermeldungen für den Profilbild-Upload.
Wertet GET-Parameter 'upload' aus, um dem Benutzer visuelles Feedback zu geben.
-->
<?php if (isset($_GET['upload']) && $_GET['upload'] === 'ok'): ?>
<p class="auth__alert__sucess account__toast" role="status">Profilbild wurde erfolgreich
aktualisiert.</p>
@ -54,7 +128,12 @@ include 'header.php';
<?php endif; ?>
<!-- ═══ Profil-Sidebar ═══ -->
<!--
@brief Container für die Profildaten.
Zeigt Avatar, Anzeigename, User-ID und E-Mail-Adresse an.
-->
<div class="auth__card account__profile">
<!-- Avatar-Anzeige -->
<div class="account__avatar-wrapper">
<img class="account__avatar"
src="<?php echo htmlspecialchars($user['profilePicture']); ?>"
@ -62,8 +141,10 @@ include 'header.php';
width="180">
</div>
<!-- Name des Benutzers -->
<h1 class="account__displayname"><?php echo htmlspecialchars($user['displayName'], ENT_QUOTES, 'UTF-8'); ?></h1>
<!-- Zusätzliche Details -->
<dl class="account__details">
<div class="account__detail-row">
<dt>User-ID</dt>
@ -77,9 +158,17 @@ include 'header.php';
</div>
<!-- ═══ Einstellungen ═══ -->
<!--
@brief Container für Account-Einstellungen.
Beinhaltet Abschnitte für Profilbild ändern, Schnellaktionen und das Ausloggen.
-->
<div class="account__settings">
<!-- Profilbild ändern -->
<!--
@brief Formular zum Hochladen eines neuen Profilbilds.
Es wird ein POST-Request mit multipart/form-data an upload.php gesendet.
-->
<div class="auth__card account__section">
<h2 class="account__section-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
@ -103,6 +192,10 @@ include 'header.php';
</div>
<!-- Schnellaktionen -->
<!--
@brief Bereich für schnellen Zugriff auf wichtige Funktionen.
Wenn der Benutzer die Rolle ADMIN hat, werden zusätzliche Links angezeigt.
-->
<div class="auth__card account__section">
<h2 class="account__section-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
@ -112,7 +205,13 @@ include 'header.php';
Schnellaktionen
</h2>
<div class="account__quick-actions">
<?php if (!empty($_SESSION['user_roles']) && in_array('ADMIN', $_SESSION['user_roles'], true)): ?>
<?php
/**
* @brief Prüfung der Administrator-Rolle.
* Falls der Benutzer ein Admin ist, werden Links zur Produkt- und Benutzerverwaltung gerendert.
*/
if (!empty($_SESSION['user_roles']) && in_array('ADMIN', $_SESSION['user_roles'], true)):
?>
<a href="productAdder.php" class="auth__submit account__action-link">
Produkt hinzufügen
</a>
@ -120,6 +219,9 @@ include 'header.php';
Benutzerverwaltung
</a>
<?php endif; ?>
<!--
@brief Link zur Wunschliste für alle Benutzer.
-->
<a href="wunschliste.php"
class="auth__submit account__action-link account__action-link--secondary">
Meine Wunschliste
@ -128,6 +230,10 @@ include 'header.php';
</div>
<!-- Abmelden -->
<!--
@brief Logout-Bereich.
Stellt ein Formular bereit, das ein Ausloggen aus der Anwendung ermöglicht.
-->
<div class="auth__card account__section account__section--danger">
<h2 class="account__section-title account__section-title--danger">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
@ -150,4 +256,11 @@ include 'header.php';
</section>
</main>
<?php include 'footer.php'; ?>
<?php
/**
* @brief Einbinden des HTML-Footers.
*
* Schließt die HTML-Struktur ab und lädt mögliche globale Skripte.
*/
include 'footer.php';
?>

View File

@ -1,8 +1,33 @@
<?php
// ad_recommendation.php
/**
* @file ad_recommendation.php
* @brief Zeigt eine zufällige Produktempfehlung als Werbebanner an (Ad Recommendation).
*
* @details Diese Datei verbindet sich mit der Datenbank, wählt ein zufälliges Produkt
* aus der Tabelle `products` aus, ermittelt den günstigsten Preis für dieses Produkt
* aus der Tabelle `offers` und stellt es dann in einem ansprechenden Werbebanner dar.
* Das Banner enthält aufmerksamkeitsstarke CSS-Animationen und -Stylings.
*
* @author GitHub Copilot
* @date 2026-04-03
*/
// Binde die Bootstrap-Datei ein, welche grundlegende Konfigurationen und Bibliotheken lädt
require_once __DIR__ . '/lib/bootstrap.php';
/**
* @var mysqli $conn Die aktive Datenbankverbindung.
*/
$conn = db_connect();
/**
* @brief Führt eine SQL-Abfrage aus, um ein zufälliges Produkt mit dessen Mindestpreis zu holen.
*
* @details Die Abfrage nutzt einen LEFT JOIN zwischen `products` (p) und `offers` (o).
* Sie gruppiert nach der Produkt-ID und wählt das Minimum der offer.price Spalte.
* ORDER BY RAND() wird verwendet, um bei jedem Aufruf ein zufälliges Produkt zu erhalten.
* LIMIT 1 stellt sicher, dass exakt ein Datensatz zurückgeliefert wird.
*/
$stmt = $conn->query("
SELECT p.productID, p.model, p.description, p.imagePath, MIN(o.price) as minPrice
FROM products p
@ -12,13 +37,29 @@ $stmt = $conn->query("
LIMIT 1
");
// Prüfe, ob die Abfrage erfolgreich war und mindestens ein Ergebnis gefunden wurde
if ($stmt && $stmt->num_rows > 0) {
/**
* @var array $randomProduct Assoziatives Array mit den abgerufenen Produktdaten.
*/
$randomProduct = $stmt->fetch_assoc();
/** @var int $rID Die eindeutige Produkt-ID, zu einem Integer gecastet. */
$rID = (int)$randomProduct['productID'];
/** @var string $rModel Das Modell bzw. der Name des Produkts, HTML-Entitäten umgewandelt. */
$rModel = htmlspecialchars($randomProduct['model'] ?? '');
/** @var string $rDesc Die Beschreibung des Produkts, HTML-Entitäten umgewandelt. */
$rDesc = htmlspecialchars($randomProduct['description'] ?? '');
/** @var string $rImg Der Pfad zum Produktbild, Fallback auf Platzhalter falls leer. */
$rImg = htmlspecialchars($randomProduct['imagePath'] ?? 'assets/images/placeholder.png');
/** @var float|null $rPriceRaw Der unverarbeitete Mindestpreis aus der Datenbank. */
$rPriceRaw = $randomProduct['minPrice'];
/** @var string $rPriceFormatted Der formatierte Preis im deutschen Format (z.B. 100,00), oder Fallback 100,00 falls kein Preis gefunden wurde. */
$rPriceFormatted = $rPriceRaw ? number_format((float)$rPriceRaw, 2, ',', '.') : '100,00';
?>
<style>
@ -219,21 +260,35 @@ if ($stmt && $stmt->num_rows > 0) {
}
</style>
<!-- Start des Werbebanner-Containers -->
<div class="ad-recommendation-wrapper">
<!-- Fliegende Catchy Tags für mehr Aufmerksamkeit -->
<div class="ad-floating-tag tag-left">SALE!</div>
<div class="ad-floating-tag tag-right">HOT DEAL</div>
<!-- Eigentlicher Inhalt der Empfehlung -->
<div class="ad-recommendation">
<!-- Textinhalt des Banners (Badge, Titel, Preis, Beschreibung, Button) -->
<div class="ad-recommendation__content">
<span class="ad-recommendation__badge">Empfehlung des Tages</span>
<h2><?= $rModel ?></h2>
<div class="ad-recommendation__price">ab <?= $rPriceFormatted ?> &euro;</div>
<?php
/**
* @brief Bestimmt die Länge der Beschreibung und kürzt sie bei Bedarf ab.
* @details Verwendet Multibyte-String-Funktionen falls verfügbar (mb_strlen / mb_substr),
* ansonsten die Standard-String-Funktionen.
*/
$descLen = function_exists('mb_strlen') ? mb_strlen($rDesc) : strlen($rDesc);
/** @var string $descShort Gekürzte Beschreibung (max 120 Zeichen), mit angehängten Punkten falls gekürzt. */
$descShort = $descLen > 120 ? (function_exists('mb_substr') ? mb_substr($rDesc, 0, 120) : substr($rDesc, 0, 120)) . '...' : $rDesc;
?>
<p><?= $descShort ?></p>
<a href="productpage.php?id=<?= $rID ?>" class="ad-recommendation__btn">Jetzt ansehen &rsaquo;</a>
</div>
<!-- Bild-Container zur Darstellung des Produktfotos -->
<div class="ad-recommendation__image-wrapper">
<img src="<?= $rImg ?>" alt="<?= $rModel ?>">
</div>

View File

@ -1,79 +1,142 @@
<?php
// admin_users.php
/**
* @file admin_users.php
* @brief Backend-Skript für die Benutzerverwaltung (Admin-Panel).
* @author GitHub Copilot
* @date 2026-04-03
*
* @details Ermöglicht es Administratoren, Benutzer anzuzeigen, nach ihnen zu suchen,
* ihre Rollen zu ändern und sie endgültig zu löschen. Zentrale administrative Komponente
* des Geizkragen-Projekts.
*/
/**
* @brief Bindet die Bootstrap-Datei ein, um die grundlegende Konfiguration, Session-Start
* und Hilfsfunktionen zu laden.
*/
require_once __DIR__ . '/lib/bootstrap.php';
// 1) Zugriffskontrolle nur ADMIN
/**
* 1) Zugriffskontrolle nur ADMIN
* @brief Überprüft, ob der angemeldete Benutzer die "ADMIN"-Rolle besitzt, um unbefugten
* Zugriff zu verhindern.
*/
if (empty($_SESSION['user_id']) || empty($_SESSION['user_roles']) || !in_array('ADMIN', $_SESSION['user_roles'], true)) {
/// @details Beendet die Skriptausführung mit einer Fehlermeldung, falls die Berechtigungen unzureichend sind.
/// Das verhindert das Ausführen jeglicher Administrationslogik durch normale Nutzer.
die("Zugriff verweigert. Nur Administratoren dürfen diese Seite sehen.");
}
/**
* @var mysqli $conn
* @brief Datenbankverbindung, um Abfragen auf der Nutzer-Tabelle auszuführen.
*/
$conn = db_connect();
// 2) Aktion: Benutzer löschen
/**
* 2) Aktion: Benutzer löschen
* @brief Verarbeitet Löschanfragen für Benutzer.
*/
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_user_id'])) {
/// @var int $deleteId Konvertiert die übergebene Benutzer-ID sicher in einen Integer.
$deleteId = (int)$_POST['delete_user_id'];
// Vermeide Selbstlöschung zur Sicherheit
/**
* @brief Vermeidet Selbstlöschung zur Sicherheit.
*/
if ($deleteId !== (int)$_SESSION['user_id']) {
// Zunächst Abhängigkeiten wie Rollen löschen
/// Entfernt zunächst die Einträge des Benutzers aus der `userRoles`-Tabelle, um Fremdschlüsselprobleme zu vermeiden.
$conn->query("DELETE FROM userRoles WHERE userID = $deleteId");
/// @var mysqli_stmt $delStmt Bereitet das Lösch-Statement für die `users`-Tabelle vor.
$delStmt = $conn->prepare("DELETE FROM users WHERE userID = ?");
/// Bindet die Benutzer-ID (Parameter-Typ i=Integer) an das Statement.
$delStmt->bind_param("i", $deleteId);
/// Führt die Löschung in der Datenbank aus.
$delStmt->execute();
/// Schließt das Prepared Statement.
$delStmt->close();
/// @var string $successMsg Setzt die Erfolgsmeldung.
$successMsg = "Benutzer erfolgreich gelöscht.";
} else {
/// @var string $errorMsg Setzt die Fehlermeldung (Selbstlöschung unzulässig).
$errorMsg = "Du kannst dich nicht selbst löschen.";
}
}
// 2b) Aktion: Rollen aktualisieren
/**
* 2b) Aktion: Rollen aktualisieren
* @brief Verarbeitet Massen-Aktualisierungen von Benutzerrollen aus dem Formular.
* @details Diese Aktion wird ausgelöst, wenn der Administrator auf "Alle Rollen speichern" klickt.
*/
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_all_roles'])) {
/// @var array $usersRolesData Die neu zugewiesenen Benutzerrollen (ID als Key, Rollen-ID als Value).
$usersRolesData = isset($_POST['user_roles']) && is_array($_POST['user_roles']) ? $_POST['user_roles'] : [];
/// @var array $submittedUsers Liste der Benutzer, deren Rollen übermittelt wurden.
$submittedUsers = isset($_POST['submitted_users']) && is_array($_POST['submitted_users']) ? $_POST['submitted_users'] : [];
/// Iteriert über jeden übermittelten Benutzer, um seine Rollenzuweisungen anzupassen.
foreach ($submittedUsers as $uId) {
/// @var int $updateId Die ID des aktuellen Benutzers.
$updateId = (int)$uId;
/// Übergeht den Administrator, der die Seite gerade aufruft (Sicherheitsmassnahme).
if ($updateId === (int)$_SESSION['user_id']) {
continue;
}
/// @var string $selectedRole Die aus dem Formular ausgewählte Rolle.
$selectedRole = isset($usersRolesData[$updateId]) ? $usersRolesData[$updateId] : '';
/// @var mysqli_stmt $delStmt Entfernt vorab bestehende Rollenzuweisungen des Benutzers aus der DB.
$delStmt = $conn->prepare("DELETE FROM userRoles WHERE userID = ?");
$delStmt->bind_param("i", $updateId);
$delStmt->execute();
$delStmt->close();
/// Falls eine neue Rolle gewählt wurde, fügt diese in die Datenbank ein.
if (!empty($selectedRole)) {
/// @var mysqli_stmt $insStmt Bereitet das Insert-Statement für `userRoles` vor.
$insStmt = $conn->prepare("INSERT INTO userRoles (userID, roleID) VALUES (?, ?)");
/// @var int $roleIdInt Die Rolle wird sicher auf Integer gecastet.
$roleIdInt = (int)$selectedRole;
/// Bindet UserID und Rollen-ID als Integer-Werte an die Query.
$insStmt->bind_param("ii", $updateId, $roleIdInt);
$insStmt->execute();
$insStmt->close();
}
}
/// Setzt Erfolgs-Feedback für den Bereich Rollenverwaltung.
$successMsg = "Rollen erfolgreich aktualisiert.";
}
// 2c) Alle verfügbaren Rollen laden
/**
* 2c) Alle verfügbaren Rollen laden
* @brief Liest alle Rollen aus, um sie in den Dropdowns zur Auswahl anzuzeigen.
*/
$allRoles = [];
/// Startet die Abfrage aller Datensätze der Tabelle `roles`, aufsteigend sortiert nach Namen.
$rolesQuery = $conn->query("SELECT roleID, name FROM roles ORDER BY name ASC");
if ($rolesQuery) {
/// Fügt jede gefundene Rolle dem Array hinzu.
while ($r = $rolesQuery->fetch_assoc()) {
$allRoles[] = $r;
}
}
// 3) Alle Benutzer laden (mit Suche)
/**
* 3) Alle Benutzer laden (mit Suche)
* @brief Verarbeitet Such- und Filterkriterien, um ein Set an Benutzern abzufragen.
*/
/// @var string $searchQuery Die übermittelte Suchanfrage.
$searchQuery = isset($_GET['search']) ? trim($_GET['search']) : '';
/// @var string $searchParam Wird als Parameter für die SQL-Wildcard-Suche vorbereitet.
$searchParam = '%' . $searchQuery . '%';
/// @var int $filterRole Ggf. übergebene Rollen-Filterung des Nutzers.
$filterRole = isset($_GET['role']) ? (int)$_GET['role'] : 0;
/// @var string $sql Basis-Select zum Abfragen der Benutzerinformationen und der gruppierten Rollen-IDs.
$sql = "
SELECT u.userID, u.email, u.displayname, u.profilePicture, u.isActive,
GROUP_CONCAT(ur.roleID) as roleIDs
@ -81,44 +144,61 @@ $sql = "
LEFT JOIN userRoles ur ON u.userID = ur.userID
";
/// @var array $whereClauses Array, mit dem die WHERE-Bedingungen flexibel aufgebaut werden sollen.
$whereClauses = [];
/// @var string $types Zusammenstellung von Typ-Spezifizierern für das `bind_param`.
$types = "";
/// @var array $params Liste der Werte, die für Prepared Statements gebunden werden.
$params = [];
/// Sofern die Suchanfrage nicht leer ist, wird nach Displayname und E-Mail gefiltert.
if ($searchQuery !== '') {
$whereClauses[] = "(u.displayname LIKE ? OR u.email LIKE ?)";
$types .= "ss";
$types .= "ss"; // ss referenziert 2 String Parameter
$params[] = $searchParam;
$params[] = $searchParam;
}
/// Fügt eine Bedingung hinzu, sofern mit einer bestimmten Rolle gefiltert werden soll.
if ($filterRole > 0) {
// Da wir einen LEFT JOIN mit GROUP_CONCAT haben und auf Rollen filtern wollen,
// können wir als einfache Lösung einen Subselect für EXISTS machen, damit
// alle Rollen des Benutzers in GROUP_CONCAT erhalten bleiben,
// aber nur Nutzer gezeigt werden, die auch die geforderte Rolle haben.
/**
* Da wir einen LEFT JOIN mit GROUP_CONCAT haben und auf Rollen filtern wollen,
* können wir als einfache Lösung einen Subselect für EXISTS machen, damit
* alle Rollen des Benutzers in GROUP_CONCAT erhalten bleiben,
* aber nur Nutzer gezeigt werden, die auch die geforderte Rolle haben.
*/
$whereClauses[] = "EXISTS (SELECT 1 FROM userRoles sub_ur WHERE sub_ur.userID = u.userID AND sub_ur.roleID = ?)";
$types .= "i";
$types .= "i"; // i referenziert einen Integer
$params[] = $filterRole;
}
/// Erweitert den SQL-String um die zusammengefassten Filter-Clauses.
if (!empty($whereClauses)) {
$sql .= " WHERE " . implode(" AND ", $whereClauses);
}
/// Fügt der SQL die finale Gruppierung und Sortierung nach User ID an.
$sql .= " GROUP BY u.userID ORDER BY u.userID ASC";
/// @var mysqli_stmt $stmtUsers Bereitet das komplette Nutzer-Bezugs-Statement vor.
$stmtUsers = $conn->prepare($sql);
/// Wenn Bindings existieren, binden wir sie dynamisch via `$params`-Spread-Operator an das SQL-Statement.
if (!empty($params)) {
$stmtUsers->bind_param($types, ...$params);
}
/// Das vorbereitete SQL ausführen.
$stmtUsers->execute();
/// @var mysqli_result $usersResult Empfängt das Result-Set aus der getätigten Fetch-Operation.
$usersResult = $stmtUsers->get_result();
/**
* @brief Sammelt Filter-Attribute für die Fortführung der URL-Parameter bei Massenänderungen.
*/
$formActionParams = [];
if ($searchQuery !== '') $formActionParams['search'] = $searchQuery;
if ($filterRole > 0) $formActionParams['role'] = $filterRole;
/// @var string $formActionUrl Stellt die Request-URI des Formulars samt GET-Parametern zusammen.
$formActionUrl = "admin_users.php";
if (!empty($formActionParams)) {
$formActionUrl .= "?" . http_build_query($formActionParams);
@ -126,9 +206,22 @@ if (!empty($formActionParams)) {
?>
<?php include 'header.php'; ?>
<?php
/** @brief Bindet das allgemeine Header-Template ein.
* @details Stellt den HTML-Kopf, CSS-Einbindungen und die Navigation bereit.
*/
include 'header.php';
?>
<!--
@brief Hauptcontainer für die Benutzerverwaltungs-Ansicht.
@details Nutzt die Klasse 'auth' für konsistentes Styling.
-->
<main class="auth" role="main">
<!--
@brief Abschnitt für das Grid-Layout der Admin-Tabelle.
@details Ermöglicht horizontales Scrollen auf kleineren Bildschirmen.
-->
<section class="auth__grid" style="display: block; max-width: 95%; width: max-content; margin: 40px auto; overflow-x: auto;">
<div class="auth__card" style="width: 100%; min-width: max-content;">
<header class="auth__header">
@ -136,10 +229,14 @@ if (!empty($formActionParams)) {
<p style="text-align: center; color: #94a3b8; font-size: 0.9rem; margin-top: 5px;">Hier siehst du alle registrierten Benutzer.</p>
</header>
<?php if (!empty($successMsg)): ?>
<?php
/** @brief Zeigt ggf. anfallende Erfolgsmeldungen formatiert an. */
if (!empty($successMsg)): ?>
<div class="auth__message auth__message--success" style="color: #4ade80; background: #064e3b; padding: 10px; border-radius: 4px; margin-bottom: 15px; text-align: center;"><?= htmlspecialchars($successMsg) ?></div>
<?php endif; ?>
<?php if (!empty($errorMsg)): ?>
<?php
/** @brief Zeigt ggf. anfallende Fehlermeldungen formatiert an. */
if (!empty($errorMsg)): ?>
<div class="auth__message auth__message--error" style="color: #f87171; background: #7f1d1d; padding: 10px; border-radius: 4px; margin-bottom: 15px; text-align: center;"><?= htmlspecialchars($errorMsg) ?></div>
<?php endif; ?>
@ -188,9 +285,13 @@ if (!empty($formActionParams)) {
</tr>
</thead>
<tbody>
<?php while ($user = $usersResult->fetch_assoc()): ?>
<?php
/** @brief Iteriert über die aus der Datenbank empfangene Liste der User. */
while ($user = $usersResult->fetch_assoc()): ?>
<?php
/// @var array $userRoles Explodiert (splittet) die aus den DB-Verknüpfungen abgeleiteten Strings in Arrays.
$userRoles = !empty($user['roleIDs']) ? explode(',', $user['roleIDs']) : [];
/// @var bool $isSelf Validiert, ob das bearbeitete Profil zum gerade agierenden Admin-User gehört.
$isSelf = (int)$user['userID'] === (int)$_SESSION['user_id'];
?>
<tr style="border-bottom: 1px solid #1e293b;">
@ -251,4 +352,7 @@ if (!empty($formActionParams)) {
</section>
</main>
<?php include 'footer.php'; ?>
<?php
/** @brief Bindet das allgemeine Footer-Template ein. */
include 'footer.php';
?>

View File

@ -1,39 +1,90 @@
<?php
/**
* @file search_products.php
* @brief Skript zur asynchronen Produktsuche (Live-Suche).
*
* @details Diese Datei dient als API-Endpunkt für die Echtzeit-Suche. Sie nimmt eine
* Suchanfrage über den GET-Parameter 'q' entgegen, durchsucht die Datenbanktabelle 'products'
* nach übereinstimmenden Modellnamen oder Beschreibungen und gibt die Resultate im JSON-Format zurück.
* Die Ergebnisse werden priorisiert, sodass exakte oder teilweise Übereinstimmungen im Modellnamen
* weiter oben erscheinen.
*
* @author Fabian
* @date 2026-04-04
*/
declare(strict_types=1);
/**
* Einbinden der Bootstrap-Datei.
* Übernimmt die Basiskonfiguration und lädt wichtige Bibliotheken.
*/
require_once __DIR__ . '/../lib/bootstrap.php';
/**
* Setzen des Content-Type-Headers auf JSON, da dieses Skript
* von JavaScript (AJAX/Fetch) aufgerufen wird und JSON erwartet.
*/
header('Content-Type: application/json; charset=utf-8');
/**
* Sicherheits-Header, der verhindert, dass der Browser den MIME-Typ der Antwort
* errät (MIME-Sniffing). Erhöht die Sicherheit der API.
*/
header('X-Content-Type-Options: nosniff');
try {
/**
* @var mysqli|PDO db_connect() Stellt eine Verbindung zur Datenbank her.
*/
$conn = db_connect();
// Query
/**
* @var string $q Der Such-String aus dem GET-Parameter 'q'.
* Falls nicht vorhanden, wird ein leerer String verwendet.
*/
$q = isset($_GET['q']) ? (string)$_GET['q'] : '';
$q = trim($q);
// Limit
/**
* @var int $limit Die maximale Anzahl der zurückzugebenden Suchergebnisse.
* Standard ist 8, falls kein Limit oder ein ungültiges Limit angegeben wird.
*/
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 8;
// Sicherstellen, dass mindestens 1 Ergebnis zurückgegeben wird
if ($limit < 1) {
$limit = 1;
}
// Begrenzen der maximalen Ergebnisse auf 15, um Datenbanküberlastung zu vermeiden
if ($limit > 15) {
$limit = 15;
}
// Minimum query length to reduce load/noise
/**
* Mindestlänge für die Suche.
* Wenn der Suchbegriff leer ist oder aus weniger als 1 Zeichen besteht,
* wird ein leeres JSON-Array zurückgegeben und die Ausführung beendet.
*/
if (mb_strlen($q, 'UTF-8') < 1) {
echo json_encode(['items' => []], JSON_UNESCAPED_UNICODE);
exit;
}
// Escape LIKE wildcards (% _), then add %...%
/**
* @var string $like Der escapte Such-String für das LIKE-Statement in SQL.
* Spezielle Wildcard-Zeichen (%) und (_) werden escapt, um SQL-Fehler
* oder unerwartetes Verhalten bei Benutzereingaben zu verhindern.
*/
$like = addcslashes($q, "%_\\");
$like = '%' . $like . '%';
// Simple search: model + description
/**
* @var string $sql Die SQL-Abfrage zur Suche nach Produkten.
* Durchsucht die Spalten `model` und `description`.
* Sortiert Treffer im `model` vor Treffern in der `description`.
*/
$sql = "
SELECT p.productID, p.model, p.description, p.imagePath
FROM products p
@ -44,24 +95,49 @@ try {
LIMIT ?
";
/**
* @var mysqli_stmt|PDOStatement $stmt Das vorbereitete SQL-Statement.
*/
$stmt = $conn->prepare($sql);
// Überprüfen, ob das Statement erfolgreich vorbereitet wurde
if (!$stmt) {
// HTTP-Statuscode 500 für einen internen Serverfehler setzen
http_response_code(500);
echo json_encode(['error' => 'DB-Query konnte nicht vorbereitet werden.'], JSON_UNESCAPED_UNICODE);
exit;
}
/**
* Binden der Parameter an das vorbereitete Statement.
* 'sssi' bedeutet: String, String, String, Integer.
*/
$stmt->bind_param('sssi', $like, $like, $like, $limit);
// Ausführen der vorbereiteten Abfrage
$stmt->execute();
// Holen des Ergebnisses aus der Datenbank
$res = $stmt->get_result();
/**
* @var array $items Das Array zur Speicherung der aufbereiteten Suchergebnisse.
*/
$items = [];
// Durchlaufen der einzelnen Datensätze / Zeilen
while ($row = $res->fetch_assoc()) {
/**
* @var int $id Die Produkt-ID iterierten Produkts.
*/
$id = (int)($row['productID'] ?? 0);
// Überspringe ungültige oder defekte IDs
if ($id <= 0) {
continue;
}
// Hinzufügen des formatierten Produkts in das Result-Array
$items[] = [
'id' => $id,
'model' => (string)($row['model'] ?? ''),
@ -71,9 +147,17 @@ try {
];
}
/**
* Rückgabe des Arrays als JSON-kodierter String.
* JSON_UNESCAPED_UNICODE verhindert, dass Umlaute escapet werden (z.B. %u00e4).
*/
echo json_encode(['items' => $items], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
/**
* Fehlerbehandlung im Falle einer Exception (z.B. bei einem Datenbankfehler).
* Sendet einen 500er Statuscode und eine generische Fehlermeldung als JSON.
*/
http_response_code(500);
echo json_encode(['error' => 'Serverfehler'], JSON_UNESCAPED_UNICODE);
}

View File

@ -1,7 +1,21 @@
/* ==========================================================
CATEGORY BAR
========================================================== */
/**
* @file catbar.css
* @brief This stylesheet defines the styling for the category navigation bar and the attribute filter bar.
* It includes styles for navigation items, active/hover states, responsiveness, and filter form components.
* @author Geizkragen Project
*/
/**
* ==========================================================
* CATEGORY BAR
* ==========================================================
*/
/**
* @class .home-nav
* @brief Main container for the home navigation bar.
* Provides the background color, padding, and flexbox layout.
*/
.home-nav {
width: 100%;
display: flex;
@ -10,7 +24,11 @@
padding: 1px 0;
}
/* Inner zentriert */
/**
* @class .home-nav__inner
* @brief Inner wrapper for the home navigation bar.
* Ensures the content expands and is centered correctly using flexbox properties.
*/
.home-nav__inner {
flex: 1;
width: 100%;
@ -19,7 +37,11 @@
align-items: center;
}
/* List */
/**
* @class .home-nav__list
* @brief Represents the unordered list containing the navigation links.
* Removes default list styling and sets up a horizontal flexbox container.
*/
.home-nav__list {
width: 100%;
display: flex;
@ -28,16 +50,27 @@
padding: 0;
}
/* LI nur als Container */
/**
* @selector .home-nav__list li
* @brief Defines the list items inside the navigation list.
* Acts as a relative container for flex items.
*/
.home-nav__list li {
flex: 1;
position: relative;
}
/* =============================== */
/* HOME CATEGORY LINKS */
/* =============================== */
/**
* ===============================
* HOME CATEGORY LINKS
* ===============================
*/
/**
* @selector .home-nav__list li a
* @brief Styles the anchor tags inside the list items.
* Sets up dimensions, padding, colors, font styling, and transitions for hover effects.
*/
.home-nav__list li a {
width: 100%;
padding: 8px 0;
@ -59,32 +92,53 @@
transition: background-color 0.2s ease;
}
/* Hover */
/**
* @selector .home-nav__list li a:hover
* @brief Defines the hover state for the navigation links.
* Applies a subtle white overlay using rgba.
*/
.home-nav__list li a:hover {
background-color: rgba(255,255,255,0.18);
}
/* Active (Mausklick) */
/**
* @selector .home-nav__list li a:active
* @brief Defines the active (mouse click down) state for the navigation links.
* Increases the opacity of the white overlay for visual feedback.
*/
.home-nav__list li a:active {
background-color: rgba(255,255,255,0.28);
}
/* Fokus (Tastatur) */
/**
* @selector .home-nav__list li a:focus-visible
* @brief Defines the focus state, mainly for keyboard navigation accessibility.
* Removes the default outline and adds a bespoke white box-shadow inset.
*/
.home-nav__list li a:focus-visible {
outline: none;
box-shadow: inset 0 0 0 2px rgba(255,255,255,0.5);
}
/* Aktive Kategorie (per PHP) */
/**
* @selector .home-nav__list li a.active
* @brief Styles the currently active category link, typically set server-side via PHP or by JS.
*/
.home-nav__list li a.active {
background-color: rgba(255,255,255,0.25);
}
/* =============================== */
/* TRENNSTRICHE ZWISCHEN KATEGORIEN */
/* =============================== */
/**
* ===============================
* TRENNSTRICHE ZWISCHEN KATEGORIEN
* ===============================
*/
/* Trennstrich rechts außer beim letzten */
/**
* @selector .home-nav__list li:not(:last-child)::after
* @brief Creates vertical separator lines between category items,
* except after the last item in the list.
*/
.home-nav__list li:not(:last-child)::after {
content: "";
position: absolute;
@ -96,7 +150,11 @@
background-color: rgba(255,255,255,0.3);
}
/* ─── Responsive: horizontal scroll on mobile ─── */
/**
* @media (max-width: 768px)
* @brief Responsive styles for mobile and small tablet screens.
* Enables horizontal scrolling for the category list with smooth scroll-snapping.
*/
@media (max-width: 768px) {
.home-nav__inner {
justify-content: flex-start;
@ -140,16 +198,28 @@
}
}
/* ==========================================================
ATTRIBUTE FILTER BAR
========================================================== */
/**
* ==========================================================
* ATTRIBUTE FILTER BAR
* ==========================================================
*/
/**
* @class .attrbar
* @brief Main container for the attribute filtering section.
* Matches the background color of the category bar and provides bottom padding.
*/
.attrbar {
width: 100%;
background-color: rgb(45, 59, 80); /* Same as catbar background */
padding: 0 0 16px 0;
}
/**
* @class .attrbar__inner
* @brief Inner container for the filter bar.
* Includes top padding and a subtle top border to separate it from the category links above.
*/
.attrbar__inner {
display: flex;
justify-content: center;
@ -159,6 +229,11 @@
width: 100%;
}
/**
* @class .attr-filter-form
* @brief Wrapper for the filter form components.
* Uses flexbox wrap to display filter items fluidly, aligning them appropriately across widths.
*/
.attr-filter-form {
display: flex;
flex-wrap: wrap;
@ -171,6 +246,11 @@
padding: 0 20px;
}
/**
* @class .attr-filter-item
* @brief Container for a single filter component (e.g. property label + dropdown).
* Lays out child elements in a flex column.
*/
.attr-filter-item {
display: flex;
flex-direction: column; /* Stack label and select */
@ -183,6 +263,11 @@
max-width: 250px;
}
/**
* @selector .attr-filter-item label
* @brief Styles the label text above each dropdown within the filter items.
* Uses uppercase text, smaller font size, and light spacing.
*/
.attr-filter-item label {
font-weight: 600;
text-transform: uppercase;
@ -191,6 +276,11 @@
color: rgba(255, 255, 255, 0.7);
}
/**
* @selector .attr-filter-item select
* @brief Styles the select (dropdown) inputs in the filter bar.
* Removes default webkit/moz appearance and replaces it with custom backgrounds, borders, and a custom SVG arrow.
*/
.attr-filter-item select {
width: 100%;
padding: 10px 14px;
@ -209,28 +299,51 @@
background-size: 16px;
}
/**
* @selector .attr-filter-item select:hover
* @brief Hover state for the select elements, adjusting background and border color.
*/
.attr-filter-item select:hover {
background-color: rgba(25, 30, 40, 0.8);
border-color: rgba(255, 255, 255, 0.3);
}
/**
* @selector .attr-filter-item select:focus
* @brief Focus state for the select elements, applying primary color highlights.
*/
.attr-filter-item select:focus {
outline: none;
border-color: var(--color-primary, #274a97);
box-shadow: 0 0 0 3px var(--color-primary-soft, rgba(39, 74, 151, 0.15));
}
/**
* @selector .attr-filter-item select option
* @brief Styles the option elements inside the dropdown.
* Sets background and text colors to match the dark theme.
*/
.attr-filter-item select option {
background-color: var(--bg-surface, #2d3b50);
color: var(--text-primary, #ffffff);
}
/**
* @class .attr-filter-action
* @brief Container for form action elements, like the reset button.
* Aligns to the bottom to match the input fields structurally.
*/
.attr-filter-action {
display: flex;
align-items: flex-end;
padding-bottom: 2px; /* optical alignment with inputs */
}
/**
* @class .attr-filter-reset
* @brief Styles for the filter reset button.
* Provides an inline-flex layout with a distinct danger (red) color scheme and hover effects.
*/
.attr-filter-reset {
display: inline-flex;
align-items: center;
@ -248,12 +361,22 @@
height: 40px; /* match select height */
}
/**
* @selector .attr-filter-reset:hover
* @brief Hover state for the reset button.
* Creates a slight lift effect using transform and adds a shadow.
*/
.attr-filter-reset:hover {
background-color: #b91c1c; /* slightly darker danger */
transform: translateY(-1px);
box-shadow: 0 4px 12px var(--color-danger-soft, rgba(217, 45, 32, 0.2));
}
/**
* @media (max-width: 768px)
* @brief Adjustments to the attribute filter layout for mobile and tablet devices.
* Forces items to take up near-half width or full width, and correctly spaces action buttons.
*/
@media (max-width: 768px) {
.attr-filter-item {
flex: 1 1 45%;

View File

@ -1,8 +1,28 @@
/**
* @file compare.css
* @brief Stylesheet für die Produktvergleichsseite.
*
* Diese Datei enthält alle spezifischen Styles für die Darstellung
* von zu vergleichenden Produkten in einer übersichtlichen Tabelle.
* Sie definiert Farben, Ränder, Schatten und Hover-Effekte für
* ein ansprechendes Dark-Mode-Design.
*/
/**
* @brief Hauptcontainer für die Vergleichsseite.
*
* Setzt den oberen Abstand und das allgemeine Padding für die Seite.
*/
.compare-page {
margin-top: 2rem;
padding: 1rem;
}
/**
* @brief Stil für den Seitentitel der Vergleichsseite.
*
* Definiert Schriftgröße, Ausrichtung, Farbe und Schriftstärke.
*/
.compare-title {
font-size: 2.5rem;
margin-bottom: 2rem;
@ -11,6 +31,12 @@
font-weight: 700;
}
/**
* @brief Styling für dem Fallstrick "Leerer Vergleich".
*
* Wird angezeigt, wenn keine Produkte zum Vergleich ausgewählt wurden.
* Enthält ein hervorgehobenes Box-Design mit Hintergrund und Schatten.
*/
.compare-empty {
text-align: center;
color: #cbd5e1;
@ -21,6 +47,11 @@
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4);
}
/**
* @brief Titel-Stil für Produktkategorien beim Vergleich.
*
* Unterteilt die Tabelle grafisch nach Kategorien mit entsprechenden Rändern.
*/
.compare-category-title {
margin-top: 3rem;
margin-bottom: 1.5rem;
@ -30,6 +61,12 @@
padding-bottom: 0.8rem;
}
/**
* @brief Wrapper für die scrollbare Vergleichstabelle.
*
* Ermöglicht horizontales Scrollen bei vielen Produkten auf kleinen Bildschirmen
* und setzt das äußere Erscheinungsbild (Hintergrund, Rahmen, Schatten).
*/
.compare-table-wrapper {
overflow-x: auto;
background: #1e293b;
@ -40,6 +77,11 @@
border: 1px solid rgba(255, 255, 255, 0.05);
}
/**
* @brief Grund-Styling der Vergleichstabelle.
*
* Definiert Mindestbreite und reguliert die Zellabstände (Border-Collapse).
*/
.compare-table {
width: 100%;
border-collapse: separate;
@ -47,6 +89,11 @@
min-width: 800px;
}
/**
* @brief Stile für Tabellenköpfe und Datenzellen.
*
* Fügt Ränder, Abstände und zentrierte Ausrichtung sowie die Textfarbe hinzu.
*/
.compare-table th,
.compare-table td {
padding: 1.5rem 1rem;
@ -56,10 +103,18 @@
vertical-align: middle;
}
/**
* @brief Entfernt den unteren Rand für die letzte Tabellenzeile.
*/
.compare-table tbody tr:last-child td {
border-bottom: none;
}
/**
* @brief Styling für die erste Spalte (Eigenschaftsnamen).
*
* Fixiert die Spalte links (Sticky) für leichteres horizontales Scrollen.
*/
.compare-table th:first-child,
.compare-table td:first-child {
text-align: left;
@ -73,6 +128,11 @@
box-shadow: 4px 0 6px -2px rgba(0, 0, 0, 0.2);
}
/**
* @brief Styling für den Tabellenkopf.
*
* Hebt die Kopfzeile farblich vom Rest der Tabelle ab.
*/
.compare-table th {
background: #0f172a;
font-weight: bold;
@ -81,20 +141,41 @@
padding-bottom: 2rem;
}
/**
* @brief Spezifisches Styling für die obere linke Zelle im Tabellenkopf.
*
* Erhöht den z-Index, damit sie beim Scrollen immer oben liegt.
*/
.compare-table th:first-child {
background: #0f172a;
z-index: 3;
font-size: 1.2rem;
}
/**
* @brief Hover-Effekt für Tabellenzeilen im Körper (Datenzellen).
*
* Hebt die gesamte Zeile hervor, wenn der Benutzer darüber fährt.
*/
.compare-table tbody tr:hover td {
background: #2a3a52;
}
/**
* @brief Hover-Effekt für die erste Zelle in der hovernden Tabellenzeile.
*
* Sorgt für eine konsistente Hervorhebung, auch bei der fixierten Spalte.
*/
.compare-table tbody tr:hover td:first-child {
background: #2a3a52;
}
/**
* @brief Styling für das Produktbild im Tabellenkopf.
*
* Passt die Bildgröße an, setzt einen hellen Verlaufshintergrund und
* implementiert einen abgerundeten Rahmen sowie eine Größenübergangsanimation.
*/
.compare-product-img {
height: 140px;
object-fit: contain;
@ -106,10 +187,20 @@
transition: transform 0.3s ease;
}
/**
* @brief Hover-Animation für das Produktbild.
*
* Vergrößert das Bild leicht, wenn der Nutzer mit der Maus darüber fährt.
*/
.compare-product-img:hover {
transform: scale(1.05);
}
/**
* @brief Styling für den Produktnamen.
*
* Definiert Farbe, Schriftgröße und Zeilenhöhe für den anklickbaren Produktnamen.
*/
.compare-product-name {
font-size: 1.15rem;
color: #60a5fa;
@ -121,10 +212,20 @@
line-height: 1.4;
}
/**
* @brief Hover-Effekt für den Produktnamen.
*
* Ändert die Textfarbe für visuelles Feedback beim Drüberfahren.
*/
.compare-product-name:hover {
color: #93c5fd;
}
/**
* @brief Styling für den Button zum Entfernen eines Produkts aus dem Vergleich.
*
* Roter Button (Pill-Shape) mit Schatten und Übergangseffekten.
*/
.compare-remove-btn {
background: #ef4444;
color: white;
@ -138,16 +239,32 @@
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
/**
* @brief Hover-Effekt für den Entfernen-Button.
*
* Dunkelt das Rot ab, bewegt den Button leicht nach oben und vergrößert den Schatten.
*/
.compare-remove-btn:hover {
background: #dc2626;
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
/**
* @brief Zebra-Streifen-Muster für gerade Tabellenzeilen.
*
* Wirkt sich nicht auf die fixierte erste Spalte aus, um die Lesbarkeit
* dunkel schattierter Hintergründe aufrechtzuerhalten.
*/
.compare-table tbody tr:nth-child(even) td:not(:first-child) {
background: rgba(15, 23, 42, 0.2);
}
/**
* @brief Styling für längere Beschreibungstexte im Vergleich.
*
* Begrenzt die Breite, richtet den Text linksbündig aus und optimiert die Zeilenhöhe.
*/
.desc-text {
text-align: left;
display: inline-block;
@ -155,4 +272,3 @@
line-height: 1.6;
font-size: 0.95rem;
}

View File

@ -1,8 +1,22 @@
/* ==========================================================
PRODUCT CARDS & SCROLL Animated Glass Cards
========================================================== */
/**
* @file compcard.css
* @brief Styles for Product Cards and Wishlist Cards.
* @details This file contains all CSS rules for displaying animated glass-style
* product cards, grid layouts, horizontal scroll sections, and the wishlist vertical layout.
* It also includes responsive design media queries for tablets and mobile devices.
*/
/* ─── Product Grid ─── */
/**
* ==========================================================
* @section PRODUCT CARDS & SCROLL Animated Glass Cards
* ==========================================================
*/
/**
* @brief Product Grid Container
* @details Creates a CSS grid with autofit columns of 260px each.
* Defines the spacing (gap) and padding around the grid.
*/
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fit, 260px);
@ -11,7 +25,12 @@
justify-content: start;
}
/* ─── Product Card ─── */
/**
* @brief Base Product Card Styles
* @details Sets up the generic layout, colors, shadow, and transitions for a product card.
* The card is designed with a dark background (#2d3b50) and an outline/border,
* utilizing a flexbox column layout. A fade-in animation (fadeInUp) is applied.
*/
.product-card {
background: #2d3b50;
border: 1px solid var(--border-subtle);
@ -29,7 +48,11 @@
animation: fadeInUp 0.5s ease both;
}
/* Gradient border glow on hover */
/**
* @brief Gradient border glow effect on hover
* @details Uses a pseudo-element (::before) to create a glowing border using a linear gradient.
* It features a mask to only reveal the border area. Hidden by default (opacity: 0) and shown on hover.
*/
.product-card::before {
content: "";
position: absolute;
@ -51,7 +74,11 @@
pointer-events: none;
}
/* Inner glow effect on hover */
/**
* @brief Inner glow effect on hover
* @details Uses a pseudo-element (::after) to add a subtle gradient at the top of the card.
* It is hidden by default and becomes visible when the card is hovered over.
*/
.product-card::after {
content: "";
position: absolute;
@ -85,6 +112,11 @@
outline-offset: 3px;
}
/**
* @brief Staggered entrance animation delays
* @details Applies incremental animation delays to cards within a horizontal scroll container
* to create a cascading fade-in effect.
*/
/* Stagger cards in scroll */
.product-scroll .product-card:nth-child(1) { animation-delay: 0.05s; }
.product-scroll .product-card:nth-child(2) { animation-delay: 0.1s; }
@ -95,7 +127,11 @@
.product-scroll .product-card:nth-child(7) { animation-delay: 0.35s; }
.product-scroll .product-card:nth-child(8) { animation-delay: 0.4s; }
/* ─── Card Image ─── */
/**
* @brief Card Image Styling
* @details Defines the size, padding, and positioning of the product image inside the card.
* Uses a white background and adds a bottom border.
*/
.product-card img {
width: 100%;
height: 175px;
@ -115,7 +151,11 @@
transform: scale(1.05);
}
/* ─── Card Content ─── */
/**
* @brief Card Content Container
* @details The wrapper for textual content (title, description, price) inside the card.
* Uses flexbox to space and align the text vertically.
*/
.product-card__content {
padding: 1rem 1.25rem 1.15rem;
flex: 1 1 auto;
@ -127,6 +167,11 @@
z-index: 2;
}
/**
* @brief Card Title Styling
* @details Styles the <h3> heading, limits it to 2 lines using webkit-line-clamp,
* and sets up a transition for color changes on hover.
*/
.product-card__content h3 {
font-size: 0.92rem;
font-weight: 600;
@ -140,10 +185,19 @@
transition: color var(--transition-normal);
}
/**
* @brief Card Title Hover State
* @details Changes the color of the <h3> heading when the parent card is hovered.
*/
.product-card:hover .product-card__content h3 {
color: var(--text-invert);
}
/**
* @brief Card Description Styling
* @details Styles the <p> paragraph tag. Limits text to 4 lines with ellipsis
* and handles word-breaking to prevent overflow.
*/
.product-card__content p {
font-size: 0.8rem;
color: #ffffff;
@ -156,6 +210,10 @@
line-height: 1.5;
}
/**
* @brief Card Price Styling
* @details Formats the price element, making it bold, blue, and pushed to the bottom of the card.
*/
.product-card__content .price {
font-size: 1.05rem;
font-weight: 700;
@ -164,12 +222,19 @@
padding-top: 0.5rem;
}
/* ─── Product Sections ─── */
/**
* @brief Product Section Wrapper
* @details Adds padding and an entrance animation for sections containing product lists/scrolls.
*/
.product-section {
padding: 2rem 0 0.5rem;
animation: fadeInUp 0.5s ease both;
}
/**
* @brief Product Section Title
* @details Styles the <h2> heading of the section, relative positioning is used for decorators.
*/
.product-section h2 {
margin-left: 2rem;
margin-bottom: 1.25rem;
@ -181,6 +246,11 @@
display: inline-block;
}
/**
* @brief Animated Accent Line for Section Title
* @details Creates a vertical gradient line next to the <h2> tag that GROWS into place
* using a CSS keyframe animation.
*/
/* Animated accent line */
.product-section h2::before {
content: "";
@ -195,10 +265,18 @@
animation: lineGrow 0.6s ease 0.3s forwards;
}
/**
* @brief Line Grow Keyframes
* @details Animation definition for the accent line, expanding its height to 80%.
*/
@keyframes lineGrow {
to { height: 80%; }
}
/**
* @brief Subtle Glow for Section Title
* @details Adds a blurred, colored circle behind the <h2> for a decorative lighting effect.
*/
/* Subtle glow behind section title */
.product-section h2::after {
content: "";
@ -214,7 +292,11 @@
opacity: 0.3;
}
/* ─── Horizontal Scroll ─── */
/**
* @brief Horizontal Scroll Container
* @details A flexible container that allows horizontal scrolling for product cards.
* Implements CSS scroll snapping and scroll masks to fade edges.
*/
.product-scroll {
display: flex;
gap: 1.25rem;
@ -229,32 +311,59 @@
-webkit-mask-image: linear-gradient(90deg, transparent, black 2rem, black calc(100% - 2rem), transparent);
}
/**
* @brief Product Scroll Item
* @details Fixes the width of cards within the scroll container and enables scroll snapping alignments.
*/
.product-scroll .product-card {
flex: 0 0 270px;
scroll-snap-align: start;
}
/**
* @brief Hide Scrollbar
* @details Removes the default scrollbar for Webkit browsers for a cleaner look.
*/
.product-scroll::-webkit-scrollbar {
display: none;
}
/* ─── Responsive ─── */
/**
* @section RESPONSIVE DESIGN FOR PRODUCT CARDS
*/
/**
* @brief Tablet Breakpoint (max-width: 768px)
* @details Adjusts grid columns, padding, font sizes, and card dimensions for tablet displays.
*/
@media (max-width: 768px) {
/**
* @brief Responsive Product Grid
*/
.product-grid {
grid-template-columns: repeat(auto-fit, 160px);
gap: 0.8rem;
padding: 1rem;
}
/**
* @brief Responsive Product Section Padding
*/
.product-section {
padding: 1.25rem 0 0.25rem;
}
/**
* @brief Responsive Section Title
*/
.product-section h2 {
margin-left: 1rem;
font-size: 1.1rem;
}
/**
* @brief Responsive Horizontal Scroll
*/
.product-scroll {
padding: 0.5rem 1rem 1.5rem;
gap: 0.8rem;
@ -262,69 +371,119 @@
-webkit-mask-image: linear-gradient(90deg, transparent, black 1rem, black calc(100% - 1rem), transparent);
}
/**
* @brief Responsive Product Scroll Item Size
*/
.product-scroll .product-card {
flex: 0 0 200px;
min-height: 230px;
}
/**
* @brief Responsive Image Dimensions
*/
.product-card img {
height: 130px;
padding: 10px;
}
/**
* @brief Responsive Content Padding
*/
.product-card__content {
padding: 0.75rem 0.9rem 0.85rem;
gap: 0.25rem;
}
/**
* @brief Responsive Title Size
*/
.product-card__content h3 {
font-size: 0.82rem;
}
/**
* @brief Responsive Description Size
* @details Further limits description lines to 2 on tablets.
*/
.product-card__content p {
font-size: 0.72rem;
-webkit-line-clamp: 2;
}
/**
* @brief Responsive Price Size
*/
.product-card__content .price {
font-size: 0.92rem;
}
}
/**
* @brief Mobile Breakpoint (max-width: 480px)
* @details Optimizes layouts for very small screens, converting grids and making cards even smaller.
*/
@media (max-width: 480px) {
/**
* @brief Mobile Strict 2-Column Grid
*/
.product-grid {
grid-template-columns: repeat(2, 1fr);
gap: 0.6rem;
padding: 0.75rem;
}
/**
* @brief Mobile Horizontal Scroll Item
*/
.product-scroll .product-card {
flex: 0 0 170px;
min-height: 210px;
}
/**
* @brief Mobile Card Image
*/
.product-card img {
height: 110px;
padding: 8px;
}
/**
* @brief Mobile Content Padding
*/
.product-card__content {
padding: 0.6rem 0.7rem 0.7rem;
}
/**
* @brief Mobile Title
* @details Limits the title to a single line.
*/
.product-card__content h3 {
font-size: 0.78rem;
-webkit-line-clamp: 1;
}
/**
* @brief Mobile Description
* @details Completely hides the description text on small mobile screens to save space.
*/
.product-card__content p {
display: none;
}
}
/* ==========================================================
WISHLIST Vertical List Layout
========================================================== */
/**
* ==========================================================
* @section WISHLIST Vertical List Layout
* ==========================================================
*/
/**
* @brief Wishlist Layout Container
* @details Arranges wishlist items in a vertical, single-column flex column.
*/
.wishlist-grid {
display: flex;
flex-direction: column;
@ -334,6 +493,11 @@
margin: 0 auto;
}
/**
* @brief Wishlist Item Card
* @details Modifies the default product card to a horizontal row layout specifically for the wishlist.
* Ensures an exact height and equal stretching.
*/
.wishlist-card.product-card {
display: flex;
flex-direction: row;
@ -351,6 +515,11 @@
overflow: hidden;
}
/**
* @brief Wishlist Image Wrapper
* @details Styles the image specifically for the horizontal wishlist layout. Adds a border right
* instead of border bottom, ensuring it takes up full height with contain object-fit.
*/
.wishlist-card.product-card img {
width: 160px;
flex: 0 0 160px;
@ -376,6 +545,10 @@
transition: transform var(--transition-smooth);
}
/**
* @brief Wishlist Content Wrapper
* @details Vertically centers text in the available remaining width alongside the image.
*/
.wishlist-card.product-card .product-card__content {
display: flex;
flex-direction: column;
@ -384,15 +557,27 @@
flex: 1;
}
/**
* @brief Wishlist Hover Animation
* @details Moves the horizontal card slightly to the right entirely with a small scale increment.
*/
.wishlist-card.product-card:hover {
transform: translateX(6px) scale(1.01);
}
/**
* @brief Wishlist Image Hover Zoom
* @details Zooms into the product image when hovering over a wishlist card.
*/
/* Bild-Zoom beim Hovern */
.wishlist-card.product-card:hover img {
transform: scale(1.08);
}
/**
* @brief Staggered entrance animation delays for Wishlist
* @details Sequential entry animations for wishlist items to replicate the staggered scroll effect.
*/
/* Stagger animation for wishlist items */
.wishlist-grid .wishlist-card:nth-child(1) { animation-delay: 0.05s; }
.wishlist-grid .wishlist-card:nth-child(2) { animation-delay: 0.1s; }
@ -403,6 +588,14 @@
.wishlist-grid .wishlist-card:nth-child(7) { animation-delay: 0.35s; }
.wishlist-grid .wishlist-card:nth-child(8) { animation-delay: 0.4s; }
/**
* @section RESPONSIVE DESIGN FOR WISHLIST
*/
/**
* @brief Tablet Breakpoint for Wishlist (max-width: 768px)
* @details Shrinks the total height, image size, and adjusts padding/font-size accordingly.
*/
@media (max-width: 768px) {
.wishlist-grid {
padding: 0.5rem 1rem 1.5rem;
@ -433,6 +626,10 @@
}
}
/**
* @brief Mobile Breakpoint for Wishlist (max-width: 480px)
* @details Minimizes the card height and hides descriptions to maximize screen real estate on mobile devices.
*/
@media (max-width: 480px) {
.wishlist-card.product-card {
height: 90px;

View File

@ -1,8 +1,20 @@
/**
* @file login.css
* @brief Stylesheet für die Authentifizierungsseiten (Login, Registrierung, Account).
* @details Dieses Stylesheet definiert das Layout, die Farben, Animationen und das responsive Verhalten
* für alle authentifizierungsbezogenen Ansichten. Es setzt auf ein "Animated Glassmorphism Design"
* und Grid-Layouts für strukturierte und ansprechende Benutzeroberflächen.
*/
/* ==========================================================
AUTH PAGES Login, Register, Account
Animated Glassmorphism Design
========================================================== */
/**
* @section AuthWrapper Auth-Wrapper
* @brief Das Hauptelement, das die Authentifizierungsformulare umschließt.
*/
/* ─── Auth Wrapper ─── */
.auth {
display: grid;
@ -13,6 +25,10 @@
z-index: 1;
}
/**
* @brief Dekorative animierte Kugel im Hintergrund.
* @details Nutzt einen radialen Farbverlauf und eine unendliche Float-Animation, um Tiefe zu erzeugen.
*/
/* Decorative ambient orb */
.auth::before {
content: "";
@ -29,6 +45,10 @@
animation: float 12s ease-in-out infinite;
}
/**
* @brief Standard-Raster für das Auth-Formular.
* @details Zentriert die Elemente horizontal und passt sich an die Bildschirmbreite bis zu 480px an.
*/
.auth__grid {
display: grid;
grid-template-columns: 1fr;
@ -39,6 +59,10 @@
align-items: center;
}
/**
* @section SideLayout Side Layout (Account page)
* @brief Layout für breitere Ansichten (z. B. auf der Account-Seite).
*/
/* ─── Side Layout (Account page) ─── */
.auth__grid.auth__card__side {
grid-template-columns: max-content 1fr;
@ -51,6 +75,9 @@
width: 100%;
}
/**
* @brief Layout für die seitliche Profilbild-Anzeige.
*/
.auth__grid.auth__card__side .auth__card.auth__card__side__picture {
display: inline-grid;
width: max-content;
@ -68,6 +95,9 @@
margin-bottom: 0;
}
/**
* @brief Styling und Hover-Effekte für das Profilbild.
*/
.auth__grid.auth__card__side .auth__card.auth__card__side__picture img {
display: block;
max-width: none;
@ -86,6 +116,11 @@
justify-items: center;
}
/**
* @section CardGlassmorphism Card Glassmorphism
* @brief Stil für die Container-Karten der Formulare.
* @details Nutzt Dunkelmodus-Farben mit feinen Box-Shadows und Hover-Effekten zur Hervorhebung.
*/
/* ─── Card Glassmorphism ─── */
.auth__card,
.auth__sideCard {
@ -103,6 +138,10 @@
box-shadow: var(--shadow-md), var(--shadow-glow-blue);
}
/**
* @brief Gestaffelte Animation für die Formular-Karten.
* @details Sorgt dafür, dass Elemente nacheinander eingeflogen werden.
*/
/* Stagger cards */
.auth__grid .auth__card:nth-child(1) { animation-delay: 0s; }
.auth__grid .auth__card:nth-child(2) { animation-delay: 0.08s; }
@ -113,6 +152,10 @@
padding: 22px;
}
/**
* @section CardHeader Card Header
* @brief Kopfzeile der Formular-Karten (Logo und Titel).
*/
/* ─── Card Header ─── */
.auth__header {
display: grid;
@ -130,6 +173,9 @@
grid-row: span 2;
}
/**
* @brief Styling des Haupttitels mit Text-Gradient-Effekt.
*/
.auth__title {
margin: 0;
font-size: 1.45rem;
@ -142,6 +188,9 @@
background-clip: text;
}
/**
* @brief Untertitel im Header (z.B. Eingabeaufforderungen).
*/
.auth__subtitle {
margin: 0;
color: var(--text-secondary);
@ -149,6 +198,10 @@
line-height: 1.4;
}
/**
* @section Alerts Animated Alerts
* @brief Verschiedene Benachrichtigungsboxen (Erfolg, Fehler, Warnungen).
*/
/* ─── Alerts Animated ─── */
.auth__alert__error {
margin: 12px 0 14px;
@ -168,6 +221,9 @@
box-shadow: 0 8px 18px rgba(72, 142, 62, 0.18);
}
/**
* @brief Standard-Benachrichtigungs-Container mit Gradient-Hintergrund.
*/
.auth__alert {
margin: 0.75rem 0 1rem;
color: var(--text-invert);
@ -187,6 +243,9 @@
margin: 0.25rem 0;
}
/**
* @brief Spezifisches Styling für Fehlermeldungen innerhalb von Formularen.
*/
.auth__error {
margin: 0.75rem 0;
color: var(--color-danger);
@ -198,11 +257,18 @@
animation: scaleIn 0.3s ease;
}
/**
* @section Formular Formular-Elemente
* @brief Standard-Layout für die inneren Formularelemente.
*/
/* ─── Form ─── */
.auth__form {
margin-top: 0.5rem;
}
/**
* @brief Abstände für einzelne Eingabefelder.
*/
.auth__field {
margin-top: 1rem;
}
@ -211,6 +277,9 @@
margin-top: 1px;
}
/**
* @brief Styling der Labels über den Eingabefeldern.
*/
.auth__field label {
display: block;
margin: 0 0 0.4rem;
@ -224,6 +293,9 @@
color: var(--color-primary);
}
/**
* @brief Grundlegendes Styling für Text-, Passwort- und Datei-Eingabefelder.
*/
.auth__field input[type="text"],
.auth__field input[type="password"],
.auth__field input[type="email"],
@ -241,6 +313,9 @@
transition: all var(--transition-smooth);
}
/**
* @brief Fokus-Effekte (Ränder und Glow) für Eingabefelder.
*/
.auth__field input[type="text"]:focus,
.auth__field input[type="password"]:focus,
.auth__field input[type="email"]:focus,
@ -255,6 +330,9 @@
color: var(--text-muted);
}
/**
* @brief Spezifisches Styling für den Datei-Auswahl-Button.
*/
.auth__field input[type="file"]::file-selector-button {
margin-right: 10px;
padding: 10px 12px;
@ -276,11 +354,18 @@
transform: translateY(0px);
}
/**
* @section Actions Buttons & Aktionen
* @brief Hauptbuttons für die Formularübermittlung.
*/
/* ─── Actions ─── */
.auth__actions {
margin-top: 14px;
}
/**
* @brief Primärer Submit-Button mit Hover- und Active-Zuständen.
*/
.auth__submit {
width: 100%;
padding: 12px 14px;
@ -303,6 +388,9 @@
transform: translateY(0px);
}
/**
* @brief Deaktivierter Zustand für Submit-Buttons.
*/
.auth__submit:disabled {
opacity: 0.5;
cursor: not-allowed;
@ -314,11 +402,18 @@
display: none;
}
/**
* @brief Rote Variante des Submit-Buttons für gefährliche Aktionen (Löschen).
*/
/* Danger button variant */
.auth__submit--danger {
background: #d92d20;
}
/**
* @section Links Verlinkungen
* @brief Zusätzliche Textlinks unter den Formularen (z.B. Passwort vergessen, Registrieren).
*/
/* ─── Links ─── */
.auth__links {
margin-top: 14px;
@ -341,6 +436,10 @@
text-decoration: underline;
}
/**
* @section SideCard Seiten-Box
* @brief Gestaltung der Informationsbox auf der Seite von Registrierungsformularen etc.
*/
/* ─── Side Card ─── */
.auth__sideCard {
padding: 18px;
@ -364,6 +463,10 @@
color: #ffffff;
}
/**
* @section TipBox Tipp-Box
* @brief Container für hilfreiche Hinweise (oft neben Formularen platziert).
*/
/* ─── Tip Box ─── */
.auth__tip {
margin-top: 14px;
@ -374,10 +477,17 @@
color: #ffffff;
}
/**
* @section AccountPage ACCOUNT PAGE
* @brief Layout und spezifische Stile für die Account-Ansicht (Profil & Einstellungen).
*/
/* ==========================================================
ACCOUNT PAGE Profile + Settings Layout
========================================================== */
/**
* @brief Modulares Grid-Layout für die Profilübersicht.
*/
/* ─── Main Grid ─── */
.account {
display: grid;
@ -393,6 +503,10 @@
grid-column: 1 / -1;
}
/**
* @section AccountProfile Profil-Sidebar
* @brief Der seitliche Bereich in der Account-Seite, der Profilbild und Name anzeigt.
*/
/* ─── Profile Sidebar ─── */
.account__profile {
position: sticky;
@ -401,6 +515,9 @@
padding: 2rem 1.5rem !important;
}
/**
* @brief Äußerer Container für das Profilbild auf der Profilseite mit Hover-Effekten.
*/
.account__avatar-wrapper {
width: 140px;
height: 140px;
@ -425,6 +542,9 @@
display: block;
}
/**
* @brief Der angezeigte Name auf dem Benutzerprofil.
*/
.account__displayname {
margin: 0 0 1.25rem;
font-size: 1.35rem;
@ -433,6 +553,10 @@
color: #ffffff;
}
/**
* @section AccountDetails Benutzerinformationen
* @brief Zeilenbasiertes Layout für die Auflistung von Profil-Details (wie E-Mail, Beitrittsdatum).
*/
/* ─── Detail Rows (User info) ─── */
.account__details {
margin: 0;
@ -466,6 +590,10 @@
font-weight: 500;
}
/**
* @section AccountSettings Account-Einstellungen
* @brief Der Hautpbereich für sämtliche Account-bezogene Aktionen und Einstellungen.
*/
/* ─── Settings Column ─── */
.account__settings {
display: flex;
@ -473,11 +601,17 @@
gap: 1.25rem;
}
/**
* @brief Karten-Container in den Account-Einstellungen.
*/
/* ─── Section Cards ─── */
.account__section {
padding: 1.5rem !important;
}
/**
* @brief Bereichstitel mit optionalem Icon (Nutzt Flexlayout).
*/
.account__section-title {
display: flex;
align-items: center;
@ -497,6 +631,10 @@
color: var(--color-danger);
}
/**
* @section QuickActions Quick Actions
* @brief Verknüpfungen zu bestimmten Aktionen oder externen Seiten im Profil.
*/
/* ─── Quick Actions ─── */
.account__quick-actions {
display: flex;
@ -520,6 +658,10 @@
opacity: 0.9;
}
/**
* @section DangerSection Danger Zone
* @brief Rote Warnbereiche für unwiderrufliche Aktionen (z.B. Profil löschen).
*/
/* ─── Danger Section ─── */
.account__section--danger {
border-color: rgba(217, 45, 32, 0.15);
@ -537,6 +679,10 @@
line-height: 1.5;
}
/**
* @section Responsive Responsive Design
* @brief Media-Queries für mobile Endgeräte und Tablet-Optimierungen.
*/
/* ==========================================================
RESPONSIVE
========================================================== */
@ -597,6 +743,9 @@
}
}
/**
* @brief Optimierungen für Smartphones (max-width: 520px).
*/
@media (max-width: 520px) {
.auth {
padding: 1rem 0.75rem 2rem;
@ -650,6 +799,9 @@
}
}
/**
* @brief Optimierungen für sehr schmale Bildschirme (max-width: 380px).
*/
@media (max-width: 380px) {
.auth {
padding: 0.75rem 0.5rem 1.5rem;

View File

@ -1,8 +1,26 @@
/**
* @file productAdder.css
* @brief Stylesheet für die Produkt-Hinzufügen-Seite (Product Adder).
* @details Enthält spezielle Stile für Dropdowns (Select-Elemente) und Formulare,
* die für die Dateneingabe beim Hinzufügen von Produkten benötigt werden.
* Das grundlegende Layout basiert auf den gleichen Prinzipien wie die Auth-Bereiche,
* wurde jedoch für breitere Formulare (bis zu 1100px) angepasst.
*/
/* ==========================================================
PRODUCT ADDER Dropdown & Form Styles
========================================================== */
/**
* @section SelectDropdown Dropdown-Auswahl
* @brief Stile für den Container und die Elemente eines Dropdown-Menüs.
*/
/* ─── Select Wrapper ─── */
/**
* @brief Umhüllender Container für das Select-Element.
* @details Zentriert das Element, beschränkt die Maximalbreite auf 520px und
* verwendet ein CSS-Grid für saubere Abstände (gap) zwischen Label und Feld.
*/
.auth__select__wrap {
width: min(520px, 100%);
display: grid;
@ -11,11 +29,20 @@
margin-top: 12px;
}
/**
* @brief Label-Text für die Dropdown-Auswahl.
*/
.auth__select__label {
font-size: 0.95rem;
color: #ffffff;
}
/**
* @brief Hauptstil für das Dropdown-/Select-Feld.
* @details Beinhaltet Customizing des nativen Aussehens (appearance: none) und
* fügt ein eigenes Pfeil-Icon via Hintergrund-Gradient (background-image) hinzu.
* Das Feld besitzt Übergangseffekte für den Hover- und Focus-State.
*/
.auth__select {
width: 100%;
box-sizing: border-box;
@ -39,17 +66,31 @@
background-repeat: no-repeat;
}
/**
* @brief Fokus-Status für das Dropdown-Feld.
* @details Erhält den gleichen leuchtenden Rand (Glow) wie die Standard-Eingabefelder.
*/
.auth__select:focus {
border-color: rgba(37, 99, 235, 0.75);
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.28);
}
/**
* @brief Stil der Auswahloptionen (options) innerhalb des Selects.
*/
.auth__select option {
background: #1e2537;
color: #ffffff;
}
/**
* @section ProductAdderLayout Seiten-Layout
* @brief Definition der Haupt-Grid-Strukturen auf der Product-Adder-Seite.
*/
/* ─── Product Adder Layout ─── */
/**
* @brief Äußerer Container für das Formular, der die volle Viewport-Höhe einnimmt.
*/
.auth {
min-height: 100vh;
display: grid;
@ -58,6 +99,11 @@
gap: 16px;
}
/**
* @brief Das Raster für die Formular-Zellen/Karten.
* @details Erlaubt eine maximale Breite von 1100px. Bietet genügend Platz für
* umfangreiche Produkt-Eigenschaften und Beschreibungen.
*/
.auth__grid {
width: min(1100px, 100%);
display: grid;
@ -65,6 +111,10 @@
gap: 20px;
}
/**
* @brief Die Kachel/Karte für einzelne Bereiche des Formulars.
* @details Hebt Formularabschnitte visuell mit Hintergrundfarbe, Rand und Schatteneffekt ab.
*/
.auth__card {
background: #1f2937;
border: 1px solid #5e6075;
@ -73,27 +123,47 @@
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
}
/**
* @brief Kopfbereich innerhalb einer Kachel (auth__card).
*/
.auth__header {
margin-bottom: 12px;
}
/**
* @brief Titeltext der Kachel.
*/
.auth__title {
margin: 0;
font-size: 1.1rem;
color: #ffffff;
}
/**
* @section FormularElemente Formulare und Eingabefelder
* @brief Spezifische Layout-Vorgaben für die Formular-Bestandteile.
*/
/**
* @brief Ein einzelner Formular-Eintrag (Label + Input).
*/
.auth__form {
display: grid;
gap: 8px;
margin-bottom: 12px;
}
/**
* @brief Labels, die sich im Product-Adder-Formular befinden.
*/
.auth__form label {
font-size: 0.95rem;
color: #ffffff;
}
/**
* @brief Standard-Text-Eingabefeld.
* @details Verfügt über anpassbare Abstände, Randfarben und einen sanften Übergang bei Interaktionen.
*/
.auth__input {
width: 100%;
box-sizing: border-box;
@ -106,26 +176,47 @@
transition: border-color 140ms ease, box-shadow 140ms ease, background 140ms ease;
}
/**
* @brief Zustand bei aktivem Feld (Fokus).
*/
.auth__input:focus {
border-color: rgba(37, 99, 235, 0.75);
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.28);
}
/**
* @brief Aussehen des Platzhalter-Textes.
*/
.auth__input::placeholder {
color: #cbd5e1;
}
/**
* @brief Mehrzeilige Textarea für die Beschreibung.
* @details Mindesthöhe definiert, nur vertikal anpassbar, verbesserte Zeilenhöhe für besseren Lesefluss.
*/
textarea.auth__input {
min-height: 80px;
resize: vertical;
line-height: 1.5;
}
/**
* @brief Verhindert unnötigen Abstand beim letzten Formular-Element innerhalb der Karte.
*/
.auth__card .auth__form:last-child {
margin-bottom: 0;
}
/**
* @section Responsive Responsive Design
* @brief Anpassungen für Tabets und mobile Geräte.
*/
/* ─── Responsive ─── */
/**
* @brief Ansichten bis zu 720px Breite (Tablets/kleine Bildschirme).
* @details Reduziert die Paddings, um mehr Platz für den Inhalt zu schaffen.
*/
@media (max-width: 720px) {
.auth {
padding: 24px 12px;
@ -136,6 +227,11 @@ textarea.auth__input {
}
}
/**
* @brief Ansichten bis zu 480px Breite (Smartphones).
* @details Stark reduzierte Abstände (Padding, Gaps) und angepasste Schriftgrößen
* zur Sicherstellung der Lesbarkeit auf mobilen Displays.
*/
@media (max-width: 480px) {
.auth {
padding: 1rem 0.5rem;

View File

@ -1,8 +1,24 @@
/* ==========================================================
PRODUCT PAGE Animated Detail View & Shop Offers
========================================================== */
/**
* @file productpage.css
* @brief Stylesheet für die Produktdetailseite
* @details Diese Datei enthält alle CSS-Regeln für die Darstellung der Produktdetails, der Shop-Angebote sowie der Bewertungsbereiche.
* Es werden moderne CSS-Features wie Flexbox, Grid, CSS-Variablen und Keyframe-Animationen verwendet.
* @version 1.0
*/
/* ─── Wrapper ─── */
/**
* ==========================================================
* @section PRODUCT PAGE Animated Detail View & Shop Offers
* @brief Hauptbereich der Produktseite, der das Bild und die Details umschließt.
* ==========================================================
*/
/**
* @class .product-wrapper
* @brief Haupt-Container für die Produktpräsentation.
* @details Setzt eine maximale Breite, zentriert den Inhalt und ordnet die linke und rechte Spalte nebeneinander an.
* Nutzt eine Fade-In-Animation beim Laden.
*/
.product-wrapper {
max-width: 1200px;
margin: 3rem auto;
@ -12,12 +28,25 @@
animation: fadeInUp 0.6s ease both;
}
/* ─── Left Column (Image) ─── */
/**
* Left Column (Image)
*/
/**
* @class .product-left
* @brief Linke Spalte, die das Produktbild enthält.
* @details Nimmt 1 Teil des Flex-Containers ein und wird mit einer leichten Verzögerung eingeblendet.
*/
.product-left {
flex: 1;
animation: fadeInUp 0.5s ease 0.1s both;
}
/**
* @class .product-image-box
* @brief Box-Container für das eigentliche Produktbild.
* @details Beinhaltet Hintergrund, Schatten, Abrundungen und positioniert pseudo-Elemente für Hover-Effekte.
*/
.product-image-box {
background: #ffffff;
padding: 40px;
@ -29,7 +58,11 @@
overflow: hidden;
}
/* Animated gradient border on hover */
/**
* @pseudo .product-image-box::before
* @brief Animierter Gradient-Rahmen beim Hover.
* @details Nutzt Maskierungen (-webkit-mask), um einen leuchtenden Rahmen über den bestehenden Border zu zeichnen, der weich eingeblendet wird.
*/
.product-image-box::before {
content: "";
position: absolute;
@ -44,15 +77,29 @@
transition: opacity var(--transition-smooth);
}
/**
* @state .product-image-box:hover
* @brief Hover-Zustand für die Bild-Box.
* @details Verstärkt den Schatten und hebt das Element leicht an (TranslateY).
*/
.product-image-box:hover {
box-shadow: var(--shadow-lg), var(--shadow-glow-blue);
transform: translateY(-4px);
}
/**
* @state .product-image-box:hover::before
* @brief Macht den Gradient-Rahmen beim Hover sichtbar.
*/
.product-image-box:hover::before {
opacity: 1;
}
/**
* @element .product-image-box img
* @brief Das Produktbild selbst.
* @details Skaliert sich weich beim Hovern.
*/
.product-image-box img {
max-width: 100%;
height: auto;
@ -60,16 +107,33 @@
transition: transform var(--transition-smooth);
}
/**
* @state .product-image-box:hover img
* @brief Vergrößert das Bild beim Hover leicht (Scale).
*/
.product-image-box:hover img {
transform: scale(1.03);
}
/* ─── Right Column (Details) ─── */
/**
* Right Column (Details)
*/
/**
* @class .product-right
* @brief Rechte Spalte für die Produktdetails (Titel, Beschreibung, Spezifikationen).
* @details Nimmt 1.2 Teile des Flex-Platzes ein, etwas mehr als das Bild. Besitzt ebenfalls eine Fade-In Animation.
*/
.product-right {
flex: 1.2;
animation: fadeInUp 0.5s ease 0.2s both;
}
/**
* @class .product-title
* @brief Titel des Produkts.
* @details Große weiße Schrift mit einer Trennlinie nach unten.
*/
.product-title {
font-size: 32px;
color: white;
@ -79,6 +143,10 @@
padding-bottom: 15px;
}
/**
* @class .product-desc
* @brief Kurze Beschreibungstexte zum Produkt.
*/
.product-desc {
font-size: 23px;
line-height: 1.7;
@ -86,6 +154,11 @@
margin-bottom: 15px;
}
/**
* @class .product-specs
* @brief Container für die Haupt-Spezifikationen.
* @details Richtet Elemente vertikal aus (Flex-Column) mit entsprechendem Abstand (Gap).
*/
.product-specs {
display: flex;
color: #ffffff;
@ -93,6 +166,11 @@
gap: 12px;
}
/**
* @element .product-specs p
* @brief Einzelner Spec-Absatz.
* @details Halbtransparenter Hintergrund mit Blur-Effekt, um Tiefe zu simulieren.
*/
.product-specs p {
padding: 0.7rem 1rem;
background: rgba(27, 34, 48, 0.65);
@ -103,19 +181,36 @@
transition: all var(--transition-normal);
}
/**
* @state .product-specs p:hover
* @brief Hover-Effekt für Specs.
* @details Der Hintergrund wird deckender und der Text rückt leicht nach rechts ein.
*/
.product-specs p:hover {
background: rgba(27, 34, 48, 0.9);
border-color: var(--border-default);
transform: translateX(4px);
}
/**
* @element .product-specs p strong
* @brief Hervorhebung des Spec-Labels innerhalb eines Absatzes.
*/
.product-specs p strong {
color: var(--text-muted);
font-weight: 500;
margin-right: 0.5rem;
}
/* ─── Spec Rows ─── */
/**
* Spec Rows
*/
/**
* @class .spec-row
* @brief Zeilenbasierte Darstellung einer einzelnen Spezifikation.
* @details Nutzt Flexbox zur Verteilung von Label und Wert an die äußeren Ränder.
*/
.spec-row {
display: flex;
justify-content: space-between;
@ -126,22 +221,38 @@
transition: all var(--transition-normal);
}
/**
* @state .spec-row:hover
* @brief Hover-Verhalten einer Spec-Row.
* @details Ähnlich wie bei .product-specs p, leichtes Verschieben nach rechts.
*/
.spec-row:hover {
background: rgba(27, 34, 48, 0.9);
transform: translateX(4px);
}
/**
* @class .spec-name
* @brief Der Name bzw. die Bezeichnung der Spezifikation in der Zeile.
*/
.spec-name {
font-weight: 500;
color: var(--text-secondary);
}
/**
* @class .spec-value
* @brief Der tatsächliche Wert der Spezifikation, fett hervorgehoben.
*/
.spec-value {
font-weight: 600;
color: var(--text-primary);
}
/* ─── Responsive Product Page ─── */
/**
* Responsive Product Page
* @details Media-Queries für Tablets und Smartphones im Produkt-Bereich.
*/
@media (max-width: 900px) {
.product-wrapper {
flex-direction: column;
@ -207,9 +318,18 @@
}
}
/* ==========================================================
SHOP OFFERS Animated List
========================================================== */
/**
* ==========================================================
* @section SHOP OFFERS Animated List
* @brief Bereich für die Anzeige verfügbarer Angebote verschiedener Shops.
* ==========================================================
*/
/**
* @class .shop-offers
* @brief Container, der alle Shop-Angebote umschließt.
* @details Vertikales Flexbox-Layout mit Animation.
*/
.shop-offers {
max-width: 1200px;
margin: 0 auto 4rem;
@ -220,6 +340,11 @@
animation: fadeInUp 0.6s ease 0.3s both;
}
/**
* @class .shop-line
* @brief Eine einzelne Zeile für ein Shop-Angebot.
* @details Nutzt ein CSS Grid mit 3 Spalten (Bild, Details, Preis/Button).
*/
.shop-line {
display: grid;
grid-template-columns: 250px 1fr auto;
@ -235,7 +360,10 @@
overflow: hidden;
}
/* Gradient accent line on left */
/**
* @pseudo .shop-line::before
* @brief Dekorationslinie (Gradient), die links auftaucht, wenn man über das Angebot fährt.
*/
.shop-line::before {
content: "";
position: absolute;
@ -249,29 +377,49 @@
transition: opacity var(--transition-smooth);
}
/**
* @state .shop-line:hover
* @brief Interaktion mit einem Shop-Angebot.
* @details Ändert die Hintergrundfarbe, erzeugt Schatten und animiert nach oben.
*/
.shop-line:hover {
background: #243248;
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(0,0,0,0.25);
}
/**
* @state .shop-line:hover::before
* @brief Blendet die linke Dekorationslinie beim Hovern ein.
*/
.shop-line:hover::before {
opacity: 1;
}
/* Stagger shop lines */
/**
* @brief Staggered Animations
* @details Verzögert das Einblenden der Listenelemente sukzessive für einen Wasserfall-Effekt.
*/
.shop-line:nth-child(1) { animation-delay: 0.35s; }
.shop-line:nth-child(2) { animation-delay: 0.42s; }
.shop-line:nth-child(3) { animation-delay: 0.49s; }
.shop-line:nth-child(4) { animation-delay: 0.56s; }
.shop-line:nth-child(5) { animation-delay: 0.63s; }
/**
* @class .shop-left
* @brief Linker Abschnitt in einem Shop-Angebot (Logo).
*/
.shop-left {
display: flex;
align-items: center;
gap: 1rem;
}
/**
* @class .shop-logo
* @brief Container für das Shop-Logo.
*/
.shop-logo {
display: flex;
align-items: center;
@ -279,6 +427,10 @@
flex-shrink: 0;
}
/**
* @element .shop-logo img
* @brief Das Bild des Shop-Logos.
*/
.shop-logo img {
max-height: 36px;
max-width: 90px;
@ -287,25 +439,45 @@
transition: transform var(--transition-normal);
}
/**
* @state .shop-line:hover .shop-logo img
* @brief Skaliert das Logo beim Hovern über die Zeile.
*/
.shop-line:hover .shop-logo img {
transform: scale(1.08);
}
/**
* @class .shop-name
* @brief Textanzeige des Shop-Namens.
*/
.shop-name {
color: white;
font-weight: 600;
font-size: 16px;
}
/**
* @element .shop-name a
* @brief Link auf den Shop, falls der Name geklickt wird.
*/
.shop-name a {
color: white;
text-decoration: none;
}
/**
* @state .shop-name a:hover
* @brief Unterstreichung beim Hover über den Shop-Namen.
*/
.shop-name a:hover {
text-decoration: underline;
}
/**
* @class .shop-middle
* @brief Mittlerer Abschnitt mit Zusatzinfos (Versand, Bestand).
*/
.shop-middle {
display: flex;
align-items: center;
@ -314,6 +486,10 @@
color: #cbd5e1;
}
/**
* @class .shop-shipping
* @brief Bereich für Versandinformationen.
*/
.shop-shipping {
display: flex;
flex-direction: column;
@ -321,6 +497,10 @@
line-height: 1.4;
}
/**
* @class .shop-stock
* @brief Lagerbestands-Anzeige.
*/
.shop-stock {
font-weight: 500;
display: flex;
@ -328,16 +508,28 @@
gap: 6px;
}
/**
* @state .shop-stock.in-stock::before
* @brief Prefix-Icon für vorrätige Artikel (grünes Häkchen).
*/
.shop-stock.in-stock::before {
content: "✔";
color: #22c55e;
}
/**
* @state .shop-stock.out-stock::before
* @brief Prefix-Icon für nicht vorrätige Artikel (rotes X).
*/
.shop-stock.out-stock::before {
content: "✖";
color: #ef4444;
}
/**
* @class .shop-price
* @brief Preis-Hervorhebung ganz rechts in der Shop-Zeile.
*/
.shop-price {
margin-left: auto;
font-size: 18px;
@ -345,10 +537,18 @@
color: #4ade80;
}
/**
* @state .shop-line:hover .shop-price
* @brief Skaliert den Preis leicht bei Interaktion.
*/
.shop-line:hover .shop-price {
transform: scale(1.05);
}
/**
* @class .no-shop
* @brief Fallback-Ansicht, wenn kein Angebot vorhanden ist.
*/
.no-shop {
background: #1f2a3a;
padding: 20px;
@ -357,7 +557,10 @@
text-align: center;
}
/* ─── Responsive Shop Offers ─── */
/**
* Responsive Shop Offers
* @details Umbrüche für Shop-Zeilen bei kleineren Screens.
*/
@media (max-width: 900px) {
.shop-line {
grid-template-columns: 1fr;
@ -431,10 +634,17 @@
}
/* ==========================================================
PRODUCT REVIEWS
========================================================== */
/**
* ==========================================================
* @section PRODUCT REVIEWS
* @brief Anzeige und Strukturierung der Produktbewertungen.
* ==========================================================
*/
/**
* @class .reviews
* @brief Wrapper für alle Bewertungen, inklusive Überschrift.
*/
.reviews {
max-width: 1200px;
margin: 3rem auto 5rem;
@ -445,6 +655,10 @@
animation: fadeInUp 0.6s ease 0.4s both;
}
/**
* @class .reviews-title
* @brief Titel der Bewertungs-Sektion.
*/
.reviews-title {
color: white;
font-size: 22px;
@ -452,7 +666,11 @@
margin-bottom: 0.5rem;
}
/* Review Card */
/**
* @class .review-card
* @brief Einzelne Karte für eine Bewertung von einem User.
* @details Beinhaltet Hintergrund, Ränder und relative Positionierung für den Border-Effekt.
*/
.review-card {
background: #1c2533; /* leicht dunkler als shop */
border: 1px solid #2a374a;
@ -463,7 +681,10 @@
overflow: hidden;
}
/* leichter Akzent rechts statt links */
/**
* @pseudo .review-card::after
* @brief Gradient-Rahmenkante auf der rechten Seite, die beim Hover erscheint.
*/
.review-card::after {
content: "";
position: absolute;
@ -476,17 +697,28 @@
transition: opacity var(--transition-smooth);
}
/**
* @state .review-card:hover
* @brief Hover-State der Bewertung, hellt Hintergrund leicht auf.
*/
.review-card:hover {
background: #223047;
transform: translateY(-3px);
box-shadow: 0 8px 18px rgba(0,0,0,0.25);
}
/**
* @state .review-card:hover::after
* @brief Blendet den farbigen Rand rechts ein.
*/
.review-card:hover::after {
opacity: 1;
}
/* Header */
/**
* @class .review-header
* @brief Kopfzeile einer Bewertung (User links, Sterne/Datum z.B. rechts).
*/
.review-header {
display: flex;
justify-content: space-between;
@ -494,7 +726,10 @@
margin-bottom: 0.6rem;
}
/* User-Info (Avatar + Name/Datum) */
/**
* @class .review-user-info
* @brief Container für Avatar, Name und ggf. Datum.
*/
.review-user-info {
display: flex;
align-items: center;
@ -502,7 +737,10 @@
min-width: 0; /* erlaubt Text-Ellipsis/Umbruch in flex */
}
/* Profilbild in Reviews: immer gleich groß und angenehm klein */
/**
* @class .review-avatar
* @brief Profilbild in kleinen Abmessungen innerhalb der Review.
*/
.review-avatar {
width: clamp(28px, 3.2vw, 34px);
height: clamp(28px, 3.2vw, 34px);
@ -515,7 +753,10 @@
background: #0f172a; /* falls Bild transparent/fehlend */
}
/* Optional: verhindert, dass lange Namen Layout sprengen */
/**
* @class .review-user
* @brief Benutzername des Reviewers, wird abgeschnitten (ellipsis), wenn zu lang.
*/
.review-user {
overflow: hidden;
text-overflow: ellipsis;
@ -528,34 +769,55 @@
font-size: 15px;
}
/* Sterne */
/**
* @class .review-rating
* @brief Container für die Sterne-Bewertung.
*/
.review-rating {
display: flex;
gap: 4px;
}
/**
* @class .star
* @brief Ein einzelnes Stern-Symbol.
*/
.star {
font-size: 16px;
color: #475569;
transition: transform 0.2s ease;
}
/**
* @state .star.filled
* @brief Aktiver, goldener Stern.
*/
.star.filled {
color: #fbbf24;
}
/**
* @state .review-card:hover .star.filled
* @brief Skaliert goldene Sterne innerhalb einer Karte beim Hovern.
*/
.review-card:hover .star.filled {
transform: scale(1.15);
}
/* Kommentar */
/**
* @class .review-comment
* @brief Textinhalt einer Bewertung.
*/
.review-comment {
color: #cbd5e1;
font-size: 14px;
line-height: 1.6;
}
/* Keine Reviews */
/**
* @class .no-review
* @brief Fallback-Text, falls noch keine Bewertungen existieren.
*/
.no-review {
background: #1f2a3a;
border: 1px solid #2f3c52;
@ -565,10 +827,17 @@
}
/* ==========================================================
REVIEW OVERVIEW BOX (Linke Spalte) - Kompakte Version
========================================================== */
/**
* ==========================================================
* @section REVIEW OVERVIEW BOX (Linke Spalte) - Kompakte Version
* @brief Zusammenfassung aller Bewertungen (Durchschnittsnote und Sterne-Verteilung).
* ==========================================================
*/
/**
* @class .review-overview-box
* @brief Sidebar-Box zur Übersicht der Gesamtbewertung.
*/
.review-overview-box {
background: #1c2533;
border: 1px solid #2a374a;
@ -580,6 +849,10 @@
animation: fadeInUp 0.5s ease 0.3s both;
}
/**
* @class .overview-header
* @brief Headerbereich der Übersicht (Zentriert, Rand unten).
*/
.overview-header {
text-align: center;
/* Abstände verringert */
@ -588,6 +861,10 @@
border-bottom: 1px solid #2a374a;
}
/**
* @class .overview-avg
* @brief Die durchschnittliche Bewertung als große Zahl.
*/
.overview-avg {
/* Schriftgröße der Durchschnittsnote von 2.5rem auf 2rem verkleinert */
font-size: 2rem;
@ -599,11 +876,19 @@
gap: 0.4rem;
}
/**
* @element .overview-avg .star
* @brief Der Stern-Icon neben der Durchschnittsbewertung.
*/
.overview-avg .star {
/* Stern etwas verkleinert */
font-size: 1.5rem;
}
/**
* @class .overview-count
* @brief Anzahl der Gesamtbewertungen in kleinem Text.
*/
.overview-count {
/* Textgröße leicht verkleinert */
font-size: 0.8rem;
@ -611,6 +896,10 @@
margin-top: 0.2rem;
}
/**
* @class .overview-breakdown
* @brief Container für die Verteilung (Progress-Bars) der einzelnen Sterne (5,4,3,2,1).
*/
.overview-breakdown {
display: flex;
flex-direction: column;
@ -618,6 +907,10 @@
gap: 0.4rem;
}
/**
* @class .breakdown-row
* @brief Einzelne Zeile der Verteilung (Stellt z.B. 5 Sterne und deren Balken dar).
*/
.breakdown-row {
display: flex;
align-items: center;
@ -626,6 +919,10 @@
font-size: 0.8rem;
}
/**
* @class .breakdown-stars
* @brief Beschriftung am Start des Balkens ("5 Sterne").
*/
.breakdown-stars {
width: 50px;
white-space: nowrap;
@ -633,6 +930,10 @@
color: #cbd5e1;
}
/**
* @class .breakdown-bar-bg
* @brief Dunkler Hintergrund des Fortschrittsbalkens.
*/
.breakdown-bar-bg {
flex: 1;
/* Höhe der Balken von 8px auf 6px reduziert */
@ -642,6 +943,11 @@
overflow: hidden;
}
/**
* @class .breakdown-bar-fill
* @brief Goldene Füllung des Balkens, je nach prozentualer Verteilung.
* @details Nutzt CSS-Transitions für einen aufbauenden Lade-Effekt.
*/
.breakdown-bar-fill {
height: 100%;
background: #fbbf24;
@ -649,12 +955,20 @@
transition: width 0.8s ease-out;
}
/**
* @class .breakdown-num
* @brief Absolute Anzahl der Bewertungen für diese Stern-Reihe.
*/
.breakdown-num {
width: 20px;
text-align: right;
color: #94a3b8;
}
/**
* @class .overview-empty
* @brief Nachricht in der Übersicht, wenn keine Daten vorliegen.
*/
.overview-empty {
text-align: center;
color: #94a3b8;
@ -662,10 +976,17 @@
font-size: 0.85rem;
}
/* ==========================================================
REVIEW HINZUFÜGEN (Formular)
========================================================== */
/**
* ==========================================================
* @section REVIEW HINZUFÜGEN (Formular)
* @brief Bereich zum Schreiben einer eigenen Bewertung.
* ==========================================================
*/
/**
* @class .review-add
* @brief Wrapper für das Eingabeformular.
*/
.review-add {
max-width: 1200px;
margin: 0 auto 2rem;
@ -673,13 +994,22 @@
animation: fadeInUp 0.5s ease 0.4s both;
}
/**
* @class .review-input-form
* @brief Das eigentliche Formular-Layout (vertikal).
*/
.review-input-form {
display: flex;
flex-direction: column;
gap: 1.2rem;
}
/* --- Sterne-Auswahl (Radio-Button Trick) --- */
/**
* --- Sterne-Auswahl (Radio-Button Trick) ---
* @class .rating-input
* @brief Behälter für die Radio-Buttons zur Sternbewertung.
* @details Nutzt `flex-direction: row-reverse;`, um CSS-basiertes Highlighten vorheriger Sterne durch Geschwister-Selektoren zu ermöglichen.
*/
.rating-input {
display: flex;
/* Dreht die Reihenfolge um für den Hover-Effekt */
@ -688,12 +1018,18 @@
gap: 0.3rem;
}
/* Versteckt die eigentlichen runden Radio-Buttons */
/**
* @element .rating-input input[type="radio"]
* @brief Die eigentlichen HTML-Radio-Elemente werden versteckt.
*/
.rating-input input[type="radio"] {
display: none;
}
/* Stylt die Labels (die Sterne) */
/**
* @element .rating-input label
* @brief Das angezeigte Label, das visuell als auswählbarer Stern fungiert.
*/
.rating-input label {
font-size: 2.2rem;
color: #475569; /* Dunkelgrau für leere Sterne */
@ -701,19 +1037,30 @@
transition: color 0.2s ease, transform 0.2s ease;
}
/* Hover-Logik: Färbt den gehoverten/geklickten Stern und alle "folgenden" (durch row-reverse sind das die linken) */
/**
* @state .rating-input label:hover, ...
* @brief Hover- und Auswahl-Mechanik für die interaktiven Sterne.
* @details Wenn ein Stern gehovert oder angewählt (checked) wird, färben sich dieser und alle in der row-reverse-Reihenfolge nachfolgenden Sterne golden.
*/
.rating-input label:hover,
.rating-input label:hover ~ label,
.rating-input input[type="radio"]:checked ~ label {
color: #fbbf24; /* Das gleiche Gelb wie bei deinen bestehenden Sternen */
}
/* Kleiner Pop-Effekt beim drüberfahren */
/**
* @state .rating-input label:hover
* @brief Kleiner Skalierungseffekt beim drüberfahren.
*/
.rating-input label:hover {
transform: scale(1.15);
}
/* --- Textarea --- */
/**
* --- Textarea ---
* @class .review-comment-input
* @brief Das Mahrzeilen-Textfeld für den eigentlichen Kommentar.
*/
.review-comment-input {
width: 100%;
background: #1f2a3a;
@ -727,6 +1074,10 @@
transition: all var(--transition-smooth);
}
/**
* @state .review-comment-input:focus
* @brief Klick/Fokus-Status für das Textfeld, zeigt grünen Rahmen.
*/
.review-comment-input:focus {
outline: none;
border-color: #4ade80; /* Passt zu deinem grünen Button-Stil */
@ -734,13 +1085,20 @@
box-shadow: 0 0 0 3px rgba(74, 222, 128, 0.1);
}
/* --- Button Positionierung --- */
/**
* --- Button Positionierung ---
* @element .review-input-form .auth__submit
* @brief Stilisierung des Absende-Buttons innerhalb der Reviews.
*/
.review-input-form .auth__submit {
align-self: flex-start; /* Button bleibt linksbündig und wird nicht über die ganze Breite gestreckt */
padding: 0.8rem 2.5rem;
}
/* Container für alle fertigen Bewertungen */
/**
* @class .reviews-all
* @brief Container für die Auflistung aller existierenden Bewertungen (ohne das Eingabeformular).
*/
.reviews-all {
display: flex;
flex-direction: column;
@ -748,7 +1106,11 @@
margin-bottom: 3rem; /* Etwas Luft nach unten zum "Hinzufügen"-Formular */
}
/* Kommentar-Text */
/**
* @class .review-comment
* @brief (Wiederholung/Erweiterung) Anpassung des Kommentar-Texts für Wortumbrüche.
* @details Zwingt den Browser bei überlangen Wörtern rechtzeitig umzubrechen, damit das Layout nicht explodiert.
*/
.review-comment {
color: #cbd5e1;
font-size: 14px;

View File

@ -1,222 +0,0 @@
(function () {
'use strict';
function debounce(fn, delay) {
var t;
return function () {
var ctx = this;
var args = arguments;
clearTimeout(t);
t = setTimeout(function () { fn.apply(ctx, args); }, delay);
};
}
function createDropdownEl() {
var el = document.createElement('div');
el.className = 'searchDropdown';
el.hidden = true;
el.setAttribute('role', 'listbox');
return el;
}
function attachToInput(input) {
if (!input || input.dataset.autocompleteBound === '1') return;
input.dataset.autocompleteBound = '1';
// wrapper for positioning
var wrapper = input.closest('.nav__searchField');
if (!wrapper) return;
wrapper.classList.add('nav__searchField--hasDropdown');
// dropdown
var dropdown = createDropdownEl();
wrapper.appendChild(dropdown);
var activeIndex = -1;
var items = [];
var abortController = null;
function close() {
dropdown.hidden = true;
dropdown.innerHTML = '';
dropdown.classList.remove('is-open');
activeIndex = -1;
items = [];
}
function open() {
dropdown.hidden = false;
dropdown.classList.add('is-open');
}
function setStatus(text) {
dropdown.innerHTML = '';
var row = document.createElement('div');
row.className = 'searchDropdown__status';
row.textContent = text;
dropdown.appendChild(row);
open();
}
function render(list) {
dropdown.innerHTML = '';
items = list || [];
activeIndex = -1;
if (!items.length) {
setStatus('Keine Produkte gefunden.');
return;
}
for (var i = 0; i < items.length; i++) {
(function (idx) {
var it = items[idx];
var a = document.createElement('a');
a.className = 'searchDropdown__item';
a.href = it.url;
a.setAttribute('role', 'option');
a.tabIndex = -1;
var img = document.createElement('img');
img.className = 'searchDropdown__img';
img.alt = '';
img.loading = 'lazy';
img.decoding = 'async';
img.src = it.imagePath && it.imagePath.length ? it.imagePath : 'assets/images/placeholder.png';
var meta = document.createElement('div');
meta.className = 'searchDropdown__meta';
var title = document.createElement('div');
title.className = 'searchDropdown__title';
title.textContent = it.model || '';
var desc = document.createElement('div');
desc.className = 'searchDropdown__desc';
desc.textContent = it.description || '';
meta.appendChild(title);
meta.appendChild(desc);
a.appendChild(img);
a.appendChild(meta);
a.addEventListener('mouseenter', function () {
setActive(idx);
});
dropdown.appendChild(a);
})(i);
}
open();
}
function setActive(idx) {
var children = dropdown.querySelectorAll('.searchDropdown__item');
for (var i = 0; i < children.length; i++) {
children[i].classList.toggle('is-active', i === idx);
}
activeIndex = idx;
}
function fetchResults(q) {
if (abortController) {
abortController.abort();
}
abortController = new AbortController();
var url = 'api/search_products.php?q=' + encodeURIComponent(q) + '&limit=8';
setStatus('Suche…');
fetch(url, { signal: abortController.signal, headers: { 'Accept': 'application/json' } })
.then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function (data) {
render((data && data.items) ? data.items : []);
})
.catch(function (err) {
if (err && err.name === 'AbortError') return;
setStatus('Fehler bei der Suche.');
});
}
var debounced = debounce(function () {
var q = (input.value || '').trim();
if (q.length < 1) {
close();
return;
}
fetchResults(q);
}, 180);
input.addEventListener('input', debounced);
input.addEventListener('focus', function () {
var q = (input.value || '').trim();
if (q.length >= 1) {
fetchResults(q);
}
});
input.addEventListener('keydown', function (e) {
if (dropdown.hidden) return;
if (e.key === 'Escape') {
close();
return;
}
var max = items.length - 1;
if (max < 0) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
var next = activeIndex + 1;
if (next > max) next = 0;
setActive(next);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
var prev = activeIndex - 1;
if (prev < 0) prev = max;
setActive(prev);
} else if (e.key === 'Enter') {
if (activeIndex >= 0) {
var links = dropdown.querySelectorAll('.searchDropdown__item');
if (links[activeIndex]) {
e.preventDefault();
window.location.href = links[activeIndex].href;
}
}
}
});
document.addEventListener('click', function (e) {
if (wrapper.contains(e.target)) return;
close();
});
// prevent blur-close when clicking inside dropdown until navigation happens
dropdown.addEventListener('mousedown', function (e) {
e.preventDefault();
});
}
function init() {
var inputs = document.querySelectorAll('input.nav__searchInput[name="search"]');
for (var i = 0; i < inputs.length; i++) {
attachToInput(inputs[i]);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View File

@ -1,10 +1,40 @@
<?php
// attrbar.php
/**
* @file attrbar.php
* @brief Inkludierte Datei für die Darstellung und Verarbeitung der Attribut-Filterleiste (attrbar).
*
* @details Diese Datei generiert eine dynamische Filterleiste basierend auf der aktuell
* ausgewählten Produktkategorie. Sie stellt eine Verbindung zur Datenbank her, ruft
* zugehörige Attribute ab und generiert ein HTML-Formular zur Filterung der Produkte nach spezifischen Attributen.
* Das Senden des Formulars geschieht automatisch bei einer Änderung eines Auswahlfeldes (onchange) oder über
* einen Button (falls JavaScript deaktiviert ist).
*/
// Binden der benötigten Bootstrap-Datei ein, welche grundlegende Konfigurationen und Hilfsfunktionen zur Verfügung stellt.
require_once __DIR__ . '/lib/bootstrap.php';
/**
* @var mysqli $conn
* @brief Stellt eine Verbindung zur Datenbank her.
*/
$conn = db_connect();
/**
* @var string $currentCategory
* @brief Speichert die aktuell gesetzte Kategorie aus dem GET-Parameter. Standardwert ist 'all'.
*/
$currentCategory = isset($_GET['category']) ? $_GET['category'] : 'all';
/**
* @var int|null $catId
* @brief Beinhaltet die numerische ID der aktuellen Kategorie zur Verwendung in Datenbankabfragen.
*/
$catId = null;
/**
* @var array $categoriesConfig
* @brief Ein Mapping-Array, welches Kategorie-Slugs (Strings) auf deren entsprechende Datenbank-IDs (Integer) mappt.
*/
$categoriesConfig = [
'iphone' => 20,
'ipad' => 21,
@ -13,14 +43,24 @@ $categoriesConfig = [
'accessories' => 24,
];
// Überprüfen, ob die gewählte Kategorie im Konfigurations-Array vorhanden ist.
if (isset($categoriesConfig[$currentCategory])) {
// Falls vorhanden, wird die entsprechende Kategorie-ID für weitere Abfragen gesetzt.
$catId = $categoriesConfig[$currentCategory];
}
// Fetch available attributes for the category if selected
/**
* @var array $attributes
* @brief Speichert die verfügbaren Attribute (z.B. Farbe, Speichergröße) für die selektierte Kategorie.
*/
$attributes = [];
// Attribute nur laden, wenn eine gültige Kategorie-ID gefunden wurde.
if ($catId) {
// Show attributes related to the active category
/**
* @details Holt alle Attribute, die mit der aktiven Kategorie verknüpft sind,
* sortiert nach ihrem Namen, um eine geordnete Filterliste anzuzeigen.
*/
$stmtAttr = $conn->prepare("
SELECT a.attributeID, a.name, a.unit, a.dataType
FROM attributes a
@ -31,30 +71,51 @@ if ($catId) {
$stmtAttr->bind_param("i", $catId);
$stmtAttr->execute();
$resAttr = $stmtAttr->get_result();
// Iteriere über das Result-Set und speichere jedes gefundene Attribut im $attributes-Array.
while ($row = $resAttr->fetch_assoc()) {
$attributes[] = $row;
}
// Statement schließen, um Ressourcen freizugeben.
$stmtAttr->close();
}
?>
<?php if (!empty($attributes)): ?>
<?php
// Wenn Attribute existieren, wird der HTML-Container für die Filterleiste gerendert.
if (!empty($attributes)): ?>
<div class="attrbar" aria-label="Attributfilter">
<div class="attrbar__inner container">
<!-- Das Formular sendet Daten per GET an die index.php für die Filterung -->
<form action="index.php" method="GET" class="attr-filter-form">
<?php if (isset($_GET['category'])): ?>
<?php
// Aktuelle Kategorie-ID im Formular versteckt mitsenden, damit man in der Kategorie bleibt.
if (isset($_GET['category'])): ?>
<input type="hidden" name="category" value="<?= htmlspecialchars($_GET['category']) ?>">
<?php endif; ?>
<?php if (isset($_GET['search'])): ?>
<?php
// Einen eventuellen Suchbegriff ebenfalls beibehalten.
if (isset($_GET['search'])): ?>
<input type="hidden" name="search" value="<?= htmlspecialchars($_GET['search']) ?>">
<?php endif; ?>
<?php foreach ($attributes as $attr):
<?php
/**
* @details Iteriert durch jedes gefundene Attribut, um ein entsprechendes
* HTML-Select-Feld mit seinen eindeutigen Werten zu generieren.
*/
foreach ($attributes as $attr):
/** @var int $attrId Die Datenbank-ID des Attributs */
$attrId = $attr['attributeID'];
/** @var string $attrName Setzt sich zusammen aus dem Namen und, falls vorhanden, der Einheit (z.B. GB) */
$attrName = $attr['name'] . ($attr['unit'] ? ' (' . $attr['unit'] . ')' : '');
// Fetch distinct values for this attribute based on current category
// SQL-Abfrage vorbereiten, um alle unterschiedlichen (DISTINCT) Werte für genau dieses Attribut abzufragen.
if ($catId) {
// Abfrage mit Kategorie-Einschränkung
$vStmt = $conn->prepare("
SELECT DISTINCT pa.valueString, pa.valueNumber, pa.valueBool
FROM productAttributes pa
@ -63,6 +124,8 @@ if ($catId) {
");
$vStmt->bind_param("ii", $catId, $attrId);
} else {
// Alternative Abfrage (wird in diesem Skriptteil zwar aktuell nur bei $catId aufgerufen,
// aber dient als Fallback für zukünftige globale Filter).
$vStmt = $conn->prepare("
SELECT DISTINCT valueString, valueNumber, valueBool
FROM productAttributes
@ -72,14 +135,23 @@ if ($catId) {
}
$vStmt->execute();
$vRes = $vStmt->get_result();
/**
* @var array $values
* @brief Speichert die extrahierten eindeutigen Werte (bereinigt und formatiert) für die Select-Box.
*/
$values = [];
// Durchlaufe alle gefundenen Attributwerte für die aktuellen Produkte
while ($vRow = $vRes->fetch_assoc()) {
// Werte je nach Datentyp (Boolean, Number, String) extrahieren
if ($attr['dataType'] === 'boolean' || $vRow['valueBool'] !== null) {
$val = $vRow['valueBool'] ? 'Ja' : 'Nein';
// Duplikate vermeiden
if (!in_array($val, $values)) $values[] = $val;
} elseif ($attr['dataType'] === 'number' || $vRow['valueNumber'] !== null) {
$val = $vRow['valueNumber'];
// strip trailing zero for decimals if desired, e.g. 5.00 -> 5
// Entfernt überflüssige Nullen nach dem Komma (z.B. wird 5.00 zu 5)
$val = rtrim(rtrim((string)$val, '0'), '.');
if (!in_array($val, $values)) $values[] = $val;
} elseif ($vRow['valueString'] !== null) {
@ -89,22 +161,36 @@ if ($catId) {
}
$vStmt->close();
// Sort values
/**
* @details Sortiert die gesammelten Werte.
* Zahlen werden numerisch sortiert (damit z.B. 10 nach 2 kommt),
* Strings werden alphabetisch sortiert.
*/
if ($attr['dataType'] === 'number') {
usort($values, function($a, $b) { return (float)$a <=> (float)$b; });
} else {
sort($values);
}
// Wenn es keine Werte für das Filterfeld gibt, überspringe dieses Attribut.
if (empty($values)) continue;
/** @var string $paramName Der Name des GET-Parameters im Formular (z.B. attr_1) */
$paramName = "attr_" . $attrId;
/** @var string $selectedValue Der aktuell ausgewählte Wert aus dem GET-Parameter, falls gesetzt */
$selectedValue = isset($_GET[$paramName]) ? $_GET[$paramName] : '';
?>
<div class="attr-filter-item">
<!-- Label für das Attribut (z.B. Speichergröße (GB)) -->
<label for="attr_<?= $attrId ?>"><?= htmlspecialchars($attrName) ?>:</label>
<!-- Dropdown-Menu zur Auswahl eines Werts. onchange sumbittet das Formular sofort via JS. -->
<select name="<?= $paramName ?>" id="attr_<?= $attrId ?>" onchange="this.form.submit()">
<option value="">Alle</option>
<?php foreach ($values as $val): ?>
<?php
// Liste alle aufbereiteten Optionen auf
foreach ($values as $val): ?>
<option value="<?= htmlspecialchars($val) ?>" <?= (string)$val === (string)$selectedValue ? 'selected' : '' ?>>
<?= htmlspecialchars($val) ?>
</option>
@ -113,20 +199,29 @@ if ($catId) {
</div>
<?php endforeach; ?>
<!-- Fallback-Button, falls der Nutzer JavaScript in seinem Browser deaktiviert hat. -->
<noscript>
<button type="submit">Filtern</button>
</noscript>
<?php
// Show reset button only if at least one attr filter is active
/**
* @var bool $hasActiveFilter
* @brief Überprüft, ob mindestens ein Attributfilter aktiv in der URL vorkommt.
*/
$hasActiveFilter = false;
foreach ($_GET as $k => $v) {
// Wenn der GET-Key mit "attr_" beginnt und ein Wert gesetzt ist
if (strpos($k, 'attr_') === 0 && $v !== '') {
$hasActiveFilter = true;
break;
break; // Sobald ein Filter gefunden ist, kann die Schleife abgebrochen werden
}
}
// Zeige den "Filter zurücksetzen"-Button nur an, wenn wirklich Filter aktiv sind.
if ($hasActiveFilter): ?>
<div class="attr-filter-action">
<!-- Ein Link auf die index.php (mit Kategorie), der alle Attribut-GET-Parameter verwirft -->
<a href="index.php<?= isset($_GET['category']) ? '?category='.urlencode($_GET['category']) : '' ?>" class="attr-filter-reset">Filter zurücksetzen</a>
</div>
<?php endif; ?>

View File

@ -1,28 +1,80 @@
<?php
/**
* @file catbar.php
* @brief Category Navigation Bar / Kategorie-Navigationsleiste
*
* This file contains the HTML structure for the main category navigation bar
* used in the application. It provides links to filter products by their
* respective categories.
*
* @details
* Diese Datei enthält die HTML-Struktur für die Hauptkategorie-Navigationsleiste,
* die in der Anwendung verwendet wird. Sie stellt Links zur Verfügung, um Produkte
* nach ihren jeweiligen Kategorien (z.B. iPhone, iPad, MacBook) zu filtern.
* Jeder Link verweist auf index.php und übergibt die gewählte Kategorie als GET-Parameter.
*/
?>
<!--
@brief Main navigation container for the home page.
@details Nutzt das HTML5 <nav> Tag für semantisch korrekte Navigation und enthält ein aria-label zur Barrierefreiheit.
-->
<nav class="home-nav" aria-label="Homenavigation">
<!--
@brief Inner container to restrict width and center content
@details Beschränkt die Breite des Inhalts und zentriert diesen.
-->
<div class="home-nav__inner container">
<!--
@brief Unordered list representing the navigation items
@details Ungeordnete Liste, die die einzelnen Kategorielinks enthält.
-->
<ul class="home-nav__list">
<!--
@brief Category link for 'iPhone'
@details Führt zur Startseite mit dem Filter category=iphone
-->
<li class="home-nav__item">
<a href='index.php?category=iphone'>iPhone</a>
</li>
<!--
@brief Category link for 'iPad'
@details Führt zur Startseite mit dem Filter category=ipad
-->
<li class="home-nav__item">
<a href='index.php?category=ipad'>iPad</a>
</li>
<!--
@brief Category link for 'MacBook'
@details Führt zur Startseite mit dem Filter category=macbook
-->
<li class="home-nav__item">
<a href='index.php?category=macbook'>MacBook</a>
</li>
<!--
@brief Category link for 'AirPods'
@details Führt zur Startseite mit dem Filter category=airpods
-->
<li class="home-nav__item">
<a href='index.php?category=airpods'>AirPods</a>
</li>
<!--
@brief Category link for 'Watch'
@details Führt zur Startseite mit dem Filter category=watch
-->
<li class="home-nav__item">
<a href='index.php?category=watch'>Watch</a>
</li>
<!--
@brief Category link for 'Accessories' (Zubehör)
@details Führt zur Startseite mit dem Filter category=accessories
-->
<li class="home-nav__item">
<a href='index.php?category=accessories'>Accessories</a>
</li>

View File

@ -1,19 +1,67 @@
<?php
/**
* @file compare.php
* @brief Skript zur Darstellung und Verwaltung des Produktvergleichs.
*
* @details Diese Datei rendert die Produktvergleichsseite. Sie ermöglicht dem Nutzer,
* Produkte aus derselben Kategorie nebeneinander tabellarisch aufzulisten, um ihre
* Eigenschaften (Attribute, Beschreibungen etc.) zu vergleichen. Produkte, die
* zuvor zur Session-Variable 'compare' hinzugefügt wurden, werden hier ausgelesen,
* ihre Daten und zugehörigen Attribute aus der Datenbank geladen und übersichtlich
* dargestellt. Zudem bietet die Seite die Funktionalität, einzelne Produkte wieder
* aus dem Vergleich zu entfernen.
*
* Beinhaltet:
* - Session-Management für den Produktvergleich.
* - Datenbankabfragen für Kategorien, Produkte und Attribute.
* - HTML-Generierung der Vergleichstabellen.
*
* @author Geizkragen-Team
* @date 2026-04-03
*/
require_once __DIR__ . '/lib/bootstrap.php';
/**
* @var mysqli $conn Die globale Datenbankverbindung, die durch db_connect() erstellt wird.
*/
$conn = db_connect();
/**
* @var string $title Der Titel der Webseite, der im Header angezeigt wird.
*/
$title = "Produktvergleich | Geizkragen";
// Einbinden der Header-Datei für das Layout und die Navigation.
include 'header.php';
/**
* @brief Behandelt das Entfernen eines Produkts aus dem Vergleich.
*
* Überprüft, ob ein POST-Request vorliegt und das Flag 'remove_compare' gesetzt ist.
* Ist dies der Fall, wird die übergebene Produkt-ID aus der Session-Variable
* in allen Kategorien entfernt.
*/
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_compare'])) {
/**
* @var int $removeId Die zu entfernende Produkt-ID, sicher in einen Integer umgewandelt.
*/
$removeId = (int)$_POST['remove_compare_id'];
// Prüfen, ob die Session-Variable für Vergleiche existiert und ein Array ist.
if (isset($_SESSION['compare']) && is_array($_SESSION['compare'])) {
// Durchsuchen aller Kategorien in der Session nach dem zu entfernenden Produkt.
foreach ($_SESSION['compare'] as $cat => &$prodList) {
/**
* @var int|false $key Der Array-Index der Produkt-ID in der aktuellen Kategorie-Liste.
*/
$key = array_search($removeId, $prodList);
if ($key !== false) {
// Das Produkt wurde gefunden und wird aus dem Array entfernt.
unset($prodList[$key]);
}
}
// Referenz auf $prodList löschen, um unbeabsichtigte Nebeneffekte zu vermeiden.
unset($prodList);
}
}
@ -23,18 +71,33 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_compare'])) {
<h1 class="compare-title">Produktvergleich</h1>
<?php
/**
* @var bool $hasProducts Flag, welches angibt, ob aktuell Produkte für den Vergleich vorhanden sind.
*/
$hasProducts = false;
// Iteration über die Session-Variable 'compare', sofern diese existiert und gültig ist.
if (isset($_SESSION['compare']) && is_array($_SESSION['compare'])) {
foreach ($_SESSION['compare'] as $categoryId => $productIds) {
// Wenn die Liste der Produkte für diese Kategorie leer ist, überspringen wir sie.
if (empty($productIds)) continue;
// Setze Flag auf true, da mindestens ein zu vergleichendes Produkt existiert.
$hasProducts = true;
// Kategorie-Namen holen
/**
* @var string $catName Der Standardname der Kategorie (wird überschrieben, falls in der DB gefunden).
*/
$catName = "Kategorie $categoryId";
/**
* @brief Holt den echten Namen der Kategorie aus der Datenbank.
*/
$stmtCat = $conn->prepare("SELECT name FROM categories WHERE categoryID = ?");
if ($stmtCat) {
$stmtCat->bind_param("i", $categoryId);
$stmtCat->execute();
/** @var mysqli_result $resCat Das Ergebnis der Abfrage des Kategorienamens. */
$resCat = $stmtCat->get_result();
if ($row = $resCat->fetch_assoc()) {
$catName = $row['name'];
@ -42,8 +105,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_compare'])) {
$stmtCat->close();
}
// Attribute der Kategorie holen
/**
* @var array $attributes Sammelt alle Attribute (Eigenschaften), die zu dieser Kategorie gehören.
* Format: $attributes[attributeID] = [ 'attributeID' => ..., 'name' => ..., 'unit' => ..., 'dataType' => ... ]
*/
$attributes = [];
/**
* @brief Holt die Kategorie-Attribute mit Namen, Einheit und Datentyp.
*/
$stmtAttr = $conn->prepare("
SELECT a.attributeID, a.name, a.unit, a.dataType
FROM categoryAttributes ca
@ -54,6 +124,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_compare'])) {
if ($stmtAttr) {
$stmtAttr->bind_param("i", $categoryId);
$stmtAttr->execute();
/** @var mysqli_result $resAttr Das Ergebnis der Abfrage der Kategorie-Attribute. */
$resAttr = $stmtAttr->get_result();
while ($row = $resAttr->fetch_assoc()) {
$attributes[$row['attributeID']] = $row;
@ -61,9 +132,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_compare'])) {
$stmtAttr->close();
}
// Produktdaten holen
/**
* @var array $products Sammelt die Grunddaten der zugehörigen Produkte (Model, Bild, Beschreibung).
* Format: $products[productID] = [ ... Produktdaten ... ]
*/
$products = [];
/**
* @var string $idList Kommaseparierte Liste der zu vergleichenden Produkt-IDs, escapet als Integers für den IN-Query.
*/
$idList = implode(',', array_map('intval', $productIds));
/**
* @brief Holt die Produktinformationen für die IN-Klausel Liste.
*/
$stmtProd = $conn->query("SELECT productID, model, imagePath, description FROM products WHERE productID IN ($idList)");
if ($stmtProd) {
while ($row = $stmtProd->fetch_assoc()) {
@ -71,8 +152,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_compare'])) {
}
}
// Produktattribute holen
/**
* @var array $productAttrVals Sammelt die spezifischen Attribut-Werte je Produkt.
* Format: $productAttrVals[productID][attributeID] = [ ... Attributwerte ... ]
*/
$productAttrVals = [];
/**
* @brief Holt die Werte (String, Number, Bool) der Attribute für die ausgewählten Produkte.
*/
$stmtProdAttr = $conn->query("SELECT productID, attributeID, valueString, valueNumber, valueBool FROM productAttributes WHERE productID IN ($idList)");
if ($stmtProdAttr) {
while ($row = $stmtProdAttr->fetch_assoc()) {
@ -82,23 +170,34 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_compare'])) {
}
?>
<!-- Ausgabe des Kategorie-Titels -->
<h2 class="compare-category-title"><?= htmlspecialchars($catName) ?></h2>
<!-- Beginn der Vergleichstabelle der aktuellen Kategorie -->
<div class="compare-table-wrapper">
<table class="compare-table">
<thead>
<tr>
<!-- Die erste Spalte enthält den Titel "Eigenschaft" -->
<th>Eigenschaft</th>
<!-- Iteration über die Produkt-IDs zur Erzeugung der Spaltenköpfe -->
<?php foreach ($productIds as $pId): ?>
<?php if (!isset($products[$pId])) continue; ?>
<?php
// Wenn das Produkt in der DB nicht (mehr) existiert, überspringen.
if (!isset($products[$pId])) continue;
?>
<th>
<div>
<!-- Produktbild mit Link zur Produktdetailseite -->
<a href="productpage.php?id=<?= $pId ?>">
<img src="<?= htmlspecialchars($products[$pId]['imagePath'] ?? 'assets/images/placeholder.png') ?>" alt="Produktbild" class="compare-product-img">
</a>
</div>
<!-- Produktname mit Link zur Produktdetailseite -->
<h3><a href="productpage.php?id=<?= $pId ?>" class="compare-product-name"><?= htmlspecialchars($products[$pId]['model']) ?></a></h3>
<!-- Formular zum Entfernen dieses spezifischen Produkts aus dem Vergleich -->
<form method="POST" action="compare.php">
<input type="hidden" name="remove_compare" value="1">
<input type="hidden" name="remove_compare_id" value="<?= $pId ?>">
@ -109,34 +208,62 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_compare'])) {
</tr>
</thead>
<tbody>
<!-- Zeile für die Produktbeschreibung -->
<tr>
<td>Beschreibung</td>
<?php foreach ($productIds as $pId): ?>
<?php if (!isset($products[$pId])) continue; ?>
<?php
// Auch hier prüfen, ob die Produktdaten valide sind
if (!isset($products[$pId])) continue;
?>
<td>
<!-- Ausgabe der Produktbeschreibung -->
<span class="desc-text"><?= htmlspecialchars($products[$pId]['description']) ?></span>
</td>
<?php endforeach; ?>
</tr>
<!-- Dynamische Iteration über alle dieser Kategorie zugeordneten Attribute -->
<?php foreach ($attributes as $attrId => $attr): ?>
<tr>
<!-- Ausgabe des Attributnamens -->
<td><?= htmlspecialchars($attr['name'] ?? '') ?></td>
<?php foreach ($productIds as $pId): ?>
<?php if (!isset($products[$pId])) continue; ?>
<?php
// Existenzprüfung des Produkts
if (!isset($products[$pId])) continue;
?>
<td>
<?php
/**
* @brief Logik zur Darstellung des passenden Attributwerts.
*
* Prüft, welcher Datentyp / welches Feld im Datensatz gesetzt ist
* und wählt die angemessene Formatierung (inklusive Einheit bei Zahlen,
* 'Ja'/'Nein' bei Booleans).
*/
if (isset($productAttrVals[$pId][$attrId])) {
$valRow = $productAttrVals[$pId][$attrId];
// Fall: String-Wert vorhanden
if (!empty($valRow['valueString'])) {
echo htmlspecialchars($valRow['valueString'] ?? '');
} elseif (!empty($valRow['valueNumber']) || $valRow['valueNumber'] === '0.00' || $valRow['valueNumber'] === 0) {
}
// Fall: Numerischer Wert vorhanden. '0.00' oder 0 ist ebenfalls gültig.
elseif (!empty($valRow['valueNumber']) || $valRow['valueNumber'] === '0.00' || $valRow['valueNumber'] === 0) {
echo htmlspecialchars((string)floatval($valRow['valueNumber'])) . " " . htmlspecialchars($attr['unit'] ?? '');
} elseif ($valRow['valueBool'] !== null) {
}
// Fall: Boolescher Wert (wurde in DB gesetzt, nicht null)
elseif ($valRow['valueBool'] !== null) {
echo $valRow['valueBool'] ? 'Ja' : 'Nein';
} else {
}
// Kein verwertbarer Wert vorhanden, Ausgabe eines Platzhalters.
else {
echo '-';
}
} else {
// Wenn zu diesem Produkt kein Datensatz für dieses Attribut gefunden wurde.
echo '-';
}
?>
@ -151,10 +278,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_compare'])) {
}
}
// Meldung falls keine Produkte für den Vergleich gesetzt sind.
if (!$hasProducts) {
echo "<div class='compare-empty'><p>Du hast noch keine Produkte zum Vergleich hinzugefügt. Gehe auf eine Produktseite, um Produkte hinzuzufügen.</p></div>";
}
?>
</div>
<?php include 'footer.php'; ?>
<?php
// Einbinden der Footer-Datei am Ende der Seite.
include 'footer.php';
?>

View File

@ -1,28 +1,66 @@
<?php
/**
* @file compcards.php
* @brief Datei compcards.php
*
* @details Diese Datei ist für die Anzeige der Produktkarten auf der Übersichts- oder Suchseite zuständig.
* Sie verarbeitet Suchanfragen sowie Kategorie- und Attributfilter und generiert die entsprechende HTML-Ausgabe.
* Es werden keine Änderungen am bestehenden Code vorgenommen, lediglich diese ausführlichen Kommentare hinzugefügt.
*/
// login.php
require_once __DIR__ . '/lib/bootstrap.php';
/**
* Fehleranzeige für Entwicklungszwecke aktivieren.
* Hiermit werden alle Fehler, Warnungen und Hinweise direkt ausgegeben.
*/
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// 1) DB-Verbindung (einmal)
/**
* 1) DB-Verbindung (einmalig aufbauen)
* Die Funktion db_connect() muss in den eingebundenen Bibliotheken (z. B. bootstrap.php / db.php) definiert sein.
* @var mysqli $conn Die aktive Datenbankverbindung.
*/
$conn = db_connect();
?>
<?php
// ─────────────────────────────────────────────
// Reine PHP-Suche (GET ?search=...)
// Wenn ein Suchbegriff vorhanden ist, zeigen wir nur Suchergebnisse
// (statt der Kategorie-Sektionen).
// ─────────────────────────────────────────────
/**
* ─────────────────────────────────────────────
* Reine PHP-Suche (GET ?search=...)
* ─────────────────────────────────────────────
* @details Wenn ein Suchbegriff über den URL-Parameter 'search' übergeben wurde,
* zeigen wir ausschließlich die Suchergebnisse an und überspringen die Anzeige
* der standardmäßigen Kategorie-Sektionen.
*/
/**
* @var string $searchTerm Holt den Suchbegriff aus dem GET-Parameter, trimmt überflüssige Leerzeichen.
*/
$searchTerm = isset($_GET['search']) ? trim((string)$_GET['search']) : '';
/**
* @var int $searchLen Berechnet die Länge des Suchbegriffs (multibyte-safe, falls mb_strlen existiert).
*/
$searchLen = function_exists('mb_strlen') ? mb_strlen($searchTerm, 'UTF-8') : strlen($searchTerm);
if ($searchTerm !== '') {
/**
* @details Escaping des Suchbegriffs für die LIKE-Klausel, um SQL-Injections durch Wildcards zu verhindern.
* @var string $like Der für SQL präparierte Such-String mit umschließenden %-Zeichen.
*/
$like = addcslashes($searchTerm, "%_\\");
$like = '%' . $like . '%';
/**
* @details Führt eine Suche auf die Tabelle 'products' aus.
* Es wird sowohl in der Spalte 'model' als auch in 'description' gesucht.
* @var mysqli_stmt|false $stmtSearch Das Prepared Statement für die Suche.
*/
$stmtSearch = $conn->prepare("
SELECT productID, model, description, imagePath
FROM products
@ -32,8 +70,13 @@ if ($searchTerm !== '') {
");
if ($stmtSearch) {
// Parameterbindung: Zwei Strings ('ss') für die doppelten LIKE-Bedingungen.
$stmtSearch->bind_param('ss', $like, $like);
$stmtSearch->execute();
/**
* @var mysqli_result $resultSearch Das ResultSet der ausgeführten Suchanfrage.
*/
$resultSearch = $stmtSearch->get_result();
?>
@ -41,18 +84,28 @@ if ($searchTerm !== '') {
<h2>Suchergebnisse für <?= htmlspecialchars($searchTerm) ?>“</h2>
<?php if ($resultSearch->num_rows <= 0): ?>
<!-- Wenn keine Ergebnisse gefunden wurden -->
<p class="search-empty">Keine Produkte gefunden.</p>
<?php else: ?>
<!-- Grid für die Anzeige der Suchergebnisse -->
<div class="product-grid">
<?php while ($product = $resultSearch->fetch_assoc()): ?>
<?php $productId = (int)$product['productID']; ?>
<?php
/**
* @var int $productId Casting der Produkt-ID auf Integer für den Link.
*/
$productId = (int)$product['productID'];
?>
<a class="product-card" href="productpage.php?id=<?= $productId ?>">
<!-- Anzeige des Produktbildes mit Fallback auf ein Platzhalter-Bild -->
<img
src="<?= !empty($product['imagePath']) ? htmlspecialchars($product['imagePath']) : 'assets/images/placeholder.png' ?>"
alt="<?= htmlspecialchars($product['model'] ?? '') ?>">
<div class="product-card__content">
<!-- Der Modellname des Produkts -->
<h3><?= htmlspecialchars($product['model'] ?? '') ?></h3>
<!-- Die Beschreibung des Produkts -->
<p><?= htmlspecialchars($product['description'] ?? '') ?></p>
</div>
</a>
@ -62,19 +115,31 @@ if ($searchTerm !== '') {
</section>
<?php
// Schließen des Prepared Statements zur Freigabe von Ressourcen.
$stmtSearch->close();
}
// Wichtig: In Suchmodus KEINE Kategorien rendern.
/**
* Wichtig: Im Suchmodus beenden wir die Einbindung dieser Datei hier (return),
* damit keine weiteren Kategorie-Blöcke gerendert werden.
*/
return;
}
?>
<?php
/**
* @details Ermitteln der aktiven Kategorie. Falls keine angegeben wurde, ist 'all' der Standard.
* @var string $activeCategory Die aktive Kategorie-ID in Textform.
*/
$activeCategory = isset($_GET['category']) ? $_GET['category'] : 'all';
?>
<?php
/**
* @details Mapping von Kategorie-Keys zu Datenbank-IDs und lesbaren Labels.
* @var array $categories Ein assoziatives Array der verfügbaren Hauptkategorien.
*/
$categories = [
'iphone' => ['id' => 20, 'label' => 'iPhone'],
'ipad' => ['id' => 21, 'label' => 'iPad'],
@ -84,64 +149,128 @@ $categories = [
];
?>
<?php
/**
* Iteration über alle definierten Kategorien, um jede als einzelnen Sektor anzuzeigen.
*/
foreach ($categories as $key => $cat):
?>
<?php foreach ($categories as $key => $cat): ?>
<?php if ($activeCategory === 'all' || $activeCategory === $key): ?>
<?php
/**
* @details Nur dann Daten laden und Sektion anzeigen, wenn entweder alle Kategorien
* gewünscht sind oder der key mit der aktuell gewählten Kategorie übereinstimmt.
*/
if ($activeCategory === 'all' || $activeCategory === $key):
?>
<?php
/**
* @var string $baseQuery Der grundlegende SELECT-Teil der dynamischen Produktsuche.
*/
$baseQuery = "SELECT DISTINCT p.productID, p.model, p.description, p.imagePath FROM products p ";
/**
* @var array $whereClauses Array zum Sammeln aller WHERE-Bedingungen.
*/
$whereClauses = ["p.categoryID = ?"];
/**
* @var array $params Array zur Aufnahme der Bind-Parameter für das Prepared Statement.
*/
$params = [$cat['id']];
/**
* @var string $types Enthält den Typ-String für bind_param (z. B. 'i', 's', 'd').
*/
$types = "i";
// Find attribute filters from $_GET
/**
* @details Auslesen von dynamischen Attribut-Filtern aus der URL ($_GET).
* @var int $attrIndex Zähler zur Erzeugung eindeutiger Aliase für JOINs.
*/
$attrIndex = 0;
foreach ($_GET as $k => $v) {
// Nur Parameter berücksichtigen, die auf 'attr_' beginnen und einen Wert haben.
if ($v !== '' && strpos($k, 'attr_') === 0) {
$attrId = (int)substr($k, 5);
$attrAlias = "pa" . $attrIndex;
// Dynamischer JOIN der productAttributes-Tabelle für jedes gefilterte Attribut.
$baseQuery .= " JOIN productAttributes $attrAlias ON p.productID = $attrAlias.productID ";
// Assume string or number comparison. For simplicity, check string or number.
// In DB, valueString, valueNumber, valueBool can be checked.
/**
* @details Sucht im Attribut entweder nach String, Number oder Boolean ('Ja'/'Nein').
*/
$whereClauses[] = "($attrAlias.attributeID = ? AND ($attrAlias.valueString = ? OR $attrAlias.valueNumber = ? OR ($attrAlias.valueBool = 1 AND ? = 'Ja') OR ($attrAlias.valueBool = 0 AND ? = 'Nein')))";
// Parameter für bind_param befüllen
$params[] = $attrId;
$params[] = $v;
$params[] = is_numeric($v) ? (float)$v : 0;
$params[] = $v;
$params[] = $v;
// Typen ergänzen: Integer, String, Double, String, String an SQL übergeben
$types .= "isdss";
$attrIndex++;
}
}
/**
* @var string $sql Zusammensetzen der kompletten SQL-Abfrage aus Base, JOINs und WHEREs.
*/
$sql = $baseQuery . " WHERE " . implode(" AND ", $whereClauses);
/**
* @var mysqli_stmt $stmt Das Prepared Statement für die Kategorieabfrage.
*/
$stmt = $conn->prepare($sql);
// Bind Parameter per Spread-Operator aus dem Params-Array.
$stmt->bind_param($types, ...$params);
$stmt->execute();
/**
* @var mysqli_result $result Das Ergebnis-Set mit den gefundenen Produkten dieser Kategorie.
*/
$result = $stmt->get_result();
?>
<?php if ($result->num_rows > 0): ?>
<?php
/**
* Rendern der Produktsektion nur, falls auch Produkte in dieser Kategorie gefunden wurden.
*/
if ($result->num_rows > 0):
?>
<section class="product-section">
<!-- Ausgabe des Kategorie-Labels als Überschrift -->
<h2><?= htmlspecialchars($cat['label']) ?></h2>
<!-- Horizontaler Scroll-Bereich für die Produktkarten einer Kategorie -->
<div class="product-scroll">
<?php while ($product = $result->fetch_assoc()): ?>
<?php $productId = (int)$product['productID']; ?>
<?php
/**
* Fetch-Schleife über jedes Produkt im Result-Set.
*/
while ($product = $result->fetch_assoc()):
?>
<?php
/**
* @var int $productId Casting der Produkt-ID
*/
$productId = (int)$product['productID'];
?>
<a class="product-card" href="productpage.php?id=<?= $productId ?>">
<!-- Produktbild und Fallback auf Platzhalter -->
<img
src="<?= isset($product['imagePath']) ? $product['imagePath'] : 'assets/images/placeholder.png' ?>"
alt="<?= htmlspecialchars($product['model']) ?>">
<div class="product-card__content">
<!-- Anzeige des Produktnamens -->
<h3><?= htmlspecialchars($product['model']) ?></h3>
<!-- Anzeige der Produktbeschreibung -->
<p><?= htmlspecialchars($product['description']) ?></p>
</div>
</a>
@ -150,9 +279,11 @@ $categories = [
</section>
<?php endif; ?>
<?php $stmt->close(); ?>
<?php
// Schließen des Prepared Statements der Kategorie, um Ressourcen freizugeben.
$stmt->close();
?>
<?php endif; ?>
<?php endforeach; ?>

View File

@ -1 +0,0 @@
<?php

View File

@ -1,12 +1,54 @@
<?php
/**
* @file footer.php
* @brief Enthält den Footer-Bereich der Geizkragen-Anwendung.
*
* Diese Datei wird am Ende der Seiten eingebunden, um den Footer darzustellen.
* Sie schließt die grundlegenden HTML-Strukturen ab und enthält das Copyright
* mit dem sich dynamisch aktualisierenden Jahr, sowie sekundäre Navigationslinks
* (wie z.B. das Impressum).
*/
?>
<!--
======================================================================
Footer Sektion
Das <footer/> Element stellt den Abschluss der Webseitenstruktur dar.
Die Rolle 'contentinfo' ist für die Barrierefreiheit (Screenreader) wichtig
und weist darauf hin, dass hier Metainformationen wie Copyright und rechtliche
Hinweise zu finden sind.
======================================================================
-->
<footer class="footer" role="contentinfo">
<!--
Container-DIV
Dient zur Zentrierung und Breitenbegrenzung des Footer-Inhalts,
um ein konsistentes Layout zu gewährleisten.
-->
<div class="container">
<!--
Copyright-Absatz
Zeigt das Copyright-Zeichen, das aktuelle Jahr (dynamisch erzeugt
durch PHP date('Y')) und den Namen der Plattform an.
-->
<p>&copy; <?= date('Y') ?> Geizkragen</p>
<!--
Footer-Navigation
Stellt sekundäre Links bereit.
Das Attribut aria-label="Footer Navigation" verbessert die
Barrierefreiheit (Accessibility) für assistive Technologien.
-->
<nav class="footer__nav" aria-label="Footer Navigation">
<!-- Link zur rechtsverbindlichen Impressumsseite -->
<a href="impressum.php">Impressum</a>
</nav>
</div>
</footer>
<!--
Schließen der grundlegenden HTML-Tags.
Diese Tags wurden üblicherweise in der header.php geöffnet.
-->
</body>
</html>

View File

@ -1,3 +1,25 @@
<?php
/**
* @file header.php
* @brief Globale Header-Datei für die Geizkragen-Anwendung.
*
* Diese Datei bildet den initialen HTML-Kopf (Head) der Anwendung und enthält
* alle grundlegenden Meta-Tags, Stylesheets und Favicon-Definitionen.
* Zudem definiert sie die globale Navigationsleiste, inklusive Desktop- und
* Mobile-Layouts, einer globalen Produktsuche sowie dem Overlay-Menü.
*
* @details
* Folgende Kernkomponenten sind hier implementiert:
* - Einbindung aller relevanten CSS-Dateien (@see style.css, @see login.css, etc.).
* - Setup für Viewport und Mobile-Geräte (Safe Areas für Notch-Geräte).
* - Eine reaktionsfähige Navigationsleiste mit Links zu Home, Vergleich, Wunschliste und Account.
* - Ein Mobile-Menü (Off-Canvas), das per JavaScript geöffnet und geschlossen wird.
* - Suchformulare, die Benutzeranfragen per GET an die index.php übermitteln.
*
* @note Diese Datei wird üblicherweise in anderen Seiten inkludiert. Sie enthält KEINEN
* eigenständigen PHP-Logikbereich am Anfang, um direkt die HTML-Struktur zu rendern.
*/
?>
<!DOCTYPE html>
<html lang="de">
@ -20,14 +42,27 @@
<title>Geizkragen</title>
<style>
/* ─── Mobile tap highlight ─── */
/**
* @brief Standard-UI Anpassungen für mobile Geräte.
* Verhindert den blauen Tap-Highlight auf iOS und mobilen Browsern.
*/
* { -webkit-tap-highlight-color: transparent; }
/* ─── Safe areas (notch phones) ─── */
/**
* @brief Safe Areas für Notch-Geräte (z.B. iPhone).
* Sorgt dafür, dass der Header nicht in abgerundete Ecken oder die Kameraaussparung ragt.
*/
.header { padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); }
/**
* @brief Safe Area für den Footer.
*/
.footer { padding-bottom: env(safe-area-inset-bottom); }
/* ─── Mobile overlay ─── */
/**
* @brief Definiert das Overlay (Hintergrund-Verdunkelung/Blur) für das Mobile-Menü.
* Es wird unsichtbar gerendert und erst sichtbar, wenn die Klasse `is-visible` hinzugefügt wird.
*/
.nav__overlay {
position: fixed;
inset: 0;
@ -39,6 +74,10 @@
opacity: 0;
transition: opacity 300ms ease, visibility 0s 300ms;
}
/**
* @brief Aktiver Zustand des Overlays. Wichtig für fließende Fade-in Übergänge.
*/
.nav__overlay.is-visible {
visibility: visible;
opacity: 1;
@ -48,7 +87,11 @@
</head>
<body>
<!-- Overlay (klick = Menü schließen) -->
<!--
@brief Overlay-Container
Dient der Verdunkelung des Bildschirms beim Öffnen des mobilen Menüs.
Ein Klick auf dieses Element schließt das Menü.
-->
<div class="nav__overlay" id="nav-overlay"></div>
<header class="header" id="header">
@ -138,7 +181,11 @@
</div>
</nav>
<!-- Mobile-Suchleiste (unterhalb der Nav-Zeile, nur auf ≤900px sichtbar) -->
<!--
@brief Mobile-Suchleiste
Wird unterhalb der Nav-Zeile platziert und ist per CSS in der Regel nur
auf Bildschirmen mit einer Breite von <=900px sichtbar.
-->
<form class="nav__searchBar" action="index.php" method="GET" autocomplete="off">
<div class="nav__searchField">
<input class="nav__searchInput" type="text" name="search"
@ -148,46 +195,69 @@
</header>
<script>
/**
* @brief Kapselung der gesamten JavaScript-Logik für die Navigation.
*
* Verwendet eine IIFE (Immediately Invoked Function Expression), um globale Scope-Verschmutzung zu vermeiden.
* Handhabt das Öffnen, Schließen und die Event-Bindungen des Slide-In/Mobile-Menüs.
*/
(function () {
var toggle = document.getElementById('nav-toggle');
var closeBtn = document.getElementById('nav-close');
var menu = document.getElementById('nav-menu');
var overlay = document.getElementById('nav-overlay');
// DOM Elemente für die Mobile-Menü-Steuerung
var toggle = document.getElementById('nav-toggle'); /**< Button zum Öffnen des Menüs */
var closeBtn = document.getElementById('nav-close'); /**< Button zum Schließen des Menüs */
var menu = document.getElementById('nav-menu'); /**< Container des Slide-In-Menüs */
var overlay = document.getElementById('nav-overlay'); /**< Das verdunkelnde Desktop-Overlay */
// Abbruch, falls eines der essentiellen Elemente fehlt
if (!toggle || !closeBtn || !menu || !overlay) return;
/**
* @brief Öffnet das Mobile-Menü.
*
* Fügt dem Menü und dem Overlay die notwendigen Styling-Klassen hinzu,
* um diese sichtbar zu machen. Blockiert zudem das Scrollen der aktuellen Seite.
*/
function open() {
menu.classList.add('show-menu');
overlay.classList.add('is-visible');
document.body.style.overflow = 'hidden';
}
/**
* @brief Schließt das Mobile-Menü.
*
* Entfernt die aktiven Zustands-Klassen und stellt das gewöhnliche
* Scroll-Verhalten (overflow) wieder her.
*/
function close() {
menu.classList.remove('show-menu');
overlay.classList.remove('is-visible');
document.body.style.overflow = '';
}
// Event-Listener für das Öffnen der Navigation
toggle.addEventListener('click', open);
// Event-Listener für das Schließen der Navigation über den X-Button oder Overlay-Klick
closeBtn.addEventListener('click', close);
overlay.addEventListener('click', close);
/**
* @brief Accessibility: Schließt das Menü via Escape-Taste.
*/
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') close();
});
// Links im Menü → schließen
/**
* @brief Automatisches Schließen des Menüs bei Link-Klick.
*
* Iteriert über alle im Menü vorhandenen Anchor ('a') Tags. Sobald ein Link
* angeklickt wird, wird das Menü geschlossen.
*/
var links = menu.querySelectorAll('a');
for (var i = 0; i < links.length; i++) {
links[i].addEventListener('click', close);
}
})();
</script>

View File

@ -1,7 +1,24 @@
<?php include 'header.php'; ?>
<?php
/**
* @file impressum.php
* @brief Darstellung der Impressum-Seite für das Geizkragen-Projekt.
*
* @details Diese Datei generiert das Impressum, welches rechtliche Vorgaben gemäß § 5 TMG
* und § 25 MedienG erfüllt. Hier werden die Website-Inhaber, der Server-Betreiber sowie
* Informationen zu Haftungsausschluss, Urheberrecht und Datenschutz dargestellt.
* Die Anzeige erfolgt in einem responsiven CSS-Grid-Layout.
*
* @author Fabian Schieder, Paul Eisenbock
* @version 1.0
* @date 2026
*/
include 'header.php'; ?>
<style>
/* ─── Impressum Styles ─── */
/**
* @brief Hauptcontainer für das Impressum.
* @details Setzt Abstände, zentriert den Inhalt und fügt eine Einblend-Animation (fadeInUp) hinzu.
*/
.impressum {
padding: 3rem 1.5rem 4rem;
max-width: 900px;
@ -14,6 +31,10 @@
margin-bottom: 3rem;
}
/**
* @brief Styling für das Helden-Icon.
* @details Definiert Größe, Form, Hintergrundverlauf und Animation des Icons.
*/
.impressum__hero-icon {
display: inline-flex;
align-items: center;
@ -46,6 +67,10 @@
line-height: 1.7;
}
/**
* @brief Grid-Layout für die Impressum-Karten.
* @details Definiert ein zweispaltiges Layout mit Abstand zwischen den Karten.
*/
.impressum__grid {
display: grid;
grid-template-columns: 1fr 1fr;
@ -53,6 +78,10 @@
margin-bottom: 1.5rem;
}
/**
* @brief Styling für die Impressum-Karten.
* @details Beinhaltet Hintergrund, Rahmen, Schatten und Animationseffekte beim Hover.
*/
.impressum__card {
background: var(--bg-card);
border: 1px solid var(--border-default);
@ -89,6 +118,10 @@
grid-column: 1 / -1;
}
/**
* @brief Styling für Icons innerhalb der Impressum-Karten.
* @details Definiert Größe, Ausrichtung und Basis-Design der runden Icons.
*/
.impressum__card-icon {
display: inline-flex;
align-items: center;
@ -100,26 +133,31 @@
font-size: 1.25rem;
}
/** @brief Modifikator für blaue Icons (Server-Betreiber, Datenschutz) */
.impressum__card-icon--blue {
background: var(--color-primary-soft);
border: 1px solid rgba(59, 130, 246, 0.15);
}
/** @brief Modifikator für grüne Icons (Website-Inhaber) */
.impressum__card-icon--green {
background: var(--color-accent-soft);
border: 1px solid rgba(16, 185, 129, 0.15);
}
/** @brief Modifikator für orange Icons (Haftungsausschluss) */
.impressum__card-icon--orange {
background: rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.15);
}
/** @brief Modifikator für lila Icons (Projektinfo) */
.impressum__card-icon--purple {
background: rgba(139, 92, 246, 0.1);
border: 1px solid rgba(139, 92, 246, 0.15);
}
/** @brief Modifikator für rote Icons (Urheberrecht) */
.impressum__card-icon--red {
background: var(--color-danger-soft);
border: 1px solid rgba(239, 68, 68, 0.15);
@ -195,7 +233,10 @@
line-height: 1.7;
}
/* ─── Responsive ─── */
/**
* @brief Media Query für mobile Endgeräte.
* @details Bricht das zwei-spaltige Layout bei Bildschirmen unter 700px in ein ein-spaltiges Layout um.
*/
@media (max-width: 700px) {
.impressum__grid {
grid-template-columns: 1fr;
@ -221,6 +262,12 @@
<main class="impressum">
<!-- /**
* @section Hero-Bereich
* @brief Einleitende Sektion der Seite.
* @details Beinhaltet das animierte Waage-Icon, den Haupttitel und einen kurzen Erklärungstext
* zu den rechtlichen Grundlagen des Impressums.
*/ -->
<!-- ═══ Hero ═══ -->
<div class="impressum__hero">
<div class="impressum__hero-icon">⚖️</div>
@ -228,8 +275,20 @@
<p>Angaben gemäß § 5 TMG und § 25 MedienG Informationen über die Betreiber dieser Website.</p>
</div>
<!-- /**
* @section Karten-Grid
* @brief Hauptbereich mit den rechtlichen Informationen aufgeteilt in Informationskarten.
* @details Verwendet ein zweispaltiges CSS-Grid zur Darstellung unterschiedlicher Bereiche
* wie Seitenbetreiber, Server und Haftungsausschluss.
*/ -->
<!-- ═══ Karten-Grid ═══ -->
<div class="impressum__grid">
<!-- /**
* @subsection Website-Inhaber
* @brief Karte mit den Kontaktinformationen der Inhaber.
* @details Nimmt die volle Breite ein und listet die Details der Projektgründer Fabian Schieder und Paul Eisenbock auf.
*/ -->
<!-- Karte 1 Website-Inhaber (volle Breite) -->
<div class="impressum__card impressum__card--full">
<div class="impressum__card-icon impressum__card-icon--green">🌐</div>
@ -266,6 +325,11 @@
</div>
</div>
<!-- /**
* @subsection Server-Betreiber
* @brief Information zum technischen Hosting der Plattform.
* @details Nennt den Verantwortlichen für die technische Bereitstellung, das Hosting und die Wartung des Servers.
*/ -->
<!-- Karte 2 Server-Betreiber -->
<div class="impressum__card impressum__card--full">
<div class="impressum__card-icon impressum__card-icon--blue">🖥️</div>
@ -282,6 +346,11 @@
</ul>
</div>
<!-- /**
* @subsection Über das Projekt
* @brief Beschreibung des Projekts Geizkragen.
* @details Gibt Hintergrundinformationen, dass das Projekt als Schulprojekt zur Preisüberwachung entstanden ist.
*/ -->
<!-- Karte 3 Projektinfo (volle Breite) -->
<div class="impressum__card impressum__card--full">
<div class="impressum__card-icon impressum__card-icon--purple">🚀</div>
@ -293,6 +362,11 @@
</p>
</div>
<!-- /**
* @subsection Haftungsausschluss
* @brief Disclaimer bezüglich der angebotenen Inhalte.
* @details Schließt die Haftung für Richtigkeit, Vollständigkeit und Aktualität der Inhalte sowie für verlinkte externe Inhalte aus.
*/ -->
<!-- Karte 4 Haftungsausschluss -->
<div class="impressum__card">
<div class="impressum__card-icon impressum__card-icon--orange">⚠️</div>
@ -308,6 +382,11 @@
</p>
</div>
<!-- /**
* @subsection Urheberrecht
* @brief Regelungen bezüglich der erstellten Werke.
* @details Weist auf das österreichische Urheberrecht hin und klärt, dass eine Nutzung der Inhalte durch Dritte schriftlich genehmigt werden muss.
*/ -->
<!-- Karte 5 Urheberrecht -->
<div class="impressum__card">
<div class="impressum__card-icon impressum__card-icon--red">©</div>
@ -320,6 +399,11 @@
</p>
</div>
<!-- /**
* @subsection Datenschutzhinweis
* @brief Basisinformationen zur Datenerhebung.
* @details Informiert Benutzer über den Umgang mit personenbezogenen Daten und die Freiwilligkeit bei der Datenangabe.
*/ -->
<!-- Karte 6 Datenschutz (volle Breite) -->
<div class="impressum__card impressum__card--full">
<div class="impressum__card-icon impressum__card-icon--blue">🔒</div>
@ -335,6 +419,11 @@
</div>
<!-- /**
* @section Footer-Hinweis
* @brief Abschließender Hinweis mit Information zum Erstellungszeitpunkt.
* @details Generiert dynamisch den aktuellen Monat und das Jahr der letzten Anpassung.
*/ -->
<!-- ═══ Footer-Note ═══ -->
<div class="impressum__footer-note">
<p>
@ -345,4 +434,10 @@
</main>
<?php include 'footer.php'; ?>
<?php
/**
* @brief Einbinden des globalen Footers.
* @details Lädt die Datei footer.php, welche den Abschluss des HTML-Dokuments und weitere globale Elemente enthält.
*/
include 'footer.php';
?>

View File

@ -1,20 +1,97 @@
<?php
/**
* @file index.php
* @brief Hauptseite der Geizkragen-Anwendung.
*
* @details Diese Datei dient als Einstiegspunkt für die Benutzer. Sie lädt die grundlegenden
* Layout-Komponenten, die Navigation und zeigt, abhängig von Such- oder Kategoriefiltern,
* empfohlene Produkte oder gefilterte Listen an.
* @author Geizkragen Team
* @version 1.0.0
*
* @section description_index Beschreibung
* Dies ist die zentrale Steuerungsdatei zur Darstellung der Homepage.
* Über Includes werden Header, Kategorienleiste, Filterleiste und Produktkarten geladen.
*/
/**
* @brief Bindet die Bootstrap-Datei ein, um die grundlegende Konfiguration und Klassenverfügbarkeit sicherzustellen.
* @details Initialisiert Laufzeitumgebung, Datenbankverbindungen, Autoloader
* und andere essentielle Parameter, die für die Laufzeit der Applikation benötigt werden.
*/
require_once __DIR__ . '/lib/bootstrap.php';
?>
<?php include 'header.php'; ?>
<?php include 'catbar.php'; ?>
<?php include 'attrbar.php'; ?>
<?php
/**
* @brief Bindet das Header-Template der Seite ein.
* @details Der Header beinhaltet typischerweise den `<head>`-Bereich, Metadaten,
* CSS-Einbindungen sowie den sichtbaren Navigationskopf inklusive Logo und Menü.
*/
include 'header.php';
?>
<?php
/**
* @brief Bindet die Kategorienleiste (catbar) für die Navigation ein.
* @details Diese Leiste ermöglicht die Hauptnavigation über Produktkategorien.
*/
include 'catbar.php';
?>
<?php
/**
* @brief Bindet die Attribut-/Filterleiste (attrbar) ein.
* @details Erlaubt feingranulare Filterung der angezeigten Produkte nach diversen Eigenschaften,
* wie Preis, Hersteller oder Verfügbarkeit.
*/
include 'attrbar.php';
?>
<?php
/**
* @brief Ermittelt und verarbeitet den Suchbegriff aus der GET-Anfrage, falls vorhanden.
* @details Prüft das Array $_GET auf den Schlüssel 'search'. Falls dieser gesetzt ist,
* wird der Wert als String gecastet und Leerzeichen am Rand durch `trim()` entfernt.
*
* @var string $searchTerm Der bereinigte Suchbegriff für die spätere Datenbankabfrage.
*/
$searchTerm = isset($_GET['search']) ? trim((string)$_GET['search']) : '';
/**
* @brief Ermittelt und verarbeitet die ausgewählte Kategorie aus der GET-Anfrage.
* @details Fallback auf 'all', falls keine Kategorie spezifiziert wurde.
*
* @var string $activeCategory Die aktuell vom Benutzer fokussierte Kategorie.
*/
$activeCategory = isset($_GET['category']) ? $_GET['category'] : 'all';
/**
* @brief Prüft, ob weder ein Suchbegriff noch ein spezifischer Kategoriefilter aktiv ist (Startseiten-Bedingung).
* @details Wenn beides zutrifft, befindet sich der Benutzer auf der "nackten" Startseite
* und bekommt spezielle Werbeanzeigen oder Empfehlungen präsentiert.
*/
if ($searchTerm === '' && $activeCategory === 'all') {
/**
* @brief Bindet die Empfehlungen bzw. das Werbebanner ein.
* @details Dient zur Kundenbindung auf der Startseite.
*/
include 'ad_recommendation.php';
}
?>
<?php include 'compcards.php'; ?>
<?php
/**
* @brief Bindet die Produktkarten zur Darstellung ein.
* @details Zentrale Komponente zur Visualisierung der Produkte. Zieht Parameter wie
* `$searchTerm` und `$activeCategory` heran, um die gefilterte Liste zu generieren.
*/
include 'compcards.php';
?>
<?php include 'footer.php'; ?>
<?php
/**
* @brief Bindet das Footer-Template der Seite ein.
* @details Beendet das HTML-Dokument und lädt JavaScripts sowie rechtliche Links
* (Impressum, Datenschutz).
*/
include 'footer.php';
?>

350
info/skript.sql Normal file
View File

@ -0,0 +1,350 @@
-- MySQL Script generated by MySQL Workbench
-- Mon Mar 30 22:13:37 2026
-- Model: New Model Version: 1.0
-- MySQL Workbench Forward Engineering
SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
-- -----------------------------------------------------
-- Schema mydb
-- -----------------------------------------------------
-- -----------------------------------------------------
-- Schema FSST
-- -----------------------------------------------------
-- -----------------------------------------------------
-- Schema FSST
-- -----------------------------------------------------
CREATE SCHEMA IF NOT EXISTS `FSST` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci ;
USE `FSST` ;
-- -----------------------------------------------------
-- Table `FSST`.`attributes`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `FSST`.`attributes` (
`attributeID` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
`unit` VARCHAR(50) NULL DEFAULT NULL,
`dataType` VARCHAR(20) NULL DEFAULT NULL,
PRIMARY KEY (`attributeID`))
ENGINE = InnoDB
AUTO_INCREMENT = 70
DEFAULT CHARACTER SET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
-- -----------------------------------------------------
-- Table `FSST`.`brands`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `FSST`.`brands` (
`brandID` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
PRIMARY KEY (`brandID`))
ENGINE = InnoDB
AUTO_INCREMENT = 8
DEFAULT CHARACTER SET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
-- -----------------------------------------------------
-- Table `FSST`.`categories`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `FSST`.`categories` (
`categoryID` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
`parentCategoryID` INT NULL DEFAULT NULL,
PRIMARY KEY (`categoryID`),
INDEX `categories_ibfk_1` (`parentCategoryID` ASC) VISIBLE,
CONSTRAINT `categories_ibfk_1`
FOREIGN KEY (`parentCategoryID`)
REFERENCES `FSST`.`categories` (`categoryID`)
ON DELETE CASCADE)
ENGINE = InnoDB
AUTO_INCREMENT = 26
DEFAULT CHARACTER SET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
-- -----------------------------------------------------
-- Table `FSST`.`categoryAttributes`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `FSST`.`categoryAttributes` (
`categoryID` INT NOT NULL,
`attributeID` INT NOT NULL,
PRIMARY KEY (`categoryID`, `attributeID`),
INDEX `attributeID` (`attributeID` ASC) VISIBLE,
CONSTRAINT `categoryAttributes_ibfk_1`
FOREIGN KEY (`categoryID`)
REFERENCES `FSST`.`categories` (`categoryID`),
CONSTRAINT `categoryAttributes_ibfk_2`
FOREIGN KEY (`attributeID`)
REFERENCES `FSST`.`attributes` (`attributeID`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
-- -----------------------------------------------------
-- Table `FSST`.`users`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `FSST`.`users` (
`userID` INT NOT NULL AUTO_INCREMENT,
`email` VARCHAR(255) NOT NULL,
`passwordHash` VARCHAR(255) NOT NULL,
`displayName` VARCHAR(255) NULL DEFAULT NULL,
`isActive` TINYINT(1) NOT NULL DEFAULT '1',
`createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`profilePicture` VARCHAR(255) NULL DEFAULT NULL,
PRIMARY KEY (`userID`),
UNIQUE INDEX `email` (`email` ASC) VISIBLE)
ENGINE = InnoDB
AUTO_INCREMENT = 45
DEFAULT CHARACTER SET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
-- -----------------------------------------------------
-- Table `FSST`.`notifications`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `FSST`.`notifications` (
`notificationID` INT NOT NULL AUTO_INCREMENT,
`userID` INT NOT NULL,
`title` VARCHAR(255) NOT NULL,
`message` TEXT NOT NULL,
`isRead` TINYINT(1) NOT NULL DEFAULT '0',
`createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`notificationID`),
INDEX `fk_notifications_user` (`userID` ASC) VISIBLE,
CONSTRAINT `fk_notifications_user`
FOREIGN KEY (`userID`)
REFERENCES `FSST`.`users` (`userID`)
ON DELETE CASCADE)
ENGINE = InnoDB
AUTO_INCREMENT = 4
DEFAULT CHARACTER SET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
-- -----------------------------------------------------
-- Table `FSST`.`products`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `FSST`.`products` (
`productID` INT NOT NULL AUTO_INCREMENT,
`categoryID` INT NOT NULL,
`brandID` INT NOT NULL,
`model` VARCHAR(255) NOT NULL,
`description` TEXT NULL DEFAULT NULL,
`imagePath` VARCHAR(255) NULL DEFAULT NULL,
PRIMARY KEY (`productID`),
INDEX `categoryID` (`categoryID` ASC) VISIBLE,
INDEX `brandID` (`brandID` ASC) VISIBLE,
CONSTRAINT `products_ibfk_1`
FOREIGN KEY (`categoryID`)
REFERENCES `FSST`.`categories` (`categoryID`),
CONSTRAINT `products_ibfk_2`
FOREIGN KEY (`brandID`)
REFERENCES `FSST`.`brands` (`brandID`))
ENGINE = InnoDB
AUTO_INCREMENT = 1347
DEFAULT CHARACTER SET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
-- -----------------------------------------------------
-- Table `FSST`.`shops`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `FSST`.`shops` (
`shopID` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
`website` VARCHAR(255) NULL DEFAULT NULL,
`logoPath` VARCHAR(255) NULL DEFAULT NULL,
`shippingTime` VARCHAR(255) NULL DEFAULT NULL,
PRIMARY KEY (`shopID`))
ENGINE = InnoDB
AUTO_INCREMENT = 7
DEFAULT CHARACTER SET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
-- -----------------------------------------------------
-- Table `FSST`.`offers`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `FSST`.`offers` (
`offerID` INT NOT NULL AUTO_INCREMENT,
`productID` INT NOT NULL,
`shopID` INT NOT NULL,
`price` DECIMAL(10,2) NOT NULL,
`shippingCost` DECIMAL(10,2) NULL DEFAULT NULL,
`inStock` TINYINT(1) NULL DEFAULT NULL,
`offerURL` VARCHAR(255) NULL DEFAULT NULL,
PRIMARY KEY (`offerID`),
INDEX `productID` (`productID` ASC) VISIBLE,
INDEX `shopID` (`shopID` ASC) VISIBLE,
CONSTRAINT `offers_ibfk_1`
FOREIGN KEY (`productID`)
REFERENCES `FSST`.`products` (`productID`),
CONSTRAINT `offers_ibfk_2`
FOREIGN KEY (`shopID`)
REFERENCES `FSST`.`shops` (`shopID`))
ENGINE = InnoDB
AUTO_INCREMENT = 10
DEFAULT CHARACTER SET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
-- -----------------------------------------------------
-- Table `FSST`.`priceAlerts`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `FSST`.`priceAlerts` (
`alertID` INT NOT NULL AUTO_INCREMENT,
`userID` INT NOT NULL,
`productID` INT NOT NULL,
`targetPrice` DECIMAL(10,2) NOT NULL,
`isActive` TINYINT(1) NOT NULL DEFAULT '1',
`createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`alertID`),
INDEX `fk_pricealerts_user` (`userID` ASC) VISIBLE,
INDEX `fk_pricealerts_product` (`productID` ASC) VISIBLE,
CONSTRAINT `fk_pricealerts_product`
FOREIGN KEY (`productID`)
REFERENCES `FSST`.`products` (`productID`)
ON DELETE CASCADE,
CONSTRAINT `fk_pricealerts_user`
FOREIGN KEY (`userID`)
REFERENCES `FSST`.`users` (`userID`)
ON DELETE CASCADE)
ENGINE = InnoDB
AUTO_INCREMENT = 4
DEFAULT CHARACTER SET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
-- -----------------------------------------------------
-- Table `FSST`.`productAttributes`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `FSST`.`productAttributes` (
`productID` INT NOT NULL,
`attributeID` INT NOT NULL,
`valueString` VARCHAR(255) NULL DEFAULT NULL,
`valueNumber` DECIMAL(10,2) NULL DEFAULT NULL,
`valueBool` TINYINT(1) NULL DEFAULT NULL,
PRIMARY KEY (`productID`, `attributeID`),
INDEX `attributeID` (`attributeID` ASC) VISIBLE,
CONSTRAINT `productAttributes_ibfk_1`
FOREIGN KEY (`productID`)
REFERENCES `FSST`.`products` (`productID`),
CONSTRAINT `productAttributes_ibfk_2`
FOREIGN KEY (`attributeID`)
REFERENCES `FSST`.`attributes` (`attributeID`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
-- -----------------------------------------------------
-- Table `FSST`.`reviews`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `FSST`.`reviews` (
`reviewID` INT NOT NULL AUTO_INCREMENT,
`userID` INT NOT NULL,
`productID` INT NOT NULL,
`rating` INT NOT NULL,
`comment` TEXT NULL DEFAULT NULL,
`createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`reviewID`),
INDEX `fk_reviews_user` (`userID` ASC) VISIBLE,
INDEX `fk_reviews_product` (`productID` ASC) VISIBLE,
CONSTRAINT `fk_reviews_product`
FOREIGN KEY (`productID`)
REFERENCES `FSST`.`products` (`productID`)
ON DELETE CASCADE,
CONSTRAINT `fk_reviews_user`
FOREIGN KEY (`userID`)
REFERENCES `FSST`.`users` (`userID`)
ON DELETE CASCADE)
ENGINE = InnoDB
AUTO_INCREMENT = 42
DEFAULT CHARACTER SET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
-- -----------------------------------------------------
-- Table `FSST`.`roles`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `FSST`.`roles` (
`roleID` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL,
PRIMARY KEY (`roleID`),
UNIQUE INDEX `name` (`name` ASC) VISIBLE)
ENGINE = InnoDB
AUTO_INCREMENT = 4
DEFAULT CHARACTER SET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
-- -----------------------------------------------------
-- Table `FSST`.`userFavorites`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `FSST`.`userFavorites` (
`userID` INT NOT NULL,
`productID` INT NOT NULL,
`createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`userID`, `productID`),
INDEX `fk_favorites_product` (`productID` ASC) VISIBLE,
CONSTRAINT `fk_favorites_product`
FOREIGN KEY (`productID`)
REFERENCES `FSST`.`products` (`productID`)
ON DELETE CASCADE,
CONSTRAINT `fk_favorites_user`
FOREIGN KEY (`userID`)
REFERENCES `FSST`.`users` (`userID`)
ON DELETE CASCADE)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
-- -----------------------------------------------------
-- Table `FSST`.`userRoles`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `FSST`.`userRoles` (
`userID` INT NOT NULL,
`roleID` INT NOT NULL,
PRIMARY KEY (`userID`, `roleID`),
INDEX `fk_userroles_role` (`roleID` ASC) VISIBLE,
CONSTRAINT `fk_userroles_role`
FOREIGN KEY (`roleID`)
REFERENCES `FSST`.`roles` (`roleID`)
ON DELETE CASCADE,
CONSTRAINT `fk_userroles_user`
FOREIGN KEY (`userID`)
REFERENCES `FSST`.`users` (`userID`)
ON DELETE CASCADE)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
-- -----------------------------------------------------
-- Table `FSST`.`userSessions`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `FSST`.`userSessions` (
`sessionID` VARCHAR(128) NOT NULL,
`userID` INT NOT NULL,
`expiresAt` TIMESTAMP NOT NULL,
PRIMARY KEY (`sessionID`),
INDEX `fk_sessions_user` (`userID` ASC) VISIBLE,
CONSTRAINT `fk_sessions_user`
FOREIGN KEY (`userID`)
REFERENCES `FSST`.`users` (`userID`)
ON DELETE CASCADE)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
SET SQL_MODE=@OLD_SQL_MODE;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;

View File

@ -1,19 +1,48 @@
<?php
/**
* @file bootstrap.php
* @brief Zentrale Initialisierung der Anwendung
* @brief Zentrale Initialisierungsdatei der Anwendung.
*
* Startet die Session, lädt die Datenbankverbindung
* und aktualisiert die Benutzerrollen.
* @details Diese Datei dient als Bootstrap-Skript für die gesamte Webanwendung.
* Sie ist dafür verantwortlich, grundlegende Konfigurationen vorzunehmen,
* die Session-Verwaltung zu initiieren und die Datenbankverbindung herzustellen.
* Zudem werden hier die Rollen eines aktuell angemeldeten Benutzers bei jedem
* Request frisch aus der Datenbank geladen und in der Session gespeichert,
* um eine konsistente und sichere Rechteverwaltung zu gewährleisten.
*
* @author Automatisch generierter Doxygen-Header / Erweitert durch Entwickler
* @date 2026-04-04
*/
/**
* Einbinden der grundlegenden Datenbankfunktionen.
*
* Beinhaltet die Funktion db_connect() und weitere für den Datenbankzugriff
* notwendige Konfigurationen.
*/
require_once __DIR__ . '/db.php';
/**
* Aktivierung der Fehlerausgabe für Entwicklungszwecke.
*
* Diese Einstellungen sorgen dafür, dass alle Arten von Fehlern, Warnungen
* und Parse-Fehlern direkt im Browser angezeigt werden. Dies ist besonders
* nützlich während der Entwicklung, sollte aber in einer Produktionsumgebung
* idealerweise deaktiviert oder durch ein entsprechendes Logging ersetzt werden.
*/
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);
/**
* Initialisierung der PHP-Session.
*
* Es wird geprüft, ob bereits eine aktive Session existiert. Falls nicht,
* werden spezifische Cookie-Parameter gesetzt (z.B. eine Lebensdauer von 30 Tagen)
* und die Session wird gestartet. Diese Konfiguration erhöht die Sicherheit
* (mittels httponly und samesite=Lax) und verbessert das Benutzererlebnis
* durch ein langlebiges Session-Cookie.
*/
if (session_status() !== PHP_SESSION_ACTIVE)
{
// Session-Cookie Lifetime auf 30 Tage setzen
@ -29,32 +58,76 @@ if (session_status() !== PHP_SESSION_ACTIVE)
}
// Rollen bei jedem Request aus der DB aktualisieren
/**
* @brief Aktualisierung der Benutzerrollen aus der Datenbank.
*
* @details Dieser Block wird ausgeführt, um zu Beginn jedes HTTP-Requests
* die aktuellen Rechte/Rollen des eingeloggten Nutzers zu evaluieren.
* Ist eine `user_id` in der Session vorhanden, wird eine Datenbankabfrage
* durchgeführt, um alle verknüpften Rollen des entsprechenden Nutzers zu ermitteln.
* Die gefundenen Rollen werden danach als Array in `$_SESSION['user_roles']` gespeichert.
* Ist kein Benutzer eingeloggt, wird das Array leer gelassen.
*/
if (!empty($_SESSION['user_id']))
{
/**
* @var mysqli $__bsConn
* @brief Die aktive Datenbankverbindung für den Bootstrap-Prozess.
*/
$__bsConn = db_connect();
/**
* @var mysqli_stmt|false $__bsStmt
* @brief Prepared Statement zum Abfragen der Benutzerrollen.
*
* Hierbei werden die Tabellen `userRoles` (ur) und `roles` (r) miteinander verknüpft,
* um anhand der `userID` die textuellen Namen der Rollen zu selektieren.
*/
$__bsStmt = $__bsConn->prepare('SELECT r.name FROM userRoles ur JOIN roles r ON r.roleID = ur.roleID WHERE ur.userID = ?');
if ($__bsStmt)
{
/**
* @var int $__bsUid
* @brief Typisierte Benutzer-ID, gecastet aus der Session.
*/
$__bsUid = (int)$_SESSION['user_id'];
// Parameterbindung (Interger) an das Prepared Statement
$__bsStmt->bind_param('i', $__bsUid);
// Ausführung der Abfrage
$__bsStmt->execute();
/**
* @var mysqli_result $__bsResult
* @brief Das Resultset der ausgeführten Datenbankabfrage.
*/
$__bsResult = $__bsStmt->get_result();
// Reset des Rollen-Arrays in der Session
$_SESSION['user_roles'] = [];
/**
* Iteration durch die zurückgegebenen Datensätze und Speicherung in der Session.
*/
while ($__bsRow = $__bsResult->fetch_assoc())
{
$_SESSION['user_roles'][] = $__bsRow['name'];
}
// Schließen des Statements, um Ressourcen freizugeben
$__bsStmt->close();
}
// Schließen der Datenbankverbindung nach erfolgreicher Abfrage
$__bsConn->close();
}
else
{
/**
* Für Gäste (nicht eingeloggte Benutzer) wird das Rollen-Array explizit geleert.
*/
$_SESSION['user_roles'] = [];
}

View File

@ -1,16 +1,72 @@
<?php
// Zentrale Konfiguration
/**
* @file config.php
* @brief Zentrale Konfigurationsdatei für das Geizkragen-Projekt.
*
* @details Diese Datei liefert die Basis-Konfigurationseinstellungen für die gesamte Anwendung,
* insbesondere die erforderlichen Verbindungsdaten für die Datenbank. Die Werte werden primär
* über Umgebungsvariablen (Environment Variables) dynamisch ausgelesen. Sind diese Variablen
* auf dem jeweiligen System nicht gesetzt, wird auf standardisierte Fallback-Werte (Defaults)
* zurückgegriffen. Dies ermöglicht eine sichere und flexible Trennung von Quellcode und
* umgebungsspezifischen Konfigurationen.
*/
// Zentrale Konfiguration der Anwendung
declare(strict_types=1);
/**
* @brief Konfigurations-Array der Anwendung.
*
* @details Gibt ein mehrdimensionales, assoziatives Array zurück, welches alle
* betriebsrelevanten Konfigurationsparameter der Anwendung kapselt. Aktuell umfasst
* dies im Wesentlichen die Datenbank-Konfiguration unter dem Schlüssel 'db'.
*
* @return array Assoziatives Array mit den Systemeinstellungen.
*/
return [
/**
* @brief Datenbank-Konfigurationseinstellungen.
* @details Enthält alle Parameter, die zum Aufbau einer PDO- oder MySQLi-Verbindung notwendig sind.
*/
'db' => [
'host' => getenv('GEIZKRAGEN_DB_HOST') ?: 'HOST',
/**
* @brief Der Hostname oder die IP-Adresse des Datenbankservers.
* @details Ausgelesen aus der Umgebungsvariable 'GEIZKRAGEN_DB_HOST'.
* Fallback ist 'localhost'.
*/
'host' => getenv('GEIZKRAGEN_DB_HOST') ?: 'localhost',
/**
* @brief Der TCP-Port des Datenbankservers.
* @details Ausgelesen aus 'GEIZKRAGEN_DB_PORT'. Der Wert wird für Typsicherheit strikt in
* einen Integer umgewandelt. Fallback ist der Standard-MySQL/MariaDB Port 3306.
*/
'port' => (int)(getenv('GEIZKRAGEN_DB_PORT') ?: 3306),
'user' => getenv('GEIZKRAGEN_DB_USER') ?: 'USER',
'pass' => getenv('GEIZKRAGEN_DB_PASS') ?: 'PASSWORD',
'name' => getenv('GEIZKRAGEN_DB_NAME') ?: 'DATABASE',
/**
* @brief Der Benutzername zur Authentifizierung an der Datenbank.
* @details Ausgelesen aus 'GEIZKRAGEN_DB_USER'. Fallback ist 'FSST'.
*/
'user' => getenv('GEIZKRAGEN_DB_USER') ?: 'FSST',
/**
* @brief Das Passwort zur Authentifizierung an der Datenbank.
* @details Ausgelesen aus 'GEIZKRAGEN_DB_PASS'. Das Fallback-Passwort dient nur für lokale
* oder vordefinierte Entwicklungsumgebungen.
*/
'pass' => getenv('GEIZKRAGEN_DB_PASS') ?: 'L9wUNZZ9Qkbt',
/**
* @brief Der Name der verwendeten Datenbankstruktur/Schema.
* @details Ausgelesen aus 'GEIZKRAGEN_DB_NAME'. Fallback ist 'FSST'.
*/
'name' => getenv('GEIZKRAGEN_DB_NAME') ?: 'FSST',
/**
* @brief Der verwendete Zeichensatz (Charset) für die Datenbankverbindung.
* @details Ausgelesen aus 'GEIZKRAGEN_DB_CHARSET'. Fallback ist 'utf8mb4',
* was empfohlen wird, um vollständige Unicode-Unterstützung (inkl. Emojis) zu garantieren.
*/
'charset' => getenv('GEIZKRAGEN_DB_CHARSET') ?: 'utf8mb4',
],
];

View File

@ -1,36 +1,107 @@
<?php
declare(strict_types=1); // Erzwingt strikte Typenprüfung für Parameter und Rückgabewerte -> keine automatische Typenumwandlung
/**
* @file db.php
* @brief Enthält grundlegende Funktionen und Initialisierungen für die Datenbankinteraktion.
*
* @details Diese Datei kapselt den gesamten Prozess des Verbindungsaufbaus zur MySQL/MariaDB-Datenbank.
* Sie nutzt die zentralen Konfigurationseinstellungen (wie Host, Benutzer, Passwort, Datenbankname und Port),
* die in der Datei config.php hinterlegt sind. Durch die Auslagerung in diese Datei wird eine einheitliche
* und zentrale Stelle für das Verbindungsmanagement geschaffen, was die Wartbarkeit, Sicherheit und
* Erweiterbarkeit des Gesamtsystems signifikant verbessert.
*
* Besonderes Augenmerk liegt auf der Fehlerbehandlung beim Verbindungsaufbau: Schlägt dieser fehl,
* so wird der Ausführungskontext sicher terminiert und ein HTTP-Statuscode 500 generiert. Zudem wird
* der Zeichensatz streng konfiguriert (meist utf8mb4), was für die fehlerfreie Verarbeitung von
* Sonderzeichen und Emojis essenziell ist und Schutz vor bestimmten SQL-Injection-Vektoren bietet.
*
* @author Geizkragen Entwicklerteam
* @version 1.1
* @date 2026-04-04
*/
declare(strict_types=1); ///< Erzwingt auf Sprachebene strikte Typenprüfung für Parameter und Rückgabewerte dieses Skripts. Dies verhindert fehleranfällige, implizite automatische Typenumwandlungen.
/**
* Liefert eine MySQLi-Verbindung anhand der zentralen Konfiguration.
* @brief Baut eine MySQLi-Datenbankverbindung anhand der zentralen Konfiguration auf und liefert diese zurück.
*
* Nutzung:
* $conn = db_connect();
* @details Diese essenzielle Funktion greift auf die zentrale Konfigurationsdatei (config.php) zu,
* lädt deren Parameterstruktur und initialisiert eine neue Instanz der Klasse mysqli, sofern dies
* für den aktuellen Ausführungskontext notwendig ist. Um Performance zu optimieren, nutzt sie
* das Singleton-ähnliche Konzept mittels einer statischen Variablen, sodass die Konfiguration
* (und die Datei-Lese-Operation) nur genau einmal pro Skriptlauf ausgeführt wird.
*
* @attention Bei einem Verbindungsfehler bricht das System das Skript mittels die() hart ab.
* Sensible Datenbankfehler (wie falsche Zugangsdaten) werden dabei absichtlich nicht ausgegeben,
* um so genannte Information Disclosure-Vulnerabilities zu vermeiden. Stattdessen wird lediglich
* der neutrale Text "Datenbankfehler" gezeigt und der HTTP Header auf Status 500 gesetzt.
*
* @b Anwendungsbeispiel:
* @code
* // Verbindung zur Datenbank aufbauen
* $mysqli_connection = db_connect();
*
* // Abfrage ausführen
* $result = $mysqli_connection->query("SELECT * FROM users");
* @endcode
*
* @return mysqli Das erfolgreich aufgebaute und vollständig konfigurierte Datenbankverbindungs-Objekt.
*
* @throws Exception Löst im Hintergrund potenziell Ausnahmen aus, fängt diese durch manuelle Fehlerabfragen ($conn->connect_error) aber sauber ab.
*/
function db_connect(): mysqli
{
/**
* @var array|null $cfg Statische Variable zur Wiederverwendung der Systemkonfiguration.
* Durch das Schlüsselwort 'static' behält diese Variable ihren Wert auch über
* multiple Funktionsaufrufe hinweg und verhindert mehrfaches Einlesen der config.php.
*/
static $cfg;
/// Prüft, ob die Konfiguration im aktuellen Skriptlauf bereits geladen und im Speicher abgelegt wurde.
if ($cfg === null)
{
/** @var array{db: array{host:string,port:int,user:string,pass:string,name:string,charset:string}} $cfg */
/**
* @var array{db: array{host:string,port:int,user:string,pass:string,name:string,charset:string}} $cfg
* @brief Bindet die Konfigurationsdaten ein und lädt das rückgegebene Array in die statische Variable $cfg.
* Der Dateipfad wird hierbei relativ und sicher über die magische Konstante __DIR__ dynamisch aufgelöst.
*/
$cfg = require __DIR__ . '/config.php';
}
/// Extrahiert aus Leistungs- und Lesbarkeitsgründen den spezifischen Datenbankbereich der geladenen Gesamt-Konfiguration in die lokale Variable $db.
$db = $cfg['db'];
/// Initialisiert direkt eine neue, native mysqli-Verbindung mit den extrahierten Zugangsdaten (Host, Benutzer, Passwort, DB-Name, Port).
$conn = new mysqli($db['host'], $db['user'], $db['pass'], $db['name'], $db['port']);
/**
* @brief Überprüft unmittelbar, ob beim Versuch des Verbindungsaufbaus durch die MySQLi-Extension ein Fehler aufgetreten ist.
* Wenn der Wert von $conn->connect_error einen String enthält (und somit nicht null ist), war der Verbindungsversuch erfolglos.
*/
if ($conn->connect_error)
{
/**
* @brief Ein fataler Fehler ist aufgetreten: Setzt den HTTP-Response-Statuscode auf 500 (Internal Server Error).
* Dies teilt dem Browser oder API-Konsumenten strukturiert mit, dass der Server ein internes Problem hat.
*/
http_response_code(500);
/**
* @brief Bricht die Ausführung des gesamten PHP-Skripts unwiderruflich ab.
* Aus Sicherheitsgründen wird dem Client lediglich eine maskierte, generelle String-Fehlermeldung ("Datenbankfehler") ausgegeben.
*/
die('Datenbankfehler');
}
// Einheitliches Charset (wichtig für Umlaute/Emojis & Sicherheit)
/**
* @brief Setzt für die nun erfolgreich initiierte Verbindung ein einheitliches, konfiguriertes Charset.
* Dies ist (insbesondere bei utf8mb4) aus Sicherheits- und Stabilitätsgründen äußerst wichtig, um Encoding-Probleme,
* abgeschnittene Texte und potenzielle SQL-Sicherheitslücken beim Escaping vollständig auszuschließen.
*/
$conn->set_charset($db['charset']);
/// Der Aufbau war erfolgreich. Gibt die einsatzbereite und abgesicherte MySQLi-Klasseninstanz an den aufrufenden Kontext zurück.
return $conn;
}

View File

@ -1,45 +1,83 @@
<?php
/**
* @file strings.php
* @brief Enthält String-Hilfsfunktionen ohne harte Abhängigkeit von Erweiterungen wie mbstring.
*
* @details Diese Datei stellt nützliche Helfer-Funktionen für die Zeichenkettenverarbeitung bereit,
* insbesondere zur Ermittlung der tatsächlichen Zeichenlänge in UTF-8. Das Hauptziel ist es,
* Längenvalidierungen zeichenbasiert anstatt bytebasiert durchzuführen, um korrekte Ergebnisse
* in einer Multibyte-Umgebung wie UTF-8 zu gewährleisten, selbst wenn bestimmte PHP-Extensions fehlen.
*/
// lib/strings.php
// Kleine String-Helper ohne harte Abhängigkeit von mbstring.
// Ziel: Längenvalidierung möglichst „zeichenbasiert“ und UTF-8-tauglich.
/**
* Liefert die Länge eines Strings in Zeichen (nicht Bytes), wenn möglich.
* @brief Ermittelt die Länge eines Strings in tatsächlichen Zeichen (nicht Bytes), wenn möglich.
*
* Priorität:
* 1) mb_strlen (mbstring)
* 2) grapheme_strlen (intl)
* 3) UTF-8 Codepoint-Zählung via PCRE
* 4) Fallback: strlen (Bytes)
* @details Diese Funktion versucht, die genaueste und sicherste Methode zur Bestimmung der
* Zeichenanzahl eines Strings in einer UTF-8-codierten Multibyte-Zeichenkette zu verwenden.
* Dabei bedient sie sich mehrerer Mechanismen in einer festgelegten Prioritätenfolge:
* - Priorität 1: Nutzung von mb_strlen() (sofern die mbstring-Erweiterung aktiv ist).
* - Priorität 2: Nutzung von grapheme_strlen() (sofern die intl-Erweiterung aktiv ist).
* - Priorität 3: Nutzung regulärer Ausdrücke zur UTF-8 Codepoint-Zählung via PCRE.
* - Priorität 4: Fallback auf die native strlen()-Funktion (welche lediglich Bytes zählt, was
* bei Multibyte-Zeichen ungenau ist, aber sicher nicht fehlschlägt).
*
* @param string $s Die Eingabe-Zeichenkette, deren Länge bestimmt werden soll.
*
* @return int Die ermittelte Länge der Zeichenkette. Im Idealfall entspricht dies der Anzahl der Zeichen.
* Im Fallback-Szenario entspricht dies der Anzahl der Bytes.
*/
function str_length(string $s): int
{
/**
* Prüfen, ob die Funktion mb_strlen verfügbar ist.
* Dies ist die sicherste und schnellste Methode, da sie für Multibyte-Strings gemacht ist.
*/
if (function_exists('mb_strlen'))
{
// Rückgabe der Zeichenlänge unter expliziter Angabe des Encodings UTF-8.
return (int)mb_strlen($s, 'UTF-8');
}
/**
* Falls mb_strlen nicht verfügbar ist, prüfen wir, ob die intl-Extension aktiviert ist
* und grapheme_strlen zur Verfügung stellt.
*/
if (function_exists('grapheme_strlen'))
{
// Ermittlung der Anzahl der Grapheme in der Zeichenfolge.
$len = grapheme_strlen($s);
// Überprüfung, ob ein gültiges Ergebnis zurückgeliefert wurde (nicht false).
if ($len !== false)
{
return (int)$len;
}
}
// UTF-8 Codepoints zählen (best-effort ohne Extensions)
/**
* Falls weder mb_strlen noch grapheme_strlen nutzbar sind,
* versuchen wir die UTF-8 Codepoints manuell via Regex (PCRE) zu zählen.
*/
$m = [];
// Der Modifier 'u' steht für UTF-8-Behandlung, 's' sorgt dafür, dass der Punkt (.) auch Zeilenumbrüche matched.
$ok = @preg_match_all('/./us', $s, $m);
// Wenn preg_match_all erfolgreich war, enthält $ok die Anzahl der Matches (also die Zeichenlänge).
if ($ok !== false)
{
return (int)$ok;
}
// Letzter Fallback (Bytes)
/**
* Letzter Fallback-Mechanismus:
* Wenn alle UTF-8 spezifischen Ansätze fehlschlagen, zählen wir einfach die Bytes.
* Dies kann bei Zeichenketten mit Sonderzeichen zu längeren Ergebnissen führen,
* stellt aber sicher, dass in jedem Fall eine Ganzzahl (int) zurückgegeben wird.
*/
return strlen($s);
}

108
login.php
View File

@ -1,103 +1,180 @@
<?php
/**
* @file login.php
* @brief Steuert den Login-Vorgang und baut eine Benutzersitzung auf.
*
* @details Enthält das Formular und die Verarbeitung des POST-Requests,
* verifiziert Passwort-Hashes, veranlasst ggf. einen Re-Hash und speichert Attribute in der Session.
*/
// login.php
/**
* @brief Bindet die Bootstrap-Datei ein, um Session und Basiskonfigurationen zu gewährleisten.
*/
require_once __DIR__ . '/lib/bootstrap.php';
// 1) DB-Verbindung (einmal)
/**
* 1) DB-Verbindung (einmal)
* @brief Stellt eine Datenbankverbindung her.
* @var mysqli $conn Die etablierte Datenbankverbindung.
*/
$conn = db_connect();
// 2) POST-Verarbeitung VOR jeglicher Ausgabe
/**
* 2) POST-Verarbeitung VOR jeglicher Ausgabe
* @brief Initialisierung von Fehler- und Infomeldungen.
*/
/** @var string|null $loginError Speichert mögliche Fehlermeldungen während des Login-Prozesses. */
$loginError = null;
/** @var string|null $loginInfo Speichert mögliche Informationsmeldungen für den Benutzer aus. */
$loginInfo = null;
/**
* @brief Prüft, ob der Nutzer frisch von der Registrierungsseite kommt.
* @details Die Variable $_GET['registered'] wird gesetzt, wenn eine erfolgreiche Weiterleitung von register.php stattfand.
*/
if (isset($_GET['registered']) && $_GET['registered'] === '1')
{
/** @brief Setzt eine Info-Nachricht für die UI, dass die Registrierung erfolgreich war. */
$loginInfo = 'Registrierung erfolgreich. Du kannst dich jetzt einloggen.';
}
/**
* @brief Fängt das Formular-Submit ab, wenn die Anfrage-Methode POST ist.
* @details Überprüft, ob Daten aus dem Formular abgesendet wurden.
*/
if ($_SERVER['REQUEST_METHOD'] === 'POST')
{
/** @var string $uname Speichert den eingegebenen Nutzernamen vor der Validierung. */
$uname = '';
/**
* @brief Prüft, ob das Feld `uname` übermittelt wurde und weist es zu.
*/
if (isset($_POST['uname']))
{
/** @details Führt zudem ein trim() aus, um überschüssige Leerzeichen am Anfang und Ende zu entfernen. */
$uname = trim($_POST['uname']);
}
/**
* @brief Prüft, ob das Feld `pw` übermittelt wurde und weist es zu.
*/
if (isset($_POST['pw']))
{
/** @var string $pw Speichert das eingegebene Passwort im Klartext. */
$pw = $_POST['pw'];
}
else
{
/** @details Falls kein Passwort übergeben wurde, wird ein leerer String zugewiesen. */
$pw = '';
}
// Basic Validierung
/**
* @brief Basic Validierung vor dem Datenbank-Look-Up.
* @details Prüft, ob Nutzername oder Passwort komplett leer sind.
*/
if ($uname === '' || $pw === '')
{
/** @brief Fehler-Status setzen, wenn mindestens eins der benötigten Felder leer ist. */
$loginError = "Bitte Username und Passwort eingeben.";
}
else
{
// Login ist SELECT, mit Prepared Statement (sicher) und ?-Platzhalter
/**
* @brief Login ist SELECT, mit Prepared Statement (sicher) und ?-Platzhalter
* @details Es werden die Felder userID, displayName und passwordHash gesucht, um den Nutzer später authentifizieren zu können.
* @var mysqli_stmt|false $stmt Das vorbereitete Query-Statement.
*/
$stmt = mysqli_prepare(
$conn,
"SELECT userID, displayName, passwordHash FROM users WHERE displayName = ? LIMIT 1"
);
/// Prüft, ob beim Vorbereiten des Statements ein Fehler geschah.
if (!$stmt)
{
$loginError = "Datenbankfehler.";
}
else
{
/// Bindet den gesuchten Nutzernamen als String (`s`) an das SQL-Statement.
mysqli_stmt_bind_param($stmt, "s", $uname);
/// Führt die Datenbankabfrage aus.
mysqli_stmt_execute($stmt);
/// @var mysqli_result|false $result Das ResultSet der Select-Abfrage.
$result = mysqli_stmt_get_result($stmt);
/** @var array|null $user Enthält die abgerufenen Datensätze aus der DB. */
$user = null;
/// Sofern ein Resultat gefunden wurde, wird die erste Zeile assoziativ in `$user` geschrieben.
if ($result)
{
$user = mysqli_fetch_assoc($result);
}
// Passwort prüfen: Eingabe gegen gespeicherten Hash (password_hash/password_verify)
/**
* @brief Passwort prüfen: Eingabe gegen gespeicherten Hash (password_hash/password_verify)
*/
if ($user && password_verify($pw, $user['passwordHash']))
{
// Optional: Rehash, falls Algorithmus/Cost geändert wurde
/**
* @brief Optional: Rehash, falls Algorithmus/Cost geändert wurde.
* @details Überprüft, ob der bestehende Hash nicht mehr den aktuellen Anforderungen von PASSWORD_DEFAULT entspricht.
*/
if (password_needs_rehash($user['passwordHash'], PASSWORD_DEFAULT))
{
/** @var string $newHash Erzeugt einen neuen Hash mit dem aktuellen Standard-Algorithmus. */
$newHash = password_hash($pw, PASSWORD_DEFAULT);
/** @var mysqli_stmt|false $upd Bereitet das Statement für das Update in der DB vor. */
$upd = mysqli_prepare($conn, "UPDATE users SET passwordHash = ? WHERE userID = ?");
if ($upd)
{
/// @var int $userID Konvertiert die ID sicher in einen Integer.
$userID = (int)$user['userID'];
/// Bindet Parameter für das Update-Query (s = string, i = int).
mysqli_stmt_bind_param($upd, "si", $newHash, $userID);
/// Führt das Update auf den Passworthash durch.
mysqli_stmt_execute($upd);
/// Schließt das Update-Statement.
mysqli_stmt_close($upd);
}
}
/**
* @brief Setzt die Session-Werte zur Authentifizierung des Nutzers.
*/
$_SESSION['user_id'] = (int)$user['userID'];
$_SESSION['displayName'] = $user['displayName'];
/** @brief Schließt das Query-Statement und die DB-Verbindung (falls nicht gepoolt) sauber ab. */
mysqli_stmt_close($stmt);
mysqli_close($conn);
/**
* @brief Leitet den Benutzer ins Profil/Konto weiter.
* @details Bricht zudem die Skriptausführung mittels exit ab, nachdem der Header gesendet wurde.
*/
header("Location: account.php");
exit;
}
$loginError = "Ungültige Zugangsdaten.";
/**
* @brief Fallback, falls Authentifizierung am PW oder fehlendem Nutzer scheitert.
*/
$loginError = "Ungltige Zugangsdaten.";
mysqli_stmt_close($stmt);
}
}
}
/**
* @brief Bindet die Header-HTML-Komponente ein.
*/
include 'header.php';
?>
@ -108,12 +185,16 @@ include 'header.php';
<h2 class="auth__title">Login</h2>
</header>
<?php if ($loginInfo): ?>
<?php
/** @brief Rendert optional Erfolgsnachrichten (z.B. nach Registrierung). */
if ($loginInfo): ?>
<p class="auth__alert__sucess"
role="status"><?php echo htmlspecialchars($loginInfo, ENT_QUOTES, 'UTF-8'); ?></p>
<?php endif; ?>
<?php if ($loginError): ?>
<?php
/** @brief Rendert optional aufgetretene Fehler beim Login-Versuch. */
if ($loginError): ?>
<p class="auth__alert__error"
role="alert"><?php echo htmlspecialchars($loginError, ENT_QUOTES, 'UTF-8'); ?></p>
<?php endif; ?>
@ -142,6 +223,13 @@ include 'header.php';
</main>
<?php
/**
* @brief Schließt die Datenbankbindung, bevor der Footer geladen und die Seite an den Client geschickt wird.
*/
mysqli_close($conn);
/**
* @brief Bindet die Footer-HTML-Komponente ein.
*/
include 'footer.php';
?>

View File

@ -1,12 +1,36 @@
<?php
/**
* @file logout.php
* @brief Beendet die Sitzung des Benutzers.
*
* @details Diese Datei setzt alle Session-Parameter zurück, löscht das Session-Cookie
* und leitet den Benutzer auf die Anmeldeseite um.
*/
/**
* @brief Bindet die Bootstrap-Datei ein, um Session-Start und Basiskonfigurationen zu gewährleisten.
*/
require_once __DIR__ . '/lib/bootstrap.php';
/* Alle Session-Variablen löschen */
/**
* @brief Leert das komplette Session-Array.
* @details Setzt `$_SESSION` als leeres Array, um alle nutzerbezogenen Daten der Sitzung zu entfernen.
*/
$_SESSION = [];
/* Session-Cookie löschen (wichtig!) */
/**
* @brief Prüft, ob Sessions über Cookies abgewickelt werden.
* @details Falls ja, wird das dazugehörige Session-Cookie aktiv im Client des Benutzers invalidiert.
*/
if (ini_get("session.use_cookies")) {
/**
* @var array $params Liest die aktuellen Parameter zum Verwalten der Cookies aus.
*/
$params = session_get_cookie_params();
/**
* @brief Setzt ein invalides und abgelaufenes Cookie, um das bestehende Cookie des Nutzers zu verwerfen.
*/
setcookie(
session_name(),
'',
@ -18,13 +42,22 @@ if (ini_get("session.use_cookies")) {
);
}
/* Session zerstören */
/**
* @brief Zerstört die serverseitige Sitzung final.
*/
session_destroy();
/* Optional: Remember-Me Cookie löschen (falls vorhanden)
/* Optional: Remember-Me Cookie lschen (falls vorhanden)
setcookie("remember_token", "", time() - 3600, "/");
*/
/**
* @brief Weiterleitung auf die Login-Seite.
*/
header("Location: login.php");
exit();
/**
* @brief Stoppt das Skript nach erfolgtem Redirect.
*/
exit();
?>

View File

@ -1,13 +0,0 @@
<html>
<body>
<h2>TestProjekt-Login</h2>
<form action="login.php" method="POST">
<label for="fname">Username:</label>
<input type="text" id="uname" name="uname"><br>
<label for="lname">Password:</label>
<input type="text" id="pw" name="pw"><br><br>
<input type="submit" value="Login">
</form>
<p><a href="register.html">Register</a></p>
</body>
</html>

View File

@ -1,8 +1,30 @@
<?php
/**
* @file productAdder.php
* @brief Datei productAdder.php zur Anlage neuer Produkte im System.
*
* @details Diese Datei stellt ein Formular sowie die serverseitige Logik bereit,
* um neue Produkte in die Datenbank einzufügen. Dabei werden Kategorien,
* Marken und dynamische Attribute berücksichtigt. Ein Bild-Upload oder
* die Angabe einer Bild-URL sind ebenfalls möglich.
* Automatisch generierter Doxygen-Header wurde erweitert.
*
* @author Fabian
* @date 2026-04-04
*/
// product_add.php
require_once __DIR__ . '/lib/bootstrap.php';
/**
* @section Zugriffskontrolle
* @brief Überprüfung der Administrator-Rechte.
*
* Falls der Benutzer nicht eingeloggt ist oder die Rolle 'ADMIN' nicht besitzt,
* wird der Zugriff mit einem HTTP-Status 403 (Forbidden) verweigert und eine
* Fehlermeldung ausgegeben.
*/
/* =======================
0) Zugriffskontrolle nur ADMIN
======================= */
@ -17,6 +39,13 @@ if (empty($_SESSION['user_id']) || empty($_SESSION['user_roles']) || !in_array('
exit;
}
/**
* @section Kategorie-Auswahl
* @brief Ermittlung der ausgewählten Kategorie-ID.
*
* Die Kategorie-ID wird entweder aus den GET- oder POST-Parametern bezogen.
* Es wird sichergestellt, dass die ID aus Ziffern besteht (ctype_digit).
*/
/* =======================
1) Kategorie aus GET
======================= */
@ -27,11 +56,25 @@ if (isset($_GET['categoryID']) && ctype_digit($_GET['categoryID'])) {
$categoryID = (int)$_POST['categoryID'];
}
/**
* @section Datenbankverbindung
* @brief Initialisierung der Datenbankverbindung.
*
* Nutzt die globale Funktion db_connect() aus bootstrap.php / db.php,
* um eine Verbindung zur MySQL-Datenbank herzustellen.
*/
/* =======================
2) DB-Verbindung
======================= */
$conn = db_connect();
/**
* @section Kategorien-Laden
* @brief Abrufen aller verfügbaren Kategorien.
*
* Die Kategorien werden für das Dropdown-Menü im Formular benötigt
* und alphabetisch sortiert geladen.
*/
/* =======================
3) Kategorien laden
======================= */
@ -45,6 +88,13 @@ while ($row = $result->fetch_assoc()) {
$categories[] = $row;
}
/**
* @section Marken-Laden
* @brief Abrufen aller verfügbaren Marken.
*
* Lädt die Marken alphabetisch sortiert aus der Datenbank,
* damit sie bei der Produkterstellung ausgewählt werden können.
*/
/* =======================
3b) Marken laden
======================= */
@ -58,6 +108,13 @@ while ($row = $result->fetch_assoc()) {
$brands[] = $row;
}
/**
* @section Attribute-Laden
* @brief Abrufen der kategoriespezifischen Attribute.
*
* Falls eine Kategorie ausgewählt wurde ($categoryID > 0), werden
* die zugehörigen Attribute (inklusive Einheit und Datentyp) geladen.
*/
/* =======================
4) Attribute zur Kategorie
======================= */
@ -78,21 +135,35 @@ if ($categoryID > 0) {
}
}
/**
* @section Produkt-Speicherung
* @brief Verarbeitung des abgesendeten Formulars zum Speichern eines Produkts.
*
* Diese Sektion verarbeitet alle übergebenen Formulardaten (Modell, Beschreibung,
* Kategorie, Marke, Bild-Upload/URL, Attribute), validiert sie und fügt das
* neue Produkt in die Datenbank ein.
*/
/* =======================
5) Produkt speichern
======================= */
$saveError = null;
/** @var bool $debugMode Aktiviert den Debug-Modus für zusätzliche Fehlerausgaben, wenn ?debug=1 übergeben wird. */
$debugMode = isset($_GET['debug']) && $_GET['debug'] === '1';
$debugDetails = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['saveProduct'])) {
/** @var string $model Der Name bzw. das Modell des Produkts */
$model = trim($_POST['model']);
/** @var string|null $description Optionale Produktbeschreibung */
$description = $_POST['description'] ?? null;
$categoryID = (int)$_POST['categoryID'];
$brandID = (int)($_POST['brandID'] ?? 0);
/** @var string $imageUrl Optionale Bild-URL */
$imageUrl = trim((string)($_POST['imageUrl'] ?? ''));
/** @var array|null $imageFile Das hochgeladene Dateiobjekt aus $_FILES */
$imageFile = (isset($_FILES['productImage']) && is_array($_FILES['productImage'])) ? $_FILES['productImage'] : null;
/** @var bool $hasUpload Wahr, wenn eine Datei hochgeladen wurde und kein Fehler UPLOAD_ERR_NO_FILE vorliegt */
$hasUpload = $imageFile && isset($imageFile['error']) && (int)$imageFile['error'] !== UPLOAD_ERR_NO_FILE;
$uploadMime = null;
@ -109,6 +180,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['saveProduct'])) {
$debugDetails['post_max_size'] = ini_get('post_max_size');
}
// Validierung der Pflichtfelder und hochgeladenen Dateien
if ($categoryID <= 0) {
$saveError = 'Bitte eine Kategorie auswählen.';
} elseif ($brandID <= 0) {
@ -168,6 +240,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['saveProduct'])) {
}
if ($saveError === null) {
/**
* @brief Produkt anlegen
* Speichert die grundlegenden Produktdaten in der Tabelle 'products'.
*/
// --- Produkt anlegen ---
$stmt = $conn->prepare("
INSERT INTO products (categoryID, brandID, model, description)
@ -186,11 +262,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['saveProduct'])) {
$debugDetails['db_error'] = $stmt->error;
}
} else {
/** @var int $productID Die ID des neu erstellten Produkts (Last Insert ID) */
$productID = $stmt->insert_id;
/** @var string|null $publicImagePath Der öffentliche Pfad zum gespeicherten Produktbild */
$publicImagePath = null;
if ($hasUpload) {
// Verarbeitung des hochgeladenen Bildes und Generierung des Zielpfades
$relativeTargetDir = 'assets/images/products';
$dirTargetDir = rtrim(__DIR__, "\\/") . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relativeTargetDir);
$documentRoot = isset($_SERVER['DOCUMENT_ROOT']) ? (string)$_SERVER['DOCUMENT_ROOT'] : '';
@ -241,6 +320,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['saveProduct'])) {
}
if ($saveError === null && $publicImagePath !== null) {
/**
* @brief Bildpfad aktualisieren
* Aktualisiert den Datensatz des Produkts mit dem korrekten Bildpfad.
*/
$stmtImg = $conn->prepare("UPDATE products SET imagePath = ? WHERE productID = ?");
if ($stmtImg) {
$stmtImg->bind_param("si", $publicImagePath, $productID);
@ -248,6 +331,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['saveProduct'])) {
}
}
/**
* @brief Dynamische Attribute speichern
* Iteriert über die übergebenen Attribute und speichert sie
* typgerecht in der Tabelle 'productAttributes'.
*/
// --- Attribute speichern ---
if (!empty($_POST['attributes'])) {
@ -288,6 +376,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['saveProduct'])) {
}
if ($saveError === null) {
// Nach erfolgreichem Speichern wird die Seite neu geladen (Post/Redirect/Get-Pattern)
header("Location: productAdder.php?categoryID=" . $categoryID);
exit;
}
@ -296,6 +385,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['saveProduct'])) {
}
}
// Inkludiert den Header-Bereich des HTML-Dokuments
include 'header.php';
?>
@ -398,6 +488,9 @@ include 'header.php';
</main>
<?php
/**
* @brief Datenbankverbindung schließen und Footer inkludieren.
*/
$conn->close();
include 'footer.php';
?>

View File

@ -1,11 +1,31 @@
<?php
/**
* @file productpage.php
* @brief Darstellung der Detailseite eines spezifischen Produkts.
*
* @details Diese Datei generiert die Produktdetailseite. Sie umfasst das Laden der Produktdaten
* anhand der übergebenen ID, Behandeln von POST-Anfragen (Bewertung löschen, Wunschliste bearbeiten,
* Vergleiche verwalten) und die Darstellung der zugehörigen Shops, Attribute sowie Bewertungen.
*
* @author System
* @version 1.0
*/
// productpage.php
require_once __DIR__ . '/lib/bootstrap.php';
/**
* @brief Initiiert die Datenbankverbindung.
* @details Stellt eine globale Verbindung zur Datenbank her, um die nachfolgenden Queries ausführen zu können.
*/
// 1) DB-Verbindung (einmal)
$conn = db_connect();
/**
* @brief Holt die Produkt-ID aus dem GET-Parameter.
* @details Validierung der ID, Rückfall auf 0, wenn kein valider Wert übergeben wurde.
*/
$productId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($productId <= 0) {
@ -13,6 +33,10 @@ if ($productId <= 0) {
exit;
}
/**
* @brief Überprüft, ob das Produkt in der Datenbank existiert.
* @details Falls kein Datensatz für die gegebene productID gefunden wird, wird der Nutzer auf eine 404-Seite umgeleitet.
*/
$checkStmt = $conn->prepare("SELECT productID FROM products WHERE productID = ?");
$checkStmt->bind_param("i", $productId);
$checkStmt->execute();
@ -22,6 +46,11 @@ if ($checkResult->num_rows === 0) {
exit;
}
/**
* @brief Behandelt das Löschen von Bewertungen.
* @details Administrator-Nutzer können Bewertungen über einen POST-Request löschen.
* Überprüft die Nutzerrolle in der Session und führt das DELETE-Statement aus.
*/
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_review']) && isset($_POST['delete_review_id'])) {
if (!empty($_SESSION['user_roles']) && in_array('ADMIN', $_SESSION['user_roles'], true)) {
$deleteId = (int)$_POST['delete_review_id'];
@ -40,6 +69,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_review']) && i
<?php include 'header.php'; ?>
<?php
/**
* @brief Holt die detaillierten Produktdaten und -attribute.
* @details Führt einen JOIN über products, categoryAttributes, attributes und productAttributes aus,
* um alle Eigenschaften für die Anzeige abzurufen.
*/
$stmt = $conn->prepare("
SELECT
a.name,
@ -77,8 +111,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_review']) && i
$product = $result->fetch_assoc();
$categoryId = $product['categoryID'];
/**
* @brief Initialer Status für die Wunschliste.
*/
$alreadyInWishlist = false;
/**
* @brief Prüft, ob sich das Produkt bereits auf der Wunschliste des angemeldeten Nutzers befindet.
* @details Falls der Nutzer angemeldet ist (`user_id` ist gesetzt), wird in `userFavorites` gesucht.
*/
if (isset($_SESSION['user_id'])) {
$stmtCheck = mysqli_prepare(
@ -109,6 +150,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_review']) && i
?>
<?php
/**
* @brief Verarbeitet POST-Requests für die Wunschliste.
* @details Benutzer können das Produkt der Wunschliste hinzufügen oder daraus entfernen.
* Benötigt eine aktive Session mit gültiger `user_id`.
*/
// PRÜFEN: POST-Request und Nutzer ist eingeloggt
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SESSION['user_id'])) {
@ -138,6 +184,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SESSION['user_id'])) {
}
}
/**
* @brief Logik für das Produktvergleichs-Feature.
* @details Speichert Produkte nach `categoryID` gruppiert in der Session-Variable `compare`,
* um später mehrere Produkte vergleichen zu können. POST-Requests updaten dieses Array.
*/
// Vergleichslogik
if (!isset($_SESSION['compare'])) {
$_SESSION['compare'] = [];
@ -160,6 +211,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<?php
/**
* @brief Berechnet Bewertungs-Statistiken für dieses Produkt.
* @details Holt den Durchschnitt und zählt, wie viele Bewertungen je Sterne-Kategorie existieren,
* um die Bewertungsübersicht (Balkendiagramm) darzustellen.
*/
// SQL korrigiert: SUM statt COUNT für die bedingten Zählungen
$stmtRevOv = mysqli_prepare($conn,
"SELECT ROUND(AVG(rating), 1) as avgRating,
@ -295,6 +351,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
</div>
<?php
/**
* @brief Gibt die Produktattribute (Specs) dynamisch aus.
* @details Iteriert durch die vorab geladenen Attribute und formatiert die Ausgabe
* entsprechend den Datentypen (String, Number, Boolean).
*/
while ($row = $result->fetch_assoc()) {
echo "<p><strong>{$row['name']}:</strong> ";
if (!empty($row['valueString'])) echo $row['valueString'];
@ -308,6 +369,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
</div>
<?php
/**
* @brief Holt die anbietenden Shops inkl. Preisen und Links zum Produkt.
* @details Abhängig von der bestehenden Datenbankstruktur ('productURL' vs 'offerURL') wird
* die korrekte Spalte per Fallback ermittelt und abgefragt.
*/
// Unterschiedliche DB-Stände: URL-Spalte heißt je nach Schema z.B. productURL oder offerURL.
// Wir ermitteln die existierende Spalte dynamisch, damit die Seite nicht mit "Unknown column" crasht.
$urlColumn = '';
@ -335,6 +401,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$shopInfo = [];
/**
* @brief Speichert die Shop-Ergebnisseingebungen in einem Array für die spätere Iteration.
*/
while ($row = $result->fetch_assoc()) {
$shopInfo[] = $row;
}
@ -389,6 +458,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<?php
/**
* @brief Holt alle Nutzer-Bewertungen zu diesem Produkt.
* @details Liefert Bewertungstexte, Anzahl vergebener Sterne und die nutzerspezifischen Profileigenschaften.
*/
// HIER ANGEPASST: profilePicture und createdAt zum SELECT hinzugefügt
$stmt = mysqli_prepare($conn,
" SELECT reviews.reviewID, rating, comment, users.displayname, users.profilePicture, reviews.createdAt
@ -402,6 +475,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$reviews = [];
/**
* @brief Speichert alle ermittelten Bewertungen im Array für die Ansicht.
*/
while ($row = $result->fetch_assoc()) {
$reviews[] = $row;
}
@ -471,6 +547,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<h2 class="reviews-title">Füge deine Bewertung hinzu!</h2>
<?php
/**
* @brief Steuert den Logik-Fluss zum Hinzufügen einer eigenen Bewertung.
* @details Prüft zunächst, ob der momentan eingeloggte Nutzer das Produkt bereits
* bewertet hat. Falls nicht, kann eine neue Bewertung über einen POST-Request verarbeitet werden.
*/
$userHasReviewed = false;
// 1. Prüfen, ob der eingeloggte Nutzer schon bewertet hat
@ -517,7 +598,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
mysqli_stmt_execute($stmtInsertRev);
mysqli_stmt_close($stmtInsertRev);
// JS Weiterleitung
// JS Weiterleitung nach dem erfolgreichen Anlegen der Bewertung
echo "<script>window.location.href = 'productpage.php?id=" . $productId . "';</script>";
exit;
}
@ -540,6 +621,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
</div>
<?php else: ?>
<!--
* @brief Eingabeformular für Bewertungen.
* @details Bietet ein Feld für den Kommentartext sowie Radio-Buttons für das Sterne-Rating (1-5).
-->
<form class="review-input-form" method="post" autocomplete="off">
<input type="hidden" name="submit_review" value="1">
@ -569,4 +654,4 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
</div>
</div>
<?php include 'footer.php'; ?>
<?php include 'footer.php'; ?>

View File

@ -1,50 +1,109 @@
<?php
/**
* @file register.php
* @brief Behandelt die Registrierung neuer Benutzer.
*
* @details Diese Datei stellt das Registrierungsformular bereit und verarbeitet die
* abgesendeten POST-Daten. Sie validiert die Eingaben (E-Mail, Benutzername, Passwort),
* prüft auf Duplikate in der Datenbank und legt bei erfolgreicher Validierung einen neuen
* Benutzer samt zugehöriger Standard-Rolle ("USER") in der Datenbank an.
*
* @author System
* @version 1.0
*/
// register.php
require_once __DIR__ . '/lib/bootstrap.php';
require_once __DIR__ . '/lib/strings.php';
/**
* @brief Initiiert die globale Datenbankverbindung.
* @details Ruft die Funktion db_connect() auf, um eine Verbindung zur konfigurierten
* Datenbank herzustellen. Diese Verbindung wird für alle folgenden Queries verwendet.
*/
// 1) DB-Verbindung (einmal)
$conn = db_connect();
/**
* @brief Array zur Speicherung möglicher Validierungs- oder Datenbankfehler.
* @var array $errors
*/
$errors = [];
/**
* @brief Array zur Zwischenspeicherung der eingegebenen Formulardaten.
* @details Dient dazu, die Eingaben des Nutzers (E-Mail und Anzeigename) nach
* einem fehlerhaften Submit-Versuch wieder im Formularfeld anzeigen zu können.
* @var array $values
*/
$values = [
'email' => '',
'displayName' => ''
];
/**
* @brief Prüft, ob das Formular per POST-Methode abgesendet wurde.
* @details Alle Validierungs- und Registrierungsschritte werden nur ausgeführt,
* wenn Daten via POST übermittelt wurden.
*/
if ($_SERVER['REQUEST_METHOD'] === 'POST')
{
/**
* @brief Holt und bereinigt die eingegebene E-Mail-Adresse.
* @var string $email
*/
$email = '';
if (isset($_POST['email']))
{
$email = trim((string)$_POST['email']);
}
/**
* @brief Holt und bereinigt den eingegebenen Benutzernamen.
* @var string $displayName
*/
$displayName = '';
if (isset($_POST['displayName']))
{
$displayName = trim((string)$_POST['displayName']);
}
/**
* @brief Holt das eingegebene Passwort.
* @var string $pw
*/
$pw = '';
if (isset($_POST['pw']))
{
$pw = (string)$_POST['pw'];
}
/**
* @brief Holt die Wiederholung des eingegebenen Passworts.
* @var string $pw2
*/
$pw2 = '';
if (isset($_POST['pw2']))
{
$pw2 = (string)$_POST['pw2'];
}
/**
* @brief Standard-Profilbild für neu registrierte Nutzer.
* @var string $profilePicture
*/
$profilePicture = 'assets/images/profilePictures/default.png';
// Aktualisiere das $values-Array für die erneute Anzeige bei Fehlern
$values['email'] = $email;
$values['displayName'] = $displayName;
/**
* @brief Validierung der Eingabedaten.
* @details Prüft E-Mail-Format, Länge des Benutzernamens (3-50 Zeichen),
* Passwortlänge (min. 8 Zeichen) und Übereinstimmung der Passwörter.
*/
// Validierung
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL))
{
@ -66,6 +125,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST')
$errors[] = 'Die Passwörter stimmen nicht überein.';
}
/**
* @brief Prüfung auf bereits existierende E-Mail.
* @details Falls bisher keine Fehler aufgetreten sind, wird in der Datenbank
* geprüft, ob die angegebene E-Mail bereits von einem anderen Account genutzt wird.
*/
// Duplicate-Checks
if (!$errors)
{
@ -87,6 +151,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST')
}
}
/**
* @brief Prüfung auf bereits existierenden Benutzernamen.
* @details Führt eine Datenbankabfrage durch, um sicherzustellen, dass
* der gewählte Anzeigename (displayName) noch verfügbar ist.
*/
if (!$errors)
{
$stmt = mysqli_prepare($conn, 'SELECT userID FROM users WHERE displayName = ? LIMIT 1');
@ -107,6 +176,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST')
}
}
/**
* @brief Finaler Insert-Vorgang für den neuen Benutzer.
* @details Wenn keine Fehler vorliegen, wird das Passwort gehasht und der
* Benutzer in der Tabelle `users` gespeichert. Anschließend wird ihm die
* Standard-Rolle (USER) auf der Tabelle `userRoles` zugewiesen und
* auf die Login-Seite weitergeleitet.
*/
// Insert
if (!$errors)
{
@ -127,8 +203,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST')
if ($ok)
{
/**
* @brief ID des soeben neu erstellten Benutzers.
* @var int $newUserId
*/
$newUserId = mysqli_insert_id($conn);
/**
* @brief Zuweisung der initialen Benutzerrolle "USER".
* @details Sucht die ID der "USER"-Rolle aus der Tabelle `roles`
* und verknüpft sie mit der neuen Benutzer-ID in `userRoles`.
*/
// Get USER roleID
$roleStmt = mysqli_prepare($conn, "SELECT roleID FROM roles WHERE name = 'USER' LIMIT 1");
if ($roleStmt) {
@ -150,6 +235,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST')
mysqli_stmt_close($stmt);
mysqli_close($conn);
/**
* @brief Weiterleitung nach erfolgreicher Registrierung.
* @details Leitet zurück auf login.php mit dem GET-Parameter `registered=1`,
* damit der Nutzer eine Erfolgsmeldung erhält.
*/
header('Location: login.php?registered=1');
exit;
}
@ -160,9 +251,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST')
}
}
/**
* @brief Einbinden des Seiten-Headers.
* @details Lädt die globale Navigationstruktur für die visuelle Darstellung (HTML/CSS).
*/
include 'header.php';
?>
<!--
@brief Start des HTML-Hauptbereichs für die Registrierung.
@details Beinhaltet das Registrierungsformular, die Ausgabe etwaiger Fehler
sowie Links zum Login für bereits angemeldete Benutzer.
-->
<main class="auth" role="main">
<section class="auth__grid" aria-label="Registrierung Bereich">
<div class="auth__card">
@ -217,6 +317,11 @@ include 'header.php';
</main>
<?php
/**
* @brief Schließt die Datenbankverbindung und bindet den Footer ein.
* @details Säubert die offene MySQLi-Verbindung. Der Footer schließt
* das HTML-Dokument korrekt ab.
*/
mysqli_close($conn);
include 'footer.php';
?>

File diff suppressed because one or more lines are too long

136
style.css
View File

@ -3,11 +3,29 @@
Animated Dark Theme · Glassmorphism · Glow
========================================================== */
/**
* @file style.css
* @brief Main stylesheet for the Geizkragen Design System v2.
*
* This file contains all global styles, CSS variables, keyframe animations,
* layout rules, typography, and component-specific styling (such as buttons,
* cards, navigation, and footer) for the application. It utilizes a dark theme
* with glassmorphism and glow effects.
*
* @version 2.0
*/
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap');
/* ==========================================================
RESET & BASIS
========================================================== */
/**
* @brief Global CSS reset.
*
* Resets margin and padding for all elements and sets box-sizing to border-box
* to ensure consistent box model calculations across the entire application.
*/
*,
*::before,
*::after {
@ -16,6 +34,17 @@
padding: 0;
}
/**
* @brief Root CSS variables defining the design token system.
*
* This pseudo-class contains all the fundamental design tokens including:
* - Colors (backgrounds, semantics, text, borders)
* - Border radii for various component sizes
* - Box shadows for elevation and glow effects
* - Transition timings for smooth animations
* - Layout spacing values
* - Primary typography settings
*/
:root {
/* ─── Farben ─── */
--bg-main: #151923;
@ -76,47 +105,89 @@
/* ==========================================================
KEYFRAME ANIMATIONS
========================================================== */
/**
* @brief Fade in and slide up animation.
*
* Elements transition from transparent and slightly translated down
* to fully opaque at their original position.
*/
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(24px); }
to { opacity: 1; transform: translateY(0); }
}
/**
* @brief Simple fade in animation.
*/
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/**
* @brief Slide in from the right animation.
*
* Elements transition from transparent and translated right
* to fully opaque at their original position.
*/
@keyframes slideInRight {
from { opacity: 0; transform: translateX(20px); }
to { opacity: 1; transform: translateX(0); }
}
/**
* @brief Scale in animation for a subtle zoom effect.
*
* Elements transition from transparent and slightly scaled down
* to fully opaque at their original size.
*/
@keyframes scaleIn {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
/**
* @brief Shimmering animation for loading placeholders.
*
* Creates a moving gradient effect to simulate a shimmering light.
*/
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/**
* @brief Pulsating glow animation for emphasis.
*
* Elements receive a glowing shadow that intensifies and fades.
*/
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 20px rgba(37, 99, 235, 0.08); }
50% { box-shadow: 0 0 40px rgba(37, 99, 235, 0.18); }
}
/**
* @brief Continuous float animation for ambient background elements.
*/
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
/**
* @brief Gradient shifting animation for vibrant backgrounds.
*/
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/**
* @brief Border glowing animation for emphasis.
*
* Elements' borders transition between two glow states.
*/
@keyframes borderGlow {
0%, 100% { border-color: rgba(37, 99, 235, 0.15); }
50% { border-color: rgba(37, 99, 235, 0.35); }
@ -125,10 +196,21 @@
/* ==========================================================
GLOBAL
========================================================== */
/**
* @brief Global standard HTML element rules.
*
* Enables smooth scrolling behavior across the entire document.
*/
html {
scroll-behavior: smooth;
}
/**
* @brief Primary body base styles.
*
* Establishes the core typography, background color, text color,
* and standard flexbox layout structure to ensure the footer sticks to the bottom.
*/
body {
font-family: var(--font-family);
background: var(--bg-main);
@ -196,6 +278,12 @@ a:hover {
/* ==========================================================
CONTAINER
========================================================== */
/**
* @brief Standard layout container.
*
* Centers content horizontally and limits its maximum width to maintain
* readability on large screens, applying standard horizontal padding.
*/
.container {
max-width: 1400px;
margin-inline: auto;
@ -205,6 +293,12 @@ a:hover {
/* ==========================================================
HEADER Glassmorphism
========================================================== */
/**
* @brief Primary site header with glassmorphism effect.
*
* Ensures the header is sticky at the top, applies a blur filter for the
* glassmorphic look, and controls the initial fade-in animation.
*/
.header {
position: sticky;
top: 0;
@ -261,10 +355,16 @@ a:hover {
/* ==========================================================
SEARCH AUTOCOMPLETE DROPDOWN
========================================================== */
/**
* @brief Wrapper for the search field when it contains a dropdown.
*/
.nav__searchField--hasDropdown {
position: relative;
}
/**
* @brief Autocomplete dropdown container styling.
*/
.searchDropdown {
position: absolute;
left: 0;
@ -683,6 +783,11 @@ a:hover {
/* ==========================================================
LAYOUT (Grid mit Sidebar)
========================================================== */
/**
* @brief Main grid layout establishing a sidebar and main content area.
*
* Employs CSS Grid to define a 260px sidebar alongside a fluid main content area.
*/
.layout {
margin: 2rem auto;
padding: 0 2rem;
@ -694,6 +799,11 @@ a:hover {
/* ==========================================================
FILTER / SIDEBAR
========================================================== */
/**
* @brief Sidebar container styles.
*
* Utilizes standard card background, subtle borders, and smooth fade-in animations.
*/
.sidebar {
background: var(--bg-card);
border: 1px solid var(--border-subtle);
@ -744,6 +854,11 @@ a:hover {
/* ==========================================================
PRODUCT GRID
========================================================== */
/**
* @brief CSS Grid setup for laying out multiple product cards.
*
* Implements an auto-fill behavior for highly responsive product grids.
*/
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
@ -759,6 +874,12 @@ a:hover {
/* ==========================================================
PRODUCT CARD (global)
========================================================== */
/**
* @brief Main product card component styling.
*
* Sets the background, borders, border-radius, and shadow for product items.
* Implements hover scaling and glow transitions.
*/
.product-card {
background: var(--bg-card);
border: 1px solid var(--border-subtle);
@ -841,6 +962,12 @@ a:hover {
/* ==========================================================
BUTTONS Gradient & Glow
========================================================== */
/**
* @brief Base styling for all button elements.
*
* Defines flexible sizing, border radius, typography, and foundational
* transition properties for standard interactive elements.
*/
.btn {
flex: 1;
padding: 0.6rem 1.1rem;
@ -910,6 +1037,9 @@ a:hover {
/* ==========================================================
BADGES
========================================================== */
/**
* @brief Basic aesthetic container for miniature tags or status labels.
*/
.badge {
display: inline-flex;
align-items: center;
@ -960,6 +1090,12 @@ a:hover {
/* ==========================================================
FOOTER Glassmorphism
========================================================== */
/**
* @brief Global footer component.
*
* Adheres to the overall glassmorphism aesthetic with background blur,
* border separation, and sticky-to-bottom layout integration.
*/
.footer {
margin-top: auto;
background: rgba(21, 25, 35, 0.92);

View File

@ -1,30 +1,77 @@
<?php
/**
* @file upload.php
* @brief Behandelt den Dateiupload für Profilbilder der Benutzer.
*
* @details Diese Datei nimmt ein hochgeladenes Bild als POST-Request entgegen.
* Sie validiert den Upload-Fehlercode, stellt sicher, dass es sich um ein gültiges
* Bild handelt (JPEG oder PNG) und verschiebt die Datei in das entsprechende
* Assets-Verzeichnis. Abschließend wird der Pfad zum hochgeladenen Bild in der
* Datenbank des Benutzers gespeichert. Alle Fehler und Erfolgsmeldungen leiten
* auf die account.php mit einem entsprechenden URL-Parameter zurück.
*/
/**
* @brief Bindet die grundlegenden Konfigurationen und Bibliotheken ein.
*/
require_once __DIR__ . '/lib/bootstrap.php';
/**
* @brief Überprüft, ob der Benutzer angemeldet ist.
*
* @details Wenn keine Benutzer-ID in der Session vorhanden ist,
* wird der Benutzer sofort auf die Login-Seite umgeleitet und das Skript beendet.
*/
if (empty($_SESSION['user_id']))
{
header('Location: login.php');
exit();
}
/**
* @var int $userId Die ID des aktuell angemeldeten Benutzers, ausgelesen aus der Session.
*/
$userId = (int)$_SESSION['user_id'];
/**
* @brief Überprüft, ob die Anfrage über die POST-Methode gesendet wurde.
*
* @details Auf diese Datei darf nur per HTTP POST zugegriffen werden (Formular-Upload).
* Andernfalls wird der Benutzer zur account.php umgeleitet.
*/
if ($_SERVER['REQUEST_METHOD'] !== 'POST')
{
header('Location: account.php');
exit();
}
/**
* @brief Überprüft, ob das Feld 'uploadFile' im $_FILES Array gesetzt ist.
*
* @details Es muss eine Datei über das erwartete Formularfeld übertragen worden sein.
*/
if (!isset($_FILES['uploadFile']) || !is_array($_FILES['uploadFile']))
{
header('Location: account.php?upload=err');
exit();
}
/**
* @var array $file Referenz auf die hochgeladene Datei im $_FILES Array.
*/
$file = $_FILES['uploadFile'];
/**
* @var int $fileError Ermittelt den PHP-Upload-Fehlercode (UPLOAD_ERR_OK, falls erfolgreich).
*/
$fileError = isset($file['error']) ? (int)$file['error'] : UPLOAD_ERR_NO_FILE;
/**
* @brief Überprüft, ob beim Upload ein Fehler aufgetreten ist.
*
* @details Wenn $fileError nicht UPLOAD_ERR_OK ist, wird abgebrochen, der Fehler ins
* Error-Log geschrieben und zur Account-Seite mit dem Fehlercode umgeleitet.
*/
if ($fileError !== UPLOAD_ERR_OK)
{
// Serverseitiges Log ist ok, aber kein Pfad/Interna im Browser
@ -33,8 +80,17 @@ if ($fileError !== UPLOAD_ERR_OK)
exit();
}
/**
* @var string $tmp Der temporäre Dateipfad, an dem die hochgeladene Datei abgelegt wurde.
*/
// Basic Validierung
$tmp = isset($file['tmp_name']) ? (string)$file['tmp_name'] : '';
/**
* @brief Überprüft die Existenz und Gültigkeit der temporären Upload-Datei.
*
* @details Nutzt is_uploaded_file, um Exploits und Path-Traversal-Angriffe zu verhindern.
*/
if ($tmp === '' || !is_uploaded_file($tmp))
{
// Debug-Detail (tmp-Pfad) nicht loggen
@ -43,14 +99,29 @@ if ($tmp === '' || !is_uploaded_file($tmp))
exit();
}
/**
* @var array $allowedMimeToExt Zuordnung von erlaubten MIME-Types zu deren Dateiendungen.
*/
$allowedMimeToExt = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
];
/**
* @var finfo $finfo PHP finfo-Instanz zur Bestimmung des tatsächlichen MIME-Types der Datei.
*/
$finfo = new finfo(FILEINFO_MIME_TYPE);
/**
* @var string|false $mime Der ausgelesene MIME-Type der temporären Datei.
*/
$mime = $finfo->file($tmp);
/**
* @brief Validiert den MIME-Type.
*
* @details Erlaubt sind nur die in $allowedMimeToExt definierten Dateitypen (JPEG, PNG).
*/
if (!$mime || !isset($allowedMimeToExt[$mime]))
{
// Mime loggen ist ok (kein Secret), hilft bei Support
@ -59,21 +130,45 @@ if (!$mime || !isset($allowedMimeToExt[$mime]))
exit();
}
/**
* @var string $ext Die zur validierten Datei passende Dateiendung.
*/
$ext = $allowedMimeToExt[$mime];
/**
* @var string $relativeTargetDir Relativer Pfad zum Verzeichnis der Profilbilder.
*/
// Zielordner: assets/images/profilePictures relativ zum Projekt (upload.php liegt im Webroot)
$relativeTargetDir = 'assets/images/profilePictures';
/**
* @var string $dirTargetDir Absoluter Pfad zum Zielordner, basierend auf der aktuellen Datei (__DIR__).
*/
// Kandidat 1: relativ zu __DIR__ (robust gegen VHost/Alias)
$dirTargetDir = rtrim(__DIR__, "\\/") . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relativeTargetDir);
/**
* @var string $documentRoot Dokumentenwurzel des Webservers (falls gesetzt).
*/
// Kandidat 2: relativ zu DOCUMENT_ROOT (nur wenn gesetzt)
$documentRoot = isset($_SERVER['DOCUMENT_ROOT']) ? (string)$_SERVER['DOCUMENT_ROOT'] : '';
/**
* @var string $docRootTrim Formatierter Dokumenten-Root-Pfad (ohne abschließende Slashes).
*/
$docRootTrim = rtrim($documentRoot, "\\/");
/**
* @var string $docTargetDir Absoluter Zielordner aus der Server-Variablen (DOCUMENT_ROOT).
*/
$docTargetDir = ($docRootTrim !== '')
? $docRootTrim . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relativeTargetDir)
: '';
/**
* @var string $targetDir Finaler Pfad, in den die Datei verschoben wird.
* @details Bevorzugt wird __DIR__. Falls dieses aus Systemgründen fehlschlägt und DOCUMENT_ROOT zur Verfügung steht, wird dieser verwendet.
*/
// Bevorzugt __DIR__. Falls __DIR__ aus irgendeinem Grund nicht ins Projekt zeigt, und DOCUMENT_ROOT plausibel ist, nutze DOCUMENT_ROOT.
$targetDir = $dirTargetDir;
if ($docTargetDir !== '' && !is_dir($dirTargetDir) && is_dir($docTargetDir))
@ -81,6 +176,11 @@ if ($docTargetDir !== '' && !is_dir($dirTargetDir) && is_dir($docTargetDir))
$targetDir = $docTargetDir;
}
/**
* @brief Prüft, ob das Zielverzeichnis existiert und erstellt es gegebenenfalls.
*
* @details Nutzt mkdir() rekursiv, um fehlende Ordnerstrukturen zu generieren.
*/
if (!is_dir($targetDir))
{
$mkOk = @mkdir($targetDir, 0755, true);
@ -94,6 +194,9 @@ if (!is_dir($targetDir))
}
}
/**
* @brief Prüft, ob in den Zielordner geschrieben werden darf.
*/
if (!is_writable($targetDir))
{
error_log('Upload: targetDir not writable: ' . $targetDir);
@ -101,12 +204,28 @@ if (!is_writable($targetDir))
exit();
}
/**
* @var string $timestamp Ein Zeitstempel zur Vergabe einmaliger Dateinamen.
*/
// Dateiname: user_<ID>_<Datum>.<ext>
// Format ist dateisystem-sicher (keine Doppelpunkte) und eindeutig genug.
$timestamp = gmdate('Ymd-His');
/**
* @var string $filename Der zu verwendende neue Name für die hochgeladene Datei.
*/
$filename = 'user_' . $userId . '_' . $timestamp . '.' . $ext;
/**
* @var string $targetPath Der genaue finale Pfad (inkl. Dateiname).
*/
$targetPath = rtrim($targetDir, "\\/") . DIRECTORY_SEPARATOR . $filename;
/**
* @brief Verschiebt die hochgeladene Datei aus dem temporären Verzeichnis in den endgültigen Pfad.
*
* @details Schlägt dieser Vorgang fehl, wird ein serverseitiges Error-Log geschrieben und eine Weiterleitung durchgeführt.
*/
if (!move_uploaded_file($tmp, $targetPath))
{
$lastErr = error_get_last();
@ -116,15 +235,36 @@ if (!move_uploaded_file($tmp, $targetPath))
exit();
}
/**
* @var string $publicPath Öffentlich zugänglicher URL-Pfad relativ zum Webroot.
* Dies ist der Pfad, der in der Datenbank gespeichert wird.
*/
// Pfad, der in HTML genutzt wird (URL relativ zur Webroot)
$publicPath = 'assets/images/profilePictures/' . $filename;
/**
* @var string $servername Adresse des Datenbankservers (veraltet in diesem Skript, aber vorhanden).
*/
$servername = "localhost";
/**
* @var int $port Portadresse der Datenbank (ebenfalls ungenutzt im direkten Setup hier).
*/
$port = 3306;
/**
* @var mysqli|bool $conn Die per db_connect (aus lib/db.php) aufgebaute Verbindung zur Datenbank.
*/
$conn = db_connect();
/**
* @var mysqli_stmt|false $stmt Das vorbereitete Statement zur Aktualisierung des Benutzer-Profilbilds.
*/
$stmt = mysqli_prepare($conn, "UPDATE users SET profilePicture = ? WHERE userID = ?");
/**
* @brief Bricht ab, wenn das Prepared Statement nicht erstellt werden konnte.
*/
if (!$stmt)
{
mysqli_close($conn);
@ -132,16 +272,26 @@ if (!$stmt)
exit();
}
/**
* @brief Bindet Parameter, führt das Statement aus und schließt anschließend Verbindung und Statement.
*/
mysqli_stmt_bind_param($stmt, 'si', $publicPath, $userId);
$ok = mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
$conn->close();
/**
* @brief Leitet bei einem fehlgeschlagenen Datenbankupdate zur Error-Variante der account.php weiter.
*/
if (!$ok)
{
header('Location: account.php?upload=err');
exit();
}
/**
* @brief Der Upload war erfolgreich; Umleitung zur Account-Seite mit Erfolgsmeldung.
*/
header('Location: account.php?upload=ok');
exit();

View File

@ -1,37 +1,118 @@
<?php
/**
* @file wunschliste.php
* @brief Darstellung der persönlichen Wunschliste eines Benutzers
*
* @details Diese Datei ist dafür verantwortlich, alle Produkte anzuzeigen,
* die der aktuell eingeloggte Benutzer zu seiner Wunschliste hinzugefügt hat.
* Sie greift auf die Datenbank zu, um die Verknüpfung zwischen Benutzer
* (über die aktuelle Session) und den favorisierten Produkten (aus der Tabelle userFavorites)
* herzustellen.
* @author Geizkragen-Team
* @version 1.0
* @since 1.0
*/
// wunschliste.php
/**
* @brief Einbinden der grundlegenden Bootstrapping-Konfiguration
* @details Die Datei bootstrap.php lädt Helferfunktionen, Konfigurationsdaten
* und initialisiert die Session, wodurch Zugriffe auf $_SESSION möglich werden.
*/
require_once __DIR__ . '/lib/bootstrap.php';
/**
* @brief Herstellen der Datenbankverbindung
* @details Ruft db_connect() auf, um eine Instanz zur Datenbank aufzubauen.
* Diese wird für das spätere Abrufen der Wunschlisten-Elemente benötigt.
* @var mysqli $conn Aktives Datenbankverbindungsobjekt.
*/
// 1) DB-Verbindung (einmal)
$conn = db_connect();
/**
* @brief Einschränkung des Datenzugriffs und Weiterleitung unbefugter Benutzer
* @details Prüft, ob ein Benutzer aktuell eingeloggt ist. Die Weiterleitung
* (HTTP-Header Location) muss zwingend erfolgen, bevor jegliche HTML-Struktur
* gerendert wurde. Danach beendet exit() sofort die Ausführung dieses Skripts.
*/
// Login-Check + Redirect MUSS vor jeglicher HTML-Ausgabe passieren
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit();
}
/**
* @brief Laden der Wunschlisten-Daten aus der Datenbank
* @details Ein "Prepared Statement" wird aufgebaut, um die Produktdaten (ID, Modell,
* Beschreibung, Bildpfad) sicher aus der Datenbank abzufragen. Hierbei werden die
* Tabellen 'userFavorites' und 'products' per INNER JOIN anhand der productID zusammengeführt.
* @var mysqli_stmt $stmt Das vorbereitete SQL-Statement Objekt.
*/
// Daten laden
$stmt = $conn->prepare("
SELECT products.productID, products.model, products.description, products.imagePath
FROM userFavorites INNER JOIN products ON userFavorites.productID = products.productID
WHERE userID = ?
");
/**
* @brief Binden der Nutzer-ID an das SQL-Statement
* @details Verbindet den Typ 'i' (Integer) mit dem tatsächlichen Wert der aktuellen Session-ID.
*/
$stmt->bind_param("i", $_SESSION['user_id']);
/**
* @brief Ausführung der vorbereiteten Abfrage
*/
$stmt->execute();
/**
* @brief Extrahieren der Ergebnismenge
* @details Speichert das Resulat der Abfrage, welches in der GUI anschließend durchlaufen wird.
* @var mysqli_result $result Die vom Datenbankserver zurückgelieferte Ergebnismenge.
*/
$result = $stmt->get_result();
?>
<?php include 'header.php'; ?>
<?php
/**
* @brief Einbinden des HTML-Headers
* @details Lädt die globale Navigation, CSS-Dateien und nötige Meta-Informationen.
*/
include 'header.php';
?>
<?php if ($result->num_rows > 0): ?>
<?php
/**
* @brief Bedingung zur Anzeige der Produktliste
* @details Evaluiert, ob das vorherige Statement mindestens ein favorisiertes
* Produkt für den Benutzer zurückgeliefert hat. Ist dies der Fall, wird die
* Liste gerendert. Andernfalls erscheint ein leerer Hinweisbildschirm.
*/
if ($result->num_rows > 0):
?>
<main>
<section class="product-section">
<h2>Deine Wunschliste</h2>
<div class="wishlist-grid">
<?php while ($product = $result->fetch_assoc()): ?>
<?php $productId = (int)$product['productID']; ?>
<?php
/**
* @brief Iteration über alle favorisierten Produkte
* @details In jedem Durchlauf wird ein Produkt als assoziatives Array
* zurückgegeben und direkt per HTML im Frontend abgebildet.
*/
while ($product = $result->fetch_assoc()):
?>
<?php
/**
* @brief Casting der Produkt ID
* @var int $productId Stellt sicher, dass die Produkt-ID als strenger Integer
* zur Erzeugung der Link-URL vorliegt.
*/
$productId = (int)$product['productID'];
?>
<a class="product-card wishlist-card" href="productpage.php?id=<?= $productId ?>">
<img
src="<?= isset($product['imagePath']) ? $product['imagePath'] : 'assets/images/placeholder.png' ?>"
@ -47,12 +128,30 @@ $result = $stmt->get_result();
</section>
</main>
<?php else: ?>
<?php
/**
* @brief Fallback Ansicht
* @details Dieser Abschnitt wird angezeigt, wenn der Nutzer noch
* keine Artikel als Favoriten markiert hat.
*/
?>
<main style="padding: 2rem 1rem; text-align: center; animation: fadeInUp 0.5s ease both;">
<p style="color: var(--text-secondary); font-size: 1rem;">Deine Wunschliste ist noch leer.</p>
</main>
<?php endif; ?>
<?php $stmt->close(); ?>
<?php
/**
* @brief Freigabe der SQL-Ressourcen
* @details Schließt das PreparedStatement sicher und räumt belegten Speicher wieder auf.
*/
$stmt->close();
?>
<?php include 'footer.php'; ?>
<?php
/**
* @brief Einbinden des HTML-Footers
* @details Rendert rechtliche Links sowie abschließende Skripte für die Seite.
*/
include 'footer.php';
?>