9 Commits

20 changed files with 185 additions and 90 deletions

View File

@@ -40,7 +40,7 @@ define('DB_NAME', 'your db name');
3. Start an local web server 3. Start an local web server
- You can use php's integrated server by running this: - You can use php's integrated server by running this:
```bash ```bash
php -S localhost:8000 php -S localhost:8000 -t ./public
``` ```
- You can use any host and any port you want. - You can use any host and any port you want.

View File

@@ -25,10 +25,10 @@ class AuthController extends Controller {
if($result === true) { if($result === true) {
$this->redirect('/dashboard'); $this->redirect('/dashboard');
} else { } else {
$this->view('auth/signin', ['error' => $result]); $this->view('auth/signin', ['error' => $result], 'noheader');
} }
} else { } 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', [ $this->view('auth/signup', [
'error' => 'Please correct the errors below.', 'error' => 'Please correct the errors below.',
'validationErrors' => $validator->errors() ?: [], 'validationErrors' => $validator->errors() ?: [],
]); ], 'noheader');
return; return;
} }
@@ -67,13 +67,13 @@ class AuthController extends Controller {
$this->view('auth/signup', [ $this->view('auth/signup', [
'error' => $result, 'error' => $result,
'validationErrors' => [], 'validationErrors' => [],
]); ], 'noheader');
} }
} else { } else {
$this->view('auth/signup', [ $this->view('auth/signup', [
'title' => 'Register', 'title' => 'Register',
'validationErrors' => [], 'validationErrors' => [],
]); ], 'noheader');
} }
} }

View File

@@ -25,4 +25,9 @@
<input type="submit" value="Sign In"> <input type="submit" value="Sign In">
</form> </form>
<div class="bordered">
<p>New to Habit Tracker?</p>
<a href="/auth/signup">Create an account</a>
</div>
</section> </section>

View File

@@ -37,4 +37,9 @@
<input type="submit" value="Sign Up"> <input type="submit" value="Sign Up">
</form> </form>
<div class="bordered">
<p>Already have an account?</p>
<a href="/auth/signin">Sign in</a>
</div>
</section> </section>

View File

@@ -2,7 +2,10 @@
<section class="dashboard"> <section class="dashboard">
<h1>Welcome, <?= htmlspecialchars($_SESSION['user']['username']) ?>!</h1> <h1>Welcome, <?= htmlspecialchars($_SESSION['user']['username']) ?>!</h1>
<a href="/habits/create" class="btn-primary">Create new habit!</a> <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"> <div class="card-wrapper">
<section class="card upcoming"> <section class="card upcoming">
<h2>Upcoming</h2> <h2>Upcoming</h2>

View File

@@ -10,10 +10,10 @@
<?php endif; ?> <?php endif; ?>
<form method="POST" action="/habits/create"> <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'] ?? '') ?>"> <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)"> <select name="frequency" id="frequency" onchange="toggleCustomFrequency(this.value)">
<option value="Daily">Daily</option> <option value="Daily">Daily</option>
<option value="Weekly">Weekly</option> <option value="Weekly">Weekly</option>
@@ -21,7 +21,7 @@
</select> </select>
<div id="custom-frequency" style="display: none;"> <div id="custom-frequency" style="display: none;">
<label id="lbl_dow">Days of the Week:</label> <label id="lbl_dow">Days of the Week</label>
<div class="dow_chb_wrapper"> <div class="dow_chb_wrapper">
<label for="dow_mon">Monday</label> <label for="dow_mon">Monday</label>
<input type="checkbox" name="days_of_week[]" id="dow_mon" value="1"> <input type="checkbox" name="days_of_week[]" id="dow_mon" value="1">
@@ -51,10 +51,10 @@
<input type="checkbox" name="days_of_week[]" id="dow_sun" value="7"> <input type="checkbox" name="days_of_week[]" id="dow_sun" value="7">
</div> </div>
<label for="days_of_month" id="lbl_dom">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)"> <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)"> <input type="text" name="months" id="months" placeholder="1,7,12 (comma-separated)">
</div> </div>

View File

@@ -3,33 +3,22 @@
<?php if (empty($this->get('habits'))): ?> <?php if (empty($this->get('habits'))): ?>
<p>No habits yet. <a href="/habits/create">Create your first habit</a>.</p> <p>No habits yet. <a href="/habits/create">Create your first habit</a>.</p>
<?php else: ?> <?php else: ?>
<table> <div class="habits-wrapper">
<thead> <?php foreach ($this->get('habits') as $habit): ?>
<tr> <div class="habit bordered">
<th>Title</th> <b><?= htmlspecialchars($habit['title']) ?></b>
<th>Frequency</th> <p>Frequency: <?= htmlspecialchars($habit['frequency']) ?></p>
<th>Custom Schedule</th> <?php if (isset($habit['custom_frequency'])): ?>
<th>Points</th> <p><?= htmlspecialchars($habit['custom_frequency'] ?? 'N/A') ?></p>
<th>Created At</th> <?php endif; ?>
<th>Actions</th> <p><?= htmlspecialchars($habit['reward_points']) ?></p>
</tr> <p><?= htmlspecialchars($habit['created_at']) ?></p>
</thead> <a href="/habits/done">Mark as done</a> |
<tbody> <a href="/habits/edit?id=<?= $habit['id'] ?>">Edit</a> |
<?php foreach ($this->get('habits') as $habit): ?> <a href="/habits/delete?id=<?= $habit['id'] ?>" onclick="return confirm('Are you sure you want to delete this habit?')">Delete</a>
<tr> </div>
<td><?= htmlspecialchars($habit['title']) ?></td> <?php endforeach; ?>
<td><?= htmlspecialchars($habit['frequency']) ?></td> </div>
<td><?= htmlspecialchars($habit['custom_frequency'] ?? 'N/A') ?></td> <a href="/habits/create" class="btn-green">Create new habit!</a>
<td><?= htmlspecialchars($habit['reward_points']) ?></td>
<td><?= htmlspecialchars($habit['created_at']) ?></td>
<td>
<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>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<a href="/habits/create">Create new habit!</a>
<?php endif; ?> <?php endif; ?>
</section> </section>

View File

@@ -10,13 +10,23 @@
<link rel="stylesheet" href="/css/main.css"> <link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/global.css"> <link rel="stylesheet" href="/css/global.css">
<link rel="stylesheet" href="/css/vars.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"> <link rel="icon" type="image/x-icon" href="/img/favicon.ico">
</head> </head>
<body> <body>
<header> <header>
<a href="/auth/signin">Log In</a> <div id="hd-left">
<a href="/auth/signup">Sign Up</a> <a href="/"><img src="/img/logo.jpg" alt="home"></a>
<a href="/auth/logout">Log Out</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> </header>
<main class="content"> <main class="content">
<?= $content ?> <?= $content ?>

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>

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');

View File

@@ -16,8 +16,8 @@ class Controller {
* @param string $viewName * @param string $viewName
* @param array $data * @param array $data
*/ */
public function view($viewName, $data = []) { public function view($viewName, $data = [], $layout = "base") {
$view = new View(); $view = new View();
$view->render($viewName, $data); $view->render($viewName, $data, $layout);
} }
} }

View File

@@ -78,7 +78,7 @@ class Router {
} else { } else {
http_response_code(404); http_response_code(404);
$view = new View(); $view = new View();
$view->render('errors/404'); $view->render('errors/404', [], 'noheader');
} }
} }
} }

View File

@@ -1,27 +1,31 @@
services: services:
mariadb: mariadb:
image: mariadb:11.4 # LTS at 25. 12. 2025 image: mariadb:11.4 # LTS at 25. 12. 2025
restart: always restart: on-failure:2
environment: environment:
MARIADB_ROOT_PASSWORD: root MARIADB_ROOT_PASSWORD: root
ports: ports:
- 3306:3306 - 3306:3306
profiles: ["prod", "dev"]
phpmyadmin: phpmyadmin:
image: phpmyadmin image: phpmyadmin
restart: always restart: on-failure:2
ports: ports:
- 8080:80 - 8080:80
environment: environment:
- PMA_ARBITRARY=1 - PMA_ARBITRARY=1
profiles: ["dev"]
habittracker: habittracker:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
#volumes: volumes:
# - .:/var/www/html - .:/var/www/html
ports: ports:
- 8000:80 - 8000:80
depends_on: depends_on:
- mariadb - mariadb
restart: on-failure:2
profiles: ["prod"]

View File

@@ -1,7 +1,6 @@
.dashboard { .dashboard {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100vw;
align-items: center; align-items: center;
} }

View File

@@ -3,7 +3,6 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100vw;
} }
body { body {
@@ -59,3 +58,21 @@ select {
.form small.error { .form small.error {
width: 15rem; 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;
}

View File

@@ -1,32 +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"); @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 { body {
font-family: "Open Sans", serif; font-family: "Open Sans", serif;
background-color: var(--clr-primary); background-color: var(--clr-primary);
color: white; color: white;
font-size: 14px; font-size: 14px;
} }
a { a {
color: white; color: white;
} }
h1 { h1 {
margin-top: .5rem; margin-top: .5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.btn-primary, .btn-primary,
.btn-secondary,
.btn-tertiary,
.btn-green,
.btn-danger { .btn-danger {
background-color: var(--clr-green); background-color: var(--clr-primary);
padding: .5rem; padding: .5rem;
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
border-radius: var(--border-radious); border-radius: var(--border-radious);
border: var(--borderWidth-thin) solid var(--clr-border); 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 { .btn-danger {
background-color: var(--clr-danger-muted) !important; background-color: var(--clr-danger-muted);
border: var(--borderWidth-thin) solid var(--clr-border-danger); border: var(--borderWidth-thin) solid var(--clr-border-danger);
} }

View File

@@ -1,26 +1,15 @@
.habits h1 { .habits-wrapper {
font-size: 2rem; display: flex;
margin-bottom: 1rem; gap: 1rem;
flex-wrap: wrap;
justify-content: center;
} }
.habits table { .habits .bordered {
width: 100%; border-radius: var(--border-radious);
border-collapse: collapse; border: var(--borderWidth-thin) solid var(--clr-border);
width: 17rem;
padding: 1rem;
margin-top: 1rem; margin-top: 1rem;
} text-align: center;
.habits table th,
.habits table td {
border: 1px solid #ccc;
padding: 8px;
text-align: left;
}
.habits a {
color: #007bff;
text-decoration: none;
}
.habits a:hover {
text-decoration: underline;
} }

26
public/css/header.css Normal file
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;
}

View File

@@ -6,7 +6,7 @@
main { main {
display: flex; display: flex;
gap: 2rem; flex-direction: column;
margin-top: 2rem; margin-top: 2rem;
margin-bottom: 2rem; margin-bottom: 2rem;
padding: 0 var(--container-size); padding: 0 var(--container-size);

View File

@@ -6,6 +6,13 @@
--clr-green: #238636; --clr-green: #238636;
--clr-danger-muted: #f851491a; --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; --border-radious: 5px;
--borderWidth-thin: max(1px, 0.0625rem); --borderWidth-thin: max(1px, 0.0625rem);
--clr-border: #3d444db3; --clr-border: #3d444db3;