first commit

This commit is contained in:
2026-01-29 21:07:02 +01:00
commit bda2bc85d0
22 changed files with 2594 additions and 0 deletions

9
.htaccess Normal file
View File

@@ -0,0 +1,9 @@
RewriteEngine On
RewriteBase /
# Jeśli plik lub katalog istnieje - nie przekierowuj
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Przekieruj wszystko do index.php
RewriteRule ^(.*)$ index.php?route=$1 [QSA,L]

17
.vscode/ftp-kr.json vendored Normal file
View File

@@ -0,0 +1,17 @@
{
"host": "host117523.hostido.net.pl",
"username": "www@cmspro.it",
"password": "jWD4awMpyMxr7FAYkPL3",
"remotePath": "/public_html/",
"protocol": "ftp",
"port": 21,
"fileNameEncoding": "utf8",
"autoUpload": true,
"autoDelete": false,
"autoDownload": false,
"ignoreRemoteModification": true,
"ignore": [
".git",
"/.vscode"
]
}

12
.vscode/sftp.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "host117523.hostido.net.pl",
"host": "host117523.hostido.net.pl",
"protocol": "ftp",
"port": 21,
"username": "www@cmspro.it",
"password": "jWD4awMpyMxr7FAYkPL3",
"remotePath": "/public_html/",
"uploadOnSave": false,
"useTempFile": false,
"openSsh": false
}

315
PROJEKT.md Normal file
View File

@@ -0,0 +1,315 @@
# Projekt: System 2FA z Notatnikiem i Kalendarzem
## Informacje ogólne
- **Technologie:** PHP 8+, SQLite, Bootstrap 5.3, Bootstrap Icons
- **Architektura:** Uproszczone MVC (kontrolery, modele, widoki w PHP)
- **Routing:** Przyjazne linki (SEO-friendly URLs) przez .htaccess
---
## Struktura projektu
```
projektphp2/
├── .htaccess # Reguły mod_rewrite
├── index.php # Front controller (routing)
├── init.php # Inicjalizacja bazy danych
├── PROJEKT.md # Ten plik - dokumentacja
├── app/
│ ├── controllers/
│ │ ├── AuthController.php # Logowanie, 2FA, wylogowanie
│ │ ├── NoteController.php # CRUD notatek
│ │ └── EventController.php # CRUD wydarzeń kalendarza
│ ├── models/
│ │ ├── Database.php # Singleton PDO SQLite
│ │ ├── User.php # Model użytkownika + kody weryfikacyjne
│ │ ├── Note.php # Model notatek (auto-migracja)
│ │ └── Event.php # Model wydarzeń (auto-migracja)
│ └── views/
│ ├── layout.php # Główny szablon (z/bez menu)
│ ├── login.php # Formularz logowania
│ ├── verify.php # Weryfikacja kodu 2FA
│ ├── dashboard.php # Pulpit użytkownika
│ ├── notes/
│ │ ├── index.php # Lista notatek (karty)
│ │ └── form.php # Formularz dodawania/edycji notatki
│ └── calendar/
│ ├── index.php # Widok kalendarza miesięcznego
│ ├── day.php # Lista wydarzeń danego dnia
│ └── form.php # Formularz dodawania/edycji wydarzenia
└── database/
└── app.db # Baza SQLite (tworzona przez init.php)
```
---
## Routing (przyjazne linki)
### Autoryzacja
| URL | Metoda | Akcja |
|-----|--------|-------|
| `/` lub `/logowanie` | GET | Formularz logowania |
| `/zaloguj` | POST | Weryfikacja login/hasło |
| `/weryfikacja` | GET | Formularz kodu 2FA |
| `/zweryfikuj` | POST | Weryfikacja kodu 2FA |
| `/panel` | GET | Pulpit (wymaga auth) |
| `/wyloguj-sie` | GET | Wylogowanie |
### Notatnik (wymaga zalogowania)
| URL | Metoda | Akcja |
|-----|--------|-------|
| `/notatnik` | GET | Lista wszystkich notatek |
| `/notatnik/nowa` | GET | Formularz nowej notatki |
| `/notatnik/dodaj` | POST | Zapisanie nowej notatki |
| `/notatnik/edytuj/{id}` | GET | Formularz edycji |
| `/notatnik/zapisz/{id}` | POST | Zapisanie zmian |
| `/notatnik/usun/{id}` | POST | Usunięcie notatki |
### Kalendarz (wymaga zalogowania)
| URL | Metoda | Akcja |
|-----|--------|-------|
| `/kalendarz` | GET | Widok kalendarza miesięcznego |
| `/kalendarz?rok=2024&miesiac=12` | GET | Kalendarz dla konkretnego miesiąca |
| `/kalendarz/dzien/{YYYY-MM-DD}` | GET | Lista wydarzeń danego dnia |
| `/kalendarz/nowe` | GET | Formularz nowego wydarzenia |
| `/kalendarz/nowe/{YYYY-MM-DD}` | GET | Formularz z predefiniowaną datą |
| `/kalendarz/dodaj` | POST | Zapisanie nowego wydarzenia |
| `/kalendarz/edytuj/{id}` | GET | Formularz edycji wydarzenia |
| `/kalendarz/zapisz/{id}` | POST | Zapisanie zmian |
| `/kalendarz/usun/{id}` | POST | Usunięcie wydarzenia |
---
## Baza danych (SQLite)
### Tabela: users
```sql
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
login VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL, -- password_hash()
email VARCHAR(100),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
```
### Tabela: verification_codes
```sql
CREATE TABLE verification_codes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
code VARCHAR(6) NOT NULL,
expires_at DATETIME NOT NULL, -- +10 minut
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id)
);
```
### Tabela: notes (auto-tworzona przy pierwszym użyciu)
```sql
CREATE TABLE notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
content TEXT,
color VARCHAR(20) DEFAULT 'primary', -- primary/success/danger/warning/info/secondary
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
```
### Tabela: events (auto-tworzona przy pierwszym użyciu)
```sql
CREATE TABLE events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
content TEXT,
event_date DATE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
```
---
## Dane testowe
- **Login:** `projectpro`
- **Hasło:** `testowehaslo`
- **Inicjalizacja:** wywołać `/init.php`
---
## Funkcjonalności
### Zrealizowane ✅
1. **Logowanie dwuskładnikowe (2FA)**
- Formularz login/hasło
- Generowanie 6-cyfrowego kodu
- Symulacja wysyłki email (kod wyświetlany na stronie)
- Weryfikacja kodu z czasem wygaśnięcia (10 min)
- Sesje PHP
2. **Panel użytkownika (Pulpit)**
- Powitanie użytkownika
- Statystyki (status 2FA, liczba notatek, wydarzenia tego tygodnia, sesja)
- Szybkie akcje (nowa notatka, nowe wydarzenie, kalendarz)
- Lista nadchodzących wydarzeń z tego tygodnia
- Informacje o logowaniu
3. **Notatnik**
- Lista notatek jako kolorowe karty
- Dodawanie notatek (tytuł, treść, kolor)
- Edycja notatek
- Usuwanie z potwierdzeniem (modal Bootstrap)
- 6 kolorów do wyboru
- Auto-migracja tabeli
4. **Kalendarz**
- Widok miesięczny z nawigacją (poprzedni/następny miesiąc)
- Podgląd wydarzeń na poszczególnych dniach
- Kliknięcie w dzień otwiera listę wydarzeń
- Dodawanie wydarzeń (tytuł, treść, data)
- Edycja wydarzeń
- Usuwanie z potwierdzeniem (modal Bootstrap)
- Wyróżnienie dzisiejszego dnia
- Wyróżnienie dni z wydarzeniami
- Szybkie dodawanie z predefiniowaną datą
- Auto-migracja tabeli
5. **Menu nawigacyjne**
- Pulpit / Notatnik / Kalendarz
- Dropdown z użytkownikiem
- Wylogowanie
- Aktywna pozycja menu podświetlona
6. **Wygląd**
- Bootstrap 5.3 + Bootstrap Icons
- Gradient fioletowy jako tło
- Karty z cieniami i zaokrągleniami
- Responsywność (mobile-friendly)
- Animacje hover
---
## Wymagania serwera
- PHP 8.0+
- Rozszerzenie PDO SQLite
- Apache z mod_rewrite
- Uprawnienia do zapisu w katalogu `database/`
---
## Sesje PHP
Używane klucze sesji:
- `user_id` - ID zalogowanego użytkownika
- `user_login` - Login użytkownika
- `logged_in` - Flaga pełnej autoryzacji (po 2FA)
- `pending_user_id` - ID podczas weryfikacji 2FA
- `pending_user_login` - Login podczas weryfikacji 2FA
- `simulated_code` - Kod 2FA (tylko do testów!)
- `error` - Komunikat błędu (flash message)
- `success` - Komunikat sukcesu (flash message)
---
## Bezpieczeństwo
- Hasła hashowane przez `password_hash()` / `password_verify()`
- Prepared statements (PDO) - ochrona przed SQL injection
- `htmlspecialchars()` - ochrona przed XSS
- Kody 2FA z czasem wygaśnięcia
- Usuwanie kodów po użyciu
- Sprawdzanie właściciela notatki/wydarzenia przy każdej operacji
---
## Do zrobienia w przyszłości (pomysły)
- [ ] Prawdziwa wysyłka emaili (PHPMailer / SMTP)
- [ ] Rejestracja nowych użytkowników
- [ ] Przypomnienie hasła
- [ ] Wyszukiwanie notatek
- [ ] Kategorie/tagi dla notatek
- [ ] Eksport notatek (PDF/TXT)
- [ ] Limit prób logowania (brute-force protection)
- [ ] Zapamiętaj mnie (persistent login)
- [ ] Logi aktywności użytkownika
- [ ] Tryb ciemny (dark mode)
- [ ] API REST dla notatek i wydarzeń
- [ ] Przypomnienia o wydarzeniach
- [ ] Powtarzające się wydarzenia (cykliczne)
- [ ] Widok tygodniowy/dzienny kalendarza
- [ ] Drag & drop dla wydarzeń
- [ ] Eksport kalendarza (iCal)
---
## Historia zmian
### v1.0 (początkowa wersja)
- System logowania z 2FA
- Baza SQLite z użytkownikiem testowym
- Widoki: logowanie, weryfikacja, dashboard
- Bootstrap 5.3 + gradient design
### v1.1 (przyjazne linki)
- Routing przez .htaccess (mod_rewrite)
- Przeniesienie do katalogu głównego (bez /public/)
- SEO-friendly URLs (/logowanie, /panel, etc.)
### v1.2 (notatnik)
- Menu nawigacyjne (Pulpit, Notatnik)
- Pełny CRUD notatek
- Kolorowe karty z podglądem
- Modal potwierdzenia usunięcia
- Statystyki na pulpicie
- Auto-migracja tabeli notes
### v1.3 (kalendarz)
- Nowa pozycja menu: Kalendarz
- Widok kalendarza miesięcznego z nawigacją
- Widok dnia z listą wydarzeń
- Pełny CRUD wydarzeń (tytuł, treść, data)
- Podgląd wydarzeń bezpośrednio w kalendarzu
- Wyróżnienie dzisiejszego dnia i dni z wydarzeniami
- Statystyki wydarzeń na pulpicie (ten tydzień)
- Lista nadchodzących wydarzeń na pulpicie
- Szybkie akcje: nowe wydarzenie, otwórz kalendarz
- Auto-migracja tabeli events
---
## Notatki dla Claude
1. **Layout** - funkcja `renderLayout()` przyjmuje parametry:
- `$title` - tytuł strony
- `$content` - zawartość
- `$showMenu` - czy pokazać menu (domyślnie false)
- `$activeMenu` - która pozycja menu jest aktywna ('pulpit'/'notatnik'/'kalendarz')
- `$containerSize` - klasa Bootstrap dla kontenera (domyślnie 'col-md-5')
2. **Modele z auto-migracją** - metoda `ensureTable()` w Note i Event automatycznie tworzy tabelę jeśli nie istnieje
3. **Flash messages** - komunikaty błędów/sukcesu przez sesję, czyszczone po wyświetleniu
4. **Routing** - w `index.php` używany `switch(true)` z `preg_match` dla dynamicznych URL
5. **Kolory notatek** - dostępne: primary, success, danger, warning, info, secondary
6. **Kalendarz - parametry URL:**
- `rok` - rok (YYYY)
- `miesiac` - miesiąc (1-12)
- Format daty w URL: `YYYY-MM-DD`
7. **Event::countThisWeek()** - zlicza wydarzenia od poniedziałku do niedzieli bieżącego tygodnia
8. **Event::getThisWeek()** - pobiera listę wydarzeń z bieżącego tygodnia (dla pulpitu)

View File

@@ -0,0 +1,117 @@
<?php
require_once __DIR__ . '/../models/User.php';
class AuthController
{
public function loginForm(): void
{
$error = $_SESSION['error'] ?? null;
unset($_SESSION['error']);
require __DIR__ . '/../views/login.php';
}
public function login(): void
{
$login = trim($_POST['login'] ?? '');
$password = $_POST['password'] ?? '';
if (empty($login) || empty($password)) {
$_SESSION['error'] = 'Proszę podać login i hasło.';
header('Location: /logowanie');
exit;
}
$user = User::verifyPassword($login, $password);
if (!$user) {
$_SESSION['error'] = 'Nieprawidłowy login lub hasło.';
header('Location: /logowanie');
exit;
}
// Generowanie kodu weryfikacyjnego
$code = sprintf('%06d', random_int(0, 999999));
User::saveVerificationCode($user['id'], $code);
// Symulacja wysyłki maila
$_SESSION['pending_user_id'] = $user['id'];
$_SESSION['pending_user_login'] = $user['login'];
$_SESSION['simulated_code'] = $code; // W produkcji nie pokazywać!
header('Location: /weryfikacja');
exit;
}
public function verifyForm(): void
{
if (!isset($_SESSION['pending_user_id'])) {
header('Location: /logowanie');
exit;
}
$error = $_SESSION['error'] ?? null;
$simulatedCode = $_SESSION['simulated_code'] ?? null;
$userLogin = $_SESSION['pending_user_login'] ?? '';
unset($_SESSION['error']);
require __DIR__ . '/../views/verify.php';
}
public function verify(): void
{
if (!isset($_SESSION['pending_user_id'])) {
header('Location: /logowanie');
exit;
}
$code = trim($_POST['code'] ?? '');
$userId = $_SESSION['pending_user_id'];
if (empty($code)) {
$_SESSION['error'] = 'Proszę podać kod weryfikacyjny.';
header('Location: /weryfikacja');
exit;
}
if (!User::verifyCode($userId, $code)) {
$_SESSION['error'] = 'Nieprawidłowy lub wygasły kod weryfikacyjny.';
header('Location: /weryfikacja');
exit;
}
// Usunięcie kodu po użyciu
User::deleteVerificationCode($userId);
// Pełna autoryzacja
$_SESSION['user_id'] = $userId;
$_SESSION['user_login'] = $_SESSION['pending_user_login'];
$_SESSION['logged_in'] = true;
unset($_SESSION['pending_user_id']);
unset($_SESSION['pending_user_login']);
unset($_SESSION['simulated_code']);
header('Location: /panel');
exit;
}
public function dashboard(): void
{
if (!isset($_SESSION['logged_in']) || !$_SESSION['logged_in']) {
header('Location: /logowanie');
exit;
}
$userLogin = $_SESSION['user_login'] ?? 'Użytkownik';
require __DIR__ . '/../views/dashboard.php';
}
public function logout(): void
{
session_destroy();
header('Location: /logowanie');
exit;
}
}

View File

@@ -0,0 +1,179 @@
<?php
require_once __DIR__ . '/../models/Event.php';
class EventController
{
private function requireAuth(): void
{
if (!isset($_SESSION['logged_in']) || !$_SESSION['logged_in']) {
header('Location: /logowanie');
exit;
}
}
public function index(): void
{
$this->requireAuth();
$userId = $_SESSION['user_id'];
$userLogin = $_SESSION['user_login'];
// Pobierz rok i miesiąc z parametrów lub użyj bieżących
$year = isset($_GET['rok']) ? (int) $_GET['rok'] : (int) date('Y');
$month = isset($_GET['miesiac']) ? (int) $_GET['miesiac'] : (int) date('m');
// Walidacja
if ($month < 1) {
$month = 12;
$year--;
} elseif ($month > 12) {
$month = 1;
$year++;
}
$events = Event::getByMonth($userId, $year, $month);
// Grupowanie wydarzeń po dacie
$eventsByDate = [];
foreach ($events as $event) {
$eventsByDate[$event['event_date']][] = $event;
}
$success = $_SESSION['success'] ?? null;
$error = $_SESSION['error'] ?? null;
unset($_SESSION['success'], $_SESSION['error']);
require __DIR__ . '/../views/calendar/index.php';
}
public function dayEvents(string $date): void
{
$this->requireAuth();
$userId = $_SESSION['user_id'];
$userLogin = $_SESSION['user_login'];
// Walidacja daty
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
$_SESSION['error'] = 'Nieprawidłowy format daty.';
header('Location: /kalendarz');
exit;
}
$events = Event::getByDate($userId, $date);
$success = $_SESSION['success'] ?? null;
$error = $_SESSION['error'] ?? null;
unset($_SESSION['success'], $_SESSION['error']);
require __DIR__ . '/../views/calendar/day.php';
}
public function create(string $date = null): void
{
$this->requireAuth();
$userLogin = $_SESSION['user_login'];
$event = null;
$isEdit = false;
$selectedDate = $date ?? date('Y-m-d');
require __DIR__ . '/../views/calendar/form.php';
}
public function store(): void
{
$this->requireAuth();
$userId = $_SESSION['user_id'];
$title = trim($_POST['title'] ?? '');
$content = trim($_POST['content'] ?? '');
$eventDate = $_POST['event_date'] ?? '';
if (empty($title)) {
$_SESSION['error'] = 'Tytuł wydarzenia jest wymagany.';
header('Location: /kalendarz/nowe');
exit;
}
if (empty($eventDate)) {
$_SESSION['error'] = 'Data wydarzenia jest wymagana.';
header('Location: /kalendarz/nowe');
exit;
}
Event::create($userId, $title, $content, $eventDate);
$_SESSION['success'] = 'Wydarzenie zostało dodane.';
header('Location: /kalendarz/dzien/' . $eventDate);
exit;
}
public function edit(int $id): void
{
$this->requireAuth();
$userId = $_SESSION['user_id'];
$userLogin = $_SESSION['user_login'];
$event = Event::getById($id, $userId);
if (!$event) {
$_SESSION['error'] = 'Wydarzenie nie zostało znalezione.';
header('Location: /kalendarz');
exit;
}
$isEdit = true;
$selectedDate = $event['event_date'];
require __DIR__ . '/../views/calendar/form.php';
}
public function update(int $id): void
{
$this->requireAuth();
$userId = $_SESSION['user_id'];
$title = trim($_POST['title'] ?? '');
$content = trim($_POST['content'] ?? '');
$eventDate = $_POST['event_date'] ?? '';
if (empty($title)) {
$_SESSION['error'] = 'Tytuł wydarzenia jest wymagany.';
header("Location: /kalendarz/edytuj/$id");
exit;
}
$event = Event::getById($id, $userId);
if (!$event) {
$_SESSION['error'] = 'Wydarzenie nie zostało znalezione.';
header('Location: /kalendarz');
exit;
}
Event::update($id, $userId, $title, $content, $eventDate);
$_SESSION['success'] = 'Wydarzenie zostało zaktualizowane.';
header('Location: /kalendarz/dzien/' . $eventDate);
exit;
}
public function delete(int $id): void
{
$this->requireAuth();
$userId = $_SESSION['user_id'];
$event = Event::getById($id, $userId);
if (!$event) {
$_SESSION['error'] = 'Wydarzenie nie zostało znalezione.';
header('Location: /kalendarz');
exit;
}
$eventDate = $event['event_date'];
Event::delete($id, $userId);
$_SESSION['success'] = 'Wydarzenie zostało usunięte.';
header('Location: /kalendarz/dzien/' . $eventDate);
exit;
}
}

View File

@@ -0,0 +1,126 @@
<?php
require_once __DIR__ . '/../models/Note.php';
class NoteController
{
private function requireAuth(): void
{
if (!isset($_SESSION['logged_in']) || !$_SESSION['logged_in']) {
header('Location: /logowanie');
exit;
}
}
public function index(): void
{
$this->requireAuth();
$userId = $_SESSION['user_id'];
$userLogin = $_SESSION['user_login'];
$notes = Note::getAllByUser($userId);
$success = $_SESSION['success'] ?? null;
$error = $_SESSION['error'] ?? null;
unset($_SESSION['success'], $_SESSION['error']);
require __DIR__ . '/../views/notes/index.php';
}
public function create(): void
{
$this->requireAuth();
$userLogin = $_SESSION['user_login'];
$note = null;
$isEdit = false;
require __DIR__ . '/../views/notes/form.php';
}
public function store(): void
{
$this->requireAuth();
$userId = $_SESSION['user_id'];
$title = trim($_POST['title'] ?? '');
$content = trim($_POST['content'] ?? '');
$color = $_POST['color'] ?? 'primary';
if (empty($title)) {
$_SESSION['error'] = 'Tytuł notatki jest wymagany.';
header('Location: /notatnik/nowa');
exit;
}
Note::create($userId, $title, $content, $color);
$_SESSION['success'] = 'Notatka została dodana.';
header('Location: /notatnik');
exit;
}
public function edit(int $id): void
{
$this->requireAuth();
$userId = $_SESSION['user_id'];
$userLogin = $_SESSION['user_login'];
$note = Note::getById($id, $userId);
if (!$note) {
$_SESSION['error'] = 'Notatka nie została znaleziona.';
header('Location: /notatnik');
exit;
}
$isEdit = true;
require __DIR__ . '/../views/notes/form.php';
}
public function update(int $id): void
{
$this->requireAuth();
$userId = $_SESSION['user_id'];
$title = trim($_POST['title'] ?? '');
$content = trim($_POST['content'] ?? '');
$color = $_POST['color'] ?? 'primary';
if (empty($title)) {
$_SESSION['error'] = 'Tytuł notatki jest wymagany.';
header("Location: /notatnik/edytuj/$id");
exit;
}
$note = Note::getById($id, $userId);
if (!$note) {
$_SESSION['error'] = 'Notatka nie została znaleziona.';
header('Location: /notatnik');
exit;
}
Note::update($id, $userId, $title, $content, $color);
$_SESSION['success'] = 'Notatka została zaktualizowana.';
header('Location: /notatnik');
exit;
}
public function delete(int $id): void
{
$this->requireAuth();
$userId = $_SESSION['user_id'];
$note = Note::getById($id, $userId);
if (!$note) {
$_SESSION['error'] = 'Notatka nie została znaleziona.';
header('Location: /notatnik');
exit;
}
Note::delete($id, $userId);
$_SESSION['success'] = 'Notatka została usunięta.';
header('Location: /notatnik');
exit;
}
}

22
app/models/Database.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
class Database
{
private static ?PDO $instance = null;
private static string $dbPath = __DIR__ . '/../../database/app.db';
public static function getInstance(): PDO
{
if (self::$instance === null) {
self::$instance = new PDO('sqlite:' . self::$dbPath);
self::$instance->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
self::$instance->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
}
return self::$instance;
}
public static function getDbPath(): string
{
return self::$dbPath;
}
}

182
app/models/Event.php Normal file
View File

@@ -0,0 +1,182 @@
<?php
require_once __DIR__ . '/Database.php';
class Event
{
public static function ensureTable(): void
{
$db = Database::getInstance();
$db->exec('
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
content TEXT,
event_date DATE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)
');
}
public static function getAllByUser(int $userId): array
{
self::ensureTable();
$db = Database::getInstance();
$stmt = $db->prepare('
SELECT * FROM events
WHERE user_id = :user_id
ORDER BY event_date ASC
');
$stmt->execute(['user_id' => $userId]);
return $stmt->fetchAll();
}
public static function getByMonth(int $userId, int $year, int $month): array
{
self::ensureTable();
$db = Database::getInstance();
$startDate = sprintf('%04d-%02d-01', $year, $month);
$endDate = sprintf('%04d-%02d-%02d', $year, $month, cal_days_in_month(CAL_GREGORIAN, $month, $year));
$stmt = $db->prepare('
SELECT * FROM events
WHERE user_id = :user_id
AND event_date BETWEEN :start_date AND :end_date
ORDER BY event_date ASC
');
$stmt->execute([
'user_id' => $userId,
'start_date' => $startDate,
'end_date' => $endDate
]);
return $stmt->fetchAll();
}
public static function getByDate(int $userId, string $date): array
{
self::ensureTable();
$db = Database::getInstance();
$stmt = $db->prepare('
SELECT * FROM events
WHERE user_id = :user_id AND event_date = :event_date
ORDER BY created_at ASC
');
$stmt->execute(['user_id' => $userId, 'event_date' => $date]);
return $stmt->fetchAll();
}
public static function getById(int $id, int $userId): ?array
{
self::ensureTable();
$db = Database::getInstance();
$stmt = $db->prepare('
SELECT * FROM events
WHERE id = :id AND user_id = :user_id
');
$stmt->execute(['id' => $id, 'user_id' => $userId]);
$event = $stmt->fetch();
return $event ?: null;
}
public static function create(int $userId, string $title, string $content, string $eventDate): int
{
self::ensureTable();
$db = Database::getInstance();
$stmt = $db->prepare('
INSERT INTO events (user_id, title, content, event_date, created_at, updated_at)
VALUES (:user_id, :title, :content, :event_date, datetime("now"), datetime("now"))
');
$stmt->execute([
'user_id' => $userId,
'title' => $title,
'content' => $content,
'event_date' => $eventDate
]);
return (int) $db->lastInsertId();
}
public static function update(int $id, int $userId, string $title, string $content, string $eventDate): bool
{
self::ensureTable();
$db = Database::getInstance();
$stmt = $db->prepare('
UPDATE events
SET title = :title, content = :content, event_date = :event_date, updated_at = datetime("now")
WHERE id = :id AND user_id = :user_id
');
return $stmt->execute([
'id' => $id,
'user_id' => $userId,
'title' => $title,
'content' => $content,
'event_date' => $eventDate
]);
}
public static function delete(int $id, int $userId): bool
{
$db = Database::getInstance();
$stmt = $db->prepare('
DELETE FROM events
WHERE id = :id AND user_id = :user_id
');
return $stmt->execute(['id' => $id, 'user_id' => $userId]);
}
public static function countByUser(int $userId): int
{
self::ensureTable();
$db = Database::getInstance();
$stmt = $db->prepare('SELECT COUNT(*) as count FROM events WHERE user_id = :user_id');
$stmt->execute(['user_id' => $userId]);
return (int) $stmt->fetch()['count'];
}
public static function countThisWeek(int $userId): int
{
self::ensureTable();
$db = Database::getInstance();
// Poniedziałek tego tygodnia
$monday = date('Y-m-d', strtotime('monday this week'));
// Niedziela tego tygodnia
$sunday = date('Y-m-d', strtotime('sunday this week'));
$stmt = $db->prepare('
SELECT COUNT(*) as count FROM events
WHERE user_id = :user_id
AND event_date BETWEEN :monday AND :sunday
');
$stmt->execute([
'user_id' => $userId,
'monday' => $monday,
'sunday' => $sunday
]);
return (int) $stmt->fetch()['count'];
}
public static function getThisWeek(int $userId): array
{
self::ensureTable();
$db = Database::getInstance();
$monday = date('Y-m-d', strtotime('monday this week'));
$sunday = date('Y-m-d', strtotime('sunday this week'));
$stmt = $db->prepare('
SELECT * FROM events
WHERE user_id = :user_id
AND event_date BETWEEN :monday AND :sunday
ORDER BY event_date ASC
');
$stmt->execute([
'user_id' => $userId,
'monday' => $monday,
'sunday' => $sunday
]);
return $stmt->fetchAll();
}
}

103
app/models/Note.php Normal file
View File

@@ -0,0 +1,103 @@
<?php
require_once __DIR__ . '/Database.php';
class Note
{
public static function ensureTable(): void
{
$db = Database::getInstance();
$db->exec('
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
content TEXT,
color VARCHAR(20) DEFAULT "primary",
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)
');
}
public static function getAllByUser(int $userId): array
{
self::ensureTable();
$db = Database::getInstance();
$stmt = $db->prepare('
SELECT * FROM notes
WHERE user_id = :user_id
ORDER BY updated_at DESC
');
$stmt->execute(['user_id' => $userId]);
return $stmt->fetchAll();
}
public static function getById(int $id, int $userId): ?array
{
self::ensureTable();
$db = Database::getInstance();
$stmt = $db->prepare('
SELECT * FROM notes
WHERE id = :id AND user_id = :user_id
');
$stmt->execute(['id' => $id, 'user_id' => $userId]);
$note = $stmt->fetch();
return $note ?: null;
}
public static function create(int $userId, string $title, string $content, string $color = 'primary'): int
{
self::ensureTable();
$db = Database::getInstance();
$stmt = $db->prepare('
INSERT INTO notes (user_id, title, content, color, created_at, updated_at)
VALUES (:user_id, :title, :content, :color, datetime("now"), datetime("now"))
');
$stmt->execute([
'user_id' => $userId,
'title' => $title,
'content' => $content,
'color' => $color
]);
return (int) $db->lastInsertId();
}
public static function update(int $id, int $userId, string $title, string $content, string $color): bool
{
self::ensureTable();
$db = Database::getInstance();
$stmt = $db->prepare('
UPDATE notes
SET title = :title, content = :content, color = :color, updated_at = datetime("now")
WHERE id = :id AND user_id = :user_id
');
return $stmt->execute([
'id' => $id,
'user_id' => $userId,
'title' => $title,
'content' => $content,
'color' => $color
]);
}
public static function delete(int $id, int $userId): bool
{
$db = Database::getInstance();
$stmt = $db->prepare('
DELETE FROM notes
WHERE id = :id AND user_id = :user_id
');
return $stmt->execute(['id' => $id, 'user_id' => $userId]);
}
public static function countByUser(int $userId): int
{
self::ensureTable();
$db = Database::getInstance();
$stmt = $db->prepare('SELECT COUNT(*) as count FROM notes WHERE user_id = :user_id');
$stmt->execute(['user_id' => $userId]);
return (int) $stmt->fetch()['count'];
}
}

61
app/models/User.php Normal file
View File

@@ -0,0 +1,61 @@
<?php
require_once __DIR__ . '/Database.php';
class User
{
public static function findByLogin(string $login): ?array
{
$db = Database::getInstance();
$stmt = $db->prepare('SELECT * FROM users WHERE login = :login');
$stmt->execute(['login' => $login]);
$user = $stmt->fetch();
return $user ?: null;
}
public static function verifyPassword(string $login, string $password): ?array
{
$user = self::findByLogin($login);
if ($user && password_verify($password, $user['password'])) {
return $user;
}
return null;
}
public static function saveVerificationCode(int $userId, string $code): bool
{
$db = Database::getInstance();
$expiresAt = date('Y-m-d H:i:s', strtotime('+10 minutes'));
$stmt = $db->prepare('
INSERT OR REPLACE INTO verification_codes (user_id, code, expires_at, created_at)
VALUES (:user_id, :code, :expires_at, datetime("now"))
');
return $stmt->execute([
'user_id' => $userId,
'code' => $code,
'expires_at' => $expiresAt
]);
}
public static function verifyCode(int $userId, string $code): bool
{
$db = Database::getInstance();
$stmt = $db->prepare('
SELECT * FROM verification_codes
WHERE user_id = :user_id
AND code = :code
AND expires_at > datetime("now")
');
$stmt->execute(['user_id' => $userId, 'code' => $code]);
return (bool) $stmt->fetch();
}
public static function deleteVerificationCode(int $userId): bool
{
$db = Database::getInstance();
$stmt = $db->prepare('DELETE FROM verification_codes WHERE user_id = :user_id');
return $stmt->execute(['user_id' => $userId]);
}
}

164
app/views/calendar/day.php Normal file
View File

@@ -0,0 +1,164 @@
<?php
require_once __DIR__ . '/../layout.php';
$dayNames = [
1 => 'Poniedziałek', 2 => 'Wtorek', 3 => 'Środa', 4 => 'Czwartek',
5 => 'Piątek', 6 => 'Sobota', 7 => 'Niedziela'
];
$monthNames = [
1 => 'stycznia', 2 => 'lutego', 3 => 'marca', 4 => 'kwietnia',
5 => 'maja', 6 => 'czerwca', 7 => 'lipca', 8 => 'sierpnia',
9 => 'września', 10 => 'października', 11 => 'listopada', 12 => 'grudnia'
];
$timestamp = strtotime($date);
$dayOfWeek = date('N', $timestamp);
$day = date('j', $timestamp);
$month = date('n', $timestamp);
$year = date('Y', $timestamp);
$formattedDate = $dayNames[$dayOfWeek] . ', ' . $day . ' ' . $monthNames[$month] . ' ' . $year;
$isToday = ($date === date('Y-m-d'));
ob_start();
?>
<?php if (isset($success)): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i>
<?= htmlspecialchars($success) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if (isset($error)): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<?= htmlspecialchars($error) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<a href="/kalendarz?rok=<?= $year ?>&miesiac=<?= $month ?>" class="btn btn-outline-light mb-2">
<i class="bi bi-arrow-left me-1"></i>Powrót do kalendarza
</a>
<h2 class="text-white mb-0">
<?php if ($isToday): ?>
<span class="badge bg-success me-2">Dzisiaj</span>
<?php endif; ?>
<?= $formattedDate ?>
</h2>
</div>
<a href="/kalendarz/nowe/<?= $date ?>" class="btn btn-light btn-lg">
<i class="bi bi-plus-lg me-1"></i>Dodaj wydarzenie
</a>
</div>
<?php if (empty($events)): ?>
<div class="card">
<div class="card-body text-center py-5">
<i class="bi bi-calendar-x text-muted" style="font-size: 4rem;"></i>
<h4 class="mt-3 text-muted">Brak wydarzeń</h4>
<p class="text-muted mb-4">Nie masz żadnych wydarzeń zaplanowanych na ten dzień.</p>
<a href="/kalendarz/nowe/<?= $date ?>" class="btn btn-primary">
<i class="bi bi-plus-lg me-1"></i>Dodaj wydarzenie
</a>
</div>
</div>
<?php else: ?>
<div class="row g-3">
<?php foreach ($events as $event): ?>
<div class="col-12">
<div class="card event-card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h5 class="card-title mb-2">
<i class="bi bi-calendar-event text-primary me-2"></i>
<?= htmlspecialchars($event['title']) ?>
</h5>
<?php if (!empty($event['content'])): ?>
<p class="card-text text-muted mb-2">
<?= nl2br(htmlspecialchars($event['content'])) ?>
</p>
<?php endif; ?>
<small class="text-muted">
<i class="bi bi-clock me-1"></i>
Dodano: <?= date('d.m.Y H:i', strtotime($event['created_at'])) ?>
</small>
</div>
<div class="event-actions ms-3">
<a href="/kalendarz/edytuj/<?= $event['id'] ?>" class="btn btn-sm btn-outline-primary" title="Edytuj">
<i class="bi bi-pencil"></i>
</a>
<button type="button" class="btn btn-sm btn-outline-danger"
onclick="confirmDelete(<?= $event['id'] ?>, '<?= htmlspecialchars(addslashes($event['title'])) ?>')"
title="Usuń">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Modal potwierdzenia usunięcia -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header border-0">
<div class="text-center w-100">
<div class="bg-danger bg-opacity-10 rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 80px; height: 80px;">
<i class="bi bi-exclamation-triangle text-danger" style="font-size: 2.5rem;"></i>
</div>
<h5 class="modal-title">Potwierdzenie usunięcia</h5>
</div>
<button type="button" class="btn-close position-absolute top-0 end-0 m-3" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<p class="mb-1">Czy na pewno chcesz usunąć wydarzenie:</p>
<p class="fw-bold text-primary" id="deleteEventTitle"></p>
<p class="text-muted small mb-0">Ta operacja jest nieodwracalna.</p>
</div>
<div class="modal-footer justify-content-center border-0">
<button type="button" class="btn btn-secondary px-4" data-bs-dismiss="modal">
<i class="bi bi-x-lg me-1"></i>Anuluj
</button>
<form id="deleteForm" method="POST" class="d-inline">
<button type="submit" class="btn btn-danger px-4">
<i class="bi bi-trash me-1"></i>Usuń
</button>
</form>
</div>
</div>
</div>
</div>
<style>
.event-card {
border-left: 4px solid #667eea;
transition: transform 0.2s, box-shadow 0.2s;
}
.event-card:hover {
transform: translateX(5px);
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
}
</style>
<script>
function confirmDelete(id, title) {
document.getElementById('deleteEventTitle').textContent = '"' + title + '"';
document.getElementById('deleteForm').action = '/kalendarz/usun/' + id;
new bootstrap.Modal(document.getElementById('deleteModal')).show();
}
</script>
<?php
$content = ob_get_clean();
renderLayout('Wydarzenia - ' . $formattedDate, $content, true, 'kalendarz', 'col-lg-8');

View File

@@ -0,0 +1,72 @@
<?php
require_once __DIR__ . '/../layout.php';
$error = $_SESSION['error'] ?? null;
unset($_SESSION['error']);
ob_start();
?>
<div class="card">
<div class="card-header text-center text-white">
<i class="bi bi-<?= $isEdit ? 'pencil-square' : 'calendar-plus' ?> icon-large"></i>
<h4 class="mt-2 mb-0"><?= $isEdit ? 'Edytuj wydarzenie' : 'Nowe wydarzenie' ?></h4>
</div>
<div class="card-body p-4">
<?php if (isset($error)): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<?= htmlspecialchars($error) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<form method="POST" action="<?= $isEdit ? '/kalendarz/zapisz/' . $event['id'] : '/kalendarz/dodaj' ?>">
<div class="mb-3">
<label for="title" class="form-label">
<i class="bi bi-type me-1"></i>Tytuł wydarzenia
</label>
<input type="text" class="form-control" id="title" name="title"
value="<?= htmlspecialchars($event['title'] ?? '') ?>"
placeholder="Np. Spotkanie z klientem" required autofocus>
</div>
<div class="mb-3">
<label for="event_date" class="form-label">
<i class="bi bi-calendar-date me-1"></i>Data
</label>
<input type="date" class="form-control" id="event_date" name="event_date"
value="<?= htmlspecialchars($event['event_date'] ?? $selectedDate) ?>" required>
</div>
<div class="mb-4">
<label for="content" class="form-label">
<i class="bi bi-text-paragraph me-1"></i>Opis (opcjonalnie)
</label>
<textarea class="form-control" id="content" name="content" rows="4"
placeholder="Dodatkowe informacje o wydarzeniu..."><?= htmlspecialchars($event['content'] ?? '') ?></textarea>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-check-lg me-2"></i><?= $isEdit ? 'Zapisz zmiany' : 'Dodaj wydarzenie' ?>
</button>
<?php
$backUrl = '/kalendarz';
if ($isEdit && isset($event['event_date'])) {
$backUrl = '/kalendarz/dzien/' . $event['event_date'];
} elseif (isset($selectedDate)) {
$backUrl = '/kalendarz/dzien/' . $selectedDate;
}
?>
<a href="<?= $backUrl ?>" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i>Anuluj
</a>
</div>
</form>
</div>
</div>
<?php
$content = ob_get_clean();
renderLayout($isEdit ? 'Edytuj wydarzenie' : 'Nowe wydarzenie', $content, true, 'kalendarz', 'col-md-6');

View File

@@ -0,0 +1,200 @@
<?php
require_once __DIR__ . '/../layout.php';
$monthNames = [
1 => 'Styczeń', 2 => 'Luty', 3 => 'Marzec', 4 => 'Kwiecień',
5 => 'Maj', 6 => 'Czerwiec', 7 => 'Lipiec', 8 => 'Sierpień',
9 => 'Wrzesień', 10 => 'Październik', 11 => 'Listopad', 12 => 'Grudzień'
];
$dayNames = ['Pon', 'Wt', 'Śr', 'Czw', 'Pt', 'Sob', 'Ndz'];
// Obliczenia kalendarza
$firstDayOfMonth = mktime(0, 0, 0, $month, 1, $year);
$daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
$dayOfWeek = date('N', $firstDayOfMonth); // 1 = poniedziałek, 7 = niedziela
$prevMonth = $month - 1;
$prevYear = $year;
if ($prevMonth < 1) {
$prevMonth = 12;
$prevYear--;
}
$nextMonth = $month + 1;
$nextYear = $year;
if ($nextMonth > 12) {
$nextMonth = 1;
$nextYear++;
}
$today = date('Y-m-d');
ob_start();
?>
<?php if (isset($success)): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i>
<?= htmlspecialchars($success) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if (isset($error)): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<?= htmlspecialchars($error) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="card">
<div class="card-header bg-transparent">
<div class="d-flex justify-content-between align-items-center">
<a href="/kalendarz?rok=<?= $prevYear ?>&miesiac=<?= $prevMonth ?>" class="btn btn-outline-primary">
<i class="bi bi-chevron-left"></i>
</a>
<h4 class="mb-0">
<i class="bi bi-calendar3 me-2"></i>
<?= $monthNames[$month] ?> <?= $year ?>
</h4>
<a href="/kalendarz?rok=<?= $nextYear ?>&miesiac=<?= $nextMonth ?>" class="btn btn-outline-primary">
<i class="bi bi-chevron-right"></i>
</a>
</div>
</div>
<div class="card-body p-0">
<table class="table table-bordered mb-0 calendar-table">
<thead>
<tr>
<?php foreach ($dayNames as $dayName): ?>
<th class="text-center py-2 bg-light"><?= $dayName ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php
$currentDay = 1;
$started = false;
for ($week = 0; $week < 6; $week++):
if ($currentDay > $daysInMonth) break;
?>
<tr>
<?php for ($dow = 1; $dow <= 7; $dow++):
$cellDate = null;
$isToday = false;
$hasEvents = false;
$dayEvents = [];
if (!$started && $dow == $dayOfWeek) {
$started = true;
}
if ($started && $currentDay <= $daysInMonth) {
$cellDate = sprintf('%04d-%02d-%02d', $year, $month, $currentDay);
$isToday = ($cellDate === $today);
$hasEvents = isset($eventsByDate[$cellDate]);
if ($hasEvents) {
$dayEvents = $eventsByDate[$cellDate];
}
$displayDay = $currentDay;
$currentDay++;
} else {
$displayDay = null;
}
?>
<td class="calendar-cell <?= $isToday ? 'today' : '' ?> <?= $hasEvents ? 'has-events' : '' ?>"
<?php if ($cellDate): ?>onclick="window.location='/kalendarz/dzien/<?= $cellDate ?>'"<?php endif; ?>>
<?php if ($displayDay): ?>
<div class="day-number <?= $isToday ? 'bg-primary text-white' : '' ?>">
<?= $displayDay ?>
</div>
<?php if ($hasEvents): ?>
<div class="day-events">
<?php foreach (array_slice($dayEvents, 0, 2) as $evt): ?>
<div class="event-dot" title="<?= htmlspecialchars($evt['title']) ?>">
<?= htmlspecialchars(mb_substr($evt['title'], 0, 15)) ?><?= mb_strlen($evt['title']) > 15 ? '...' : '' ?>
</div>
<?php endforeach; ?>
<?php if (count($dayEvents) > 2): ?>
<div class="event-more">+<?= count($dayEvents) - 2 ?> więcej</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</td>
<?php endfor; ?>
</tr>
<?php endfor; ?>
</tbody>
</table>
</div>
<div class="card-footer bg-transparent">
<div class="d-flex justify-content-between align-items-center">
<a href="/kalendarz?rok=<?= date('Y') ?>&miesiac=<?= date('m') ?>" class="btn btn-outline-secondary">
<i class="bi bi-calendar-check me-1"></i>Dzisiaj
</a>
<a href="/kalendarz/nowe" class="btn btn-primary">
<i class="bi bi-plus-lg me-1"></i>Nowe wydarzenie
</a>
</div>
</div>
</div>
<style>
.calendar-table {
table-layout: fixed;
}
.calendar-cell {
height: 100px;
vertical-align: top;
padding: 5px !important;
cursor: pointer;
transition: background-color 0.2s;
}
.calendar-cell:hover {
background-color: #f8f9fa;
}
.calendar-cell.today {
background-color: #e7f1ff;
}
.calendar-cell.has-events {
background-color: #fff3cd;
}
.calendar-cell.today.has-events {
background-color: #d1e7dd;
}
.day-number {
display: inline-block;
width: 28px;
height: 28px;
line-height: 28px;
text-align: center;
border-radius: 50%;
font-weight: 600;
margin-bottom: 3px;
}
.day-events {
font-size: 0.7rem;
}
.event-dot {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1px 5px;
border-radius: 3px;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.event-more {
color: #6c757d;
font-style: italic;
}
</style>
<?php
$content = ob_get_clean();
renderLayout('Kalendarz', $content, true, 'kalendarz', 'col-lg-10');

179
app/views/dashboard.php Normal file
View File

@@ -0,0 +1,179 @@
<?php
require_once __DIR__ . '/layout.php';
require_once __DIR__ . '/../models/Note.php';
require_once __DIR__ . '/../models/Event.php';
$userId = $_SESSION['user_id'] ?? 0;
$notesCount = Note::countByUser($userId);
$eventsThisWeek = Event::countThisWeek($userId);
$upcomingEvents = Event::getThisWeek($userId);
ob_start();
?>
<div class="row">
<div class="col-12 mb-4">
<div class="card">
<div class="card-body p-4">
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-4"
style="width: 80px; height: 80px;">
<i class="bi bi-person-circle text-primary" style="font-size: 2.5rem;"></i>
</div>
<div>
<h3 class="mb-1">
Witaj, <span class="text-primary"><?= htmlspecialchars($userLogin) ?></span>!
</h3>
<p class="text-muted mb-0">
Pomyślnie zalogowano z uwierzytelnianiem dwuskładnikowym.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-md-3">
<div class="card h-100">
<div class="card-body text-center p-4">
<div class="bg-success bg-opacity-10 rounded-circle d-inline-flex align-items-center justify-content-center mb-3"
style="width: 60px; height: 60px;">
<i class="bi bi-shield-fill-check text-success fs-3"></i>
</div>
<h5 class="card-title">Status 2FA</h5>
<p class="text-success mb-0 fw-bold">Aktywne</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card h-100">
<div class="card-body text-center p-4">
<div class="bg-primary bg-opacity-10 rounded-circle d-inline-flex align-items-center justify-content-center mb-3"
style="width: 60px; height: 60px;">
<i class="bi bi-journal-text text-primary fs-3"></i>
</div>
<h5 class="card-title">Notatki</h5>
<p class="text-primary mb-0 fw-bold fs-4"><?= $notesCount ?></p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card h-100">
<div class="card-body text-center p-4">
<div class="bg-warning bg-opacity-10 rounded-circle d-inline-flex align-items-center justify-content-center mb-3"
style="width: 60px; height: 60px;">
<i class="bi bi-calendar-event text-warning fs-3"></i>
</div>
<h5 class="card-title">Ten tydzień</h5>
<p class="text-warning mb-0 fw-bold fs-4"><?= $eventsThisWeek ?> <?= $eventsThisWeek == 1 ? 'wydarzenie' : ($eventsThisWeek >= 2 && $eventsThisWeek <= 4 ? 'wydarzenia' : 'wydarzeń') ?></p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card h-100">
<div class="card-body text-center p-4">
<div class="bg-info bg-opacity-10 rounded-circle d-inline-flex align-items-center justify-content-center mb-3"
style="width: 60px; height: 60px;">
<i class="bi bi-clock-fill text-info fs-3"></i>
</div>
<h5 class="card-title">Sesja</h5>
<p class="text-info mb-0 fw-bold">Aktywna</p>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-transparent">
<h5 class="mb-0"><i class="bi bi-lightning-fill text-warning me-2"></i>Szybkie akcje</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="/notatnik/nowa" class="btn btn-outline-primary">
<i class="bi bi-plus-lg me-2"></i>Nowa notatka
</a>
<a href="/kalendarz/nowe" class="btn btn-outline-warning">
<i class="bi bi-calendar-plus me-2"></i>Nowe wydarzenie
</a>
<a href="/kalendarz" class="btn btn-outline-secondary">
<i class="bi bi-calendar3 me-2"></i>Otwórz kalendarz
</a>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-transparent d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-calendar-week text-primary me-2"></i>Wydarzenia tego tygodnia</h5>
<a href="/kalendarz" class="btn btn-sm btn-outline-primary">Zobacz wszystkie</a>
</div>
<div class="card-body">
<?php if (empty($upcomingEvents)): ?>
<p class="text-muted text-center mb-0 py-3">
<i class="bi bi-calendar-x d-block mb-2" style="font-size: 2rem;"></i>
Brak wydarzeń w tym tygodniu
</p>
<?php else: ?>
<ul class="list-unstyled mb-0">
<?php foreach (array_slice($upcomingEvents, 0, 4) as $event): ?>
<li class="d-flex align-items-center mb-2 pb-2 border-bottom">
<div class="bg-primary bg-opacity-10 rounded p-2 me-3">
<i class="bi bi-calendar-event text-primary"></i>
</div>
<div class="flex-grow-1">
<div class="fw-semibold"><?= htmlspecialchars($event['title']) ?></div>
<small class="text-muted">
<?= date('d.m (D)', strtotime($event['event_date'])) ?>
</small>
</div>
<a href="/kalendarz/dzien/<?= $event['event_date'] ?>" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-eye"></i>
</a>
</li>
<?php endforeach; ?>
<?php if (count($upcomingEvents) > 4): ?>
<li class="text-center text-muted small pt-2">
+ <?= count($upcomingEvents) - 4 ?> więcej wydarzeń
</li>
<?php endif; ?>
</ul>
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-transparent">
<h5 class="mb-0"><i class="bi bi-info-circle-fill text-info me-2"></i>Informacje o sesji</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<i class="bi bi-calendar3 text-muted me-2"></i>
<strong>Data logowania:</strong> <?= date('d.m.Y H:i') ?>
</div>
<div class="col-md-4">
<i class="bi bi-shield-check text-muted me-2"></i>
<strong>Metoda 2FA:</strong> Kod Email
</div>
<div class="col-md-4">
<i class="bi bi-lock text-muted me-2"></i>
<strong>Połączenie:</strong> Szyfrowane
</div>
</div>
</div>
</div>
</div>
</div>
<?php
$content = ob_get_clean();
renderLayout('Pulpit', $content, true, 'pulpit', 'col-lg-10');

203
app/views/layout.php Normal file
View File

@@ -0,0 +1,203 @@
<?php
function renderLayout(string $title, string $content, bool $showMenu = false, string $activeMenu = '', string $containerSize = 'col-md-5'): void
{
$userLogin = $_SESSION['user_login'] ?? '';
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($title) ?> - System 2FA</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
body.with-menu {
display: block;
padding-top: 70px;
}
body:not(.with-menu) {
display: flex;
align-items: center;
justify-content: center;
}
.navbar-custom {
background: rgba(255,255,255,0.95) !important;
backdrop-filter: blur(10px);
box-shadow: 0 2px 20px rgba(0,0,0,0.1);
}
.navbar-brand {
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.nav-link {
color: #495057 !important;
font-weight: 500;
padding: 0.5rem 1rem !important;
border-radius: 10px;
transition: all 0.2s;
}
.nav-link:hover, .nav-link.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white !important;
}
.card {
border: none;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
.card-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 15px 15px 0 0 !important;
padding: 1.5rem;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
padding: 0.75rem 2rem;
border-radius: 25px;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
background: linear-gradient(135deg, #5a6fd6 0%, #6a4190 100%);
}
.form-control, .form-select {
border-radius: 10px;
padding: 0.75rem 1rem;
border: 2px solid #e9ecef;
}
.form-control:focus, .form-select:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
.icon-large {
font-size: 3rem;
}
.code-display {
background: #f8f9fa;
border: 2px dashed #667eea;
border-radius: 10px;
padding: 1rem;
font-family: monospace;
font-size: 1.5rem;
letter-spacing: 0.5rem;
text-align: center;
}
.note-card {
border-radius: 15px;
transition: transform 0.2s, box-shadow 0.2s;
height: 100%;
}
.note-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
}
.note-card .card-body {
display: flex;
flex-direction: column;
}
.note-content {
flex-grow: 1;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
}
.note-actions {
opacity: 0;
transition: opacity 0.2s;
}
.note-card:hover .note-actions {
opacity: 1;
}
.color-option {
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
border: 3px solid transparent;
transition: transform 0.2s;
}
.color-option:hover {
transform: scale(1.1);
}
.color-option.selected {
border-color: #333;
}
.modal-confirm .modal-header {
border-bottom: none;
padding-bottom: 0;
}
.modal-confirm .modal-footer {
border-top: none;
padding-top: 0;
}
</style>
</head>
<body class="<?= $showMenu ? 'with-menu' : '' ?>">
<?php if ($showMenu): ?>
<nav class="navbar navbar-expand-lg navbar-custom fixed-top">
<div class="container">
<a class="navbar-brand" href="/panel">
<i class="bi bi-shield-lock-fill me-2"></i>System 2FA
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link <?= $activeMenu === 'pulpit' ? 'active' : '' ?>" href="/panel">
<i class="bi bi-house-fill me-1"></i>Pulpit
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $activeMenu === 'notatnik' ? 'active' : '' ?>" href="/notatnik">
<i class="bi bi-journal-text me-1"></i>Notatnik
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $activeMenu === 'kalendarz' ? 'active' : '' ?>" href="/kalendarz">
<i class="bi bi-calendar3 me-1"></i>Kalendarz
</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle me-1"></i><?= htmlspecialchars($userLogin) ?>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item text-danger" href="/wyloguj-sie">
<i class="bi bi-box-arrow-right me-2"></i>Wyloguj się
</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<?php endif; ?>
<div class="container <?= $showMenu ? 'py-4' : '' ?>">
<div class="row justify-content-center">
<div class="<?= $containerSize ?>">
<?= $content ?>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
<?php
}

53
app/views/login.php Normal file
View File

@@ -0,0 +1,53 @@
<?php
require_once __DIR__ . '/layout.php';
ob_start();
?>
<div class="card">
<div class="card-header text-center text-white">
<i class="bi bi-shield-lock icon-large"></i>
<h4 class="mt-2 mb-0">Logowanie</h4>
</div>
<div class="card-body p-4">
<?php if (isset($error)): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<?= htmlspecialchars($error) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<form method="POST" action="/zaloguj">
<div class="mb-3">
<label for="login" class="form-label">
<i class="bi bi-person-fill me-1"></i>Login
</label>
<input type="text" class="form-control" id="login" name="login"
placeholder="Wprowadź login" required autofocus>
</div>
<div class="mb-4">
<label for="password" class="form-label">
<i class="bi bi-key-fill me-1"></i>Hasło
</label>
<input type="password" class="form-control" id="password" name="password"
placeholder="Wprowadź hasło" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-box-arrow-in-right me-2"></i>Zaloguj się
</button>
</div>
</form>
</div>
<div class="card-footer text-center text-muted bg-transparent border-0 pb-4">
<small>
<i class="bi bi-info-circle me-1"></i>
System zabezpieczony uwierzytelnianiem dwuskładnikowym
</small>
</div>
</div>
<?php
$content = ob_get_clean();
renderLayout('Logowanie', $content);

91
app/views/notes/form.php Normal file
View File

@@ -0,0 +1,91 @@
<?php
require_once __DIR__ . '/../layout.php';
$error = $_SESSION['error'] ?? null;
unset($_SESSION['error']);
$colors = [
'primary' => '#667eea',
'success' => '#28a745',
'danger' => '#dc3545',
'warning' => '#ffc107',
'info' => '#17a2b8',
'secondary' => '#6c757d'
];
$currentColor = $note['color'] ?? 'primary';
ob_start();
?>
<div class="card">
<div class="card-header text-center text-white">
<i class="bi bi-<?= $isEdit ? 'pencil-square' : 'plus-circle' ?> icon-large"></i>
<h4 class="mt-2 mb-0"><?= $isEdit ? 'Edytuj notatkę' : 'Nowa notatka' ?></h4>
</div>
<div class="card-body p-4">
<?php if (isset($error)): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<?= htmlspecialchars($error) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<form method="POST" action="<?= $isEdit ? '/notatnik/zapisz/' . $note['id'] : '/notatnik/dodaj' ?>">
<div class="mb-3">
<label for="title" class="form-label">
<i class="bi bi-type me-1"></i>Tytuł
</label>
<input type="text" class="form-control" id="title" name="title"
value="<?= htmlspecialchars($note['title'] ?? '') ?>"
placeholder="Wprowadź tytuł notatki" required autofocus>
</div>
<div class="mb-3">
<label for="content" class="form-label">
<i class="bi bi-text-paragraph me-1"></i>Treść
</label>
<textarea class="form-control" id="content" name="content" rows="6"
placeholder="Wprowadź treść notatki..."><?= htmlspecialchars($note['content'] ?? '') ?></textarea>
</div>
<div class="mb-4">
<label class="form-label">
<i class="bi bi-palette me-1"></i>Kolor
</label>
<div class="d-flex gap-2 flex-wrap">
<?php foreach ($colors as $name => $hex): ?>
<div class="color-option <?= $currentColor === $name ? 'selected' : '' ?>"
style="background-color: <?= $hex ?>;"
onclick="selectColor('<?= $name ?>', this)"
title="<?= ucfirst($name) ?>">
</div>
<?php endforeach; ?>
</div>
<input type="hidden" name="color" id="colorInput" value="<?= htmlspecialchars($currentColor) ?>">
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-check-lg me-2"></i><?= $isEdit ? 'Zapisz zmiany' : 'Dodaj notatkę' ?>
</button>
<a href="/notatnik" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i>Powrót do listy
</a>
</div>
</form>
</div>
</div>
<script>
function selectColor(color, element) {
document.querySelectorAll('.color-option').forEach(el => el.classList.remove('selected'));
element.classList.add('selected');
document.getElementById('colorInput').value = color;
}
</script>
<?php
$content = ob_get_clean();
renderLayout($isEdit ? 'Edytuj notatkę' : 'Nowa notatka', $content, true, 'notatnik', 'col-md-6');

120
app/views/notes/index.php Normal file
View File

@@ -0,0 +1,120 @@
<?php
require_once __DIR__ . '/../layout.php';
ob_start();
?>
<?php if (isset($success)): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i>
<?= htmlspecialchars($success) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if (isset($error)): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<?= htmlspecialchars($error) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="text-white mb-0">
<i class="bi bi-journal-text me-2"></i>Moje notatki
</h2>
<a href="/notatnik/nowa" class="btn btn-light btn-lg">
<i class="bi bi-plus-lg me-1"></i>Nowa notatka
</a>
</div>
<?php if (empty($notes)): ?>
<div class="card">
<div class="card-body text-center py-5">
<i class="bi bi-journal-x text-muted" style="font-size: 4rem;"></i>
<h4 class="mt-3 text-muted">Brak notatek</h4>
<p class="text-muted mb-4">Nie masz jeszcze żadnych notatek. Utwórz pierwszą!</p>
<a href="/notatnik/nowa" class="btn btn-primary">
<i class="bi bi-plus-lg me-1"></i>Utwórz notatkę
</a>
</div>
</div>
<?php else: ?>
<div class="row g-4">
<?php foreach ($notes as $note): ?>
<div class="col-md-6 col-lg-4">
<div class="card note-card border-top border-4 border-<?= htmlspecialchars($note['color']) ?>">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<h5 class="card-title mb-0"><?= htmlspecialchars($note['title']) ?></h5>
<div class="note-actions">
<a href="/notatnik/edytuj/<?= $note['id'] ?>" class="btn btn-sm btn-outline-primary" title="Edytuj">
<i class="bi bi-pencil"></i>
</a>
<button type="button" class="btn btn-sm btn-outline-danger"
onclick="confirmDelete(<?= $note['id'] ?>, '<?= htmlspecialchars(addslashes($note['title'])) ?>')"
title="Usuń">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<p class="note-content text-muted small">
<?= nl2br(htmlspecialchars($note['content'] ?: 'Brak treści')) ?>
</p>
<div class="mt-auto pt-2 border-top">
<small class="text-muted">
<i class="bi bi-clock me-1"></i>
<?= date('d.m.Y H:i', strtotime($note['updated_at'])) ?>
</small>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Modal potwierdzenia usunięcia -->
<div class="modal fade modal-confirm" id="deleteModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<div class="text-center w-100">
<div class="bg-danger bg-opacity-10 rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 80px; height: 80px;">
<i class="bi bi-exclamation-triangle text-danger" style="font-size: 2.5rem;"></i>
</div>
<h5 class="modal-title">Potwierdzenie usunięcia</h5>
</div>
<button type="button" class="btn-close position-absolute top-0 end-0 m-3" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<p class="mb-1">Czy na pewno chcesz usunąć notatkę:</p>
<p class="fw-bold text-primary" id="deleteNoteTitle"></p>
<p class="text-muted small mb-0">Ta operacja jest nieodwracalna.</p>
</div>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-secondary px-4" data-bs-dismiss="modal">
<i class="bi bi-x-lg me-1"></i>Anuluj
</button>
<form id="deleteForm" method="POST" class="d-inline">
<button type="submit" class="btn btn-danger px-4">
<i class="bi bi-trash me-1"></i>Usuń
</button>
</form>
</div>
</div>
</div>
</div>
<script>
function confirmDelete(id, title) {
document.getElementById('deleteNoteTitle').textContent = '"' + title + '"';
document.getElementById('deleteForm').action = '/notatnik/usun/' + id;
new bootstrap.Modal(document.getElementById('deleteModal')).show();
}
</script>
<?php
$content = ob_get_clean();
renderLayout('Notatnik', $content, true, 'notatnik', 'col-12');

74
app/views/verify.php Normal file
View File

@@ -0,0 +1,74 @@
<?php
require_once __DIR__ . '/layout.php';
ob_start();
?>
<div class="card">
<div class="card-header text-center text-white">
<i class="bi bi-phone icon-large"></i>
<h4 class="mt-2 mb-0">Weryfikacja dwuskładnikowa</h4>
</div>
<div class="card-body p-4">
<?php if (isset($error)): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<?= htmlspecialchars($error) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="alert alert-info">
<i class="bi bi-envelope-fill me-2"></i>
Kod weryfikacyjny został wysłany na adres e-mail powiązany z kontem
<strong><?= htmlspecialchars($userLogin) ?></strong>.
</div>
<?php if (isset($simulatedCode)): ?>
<div class="mb-4">
<label class="form-label text-muted">
<i class="bi bi-bug-fill me-1"></i>Kod testowy (symulacja):
</label>
<div class="code-display">
<?= htmlspecialchars($simulatedCode) ?>
</div>
</div>
<?php endif; ?>
<form method="POST" action="/zweryfikuj">
<div class="mb-4">
<label for="code" class="form-label">
<i class="bi bi-123 me-1"></i>Kod weryfikacyjny
</label>
<input type="text" class="form-control form-control-lg text-center"
id="code" name="code" maxlength="6"
placeholder="000000" required autofocus
style="letter-spacing: 0.5rem; font-size: 1.5rem;">
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-check-circle me-2"></i>Zweryfikuj
</button>
<a href="/wyloguj-sie" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i>Anuluj i wróć
</a>
</div>
</form>
</div>
<div class="card-footer text-center text-muted bg-transparent border-0 pb-4">
<small>
<i class="bi bi-clock me-1"></i>
Kod jest ważny przez 10 minut
</small>
</div>
</div>
<script>
document.getElementById('code').addEventListener('input', function(e) {
this.value = this.value.replace(/[^0-9]/g, '');
});
</script>
<?php
$content = ob_get_clean();
renderLayout('Weryfikacja', $content);

132
index.php Normal file
View File

@@ -0,0 +1,132 @@
<?php
session_start();
require_once __DIR__ . '/app/controllers/AuthController.php';
require_once __DIR__ . '/app/controllers/NoteController.php';
require_once __DIR__ . '/app/controllers/EventController.php';
// Pobranie ścieżki URL
$route = $_GET['route'] ?? '';
$route = trim($route, '/');
$authController = new AuthController();
$noteController = new NoteController();
$eventController = new EventController();
// Routing z przyjaznymi linkami
switch (true) {
// Strona główna / logowanie
case $route === '':
case $route === 'logowanie':
$authController->loginForm();
break;
case $route === 'zaloguj':
$authController->login();
break;
case $route === 'weryfikacja':
$authController->verifyForm();
break;
case $route === 'zweryfikuj':
$authController->verify();
break;
case $route === 'panel':
$authController->dashboard();
break;
case $route === 'wyloguj-sie':
$authController->logout();
break;
// Notatnik
case $route === 'notatnik':
$noteController->index();
break;
case $route === 'notatnik/nowa':
$noteController->create();
break;
case $route === 'notatnik/dodaj':
$noteController->store();
break;
case preg_match('#^notatnik/edytuj/(\d+)$#', $route, $matches) === 1:
$noteController->edit((int) $matches[1]);
break;
case preg_match('#^notatnik/zapisz/(\d+)$#', $route, $matches) === 1:
$noteController->update((int) $matches[1]);
break;
case preg_match('#^notatnik/usun/(\d+)$#', $route, $matches) === 1:
$noteController->delete((int) $matches[1]);
break;
// Kalendarz
case $route === 'kalendarz':
$eventController->index();
break;
case $route === 'kalendarz/nowe':
$eventController->create();
break;
case preg_match('#^kalendarz/nowe/(\d{4}-\d{2}-\d{2})$#', $route, $matches) === 1:
$eventController->create($matches[1]);
break;
case $route === 'kalendarz/dodaj':
$eventController->store();
break;
case preg_match('#^kalendarz/dzien/(\d{4}-\d{2}-\d{2})$#', $route, $matches) === 1:
$eventController->dayEvents($matches[1]);
break;
case preg_match('#^kalendarz/edytuj/(\d+)$#', $route, $matches) === 1:
$eventController->edit((int) $matches[1]);
break;
case preg_match('#^kalendarz/zapisz/(\d+)$#', $route, $matches) === 1:
$eventController->update((int) $matches[1]);
break;
case preg_match('#^kalendarz/usun/(\d+)$#', $route, $matches) === 1:
$eventController->delete((int) $matches[1]);
break;
default:
// Strona 404
http_response_code(404);
echo '<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - Nie znaleziono</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
</style>
</head>
<body>
<div class="text-center text-white">
<h1 class="display-1">404</h1>
<p class="lead">Strona nie została znaleziona</p>
<a href="/" class="btn btn-light btn-lg mt-3">Wróć do strony głównej</a>
</div>
</body>
</html>';
break;
}

163
init.php Normal file
View File

@@ -0,0 +1,163 @@
<?php
require_once __DIR__ . '/app/models/Database.php';
// Sprawdzenie czy baza istnieje
$dbPath = Database::getDbPath();
$dbExists = file_exists($dbPath);
try {
$db = Database::getInstance();
// Tworzenie tabeli użytkowników
$db->exec('
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
login VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
email VARCHAR(100),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
');
// Tworzenie tabeli kodów weryfikacyjnych
$db->exec('
CREATE TABLE IF NOT EXISTS verification_codes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
code VARCHAR(6) NOT NULL,
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id)
)
');
// Sprawdzenie czy użytkownik testowy istnieje
$stmt = $db->prepare('SELECT id FROM users WHERE login = :login');
$stmt->execute(['login' => 'projectpro']);
$userExists = $stmt->fetch();
if (!$userExists) {
// Dodanie testowego użytkownika
$password = password_hash('testowehaslo', PASSWORD_DEFAULT);
$stmt = $db->prepare('
INSERT INTO users (login, password, email)
VALUES (:login, :password, :email)
');
$stmt->execute([
'login' => 'projectpro',
'password' => $password,
'email' => 'projectpro@example.com'
]);
$userCreated = true;
} else {
$userCreated = false;
}
// Wyświetlenie statusu
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inicjalizacja bazy danych</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.card {
border: none;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
</style>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header bg-success text-white text-center py-3">
<i class="bi bi-database-check fs-1"></i>
<h4 class="mt-2 mb-0">Inicjalizacja zakończona</h4>
</div>
<div class="card-body p-4">
<div class="alert alert-success">
<i class="bi bi-check-circle-fill me-2"></i>
<strong>Baza danych została zainicjalizowana pomyślnie!</strong>
</div>
<h5 class="mt-4 mb-3">Status:</h5>
<ul class="list-group mb-4">
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><i class="bi bi-table me-2"></i>Tabela <code>users</code></span>
<span class="badge bg-success">OK</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><i class="bi bi-table me-2"></i>Tabela <code>verification_codes</code></span>
<span class="badge bg-success">OK</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><i class="bi bi-person me-2"></i>Użytkownik testowy</span>
<span class="badge bg-<?= $userCreated ? 'success' : 'info' ?>">
<?= $userCreated ? 'Utworzony' : 'Już istnieje' ?>
</span>
</li>
</ul>
<h5 class="mb-3">Dane testowe:</h5>
<div class="bg-light p-3 rounded">
<p class="mb-2">
<strong>Login:</strong>
<code>projectpro</code>
</p>
<p class="mb-0">
<strong>Hasło:</strong>
<code>testowehaslo</code>
</p>
</div>
<div class="d-grid mt-4">
<a href="/logowanie" class="btn btn-primary btn-lg">
<i class="bi bi-box-arrow-in-right me-2"></i>Przejdź do logowania
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
<?php
} catch (Exception $e) {
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Błąd inicjalizacji</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-danger bg-opacity-10">
<div class="container py-5">
<div class="alert alert-danger">
<h4><i class="bi bi-exclamation-triangle me-2"></i>Błąd inicjalizacji bazy danych</h4>
<hr>
<p class="mb-0"><?= htmlspecialchars($e->getMessage()) ?></p>
</div>
</div>
</body>
</html>
<?php
}