37 Commits

Author SHA1 Message Date
18c78e37a4 In progress: offline fuel record creation 2025-01-26 17:21:30 +01:00
ccbb0eac64 Added: Stats in dashboard
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 12s
2025-01-05 21:24:55 +01:00
21c2f4598b Added: Create new refuel record
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 13s
2025-01-05 19:05:30 +01:00
860a20d946 Added: Fuel record create - not complete yet
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 12s
2025-01-03 17:10:08 +01:00
c5955010cb Edited: vehicle list styled
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 13s
2025-01-03 16:16:15 +01:00
fc163431f8 Added: Gitea Workflow
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 12s
2025-01-03 00:45:54 +01:00
15029970d6 Fix: wrong action url in vehicle create 2025-01-03 00:18:29 +01:00
e13edeccfc Edited: vehicle create route renamed from vehicle add 2025-01-02 02:02:14 +01:00
be6b465684 Added: vehicle creation 2024-12-31 15:53:15 +01:00
aded859a79 Added: tables for fuel-stats 2024-12-31 14:21:41 +01:00
c29bd7cbab Edited: App is rebranded to Fuel Stats 2024-12-31 11:34:39 +01:00
eff5be49c4 Edited: docker-compose.yaml 2024-12-31 10:46:32 +01:00
d13c490efb Added: Habits dashboard 2024-12-30 14:35:16 +01:00
d98c208df9 Edited: README.md - start local server in correct folder 2024-12-28 22:27:24 +01:00
85af85b1ee Edited: Dashboard links and global button styles, added new color variables 2024-12-27 17:07:09 +01:00
1ff7fc454f Edited: small changes in header 2024-12-27 15:26:08 +01:00
e0dfc7120f Edited: Removed colons from labels in habit creation form 2024-12-27 15:12:54 +01:00
9c90710bf3 Added: No-header layout, signup/signin link enhancements, and header design updates 2024-12-27 15:05:55 +01:00
4b8ee90d8a Added: environment.php.example 2024-12-27 14:18:38 +01:00
59246decd7 Edited: docker-compose - added profiles for dev and prod 2024-12-27 13:38:50 +01:00
147e3b1499 Edited: meta tags in default layout 2024-12-27 02:52:58 +01:00
daec4ec1c9 Edited: README.md 2024-12-27 02:36:34 +01:00
3c6ecfb5e2 Added: .gitignore 2024-12-27 02:35:40 +01:00
b4e08f28ca Edited: README.md 2024-12-27 02:35:01 +01:00
c4366edb29 Added: Dockerfile and docker support 2024-12-27 02:29:07 +01:00
d9f632da26 Fix: check if the session is set before using it 2024-12-27 02:28:03 +01:00
2847231376 Added: favicon, Dashboard, Habits list, some styles, dashboard redirect 2024-12-27 02:06:32 +01:00
d33d233f0f Edited: Login and Signup pages are now styled, Added: App logo 2024-12-26 23:25:34 +01:00
43960ddcb9 Added: Habit creation logic 2024-12-26 18:47:00 +01:00
85209ff134 Added: Router route groups 2024-12-26 17:47:42 +01:00
4c44dac115 Auth logic is completed (signin, signup, logout), Added: Middlewares, RequireAuth middleware 2024-12-26 14:44:40 +01:00
01b057986e README.md translated from czech to english 2024-12-26 02:13:03 +01:00
ecb2a6b74b Added: TODO.md 2024-12-26 02:03:18 +01:00
5bca9b12aa Added: Signin Up, connection to the DB, validator, base controller, base view, environment file, User model 2024-12-26 02:00:33 +01:00
d7b8aba072 Added: docker-compose for mariadb and phpmyadmin 2024-12-25 23:37:07 +01:00
3144078860 Added: MVC structure, Router, Views and Controllers for home page and login/signup 2024-12-25 23:02:09 +01:00
7b4b349816 Edited: README.md 2024-12-25 23:00:55 +01:00
50 changed files with 1886 additions and 5 deletions

53
.docker/nginx.conf Normal file
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
.dockerignore Normal file
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

View File

@@ -0,0 +1,36 @@
name: Build and Deploy Zola Website
on:
push:
branches:
- master
env:
HOST: ${{ secrets.SERVER_IP }}
SSH_USERNAME: ${{ secrets.USERNAME }}
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_KEY }}
DEST_FOLDER: "/srv/www/cz/filiprojek/fuelstats"
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Deploy
run: |
apt update -y && apt-get install -y --no-install-recommends rsync
eval "$(ssh-agent -s)"
ssh-add - <<< "${SSH_PRIVATE_KEY}"
mkdir -p ~/.ssh/
ssh-keyscan -H ${HOST} >> ~/.ssh/known_hosts
rsync -r --delete-after ./* "${SSH_USERNAME}@${HOST}:${{ env.DEST_FOLDER }}"
- name: Copy environment.php
run: |
eval "$(ssh-agent -s)"
ssh-add - <<< "${SSH_PRIVATE_KEY}"
mkdir -p ~/.ssh/
ssh-keyscan -H ${HOST} >> ~/.ssh/known_hosts
ssh ${SSH_USERNAME}@${HOST} "cp /var/websrvenv/environment.php /srv/www/cz/filiprojek/fuelstats/config/environment.php"

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.swp

18
Dockerfile Normal file
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;"

View File

@@ -208,8 +208,8 @@ If you develop a new program, and you want it to be of the greatest possible use
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
habit-tracker Fuel Stats
Copyright (C) 2024 fr Copyright (C) 2024 Filip Rojek
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@@ -221,7 +221,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
habit-tracker Copyright (C) 2024 fr Fuel Stats Copyright (C) 2024 Filip Rojek
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.

View File

@@ -1,3 +1,57 @@
# habit-tracker # Fuel Stats
An app for tracking habits and motivation to achieve personal goals. An app for tracking your fuel consumption and optimizing your driving efficiency.
## 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 --profile <dev|prod> up -d
```
The app should be available at http://localhost:8000.
PhpMyAdmin should be available at http://localhost:8080.
### Build Manually
1. Clone the repository:
```bash
git clone https://git.filiprojek.cz/fr/fuel-stats.git
```
2. Create `config/environment.php`:
- It should have the 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 the included `docker-compose.yaml` which includes both MariaDB and PhpMyAdmin.
3. Start a 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 prefer.
## Usage
1. Register and log in to the app.
2. Add your vehicles with their details (fuel type, registration, etc.).
3. Record each refueling:
- Select your vehicle.
- Input the number of liters, price per liter, and total cost.
4. Track your fuel consumption and spending through the dashboard.
5. View detailed stats and graphs to analyze your driving habits.
## License
This project is licensed under GPL3.0 and later. More information is available in the `LICENSE` file.

15
TODO.md Normal file
View File

@@ -0,0 +1,15 @@
# TODO
## Auth and User stuff
- [x] signup
- [x] signin
- [ ] edit user data - change password, mail...
## Core of the app
- [ ] intro tutorial when no car exist or just dont show anything
- [ ] change/set default car
- [ ] specific car view - charts, fuel records
- [ ] remove/edit fuel record
- [ ] IndexDB
- [ ]

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

View File

@@ -0,0 +1,42 @@
<?php
class DashboardController extends Controller {
public function index() {
$vehicle = new Vehicle();
$vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']);
$default_car = $vehicle->getDefaultVehicle($_SESSION['user']['id']);
$refuel = new Refuel();
$data = [
"date" => [],
"price" => [],
];
$raw_data = $refuel->latest_data($default_car['id'], 5);
foreach($raw_data as $one) {
array_push($data['date'], date('d. m.', strtotime($one['created_at'])));
array_push($data['price'], $one['price_per_liter']);
}
$latest_record = [
'name',
'liters',
'price_per_liter',
'total_price',
'created_at'
];
$latest_record = $refuel->latest_one($_SESSION['user']['id'])[0];
$this->view('dashboard/index', [
'title' => 'Dashboard',
'vehicles' => $vehicles,
'date_price_data' => $data,
'default_car' => $default_car,
'latest_record' => $latest_record,
]);
}
public function reroute(){
$this->redirect('/dashboard');
}
}

View 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");
}
}

View File

@@ -0,0 +1,85 @@
<?php
class RefuelController extends Controller {
public function create() {
if($_SERVER['REQUEST_METHOD'] === 'GET'){
$vehicle = new Vehicle();
$vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']);
$this->view('refuel/create', [
'title' => "New refuel record",
'vehicles' => $vehicles,
]);
return;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$vehicle_id = $_POST['vehicle'] ?? '';
$fuel_type = $_POST['fuel_type'] ?? '';
$liters = $_POST['liters'] ?? '';
$price_per_liter = $_POST['price_per_liter'] ?? '';
$total_price = $_POST['total_price'] ?? '';
$note = $_POST['note'] ?? '';
$validator = new Validator();
$validator->required('vehicle', $vehicle_id);
$validator->required('fuel_type', $fuel_type);
$validator->required('liters', $liters);
$validator->required('price_per_liter', $price_per_liter);
$validator->required('total_price', $total_price);
$validator->number('liters', $liters);
$validator->number('price_per_liter', $price_per_liter);
$validator->number('total_price', $total_price);
if (round($liters * $price_per_liter, 2) != $total_price) {
$validator->setErrors(["total_price" => "Price calculation is wrong"]);
}
if($note == "") $note = NULL;
if (!$validator->passes()) {
$vehicle = new Vehicle();
$vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']);
$this->view('refuel/create', [
'error' => 'Please correct the errors below.',
'validationErrors' => $validator->errors() ?: [],
'vehicles' => $vehicles,
'title' => 'New refuel record',
]);
return;
}
$record = new Refuel();
$result = $record->create([
'user_id' => $_SESSION['user']['id'],
'vehicle_id' => $vehicle_id,
'fuel_type' => $fuel_type,
'note' => $note,
'liters' => $liters,
'price_per_liter' => $price_per_liter,
'total_price' => $total_price,
]);
if ($result === true) {
$this->redirect('/');
} else {
$vehicle = new Vehicle();
$vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']);
$this->view('refuel/create', [
'title' => 'New refuel record',
'error' => $result,
'validationErrors' => [],
'vehicles' => $vehicles,
]);
}
return;
}
}
public function edit() {
// Edit refuel record (to be implemented later)
}
public function delete() {
// Delete refuel record (to be implemented later)
}
}

View File

@@ -0,0 +1,61 @@
<?php
class VehicleController extends Controller {
public function index() {
$vehicle = new Vehicle();
$vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']);
$this->view('vehicles/index', ['title' => 'Vehicles', 'vehicles' => $vehicles]);
}
public function create() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = $_POST['name'] ?? '';
$registration_plate = $_POST['registration_plate'] ?? '';
$fuel_type = $_POST['fuel_type'] ?? '';
$note = $_POST['note'] ?? '';
$validator = new Validator();
$validator->required('name', $name);
$validator->required('registration_plate', $registration_plate);
$validator->required('fuel_type', $fuel_type);
if($note == "") $note = NULL;
if (!$validator->passes()) {
$this->view('vehicles/create', [
'error' => 'Please correct the errors below.',
'validationErrors' => $validator->errors() ?: [],
]);
return;
}
$vehicle = new Vehicle();
$result = $vehicle->create([
'name' => $name,
'registration_plate' => strtoupper($registration_plate),
'fuel_type' => $fuel_type,
'note' => $note,
'user_id' => $_SESSION['user']['id'],
]);
if ($result === true) {
$this->redirect('/vehicles');
} else {
$this->view('vehicles/create', ['title' => 'Create vehicle', 'error' => $result, 'validationErrors' => []] );
}
} else {
$this->view('vehicles/create', ['title' => 'Create Vehicle']);
}
}
public function edit() {
// Edit vehicle (to be implemented later)
}
public function delete() {
// Delete vehicle (to be implemented later)
}
}

92
app/models/Refuel.php Normal file
View File

@@ -0,0 +1,92 @@
<?php
class Refuel {
private $db;
public function __construct() {
$this->db = Database::getInstance()->getConnection();
}
public function create($data) {
try{
$stmt = $this->db->prepare("
INSERT INTO refueling_records (user_id, vehicle_id, fuel_type, note, liters, price_per_liter, total_price, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, NOW())
");
$stmt->bind_param(
"iissddd",
$data['user_id'],
$data['vehicle_id'],
$data['fuel_type'],
$data['note'],
$data['liters'],
$data['price_per_liter'],
$data['total_price'],
);
if ($stmt->execute()) {
return true;
} else {
return "Error: " . $stmt->error;
}
} catch(mysqli_sql_exception $e) {
return $e->getMessage();
}
}
public function latest_data($vehicle_id, $record_count) {
try {
$stmt = $this->db->prepare("
SELECT `liters`, `price_per_liter`, `total_price`, `created_at`
FROM `refueling_records`
WHERE `vehicle_id` = ?
ORDER BY created_at DESC
LIMIT ?;
");
$stmt->bind_param("ii", $vehicle_id, $record_count);
if ($stmt->execute()) {
$result = $stmt->get_result();
$data = $result->fetch_all(MYSQLI_ASSOC);
$stmt->close();
return array_reverse($data);
} else {
return "Error: " . $stmt->error;
}
} catch (mysqli_sql_exception $e) {
return $e->getMessage();
}
}
public function latest_one($user_id, $record_count = 1) {
try {
$stmt = $this->db->prepare("
SELECT
`r`.`vehicle_id`,
`v`.`name` AS `vehicle_name`,
`r`.`liters`,
`r`.`price_per_liter`,
`r`.`total_price`,
`r`.`created_at`
FROM `refueling_records` AS `r`
JOIN `vehicles` AS `v` ON `r`.`vehicle_id` = `v`.`id`
WHERE `r`.`user_id` = ?
ORDER BY `r`.`created_at` DESC
LIMIT ?;
");
$stmt->bind_param("ii", $user_id, $record_count);
if ($stmt->execute()) {
$result = $stmt->get_result();
$data = $result->fetch_all(MYSQLI_ASSOC);
$stmt->close();
return array_reverse($data);
} else {
return "Error: " . $stmt->error;
}
} catch (mysqli_sql_exception $e) {
return $e->getMessage();
}
}
}

65
app/models/User.php Normal file
View 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.";
}
}

63
app/models/Vehicle.php Normal file
View File

@@ -0,0 +1,63 @@
<?php
class Vehicle {
private $db;
public function __construct() {
$this->db = Database::getInstance()->getConnection();
}
public function create($data) {
try{
$stmt = $this->db->prepare("
INSERT INTO vehicles (user_id, name, registration_plate, fuel_type, note, created_at)
VALUES (?, ?, ?, ?, ?, NOW())
");
$stmt->bind_param(
"issss",
$data['user_id'],
$data['name'],
$data['registration_plate'],
$data['fuel_type'],
$data['note'],
);
if ($stmt->execute()) {
return true;
} else {
return "Error: " . $stmt->error;
}
} catch(mysqli_sql_exception $e) {
return $e->getMessage();
}
}
public function getVehiclesByUser($user_id) {
$stmt = $this->db->prepare("SELECT id, name, registration_plate, fuel_type, note, created_at FROM vehicles WHERE user_id = ?");
$stmt->bind_param("i", $user_id);
$stmt->execute();
$result = $stmt->get_result();
$vehicles = [];
while ($row = $result->fetch_assoc()) {
$vehicles[] = $row;
}
return $vehicles;
}
public function getDefaultVehicle($user_id) {
$stmt = $this->db->prepare("
SELECT id, name, registration_plate, fuel_type, note, is_default
FROM vehicles
WHERE user_id = ? AND is_default = TRUE
LIMIT 1
");
$stmt->bind_param("i", $user_id);
$stmt->execute();
$result = $stmt->get_result();
return $result->fetch_assoc();
}
}

33
app/views/auth/signin.php Normal file
View 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="Fuel Stats Logo">
<h1>Sign in to Fuel Stats</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 Fuel Stats?</p>
<a href="/auth/signup">Create an account</a>
</div>
</section>

45
app/views/auth/signup.php Normal file
View 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="Fuel Stats Logo">
<h1>Sign up to Fuel Stats</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>

View File

@@ -0,0 +1,64 @@
<link rel="stylesheet" href="/css/dashboard.css">
<section class="dashboard">
<h1>Welcome, <?= htmlspecialchars($_SESSION['user']['username']) ?>!</h1>
<div>
<a href="/refuel/create" class="btn-green">Add new refuel record!</a>
<a href="/vehicles" class="btn-primary">List all vehicles</a>
<button class="btn-primary" id="btn-offline-add">Add new refuel record OFFLINE</button>
</div>
<div class="card-wrapper">
<section class="card latest">
<h2>Latest fuel record</h2>
<hr>
<div>
<b>Car:</b>
<p><?= $data['latest_record']['vehicle_name'] ?></p>
<b>Liters:</b>
<p><?= $data['latest_record']['liters'] ?> liters</p>
<b>Price per liter:</b>
<p><?= $data['latest_record']['price_per_liter'] ?>,-/liter</p>
<b>Total price:</b>
<p><?= $data['latest_record']['total_price'] ?>,-</p>
</div>
</section>
<section class="card">
<h2>Default car</h2>
<hr>
<b>Car</b>
<p><?= $data['default_car']['name'] ?></p>
<b>Registration plate</b>
<p><?= $data['default_car']['registration_plate'] ?></p>
<b>Fuel type</b>
<p><?= $data['default_car']['fuel_type'] ?></p>
<b>Note</b>
<p><?= $data['default_car']['note'] ?></p>
</section>
<section class="card history-graph">
<h2>Chart of Gas price</h2>
<hr>
<p><?= $data['default_car']['name'] . " | " . $data['default_car']['registration_plate']?></p>
<canvas id="chart-gas-price"></canvas>
</section>
</div>
</section>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const ctx = document.getElementById('chart-gas-price');
const data = <?= json_encode($data['date_price_data']); ?>;
new Chart(ctx, {
type: 'line',
data: {
labels: [...data['date']],
datasets: [{
label: 'Gas price history',
data: data['price'],
borderWidth: 1,
}]
},
});
</script>
<script defer src="/js/offline-records.js"></script>

13
app/views/errors/404.php Normal file
View 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>Fuel Stats | Error 494</title>
</head>
<body>
<h1>Error 404 - Page not found</h1>
<a href="/">Go back home</a>
</body>
</html>

4
app/views/home/index.php Normal file
View File

@@ -0,0 +1,4 @@
<div>
<h1>Welcome to Fuel Stats!</h1>
<p>Keep track of your refuels.</p>
</div>

View 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>Fuel Stats<?= isset($data['title']) ? " | " . $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><?= isset($data['title']) ? $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>

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>Fuel Stats<?= isset($data['title']) ? " | " . $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>

119
app/views/refuel/create.php Normal file
View File

@@ -0,0 +1,119 @@
<link rel="stylesheet" href="/css/form.css">
<link rel="stylesheet" href="/css/vehicle_create.css">
<section class="form">
<h1 class="header-form"><?= $this->get('title') ?></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="/refuel/create">
<label for="vehicle">Vehicle</label>
<select name="vehicle" id="vehicle">
<?php foreach ($this->get('vehicles') as $vehicle): ?>
<option value="<?= $vehicle['id'] ?>"><?= $vehicle['name'] . " | " . $vehicle['registration_plate'] ?></option>
<?php endforeach; ?>
</select>
<?php if (isset($this->get('validationErrors')['vehicle'])): ?>
<small class="error"><?= $this->get('validationErrors')['vehicle'] ?></small>
<?php endif; ?>
<label for="fuel_type">Fuel type</label>
<select name="fuel_type" id="fuel_type">
<option value="Diesel">Diesel</option>
<option value="Gasoline 95">Gasoline 95</option>
<option value="Gasoline 98">Gasoline 98</option>
<option value="Premium Diesel">Premium Diesel</option>
<option value="Premium Gasoline 95">Premium Gasoline 95</option>
<option value="Premium Gasoline 98">Premium Gasoline 98</option>
<option value="Other">Other</option>
</select>
<?php if (isset($this->get('validationErrors')['fuel_type'])): ?>
<small class="error"><?= $this->get('validationErrors')['fuel_type'] ?></small>
<?php endif; ?>
<label for="liters">Liters</label>
<input type="number" name="liters" id="liters" min="0" step=".01" value="<?= htmlspecialchars($_POST['liters'] ?? '0.0') ?>">
<?php if (isset($this->get('validationErrors')['liters'])): ?>
<small class="error"><?= $this->get('validationErrors')['liters'] ?></small>
<?php endif; ?>
<label for="price_per_liter">Price per liter</label>
<input type="number" name="price_per_liter" id="price_per_liter" min="0" step=".01" value="<?= htmlspecialchars($_POST['price_per_liter'] ?? '0.0') ?>">
<?php if (isset($this->get('validationErrors')['price_per_liter'])): ?>
<small class="error"><?= $this->get('validationErrors')['price_per_liter'] ?></small>
<?php endif; ?>
<label for="total_price">Total price</label>
<input type="number" name="total_price" id="total_price" min="0" step=".01" value="<?= htmlspecialchars($_POST['total_price'] ?? '0.0') ?>">
<?php if (isset($this->get('validationErrors')['total_price'])): ?>
<small class="error"><?= $this->get('validationErrors')['total_price'] ?></small>
<?php endif; ?>
<label for="note">Note</label>
<input type="text" name="note" id="note" value="<?= htmlspecialchars($_POST['note'] ?? '') ?>">
<?php if (isset($this->get('validationErrors')['note'])): ?>
<small class="error"><?= $this->get('validationErrors')['note'] ?></small>
<?php endif; ?>
<input type="submit" value="Create fuel record">
</form>
</section>
<script>
const inp_lit = document.querySelector("input#liters")
const inp_ppl = document.querySelector("input#price_per_liter")
const inp_tot = document.querySelector("input#total_price")
const rnd = (num) => Math.round((num + Number.EPSILON) * 100) / 100
function calculate(){
let liters = Number(inp_lit.value)
let price_per_liter = Number(inp_ppl.value)
let total_price = Number(inp_tot.value)
if(price_per_liter > 0 && liters > 0) {
total_price = rnd(liters * price_per_liter)
}
if(price_per_liter > 0 && total_price > 0) {
liters = rnd(total_price / price_per_liter)
}
if(liters > 0 && total_price > 0) {
price_per_liter = rnd(total_price / liters)
}
inp_lit.value = liters
inp_ppl.value = price_per_liter
inp_tot.value = total_price
}
[inp_lit, inp_ppl, inp_tot].forEach(inp => {
inp.addEventListener("change", () => {
calculate()
})
})
const vehicles = <?= json_encode($data['vehicles']); ?>;
const fuel_sel = document.querySelector("#fuel_type")
const vehic_sel = document.querySelector("#vehicle")
function selectFuel() {
const veh_id = vehic_sel.value
vehicles.forEach(el => {
if(el.id == veh_id) {
fuel_sel.value = el.fuel_type
return
}
})
}
selectFuel()
vehic_sel.addEventListener("change", () => {
selectFuel()
})
</script>

View File

View File

@@ -0,0 +1,35 @@
<link rel="stylesheet" href="/css/form.css">
<link rel="stylesheet" href="/css/vehicle_create.css">
<section class="form">
<h1 class="header-form"><?= $this->get('title', 'Create Vehicle') ?></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="/vehicles/create">
<label for="name">Vehicle name</label>
<input type="text" name="name" id="name" required value="<?= htmlspecialchars($_POST['name'] ?? '') ?>">
<label for="registration_plate">Registration plate</label>
<input type="text" name="registration_plate" id="registration_plate" maxlength="10" onkeypress="return event.charCode != 32" required value="<?= htmlspecialchars($_POST['registration_plate'] ?? '') ?>">
<label for="fuel_type">Fuel type</label>
<select name="fuel_type" id="fuel_type">
<option value="Diesel">Diesel</option>
<option value="Gasoline 95">Gasoline 95</option>
<option value="Gasoline 98">Gasoline 98</option>
<option value="Premium Diesel">Premium Diesel</option>
<option value="Premium Gasoline 95">Premium Gasoline 95</option>
<option value="Premium Gasoline 98">Premium Gasoline 98</option>
<option value="Other">Other</option>
</select>
<label for="note">Note</label>
<input type="text" name="note" id="note" value="<?= htmlspecialchars($_POST['note'] ?? '') ?>">
<input type="submit" value="Create vehicle">
</form>
</section>

View File

View File

@@ -0,0 +1,24 @@
<link rel="stylesheet" href="/css/vehicles.css">
<section class="vehicles">
<?php if (empty($this->get('vehicles'))): ?>
<p>No vehicles yet. <a href="/vehicles/create">Add your first vehicle</a>.</p>
<?php else: ?>
<div class="btn-wrapper">
<a href="/vehicles/create" class="btn-green">Add new vehicle!</a>
</div>
<div class="vehicle-wrapper">
<?php foreach ($this->get('vehicles') as $vehicle): ?>
<div class="vehicle bordered">
<b><?= htmlspecialchars($vehicle['name']) ?></b>
<p><?= htmlspecialchars($vehicle['registration_plate']) ?></p>
<p><?= htmlspecialchars($vehicle['fuel_type']) ?></p>
<p><?= htmlspecialchars($vehicle['note'] ?? "") ?></p>
<div class="actions">
<a href="/vehicles/edit?id=<?= $vehicle['id'] ?>">Edit</a>
<a href="/vehicles/delete?id=<?= $vehicle['id'] ?>" onclick="return confirm('Are you sure you want to delete this habit?')">Delete</a>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>

1
config/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
environment.php

View File

@@ -0,0 +1,6 @@
<?php
define('DB_HOST', '0.0.0.0');
define('DB_NAME', 'fuel_stats');
define('DB_USER', 'username');
define('DB_PASS', 'password');

23
core/Controller.php Normal file
View 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);
}
}

136
core/Database.php Normal file
View File

@@ -0,0 +1,136 @@
<?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 tables
$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);
}
$vehiclesTableQuery = "
CREATE TABLE IF NOT EXISTS vehicles (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
name VARCHAR(100) NOT NULL,
registration_plate VARCHAR(50) NOT NULL UNIQUE,
fuel_type ENUM('Diesel', 'Gasoline 95', 'Gasoline 98', 'Premium Diesel', 'Premium Gasoline 95', 'Premium Gasoline 98', 'Other') NOT NULL,
note VARCHAR(150) NULL,
is_default BOOLEAN DEFAULT FALSE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;
";
if (!$this->connection->query($vehiclesTableQuery)) {
die("Failed to create vehicles table: " . $this->connection->error);
}
$refuelingTableQuery = "
CREATE TABLE IF NOT EXISTS refueling_records (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
vehicle_id INT NOT NULL,
fuel_type ENUM('Diesel', 'Gasoline 95', 'Gasoline 98', 'Premium Diesel', 'Premium Gasoline 95', 'Premium Gasoline 98', 'Other') NOT NULL,
note VARCHAR(150) NULL,
liters DOUBLE(10, 2) NOT NULL,
price_per_liter DOUBLE(10, 2) NOT NULL,
total_price DOUBLE(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (vehicle_id) REFERENCES vehicles(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;
";
if (!$this->connection->query($refuelingTableQuery)) {
die("Failed to create refueling_records table: " . $this->connection->error);
}
/*
// Create table_name table
$usersTableQuery = "CREATE TABLE IF NOT EXISTS table_name (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;";
if (!$this->connection->query($usersTableQuery)) {
die("Failed to create table_name 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
View 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');
}
}
}

68
core/Validator.php Normal file
View File

@@ -0,0 +1,68 @@
<?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 contains numbers
*/
public function number($field, $value, $message = null) {
if(!is_numeric($value)) {
$this->errors[$field] = $message ?? "$field must be an number";
}
}
/**
* 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;
}
public function setErrors($errors) {
$this->errors = $errors;
}
/**
* Check if validation passed
*/
public function passes() {
return empty($this->errors);
}
}

25
core/View.php Normal file
View 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;
}
}

View 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
View 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"]
fuelstats:
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
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);
width: 18rem;
padding: 1rem;
}

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

48
public/css/global.css Normal file
View File

@@ -0,0 +1,48 @@
@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);
color: white;
}
.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);
}

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;
}

13
public/css/main.css Normal file
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
public/css/vars.css Normal file
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;
}

View File

@@ -0,0 +1,3 @@
#registration_plate {
text-transform: uppercase;
}

29
public/css/vehicles.css Normal file
View File

@@ -0,0 +1,29 @@
.vehicle-wrapper {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 1rem;
justify-content: center;
}
.vehicle {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: var(--clr-secondary);
border-radius: var(--border-radious);
border: var(--borderWidth-thin) solid var(--clr-border);
width: 17rem;
padding: 1rem;
margin-top: 1rem;
text-align: center;
min-height: 8rem;
}
.btn-wrapper {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 1rem;
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

56
public/index.php Normal file
View File

@@ -0,0 +1,56 @@
<?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 . 'Vehicle.php';
require_once models . 'Refuel.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']);
// vehicle routes
$router->group('/vehicles', ['RequireAuth'], function ($router) {
$router->add('', 'VehicleController@index');
$router->add('/create', 'VehicleController@create');
$router->add('/edit/{id}', 'VehicleController@edit');
$router->add('/delete/{id}', 'VehicleController@delete');
});
$router->group('/refuel', ['RequireAuth'], function ($router) {
$router->add('/create', 'RefuelController@create');
});
$router->dispatch();

View File

@@ -0,0 +1,98 @@
async function checkOnline() {
if (!navigator.onLine) {
console.log("Offline (no network connection)");
return false;
}
try {
const response = await fetch(
"https://www.google.com/favicon.ico?" + new Date().getTime(),
{
mode: "no-cors",
},
);
return true;
} catch (error) {
console.log("Connected to network but no internet access");
return false;
}
}
setInterval(async () => {
const isOnline = await checkOnline();
if (!isOnline) {
console.log("OFFLINE!!!");
}
}, 5000);
const offbtn = document.querySelector("#btn-offline-add");
offbtn.addEventListener("click", (e) => {
e.preventDefault();
document.querySelector("section.dashboard").style.display = "none";
const offline = document.createElement("div");
offline.classList.add("offline");
offline.innerHTML = `
<b>You're Offline</b>
<p>You can create an fuel record locally on your device and sync it later</p>
<section class="form">
<h1 class="header-form"><?= $this->get('title') ?></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="/refuel/create">
<label for="vehicle">Vehicle</label>
<select name="vehicle" id="vehicle">
<!-- <?php foreach ($this->get('vehicles') as $vehicle): ?> -->
<!-- <option value="<?= $vehicle['id'] ?>"><?= $vehicle['name'] . " | " . $vehicle['registration_plate'] ?></option> -->
<!-- <?php endforeach; ?> -->
</select>
<!-- <?php if (isset($this->get('validationErrors')['vehicle'])): ?> -->
<!-- <small class="error"><?= $this->get('validationErrors')['vehicle'] ?></small> -->
<!-- <?php endif; ?> -->
<label for="fuel_type">Fuel type</label>
<select name="fuel_type" id="fuel_type">
<option value="Diesel">Diesel</option>
<option value="Gasoline 95">Gasoline 95</option>
<option value="Gasoline 98">Gasoline 98</option>
<option value="Premium Diesel">Premium Diesel</option>
<option value="Premium Gasoline 95">Premium Gasoline 95</option>
<option value="Premium Gasoline 98">Premium Gasoline 98</option>
<option value="Other">Other</option>
</select>
<!-- <?php if (isset($this->get('validationErrors')['fuel_type'])): ?> -->
<!-- <small class="error"><?= $this->get('validationErrors')['fuel_type'] ?></small> -->
<!-- <?php endif; ?> -->
<label for="liters">Liters</label>
<input type="number" name="liters" id="liters" min="0" step=".01" value="<?= htmlspecialchars($_POST['liters'] ?? '0.0') ?>">
<!-- <?php if (isset($this->get('validationErrors')['liters'])): ?> -->
<!-- <small class="error"><?= $this->get('validationErrors')['liters'] ?></small> -->
<!-- <?php endif; ?> -->
<label for="price_per_liter">Price per liter</label>
<input type="number" name="price_per_liter" id="price_per_liter" min="0" step=".01" value="<?= htmlspecialchars($_POST['price_per_liter'] ?? '0.0') ?>">
<!-- <?php if (isset($this->get('validationErrors')['price_per_liter'])): ?> -->
<!-- <small class="error"><?= $this->get('validationErrors')['price_per_liter'] ?></small> -->
<!-- <?php endif; ?> -->
<label for="total_price">Total price</label>
<input type="number" name="total_price" id="total_price" min="0" step=".01" value="<?= htmlspecialchars($_POST['total_price'] ?? '0.0') ?>">
<!-- <?php if (isset($this->get('validationErrors')['total_price'])): ?> -->
<!-- <small class="error"><?= $this->get('validationErrors')['total_price'] ?></small> -->
<!-- <?php endif; ?> -->
<label for="note">Note</label>
<input type="text" name="note" id="note" value="<?= htmlspecialchars($_POST['note'] ?? '') ?>">
<!-- <?php if (isset($this->get('validationErrors')['note'])): ?> -->
<!-- <small class="error"><?= $this->get('validationErrors')['note'] ?></small> -->
<!-- <?php endif; ?> -->
<input type="submit" value="Create fuel record">
</form>
</section>
`;
document.querySelector("main").appendChild(offline);
});

1
storage/logs/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.log