Initial habit tracker project

This commit is contained in:
2026-07-10 16:18:31 +02:00
commit 23416ee382
20 changed files with 1157 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
config/config.php
.DS_Store
Thumbs.db
+75
View File
@@ -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.
+75
View File
@@ -0,0 +1,75 @@
<?php
class AuthController
{
public function showLogin(): void
{
View::render('auth/login');
}
public function login(): void
{
verify_csrf_token();
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || $password === '') {
flash('error', 'Bitte gib eine gueltige E-Mail und dein Passwort ein.');
redirect('login');
}
$user = User::findByEmail($email);
if (!$user || !password_verify($password, $user['password_hash'])) {
flash('error', 'E-Mail oder Passwort ist nicht korrekt.');
redirect('login');
}
session_regenerate_id(true);
$_SESSION['user_id'] = (int) $user['id'];
$_SESSION['user_name'] = $user['name'];
redirect('dashboard');
}
public function showRegister(): void
{
View::render('auth/register');
}
public function register(): void
{
verify_csrf_token();
$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
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 (User::findByEmail($email)) {
flash('error', 'Diese E-Mail ist bereits registriert.');
redirect('register');
}
User::create($name, $email, $password);
flash('success', 'Registrierung erfolgreich. Du kannst dich jetzt anmelden.');
redirect('login');
}
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']);
}
session_destroy();
redirect('login');
}
}
+137
View File
@@ -0,0 +1,137 @@
<?php
class HabitController
{
public function dashboard(): void
{
require_login();
$filters = [
'category_id' => $_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,
];
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
use PDO;
use PDOException;
class Database
{
private static ?PDO $connection = null;
public static function connection(): PDO
{
if (self::$connection !== null) {
return self::$connection;
}
$db = config('database');
$dsn = sprintf(
'mysql:host=%s;dbname=%s;charset=%s',
$db['host'],
$db['dbname'],
$db['charset']
);
try {
self::$connection = new PDO($dsn, $db['user'], $db['password'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
} catch (PDOException $exception) {
throw new PDOException('Datenbankverbindung fehlgeschlagen.');
}
return self::$connection;
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
class View
{
public static function render(string $view, array $data = []): void
{
extract($data, EXTR_SKIP);
$viewPath = __DIR__ . '/../Views/' . $view . '.php';
require __DIR__ . '/../Views/layout/header.php';
require $viewPath;
require __DIR__ . '/../Views/layout/footer.php';
}
}
+11
View File
@@ -0,0 +1,11 @@
<?php
class Category
{
public static function all(): array
{
return Database::connection()
->query('SELECT id, name, color FROM categories ORDER BY name')
->fetchAll();
}
}
+110
View File
@@ -0,0 +1,110 @@
<?php
class Habit
{
public static function allForUser(int $userId, array $filters = []): array
{
$where = ['h.user_id = :user_id'];
$params = ['user_id' => $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]);
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
class User
{
public static function create(string $name, string $email, string $password): bool
{
$sql = 'INSERT INTO users (name, email, password_hash) VALUES (:name, :email, :password_hash)';
$statement = Database::connection()->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;
}
}
+15
View File
@@ -0,0 +1,15 @@
<section class="auth-panel">
<h1>Anmelden</h1>
<form method="post" action="index.php?route=login" class="form">
<?= csrf_field() ?>
<label for="email">E-Mail</label>
<input id="email" name="email" type="email" autocomplete="email" required>
<label for="password">Passwort</label>
<input id="password" name="password" type="password" autocomplete="current-password" required>
<button type="submit">Einloggen</button>
</form>
<p>Noch kein Konto? <a href="index.php?route=register">Jetzt registrieren</a></p>
</section>
+19
View File
@@ -0,0 +1,19 @@
<section class="auth-panel">
<h1>Registrieren</h1>
<form method="post" action="index.php?route=register" class="form">
<?= csrf_field() ?>
<label for="name">Name</label>
<input id="name" name="name" type="text" autocomplete="name" required>
<label for="email">E-Mail</label>
<input id="email" name="email" type="email" autocomplete="email" required>
<label for="password">Passwort</label>
<input id="password" name="password" type="password" minlength="8" autocomplete="new-password" required>
<small>Mindestens 8 Zeichen.</small>
<button type="submit">Konto erstellen</button>
</form>
<p>Schon registriert? <a href="index.php?route=login">Zum Login</a></p>
</section>
+67
View File
@@ -0,0 +1,67 @@
<section class="dashboard-head">
<div>
<p class="eyebrow">Heute</p>
<h1>Deine Habits</h1>
</div>
<a class="button" href="index.php?route=habits/create">Habit erstellen</a>
</section>
<form method="get" action="index.php" class="filters">
<input type="hidden" name="route" value="dashboard">
<label for="category_id">Kategorie</label>
<select id="category_id" name="category_id">
<option value="">Alle Kategorien</option>
<?php foreach ($categories as $category): ?>
<option value="<?= (int) $category['id'] ?>" <?= (string) $filters['category_id'] === (string) $category['id'] ? 'selected' : '' ?>>
<?= e($category['name']) ?>
</option>
<?php endforeach; ?>
</select>
<label for="status">Status</label>
<select id="status" name="status">
<option value="">Alle</option>
<option value="open" <?= $filters['status'] === 'open' ? 'selected' : '' ?>>Offen</option>
<option value="done" <?= $filters['status'] === 'done' ? 'selected' : '' ?>>Erledigt</option>
</select>
<button type="submit">Filtern</button>
</form>
<?php if (empty($habits)): ?>
<p class="empty">Noch keine Habits gefunden. Erstelle deinen ersten Habit.</p>
<?php endif; ?>
<section class="habit-list">
<?php foreach ($habits as $habit): ?>
<article class="habit-card <?= $habit['done_today'] ? 'done' : '' ?>">
<div class="habit-main">
<span class="category-dot" style="background-color: <?= e($habit['category_color'] ?? '#9ca3af') ?>"></span>
<div>
<h2><?= e($habit['title']) ?></h2>
<p><?= e($habit['description']) ?></p>
<small>
<?= e($habit['category_name'] ?? 'Ohne Kategorie') ?> |
Ziel: <?= (int) $habit['target_per_week'] ?>x pro Woche
</small>
</div>
</div>
<div class="habit-actions">
<form method="post" action="index.php?route=habits/toggle">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int) $habit['id'] ?>">
<button type="submit"><?= $habit['done_today'] ? 'Heute erledigt' : 'Abhaken' ?></button>
</form>
<a href="index.php?route=habits/edit&id=<?= (int) $habit['id'] ?>">Bearbeiten</a>
<form method="post" action="index.php?route=habits/delete" onsubmit="return confirm('Diesen Habit wirklich loeschen?');">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int) $habit['id'] ?>">
<button class="danger" type="submit">Loeschen</button>
</form>
</div>
</article>
<?php endforeach; ?>
</section>
+58
View File
@@ -0,0 +1,58 @@
<?php $isEdit = !empty($habit['id']); ?>
<section class="form-panel">
<h1><?= $isEdit ? 'Habit bearbeiten' : 'Habit erstellen' ?></h1>
<form method="post" action="index.php?route=<?= $isEdit ? 'habits/update' : 'habits/store' ?>" class="form">
<?= csrf_field() ?>
<?php if ($isEdit): ?>
<input type="hidden" name="id" value="<?= (int) $habit['id'] ?>">
<?php endif; ?>
<label for="title">Name des Habits</label>
<input
id="title"
name="title"
type="text"
class="<?= !empty($errors['title']) ? 'input-error' : '' ?>"
value="<?= e($habit['title'] ?? '') ?>"
required
aria-describedby="title-error"
>
<?php if (!empty($errors['title'])): ?>
<small id="title-error" class="field-error"><?= e($errors['title']) ?></small>
<?php endif; ?>
<label for="description">Beschreibung <span>optional</span></label>
<textarea id="description" name="description" rows="4"><?= e($habit['description'] ?? '') ?></textarea>
<label for="category_id">Kategorie</label>
<select id="category_id" name="category_id">
<option value="">Keine Kategorie</option>
<?php foreach ($categories as $category): ?>
<option value="<?= (int) $category['id'] ?>" <?= (string) ($habit['category_id'] ?? '') === (string) $category['id'] ? 'selected' : '' ?>>
<?= e($category['name']) ?>
</option>
<?php endforeach; ?>
</select>
<label for="target_per_week">Ziel pro Woche</label>
<input
id="target_per_week"
name="target_per_week"
type="number"
class="<?= !empty($errors['target_per_week']) ? 'input-error' : '' ?>"
min="1"
max="7"
value="<?= e((string) ($habit['target_per_week'] ?? '3')) ?>"
required
aria-describedby="target-error"
>
<?php if (!empty($errors['target_per_week'])): ?>
<small id="target-error" class="field-error"><?= e($errors['target_per_week']) ?></small>
<?php endif; ?>
<button type="submit"><?= $isEdit ? 'Speichern' : 'Erstellen' ?></button>
</form>
</section>
+3
View File
@@ -0,0 +1,3 @@
</main>
</body>
</html>
+34
View File
@@ -0,0 +1,34 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= e(config('app_name')) ?></title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<header class="topbar">
<a class="brand" href="index.php?route=dashboard">Habit Tracker</a>
<nav class="nav">
<?php if (current_user_id()): ?>
<a href="index.php?route=dashboard">Dashboard</a>
<a href="index.php?route=habits/create">Neuer Habit</a>
<form method="post" action="index.php?route=logout" class="logout-form">
<?= csrf_field() ?>
<button type="submit" class="link-button">Logout</button>
</form>
<?php else: ?>
<a href="index.php?route=login">Login</a>
<a href="index.php?route=register">Registrieren</a>
<?php endif; ?>
</nav>
</header>
<main class="page">
<?php if ($message = flash('success')): ?>
<p class="notice success"><?= e($message) ?></p>
<?php endif; ?>
<?php if ($message = flash('error')): ?>
<p class="notice error"><?= e($message) ?></p>
<?php endif; ?>
+82
View File
@@ -0,0 +1,82 @@
<?php
function config(string $key, $default = null)
{
static $config = null;
if ($config === null) {
$config = require __DIR__ . '/../config/config.php';
}
$parts = explode('.', $key);
$value = $config;
foreach ($parts as $part) {
if (!is_array($value) || !array_key_exists($part, $value)) {
return $default;
}
$value = $value[$part];
}
return $value;
}
function e(?string $value): string
{
return htmlspecialchars($value ?? '', ENT_QUOTES, 'UTF-8');
}
function redirect(string $route): never
{
header('Location: index.php?route=' . urlencode($route));
exit;
}
function current_user_id(): ?int
{
return $_SESSION['user_id'] ?? null;
}
function require_login(): void
{
if (!current_user_id()) {
redirect('login');
}
}
function flash(string $key, ?string $message = null): ?string
{
if ($message !== null) {
$_SESSION['flash'][$key] = $message;
return null;
}
$value = $_SESSION['flash'][$key] ?? null;
unset($_SESSION['flash'][$key]);
return $value;
}
function csrf_token(): string
{
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function csrf_field(): string
{
return '<input type="hidden" name="csrf_token" value="' . e(csrf_token()) . '">';
}
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;
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
return [
'app_name' => 'Habit Tracker',
'base_path' => '',
'database' => [
'host' => 'localhost',
'dbname' => 'habittracker',
'user' => 'root',
'password' => '',
'charset' => 'utf8mb4',
],
];
+55
View File
@@ -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');
+275
View File
@@ -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;
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
session_set_cookie_params([
'httponly' => 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();