Initial habit tracker project
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
config/config.php
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -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.
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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; ?>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'app_name' => 'Habit Tracker',
|
||||||
|
'base_path' => '',
|
||||||
|
'database' => [
|
||||||
|
'host' => 'localhost',
|
||||||
|
'dbname' => 'habittracker',
|
||||||
|
'user' => 'root',
|
||||||
|
'password' => '',
|
||||||
|
'charset' => 'utf8mb4',
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -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');
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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();
|
||||||
Reference in New Issue
Block a user