Compare commits
26 Commits
4d7edeea88
...
last_habit
Author | SHA1 | Date | |
---|---|---|---|
eff5be49c4 | |||
d13c490efb | |||
d98c208df9 | |||
85af85b1ee | |||
1ff7fc454f | |||
e0dfc7120f | |||
9c90710bf3 | |||
4b8ee90d8a | |||
59246decd7 | |||
147e3b1499 | |||
daec4ec1c9 | |||
3c6ecfb5e2 | |||
b4e08f28ca | |||
c4366edb29 | |||
d9f632da26 | |||
2847231376 | |||
d33d233f0f | |||
43960ddcb9 | |||
85209ff134 | |||
4c44dac115 | |||
01b057986e | |||
ecb2a6b74b | |||
5bca9b12aa | |||
d7b8aba072 | |||
3144078860 | |||
7b4b349816 |
53
.docker/nginx.conf
Normal file
53
.docker/nginx.conf
Normal file
@@ -0,0 +1,53 @@
|
||||
# User and worker process settings
|
||||
user www-data;
|
||||
worker_processes auto;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
# General settings
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Logging
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_disable "msie6";
|
||||
|
||||
# Server configuration
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
root /var/www/html/public;
|
||||
index index.php index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ \\.php$ {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass 127.0.0.1:9000;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
}
|
||||
|
||||
location ~ /\.ht {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
22
.dockerignore
Normal file
22
.dockerignore
Normal file
@@ -0,0 +1,22 @@
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
storage/logs/*
|
||||
!storage/logs/.gitignore
|
||||
|
||||
Dockerfile
|
||||
docker-compose.yaml
|
||||
|
||||
*.swp
|
||||
*.swo
|
||||
*.idea/
|
||||
*.vscode/
|
||||
*.DS_Store
|
||||
|
||||
# Exclude sensitive config files (uncomment if you don't want to include environment config)
|
||||
# config/environment.php
|
||||
|
||||
README.md
|
||||
TODO.md
|
||||
LICENSE
|
||||
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.swp
|
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM php:8.2-fpm-alpine
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
RUN apk add --no-cache nginx curl \
|
||||
&& docker-php-ext-install mysqli
|
||||
|
||||
COPY . /var/www/html
|
||||
|
||||
RUN chown -R www-data:www-data /var/www/html \
|
||||
&& chmod -R 755 /var/www/html
|
||||
|
||||
COPY .docker/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD php-fpm & nginx -g "daemon off;"
|
||||
|
56
README.md
56
README.md
@@ -1,3 +1,55 @@
|
||||
# habit-tracker
|
||||
# Habit Tracker
|
||||
|
||||
An app for tracking habits and motivation to achieve personal goals
|
||||
|
||||
## Used technologies
|
||||
- **Frontend:** HTML, CSS, JavaScript
|
||||
- **Backend:** PHP (OOP)
|
||||
- **Database:** MariaDB
|
||||
|
||||
## How to build
|
||||
|
||||
### Build using docker
|
||||
Run the container using docker-compose
|
||||
```bash
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
The app should be available at http://localhost:8000
|
||||
|
||||
PhpMyAdmin should be available at http://localhost:8080
|
||||
|
||||
### Build manually
|
||||
1. Clone the repo
|
||||
```bash
|
||||
git clone https://git.filiprojek.cz/fr/habit-tracker.git
|
||||
```
|
||||
|
||||
2. Create `config/environment.php`
|
||||
- It should have following structure:
|
||||
```php
|
||||
<?php
|
||||
|
||||
define('DB_HOST', 'your db host');
|
||||
define('DB_USER', 'your db username');
|
||||
define('DB_PASS', 'your db password');
|
||||
define('DB_NAME', 'your db name');
|
||||
```
|
||||
- For the database, you can use included `docker-compose.yaml` which have both MariaDB and PhpMyAdmin
|
||||
|
||||
3. Start an local web server
|
||||
- You can use php's integrated server by running this:
|
||||
```bash
|
||||
php -S localhost:8000 -t ./public
|
||||
```
|
||||
- You can use any host and any port you want.
|
||||
|
||||
## Usage
|
||||
1. Register and Login to the app.
|
||||
2. Add your habits.
|
||||
3. Mark your habits when you're done doing them.
|
||||
4. Earn point and unlock achievements by completing you're habits!
|
||||
|
||||
## Licence
|
||||
This project is licensed under GPL3.0 and later. More information is availabe in `LICENSE` file.
|
||||
|
||||
An app for tracking habits and motivation to achieve personal goals.
|
18
TODO.md
Normal file
18
TODO.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# TODO
|
||||
|
||||
## Auth and User stuff
|
||||
- [x] signup
|
||||
- [x] signin
|
||||
- [ ] edit user data - change password, mail...
|
||||
|
||||
## Core of the app
|
||||
- [ ] header and navbar
|
||||
- [ ] dashboard
|
||||
- [x] css
|
||||
- [ ] its just plain
|
||||
- [ ] graphs
|
||||
- [x] Habits list
|
||||
- [ ] css
|
||||
- [ ] Habits create
|
||||
- [ ] validate cron input
|
||||
- [ ] Habits track
|
85
app/controllers/AuthController.php
Normal file
85
app/controllers/AuthController.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
class AuthController extends Controller {
|
||||
public function signin() {
|
||||
if($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$email = $_POST['email'] ?? '';
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
$validator = new Validator();
|
||||
$validator->required('email', $email);
|
||||
$validator->email('email', $email);
|
||||
$validator->required('password', $password);
|
||||
|
||||
if (!$validator->passes()) {
|
||||
$this->view('auth/signup', [
|
||||
'error' => 'Please correct the errors below.',
|
||||
'validationErrors' => $validator->errors() ?: [],
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$user = new User();
|
||||
$result = $user->login($email, $password);
|
||||
|
||||
if($result === true) {
|
||||
$this->redirect('/dashboard');
|
||||
} else {
|
||||
$this->view('auth/signin', ['error' => $result], 'noheader');
|
||||
}
|
||||
} else {
|
||||
$this->view('auth/signin', ['title' => 'Log In'], 'noheader');
|
||||
}
|
||||
}
|
||||
|
||||
public function signup() {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = $_POST['username'] ?? '';
|
||||
$email = $_POST['email'] ?? '';
|
||||
$password = $_POST['password'] ?? '';
|
||||
$password2 = $_POST['password-2'] ?? '';
|
||||
|
||||
$validator = new Validator();
|
||||
$validator->required('username', $username);
|
||||
$validator->email('email', $email);
|
||||
$validator->required('password', $password);
|
||||
$validator->minLength('password', $password, 8);
|
||||
$validator->alphanumeric('password', $password);
|
||||
|
||||
if ($password !== $password2) {
|
||||
$validator->errors()['password_confirmation'] = 'Passwords do not match.';
|
||||
}
|
||||
|
||||
if (!$validator->passes()) {
|
||||
$this->view('auth/signup', [
|
||||
'error' => 'Please correct the errors below.',
|
||||
'validationErrors' => $validator->errors() ?: [],
|
||||
], 'noheader');
|
||||
return;
|
||||
}
|
||||
|
||||
$user = new User();
|
||||
$result = $user->register($username, $email, $password);
|
||||
|
||||
if ($result === true) {
|
||||
$this->redirect('/auth/signin');
|
||||
} else {
|
||||
$this->view('auth/signup', [
|
||||
'error' => $result,
|
||||
'validationErrors' => [],
|
||||
], 'noheader');
|
||||
}
|
||||
} else {
|
||||
$this->view('auth/signup', [
|
||||
'title' => 'Register',
|
||||
'validationErrors' => [],
|
||||
], 'noheader');
|
||||
}
|
||||
}
|
||||
|
||||
public function logout() {
|
||||
session_unset();
|
||||
session_destroy();
|
||||
$this->redirect('/auth/signin');
|
||||
}
|
||||
}
|
16
app/controllers/DashboardController.php
Normal file
16
app/controllers/DashboardController.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
class DashboardController extends Controller {
|
||||
public function index() {
|
||||
$habit = new Habit();
|
||||
$habits = $habit->getHabitsByUser($_SESSION['user']['id']);
|
||||
|
||||
$this->view('dashboard/index', [
|
||||
'title' => 'Dashboard',
|
||||
'habits' => $habits,
|
||||
]);
|
||||
}
|
||||
|
||||
public function reroute(){
|
||||
$this->redirect('/dashboard');
|
||||
}
|
||||
}
|
57
app/controllers/HabitController.php
Normal file
57
app/controllers/HabitController.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
class HabitController extends Controller {
|
||||
public function index() {
|
||||
$habit = new Habit();
|
||||
$habits = $habit->getHabitsByUser($_SESSION['user']['id']);
|
||||
$this->view('habits/index', ['title' => 'Habits', 'habits' => $habits]);
|
||||
}
|
||||
|
||||
public function create() {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$name = $_POST['name'] ?? '';
|
||||
$frequency = $_POST['frequency'] ?? 'Daily';
|
||||
$customFrequency = null;
|
||||
|
||||
if (empty($name)) {
|
||||
$this->view('habits/create', ['error' => 'Habit name is required.']);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($frequency === 'Custom') {
|
||||
$daysOfWeek = $_POST['days_of_week'] ?? [];
|
||||
$daysOfMonth = $_POST['days_of_month'] ?? '*';
|
||||
$months = $_POST['months'] ?? '*';
|
||||
|
||||
// Combine into crontab-like string
|
||||
$customFrequency = implode(',', $daysOfWeek) . " $daysOfMonth $months";
|
||||
}
|
||||
|
||||
$habit = new Habit();
|
||||
$result = $habit->create([
|
||||
'name' => $name,
|
||||
'frequency' => $frequency,
|
||||
'custom_frequency' => $customFrequency,
|
||||
'reward_points' => intval($_POST['difficulty'] ?? 1),
|
||||
'user_id' => $_SESSION['user']['id'],
|
||||
]);
|
||||
|
||||
if ($result) {
|
||||
$this->redirect('/habits');
|
||||
} else {
|
||||
$this->view('habits/create', ['error' => 'Failed to create habit.']);
|
||||
}
|
||||
} else {
|
||||
$this->view('habits/create', ['title' => 'Create Habit']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function edit() {
|
||||
// Edit habit (to be implemented later)
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
// Delete habit (to be implemented later)
|
||||
}
|
||||
}
|
18
app/controllers/HomeController.php
Normal file
18
app/controllers/HomeController.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
class HomeController extends Controller {
|
||||
public function index() {
|
||||
$data = [
|
||||
'title' => 'Home'
|
||||
];
|
||||
$this->view('home/index', $data);
|
||||
}
|
||||
|
||||
public function home() {
|
||||
$this->index();
|
||||
}
|
||||
|
||||
public function dashboard() {
|
||||
$this->view("dashboard/index");
|
||||
}
|
||||
}
|
46
app/models/Habit.php
Normal file
46
app/models/Habit.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
class Habit {
|
||||
private $db;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = Database::getInstance()->getConnection();
|
||||
}
|
||||
|
||||
public function create($data) {
|
||||
$stmt = $this->db->prepare("
|
||||
INSERT INTO habits (user_id, title, frequency, custom_frequency, reward_points, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, NOW())
|
||||
");
|
||||
|
||||
$stmt->bind_param(
|
||||
"isssi", // Bind types: int, string, string, string, int
|
||||
$data['user_id'],
|
||||
$data['name'],
|
||||
$data['frequency'],
|
||||
$data['custom_frequency'], // Bind the custom_frequency field
|
||||
$data['reward_points']
|
||||
);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
return true;
|
||||
} else {
|
||||
error_log("Failed to create habit: " . $stmt->error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getHabitsByUser($userId) {
|
||||
$stmt = $this->db->prepare("SELECT id, title, frequency, custom_frequency, reward_points, created_at FROM habits WHERE user_id = ?");
|
||||
$stmt->bind_param("i", $userId);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
$habits = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$habits[] = $row;
|
||||
}
|
||||
|
||||
return $habits;
|
||||
}
|
||||
}
|
65
app/models/User.php
Normal file
65
app/models/User.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
require_once '../core/Database.php';
|
||||
|
||||
class User {
|
||||
private $db;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = Database::getInstance()->getConnection();
|
||||
|
||||
if ($this->db) {
|
||||
error_log("Database connection established successfully.");
|
||||
} else {
|
||||
error_log("Failed to connect to the database.");
|
||||
}
|
||||
}
|
||||
|
||||
public function register($username, $email, $password) {
|
||||
// Check if email already exists
|
||||
$stmt = $this->db->prepare("SELECT id FROM users WHERE email = ?");
|
||||
$stmt->bind_param("s", $email);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
$stmt->close();
|
||||
|
||||
if ($result->num_rows > 0) {
|
||||
return "Email is already registered";
|
||||
}
|
||||
|
||||
$hashedPassword = password_hash($password, PASSWORD_BCRYPT);
|
||||
|
||||
$stmt = $this->db->prepare("INSERT INTO users (username, email, password, points, created_at) VALUES (?, ?, ?, 0, NOW())");
|
||||
$stmt->bind_param("sss", $username, $email, $hashedPassword);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
return true;
|
||||
} else {
|
||||
return "Error: " . $stmt->error;
|
||||
}
|
||||
}
|
||||
|
||||
public function login($email, $password) {
|
||||
$hashedPassword = password_hash($password, PASSWORD_BCRYPT);
|
||||
|
||||
$stmt = $this->db->prepare("SELECT id, username, password FROM users WHERE email = ?");
|
||||
$stmt->bind_param("s", $email);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
$stmt->close();
|
||||
|
||||
if ($result->num_rows === 1) {
|
||||
$user = $result->fetch_assoc();
|
||||
if (password_verify($password, $user['password'])) {
|
||||
$_SESSION['user'] = [
|
||||
'id' => $user['id'],
|
||||
'username' => $user['username'],
|
||||
'email' => $email,
|
||||
];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return "Incorrect username or password.";
|
||||
}
|
||||
}
|
33
app/views/auth/signin.php
Normal file
33
app/views/auth/signin.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<link rel="stylesheet" href="/css/login.css">
|
||||
<link rel="stylesheet" href="/css/form.css">
|
||||
<section class="form signin">
|
||||
<div class="header-form">
|
||||
<img src="/img/logo.jpg" alt="Habit Tracker Logo">
|
||||
<h1>Sign in to Habit Tracker</h1>
|
||||
</div>
|
||||
|
||||
<?php if ($this->get('error')): ?>
|
||||
<div class="error"><?= $this->get('error') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form action="/auth/signin" method="POST">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" name="email" id="email" value="<?= htmlspecialchars($_POST['email'] ?? '') ?>" required>
|
||||
<?php if (isset($this->get('validationErrors')['email'])): ?>
|
||||
<small class="error"><?= $this->get('validationErrors')['email'] ?></small>
|
||||
<?php endif; ?>
|
||||
|
||||
<label for="password">Password</label>
|
||||
<input type="password" name="password" id="password" required>
|
||||
<?php if (isset($this->get('validationErrors')['password'])): ?>
|
||||
<small class="error"><?= $this->get('validationErrors')['password'] ?></small>
|
||||
<?php endif; ?>
|
||||
|
||||
<input type="submit" value="Sign In">
|
||||
</form>
|
||||
|
||||
<div class="bordered">
|
||||
<p>New to Habit Tracker?</p>
|
||||
<a href="/auth/signup">Create an account</a>
|
||||
</div>
|
||||
</section>
|
45
app/views/auth/signup.php
Normal file
45
app/views/auth/signup.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<link rel="stylesheet" href="/css/login.css">
|
||||
<link rel="stylesheet" href="/css/form.css">
|
||||
<section class="form signup">
|
||||
<div class="header-form">
|
||||
<img src="/img/logo.jpg" alt="Habit Tracker Logo">
|
||||
<h1>Sign up to Habit Tracker</h1>
|
||||
</div>
|
||||
|
||||
<?php if ($this->get('error')): ?>
|
||||
<div class="error"><?= $this->get('error') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form action="/auth/signup" method="POST">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" name="username" id="username" value="<?= htmlspecialchars($_POST['username'] ?? '') ?>" required>
|
||||
<?php if (isset($this->get('validationErrors')['username'])): ?>
|
||||
<small class="error"><?= $this->get('validationErrors')['username'] ?></small>
|
||||
<?php endif; ?>
|
||||
|
||||
<label for="email">Email</label>
|
||||
<input type="email" name="email" id="email" value="<?= htmlspecialchars($_POST['email'] ?? '') ?>" required>
|
||||
<?php if (isset($this->get('validationErrors')['email'])): ?>
|
||||
<small class="error"><?= $this->get('validationErrors')['email'] ?></small>
|
||||
<?php endif; ?>
|
||||
|
||||
<label for="password">Password</label>
|
||||
<input type="password" name="password" id="password" required>
|
||||
<?php if (isset($this->get('validationErrors')['password'])): ?>
|
||||
<small class="error"><?= $this->get('validationErrors')['password'] ?></small>
|
||||
<?php endif; ?>
|
||||
|
||||
<label for="password-2">Password again</label>
|
||||
<input type="password" name="password-2" id="password-2" required>
|
||||
<?php if (isset($this->get('validationErrors')['password_confirmation'])): ?>
|
||||
<small class="error"><?= $this->get('validationErrors')['password_confirmation'] ?></small>
|
||||
<?php endif; ?>
|
||||
|
||||
<input type="submit" value="Sign Up">
|
||||
</form>
|
||||
|
||||
<div class="bordered">
|
||||
<p>Already have an account?</p>
|
||||
<a href="/auth/signin">Sign in</a>
|
||||
</div>
|
||||
</section>
|
47
app/views/dashboard/index.php
Normal file
47
app/views/dashboard/index.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<link rel="stylesheet" href="/css/dashboard.css">
|
||||
|
||||
<section class="dashboard">
|
||||
<h1>Welcome, <?= htmlspecialchars($_SESSION['user']['username']) ?>!</h1>
|
||||
<div>
|
||||
<a href="/habits/create" class="btn-green">Create new habit!</a>
|
||||
<a href="/habits" class="btn-primary">List all habits</a>
|
||||
</div>
|
||||
<div class="card-wrapper">
|
||||
<section class="card upcoming">
|
||||
<h2>Upcoming</h2>
|
||||
<div class="habit">
|
||||
<b>Habit Title</b>
|
||||
<p>Frequency</p>
|
||||
<p>Reward points</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card recent">
|
||||
<h2>Recent</h2>
|
||||
<div class="habit">
|
||||
<b>Habit Title</b>
|
||||
<p>Frequency</p>
|
||||
<p>Reward points</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card missed">
|
||||
<h2>Missed</h2>
|
||||
<div class="habit">
|
||||
<b>Habit Title</b>
|
||||
<p>Frequency</p>
|
||||
<p>Reward points</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card history-graph">
|
||||
<h2>Graph of History</h2>
|
||||
</section>
|
||||
|
||||
<section class="card streak">
|
||||
<h2>Streak</h2>
|
||||
<p>You're current streak is 123 days</p>
|
||||
<p>Good job!</p>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
13
app/views/errors/404.php
Normal file
13
app/views/errors/404.php
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Habit Tracker | Error 494</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Error 404 - Page not found</h1>
|
||||
<a href="/">Go back home</a>
|
||||
</body>
|
||||
</html>
|
70
app/views/habits/create.php
Normal file
70
app/views/habits/create.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<link rel="stylesheet" href="/css/form.css">
|
||||
<link rel="stylesheet" href="/css/habits_create.css">
|
||||
<section class="form habit-create">
|
||||
<h1 class="header-form"><?= $this->get('title', 'Create Habit') ?></h1>
|
||||
|
||||
<?php if ($this->get('error')): ?>
|
||||
<div class="error" style="color: red; margin-bottom: 1rem;">
|
||||
<?= htmlspecialchars($this->get('error')) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" action="/habits/create">
|
||||
<label for="name">Habit Name</label>
|
||||
<input type="text" name="name" id="name" required value="<?= htmlspecialchars($_POST['name'] ?? '') ?>">
|
||||
|
||||
<label for="frequency">Frequency</label>
|
||||
<select name="frequency" id="frequency" onchange="toggleCustomFrequency(this.value)">
|
||||
<option value="Daily">Daily</option>
|
||||
<option value="Weekly">Weekly</option>
|
||||
<option value="Custom">Custom</option>
|
||||
</select>
|
||||
|
||||
<div id="custom-frequency" style="display: none;">
|
||||
<label id="lbl_dow">Days of the Week</label>
|
||||
<div class="dow_chb_wrapper">
|
||||
<label for="dow_mon">Monday</label>
|
||||
<input type="checkbox" name="days_of_week[]" id="dow_mon" value="1">
|
||||
</div>
|
||||
<div class="dow_chb_wrapper">
|
||||
<label for="dow_tue">Tuesday</label>
|
||||
<input type="checkbox" name="days_of_week[]" id="dow_tue" value="2">
|
||||
</div>
|
||||
<div class="dow_chb_wrapper">
|
||||
<label for="dow_wed">Wednesday</label>
|
||||
<input type="checkbox" name="days_of_week[]" id="dow_wed" value="3">
|
||||
</div>
|
||||
<div class="dow_chb_wrapper">
|
||||
<label for="dow_thu">Thursday</label>
|
||||
<input type="checkbox" name="days_of_week[]" id="dow_thu" value="4">
|
||||
</div>
|
||||
<div class="dow_chb_wrapper">
|
||||
<label for="dow_fri">Friday</label>
|
||||
<input type="checkbox" name="days_of_week[]" id="dow_fri" value="5">
|
||||
</div>
|
||||
<div class="dow_chb_wrapper">
|
||||
<label for="dow_sat">Saturday</label>
|
||||
<input type="checkbox" name="days_of_week[]" id="dow_sat" value="6">
|
||||
</div>
|
||||
<div class="dow_chb_wrapper">
|
||||
<label for="dow_sun">Sunday</label>
|
||||
<input type="checkbox" name="days_of_week[]" id="dow_sun" value="7">
|
||||
</div>
|
||||
|
||||
<label for="days_of_month" id="lbl_dom">Days of the Month</label>
|
||||
<input type="text" name="days_of_month" id="days_of_month" placeholder="1,15 (comma-separated)">
|
||||
|
||||
<label for="months">Months</label>
|
||||
<input type="text" name="months" id="months" placeholder="1,7,12 (comma-separated)">
|
||||
</div>
|
||||
|
||||
<input type="submit" value="Create Habit">
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
function toggleCustomFrequency(value) {
|
||||
const customFrequencyDiv = document.getElementById('custom-frequency');
|
||||
customFrequencyDiv.style.display = value === 'Custom' ? 'flex' : 'none';
|
||||
}
|
||||
</script>
|
0
app/views/habits/edit.php
Normal file
0
app/views/habits/edit.php
Normal file
24
app/views/habits/index.php
Normal file
24
app/views/habits/index.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<link rel="stylesheet" href="/css/habits_dashboard.css">
|
||||
<section class="habits">
|
||||
<?php if (empty($this->get('habits'))): ?>
|
||||
<p>No habits yet. <a href="/habits/create">Create your first habit</a>.</p>
|
||||
<?php else: ?>
|
||||
<div class="habits-wrapper">
|
||||
<?php foreach ($this->get('habits') as $habit): ?>
|
||||
<div class="habit bordered">
|
||||
<b><?= htmlspecialchars($habit['title']) ?></b>
|
||||
<p>Frequency: <?= htmlspecialchars($habit['frequency']) ?></p>
|
||||
<?php if (isset($habit['custom_frequency'])): ?>
|
||||
<p><?= htmlspecialchars($habit['custom_frequency'] ?? 'N/A') ?></p>
|
||||
<?php endif; ?>
|
||||
<p><?= htmlspecialchars($habit['reward_points']) ?></p>
|
||||
<p><?= htmlspecialchars($habit['created_at']) ?></p>
|
||||
<a href="/habits/done">Mark as done</a> |
|
||||
<a href="/habits/edit?id=<?= $habit['id'] ?>">Edit</a> |
|
||||
<a href="/habits/delete?id=<?= $habit['id'] ?>" onclick="return confirm('Are you sure you want to delete this habit?')">Delete</a>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<a href="/habits/create" class="btn-green">Create new habit!</a>
|
||||
<?php endif; ?>
|
||||
</section>
|
4
app/views/home/index.php
Normal file
4
app/views/home/index.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<div>
|
||||
<h1>Welcome to Habit Tracker!</h1>
|
||||
<p>Track your habits and achieve your goals.</p>
|
||||
</div>
|
35
app/views/layouts/base.php
Normal file
35
app/views/layouts/base.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="author" content="Filip Rojek | http://filiprojek.cz">
|
||||
<meta name="email" content="webmaster(@)fofrweb.com">
|
||||
<meta name="copyright" content="(c) filiprojek.cz">
|
||||
<title>Habit Tracker | <?= $data['title'] ?></title>
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/global.css">
|
||||
<link rel="stylesheet" href="/css/vars.css">
|
||||
<link rel="stylesheet" href="/css/header.css">
|
||||
<link rel="icon" type="image/x-icon" href="/img/favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div id="hd-left">
|
||||
<a href="/"><img src="/img/logo.jpg" alt="home"></a>
|
||||
<label><?= $data['title'] ?></label>
|
||||
</div>
|
||||
<div id="hd-right">
|
||||
<?php if (!isset($_SESSION['user'])): ?>
|
||||
<a href="/auth/signin">Log In</a>
|
||||
<a href="/auth/signup">Sign Up</a>
|
||||
<?php else: ?>
|
||||
<a href="/auth/logout" class="btn-secondary">Sign out</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</header>
|
||||
<main class="content">
|
||||
<?= $content ?>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
20
app/views/layouts/noheader.php
Normal file
20
app/views/layouts/noheader.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="author" content="Filip Rojek | http://filiprojek.cz">
|
||||
<meta name="email" content="webmaster(@)fofrweb.com">
|
||||
<meta name="copyright" content="(c) filiprojek.cz">
|
||||
<title>Habit Tracker | <?= $data['title'] ?></title>
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/global.css">
|
||||
<link rel="stylesheet" href="/css/vars.css">
|
||||
<link rel="icon" type="image/x-icon" href="/img/favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<main class="content">
|
||||
<?= $content ?>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
0
app/views/shared/footer.php
Normal file
0
app/views/shared/footer.php
Normal file
1
config/.gitignore
vendored
Normal file
1
config/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
environment.php
|
6
config/environment.php.example
Normal file
6
config/environment.php.example
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
define('DB_HOST', '0.0.0.0');
|
||||
define('DB_NAME', 'habit_tracker');
|
||||
define('DB_USER', 'username');
|
||||
define('DB_PASS', 'password');
|
23
core/Controller.php
Normal file
23
core/Controller.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
class Controller {
|
||||
/**
|
||||
* Redirect to a given URL
|
||||
*
|
||||
* @param string $url
|
||||
*/
|
||||
public function redirect($url) {
|
||||
header("Location: $url");
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a view
|
||||
*
|
||||
* @param string $viewName
|
||||
* @param array $data
|
||||
*/
|
||||
public function view($viewName, $data = [], $layout = "base") {
|
||||
$view = new View();
|
||||
$view->render($viewName, $data, $layout);
|
||||
}
|
||||
}
|
116
core/Database.php
Normal file
116
core/Database.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
class Database {
|
||||
private static $instance = null;
|
||||
private $connection;
|
||||
|
||||
private function __construct() {
|
||||
$this->initializeConnection();
|
||||
$this->checkAndCreateDatabase();
|
||||
$this->checkAndCreateTables();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton instance of the Database
|
||||
* @return Database
|
||||
*/
|
||||
public static function getInstance() {
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new Database();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active database connection
|
||||
* @return mysqli
|
||||
*/
|
||||
public function getConnection() {
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the connection to the database
|
||||
*/
|
||||
private function initializeConnection() {
|
||||
$this->connection = new mysqli(DB_HOST, DB_USER, DB_PASS);
|
||||
|
||||
if ($this->connection->connect_error) {
|
||||
die("Database connection failed: " . $this->connection->connect_error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and create the database if it doesn't exist
|
||||
*/
|
||||
private function checkAndCreateDatabase() {
|
||||
$query = "CREATE DATABASE IF NOT EXISTS `" . DB_NAME . "`";
|
||||
if (!$this->connection->query($query)) {
|
||||
die("Failed to create or access database: " . $this->connection->error);
|
||||
}
|
||||
|
||||
$this->connection->select_db(DB_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and create required tables if they don't exist
|
||||
*/
|
||||
private function checkAndCreateTables() {
|
||||
// Create users table
|
||||
$usersTableQuery = "CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL,
|
||||
email VARCHAR(100) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
points INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;";
|
||||
|
||||
if (!$this->connection->query($usersTableQuery)) {
|
||||
die("Failed to create users table: " . $this->connection->error);
|
||||
}
|
||||
|
||||
// Create progress table
|
||||
$progressTableQuery = "CREATE TABLE IF NOT EXISTS progress (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
habit_id INT NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
status ENUM('Done', 'Pending') DEFAULT 'Pending',
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
) ENGINE=InnoDB;";
|
||||
|
||||
if (!$this->connection->query($progressTableQuery)) {
|
||||
die("Failed to create progress table: " . $this->connection->error);
|
||||
}
|
||||
|
||||
|
||||
// Create habits table
|
||||
$habitsTableQuery = "CREATE TABLE IF NOT EXISTS habits (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
title VARCHAR(100) NOT NULL,
|
||||
frequency ENUM('Daily', 'Weekly', 'Custom') NOT NULL,
|
||||
custom_frequency VARCHAR(255) DEFAULT NULL, -- Store crontab-like string
|
||||
reward_points INT DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;";
|
||||
|
||||
if (!$this->connection->query($habitsTableQuery)) {
|
||||
die("Failed to create habits table: " . $this->connection->error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent cloning of the singleton instance
|
||||
*/
|
||||
private function __clone() {}
|
||||
|
||||
/**
|
||||
* Prevent unserialization of the singleton instance
|
||||
*/
|
||||
public function __wakeup() {}
|
||||
|
||||
}
|
84
core/Router.php
Normal file
84
core/Router.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
class Router {
|
||||
private $routes = [];
|
||||
private $middlewares = [];
|
||||
private $groupPrefix = '';
|
||||
private $groupMiddlewares = [];
|
||||
|
||||
/**
|
||||
* Add a route with a specific action and optional middleware
|
||||
*
|
||||
* @param string $route
|
||||
* @param string $action
|
||||
* @param array $middlewares Optional middlewares for this route
|
||||
*/
|
||||
public function add($route, $action, $middlewares = []) {
|
||||
$route = $this->groupPrefix . $route;
|
||||
$middlewares = array_merge($this->groupMiddlewares, $middlewares);
|
||||
$this->routes[$route] = ['action' => $action, 'middlewares' => $middlewares];
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a group of routes with shared prefix and middlewares
|
||||
*
|
||||
* @param string $prefix
|
||||
* @param array $middlewares
|
||||
* @param callable $callback
|
||||
*/
|
||||
public function group($prefix, $middlewares, $callback) {
|
||||
// Save the current state
|
||||
$previousPrefix = $this->groupPrefix;
|
||||
$previousMiddlewares = $this->groupMiddlewares;
|
||||
|
||||
// Set new group prefix and middlewares
|
||||
$this->groupPrefix = $previousPrefix . $prefix;
|
||||
$this->groupMiddlewares = array_merge($this->groupMiddlewares, $middlewares);
|
||||
|
||||
// Execute the callback to define routes in the group
|
||||
$callback($this);
|
||||
|
||||
// Restore the previous state
|
||||
$this->groupPrefix = $previousPrefix;
|
||||
$this->groupMiddlewares = $previousMiddlewares;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the current request to the correct route and execute middlewares
|
||||
*/
|
||||
public function dispatch() {
|
||||
$uri = $_SERVER['REQUEST_URI'];
|
||||
$uri = parse_url($uri, PHP_URL_PATH);
|
||||
|
||||
// Normalize the URI by removing trailing slash (except for root "/")
|
||||
if ($uri !== '/' && substr($uri, -1) === '/') {
|
||||
$uri = rtrim($uri, '/');
|
||||
}
|
||||
|
||||
if (array_key_exists($uri, $this->routes)) {
|
||||
$route = $this->routes[$uri];
|
||||
$middlewares = $route['middlewares'];
|
||||
|
||||
// Execute middlewares
|
||||
foreach ($middlewares as $middleware) {
|
||||
$middlewareInstance = new $middleware();
|
||||
if (!$middlewareInstance->handle()) {
|
||||
return; // Stop execution if middleware fails
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the route's controller and method
|
||||
$action = $route['action'];
|
||||
list($controllerName, $methodName) = explode('@', $action);
|
||||
|
||||
require_once controllers . "{$controllerName}.php";
|
||||
|
||||
$controller = new $controllerName();
|
||||
$controller->$methodName();
|
||||
} else {
|
||||
http_response_code(404);
|
||||
$view = new View();
|
||||
$view->render('errors/404', [], 'noheader');
|
||||
}
|
||||
}
|
||||
}
|
55
core/Validator.php
Normal file
55
core/Validator.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
class Validator {
|
||||
private $errors = [];
|
||||
|
||||
/**
|
||||
* Check if a field is not empty
|
||||
*/
|
||||
public function required($field, $value, $message = null) {
|
||||
if (empty(trim($value))) {
|
||||
$this->errors[$field] = $message ?? "$field is required.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a field meets minimum length
|
||||
*/
|
||||
public function minLength($field, $value, $length, $message = null) {
|
||||
if (strlen($value) < $length) {
|
||||
$this->errors[$field] = $message ?? "$field must be at least $length characters.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a field contains letters and numbers
|
||||
*/
|
||||
public function alphanumeric($field, $value, $message = null) {
|
||||
if (!preg_match('/^(?=.*[a-zA-Z])(?=.*\d).+$/', $value)) {
|
||||
$this->errors[$field] = $message ?? "$field must contain letters and numbers.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an email
|
||||
*/
|
||||
public function email($field, $value, $message = null) {
|
||||
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->errors[$field] = $message ?? "Invalid email format.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation errors
|
||||
*/
|
||||
public function errors() {
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if validation passed
|
||||
*/
|
||||
public function passes() {
|
||||
return empty($this->errors);
|
||||
}
|
||||
}
|
25
core/View.php
Normal file
25
core/View.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
class View
|
||||
{
|
||||
private $data = [];
|
||||
|
||||
public function render($view, $data = [], $layout = 'base') {
|
||||
// Store the data
|
||||
$this->data = $data;
|
||||
|
||||
// Capture the view content
|
||||
ob_start();
|
||||
require_once views . $view . '.php';
|
||||
$content = ob_get_clean();
|
||||
|
||||
// Include the base layout and inject the view content
|
||||
require_once views . "layouts/$layout.php";
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely get a value from the data array
|
||||
*/
|
||||
public function get($key) {
|
||||
return $this->data[$key] ?? null;
|
||||
}
|
||||
}
|
12
core/middlewares/RequireAuth.php
Normal file
12
core/middlewares/RequireAuth.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
class RequireAuth {
|
||||
public function handle() {
|
||||
if (!isset($_SESSION['user'])) {
|
||||
header('Location: /auth/signin');
|
||||
exit();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
31
docker-compose.yaml
Normal file
31
docker-compose.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
services:
|
||||
mariadb:
|
||||
image: mariadb:11.4 # LTS at 25. 12. 2025
|
||||
restart: on-failure:2
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: root
|
||||
ports:
|
||||
- 3306:3306
|
||||
profiles: ["prod", "dev"]
|
||||
|
||||
phpmyadmin:
|
||||
image: phpmyadmin
|
||||
restart: on-failure:2
|
||||
ports:
|
||||
- 8080:80
|
||||
environment:
|
||||
- PMA_ARBITRARY=1
|
||||
profiles: ["dev"]
|
||||
|
||||
habittracker:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
volumes:
|
||||
- .:/var/www/html
|
||||
ports:
|
||||
- 8000:80
|
||||
depends_on:
|
||||
- mariadb
|
||||
restart: on-failure:2
|
||||
profiles: ["prod"]
|
21
public/css/dashboard.css
Normal file
21
public/css/dashboard.css
Normal file
@@ -0,0 +1,21 @@
|
||||
.dashboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--clr-secondary);
|
||||
border-radius: var(--border-radious);
|
||||
border: var(--borderWidth-thin) solid var(--clr-border);
|
||||
min-width: 17rem;
|
||||
padding: 1rem;
|
||||
}
|
78
public/css/form.css
Normal file
78
public/css/form.css
Normal file
@@ -0,0 +1,78 @@
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--clr-secondary);
|
||||
}
|
||||
|
||||
.form .header-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form .header-form img {
|
||||
height: 5rem;
|
||||
}
|
||||
|
||||
.form form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
background-color: var(--clr-tertiary);
|
||||
gap: .5rem;
|
||||
border-radius: var(--border-radious);
|
||||
border: var(--borderWidth-thin) solid var(--clr-border);
|
||||
}
|
||||
|
||||
.form form input,
|
||||
select {
|
||||
background-color: var(--clr-secondary);
|
||||
caret-color: white;
|
||||
color: white;
|
||||
padding: .3rem;
|
||||
border-radius: var(--border-radious);
|
||||
border: var(--borderWidth-thin) solid var(--clr-border);
|
||||
width: 15rem;
|
||||
}
|
||||
|
||||
.form form input[type="submit"] {
|
||||
background-color: var(--clr-green);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form .error {
|
||||
width: 17rem;
|
||||
padding: 1rem;
|
||||
background-color: var(--clr-danger-muted);
|
||||
border-radius: var(--border-radious);
|
||||
border: var(--borderWidth-thin) solid var(--clr-border-danger);
|
||||
margin-bottom: 1rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form small.error {
|
||||
width: 15rem;
|
||||
}
|
||||
|
||||
.form .bordered {
|
||||
border-radius: var(--border-radious);
|
||||
border: var(--borderWidth-thin) solid var(--clr-border);
|
||||
width: 17rem;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form .bordered a {
|
||||
text-decoration: none;
|
||||
color: var(--clr-link-blue);
|
||||
}
|
||||
|
||||
.form .bordered a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
47
public/css/global.css
Normal file
47
public/css/global.css
Normal file
@@ -0,0 +1,47 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap");
|
||||
|
||||
body {
|
||||
font-family: "Open Sans", serif;
|
||||
background-color: var(--clr-primary);
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: .5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary,
|
||||
.btn-tertiary,
|
||||
.btn-green,
|
||||
.btn-danger {
|
||||
background-color: var(--clr-primary);
|
||||
padding: .5rem;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
border-radius: var(--border-radious);
|
||||
border: var(--borderWidth-thin) solid var(--clr-border);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--clr-secondary);
|
||||
}
|
||||
|
||||
.btn-tertiary {
|
||||
background-color: var(--clr-tertiary);
|
||||
}
|
||||
|
||||
.btn-green {
|
||||
background-color: var(--clr-green);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: var(--clr-danger-muted);
|
||||
border: var(--borderWidth-thin) solid var(--clr-border-danger);
|
||||
}
|
21
public/css/habits_create.css
Normal file
21
public/css/habits_create.css
Normal file
@@ -0,0 +1,21 @@
|
||||
.form form .dow_chb_wrapper input[type="checkbox"] {
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.form form .dow_chb_wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#lbl_dow {
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
#lbl_dom {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
#custom-frequency {
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
15
public/css/habits_dashboard.css
Normal file
15
public/css/habits_dashboard.css
Normal file
@@ -0,0 +1,15 @@
|
||||
.habits-wrapper {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.habits .bordered {
|
||||
border-radius: var(--border-radious);
|
||||
border: var(--borderWidth-thin) solid var(--clr-border);
|
||||
width: 17rem;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
}
|
26
public/css/header.css
Normal file
26
public/css/header.css
Normal file
@@ -0,0 +1,26 @@
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 2rem;
|
||||
width: 100vw;
|
||||
height: 3rem;
|
||||
background: var(--clr-secondary);
|
||||
border-radius: var(--border-radious);
|
||||
border-bottom: var(--borderWidth-thin) solid var(--clr-border);
|
||||
}
|
||||
|
||||
#hd-left,
|
||||
#hd-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
header a img {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
header a {
|
||||
text-decoration: none;
|
||||
}
|
13
public/css/main.css
Normal file
13
public/css/main.css
Normal file
@@ -0,0 +1,13 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
padding: 0 var(--container-size);
|
||||
}
|
20
public/css/vars.css
Normal file
20
public/css/vars.css
Normal file
@@ -0,0 +1,20 @@
|
||||
:root {
|
||||
--container-size: 5vw;
|
||||
--clr-primary: #010409;
|
||||
--clr-secondary: #0d1117;
|
||||
--clr-tertiary: #151b23;
|
||||
--clr-green: #238636;
|
||||
--clr-danger-muted: #f851491a;
|
||||
|
||||
--clr-link-blue: #4493f8;
|
||||
--clr-light-blue: #39a2ae;
|
||||
--clr-light-green: #71f79f;
|
||||
--clr-red: #9b1d20;
|
||||
--clr-orange: #e08e45;
|
||||
--clr-gray-blue: #627c85;
|
||||
|
||||
--border-radious: 5px;
|
||||
--borderWidth-thin: max(1px, 0.0625rem);
|
||||
--clr-border: #3d444db3;
|
||||
--clr-border-danger: #f8514966;
|
||||
}
|
BIN
public/img/favicon.ico
Normal file
BIN
public/img/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
public/img/logo.jpg
Normal file
BIN
public/img/logo.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
51
public/index.php
Normal file
51
public/index.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
// Show errors and warnings
|
||||
session_start();
|
||||
ini_set('display_errors', '1');
|
||||
ini_set('log_errors', '1');
|
||||
ini_set('error_log', '../storage/logs/error_log.log');
|
||||
|
||||
define('models', __DIR__ . '/../app/models/');
|
||||
define('views', __DIR__ . '/../app/views/');
|
||||
define('controllers', __DIR__ . '/../app/controllers/');
|
||||
define('config', __DIR__ . '/../config/');
|
||||
|
||||
require_once config . 'environment.php';
|
||||
|
||||
require_once '../core/Validator.php';
|
||||
require_once '../core/Router.php';
|
||||
require_once '../core/View.php';
|
||||
require_once '../core/Controller.php';
|
||||
require_once '../core/Database.php';
|
||||
require_once '../core/middlewares/RequireAuth.php';
|
||||
|
||||
require_once models . 'User.php';
|
||||
require_once models . 'Habit.php';
|
||||
|
||||
// Initialize router
|
||||
$router = new Router();
|
||||
if(!isset($_SESSION['user'])) {
|
||||
$router->add('/', 'HomeController@index');
|
||||
} else {
|
||||
$router->add('/', 'DashboardController@reroute', ['RequireAuth']);
|
||||
}
|
||||
|
||||
// auth routes
|
||||
$router->group('/auth', [], function ($router) {
|
||||
$router->add('/signin', 'AuthController@signin');
|
||||
$router->add('/signup', 'AuthController@signup');
|
||||
$router->add('/logout', 'AuthController@logout');
|
||||
});
|
||||
|
||||
// dashboard route
|
||||
$router->add('/dashboard', 'DashboardController@index', ['RequireAuth']);
|
||||
|
||||
// habits routes
|
||||
$router->group('/habits', ['RequireAuth'], function ($router) {
|
||||
$router->add('', 'HabitController@index');
|
||||
$router->add('/create', 'HabitController@create');
|
||||
$router->add('/edit/{id}', 'HabitController@edit');
|
||||
$router->add('/delete/{id}', 'HabitController@delete');
|
||||
});
|
||||
|
||||
$router->dispatch();
|
1
storage/logs/.gitignore
vendored
Normal file
1
storage/logs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.log
|
Reference in New Issue
Block a user