From 47fa3098859f60eaf967cfb2bc5fc4f6b204f4ce Mon Sep 17 00:00:00 2001 From: arslandevelopments Date: Wed, 8 Jul 2026 14:06:31 +0200 Subject: [PATCH 01/15] index.html erstellt (test) --- index.html | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 index.html diff --git a/index.html b/index.html new file mode 100644 index 0000000..0aa557f --- /dev/null +++ b/index.html @@ -0,0 +1,9 @@ + + + + Habit Tracker + + +

Habit Tracker

+ + \ No newline at end of file From 23416ee382d20e3739848652895086014b587b84 Mon Sep 17 00:00:00 2001 From: PBBFA24AAL Date: Fri, 10 Jul 2026 16:18:31 +0200 Subject: [PATCH 02/15] Initial habit tracker project --- .gitignore | 3 + README.md | 75 ++++++++ app/Controllers/AuthController.php | 75 ++++++++ app/Controllers/HabitController.php | 137 ++++++++++++++ app/Core/Database.php | 35 ++++ app/Core/View.php | 15 ++ app/Models/Category.php | 11 ++ app/Models/Habit.php | 110 +++++++++++ app/Models/User.php | 25 +++ app/Views/auth/login.php | 15 ++ app/Views/auth/register.php | 19 ++ app/Views/habits/dashboard.php | 67 +++++++ app/Views/habits/form.php | 58 ++++++ app/Views/layout/footer.php | 3 + app/Views/layout/header.php | 34 ++++ app/helpers.php | 82 +++++++++ config/config.example.php | 13 ++ database/schema.sql | 55 ++++++ public/assets/css/style.css | 275 ++++++++++++++++++++++++++++ public/index.php | 50 +++++ 20 files changed, 1157 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/Controllers/AuthController.php create mode 100644 app/Controllers/HabitController.php create mode 100644 app/Core/Database.php create mode 100644 app/Core/View.php create mode 100644 app/Models/Category.php create mode 100644 app/Models/Habit.php create mode 100644 app/Models/User.php create mode 100644 app/Views/auth/login.php create mode 100644 app/Views/auth/register.php create mode 100644 app/Views/habits/dashboard.php create mode 100644 app/Views/habits/form.php create mode 100644 app/Views/layout/footer.php create mode 100644 app/Views/layout/header.php create mode 100644 app/helpers.php create mode 100644 config/config.example.php create mode 100644 database/schema.sql create mode 100644 public/assets/css/style.css create mode 100644 public/index.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a0b81a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +config/config.php +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..c9e2e9c --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# Habit Tracker + +Habit Tracker ist eine PHP-Webanwendung, mit der Benutzer eigene Gewohnheiten anlegen, bearbeiten, filtern und taeglich abhaken koennen. + +## Funktionen + +- Registrierung und Login mit Passwort-Hashing +- Geschuetzter Benutzerbereich per Session +- Habits erstellen, bearbeiten, loeschen und fuer heute abhaken +- Kategorien und Statusfilter im Dashboard +- Tagesprotokoll ueber `habit_logs` +- Responsive Oberflaeche fuer Desktop und Smartphone +- Serverseitige Validierung und klare Fehlermeldungen +- Schutz gegen SQL-Injection durch PDO Prepared Statements +- Schutz gegen XSS durch escaped HTML-Ausgabe +- CSRF-Schutz fuer schreibende Formulare + +## Technik + +- PHP +- HTML/CSS +- SQL +- MySQL/PDO +- MVC-Struktur + +## MVC-Struktur + +- `public/index.php`: Einstiegspunkt und Routing +- `app/Controllers/`: verarbeitet Anfragen und steuert den Ablauf +- `app/Models/`: kapselt Datenbankzugriffe +- `app/Views/`: erzeugt die HTML-Ausgabe +- `app/Core/`: gemeinsame Kernklassen fuer Datenbank und Views +- `app/helpers.php`: Hilfsfunktionen fuer Escaping, Sessions, Flash-Meldungen und CSRF +- `database/schema.sql`: Datenbankstruktur mit Startdaten + +## Datenbank + +Die Datenbank besteht aus 5 Tabellen: + +- `users`: Benutzerkonten +- `categories`: Habit-Kategorien +- `habits`: Gewohnheiten der Benutzer +- `habit_logs`: taegliche Erledigungen +- `reminders`: vorbereitete Erinnerungen pro Habit + +## Sicherheit + +- Passwoerter werden mit `password_hash()` gespeichert. +- Login prueft Passwoerter mit `password_verify()`. +- SQL-Abfragen nutzen vorbereitete PDO-Statements. +- Ausgaben werden mit `htmlspecialchars()` escaped. +- Schreibende Formulare nutzen CSRF-Tokens. +- Nach erfolgreichem Login wird die Session-ID erneuert. +- Die echte `config/config.php` wird nicht mit Git hochgeladen. + +## Einrichtung + +1. `database/schema.sql` in phpMyAdmin importieren. +2. `config/config.example.php` zu `config/config.php` kopieren. +3. Datenbankdaten in `config/config.php` eintragen. +4. Projekt ueber `public/index.php` starten. + +## Testablauf + +1. Benutzer registrieren. +2. Einloggen. +3. Habit mit Kategorie und Wochenziel erstellen. +4. Habit im Dashboard filtern. +5. Habit fuer heute abhaken. +6. Habit bearbeiten und loeschen. +7. Seite auf Smartphone-Breite pruefen. + +## Bewertungsbezug + +Das Projekt zeigt eine klare MVC-Aufteilung, Datenbankarbeit mit mehreren verknuepften Tabellen, serverseitige Validierung, Sicherheitsmassnahmen gegen XSS, SQL-Injection und CSRF sowie eine responsive Benutzeroberflaeche. diff --git a/app/Controllers/AuthController.php b/app/Controllers/AuthController.php new file mode 100644 index 0000000..7d7cbda --- /dev/null +++ b/app/Controllers/AuthController.php @@ -0,0 +1,75 @@ + $_GET['category_id'] ?? '', + 'status' => $_GET['status'] ?? '', + ]; + + View::render('habits/dashboard', [ + 'habits' => Habit::allForUser(current_user_id(), $filters), + 'categories' => Category::all(), + 'filters' => $filters, + ]); + } + + public function create(): void + { + require_login(); + + View::render('habits/form', [ + 'habit' => null, + 'categories' => Category::all(), + 'errors' => [], + ]); + } + + public function store(): void + { + require_login(); + verify_csrf_token(); + + $data = $this->validatedData(); + + if (!empty($data['errors'])) { + View::render('habits/form', [ + 'habit' => $_POST, + 'categories' => Category::all(), + 'errors' => $data['errors'], + ]); + return; + } + + Habit::create(current_user_id(), $data); + flash('success', 'Habit wurde erstellt.'); + redirect('dashboard'); + } + + public function edit(): void + { + require_login(); + + $habit = Habit::findForUser((int) ($_GET['id'] ?? 0), current_user_id()); + + if (!$habit) { + flash('error', 'Habit wurde nicht gefunden.'); + redirect('dashboard'); + } + + View::render('habits/form', [ + 'habit' => $habit, + 'categories' => Category::all(), + 'errors' => [], + ]); + } + + public function update(): void + { + require_login(); + verify_csrf_token(); + + $id = (int) ($_POST['id'] ?? 0); + $data = $this->validatedData(); + + if (!empty($data['errors'])) { + View::render('habits/form', [ + 'habit' => array_merge($_POST, ['id' => $id]), + 'categories' => Category::all(), + 'errors' => $data['errors'], + ]); + return; + } + + Habit::update($id, current_user_id(), $data); + flash('success', 'Habit wurde gespeichert.'); + redirect('dashboard'); + } + + public function delete(): void + { + require_login(); + verify_csrf_token(); + + Habit::delete((int) ($_POST['id'] ?? 0), current_user_id()); + flash('success', 'Habit wurde geloescht.'); + redirect('dashboard'); + } + + public function toggle(): void + { + require_login(); + verify_csrf_token(); + + Habit::toggleToday((int) ($_POST['id'] ?? 0), current_user_id()); + redirect('dashboard'); + } + + private function validatedData(): array + { + $title = trim($_POST['title'] ?? ''); + $description = trim($_POST['description'] ?? ''); + $categoryId = $_POST['category_id'] ?? ''; + $targetPerWeek = (int) ($_POST['target_per_week'] ?? 0); + $errors = []; + + if ($title === '') { + $errors['title'] = 'Bitte gib einen Namen fuer den Habit ein.'; + } + + if ($targetPerWeek < 1 || $targetPerWeek > 7) { + $errors['target_per_week'] = 'Bitte waehle einen Wert zwischen 1 und 7.'; + } + + return [ + 'title' => $title, + 'description' => $description, + 'category_id' => $categoryId, + 'target_per_week' => $targetPerWeek, + 'errors' => $errors, + ]; + } +} + diff --git a/app/Core/Database.php b/app/Core/Database.php new file mode 100644 index 0000000..325862c --- /dev/null +++ b/app/Core/Database.php @@ -0,0 +1,35 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ]); + } catch (PDOException $exception) { + throw new PDOException('Datenbankverbindung fehlgeschlagen.'); + } + + return self::$connection; + } +} diff --git a/app/Core/View.php b/app/Core/View.php new file mode 100644 index 0000000..5b0fe82 --- /dev/null +++ b/app/Core/View.php @@ -0,0 +1,15 @@ +query('SELECT id, name, color FROM categories ORDER BY name') + ->fetchAll(); + } +} diff --git a/app/Models/Habit.php b/app/Models/Habit.php new file mode 100644 index 0000000..77808ec --- /dev/null +++ b/app/Models/Habit.php @@ -0,0 +1,110 @@ + $userId]; + + if (!empty($filters['category_id'])) { + $where[] = 'h.category_id = :category_id'; + $params['category_id'] = (int) $filters['category_id']; + } + + if (($filters['status'] ?? '') === 'done') { + $where[] = 'hl.id IS NOT NULL'; + } elseif (($filters['status'] ?? '') === 'open') { + $where[] = 'hl.id IS NULL'; + } + + $sql = 'SELECT h.*, c.name AS category_name, c.color AS category_color, + CASE WHEN hl.id IS NULL THEN 0 ELSE 1 END AS done_today + FROM habits h + LEFT JOIN categories c ON c.id = h.category_id + LEFT JOIN habit_logs hl + ON hl.habit_id = h.id + AND hl.completed_on = CURRENT_DATE + WHERE ' . implode(' AND ', $where) . ' + ORDER BY h.created_at DESC'; + + $statement = Database::connection()->prepare($sql); + $statement->execute($params); + + return $statement->fetchAll(); + } + + public static function findForUser(int $id, int $userId): ?array + { + $statement = Database::connection()->prepare( + 'SELECT * FROM habits WHERE id = :id AND user_id = :user_id LIMIT 1' + ); + $statement->execute(['id' => $id, 'user_id' => $userId]); + + $habit = $statement->fetch(); + return $habit ?: null; + } + + public static function create(int $userId, array $data): bool + { + $sql = 'INSERT INTO habits (user_id, category_id, title, description, target_per_week) + VALUES (:user_id, :category_id, :title, :description, :target_per_week)'; + + return Database::connection()->prepare($sql)->execute([ + 'user_id' => $userId, + 'category_id' => $data['category_id'] ?: null, + 'title' => $data['title'], + 'description' => $data['description'], + 'target_per_week' => (int) $data['target_per_week'], + ]); + } + + public static function update(int $id, int $userId, array $data): bool + { + $sql = 'UPDATE habits + SET category_id = :category_id, + title = :title, + description = :description, + target_per_week = :target_per_week + WHERE id = :id AND user_id = :user_id'; + + return Database::connection()->prepare($sql)->execute([ + 'id' => $id, + 'user_id' => $userId, + 'category_id' => $data['category_id'] ?: null, + 'title' => $data['title'], + 'description' => $data['description'], + 'target_per_week' => (int) $data['target_per_week'], + ]); + } + + public static function delete(int $id, int $userId): bool + { + $statement = Database::connection()->prepare('DELETE FROM habits WHERE id = :id AND user_id = :user_id'); + + return $statement->execute(['id' => $id, 'user_id' => $userId]); + } + + public static function toggleToday(int $id, int $userId): bool + { + $habit = self::findForUser($id, $userId); + + if (!$habit) { + return false; + } + + $db = Database::connection(); + $check = $db->prepare( + 'SELECT id FROM habit_logs WHERE habit_id = :habit_id AND completed_on = CURRENT_DATE LIMIT 1' + ); + $check->execute(['habit_id' => $id]); + $log = $check->fetch(); + + if ($log) { + return $db->prepare('DELETE FROM habit_logs WHERE id = :id')->execute(['id' => $log['id']]); + } + + $insert = $db->prepare('INSERT INTO habit_logs (habit_id, completed_on) VALUES (:habit_id, CURRENT_DATE)'); + return $insert->execute(['habit_id' => $id]); + } +} diff --git a/app/Models/User.php b/app/Models/User.php new file mode 100644 index 0000000..32e57d2 --- /dev/null +++ b/app/Models/User.php @@ -0,0 +1,25 @@ +prepare($sql); + + return $statement->execute([ + 'name' => $name, + 'email' => $email, + 'password_hash' => password_hash($password, PASSWORD_DEFAULT), + ]); + } + + public static function findByEmail(string $email): ?array + { + $statement = Database::connection()->prepare('SELECT * FROM users WHERE email = :email LIMIT 1'); + $statement->execute(['email' => $email]); + + $user = $statement->fetch(); + return $user ?: null; + } +} diff --git a/app/Views/auth/login.php b/app/Views/auth/login.php new file mode 100644 index 0000000..38cf9cc --- /dev/null +++ b/app/Views/auth/login.php @@ -0,0 +1,15 @@ +
+

Anmelden

+
+ + + + + + + + +
+

Noch kein Konto? Jetzt registrieren

+
+ diff --git a/app/Views/auth/register.php b/app/Views/auth/register.php new file mode 100644 index 0000000..526058e --- /dev/null +++ b/app/Views/auth/register.php @@ -0,0 +1,19 @@ +
+

Registrieren

+
+ + + + + + + + + + Mindestens 8 Zeichen. + + +
+

Schon registriert? Zum Login

+
+ diff --git a/app/Views/habits/dashboard.php b/app/Views/habits/dashboard.php new file mode 100644 index 0000000..b70688f --- /dev/null +++ b/app/Views/habits/dashboard.php @@ -0,0 +1,67 @@ +
+
+

Heute

+

Deine Habits

+
+ Habit erstellen +
+ +
+ + + + + + + + + +
+ + +

Noch keine Habits gefunden. Erstelle deinen ersten Habit.

+ + +
+ +
+
+ +
+

+

+ + | + Ziel: x pro Woche + +
+
+ +
+
+ + + +
+ Bearbeiten +
+ + + +
+
+
+ +
+ diff --git a/app/Views/habits/form.php b/app/Views/habits/form.php new file mode 100644 index 0000000..f12b11c --- /dev/null +++ b/app/Views/habits/form.php @@ -0,0 +1,58 @@ + + +
+

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ diff --git a/app/Views/layout/footer.php b/app/Views/layout/footer.php new file mode 100644 index 0000000..9765cc0 --- /dev/null +++ b/app/Views/layout/footer.php @@ -0,0 +1,3 @@ + + + diff --git a/app/Views/layout/header.php b/app/Views/layout/header.php new file mode 100644 index 0000000..cdd1fcf --- /dev/null +++ b/app/Views/layout/header.php @@ -0,0 +1,34 @@ + + + + + + <?= e(config('app_name')) ?> + + + +
+ Habit Tracker + +
+ +
+ +

+ + + +

+ diff --git a/app/helpers.php b/app/helpers.php new file mode 100644 index 0000000..627acfa --- /dev/null +++ b/app/helpers.php @@ -0,0 +1,82 @@ +'; +} + +function verify_csrf_token(): void +{ + $token = $_POST['csrf_token'] ?? ''; + + if (!is_string($token) || !hash_equals($_SESSION['csrf_token'] ?? '', $token)) { + http_response_code(419); + echo 'Sicherheitspruefung fehlgeschlagen. Bitte lade die Seite neu.'; + exit; + } +} diff --git a/config/config.example.php b/config/config.example.php new file mode 100644 index 0000000..c1e0d8f --- /dev/null +++ b/config/config.example.php @@ -0,0 +1,13 @@ + 'Habit Tracker', + 'base_path' => '', + 'database' => [ + 'host' => 'localhost', + 'dbname' => 'habittracker', + 'user' => 'root', + 'password' => '', + 'charset' => 'utf8mb4', + ], +]; diff --git a/database/schema.sql b/database/schema.sql new file mode 100644 index 0000000..eb9b2a9 --- /dev/null +++ b/database/schema.sql @@ -0,0 +1,55 @@ +CREATE DATABASE IF NOT EXISTS pbbfa24aal_habittracker + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; + +USE pbbfa24aal_habittracker; + +CREATE TABLE users ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(190) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE categories ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(80) NOT NULL, + color VARCHAR(20) NOT NULL DEFAULT '#176b5b' +); + +CREATE TABLE habits ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + category_id INT NULL, + title VARCHAR(120) NOT NULL, + description TEXT NULL, + target_per_week TINYINT UNSIGNED NOT NULL DEFAULT 3, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL +); + +CREATE TABLE habit_logs ( + id INT AUTO_INCREMENT PRIMARY KEY, + habit_id INT NOT NULL, + completed_on DATE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY unique_habit_day (habit_id, completed_on), + FOREIGN KEY (habit_id) REFERENCES habits(id) ON DELETE CASCADE +); + +CREATE TABLE reminders ( + id INT AUTO_INCREMENT PRIMARY KEY, + habit_id INT NOT NULL, + reminder_time TIME NOT NULL, + weekday TINYINT UNSIGNED NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + FOREIGN KEY (habit_id) REFERENCES habits(id) ON DELETE CASCADE +); + +INSERT INTO categories (name, color) VALUES + ('Gesundheit', '#176b5b'), + ('Lernen', '#365c9a'), + ('Sport', '#b95c20'), + ('Alltag', '#6b4ba3'); diff --git a/public/assets/css/style.css b/public/assets/css/style.css new file mode 100644 index 0000000..167d24c --- /dev/null +++ b/public/assets/css/style.css @@ -0,0 +1,275 @@ +:root { + --bg: #f6f7f9; + --panel: #ffffff; + --text: #172033; + --muted: #657086; + --line: #d9dee8; + --primary: #176b5b; + --primary-dark: #0f4c40; + --danger: #b42318; + --success-bg: #e8f5ee; + --error-bg: #fdecec; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font-family: Arial, Helvetica, sans-serif; + line-height: 1.5; +} + +a { + color: var(--primary); + text-decoration-thickness: 2px; +} + +.topbar { + align-items: center; + background: var(--panel); + border-bottom: 1px solid var(--line); + display: flex; + justify-content: space-between; + min-height: 64px; + padding: 0 32px; +} + +.brand { + color: var(--text); + font-size: 20px; + font-weight: 700; + text-decoration: none; +} + +.nav { + display: flex; + gap: 18px; +} + +.logout-form { + margin: 0; +} + +.link-button { + background: transparent; + color: var(--primary); + min-height: 0; + padding: 0; + text-decoration: underline; + text-decoration-thickness: 2px; +} + +.link-button:hover { + background: transparent; + color: var(--primary-dark); +} + +.page { + margin: 0 auto; + max-width: 1080px; + padding: 32px 24px; +} + +.notice, +.empty, +.auth-panel, +.form-panel { + background: var(--panel); + border: 1px solid var(--line); + border-radius: 8px; + padding: 20px; +} + +.notice.success { + background: var(--success-bg); +} + +.notice.error { + background: var(--error-bg); + color: var(--danger); +} + +.auth-panel, +.form-panel { + max-width: 520px; +} + +.dashboard-head { + align-items: center; + display: flex; + justify-content: space-between; + margin-bottom: 24px; +} + +.eyebrow { + color: var(--muted); + font-size: 14px; + font-weight: 700; + margin: 0 0 4px; + text-transform: uppercase; +} + +h1, +h2 { + line-height: 1.2; + margin: 0; +} + +h1 { + font-size: 34px; +} + +h2 { + font-size: 20px; +} + +.button, +button { + background: var(--primary); + border: 0; + border-radius: 6px; + color: #ffffff; + cursor: pointer; + display: inline-block; + font: inherit; + font-weight: 700; + min-height: 44px; + padding: 10px 16px; + text-decoration: none; +} + +button:hover, +.button:hover { + background: var(--primary-dark); +} + +button.danger { + background: var(--danger); +} + +.form, +.filters { + display: grid; + gap: 10px; +} + +.form label, +.filters label { + font-weight: 700; +} + +.form label span { + color: var(--muted); + font-weight: 400; +} + +input, +select, +textarea { + border: 1px solid var(--line); + border-radius: 6px; + font: inherit; + min-height: 44px; + padding: 10px 12px; + width: 100%; +} + +textarea { + resize: vertical; +} + +.field-error { + color: var(--danger); + font-weight: 700; +} + +input.input-error { + border-color: var(--danger); +} + +.filters { + align-items: end; + background: var(--panel); + border: 1px solid var(--line); + border-radius: 8px; + grid-template-columns: 1fr 1fr auto; + margin-bottom: 20px; + padding: 16px; +} + +.habit-list { + display: grid; + gap: 14px; +} + +.habit-card { + align-items: center; + background: var(--panel); + border: 1px solid var(--line); + border-radius: 8px; + display: flex; + gap: 18px; + justify-content: space-between; + padding: 18px; +} + +.habit-card.done { + border-color: var(--primary); +} + +.habit-main { + display: flex; + gap: 14px; +} + +.habit-main p { + color: var(--muted); + margin: 8px 0; +} + +.category-dot { + border-radius: 999px; + flex: 0 0 14px; + height: 14px; + margin-top: 5px; + width: 14px; +} + +.habit-actions { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: flex-end; +} + +@media (max-width: 720px) { + .topbar, + .dashboard-head, + .habit-card { + align-items: stretch; + flex-direction: column; + } + + .topbar { + gap: 12px; + padding: 16px 20px; + } + + .nav { + flex-wrap: wrap; + } + + .filters { + grid-template-columns: 1fr; + } + + .habit-actions { + justify-content: flex-start; + } +} + diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..f47fe20 --- /dev/null +++ b/public/index.php @@ -0,0 +1,50 @@ + true, + 'samesite' => 'Lax', + 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', +]); +session_start(); + +require __DIR__ . '/../app/helpers.php'; +require __DIR__ . '/../app/Core/Database.php'; +require __DIR__ . '/../app/Core/View.php'; +require __DIR__ . '/../app/Models/User.php'; +require __DIR__ . '/../app/Models/Category.php'; +require __DIR__ . '/../app/Models/Habit.php'; +require __DIR__ . '/../app/Controllers/AuthController.php'; +require __DIR__ . '/../app/Controllers/HabitController.php'; + +$route = $_GET['route'] ?? 'dashboard'; +$method = $_SERVER['REQUEST_METHOD']; + +$routes = [ + 'GET' => [ + 'login' => [AuthController::class, 'showLogin'], + 'register' => [AuthController::class, 'showRegister'], + 'dashboard' => [HabitController::class, 'dashboard'], + 'habits/create' => [HabitController::class, 'create'], + 'habits/edit' => [HabitController::class, 'edit'], + ], + 'POST' => [ + 'login' => [AuthController::class, 'login'], + 'register' => [AuthController::class, 'register'], + 'logout' => [AuthController::class, 'logout'], + 'habits/store' => [HabitController::class, 'store'], + 'habits/update' => [HabitController::class, 'update'], + 'habits/delete' => [HabitController::class, 'delete'], + 'habits/toggle' => [HabitController::class, 'toggle'], + ], +]; + +if (!isset($routes[$method][$route])) { + http_response_code(404); + echo 'Seite nicht gefunden.'; + exit; +} + +[$controllerClass, $action] = $routes[$method][$route]; +(new $controllerClass())->$action(); From 1cdef43a87e9ec1536ead3c0fa8ea43f7a366418 Mon Sep 17 00:00:00 2001 From: PBBFA24AAL Date: Fri, 10 Jul 2026 16:23:50 +0200 Subject: [PATCH 03/15] Add weekly progress and validation --- README.md | 6 ++++-- app/Controllers/HabitController.php | 11 ++++++++++- app/Models/Category.php | 10 +++++++++- app/Models/Habit.php | 8 ++++++-- app/Views/habits/dashboard.php | 3 +-- app/Views/habits/form.php | 12 +++++++++--- 6 files changed, 39 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index c9e2e9c..efdef4d 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Habit Tracker ist eine PHP-Webanwendung, mit der Benutzer eigene Gewohnheiten an - Habits erstellen, bearbeiten, loeschen und fuer heute abhaken - Kategorien und Statusfilter im Dashboard - Tagesprotokoll ueber `habit_logs` +- Wochenfortschritt im Dashboard gegen das persoenliche Wochenziel - Responsive Oberflaeche fuer Desktop und Smartphone - Serverseitige Validierung und klare Fehlermeldungen - Schutz gegen SQL-Injection durch PDO Prepared Statements @@ -67,8 +68,9 @@ Die Datenbank besteht aus 5 Tabellen: 3. Habit mit Kategorie und Wochenziel erstellen. 4. Habit im Dashboard filtern. 5. Habit fuer heute abhaken. -6. Habit bearbeiten und loeschen. -7. Seite auf Smartphone-Breite pruefen. +6. Wochenfortschritt im Dashboard kontrollieren. +7. Habit bearbeiten und loeschen. +8. Seite auf Smartphone-Breite pruefen. ## Bewertungsbezug diff --git a/app/Controllers/HabitController.php b/app/Controllers/HabitController.php index 3db81a5..1a0efef 100644 --- a/app/Controllers/HabitController.php +++ b/app/Controllers/HabitController.php @@ -119,6 +119,16 @@ class HabitController if ($title === '') { $errors['title'] = 'Bitte gib einen Namen fuer den Habit ein.'; + } elseif (mb_strlen($title) > 120) { + $errors['title'] = 'Der Name darf maximal 120 Zeichen lang sein.'; + } + + if (mb_strlen($description) > 1000) { + $errors['description'] = 'Die Beschreibung darf maximal 1000 Zeichen lang sein.'; + } + + if ($categoryId !== '' && !Category::exists((int) $categoryId)) { + $errors['category_id'] = 'Bitte waehle eine gueltige Kategorie aus.'; } if ($targetPerWeek < 1 || $targetPerWeek > 7) { @@ -134,4 +144,3 @@ class HabitController ]; } } - diff --git a/app/Models/Category.php b/app/Models/Category.php index 8982b81..c984432 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -1,4 +1,4 @@ -query('SELECT id, name, color FROM categories ORDER BY name') ->fetchAll(); } + + public static function exists(int $id): bool + { + $statement = Database::connection()->prepare('SELECT id FROM categories WHERE id = :id LIMIT 1'); + $statement->execute(['id' => $id]); + + return (bool) $statement->fetch(); + } } diff --git a/app/Models/Habit.php b/app/Models/Habit.php index 77808ec..53f0289 100644 --- a/app/Models/Habit.php +++ b/app/Models/Habit.php @@ -1,4 +1,4 @@ -

| - Ziel: x pro Woche + Woche: / erledigt @@ -64,4 +64,3 @@ - diff --git a/app/Views/habits/form.php b/app/Views/habits/form.php index f12b11c..6b6ac43 100644 --- a/app/Views/habits/form.php +++ b/app/Views/habits/form.php @@ -14,6 +14,7 @@ id="title" name="title" type="text" + maxlength="120" class="" value="" required @@ -24,10 +25,13 @@ - + + + + - + + + - From f469086bc039bede6fbca3074b92123af7792d66 Mon Sep 17 00:00:00 2001 From: PBBFA24AAL Date: Fri, 10 Jul 2026 16:49:32 +0200 Subject: [PATCH 04/15] Fix PHP file encoding --- app/Controllers/AuthController.php | 2 +- app/Controllers/HabitController.php | 2 +- app/Core/Database.php | 3 --- app/Models/Category.php | 2 +- app/Models/Habit.php | 2 +- app/Views/auth/login.php | 2 +- app/Views/auth/register.php | 2 +- app/Views/habits/dashboard.php | 2 +- app/Views/habits/form.php | 2 +- app/Views/layout/header.php | 2 +- app/helpers.php | 2 +- config/config.example.php | 2 +- public/index.php | 2 +- 13 files changed, 12 insertions(+), 15 deletions(-) diff --git a/app/Controllers/AuthController.php b/app/Controllers/AuthController.php index 7d7cbda..af55379 100644 --- a/app/Controllers/AuthController.php +++ b/app/Controllers/AuthController.php @@ -1,4 +1,4 @@ - +

Anmelden

diff --git a/app/Views/auth/register.php b/app/Views/auth/register.php index 526058e..a64e292 100644 --- a/app/Views/auth/register.php +++ b/app/Views/auth/register.php @@ -1,4 +1,4 @@ -
+

Registrieren

diff --git a/app/Views/habits/dashboard.php b/app/Views/habits/dashboard.php index 6504bcd..15c6df5 100644 --- a/app/Views/habits/dashboard.php +++ b/app/Views/habits/dashboard.php @@ -1,4 +1,4 @@ -
+

Heute

Deine Habits

diff --git a/app/Views/habits/form.php b/app/Views/habits/form.php index 6b6ac43..e32f02b 100644 --- a/app/Views/habits/form.php +++ b/app/Views/habits/form.php @@ -1,4 +1,4 @@ - +

diff --git a/app/Views/layout/header.php b/app/Views/layout/header.php index cdd1fcf..0e43b3d 100644 --- a/app/Views/layout/header.php +++ b/app/Views/layout/header.php @@ -1,4 +1,4 @@ - + diff --git a/app/helpers.php b/app/helpers.php index 627acfa..7be5164 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -1,4 +1,4 @@ - 'Habit Tracker', diff --git a/public/index.php b/public/index.php index f47fe20..0880de6 100644 --- a/public/index.php +++ b/public/index.php @@ -1,4 +1,4 @@ - Date: Fri, 10 Jul 2026 18:03:37 +0200 Subject: [PATCH 05/15] Normalize gitignore encoding --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a0b81a9..358fc5b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -config/config.php +config/config.php .DS_Store Thumbs.db From a055eb2b0a14944386801a23565ce4c54b0955a0 Mon Sep 17 00:00:00 2001 From: PBBFA24AAL Date: Sun, 9 Aug 2026 02:18:23 +0200 Subject: [PATCH 06/15] Add ER diagram and test protocol --- docs/ER-Diagramm.svg | 89 +++++++++++++++++++++++++++++++++++++++++++ docs/Testprotokoll.md | 27 +++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 docs/ER-Diagramm.svg create mode 100644 docs/Testprotokoll.md diff --git a/docs/ER-Diagramm.svg b/docs/ER-Diagramm.svg new file mode 100644 index 0000000..3351cb0 --- /dev/null +++ b/docs/ER-Diagramm.svg @@ -0,0 +1,89 @@ + + ER-Diagramm des Habit Trackers + Datenmodell mit den Tabellen users, categories, habits, habit_logs und reminders. + + + + + + + + Habit Tracker — ER-Diagramm + Fünf Tabellen mit klaren Fremdschlüssel-Beziehungen + + + 1 : n + + 1 : n + + 1 : n + + 1 : n + + + + + users + PK id + name + email UNIQUE + password_hash + created_at + Ein Konto besitzt mehrere Habits. + + + + + + categories + PK id + name + color + Kategorie eines Habits ist optional. + + + + + + habits + PK id + FK user_id → users.id + FK category_id → categories.id + title · description + target_per_week + created_at + Zentrale Tabelle der Anwendung. + + + + + + habit_logs + PK id + FK habit_id → habits.id + completed_on · created_at + UNIQUE: habit_id + completed_on + + + + + + reminders + PK id + FK habit_id → habits.id + reminder_time · weekday · is_active + Erinnerung gehört zu genau einem Habit. + + diff --git a/docs/Testprotokoll.md b/docs/Testprotokoll.md new file mode 100644 index 0000000..6bed4ef --- /dev/null +++ b/docs/Testprotokoll.md @@ -0,0 +1,27 @@ +# Testprotokoll — Habit Tracker + +**Projekt:** Habit Tracker +**Teststand:** `develop` +**Ausführung:** Vor der Abgabe jeden Test mit einem aktuellen Screenshot oder einer kurzen Notiz nachweisen. + +| ID | Testfall | Schritte | Erwartetes Ergebnis | Tatsächliches Ergebnis | Status | +| --- | --- | --- | --- | --- | --- | +| T01 | Registrierung mit gültigen Daten | Name, gültige E-Mail und Passwort mit mindestens 8 Zeichen eingeben. | Konto wird angelegt; Hinweis führt zum Login. | | Offen | +| T02 | Registrierung mit ungültigen Daten | Leeren Namen, ungültige E-Mail oder zu kurzes Passwort eingeben. | Verständliche Fehlermeldung; Konto wird nicht angelegt. | | Offen | +| T03 | Doppelte E-Mail-Adresse | Bereits registrierte E-Mail erneut registrieren. | Hinweis, dass die E-Mail bereits verwendet wird. | | Offen | +| T04 | Login mit falschem Passwort | Richtige E-Mail, falsches Passwort eingeben. | Kein Login; neutrale Fehlermeldung. | | Offen | +| T05 | Habit erstellen | Eingeloggt Habit mit Kategorie und Wochenziel erstellen. | Habit erscheint im Dashboard mit Kategorie und Wochenziel. | | Offen | +| T06 | Habit-Validierung | Leeren Namen, mehr als 120 Zeichen oder Wochenziel außerhalb 1–7 eingeben. | Feldbezogene Fehlermeldung; Eingaben bleiben sichtbar. | | Offen | +| T07 | Filter | Kategorie- und Statusfilter nacheinander verwenden. | Nur passende Habits werden angezeigt. | | Offen | +| T08 | Tagesprotokoll und Wochenfortschritt | Habit abhaken und die Seite erneut laden. | Status wechselt; Wochenfortschritt erhöht sich genau einmal. | | Offen | +| T09 | Schutz fremder Habits | Mit zweitem Konto eine fremde Habit-ID für Bearbeiten/Löschen aufrufen. | Fremde Habit ist nicht sichtbar, nicht änderbar und nicht löschbar. | | Offen | +| T10 | CSRF-Schutz | Formular ohne oder mit verändertem CSRF-Token absenden. | Aktion wird abgelehnt; keine Daten werden geändert. | | Offen | +| T11 | Responsive Ansicht | Dashboard und Formular bei Smartphone-Breite prüfen. | Navigation, Formulare und Aktionen bleiben ohne horizontales Scrollen bedienbar. | | Offen | +| T12 | Reminder-Funktion | Nach Umsetzung: Erinnerung mit Uhrzeit und Wochentag anlegen, ändern und löschen. | Erinnerung wird korrekt angezeigt und gespeichert. | | Offen | + +## Abnahme vor der Präsentation + +- Alle Zeilen T01–T11 ausführen und das Ergebnis eintragen. +- Bei Fehlerfällen mindestens einen Screenshot sichern. +- T12 erst nach vollständiger Umsetzung der Reminder-Funktion als bestanden markieren. +- Screenshots im Ordner `docs/screenshots/` eindeutig benennen, zum Beispiel `01-login.png` oder `05-wochenfortschritt.png`. From dc8407767e976c7ff0a16e7df53ae02fb4c81f16 Mon Sep 17 00:00:00 2001 From: PBBFA24AAL Date: Sun, 9 Aug 2026 02:24:11 +0200 Subject: [PATCH 07/15] Add responsive interface screenshots --- docs/screenshots/01-login-desktop.png | Bin 0 -> 23973 bytes docs/screenshots/02-login-mobil.png | Bin 0 -> 14593 bytes docs/screenshots/03-registrierung-desktop.png | Bin 0 -> 16921 bytes docs/screenshots/04-registrierung-mobil.png | Bin 0 -> 15436 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/screenshots/01-login-desktop.png create mode 100644 docs/screenshots/02-login-mobil.png create mode 100644 docs/screenshots/03-registrierung-desktop.png create mode 100644 docs/screenshots/04-registrierung-mobil.png diff --git a/docs/screenshots/01-login-desktop.png b/docs/screenshots/01-login-desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..85f87674d067db20e9426a8c69fb7d8b296c3512 GIT binary patch literal 23973 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/02-login-mobil.png b/docs/screenshots/02-login-mobil.png new file mode 100644 index 0000000000000000000000000000000000000000..c34aa8e5d3cb96dedc599beeb1b28a53b70499c4 GIT binary patch literal 14593 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/03-registrierung-desktop.png b/docs/screenshots/03-registrierung-desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..acdbeefcb93f1392c9ce1833445f6cf2a83ae1e9 GIT binary patch literal 16921 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/04-registrierung-mobil.png b/docs/screenshots/04-registrierung-mobil.png new file mode 100644 index 0000000000000000000000000000000000000000..35ee9c3714b1995402d32c381e1fdf6432113078 GIT binary patch literal 15436 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 Date: Sun, 9 Aug 2026 02:29:13 +0200 Subject: [PATCH 08/15] Add protected reminder management --- app/Controllers/ReminderController.php | 187 +++++++++++++++++++++++++ app/Models/Reminder.php | 96 +++++++++++++ app/Views/reminders/form.php | 69 +++++++++ public/index.php | 8 ++ 4 files changed, 360 insertions(+) create mode 100644 app/Controllers/ReminderController.php create mode 100644 app/Models/Reminder.php create mode 100644 app/Views/reminders/form.php diff --git a/app/Controllers/ReminderController.php b/app/Controllers/ReminderController.php new file mode 100644 index 0000000..2651425 --- /dev/null +++ b/app/Controllers/ReminderController.php @@ -0,0 +1,187 @@ +queryId('habit_id'), current_user_id()); + + if (!$habit) { + flash('error', 'Der Habit fuer die Erinnerung wurde nicht gefunden.'); + redirect('dashboard'); + } + + View::render('reminders/form', [ + 'habit' => $habit, + 'reminder' => ['habit_id' => $habit['id'], 'is_active' => 1], + 'errors' => [], + ]); + } + + public function store(): void + { + require_login(); + verify_csrf_token(); + + $habit = Habit::findForUser($this->postId('habit_id'), current_user_id()); + + if (!$habit) { + flash('error', 'Der Habit fuer die Erinnerung wurde nicht gefunden.'); + redirect('dashboard'); + } + + $data = $this->validatedData(); + + if (!empty($data['errors'])) { + View::render('reminders/form', [ + 'habit' => $habit, + 'reminder' => array_merge($_POST, $data), + 'errors' => $data['errors'], + ]); + return; + } + + if (!Reminder::createForHabit((int) $habit['id'], current_user_id(), $data)) { + flash('error', 'Die Erinnerung konnte nicht gespeichert werden.'); + redirect('dashboard'); + } + + flash('success', 'Erinnerung wurde erstellt.'); + redirect('dashboard'); + } + + public function edit(): void + { + require_login(); + + $reminder = Reminder::findForUser($this->queryId('id'), current_user_id()); + + if (!$reminder) { + flash('error', 'Erinnerung wurde nicht gefunden.'); + redirect('dashboard'); + } + + $habit = Habit::findForUser((int) $reminder['habit_id'], current_user_id()); + + View::render('reminders/form', [ + 'habit' => $habit, + 'reminder' => $reminder, + 'errors' => [], + ]); + } + + public function update(): void + { + require_login(); + verify_csrf_token(); + + $reminder = Reminder::findForUser($this->postId('id'), current_user_id()); + + if (!$reminder) { + flash('error', 'Erinnerung wurde nicht gefunden.'); + redirect('dashboard'); + } + + $habit = Habit::findForUser((int) $reminder['habit_id'], current_user_id()); + $data = $this->validatedData(); + + if (!empty($data['errors'])) { + View::render('reminders/form', [ + 'habit' => $habit, + 'reminder' => array_merge($reminder, $data), + 'errors' => $data['errors'], + ]); + return; + } + + if (!Reminder::update((int) $reminder['id'], current_user_id(), $data)) { + flash('error', 'Die Erinnerung konnte nicht gespeichert werden.'); + redirect('dashboard'); + } + + flash('success', 'Erinnerung wurde gespeichert.'); + redirect('dashboard'); + } + + public function delete(): void + { + require_login(); + verify_csrf_token(); + + $reminder = Reminder::findForUser($this->postId('id'), current_user_id()); + + if (!$reminder || !Reminder::delete((int) $reminder['id'], current_user_id())) { + flash('error', 'Die Erinnerung konnte nicht geloescht werden.'); + redirect('dashboard'); + } + + flash('success', 'Erinnerung wurde geloescht.'); + redirect('dashboard'); + } + + public function toggle(): void + { + require_login(); + verify_csrf_token(); + + $reminder = Reminder::findForUser($this->postId('id'), current_user_id()); + + if (!$reminder || !Reminder::toggle((int) $reminder['id'], current_user_id())) { + flash('error', 'Der Status der Erinnerung konnte nicht geaendert werden.'); + redirect('dashboard'); + } + + flash('success', $reminder['is_active'] ? 'Erinnerung pausiert.' : 'Erinnerung aktiviert.'); + redirect('dashboard'); + } + + private function validatedData(): array + { + $time = $this->postString('reminder_time'); + $weekdayValue = $this->postString('weekday'); + $activeValue = $this->postString('is_active'); + $errors = []; + + if (!preg_match('/^(?:[01]\\d|2[0-3]):[0-5]\\d$/', $time)) { + $errors['reminder_time'] = 'Bitte gib eine Uhrzeit im Format HH:MM ein.'; + } + + if ($weekdayValue !== '' && !preg_match('/^[1-7]$/', $weekdayValue)) { + $errors['weekday'] = 'Bitte waehle einen gueltigen Wochentag aus.'; + } + + if (!in_array($activeValue, ['0', '1'], true)) { + $errors['is_active'] = 'Bitte waehle einen gueltigen Status aus.'; + } + + return [ + 'reminder_time' => $time, + 'weekday' => $weekdayValue === '' ? null : (int) $weekdayValue, + 'is_active' => $activeValue === '0' ? 0 : 1, + 'errors' => $errors, + ]; + } + + private function queryId(string $key): int + { + $value = $_GET[$key] ?? ''; + + return is_string($value) && preg_match('/^[1-9]\\d*$/', $value) ? (int) $value : 0; + } + + private function postId(string $key): int + { + $value = $_POST[$key] ?? ''; + + return is_string($value) && preg_match('/^[1-9]\\d*$/', $value) ? (int) $value : 0; + } + + private function postString(string $key): string + { + $value = $_POST[$key] ?? ''; + + return is_string($value) ? trim($value) : ''; + } +} diff --git a/app/Models/Reminder.php b/app/Models/Reminder.php new file mode 100644 index 0000000..daebd66 --- /dev/null +++ b/app/Models/Reminder.php @@ -0,0 +1,96 @@ +prepare( + 'SELECT r.*, h.title AS habit_title + FROM reminders r + INNER JOIN habits h ON h.id = r.habit_id + WHERE r.id = :id AND h.user_id = :user_id + LIMIT 1' + ); + $statement->execute(['id' => $id, 'user_id' => $userId]); + + $reminder = $statement->fetch(); + return $reminder ?: null; + } + + public static function allForUser(int $userId): array + { + $statement = Database::connection()->prepare( + 'SELECT r.*, h.title AS habit_title + FROM reminders r + INNER JOIN habits h ON h.id = r.habit_id + WHERE h.user_id = :user_id + ORDER BY h.title ASC, r.is_active DESC, r.weekday ASC, r.reminder_time ASC' + ); + $statement->execute(['user_id' => $userId]); + + return $statement->fetchAll(); + } + + public static function createForHabit(int $habitId, int $userId, array $data): bool + { + $statement = Database::connection()->prepare( + 'INSERT INTO reminders (habit_id, reminder_time, weekday, is_active) + SELECT id, :reminder_time, :weekday, :is_active + FROM habits + WHERE id = :habit_id AND user_id = :user_id' + ); + $statement->execute([ + 'habit_id' => $habitId, + 'user_id' => $userId, + 'reminder_time' => $data['reminder_time'], + 'weekday' => $data['weekday'], + 'is_active' => $data['is_active'], + ]); + + return $statement->rowCount() === 1; + } + + public static function update(int $id, int $userId, array $data): bool + { + $statement = Database::connection()->prepare( + 'UPDATE reminders r + INNER JOIN habits h ON h.id = r.habit_id + SET r.reminder_time = :reminder_time, + r.weekday = :weekday, + r.is_active = :is_active + WHERE r.id = :id AND h.user_id = :user_id' + ); + + return $statement->execute([ + 'id' => $id, + 'user_id' => $userId, + 'reminder_time' => $data['reminder_time'], + 'weekday' => $data['weekday'], + 'is_active' => $data['is_active'], + ]); + } + + public static function delete(int $id, int $userId): bool + { + $statement = Database::connection()->prepare( + 'DELETE r + FROM reminders r + INNER JOIN habits h ON h.id = r.habit_id + WHERE r.id = :id AND h.user_id = :user_id' + ); + + return $statement->execute(['id' => $id, 'user_id' => $userId]); + } + + public static function toggle(int $id, int $userId): bool + { + $statement = Database::connection()->prepare( + 'UPDATE reminders r + INNER JOIN habits h ON h.id = r.habit_id + SET r.is_active = IF(r.is_active = 1, 0, 1) + WHERE r.id = :id AND h.user_id = :user_id' + ); + + return $statement->execute(['id' => $id, 'user_id' => $userId]); + } +} diff --git a/app/Views/reminders/form.php b/app/Views/reminders/form.php new file mode 100644 index 0000000..0b6086a --- /dev/null +++ b/app/Views/reminders/form.php @@ -0,0 +1,69 @@ + 'Montag', + '2' => 'Dienstag', + '3' => 'Mittwoch', + '4' => 'Donnerstag', + '5' => 'Freitag', + '6' => 'Samstag', + '7' => 'Sonntag', +]; +?> + +
+

+

Habit:

+ + + + + + + + + + + + + + + + + + Ohne Auswahl wird die Erinnerung taeglich angezeigt. + + + + + + + + + + + + + +

Zurueck zum Dashboard

+
diff --git a/public/index.php b/public/index.php index 0880de6..d171ea8 100644 --- a/public/index.php +++ b/public/index.php @@ -15,8 +15,10 @@ require __DIR__ . '/../app/Core/View.php'; require __DIR__ . '/../app/Models/User.php'; require __DIR__ . '/../app/Models/Category.php'; require __DIR__ . '/../app/Models/Habit.php'; +require __DIR__ . '/../app/Models/Reminder.php'; require __DIR__ . '/../app/Controllers/AuthController.php'; require __DIR__ . '/../app/Controllers/HabitController.php'; +require __DIR__ . '/../app/Controllers/ReminderController.php'; $route = $_GET['route'] ?? 'dashboard'; $method = $_SERVER['REQUEST_METHOD']; @@ -28,6 +30,8 @@ $routes = [ 'dashboard' => [HabitController::class, 'dashboard'], 'habits/create' => [HabitController::class, 'create'], 'habits/edit' => [HabitController::class, 'edit'], + 'reminders/create' => [ReminderController::class, 'create'], + 'reminders/edit' => [ReminderController::class, 'edit'], ], 'POST' => [ 'login' => [AuthController::class, 'login'], @@ -37,6 +41,10 @@ $routes = [ 'habits/update' => [HabitController::class, 'update'], 'habits/delete' => [HabitController::class, 'delete'], 'habits/toggle' => [HabitController::class, 'toggle'], + 'reminders/store' => [ReminderController::class, 'store'], + 'reminders/update' => [ReminderController::class, 'update'], + 'reminders/delete' => [ReminderController::class, 'delete'], + 'reminders/toggle' => [ReminderController::class, 'toggle'], ], ]; From 3d48595ee9c1ed9d0f23d24cbf32a40a1a32116a Mon Sep 17 00:00:00 2001 From: PBBFA24AAL Date: Sun, 9 Aug 2026 02:33:02 +0200 Subject: [PATCH 09/15] Show reminders on dashboard --- app/Controllers/HabitController.php | 10 +++- app/Views/habits/dashboard.php | 54 ++++++++++++++++++ public/assets/css/style.css | 88 ++++++++++++++++++++++++++++- 3 files changed, 150 insertions(+), 2 deletions(-) diff --git a/app/Controllers/HabitController.php b/app/Controllers/HabitController.php index 5991b3f..9b488a0 100644 --- a/app/Controllers/HabitController.php +++ b/app/Controllers/HabitController.php @@ -6,15 +6,23 @@ class HabitController { require_login(); + $userId = (int) current_user_id(); + $filters = [ 'category_id' => $_GET['category_id'] ?? '', 'status' => $_GET['status'] ?? '', ]; + $remindersByHabit = []; + foreach (Reminder::allForUser($userId) as $reminder) { + $remindersByHabit[(int) $reminder['habit_id']][] = $reminder; + } + View::render('habits/dashboard', [ - 'habits' => Habit::allForUser(current_user_id(), $filters), + 'habits' => Habit::allForUser($userId, $filters), 'categories' => Category::all(), 'filters' => $filters, + 'remindersByHabit' => $remindersByHabit, ]); } diff --git a/app/Views/habits/dashboard.php b/app/Views/habits/dashboard.php index 15c6df5..0547996 100644 --- a/app/Views/habits/dashboard.php +++ b/app/Views/habits/dashboard.php @@ -1,3 +1,16 @@ + 'Montag', + 2 => 'Dienstag', + 3 => 'Mittwoch', + 4 => 'Donnerstag', + 5 => 'Freitag', + 6 => 'Samstag', + 7 => 'Sonntag', +]; +?> +

Heute

@@ -35,6 +48,7 @@
+
@@ -61,6 +75,46 @@
+ +
+
+

Erinnerungen

+ Erinnerung hinzufuegen +
+ + +

Noch keine Erinnerung eingerichtet.

+ +
    + + +
  • + + + +
    +
    + + + +
    + Bearbeiten +
    + + + +
    +
    +
  • + +
+ +
diff --git a/public/assets/css/style.css b/public/assets/css/style.css index 167d24c..d411060 100644 --- a/public/assets/css/style.css +++ b/public/assets/css/style.css @@ -114,7 +114,8 @@ a { } h1, -h2 { +h2, +h3 { line-height: 1.2; margin: 0; } @@ -127,6 +128,10 @@ h2 { font-size: 20px; } +h3 { + font-size: 16px; +} + .button, button { background: var(--primary); @@ -212,6 +217,7 @@ input.input-error { border: 1px solid var(--line); border-radius: 8px; display: flex; + flex-wrap: wrap; gap: 18px; justify-content: space-between; padding: 18px; @@ -223,6 +229,7 @@ input.input-error { .habit-main { display: flex; + flex: 1 1 480px; gap: 14px; } @@ -247,6 +254,71 @@ input.input-error { justify-content: flex-end; } +.habit-reminders { + border-top: 1px solid var(--line); + flex-basis: 100%; + margin-top: 2px; + padding-top: 14px; +} + +.reminder-heading { + align-items: center; + display: flex; + gap: 12px; + justify-content: space-between; +} + +.reminder-empty, +.form-help, +.form small { + color: var(--muted); +} + +.reminder-list { + display: grid; + gap: 8px; + list-style: none; + margin: 12px 0 0; + padding: 0; +} + +.reminder-item { + align-items: center; + background: var(--bg); + border-radius: 6px; + display: grid; + gap: 12px; + grid-template-columns: 72px minmax(100px, 1fr) auto auto; + padding: 10px 12px; +} + +.reminder-item.paused { + opacity: 0.68; +} + +.reminder-time { + font-variant-numeric: tabular-nums; +} + +.reminder-status { + color: var(--muted); + font-size: 14px; + font-weight: 700; +} + +.reminder-actions { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: flex-end; +} + +.reminder-actions button { + min-height: 36px; + padding: 6px 10px; +} + @media (max-width: 720px) { .topbar, .dashboard-head, @@ -271,5 +343,19 @@ input.input-error { .habit-actions { justify-content: flex-start; } + + .reminder-item { + align-items: start; + grid-template-columns: 72px 1fr; + } + + .reminder-status, + .reminder-actions { + grid-column: 1 / -1; + } + + .reminder-actions { + justify-content: flex-start; + } } From f09fdfa25ea3439883e52da595c526566114c4ba Mon Sep 17 00:00:00 2001 From: PBBFA24AAL Date: Sun, 9 Aug 2026 02:38:16 +0200 Subject: [PATCH 10/15] Improve account validation and feedback --- app/Controllers/AuthController.php | 119 ++++++++++++++++++++++++----- app/Views/auth/login.php | 37 ++++++++- app/Views/auth/register.php | 55 ++++++++++++- 3 files changed, 185 insertions(+), 26 deletions(-) diff --git a/app/Controllers/AuthController.php b/app/Controllers/AuthController.php index af55379..7b11963 100644 --- a/app/Controllers/AuthController.php +++ b/app/Controllers/AuthController.php @@ -4,58 +4,107 @@ class AuthController { public function showLogin(): void { - View::render('auth/login'); + View::render('auth/login', [ + 'values' => ['email' => ''], + 'errors' => [], + ]); } public function login(): void { verify_csrf_token(); - $email = trim($_POST['email'] ?? ''); - $password = $_POST['password'] ?? ''; + $email = $this->postString('email'); + $password = $this->postPassword(); + $errors = []; - if (!filter_var($email, FILTER_VALIDATE_EMAIL) || $password === '') { - flash('error', 'Bitte gib eine gueltige E-Mail und dein Passwort ein.'); - redirect('login'); + if (!filter_var($email, FILTER_VALIDATE_EMAIL) || mb_strlen($email) > 190) { + $errors['email'] = 'Bitte gib eine gueltige E-Mail-Adresse ein.'; } - $user = User::findByEmail($email); + if ($password === '') { + $errors['password'] = 'Bitte gib dein Passwort ein.'; + } + + if (!empty($errors)) { + $this->renderLogin($email, $errors); + return; + } + + try { + $user = User::findByEmail($email); + } catch (PDOException $exception) { + $this->renderLogin($email, ['form' => 'Die Anmeldung ist gerade nicht verfuegbar. Bitte versuche es spaeter erneut.']); + return; + } if (!$user || !password_verify($password, $user['password_hash'])) { - flash('error', 'E-Mail oder Passwort ist nicht korrekt.'); - redirect('login'); + $this->renderLogin($email, ['form' => 'E-Mail oder Passwort ist nicht korrekt.']); + return; } session_regenerate_id(true); $_SESSION['user_id'] = (int) $user['id']; $_SESSION['user_name'] = $user['name']; + unset($_SESSION['csrf_token']); redirect('dashboard'); } public function showRegister(): void { - View::render('auth/register'); + View::render('auth/register', [ + 'values' => ['name' => '', 'email' => ''], + 'errors' => [], + ]); } public function register(): void { verify_csrf_token(); - $name = trim($_POST['name'] ?? ''); - $email = trim($_POST['email'] ?? ''); - $password = $_POST['password'] ?? ''; + $name = $this->postString('name'); + $email = $this->postString('email'); + $password = $this->postPassword(); + $errors = []; - if ($name === '' || !filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($password) < 8) { - flash('error', 'Bitte Name, gueltige E-Mail und ein Passwort mit mindestens 8 Zeichen eingeben.'); - redirect('register'); + if ($name === '') { + $errors['name'] = 'Bitte gib deinen Namen ein.'; + } elseif (mb_strlen($name) > 100) { + $errors['name'] = 'Der Name darf maximal 100 Zeichen lang sein.'; } - if (User::findByEmail($email)) { - flash('error', 'Diese E-Mail ist bereits registriert.'); - redirect('register'); + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + $errors['email'] = 'Bitte gib eine gueltige E-Mail-Adresse ein.'; + } elseif (mb_strlen($email) > 190) { + $errors['email'] = 'Die E-Mail-Adresse darf maximal 190 Zeichen lang sein.'; + } + + if (strlen($password) < 8) { + $errors['password'] = 'Das Passwort muss mindestens 8 Zeichen lang sein.'; + } + + if (!empty($errors)) { + $this->renderRegister($name, $email, $errors); + return; + } + + try { + if (User::findByEmail($email)) { + $this->renderRegister($name, $email, ['email' => 'Diese E-Mail ist bereits registriert.']); + return; + } + + $created = User::create($name, $email, $password); + } catch (PDOException $exception) { + $this->renderRegister($name, $email, ['form' => 'Das Konto konnte nicht erstellt werden. Bitte versuche es spaeter erneut.']); + return; + } + + if (!$created) { + $this->renderRegister($name, $email, ['form' => 'Das Konto konnte nicht erstellt werden. Bitte versuche es spaeter erneut.']); + return; } - User::create($name, $email, $password); flash('success', 'Registrierung erfolgreich. Du kannst dich jetzt anmelden.'); redirect('login'); } @@ -72,4 +121,34 @@ class AuthController session_destroy(); redirect('login'); } + + private function renderLogin(string $email, array $errors): void + { + View::render('auth/login', [ + 'values' => ['email' => $email], + 'errors' => $errors, + ]); + } + + private function renderRegister(string $name, string $email, array $errors): void + { + View::render('auth/register', [ + 'values' => ['name' => $name, 'email' => $email], + 'errors' => $errors, + ]); + } + + private function postString(string $key): string + { + $value = $_POST[$key] ?? ''; + + return is_string($value) ? trim($value) : ''; + } + + private function postPassword(): string + { + $value = $_POST['password'] ?? ''; + + return is_string($value) ? $value : ''; + } } diff --git a/app/Views/auth/login.php b/app/Views/auth/login.php index 519d511..b49bc84 100644 --- a/app/Views/auth/login.php +++ b/app/Views/auth/login.php @@ -1,12 +1,45 @@ + '']; +$errors = $errors ?? []; +?> +

Anmelden

+ + +
- + + required + > + + + - + + required + > + + +
diff --git a/app/Views/auth/register.php b/app/Views/auth/register.php index a64e292..14cf9e1 100644 --- a/app/Views/auth/register.php +++ b/app/Views/auth/register.php @@ -1,16 +1,63 @@ + '', 'email' => '']; +$errors = $errors ?? []; +?> +

Registrieren

+ + +
- + + required + > + + + - + + required + > + + + - - Mindestens 8 Zeichen. + + required + > + Mindestens 8 Zeichen. + + +
From b4811f4bd02030644d3d00a8f660d53d08672522 Mon Sep 17 00:00:00 2001 From: arslandevelopments Date: Wed, 19 Aug 2026 12:48:07 +0200 Subject: [PATCH 11/15] Kommentare zu Models, Controllern und Core ergaenzt --- app/Views/habits/dashboard.php | 2 +- app/Views/habits/form.php | 0 2 files changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 app/Views/habits/dashboard.php mode change 100644 => 100755 app/Views/habits/form.php diff --git a/app/Views/habits/dashboard.php b/app/Views/habits/dashboard.php old mode 100644 new mode 100755 index 0547996..116dc51 --- a/app/Views/habits/dashboard.php +++ b/app/Views/habits/dashboard.php @@ -117,4 +117,4 @@ $weekdayLabels = [
-
+
\ No newline at end of file diff --git a/app/Views/habits/form.php b/app/Views/habits/form.php old mode 100644 new mode 100755 From 6e1dcd9d1c2474bbb89b189a01f7d90626acbdf8 Mon Sep 17 00:00:00 2001 From: arslandevelopments Date: Wed, 19 Aug 2026 13:55:51 +0200 Subject: [PATCH 12/15] =?UTF-8?q?comments=20in=20models=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Models/Habit.php | 3 ++- app/Models/User.php | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) mode change 100644 => 100755 app/Models/Habit.php mode change 100644 => 100755 app/Models/User.php diff --git a/app/Models/Habit.php b/app/Models/Habit.php old mode 100644 new mode 100755 index 63f073c..f867d45 --- a/app/Models/Habit.php +++ b/app/Models/Habit.php @@ -49,6 +49,7 @@ class Habit return $habit ?: null; } + // Legt einen neuen Habit an public static function create(int $userId, array $data): bool { $sql = 'INSERT INTO habits (user_id, category_id, title, description, target_per_week) @@ -111,4 +112,4 @@ class Habit $insert = $db->prepare('INSERT INTO habit_logs (habit_id, completed_on) VALUES (:habit_id, CURRENT_DATE)'); return $insert->execute(['habit_id' => $id]); } -} +} \ No newline at end of file diff --git a/app/Models/User.php b/app/Models/User.php old mode 100644 new mode 100755 index 32e57d2..eaa8bd5 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,6 +2,7 @@ class User { + // Legt einen neuen Benutzer an public static function create(string $name, string $email, string $password): bool { $sql = 'INSERT INTO users (name, email, password_hash) VALUES (:name, :email, :password_hash)'; @@ -14,6 +15,7 @@ class User ]); } + // Sucht einen Benutzer anhand der E-Mail public static function findByEmail(string $email): ?array { $statement = Database::connection()->prepare('SELECT * FROM users WHERE email = :email LIMIT 1'); @@ -22,4 +24,4 @@ class User $user = $statement->fetch(); return $user ?: null; } -} +} \ No newline at end of file From f91feadedfdb507931ab2683f585928c3f9da5d5 Mon Sep 17 00:00:00 2001 From: arslandevelopments Date: Wed, 19 Aug 2026 14:01:43 +0200 Subject: [PATCH 13/15] =?UTF-8?q?comments=20in=20controllers=20hinzugef?= =?UTF-8?q?=C3=BCgt=20+=20review=20+=20testing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Controllers/AuthController.php | 8 +++++++- app/Controllers/HabitController.php | 10 +++++++++- app/Controllers/ReminderController.php | 14 ++++++++++---- app/Models/Category.php | 0 app/Models/Reminder.php | 5 ++++- 5 files changed, 30 insertions(+), 7 deletions(-) mode change 100644 => 100755 app/Controllers/AuthController.php mode change 100644 => 100755 app/Controllers/HabitController.php mode change 100644 => 100755 app/Controllers/ReminderController.php mode change 100644 => 100755 app/Models/Category.php mode change 100644 => 100755 app/Models/Reminder.php diff --git a/app/Controllers/AuthController.php b/app/Controllers/AuthController.php old mode 100644 new mode 100755 index 7b11963..35f4c4a --- a/app/Controllers/AuthController.php +++ b/app/Controllers/AuthController.php @@ -2,6 +2,7 @@ class AuthController { + // Zeigt das Login-Formular public function showLogin(): void { View::render('auth/login', [ @@ -10,6 +11,7 @@ class AuthController ]); } + // Prueft die Anmeldedaten und startet die Sitzung public function login(): void { verify_csrf_token(); @@ -50,6 +52,7 @@ class AuthController redirect('dashboard'); } + // Zeigt das Registrierungsformular public function showRegister(): void { View::render('auth/register', [ @@ -58,6 +61,7 @@ class AuthController ]); } + // Legt ein neues Konto an public function register(): void { verify_csrf_token(); @@ -109,11 +113,13 @@ class AuthController redirect('login'); } + // Beendet die Sitzung public function logout(): void { verify_csrf_token(); $_SESSION = []; + if (ini_get('session.use_cookies')) { $params = session_get_cookie_params(); setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']); @@ -151,4 +157,4 @@ class AuthController return is_string($value) ? $value : ''; } -} +} \ No newline at end of file diff --git a/app/Controllers/HabitController.php b/app/Controllers/HabitController.php old mode 100644 new mode 100755 index 9b488a0..4ac4a25 --- a/app/Controllers/HabitController.php +++ b/app/Controllers/HabitController.php @@ -2,6 +2,7 @@ class HabitController { + // Zeigt das Dashboard mit allen Habits des Benutzers public function dashboard(): void { require_login(); @@ -26,6 +27,7 @@ class HabitController ]); } + // Zeigt das leere Formular zum Anlegen public function create(): void { require_login(); @@ -37,6 +39,7 @@ class HabitController ]); } + // Speichert einen neuen Habit public function store(): void { require_login(); @@ -58,6 +61,7 @@ class HabitController redirect('dashboard'); } + // Zeigt das Formular zum Bearbeiten public function edit(): void { require_login(); @@ -76,6 +80,7 @@ class HabitController ]); } + // Uebernimmt die Aenderungen eines bestehenden Habits public function update(): void { require_login(); @@ -85,6 +90,7 @@ class HabitController $data = $this->validatedData(); if (!empty($data['errors'])) { + // ID mitgeben, sonst zeigt das Formular auf keinen Datensatz mehr View::render('habits/form', [ 'habit' => array_merge($_POST, ['id' => $id]), 'categories' => Category::all(), @@ -98,6 +104,7 @@ class HabitController redirect('dashboard'); } + // Loescht einen Habit public function delete(): void { require_login(); @@ -135,6 +142,7 @@ class HabitController $errors['description'] = 'Die Beschreibung darf maximal 1000 Zeichen lang sein.'; } + // Gegen die Datenbank pruefen, damit keine erfundene ID durchkommt if ($categoryId !== '' && !Category::exists((int) $categoryId)) { $errors['category_id'] = 'Bitte waehle eine gueltige Kategorie aus.'; } @@ -151,4 +159,4 @@ class HabitController 'errors' => $errors, ]; } -} +} \ No newline at end of file diff --git a/app/Controllers/ReminderController.php b/app/Controllers/ReminderController.php old mode 100644 new mode 100755 index 2651425..a717d6f --- a/app/Controllers/ReminderController.php +++ b/app/Controllers/ReminderController.php @@ -2,6 +2,7 @@ class ReminderController { + // Zeigt das Formular zum Anlegen einer Erinnerung public function create(): void { require_login(); @@ -20,6 +21,7 @@ class ReminderController ]); } + // Speichert eine neue Erinnerung public function store(): void { require_login(); @@ -52,6 +54,7 @@ class ReminderController redirect('dashboard'); } + // Zeigt das Formular zum Bearbeiten public function edit(): void { require_login(); @@ -105,6 +108,7 @@ class ReminderController redirect('dashboard'); } + // Loescht eine Erinnerung public function delete(): void { require_login(); @@ -144,10 +148,12 @@ class ReminderController $activeValue = $this->postString('is_active'); $errors = []; - if (!preg_match('/^(?:[01]\\d|2[0-3]):[0-5]\\d$/', $time)) { + // Uhrzeit muss dem Format HH:MM zwischen 00:00 und 23:59 + if (!preg_match('/^(?:[01]\d|2[0-3]):[0-5]\d$/', $time)) { $errors['reminder_time'] = 'Bitte gib eine Uhrzeit im Format HH:MM ein.'; } + // Leerer Wochentag ist erlaubt und bedeutet taeglich if ($weekdayValue !== '' && !preg_match('/^[1-7]$/', $weekdayValue)) { $errors['weekday'] = 'Bitte waehle einen gueltigen Wochentag aus.'; } @@ -168,14 +174,14 @@ class ReminderController { $value = $_GET[$key] ?? ''; - return is_string($value) && preg_match('/^[1-9]\\d*$/', $value) ? (int) $value : 0; + return is_string($value) && preg_match('/^[1-9]\d*$/', $value) ? (int) $value : 0; } private function postId(string $key): int { $value = $_POST[$key] ?? ''; - return is_string($value) && preg_match('/^[1-9]\\d*$/', $value) ? (int) $value : 0; + return is_string($value) && preg_match('/^[1-9]\d*$/', $value) ? (int) $value : 0; } private function postString(string $key): string @@ -184,4 +190,4 @@ class ReminderController return is_string($value) ? trim($value) : ''; } -} +} \ No newline at end of file diff --git a/app/Models/Category.php b/app/Models/Category.php old mode 100644 new mode 100755 diff --git a/app/Models/Reminder.php b/app/Models/Reminder.php old mode 100644 new mode 100755 index daebd66..a35111a --- a/app/Models/Reminder.php +++ b/app/Models/Reminder.php @@ -31,6 +31,7 @@ class Reminder return $statement->fetchAll(); } + // Legt eine Erinnerung an public static function createForHabit(int $habitId, int $userId, array $data): bool { $statement = Database::connection()->prepare( @@ -50,6 +51,7 @@ class Reminder return $statement->rowCount() === 1; } + // Aktualisiert eine Erinnerung public static function update(int $id, int $userId, array $data): bool { $statement = Database::connection()->prepare( @@ -82,6 +84,7 @@ class Reminder return $statement->execute(['id' => $id, 'user_id' => $userId]); } + // Schaltet zwischen aktiv und pausiert um public static function toggle(int $id, int $userId): bool { $statement = Database::connection()->prepare( @@ -93,4 +96,4 @@ class Reminder return $statement->execute(['id' => $id, 'user_id' => $userId]); } -} +} \ No newline at end of file From 9eb8b1de23b7d7dbade12142e4816ac58b9959ec Mon Sep 17 00:00:00 2001 From: arslandevelopments Date: Wed, 19 Aug 2026 14:03:01 +0200 Subject: [PATCH 14/15] =?UTF-8?q?comments=20hinzugef=C3=BCgt=20bei=20Core?= =?UTF-8?q?=20+=20review=20+=20testing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Core/Database.php | 4 +++- app/Core/View.php | 0 app/helpers.php | 0 3 files changed, 3 insertions(+), 1 deletion(-) mode change 100644 => 100755 app/Core/Database.php mode change 100644 => 100755 app/Core/View.php mode change 100644 => 100755 app/helpers.php diff --git a/app/Core/Database.php b/app/Core/Database.php old mode 100644 new mode 100755 index 5a3d67b..f7e7f49 --- a/app/Core/Database.php +++ b/app/Core/Database.php @@ -1,7 +1,9 @@ Date: Wed, 19 Aug 2026 14:07:43 +0200 Subject: [PATCH 15/15] readme change --- README.md | 77 ------------------------------------------------------- 1 file changed, 77 deletions(-) mode change 100644 => 100755 README.md diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 737bd77..e69de29 --- a/README.md +++ b/README.md @@ -1,77 +0,0 @@ -# Habit Tracker - -Habit Tracker ist eine PHP-Webanwendung, mit der Benutzer eigene Gewohnheiten anlegen, bearbeiten, filtern und taeglich abhaken koennen. - -## Funktionen - -- Registrierung und Login mit Passwort-Hashing -- Geschuetzter Benutzerbereich per Session -- Habits erstellen, bearbeiten, loeschen und fuer heute abhaken -- Kategorien und Statusfilter im Dashboard -- Tagesprotokoll ueber `habit_logs` -- Wochenfortschritt im Dashboard gegen das persoenliche Wochenziel -- Responsive Oberflaeche fuer Desktop und Smartphone -- Serverseitige Validierung und klare Fehlermeldungen -- Schutz gegen SQL-Injection durch PDO Prepared Statements -- Schutz gegen XSS durch escaped HTML-Ausgabe -- CSRF-Schutz fuer schreibende Formulare - -## Technik - -- PHP -- HTML/CSS -- SQL -- MySQL/PDO -- MVC-Struktur - -## MVC-Struktur - -- `public/index.php`: Einstiegspunkt und Routing -- `app/Controllers/`: verarbeitet Anfragen und steuert den Ablauf -- `app/Models/`: kapselt Datenbankzugriffe -- `app/Views/`: erzeugt die HTML-Ausgabe -- `app/Core/`: gemeinsame Kernklassen fuer Datenbank und Views -- `app/helpers.php`: Hilfsfunktionen fuer Escaping, Sessions, Flash-Meldungen und CSRF -- `database/schema.sql`: Datenbankstruktur mit Startdaten - -## Datenbank - -Die Datenbank besteht aus 5 Tabellen: - -- `users`: Benutzerkonten -- `categories`: Habit-Kategorien -- `habits`: Gewohnheiten der Benutzer -- `habit_logs`: taegliche Erledigungen -- `reminders`: vorbereitete Erinnerungen pro Habit - -## Sicherheit - -- Passwoerter werden mit `password_hash()` gespeichert. -- Login prueft Passwoerter mit `password_verify()`. -- SQL-Abfragen nutzen vorbereitete PDO-Statements. -- Ausgaben werden mit `htmlspecialchars()` escaped. -- Schreibende Formulare nutzen CSRF-Tokens. -- Nach erfolgreichem Login wird die Session-ID erneuert. -- Die echte `config/config.php` wird nicht mit Git hochgeladen. - -## Einrichtung - -1. `database/schema.sql` in phpMyAdmin importieren. -2. `config/config.example.php` zu `config/config.php` kopieren. -3. Datenbankdaten in `config/config.php` eintragen. -4. Projekt ueber `public/index.php` starten. - -## Testablauf - -1. Benutzer registrieren. -2. Einloggen. -3. Habit mit Kategorie und Wochenziel erstellen. -4. Habit im Dashboard filtern. -5. Habit fuer heute abhaken. -6. Wochenfortschritt im Dashboard kontrollieren. -7. Habit bearbeiten und loeschen. -8. Seite auf Smartphone-Breite pruefen. - -## Bewertungsbezug - -Das Projekt zeigt eine klare MVC-Aufteilung, Datenbankarbeit mit mehreren verknuepften Tabellen, serverseitige Validierung, Sicherheitsmassnahmen gegen XSS, SQL-Injection und CSRF sowie eine responsive Benutzeroberflaeche.