33 changed files with 607 additions and 47 deletions
+53
View 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
View 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
View File
@@ -0,0 +1 @@
*.swp
+18
View 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;"
+13 -1
View File
@@ -8,6 +8,18 @@ An app for tracking habits and motivation to achieve personal goals
- **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
@@ -28,7 +40,7 @@ define('DB_NAME', 'your db name');
3. Start an local web server
- You can use php's integrated server by running this:
```bash
php -S localhost:8000
php -S localhost:8000 -t ./public
```
- You can use any host and any port you want.
+8 -1
View File
@@ -6,6 +6,13 @@
- [ ] edit user data - change password, mail...
## Core of the app
- [ ] Habits list
- [ ] header and navbar
- [ ] dashboard
- [x] css
- [ ] its just plain
- [ ] graphs
- [x] Habits list
- [ ] css
- [ ] Habits create
- [ ] validate cron input
- [ ] Habits track
+5 -5
View File
@@ -25,10 +25,10 @@ class AuthController extends Controller {
if($result === true) {
$this->redirect('/dashboard');
} else {
$this->view('auth/signin', ['error' => $result]);
$this->view('auth/signin', ['error' => $result], 'noheader');
}
} else {
$this->view('auth/signin', ['title' => 'Log In']);
$this->view('auth/signin', ['title' => 'Log In'], 'noheader');
}
}
@@ -54,7 +54,7 @@ class AuthController extends Controller {
$this->view('auth/signup', [
'error' => 'Please correct the errors below.',
'validationErrors' => $validator->errors() ?: [],
]);
], 'noheader');
return;
}
@@ -67,13 +67,13 @@ class AuthController extends Controller {
$this->view('auth/signup', [
'error' => $result,
'validationErrors' => [],
]);
], 'noheader');
}
} else {
$this->view('auth/signup', [
'title' => 'Register',
'validationErrors' => [],
]);
], 'noheader');
}
}
+16
View 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');
}
}
+4 -5
View File
@@ -2,7 +2,9 @@
class HabitController extends Controller {
public function index() {
// Display the list of habits (to be implemented later)
$habit = new Habit();
$habits = $habit->getHabitsByUser($_SESSION['user']['id']);
$this->view('habits/index', ['title' => 'Habits', 'habits' => $habits]);
}
public function create() {
@@ -17,15 +19,12 @@ class HabitController extends Controller {
}
if ($frequency === 'Custom') {
var_dump($_POST);
$daysOfWeek = $_POST['days_of_week'] ?? [];
$daysOfMonth = $_POST['days_of_month'] ?? '*';
$months = $_POST['months'] ?? '*';
// Combine into crontab-like string
$customFrequency = implode(',', $daysOfWeek) . " $daysOfMonth $months";
var_dump($customFrequency);
}
$habit = new Habit();
@@ -38,7 +37,7 @@ class HabitController extends Controller {
]);
if ($result) {
//$this->redirect('/habits');
$this->redirect('/habits');
} else {
$this->view('habits/create', ['error' => 'Failed to create habit.']);
}
+14
View File
@@ -29,4 +29,18 @@ class Habit {
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;
}
}
+1 -1
View File
@@ -60,6 +60,6 @@ class User {
}
}
return "Invalid email or password.";
return "Incorrect username or password.";
}
}
+13 -1
View File
@@ -1,4 +1,11 @@
<section class="signin">
<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; ?>
@@ -18,4 +25,9 @@
<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>
+13 -1
View File
@@ -1,4 +1,11 @@
<section class="signup">
<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; ?>
@@ -30,4 +37,9 @@
<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>
+46 -2
View File
@@ -1,3 +1,47 @@
<h1>Welcome <?= $_SESSION['user']['username']?>!</h1>
<link rel="stylesheet" href="/css/dashboard.css">
<a href="/habits/create">Create new Habit</a>
<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>
+39 -16
View File
@@ -1,5 +1,7 @@
<section class="habit-create">
<h1><?= $this->get('title', 'Create Habit') ?></h1>
<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;">
@@ -8,10 +10,10 @@
<?php endif; ?>
<form method="POST" action="/habits/create">
<label for="name">Habit Name:</label>
<label for="name">Habit Name</label>
<input type="text" name="name" id="name" required value="<?= htmlspecialchars($_POST['name'] ?? '') ?>">
<label for="frequency">Frequency:</label>
<label for="frequency">Frequency</label>
<select name="frequency" id="frequency" onchange="toggleCustomFrequency(this.value)">
<option value="Daily">Daily</option>
<option value="Weekly">Weekly</option>
@@ -19,29 +21,50 @@
</select>
<div id="custom-frequency" style="display: none;">
<label>Days of the Week:</label>
<input type="checkbox" name="days_of_week[]" value="1"> Monday
<input type="checkbox" name="days_of_week[]" value="2"> Tuesday
<input type="checkbox" name="days_of_week[]" value="3"> Wednesday
<input type="checkbox" name="days_of_week[]" value="4"> Thursday
<input type="checkbox" name="days_of_week[]" value="5"> Friday
<input type="checkbox" name="days_of_week[]" value="6"> Saturday
<input type="checkbox" name="days_of_week[]" value="7"> Sunday
<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">Days of the Month:</label>
<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>
<label for="months">Months</label>
<input type="text" name="months" id="months" placeholder="1,7,12 (comma-separated)">
</div>
<button type="submit">Create Habit</button>
<input type="submit" value="Create Habit">
</form>
</section>
<script>
function toggleCustomFrequency(value) {
const customFrequencyDiv = document.getElementById('custom-frequency');
customFrequencyDiv.style.display = value === 'Custom' ? 'block' : 'none';
customFrequencyDiv.style.display = value === 'Custom' ? 'flex' : 'none';
}
</script>
+24
View 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>
+22 -4
View File
@@ -1,17 +1,35 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<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>
<a href="/auth/logout">Log Out</a>
<?php else: ?>
<a href="/auth/logout" class="btn-secondary">Sign out</a>
<?php endif; ?>
</div>
</header>
<section class="content">
<main class="content">
<?= $content ?>
</section>
</main>
</body>
</html>
+20
View 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>
+6
View 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');
+2 -2
View File
@@ -16,8 +16,8 @@ class Controller {
* @param string $viewName
* @param array $data
*/
public function view($viewName, $data = []) {
public function view($viewName, $data = [], $layout = "base") {
$view = new View();
$view->render($viewName, $data);
$view->render($viewName, $data, $layout);
}
}
+1 -1
View File
@@ -78,7 +78,7 @@ class Router {
} else {
http_response_code(404);
$view = new View();
$view->render('errors/404');
$view->render('errors/404', [], 'noheader');
}
}
}
+17 -2
View File
@@ -1,16 +1,31 @@
services:
mariadb:
image: mariadb:11.4 # LTS at 25. 12. 2025
restart: always
restart: on-failure:2
environment:
MARIADB_ROOT_PASSWORD: root
ports:
- 3306:3306
profiles: ["prod", "dev"]
phpmyadmin:
image: phpmyadmin
restart: always
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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

+6 -3
View File
@@ -24,8 +24,11 @@ require_once models . 'Habit.php';
// Initialize router
$router = new Router();
$router->add('/', 'HomeController@index');
$router->add('/home', 'HomeController@home');
if(!isset($_SESSION['user'])) {
$router->add('/', 'HomeController@index');
} else {
$router->add('/', 'DashboardController@reroute', ['RequireAuth']);
}
// auth routes
$router->group('/auth', [], function ($router) {
@@ -35,7 +38,7 @@ $router->group('/auth', [], function ($router) {
});
// dashboard route
$router->add('/dashboard', 'HomeController@dashboard', ['RequireAuth']);
$router->add('/dashboard', 'DashboardController@index', ['RequireAuth']);
// habits routes
$router->group('/habits', ['RequireAuth'], function ($router) {