First release
10
.docker/apache2/nofu.conf
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||||
|
ServerName nofu.local
|
||||||
|
DocumentRoot /var/www/public/
|
||||||
|
<Directory "/var/www/public/">
|
||||||
|
Require all granted
|
||||||
|
AllowOverride All
|
||||||
|
Options -Indexes
|
||||||
|
</Directory>
|
||||||
|
</VirtualHost>
|
2
.docker/start.sh
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
#/bin/bash
|
||||||
|
/usr/sbin/apache2ctl -DFOREGROUND
|
12
.gitignore
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
data/*
|
||||||
|
!data/.gitkeep
|
||||||
|
|
||||||
|
public/index.html
|
||||||
|
public/assets/css/user.css
|
||||||
|
public/assets/js/user.js
|
||||||
|
public/imgs/big_favicons/*.webp
|
||||||
|
public/imgs/favicons/*.webp
|
||||||
|
public/imgs/screenshots/*.webp
|
||||||
|
public/imgs/thumbs/*.webp
|
||||||
|
vendor/
|
||||||
|
composer.lock
|
38
Dockerfile
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
FROM debian:stable-slim
|
||||||
|
|
||||||
|
MAINTAINER Knah Tsaeb <knah-tsaeb_soshot@knah-tsaeb.org>
|
||||||
|
|
||||||
|
LABEL version="0.1.0"
|
||||||
|
LABEL description="Apache 2 / PHP / Nofu"
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
RUN apt-get -y update && apt-get install -y git composer php apache2 php-cli php-curl php-gd && apt-get clean && apt-get autoclean && apt-get autoremove
|
||||||
|
|
||||||
|
RUN rm -r /var/www/ && mkdir /var/www/
|
||||||
|
|
||||||
|
WORKDIR /var/www/
|
||||||
|
|
||||||
|
RUN git clone https://forge.leslibres.org/Knah-Tsaeb/Nofu.git --branch main --single-branch --depth 1 .
|
||||||
|
|
||||||
|
RUN composer install --no-dev && chown -R www-data:www-data /var/www/
|
||||||
|
|
||||||
|
COPY .docker/start.sh /usr/bin/start.sh
|
||||||
|
RUN chmod +x /usr/bin/start.sh
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
COPY .docker/apache2/nofu.conf /etc/apache2/sites-available/nofu.conf
|
||||||
|
RUN a2dissite 000-default.conf && a2ensite nofu.conf && a2enmod rewrite
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
WORKDIR /var/www/
|
||||||
|
|
||||||
|
ENTRYPOINT "start.sh"
|
||||||
|
|
||||||
|
# Build image
|
||||||
|
# docker buildx build -t nofu:0.1.0 .
|
||||||
|
# Run container
|
||||||
|
# docker run -d --restart unless-stopped -v nofu_data:/var/www/data -e TZ=UTC -p 8189:80 --name nofu nofu:0.1.0
|
||||||
|
# docker run -d --restart unless-stopped -v /opt/nofu:/var/www/data -e TZ=UTC -p 8189:80 --name nofu nofu:0.1.0
|
107
README.md
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
# Nofu
|
||||||
|
|
||||||
|
Nofu for **N**ot **O**nly **F**or **U**s is personal dashboard
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Introduction](#introduction)
|
||||||
|
- [Features](#features)
|
||||||
|
- [Instalation](#instalation)
|
||||||
|
- [Licence](#licence)
|
||||||
|
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
There are many impressive dashboards ([awesome-selfhosted](https://awesome-selfhosted.net/tags/personal-dashboards.html)), which are perfect for our needs. However, for non-technical people/geeks/computer enthusiasts/dev...., it can be difficult to understand all the features offered by these dashboards. That's why I created NOFU in 'scratch-an-itch' mode. Although it may not be perfect for everyone, it meets my needs and those of my family circle.
|
||||||
|
|
||||||
|
I also wanted a place where my family could find all my services with a quick documentation on my infrastructure (software used, what it's for, where it's located, where backups are stored...), in case I stop functioning one day. So that they can recover the family data or call someone to help them.
|
||||||
|
|
||||||
|
![screenshot]()
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* Simple to understand
|
||||||
|
* Easy customisation
|
||||||
|
* Minimal dependance
|
||||||
|
* No database
|
||||||
|
* Easy backup and deploy
|
||||||
|
* Static page
|
||||||
|
* Fast
|
||||||
|
* No JS or only for eye candy
|
||||||
|
* Responsive
|
||||||
|
|
||||||
|
## Instalation
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
|
||||||
|
Classic git clone, run composer, create website with your web server, that's all.
|
||||||
|
|
||||||
|
#### Clone
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://forge.leslibres.org/Knah-Tsaeb/Nofu.git
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Install dep
|
||||||
|
|
||||||
|
```shell
|
||||||
|
composer install --no-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Serve public folder throw your web server.
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
Clone, build and run.
|
||||||
|
|
||||||
|
#### Clone
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://forge.leslibres.org/Knah-Tsaeb/Nofu.git
|
||||||
|
cd nofu
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Build
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker buildx build -t nofu:0.1.0 .
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Run
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run -d --restart unless-stopped -v nofu_data:/var/www/data -e TZ=UTC -p 8189:80 --name nofu nofu:0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ressources
|
||||||
|
|
||||||
|
* <a href="https://"><img alt="docker icon" src="public/assets/icons/docker.svg" width="32"> SVGICON</a>
|
||||||
|
* <a href="https://"><img alt="docker icon" src="public/assets/icons/docker.svg" width="32"> SVGICON</a>
|
||||||
|
* <a href="https://"><img alt="docker icon" src="public/assets/icons/docker.svg" width="32"> SVGICON</a>
|
||||||
|
* <a href="https://"><img alt="docker icon" src="public/assets/icons/docker.svg" width="32"> SVGICON</a>
|
||||||
|
* <a href="https://"><img alt="docker icon" src="public/assets/icons/docker.svg" width="32"> SVGICON</a>
|
||||||
|
* <a href="https://"><img alt="docker icon" src="public/assets/icons/docker.svg" width="32"> SVGICON</a>
|
||||||
|
* <a href="https://"><img alt="docker icon" src="public/assets/icons/docker.svg" width="32"> SVGICON</a>
|
||||||
|
|
||||||
|
And some code from Stack Overflow :-)
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
WTFPL
|
||||||
|
|
||||||
|
```
|
||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
Version 2, December 2004
|
||||||
|
|
||||||
|
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim or modified
|
||||||
|
copies of this license document, and changing it is allowed as long
|
||||||
|
as the name is changed.
|
||||||
|
|
||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||||
|
|
||||||
|
```
|
171
app/App.php
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace KTH;
|
||||||
|
|
||||||
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
|
||||||
|
class App {
|
||||||
|
|
||||||
|
private $config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the cache file exists.
|
||||||
|
*
|
||||||
|
* @return bool True if the cache file exists, false otherwise.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
public function cacheExist(): bool {
|
||||||
|
if (file_exists('index.html')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a menu from an array of services.
|
||||||
|
*
|
||||||
|
* @param array $services An array of service data.
|
||||||
|
*
|
||||||
|
* @return array An array containing the unique types and locations of the services.
|
||||||
|
*/
|
||||||
|
public function makeMenu(array $services): array {
|
||||||
|
foreach ($services as $service) {
|
||||||
|
$menu['type'][] = $service['type'];
|
||||||
|
$menu['location'][] = $service['location'];
|
||||||
|
}
|
||||||
|
$menu['type'] = array_unique($menu['type'], SORT_LOCALE_STRING);
|
||||||
|
$menu['location'] = array_unique($menu['location'], SORT_LOCALE_STRING);
|
||||||
|
return $menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the path to an image file or a default image if the file does not exist.
|
||||||
|
*
|
||||||
|
* @param string $file The name of the image file.
|
||||||
|
* @param string $type The type of image (e.g., 'favicons', 'screenshots').
|
||||||
|
*
|
||||||
|
* @return string The path to the image file or a default image.
|
||||||
|
*/
|
||||||
|
public function returnImg(string $file, string $type): string {
|
||||||
|
$defaultFile = 'assets/icons/missing.svg';
|
||||||
|
$permitType = ['favicons', 'big_favicons', 'screenshots', 'thumbs'];
|
||||||
|
|
||||||
|
if (empty($file)) {
|
||||||
|
return $defaultFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($type, $permitType)) {
|
||||||
|
return $defaultFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter_var($file, FILTER_VALIDATE_URL)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_exists('imgs/' . $type . '/' . $file . '.webp')) {
|
||||||
|
return 'imgs/' . $type . '/' . $file . '.webp';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $defaultFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current IP address is permitted to bypass authentication.
|
||||||
|
*
|
||||||
|
* @param array|null $permitIP An array of permitted IP addresses or null if all IP addresses are permitted.
|
||||||
|
*
|
||||||
|
* @return bool True if the current IP address is permitted to bypass authentication, false otherwise.
|
||||||
|
*/
|
||||||
|
public function canByPassAuth(array|null $permitIP): bool {
|
||||||
|
if ($permitIP === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (in_array($_SERVER['REMOTE_ADDR'], $permitIP)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the user configuration settings.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function saveUserConfig(): void {
|
||||||
|
$colorScheme = htmlspecialchars($_POST['colorScheme']);
|
||||||
|
$view = htmlspecialchars($_POST['view']);
|
||||||
|
$title = htmlspecialchars($_POST['title']);
|
||||||
|
|
||||||
|
if (isset($_POST['reimport'])) {
|
||||||
|
if (htmlspecialchars($_POST['reimport']) == '1') {
|
||||||
|
$_SESSION['reimport'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$userConfig = [
|
||||||
|
'colorScheme' => $colorScheme,
|
||||||
|
'view' => $view,
|
||||||
|
'title' => $title
|
||||||
|
];
|
||||||
|
|
||||||
|
$config = Yaml::dump($userConfig);
|
||||||
|
file_put_contents('../data/config.yaml', $config);
|
||||||
|
$this->clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the configuration settings.
|
||||||
|
*
|
||||||
|
* @param array $defConfig The default configuration settings.
|
||||||
|
*
|
||||||
|
* @return array The configuration settings.
|
||||||
|
*/
|
||||||
|
public function getConfig(array $defConfig): array {
|
||||||
|
if (file_exists('../data/config.yaml')) {
|
||||||
|
$userConfig = Yaml::parseFile('../data/config.yaml');
|
||||||
|
if (empty($userConfig)) {
|
||||||
|
return $defConfig;
|
||||||
|
}
|
||||||
|
$config = array_merge($defConfig, $userConfig);
|
||||||
|
$this->config = $config;
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
$this->config = $defConfig;
|
||||||
|
return $defConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the cache file.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function clearCache(): void {
|
||||||
|
if (file_exists('../public/index.html')) {
|
||||||
|
unlink('../public/index.html');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the data directories.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
static function initializeDataDir(): void {
|
||||||
|
if (!is_dir(('../data/assets/css'))) {
|
||||||
|
mkdir('../data/assets/css', 0775, true);
|
||||||
|
}
|
||||||
|
if (!is_dir(('../data/assets/icons'))) {
|
||||||
|
mkdir('../data/assets/icons', 0775, true);
|
||||||
|
}
|
||||||
|
if (!is_dir(('../data/assets/js'))) {
|
||||||
|
mkdir('../data/assets/js', 0775, true);
|
||||||
|
}
|
||||||
|
if (!is_dir(('../data/imgs/favicons'))) {
|
||||||
|
mkdir('../data/imgs/favicons', 0775, true);
|
||||||
|
}
|
||||||
|
if (!is_dir(('../data/imgs/screenshots'))) {
|
||||||
|
mkdir('../data/imgs/screenshots', 0775, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
142
app/generator/HTMLGenerator.php
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace KTH\HTMLGenerator;
|
||||||
|
|
||||||
|
use League\CommonMark\Environment\Environment;
|
||||||
|
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
|
||||||
|
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
|
||||||
|
use League\CommonMark\Extension\TableOfContents\TableOfContentsExtension;
|
||||||
|
use League\CommonMark\MarkdownConverter;
|
||||||
|
|
||||||
|
class HTMLGenerator {
|
||||||
|
|
||||||
|
private $favicons;
|
||||||
|
private $screenshots;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new HTMLGenerator object.
|
||||||
|
*
|
||||||
|
* @param array $services An array of service configurations.
|
||||||
|
*/
|
||||||
|
public function __construct(array $services) {
|
||||||
|
foreach ($services as $service) {
|
||||||
|
if (!empty($service['favicon'])) {
|
||||||
|
$favicons[] = $service['favicon'];
|
||||||
|
}
|
||||||
|
if (!empty($service['screenshot'])) {
|
||||||
|
$screenshots[] = $service['screenshot'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->favicons = array_unique($favicons, SORT_LOCALE_STRING);
|
||||||
|
$this->screenshots = array_unique($screenshots, SORT_LOCALE_STRING);
|
||||||
|
if (isset($_SESSION['reimport'])) {
|
||||||
|
$this->resizeImg();
|
||||||
|
unset($_SESSION['reimport']);
|
||||||
|
}
|
||||||
|
$this->copyUserFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resizes the favicons and screenshots.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function resizeImg(): void {
|
||||||
|
$image = new \Zebra_Image();
|
||||||
|
foreach ($this->favicons as $favicon) {
|
||||||
|
if (!str_starts_with(strtolower($favicon), 'http')) {
|
||||||
|
if (is_file('../data/imgs/favicons/' . $favicon)) {
|
||||||
|
$image->source_path = '../data/imgs/favicons/' . $favicon;
|
||||||
|
$image->target_path = '../public/imgs/favicons/' . $favicon . '.webp';
|
||||||
|
$image->preserve_aspect_ratio = true;
|
||||||
|
$image->enlarge_smaller_images = false;
|
||||||
|
$image->resize(32, 32);
|
||||||
|
|
||||||
|
$image->target_path = '../public/imgs/big_favicons/' . $favicon . '.webp';
|
||||||
|
$image->resize(128, 128);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->screenshots as $screenshot) {
|
||||||
|
if (!str_starts_with(strtolower($screenshot), 'http')) {
|
||||||
|
if (is_file('../data/imgs/screenshots/' . $favicon)) {
|
||||||
|
$image->source_path = '../data/imgs/screenshots/' . $screenshot;
|
||||||
|
$image->target_path = '../public/imgs/screenshots/' . $screenshot . '.webp';
|
||||||
|
$image->preserve_aspect_ratio = true;
|
||||||
|
$image->enlarge_smaller_images = false;
|
||||||
|
$image->resize(1120);
|
||||||
|
|
||||||
|
$image->target_path = '../public/imgs/thumbs/' . $screenshot . '.webp';
|
||||||
|
$image->resize(250);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy user-provided CSS, JS, and SVG files to the public directory.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function copyUserFile(): void {
|
||||||
|
if (file_exists('../data/assets/css/user.css')) {
|
||||||
|
copy('../data/assets/css/user.css', '../public/assets/css/user.css');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_exists('../data/assets/js/user.js')) {
|
||||||
|
copy('../data/assets/js/user.js', '../public/assets/js/user.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (glob("../data/assets/icons/*.svg") as $filePath) {
|
||||||
|
$filename = pathinfo($filePath, PATHINFO_BASENAME);
|
||||||
|
copy($filePath, $filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the user documentation in HTML format.
|
||||||
|
*
|
||||||
|
* @return string | null The HTML content of the user documentation.
|
||||||
|
*/
|
||||||
|
public function genUserDoc(): string | null {
|
||||||
|
if (file_exists('../data/help.md')) {
|
||||||
|
$configMD = [
|
||||||
|
'table_of_contents' => [
|
||||||
|
'html_class' => 'table-of-contents',
|
||||||
|
'position' => 'top',
|
||||||
|
'style' => 'bullet',
|
||||||
|
'min_heading_level' => 1,
|
||||||
|
'max_heading_level' => 6,
|
||||||
|
'normalize' => 'relative',
|
||||||
|
'placeholder' => null,
|
||||||
|
],
|
||||||
|
'heading_permalink' => [
|
||||||
|
'html_class' => 'heading-permalink',
|
||||||
|
'id_prefix' => 'content',
|
||||||
|
'apply_id_to_heading' => false,
|
||||||
|
'heading_class' => '',
|
||||||
|
'fragment_prefix' => 'content',
|
||||||
|
'insert' => 'before',
|
||||||
|
'min_heading_level' => 1,
|
||||||
|
'max_heading_level' => 6,
|
||||||
|
'title' => 'Permalink',
|
||||||
|
'symbol' => '',
|
||||||
|
'aria_hidden' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$environment = new Environment($configMD);
|
||||||
|
$environment->addExtension(new CommonMarkCoreExtension());
|
||||||
|
$environment->addExtension(new HeadingPermalinkExtension());
|
||||||
|
$environment->addExtension(new TableOfContentsExtension());
|
||||||
|
$converter = new MarkdownConverter($environment);
|
||||||
|
|
||||||
|
return $converter->convert(file_get_contents('../data/help.md'))->getContent();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
93
app/login/Login.php
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Login;
|
||||||
|
|
||||||
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
|
||||||
|
class Login {
|
||||||
|
|
||||||
|
private $listUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance of the class.
|
||||||
|
*
|
||||||
|
* Initialize the listUser property as an empty array and call the loadUser method.
|
||||||
|
*/
|
||||||
|
function __construct() {
|
||||||
|
$this->listUser = [];
|
||||||
|
$this->loadUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new user to the list of users.
|
||||||
|
*
|
||||||
|
* @param string $login The login name of the new user.
|
||||||
|
* @param string $password The password of the new user.
|
||||||
|
* @param string $role The role of the new user.
|
||||||
|
*
|
||||||
|
* @return bool True if the user was successfully added, false otherwise.
|
||||||
|
*/
|
||||||
|
public function addUser(string $login, string $password, string $role): bool {
|
||||||
|
if ($this->listUser[$login]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$newUser = [$login => ['password' => password_hash($password, PASSWORD_DEFAULT), 'role' => $role]];
|
||||||
|
$listUser = array_merge($newUser, $this->listUser);
|
||||||
|
|
||||||
|
$yaml = Yaml::dump($listUser);
|
||||||
|
|
||||||
|
return file_put_contents('../data/users.yaml', $yaml);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the users from the YAML file.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function loadUsers() {
|
||||||
|
if (file_exists('../data/users.yaml')) {
|
||||||
|
$listUser = Yaml::parseFile('../data/users.yaml');
|
||||||
|
if (!empty($listUser)) {
|
||||||
|
$this->listUser = $listUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log in a user with the given credentials.
|
||||||
|
*
|
||||||
|
* @param string $user The username.
|
||||||
|
* @param string $password The password.
|
||||||
|
*
|
||||||
|
* @return bool True if the user is logged in successfully, false otherwise.
|
||||||
|
*/
|
||||||
|
public function logIn(string $user, string $password): bool {
|
||||||
|
if ($this->listUser[$user] && password_verify($password, $this->listUser[$user]['password'])) {
|
||||||
|
$_SESSION['isLogged'] = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log out the user and redirect to the index page.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
static function logOut(): void {
|
||||||
|
session_destroy();
|
||||||
|
header("Location: index.php");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the user is logged in.
|
||||||
|
*
|
||||||
|
* @return bool True if the user is logged in, false otherwise.
|
||||||
|
*/
|
||||||
|
static function isLogged(): bool {
|
||||||
|
if (isset($_SESSION['isLogged']) && $_SESSION['isLogged'] === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
39
app/utils/CsrfToken.php
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Utils;
|
||||||
|
|
||||||
|
class CsrfToken {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a CSRF token and store it in the session.
|
||||||
|
*
|
||||||
|
* This static method generates a CSRF token using random bytes and stores it in the session.
|
||||||
|
* The generated token is a hexadecimal string with a length of 32 characters.
|
||||||
|
*
|
||||||
|
* @return string The generated CSRF token.
|
||||||
|
*/
|
||||||
|
public static function generateToken(): string {
|
||||||
|
$token = bin2hex(random_bytes(32));
|
||||||
|
$_SESSION['csrf_token'] = $token;
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a CSRF token against the one stored in the session.
|
||||||
|
*
|
||||||
|
* This static method validates a given CSRF token against the one stored in the session.
|
||||||
|
* It returns true if the provided token matches the one in the session; otherwise, it returns false.
|
||||||
|
*
|
||||||
|
* @param string $token The CSRF token to be validated.
|
||||||
|
*
|
||||||
|
* @return bool True if the provided token is valid; otherwise, false.
|
||||||
|
*/
|
||||||
|
public static function validateToken(string $token): bool {
|
||||||
|
if (isset($_SESSION['csrf_token']) && $_SESSION['csrf_token'] === $token) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
app/utils/Debug.php
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Utils;
|
||||||
|
|
||||||
|
class Debug {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print the contents of a variable with a customizable name and backtrace information.
|
||||||
|
*
|
||||||
|
* @param mixed $data The variable to print.
|
||||||
|
* @param string $name The name to display above the variable contents.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function n_print(mixed $data, string $name = ''): void {
|
||||||
|
error_reporting(-1);
|
||||||
|
$aBackTrace = debug_backtrace();
|
||||||
|
echo '<h2>', $name, '</h2>';
|
||||||
|
echo '<fieldset style="border: 1px solid orange; padding: 5px;color: #333; background-color: #fff;">';
|
||||||
|
echo '<legend style="border:1px solid orange;padding: 1px;background-color:#eee;color:orange;">', $aBackTrace[0]['file'], ' ligne => ', $aBackTrace[0]['line'], '</legend>';
|
||||||
|
echo '<pre>', htmlentities(print_r($data, 1)), '</pre>';
|
||||||
|
echo '</fieldset><br />';
|
||||||
|
}
|
||||||
|
}
|
20
app/utils/Select.php
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Utils;
|
||||||
|
|
||||||
|
class Select {
|
||||||
|
/**
|
||||||
|
* Check if a given value is selected.
|
||||||
|
*
|
||||||
|
* @param mixed $ref The reference value to compare against.
|
||||||
|
* @param mixed $param The value to compare.
|
||||||
|
*
|
||||||
|
* @return string The string 'selected' if the values match, or an empty string otherwise.
|
||||||
|
*/
|
||||||
|
static function isSelected(string $ref, string $param): string {
|
||||||
|
if ($ref === $param) {
|
||||||
|
return 'selected';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
15
composer.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"symfony/yaml": "^7.0",
|
||||||
|
"stefangabos/zebra_image": "^2.8",
|
||||||
|
"league/commonmark": "^2.4"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Utils\\" : "app/utils/",
|
||||||
|
"Login\\" : "app/login/",
|
||||||
|
"KTH\\" : "app/",
|
||||||
|
"KTH\\HTMLGenerator\\" : "app/generator/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
0
data/.gitkeep
Normal file
13
public/.htaccess
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# 480 weeks
|
||||||
|
<FilesMatch "\.(html|ico|jpg|jpeg|png|gif|js|css)$">
|
||||||
|
# Header set Cache-Control "max-age=290304000, public"
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
<ifModule mod_gzip.c>
|
||||||
|
mod_gzip_on Yes
|
||||||
|
mod_gzip_dechunk Yes
|
||||||
|
mod_gzip_item_include file \.(html?|txt|css|js|php)$
|
||||||
|
mod_gzip_item_include mime ^application/x-javascript.*
|
||||||
|
mod_gzip_item_exclude mime ^image/.*
|
||||||
|
mod_gzip_item_exclude rspheader ^Content-Encoding:.*gzip.*
|
||||||
|
</ifModule>
|
543
public/assets/css/style.css
Normal file
|
@ -0,0 +1,543 @@
|
||||||
|
/*
|
||||||
|
########################################################
|
||||||
|
############# DO NOT EDIT THIS FILE ####################
|
||||||
|
############# USE data/user.css ####################
|
||||||
|
########################################################
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background-color: #dadada;
|
||||||
|
--border-radius: 10px;
|
||||||
|
--light-color: #ececec;
|
||||||
|
--nav-background-color: #34495e;
|
||||||
|
--text-color: #1d1e22;
|
||||||
|
--margin: .7em;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login {
|
||||||
|
color: var(--text-color);
|
||||||
|
border: 1px solid var(--text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
|
||||||
|
--background-color: #1d1e22;
|
||||||
|
--border-radius: 10px;
|
||||||
|
--light-color: #ececec;
|
||||||
|
--nav-background-color: #34495e;
|
||||||
|
--text-color: #1d1e22;
|
||||||
|
--margin: .7em;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: var(--light-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login {
|
||||||
|
color: var(--light-color);
|
||||||
|
border: 1px solid var(--light-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*:after,
|
||||||
|
*:before {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 59%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
margin: var(--margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleBar {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleBar .linkList {
|
||||||
|
float: right;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 2.5em;
|
||||||
|
width: 2.5em;
|
||||||
|
margin: var(--margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-align: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleBar div {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 2.5em;
|
||||||
|
height: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: var(--light-color);
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--nav-background-color);
|
||||||
|
height: 1.2em;
|
||||||
|
transition: .3s all ease;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg:hover {
|
||||||
|
background-color: var(--nav-background-color);
|
||||||
|
fill: var(--light-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-container {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 100dvw;
|
||||||
|
padding: 0 .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-container h3 a {
|
||||||
|
color: var(--text-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-container .card {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-container .card {
|
||||||
|
background: var(--light-color);
|
||||||
|
box-shadow: 0 0 2px 2px rgba(0, 0, 0, .05);
|
||||||
|
margin: var(--margin);
|
||||||
|
transition: .3s all ease;
|
||||||
|
width: calc(100% / 7 - 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-container .card:hover {
|
||||||
|
border-radius: 15px 15px var(--border-radius) var(--border-radius);
|
||||||
|
box-shadow: 0 0 30px 15px rgba(0, 0, 0, 0.56);
|
||||||
|
transform: scale(1.20);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-container .card .thumb {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
transition: .3s all ease;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-container .card .thumb:hover {
|
||||||
|
border-top-left-radius: var(--border-radius);
|
||||||
|
border-top-right-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-container .card .content {
|
||||||
|
padding: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-container .card h3 {
|
||||||
|
font: 2.6rem/3.2rem 'Bree Serif', serif;
|
||||||
|
letter-spacing: -.075rem;
|
||||||
|
margin: 0 0 5px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-container .card p {
|
||||||
|
color: var(--text-color);
|
||||||
|
font: 400 1.6rem/2.2rem 'Open Sans script=all', sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-container .readMore {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .favicon {
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
margin-right: .1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icons {
|
||||||
|
.card {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .content {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .content h3 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .content .favicon {
|
||||||
|
margin: 0;
|
||||||
|
width: 128px;
|
||||||
|
height: 128px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
height: 128px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
transition: .3s all ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
img:hover {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background-color: rgb(0, 0, 0);
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
display: none;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding-top: 100px;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
animation-duration: 0.3s;
|
||||||
|
animation-name: animatetop;
|
||||||
|
background-color: var(--light-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||||
|
font-size: 150%;
|
||||||
|
height: calc(100% / 1 - 100px);
|
||||||
|
margin: auto;
|
||||||
|
padding: .2em;
|
||||||
|
position: relative;
|
||||||
|
width: calc(100% / 2 - 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content .screenshot {
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header a,
|
||||||
|
.modal-header a:visited {
|
||||||
|
color: var(--nav-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header a:hover,
|
||||||
|
.modal-header a:visited:hover {
|
||||||
|
filter: brightness(1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.userDoc {
|
||||||
|
overflow: scroll;
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animatetop {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
top: -400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
color: var(--text-color);
|
||||||
|
display: block;
|
||||||
|
float: right;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: .3s all ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close:hover,
|
||||||
|
.close:focus {
|
||||||
|
color: var(--nav-background-color);
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal h2 {
|
||||||
|
font-size: 160%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content h2 img {
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
background-color: var(--light-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: 1px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 2px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body img {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
background-color: var(--nav-background-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
height: auto;
|
||||||
|
margin: .5em 0;
|
||||||
|
padding: .5em 0;
|
||||||
|
position: relative;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav img {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav span {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
margin: auto var(--margin);
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readMore {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
height: auto;
|
||||||
|
margin: var(--margin);
|
||||||
|
padding: var(--margin);
|
||||||
|
position: relative;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background-color: var(--nav-background-color);
|
||||||
|
border: 1px solid var(--light-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
color: var(--light-color);
|
||||||
|
padding: .2em .3em;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background-color: var(--light-color);
|
||||||
|
border: 1px solid var(--nav-background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a {
|
||||||
|
background-clip: text;
|
||||||
|
background-image: linear-gradient(to right,
|
||||||
|
var(--background-color),
|
||||||
|
var(--background-color) 50%,
|
||||||
|
var(--light-color) 50%);
|
||||||
|
background-position: -100%;
|
||||||
|
background-size: 200% 100%;
|
||||||
|
color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
margin: auto var(--margin);
|
||||||
|
padding: 5px 0;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a:before,
|
||||||
|
nav .active::before {
|
||||||
|
background: var(--text-color);
|
||||||
|
bottom: -3px;
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
height: 3px;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav .active {
|
||||||
|
background-image: linear-gradient(to right,
|
||||||
|
var(--background-color),
|
||||||
|
var(--background-color) 50%,
|
||||||
|
var(--light-color) 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a:hover,
|
||||||
|
nav .active {
|
||||||
|
background-position: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a:hover::before,
|
||||||
|
nav .active::before {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login {
|
||||||
|
border: 1px solid var(--light-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
color: var(--light-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 2rem;
|
||||||
|
gap: 1rem;
|
||||||
|
margin: var(--margin) auto 0;
|
||||||
|
padding: 1rem;
|
||||||
|
width: 30vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text],
|
||||||
|
input[type=password],
|
||||||
|
select {
|
||||||
|
border: 1px solid var(--nav-background-color);
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 0 var(--margin) 0;
|
||||||
|
padding: 12px 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button[type=submit] {
|
||||||
|
height: 3em;
|
||||||
|
margin: auto;
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.card-container:not(.icons) .card {
|
||||||
|
width: calc(100% / 1 - 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login {
|
||||||
|
width: 80vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 600px) {
|
||||||
|
.card-container:not(.icons) .card {
|
||||||
|
width: calc(100% / 2 - 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: calc(100% / 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login {
|
||||||
|
width: 70vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 768px) {
|
||||||
|
.card-container:not(.icons) .card {
|
||||||
|
width: calc(100% / 3 - 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: calc(100% / 2 + 150px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login {
|
||||||
|
width: 60vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 992px) {
|
||||||
|
.card-container:not(.icons) .card {
|
||||||
|
width: calc(100% / 4 - 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: calc(100% / 2 + 100px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login {
|
||||||
|
width: 50vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 1200px) {
|
||||||
|
.card-container:not(.icons) .card {
|
||||||
|
width: calc(100% / 5 - 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: calc(100% / 2 + 50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login {
|
||||||
|
width: 40vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 1600px) {
|
||||||
|
.card-container:not(.icons) .card {
|
||||||
|
width: calc(100% / 7 - 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: calc(100% / 2 + 200px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login {
|
||||||
|
width: 30vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
display: none;
|
||||||
|
}
|
1
public/assets/icons/docker.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21.81 10.25C21.75 10.21 21.25 9.82 20.17 9.82C19.89 9.82 19.61 9.85 19.33 9.9C19.12 8.5 17.95 7.79 17.9 7.76L17.61 7.59L17.43 7.86C17.19 8.22 17 8.63 16.92 9.05C16.72 9.85 16.84 10.61 17.25 11.26C16.76 11.54 15.96 11.61 15.79 11.61H2.62C2.28 11.61 2 11.89 2 12.24C2 13.39 2.18 14.54 2.58 15.62C3.03 16.81 3.71 17.69 4.58 18.23C5.56 18.83 7.17 19.17 9 19.17C9.79 19.17 10.61 19.1 11.42 18.95C12.54 18.75 13.62 18.36 14.61 17.79C15.43 17.32 16.16 16.72 16.78 16C17.83 14.83 18.45 13.5 18.9 12.35H19.09C20.23 12.35 20.94 11.89 21.33 11.5C21.59 11.26 21.78 10.97 21.92 10.63L22 10.39L21.81 10.25M3.85 11.24H5.61C5.69 11.24 5.77 11.17 5.77 11.08V9.5C5.77 9.42 5.7 9.34 5.61 9.34H3.85C3.76 9.34 3.69 9.41 3.69 9.5V11.08C3.7 11.17 3.76 11.24 3.85 11.24M6.28 11.24H8.04C8.12 11.24 8.2 11.17 8.2 11.08V9.5C8.2 9.42 8.13 9.34 8.04 9.34H6.28C6.19 9.34 6.12 9.41 6.12 9.5V11.08C6.13 11.17 6.19 11.24 6.28 11.24M8.75 11.24H10.5C10.6 11.24 10.67 11.17 10.67 11.08V9.5C10.67 9.42 10.61 9.34 10.5 9.34H8.75C8.67 9.34 8.6 9.41 8.6 9.5V11.08C8.6 11.17 8.66 11.24 8.75 11.24M11.19 11.24H12.96C13.04 11.24 13.11 11.17 13.11 11.08V9.5C13.11 9.42 13.05 9.34 12.96 9.34H11.19C11.11 9.34 11.04 9.41 11.04 9.5V11.08C11.04 11.17 11.11 11.24 11.19 11.24M6.28 9H8.04C8.12 9 8.2 8.91 8.2 8.82V7.25C8.2 7.16 8.13 7.09 8.04 7.09H6.28C6.19 7.09 6.12 7.15 6.12 7.25V8.82C6.13 8.91 6.19 9 6.28 9M8.75 9H10.5C10.6 9 10.67 8.91 10.67 8.82V7.25C10.67 7.16 10.61 7.09 10.5 7.09H8.75C8.67 7.09 8.6 7.15 8.6 7.25V8.82C8.6 8.91 8.66 9 8.75 9M11.19 9H12.96C13.04 9 13.11 8.91 13.11 8.82V7.25C13.11 7.16 13.04 7.09 12.96 7.09H11.19C11.11 7.09 11.04 7.15 11.04 7.25V8.82C11.04 8.91 11.11 9 11.19 9M11.19 6.72H12.96C13.04 6.72 13.11 6.65 13.11 6.56V5C13.11 4.9 13.04 4.83 12.96 4.83H11.19C11.11 4.83 11.04 4.89 11.04 5V6.56C11.04 6.64 11.11 6.72 11.19 6.72M13.65 11.24H15.41C15.5 11.24 15.57 11.17 15.57 11.08V9.5C15.57 9.42 15.5 9.34 15.41 9.34H13.65C13.57 9.34 13.5 9.41 13.5 9.5V11.08C13.5 11.17 13.57 11.24 13.65 11.24" /></svg>
|
After Width: | Height: | Size: 2 KiB |
1
public/assets/icons/logout.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 7L15.59 8.41L18.17 11H8V13H18.17L15.59 15.58L17 17L22 12M4 5H12V3H4C2.9 3 2 3.9 2 5V19C2 20.1 2.9 21 4 21H12V19H4V5Z" /></svg>
|
After Width: | Height: | Size: 199 B |
38
public/assets/icons/missing.svg
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 13.5"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="missing.svg"
|
||||||
|
width="24"
|
||||||
|
height="13.5"
|
||||||
|
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="38.583333"
|
||||||
|
inkscape:cx="11.974082"
|
||||||
|
inkscape:cy="12"
|
||||||
|
inkscape:window-width="1860"
|
||||||
|
inkscape:window-height="1172"
|
||||||
|
inkscape:window-x="60"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<path
|
||||||
|
d="M 18.110644,0.3007551 H 5.8893555 c -0.8402142,0 -1.5276614,0.580432 -1.5276614,1.2898486 V 13.199245 L 7.4170169,10.619546 H 18.110644 c 0.840213,0 1.527662,-0.580432 1.527662,-1.2898491 V 1.5906037 c 0,-0.7094166 -0.687449,-1.2898486 -1.527662,-1.2898486 m 0,9.0289418 H 6.8059526 L 5.8893555,10.103606 V 1.5906037 H 18.110644 v 7.7390932 m -11.4574584,-1.289849 2.6734072,-2.90216 1.9095772,1.934773 2.673407,-2.9021597 3.437236,3.8695467"
|
||||||
|
id="path1"
|
||||||
|
style="stroke-width:0.701863" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
1
public/assets/icons/redirection.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10.5 18H18V20H10.5C6.91 20 4 17.09 4 13.5S6.91 7 10.5 7H16.17L13.08 3.91L14.5 2.5L20 8L14.5 13.5L13.09 12.09L16.17 9H10.5C8 9 6 11 6 13.5S8 18 10.5 18Z" /></svg>
|
After Width: | Height: | Size: 231 B |
1
public/assets/icons/server.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M1.75 1h12.5c.966 0 1.75.784 1.75 1.75v4c0 .372-.116.717-.314 1 .198.283.314.628.314 1v4a1.75 1.75 0 0 1-1.75 1.75H1.75A1.75 1.75 0 0 1 0 12.75v-4c0-.358.109-.707.314-1a1.739 1.739 0 0 1-.314-1v-4C0 1.784.784 1 1.75 1ZM1.5 2.75v4c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-4a.25.25 0 0 0-.25-.25H1.75a.25.25 0 0 0-.25.25Zm.25 5.75a.25.25 0 0 0-.25.25v4c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-4a.25.25 0 0 0-.25-.25ZM7 4.75A.75.75 0 0 1 7.75 4h4.5a.75.75 0 0 1 0 1.5h-4.5A.75.75 0 0 1 7 4.75ZM7.75 10h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM3 4.75A.75.75 0 0 1 3.75 4h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 3 4.75ZM3.75 10h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5Z"/></svg>
|
After Width: | Height: | Size: 788 B |
1
public/assets/icons/vm.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M12.003.064C5.376.064 0 5.407 0 12s5.376 11.936 12.003 11.936c2.169 0 4.2-.57 5.955-1.57l.624 1.57h4.841l-1.893-4.679A11.85 11.85 0 0 0 24 12C24 5.407 18.63.064 12.003.064M8.818 2.03c.398.339.324.198.86.134c.61-.397.893.942 1.147.195c.748.097 1.542.34 2.25.584a3.45 3.45 0 0 1 1.859 1.128l-.014.007l.35.463c.045.08.082.164.12.248c.142 1.205 1.48 1.19 2.377 1.625c.767.272 1.69.686 1.785 1.611c-.193-.042-.941-.921-1.53-1.007a4 4 0 0 1-1.094-.255L14.86 6.38v-.007a3 3 0 0 1-.309-.053v.013l-2.927-.362c.048.033.1.077.148.12l3 .585v-.007l.209.053l.839.188c.166.016.334.043.47.067c.856.236 1.868.194 2.571.792c-.184.352-1.21.153-1.719.108c-.062-.012-.131-.023-.194-.034l-.034-.007c-.696-.113-1.411-.12-2.081.088h-.007a3.2 3.2 0 0 0-.671.302c-.968.563-2.164.767-2.967 1.577c-.787.847-.739 2.012-.604 3.095h.033v.275l.04.282c.41 2.19 1.5 4.2 1.84 6.412c.065.843.203 1.932.309 2.618c-.306-.091-.475-1.462-.544-1.007a38 38 0 0 0-3.565-5.25c-.853-1.004-1.697-2.06-2.712-2.894c-.685-.528-.468-1.55-.537-2.302c-.23-.926-.094-1.848.06-2.773c.313-.963.418-1.968.846-2.893c.653-.581.669-1.63 1.303-2.135c.094.058.157.085.2.1l.068.008h.007c.09-.095-.888-1.116.02-.712c.035-.537.854-.128.866-.597m3.847 2.182c-.323.009-.574.13-.645.335c-.114.33.273.755.866.96c.594.205 1.168.109 1.282-.221s-.272-.762-.866-.967a1.8 1.8 0 0 0-.637-.107"/></svg>
|
After Width: | Height: | Size: 1.4 KiB |
1
public/assets/icons/web.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16.36,14C16.44,13.34 16.5,12.68 16.5,12C16.5,11.32 16.44,10.66 16.36,10H19.74C19.9,10.64 20,11.31 20,12C20,12.69 19.9,13.36 19.74,14M14.59,19.56C15.19,18.45 15.65,17.25 15.97,16H18.92C17.96,17.65 16.43,18.93 14.59,19.56M14.34,14H9.66C9.56,13.34 9.5,12.68 9.5,12C9.5,11.32 9.56,10.65 9.66,10H14.34C14.43,10.65 14.5,11.32 14.5,12C14.5,12.68 14.43,13.34 14.34,14M12,19.96C11.17,18.76 10.5,17.43 10.09,16H13.91C13.5,17.43 12.83,18.76 12,19.96M8,8H5.08C6.03,6.34 7.57,5.06 9.4,4.44C8.8,5.55 8.35,6.75 8,8M5.08,16H8C8.35,17.25 8.8,18.45 9.4,19.56C7.57,18.93 6.03,17.65 5.08,16M4.26,14C4.1,13.36 4,12.69 4,12C4,11.31 4.1,10.64 4.26,10H7.64C7.56,10.66 7.5,11.32 7.5,12C7.5,12.68 7.56,13.34 7.64,14M12,4.03C12.83,5.23 13.5,6.57 13.91,8H10.09C10.5,6.57 11.17,5.23 12,4.03M18.92,8H15.97C15.65,6.75 15.19,5.55 14.59,4.44C16.43,5.07 17.96,6.34 18.92,8M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></svg>
|
After Width: | Height: | Size: 995 B |
1
public/assets/icons/webapp.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6,2H18A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M12,4A6,6 0 0,0 6,10C6,13.31 8.69,16 12.1,16L11.22,13.77C10.95,13.29 11.11,12.68 11.59,12.4L12.45,11.9C12.93,11.63 13.54,11.79 13.82,12.27L15.74,14.69C17.12,13.59 18,11.9 18,10A6,6 0 0,0 12,4M12,9A1,1 0 0,1 13,10A1,1 0 0,1 12,11A1,1 0 0,1 11,10A1,1 0 0,1 12,9M7,18A1,1 0 0,0 6,19A1,1 0 0,0 7,20A1,1 0 0,0 8,19A1,1 0 0,0 7,18M12.09,13.27L14.58,19.58L17.17,18.08L12.95,12.77L12.09,13.27Z" /></svg>
|
After Width: | Height: | Size: 538 B |
62
public/assets/js/app.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
function showReadMore(modal) {
|
||||||
|
|
||||||
|
// Get the modal
|
||||||
|
var divModal = document.getElementById('modal-' + modal);
|
||||||
|
|
||||||
|
divModal.style.display = "block";
|
||||||
|
|
||||||
|
// Get the <span> element that closes the modal
|
||||||
|
var close = document.getElementById('close-' + modal);
|
||||||
|
|
||||||
|
// When the user clicks the button, open the modal
|
||||||
|
|
||||||
|
// When the user clicks on <span> (x), close the modal
|
||||||
|
close.onclick = function () {
|
||||||
|
divModal.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the user clicks anywhere outside of the modal, close it
|
||||||
|
window.onclick = function (event) {
|
||||||
|
if (event.target == divModal) {
|
||||||
|
divModal.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFilter(element, filter) {
|
||||||
|
|
||||||
|
let filterListNav = document.getElementsByTagName('nav')
|
||||||
|
for (var i = 0; i < filterListNav.length; i++) {
|
||||||
|
for (var i2 = 0; i2 < filterListNav[i].children.length; i2++) {
|
||||||
|
if (!filterListNav[i].children[i2].classList.contains(filter)) {
|
||||||
|
filterListNav[i].children[i2].classList.remove('active')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
element.classList.toggle('active')
|
||||||
|
|
||||||
|
let filterList = document.getElementsByClassName('card')
|
||||||
|
let filterListLength = filterList.length
|
||||||
|
|
||||||
|
for (var i = 0; i < filterListLength; i++) {
|
||||||
|
if (element.classList.contains('active')) {
|
||||||
|
if (!filterList[i].classList.contains(filter)) {
|
||||||
|
filterList[i].style.opacity = .3
|
||||||
|
} else {
|
||||||
|
filterList[i].style.opacity = 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filterList[i].style.opacity = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTheme(e) {
|
||||||
|
let actualTheme = document.documentElement.getAttribute('data-theme');
|
||||||
|
if (actualTheme === null || actualTheme === 'light') {
|
||||||
|
document.documentElement.setAttribute('data-theme', 'dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.setAttribute('data-theme', 'light');
|
||||||
|
}
|
||||||
|
}
|
BIN
public/favicon.png
Normal file
After Width: | Height: | Size: 203 B |
0
public/imgs/big_favicons/.gitkeep
Normal file
0
public/imgs/favicons/.gitkeep
Normal file
0
public/imgs/screenshots/.gitkeep
Normal file
0
public/imgs/thumbs/.gitkeep
Normal file
83
public/index.php
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require '../vendor/autoload.php';
|
||||||
|
|
||||||
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
use KTH\App;
|
||||||
|
use KTH\HTMLGenerator\HTMLGenerator;
|
||||||
|
use Login\Login;
|
||||||
|
|
||||||
|
$defConfig['title'] = 'KT-HomePage';
|
||||||
|
$defConfig['desc'] = 'Dashboard de Knah Tsaeb';
|
||||||
|
$defConfig['favicon'] = 'favicon.png';
|
||||||
|
$defConfig['noAuth'] = ['128.0.0.1'];
|
||||||
|
$defConfig['public'] = false;
|
||||||
|
$defConfig['colorScheme'] = 'dark';
|
||||||
|
$defConfig['view'] = 'full';
|
||||||
|
|
||||||
|
$breadcrumbs = '';
|
||||||
|
|
||||||
|
$KTH = new App();
|
||||||
|
$config = $KTH->getConfig($defConfig);
|
||||||
|
|
||||||
|
if (isset($_GET['logout'])) {
|
||||||
|
$logout = Login::logOut();
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($config['public'] === false) {
|
||||||
|
if (!$KTH->canByPassAuth($defConfig['noAuth'])) {
|
||||||
|
if (!Login::isLogged()) {
|
||||||
|
if (file_exists('../data/users.yaml')) {
|
||||||
|
require('../template/default/login.php');
|
||||||
|
} else {
|
||||||
|
require('../template/default/register.php');
|
||||||
|
}
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_GET['settings'])) {
|
||||||
|
require('../template/default/settings.php');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_POST['settings'])) {
|
||||||
|
$KTH->saveUserConfig();
|
||||||
|
header("Location: /");
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($KTH->cacheExist()) {
|
||||||
|
echo file_get_contents('index.html');
|
||||||
|
} else {
|
||||||
|
if (file_exists('../data/services.yaml')) {
|
||||||
|
$services = Yaml::parseFile('../data/services.yaml');
|
||||||
|
} else {
|
||||||
|
$services = [0 => [
|
||||||
|
'title' => 'Wikipedia',
|
||||||
|
'screenshot' => 'wikipedia.png',
|
||||||
|
'favicon' => 'wikipedia.png',
|
||||||
|
'link' => 'https://en.wikipedia.org/wiki/Dashboard_(computing)',
|
||||||
|
'appHome' => 'https://www.mediawiki.org/wiki/MediaWiki',
|
||||||
|
'location' => 'web',
|
||||||
|
'desc' => 'Wikipedia, the free encyclopedia',
|
||||||
|
'type' => 'webapp'
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
$generator = new HTMLGenerator($services);
|
||||||
|
$userDoc = $generator->genUserDoc();
|
||||||
|
$menuData = $KTH->makeMenu($services);
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
require('../template/default/header.php');
|
||||||
|
require('../template/default/titleBar.php');
|
||||||
|
require('../template/default/nav.php');
|
||||||
|
require('../template/default/content.php');
|
||||||
|
require('../template/default/footer.php');
|
||||||
|
|
||||||
|
$out = ob_get_contents();
|
||||||
|
file_put_contents('index.html', $out);
|
||||||
|
}
|
70
template/default/content.php
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<div class="card-container <?= $config['view']; ?>">
|
||||||
|
<?php foreach ($services as $service) : ?>
|
||||||
|
<div class="card <?= $service['type'] . ' ' . $service['location']; ?>">
|
||||||
|
<?php if ($config['view'] !== 'icons') : ?>
|
||||||
|
<a href="<?= $service['link']; ?>" rel="noopener noreferrer" target="_blank">
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($config['view'] === 'full') : ?>
|
||||||
|
<img class="thumb" src="<?= $KTH->returnImg($service['screenshot'], 'thumbs'); ?>" alt="Thumbshot" />
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($config['view'] !== 'icons') : ?>
|
||||||
|
</a><?php endif; ?>
|
||||||
|
<div class="content">
|
||||||
|
<h3>
|
||||||
|
<a href="<?= $service['link']; ?>" rel="noopener noreferrer" target="_blank" title="<?= $service['title']; ?>" >
|
||||||
|
<?php if ($config['view'] === 'icons') : ?>
|
||||||
|
<img class="favicon" loading="lazy" src="<?= $KTH->returnImg($service['favicon'], 'big_favicons'); ?>" alt="Favicon" />
|
||||||
|
<?php else :; ?>
|
||||||
|
<img class="favicon" loading="lazy" src="<?= $KTH->returnImg($service['favicon'], 'favicons'); ?>" alt="Favicon" />
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($config['view'] !== 'icons') : ?><?= $service['title']; ?><?php endif; ?>
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
<?php if ($config['view'] !== 'icons') : ?>
|
||||||
|
<p class="readMore">
|
||||||
|
<a class="button" onclick="showReadMore('<?= md5($service['link']); ?>')">More info</a>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="modal-<?= md5($service['link']); ?>" class="modal">
|
||||||
|
<!-- Modal content -->
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span class="close" id="close-<?= md5($service['link']); ?>">×</span>
|
||||||
|
<h2>
|
||||||
|
<img class="favicon" loading="lazy" src="<?= $KTH->returnImg($service['favicon'], 'favicons'); ?>" alt="Favicon" height="32px" />
|
||||||
|
<?= $service['title']; ?>
|
||||||
|
</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Software : <a href="<?= $service['appHome']; ?>" target="_blank"><?= $service['appHome']; ?></a></li>
|
||||||
|
<li>Installation type : <?= $service['type']; ?></li>
|
||||||
|
<li>Machine : <?= $service['location']; ?></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p><?= $service['desc']; ?></p>
|
||||||
|
<p class="">
|
||||||
|
<a href="<?= $service['link']; ?>" rel="noopener noreferrer" target="_blank">
|
||||||
|
<img loading="lazy" class="screenshot" src="<?= $KTH->returnImg($service['screenshot'], 'screenshots'); ?>" alt="Screenshot" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
|
||||||
|
<div id="modal-userDoc" class="modal">
|
||||||
|
<!-- Modal content -->
|
||||||
|
<div class="modal-content userDoc">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span class="close" id="close-userDoc">×</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<?= $userDoc; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
7
template/default/footer.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<script src="assets/js/app.js"></script>
|
||||||
|
<?php if (file_exists('assets/js/user.js')) : ?>
|
||||||
|
<script src="assets/js/user.js"></script>
|
||||||
|
<?php endif; ?>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
18
template/default/header.php
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html data-theme="<?= $config['colorScheme']; ?>">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
<meta name="description" content="<?= $config['desc']; ?>">
|
||||||
|
<meta name="color-scheme" content="<?= $config['colorScheme']; ?>">
|
||||||
|
<title><?= $config['title']; ?></title>
|
||||||
|
<link rel="stylesheet" href="assets/css/style.css" />
|
||||||
|
<?php if (file_exists('assets/css/user.css')) : ?>
|
||||||
|
<link rel="stylesheet" href="assets/css/user.css" />
|
||||||
|
<?php endif; ?>
|
||||||
|
<link rel="icon" type="image/png" href="<?= $config['favicon']; ?>">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
47
template/default/login.php
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Login\Login;
|
||||||
|
use Utils\CsrfToken;
|
||||||
|
use Utils\Debug;
|
||||||
|
|
||||||
|
$error = null;
|
||||||
|
$breadcrumbs = ' / Login';
|
||||||
|
|
||||||
|
$debug = new Debug;
|
||||||
|
|
||||||
|
if (!empty($_POST)) {
|
||||||
|
if (empty($_POST['login']) || empty($_POST['password'])) {
|
||||||
|
$error = 'Please fill login and password.';
|
||||||
|
} else {
|
||||||
|
$login = new Login;
|
||||||
|
if (CsrfToken::validateToken($_POST['token'])) {
|
||||||
|
if ($login->LogIn($_POST['login'], $_POST['password'])) {
|
||||||
|
header('Location: index.php');
|
||||||
|
} else {
|
||||||
|
$error = 'Wrong password ar login.';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$error = 'Error 06 : Wrong token';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require 'header.php';
|
||||||
|
?>
|
||||||
|
<div class="titleBar">
|
||||||
|
<h1><?= $config['title']; ?> / Login</h1>
|
||||||
|
</div>
|
||||||
|
<form action="?" class="login" method="post">
|
||||||
|
<div class="alert" style="color: red;">
|
||||||
|
<?= $error; ?>
|
||||||
|
</div>
|
||||||
|
<label>Login</label>
|
||||||
|
<input type="text" name="login" required>
|
||||||
|
|
||||||
|
<label>Password</label>
|
||||||
|
<input type="password" name="password" required>
|
||||||
|
<input type="hidden" name="token" value="<?= CsrfToken::generateToken(); ?>">
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
13
template/default/nav.php
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<nav>
|
||||||
|
<span>Type d'installation</span>
|
||||||
|
<?php foreach ($menuData['type'] as $type) :; ?>
|
||||||
|
<a class="filter <?= $type; ?>" data-filter="<?= $type; ?>" onclick="toggleFilter(this, '<?= $type; ?>')"><img width="20px" height="20px" src="assets/icons/<?= $type; ?>.svg"> <?= $type; ?></a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<span>Machine</span>
|
||||||
|
<?php foreach ($menuData['location'] as $type) :; ?>
|
||||||
|
<a class="filter <?= $type; ?>" data-filter="<?= $type; ?>" onclick="toggleFilter(this, '<?= $type; ?>')"><img width="20px" height="20px" src="assets/icons/server.svg"> <?= $type; ?></a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</nav>
|
54
template/default/register.php
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Login\Login;
|
||||||
|
use Utils\CsrfToken;
|
||||||
|
use KTH\App;
|
||||||
|
|
||||||
|
$error = null;
|
||||||
|
$breadcrumbs = ' / Create user';
|
||||||
|
App::initializeDataDir();
|
||||||
|
|
||||||
|
if (!empty($_POST)) {
|
||||||
|
if (empty($_POST['login']) || empty($_POST['password']) || empty($_POST['role'])) {
|
||||||
|
$error = 'Please fill login, password and role.';
|
||||||
|
} else {
|
||||||
|
if (CsrfToken::validateToken($_POST['token'])) {
|
||||||
|
$login = new Login;
|
||||||
|
$addUser = $login->addUser($_POST['login'], $_POST['password'], $_POST['role']);
|
||||||
|
if ($addUser === true) {
|
||||||
|
header('Location: index.php');
|
||||||
|
} else {
|
||||||
|
$error = 'Error 02 - This user already exist';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$error = 'Error 07 : Wrong token';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require 'header.php';
|
||||||
|
?>
|
||||||
|
<div class="titleBar">
|
||||||
|
<h1><?= $config['title'] . $breadcrumbs; ?></h1>
|
||||||
|
</div>
|
||||||
|
<form action="?" class="login" method="post">
|
||||||
|
<div class="alert" style="color: red;">
|
||||||
|
<?= $error; ?>
|
||||||
|
</div>
|
||||||
|
<label>Login</label>
|
||||||
|
<input type="text" name="login" required>
|
||||||
|
|
||||||
|
<label>Password</label>
|
||||||
|
<input type="password" name="password" required>
|
||||||
|
|
||||||
|
<label>Rôle</label>
|
||||||
|
<select name="role" required>
|
||||||
|
<option value="user">User</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input type="hidden" name="token" value="<?= CsrfToken::generateToken(); ?>">
|
||||||
|
<button type="submit">Create user</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
45
template/default/settings.php
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Utils\CsrfToken;
|
||||||
|
use Utils\Select;
|
||||||
|
use Utils\Debug;
|
||||||
|
|
||||||
|
$error = null;
|
||||||
|
$breadcrumbs = ' / Settings';
|
||||||
|
|
||||||
|
require 'header.php';
|
||||||
|
require 'titleBar.php';
|
||||||
|
?>
|
||||||
|
<form action="index.php" class="login" method="post">
|
||||||
|
<div class="alert" style="color: red;">
|
||||||
|
<?= $error; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label for="title">Title</label>
|
||||||
|
<input type="text" name="title" id="title" value="<?= $config['title']; ?>">
|
||||||
|
|
||||||
|
<label for="colorScheme">Color scheme</label>
|
||||||
|
<select name="colorScheme" id="colorScheme">
|
||||||
|
<option value="light" <?= Select::isSelected('light', $config['colorScheme']); ?>>Light</option>
|
||||||
|
<option value="dark" <?= Select::isSelected('dark', $config['colorScheme']); ?>>Dark</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label for="view">View</label>
|
||||||
|
<select name="view" id="view">
|
||||||
|
<option value="full" <?= Select::isSelected('full', $config['view']); ?>>Full</option>
|
||||||
|
<option value="compact" <?= Select::isSelected('compact', $config['view']); ?>>Compact</option>
|
||||||
|
<option value="icons" <?= Select::isSelected('icons', $config['view']); ?>>Icons</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<p class="checkbox">
|
||||||
|
<label for="reimport">Reimport images and user files</label>
|
||||||
|
<input type="checkbox" name="reimport" id="reimport" value="1"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<input type="hidden" name="token" value="<?= CsrfToken::generateToken(); ?>">
|
||||||
|
<input type="hidden" name="settings" value="1" />
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
require('footer.php');
|
54
template/default/titleBar.php
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<div class="titleBar">
|
||||||
|
<div class="linkList">
|
||||||
|
<a title="Toggle light/dark mode" href="#" onclick="switchTheme();">
|
||||||
|
<svg fill="#000000" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" xml:space="preserve">
|
||||||
|
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||||
|
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||||
|
<g id="SVGRepo_iconCarrier">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M256,0C114.842,0,0,114.84,0,256s114.842,256,256,256s256-114.84,256-256S397.158,0,256,0z M322.225,451.558 c-20.797,7.062-43.071,10.894-66.225,10.894c-113.837,0-206.452-92.614-206.452-206.452S142.163,49.548,256,49.548 c23.154,0,45.429,3.832,66.226,10.894C266.612,107.439,231.226,177.657,231.226,256S266.612,404.561,322.225,451.558z"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg></a>
|
||||||
|
<a title="Setting" href="index.php?settings=1"><svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 363.715 363.715" xml:space="preserve">
|
||||||
|
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||||
|
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||||
|
<g id="SVGRepo_iconCarrier">
|
||||||
|
<g>
|
||||||
|
<path d="M236.25,222.275c0.865-3.233,0.421-6.608-1.252-9.506l-26.079-45.174c-2.232-3.864-6.393-6.267-10.862-6.267 c-2.186,0-4.347,0.582-6.249,1.681l-13.595,7.85c-5.525-4.053-11.5-7.526-17.834-10.332v-15.662 c0-6.908-5.621-12.526-12.527-12.526H95.688c-6.906,0-12.525,5.618-12.525,12.526v15.661c-6.335,2.806-12.309,6.28-17.835,10.333 l-13.595-7.849c-1.902-1.099-4.064-1.68-6.25-1.68c-4.468,0-8.629,2.401-10.861,6.266L8.542,212.768 c-1.673,2.899-2.118,6.274-1.253,9.507c0.867,3.232,2.939,5.934,5.836,7.605l13.557,7.826c-0.365,3.391-0.559,6.832-0.559,10.318 c0,3.486,0.193,6.928,0.559,10.319l-13.557,7.827c-2.898,1.672-4.969,4.373-5.836,7.606c-0.865,3.231-0.42,6.608,1.253,9.505 l26.079,45.174c2.232,3.865,6.394,6.266,10.861,6.266c2.186,0,4.348-0.58,6.25-1.68l13.596-7.849 c5.525,4.052,11.5,7.526,17.834,10.332v15.661c0,3.346,1.303,6.491,3.67,8.857c2.366,2.365,5.512,3.67,8.855,3.67h52.164 c6.906,0,12.527-5.62,12.527-12.527v-15.662c6.334-2.806,12.308-6.279,17.833-10.332l13.596,7.849c1.902,1.1,4.064,1.68,6.249,1.68 c4.47,0,8.63-2.4,10.862-6.266l26.079-45.174c1.673-2.897,2.117-6.273,1.252-9.505c-0.865-3.233-2.938-5.935-5.834-7.606 l-13.557-7.828c0.365-3.391,0.558-6.833,0.558-10.319c0-3.486-0.192-6.928-0.558-10.318l13.557-7.827 C233.313,228.209,235.385,225.508,236.25,222.275z M121.77,302.423c-30.043,0-54.396-24.354-54.396-54.397 c0-30.041,24.354-54.396,54.396-54.396s54.397,24.355,54.397,54.396C176.167,278.068,151.813,302.423,121.77,302.423z"></path>
|
||||||
|
<path d="M167.512,93.593c-0.572,2.14-0.277,4.374,0.83,6.29l17.256,29.892c1.479,2.559,4.231,4.146,7.188,4.146 c1.447,0,2.876-0.384,4.137-1.111l9.002-5.197c3.654,2.68,7.606,4.972,11.795,6.827v10.377c0,2.214,0.861,4.295,2.428,5.861 c1.566,1.566,3.647,2.427,5.86,2.427h34.517c4.57,0,8.29-3.718,8.29-8.288v-10.377c4.188-1.856,8.14-4.148,11.794-6.828 l9.004,5.198c1.258,0.728,2.688,1.111,4.135,1.111c2.957,0,5.711-1.588,7.188-4.146l17.256-29.892 c1.108-1.916,1.402-4.15,0.83-6.29c-0.574-2.139-1.944-3.926-3.861-5.033l-8.975-5.182c0.241-2.243,0.373-4.519,0.373-6.825 c0-2.306-0.132-4.581-0.373-6.825l8.975-5.181c1.917-1.107,3.287-2.895,3.861-5.034c0.572-2.139,0.277-4.372-0.83-6.29 l-17.256-29.892c-1.477-2.558-4.23-4.147-7.188-4.147c-1.447,0-2.877,0.385-4.135,1.113l-9.004,5.198 c-3.654-2.68-7.605-4.972-11.794-6.827V8.289c0-4.57-3.72-8.289-8.29-8.289h-34.517c-4.57,0-8.288,3.719-8.288,8.289v10.378 c-4.188,1.856-8.141,4.148-11.794,6.827l-9.003-5.198c-1.261-0.729-2.689-1.113-4.137-1.113c-2.956,0-5.709,1.59-7.188,4.147 l-17.256,29.892c-1.107,1.918-1.402,4.151-0.83,6.29c0.574,2.14,1.945,3.927,3.861,5.034l8.975,5.181 c-0.241,2.243-0.373,4.519-0.373,6.825c0,2.307,0.132,4.582,0.373,6.825l-8.975,5.182 C169.457,89.667,168.086,91.454,167.512,93.593z M243.266,40.558c19.881,0,35.996,16.116,35.996,35.995 s-16.115,35.995-35.996,35.995c-19.88,0-35.995-16.116-35.995-35.995S223.386,40.558,243.266,40.558z"></path>
|
||||||
|
<path d="M354.003,209.477l-6.179-3.567c0.167-1.544,0.258-3.111,0.258-4.699c0-1.588-0.091-3.154-0.258-4.699l6.179-3.567 c1.319-0.762,2.263-1.992,2.657-3.465c0.395-1.473,0.191-3.01-0.57-4.33l-11.88-20.576c-1.017-1.762-2.911-2.855-4.946-2.855 c-0.996,0-1.98,0.265-2.848,0.766l-6.197,3.578c-2.516-1.845-5.236-3.423-8.119-4.7v-7.144c0-3.145-2.56-5.706-5.705-5.706h-23.76 c-3.147,0-5.706,2.561-5.706,5.706v7.144c-2.884,1.277-5.603,2.855-8.119,4.7l-6.198-3.578c-0.866-0.501-1.851-0.766-2.847-0.766 c-2.035,0-3.931,1.093-4.946,2.855L252.94,185.15c-0.764,1.32-0.967,2.857-0.572,4.33c0.396,1.473,1.339,2.703,2.658,3.465 l6.18,3.567c-0.167,1.544-0.258,3.11-0.258,4.698c0,1.588,0.091,3.154,0.258,4.698l-6.18,3.567 c-1.319,0.761-2.263,1.99-2.658,3.464c-0.395,1.473-0.191,3.011,0.572,4.33l11.879,20.576c1.016,1.762,2.911,2.855,4.946,2.855 c0.996,0,1.98-0.266,2.847-0.766l6.198-3.578c2.516,1.845,5.235,3.422,8.119,4.7v7.144c0,1.523,0.593,2.957,1.671,4.034 c1.078,1.079,2.512,1.672,4.035,1.672h23.76c3.145,0,5.705-2.56,5.705-5.706v-7.144c2.883-1.277,5.604-2.855,8.119-4.7l6.197,3.578 c0.867,0.5,1.852,0.766,2.848,0.766c2.035,0,3.93-1.093,4.946-2.855l11.88-20.576c0.762-1.319,0.965-2.857,0.57-4.33 C356.266,211.467,355.322,210.237,354.003,209.477z M304.515,225.989c-13.686,0-24.778-11.095-24.778-24.778 c0-13.685,11.092-24.779,24.778-24.779c13.685,0,24.777,11.095,24.777,24.779C329.292,214.895,318.199,225.989,304.515,225.989z"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg></a>
|
||||||
|
<a title="Help" href="#" onclick="showReadMore('userDoc')">
|
||||||
|
<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52" enable-background="new 0 0 52 52" xml:space="preserve">
|
||||||
|
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||||
|
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||||
|
<g id="SVGRepo_iconCarrier">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M28.4,38h-5c-0.8,0-1.4-0.6-1.4-1.4v-1.5c0-4.2,2.7-8,6.7-9.4c1.2-0.4,2.3-1.1,3.2-2.1 c5-6,0.4-13.2-5.6-13.4c-2.2-0.1-4.3,0.7-5.9,2.2c-1.3,1.2-2.1,2.7-2.3,4.4c-0.1,0.6-0.7,1.1-1.5,1.1h-5c-0.9,0-1.6-0.7-1.5-1.6 c0.4-3.8,2.1-7.2,4.8-9.9c3.2-3,7.3-4.6,11.7-4.5c8.3,0.3,15.1,7.1,15.4,15.4c0.3,7-4,13.3-10.5,15.7c-0.9,0.4-1.5,1.1-1.5,2v1.5 C30,37.4,29.2,38,28.4,38z"></path>
|
||||||
|
</g>
|
||||||
|
<path d="M30,48.5c0,0.8-0.7,1.5-1.5,1.5h-5c-0.8,0-1.5-0.7-1.5-1.5v-5c0-0.8,0.7-1.5,1.5-1.5h5 c0.8,0,1.5,0.7,1.5,1.5V48.5z"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg></a>
|
||||||
|
<a title="Logout" href="index.php?logout=1">
|
||||||
|
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 96.943 96.943" xml:space="preserve">
|
||||||
|
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||||
|
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||||
|
<g id="SVGRepo_iconCarrier">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M61.168,83.92H11.364V13.025H61.17c1.104,0,2-0.896,2-2V3.66c0-1.104-0.896-2-2-2H2c-1.104,0-2,0.896-2,2v89.623 c0,1.104,0.896,2,2,2h59.168c1.105,0,2-0.896,2-2V85.92C63.168,84.814,62.274,83.92,61.168,83.92z"></path>
|
||||||
|
<path d="M96.355,47.058l-26.922-26.92c-0.75-0.751-2.078-0.75-2.828,0l-6.387,6.388c-0.781,0.781-0.781,2.047,0,2.828 l12.16,12.162H19.737c-1.104,0-2,0.896-2,2v9.912c0,1.104,0.896,2,2,2h52.644L60.221,67.59c-0.781,0.781-0.781,2.047,0,2.828 l6.387,6.389c0.375,0.375,0.885,0.586,1.414,0.586c0.531,0,1.039-0.211,1.414-0.586l26.922-26.92 c0.375-0.375,0.586-0.885,0.586-1.414C96.943,47.941,96.73,47.433,96.355,47.058z"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg></a>
|
||||||
|
</div>
|
||||||
|
<h1><?= $config['title'] . $breadcrumbs; ?></h1>
|
||||||
|
</div>
|