This commit is contained in:
Stepan 2025-06-02 13:34:17 +02:00
commit 4691f92900
165 changed files with 23947 additions and 0 deletions

33
.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
# Composer vendor directory
/vendor/
# ОС/IDE/Editor
.DS_Store
Thumbs.db
# PHPStorm / JetBrains
.idea/
# VSCode
.vscode/
# Cache Composer
composer.phar
# Cache and Logs
*.log
*.cache
# PHPUnit
/phpunit.xml
/phpunit.xml.dist
/test-results/
# Coverage reports
coverage/
# Build artifacts
/build/
# Production config
config.php

269
README.md Normal file
View File

@ -0,0 +1,269 @@
# FridgeBites Project
The FridgeBites school project is a web-based platform designed to ....
The FridgeBites was developed almost entirely from scratch using PHP, without relying on any frameworks.
> The FridgeBites project was developed as part of a coursework assignment for the [Subotica Tech - College of Applied Sciences](https://www.vts.su.ac.rs/) program, and it is not intended for commercial use.
* [Libraries](#libraries)
* [Installing](#installing)
* [Configuration](#configuration)
* Server
* [Project structure](#project-structure)
* [MVC (Model-View-Controller)](#mvc-model-view-controller)
* [BaseModel](#basemodel)
* [BaseController and Page Rendering](#basecontroller-and-page-rendering)
* [Router](#router-and-url-handling)
* [Ajax](#ajax)
## Libraries
The website is built entirely from scratch using pure PHP, without relying on any frameworks. This was done as an experimental project to enhance personal skills. Despite this, some libraries were used to add specific functionality:
- [\[PHP\] PHPMailer](https://github.com/PHPMailer/PHPMailer): A library for sending emails, used for password recovery functionality.
- [\[JS\] Swiper.js](https://github.com/nolimits4web/swiper): A highly customizable library for creating sliders and carousels.
- [\[JS\] Toastify.js](https://github.com/apvarun/toastify-js): A lightweight library for creating beautiful and customizable toast notifications.
## Installing
1. Install PHP dependencies using Composer:
```
composer install
```
### Configuration
The configuration template file is located in the root directory of the project as `config-default.php`.
You need to make a copy of this file named `config.php` and update it with the appropriate settings for your project.
## Project Structure
The project is organized as follows:
- `apps/` - Contains all mini-applications.
- `{app_name}/` - A single mini-application.
- `Templates/` - Page views.
- `components/` - Views for individual components.
- `Models/` - Models for creating and managing objects.
- `Controllers/` - Controllers manage the logic and coordinate between Models/ and Templates/.
- `components.php` *(optional)* - Functions for handling the logic and rendering of specific components.
- `functions.php` *(optional)* - Utility functions specific to the application.
- `urls.php` - Defines an array of routes that map to controllers for rendering pages. These routes are later linked to the root `urls.php`.
- `assets/` - Static files such as JavaScript, CSS, and images.
- `includes/` - Core PHP scripts and libraries.
- `media/` - Uploaded files stored on the server.
- `.htaccess` - Configuration file for the web server (e.g., URL rewriting).
- `config-default.php` - A template configuration file. Can be used as a base to create `config.php`.
- `config.php` - The main configuration file, containing constants and key settings.
- `db.php` - Functions for interacting with the database.
- `functions.php` - Global utility functions used across the project.
- `index.php` - The entry point for the site. Loads the router, configuration files, and more.
- `urls.php` - Combines all `urls.php` files from mini-applications into a unified router.
## MVC (Model-View-Controller)
To interact with database objects, we use classes inherited from `BaseModel`. For rendering pages, all logic is implemented in controllers inherited from `BaseController`, with a specified path to the corresponding view.
All views are connected via the router.
### BaseModel
`use Lycoreco\Includes\Model\BaseModel;`
The `BaseModel` class is responsible for managing data stored and processed on the server. All other models inherit from this base class.
#### To create a model, the following fields need to be defined:
- `public $field_{column_name}`: Public fields corresponding to the current table's columns, which will later be accessible.
- `static protected $table_name`: The name of the database table.
- `static protected $table_fields`: Specifies the fields and their data types (`int`, `string`, `bool`, or `DateTime`):
```php
static protected $table_fields = [
'id' => 'int',
'user_id' => 'int',
'recovery_slug' => 'string',
'created_at' => 'DateTime',
'is_used' => 'bool'
];
```
- `static protected $additional_fields`: Additional fields to be joined with the current model. The field names will later be the same as the variables. (Field names from the main table are prefixed with `obj.{column_name}`):
```php
static protected $additional_fields = [
[
"field" => [
"tb2.name AS platform_title",
"tb2.icon_html AS platform_icon",
"tb2.background_color AS platform_background"
],
"join_table" => "taxonomies tb2 ON tb2.id = obj.platform_id"
]
];
```
- `static protected $search_fields`: A list of fields to use for search queries. (Field names from the main table are prefixed with `obj.{column_name}`):
```php
static protected $search_fields = ['obj.title'];
```
#### Methods in BaseModel:
* `static init_table()`: Creates a new table for the model (used only in `setup.php`). Can use `db_query()` internally.
* `static filter(fields, sort_by, count, field_relation, offset, search, additional_fields)`: Retrieves an array of objects based on the specified filter.
```php
$objects = CustomModel::filter(
[
// Field filters
[
'name' => 'obj.sales', // Field name
'type' => '>', // Comparison type (=, >, <, >=, <=, LIKE, IN). Default is "="
'value' => 20, // Value to compare
'is_having' => false // Search in WHERE or HAVING (not tied to the model)
]
],
['-obj.sales', 'obj.title'], // Sorting (prefix with "-" for descending order)
10, // Number of objects to retrieve (COUNT)
'AND', // Combine WHERE/HAVING with AND or OR
0, // Offset
'detroit', // Search value
[] // Additional fields (same as `$additional_fields`)
);
```
- `static get(fields, sort_by)`: Similar to `filter()`, but retrieves only the first matching model. Returns false if none is found.
- `static count(fields, search)`: Gets the count of objects matching the filter.
- `is_saved()`: Checks if the current object is saved in the database.
- `delete()`: Deletes the object from the database.
- `save()`: Saves the object to the database (either updates or creates a new record).
---
- `after_save()`: Additional functionality to execute after saving (optional).
- `valid()`: Validates the data. If there are issues, returns an array of string messages.
### BaseController and Page Rendering
`use Lycoreco\Include\BaseController;`
For managing a specific page, we use a class that extends the `BaseController`, located at `/includes/BaseController.php`.
Within this class, we can implement and modify the following methods and fields:
* `$template_name` - The path to the view. Typically located in the `Templates/` folder of the current application.
* `$allow_role` - Specifies which user role is permitted to access the view (`null`, `"user"`, or `"admin"`).
* `get_model()` - Retrieves a specific model associated with the page (e.g., `SinglePostController`).
* `get_context_data()` - Passes an array of variables in `$context` to the view.
* `distinct()` - Provides additional functionality before rendering the view.
* `post()` - Executes only if the page is accessed via the POST method.
### Router and URL Handling
The main `Router` class is located at `includes/Routing/Router.php`. This class serves as the core for routing and displaying the appropriate controller based on the path.
#### Classes Overview
`namespace Lycoreco\Includes\Routing;`
- **`Router`**: The primary class used for determining which controller to render based on the requested path.
- **`Path`**: A helper class where the path to a controller and the link name are specified.
- **Exception Classes**: Used for handling errors, including:
- `PageError` (general, 500)
- `PermissionDenied403`
- `Unauthorized401`
- `BadRequest400`
- `NotFound404`
#### Defining Routes
In mini-applications, routes are defined in a `urls.php` file, where paths and controllers are specified. Variables like `slug` or `id` can be passed using placeholders such as `[:string]` or `[:int]`.
```php
use Lycoreco\Apps\MyApp\Controllers;
$my_app_urls = [
// path, controller, name
new Path('/item/[:string]', new Controllers\SingleController(), 'single'),
new Path('/home', new Controllers\HomeController(), 'home'),
];
```
#### Integrating Routes into the Main Application
The array of mini-application routes is then added to the **root directory**'s `urls.php` file, specifying the group name. Additionally, error controllers (e.g., "default", 404, 500) can also be defined:
```php
use Lycoreco\Includes\Routing\Router;
require APPS_PATH . '/my_app/urls.php';
Router::includes($my_app_urls, "myapp");
Router::set_error_controller('default', new ErrorController());
```
## Ajax
The website supports AJAX functionality through a dedicated mini-application called `ajax` and an `AjaxController` (accessible via `/ajax`). This setup enables features such as:
* Adding items to the wishlist
* Real-time search
* Adding items to the cart
* Viewing the cart
### How It Works
1. **Data Processing:** In the `AjaxController`, data is sent via the request body in JSON format. The data is then processed by a function specified in the action attribute of the JSON object.
2. **Creating a Custom Action:** To create a custom action, you need to add a new function in `apps/Ajax/ajax-actions.php`.
* The function must be named `ajax_{your_action_name}`.
* It takes an associative array from the args attribute of the JSON object as its argument.
* The function should return an array (either associative or indexed), which will be encoded into JSON for the client.
* Error Handling: To trigger an error, return `get_ajax_error($message, $error_code)`
3. **Frontend Interaction:** On the frontend, you can use the `fetch()` API or an existing `sendAjax` function. The sendAjax function accepts the following parameters:
* `action` — The name of the action to be executed.
* `args` — An object containing arguments to be passed to the action.
* `onLoad` — A callback function executed when the request starts.
* `onSuccess` — A callback function executed upon a successful response.
* `onError` — A callback function executed if an error occurs.
### Example: Custom Action in PHP (backend) and Javascript (frontend)
**PHP (backend)**
```php
// Example action: Return a number with a message
function ajax_my_number($args) {
$result = array();
// Validate the input
$num = $args['number'];
if (!isset($num) || !is_numeric($num)) {
return get_ajax_error("Invalid number", 400); // Return error for invalid input
}
// Prepare the response
$result['message'] = "My number is " . $num;
return json_encode($result, JSON_PRETTY_PRINT); // Return JSON-encoded result
}
```
**Javascript (frontend)**
```js
sendAjax(
// action
"my_number",
// args
{
number: 5
},
// onLoad
() => {
console.log('loading...');
},
// onSuccess
(result) => {
console.log(result.message);
},
// onError
(error) => {
console.log(error.message);
});
```

View File

@ -0,0 +1,20 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers\Abstract;
use Lycoreco\Includes\BaseController;
abstract class AdminBaseController extends BaseController
{
protected $allow_role = 'admin';
public function get_context_data()
{
$context = array();
// Add admin components (the_admin_header, ...)
require_once APPS_PATH . '/Admin/components.php';
return $context;
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers\Abstract;
define('ADMIN_MAX_ELEMENTS', value: 20);
abstract class AdminListController extends AdminBaseController
{
protected $template_name = APPS_PATH . '/Admin/Templates/list-view.php';
// Model related with BaseModel
protected $model_сlass_name;
/**
* Fields to show in the table
* array(
* "title_1" => "field_name_1",
* "title_2" => "field_name_2",
* )
* @var array
*/
protected $table_fields;
protected $single_router_name;
protected $create_router_name;
protected $verbose_name = "object";
protected $verbose_name_multiply = "objects";
protected $sort_by = array();
public function custom_filter_fields()
{
return array();
}
public function get_context_data()
{
$context = parent::get_context_data();
$search = $_GET['s'] ?? '';
$page = 1;
if (isset($_GET['page'])) {
$page = (int)$_GET['page'];
}
// Get objects by sort and search
$context['objects'] = $this->model_сlass_name::filter(
$this->custom_filter_fields(),
$this->sort_by,
ADMIN_MAX_ELEMENTS,
'OR',
calc_page_offset(ADMIN_MAX_ELEMENTS, $page),
$search
);
$context['page'] = $page;
$context['elem_per_page'] = ADMIN_MAX_ELEMENTS;
$context['count'] = $this->model_сlass_name::count(
$this->custom_filter_fields(),
$search
);
// Get fields to display in the table
$table_fields = array();
foreach ($this->table_fields as $title => $field) {
// Check field is function or not
$is_func = false;
if (str_ends_with($field, "()")) {
$is_func = true;
$field = substr($field, 0, -strlen('()'));
}
$table_fields[] = array(
'field_title' => $title,
'is_func' => $is_func,
'field_name' => $field
);
}
$context['table_fields'] = $table_fields;
// Verbose names
$context['verbose_name'] = $this->verbose_name;
$context['verbose_name_multiply'] = $this->verbose_name_multiply;
// Router name to link every object and link to create object
$context['single_router_name'] = $this->single_router_name;
$context['create_router_name'] = $this->create_router_name;
return $context;
}
}

View File

@ -0,0 +1,186 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers\Abstract;
use Lycoreco\Includes\Model\{
ValidationError,
CustomDateTime
};
use Lycoreco\Includes\Routing\HttpExceptions;
abstract class AdminSingleController extends AdminBaseController
{
protected $template_name = APPS_PATH . '/Admin/Templates/single-view.php';
protected $model_сlass_name;
/**
* Router name to redirect new object
* @var string
*/
protected $object_router_name;
/**
* Relation form fields and model fields
* array(
* [
* 'model_field' => 'username',
* 'input_type' => 'text',
* 'dynamic_save' => false, // Default is true. If false, field won't save automatically. You can use before_save()
* 'input_label' => 'My Username', // If not set, default is 'model_field' with Capitalize
* 'input_attrs' => ['disabled', 'required', 'maxlength="50"'], // Default empty
* 'input_values' => [ ['value_1', 'My Value 1'], ... ] // For select field.
* ]
* )
* @var array
*/
protected $fields = array();
protected $field_title = 'field_id';
protected $edit_title_template = 'Edit [:verbose] "[:field]"';
protected $can_save = true;
/**
* Function names with $object attribute.
* ['the_last_feedbacks']
* @var array
*/
protected $component_widgets = array();
protected $verbose_name = 'object';
/**
* Show model in admin panel
* @param bool $is_new is new model (Create)?
*/
public function __construct($is_new = false)
{
$this->context['is_new'] = $is_new;
}
/**
* Custom model object
* @return mixed
*/
protected function get_model()
{
if (isset($this->__model))
return $this->__model;
if ($this->context['is_new']) {
$this->__model = new $this->model_сlass_name();
} else {
$this->__model = $this->model_сlass_name::get(
array(
[
'name' => 'obj.id',
'type' => '=',
'value' => $this->url_context['url_1']
]
)
);
if (empty($this->__model))
throw new HttpExceptions\NotFound404();
}
return $this->__model;
}
public function get_context_data()
{
$context = parent::get_context_data();
$object = $this->get_model();
$context['object'] = $object;
$context['verbose_name'] = $this->verbose_name;
$context['edit_title'] = str_replace('[:verbose]', $this->verbose_name, $this->edit_title_template);
if ($object->is_saved())
$context['edit_title'] = str_replace('[:field]', $object->{$this->field_title}, $context['edit_title']);
$context['object_table_name'] = $object->get_table_name();
$context['fields'] = $this->fields;
$context['can_save'] = $this->can_save;
$context['component_widgets'] = $this->component_widgets;
return $context;
}
/**
* Some code for updating object before save
* @param mixed $object
* @return void
*/
protected function before_save(&$object) {}
protected function save(&$object)
{
$object->save();
}
protected function post()
{
$object = $this->get_model();
foreach ($this->fields as $field) {
// Exceptions
if (gettype($field) == 'string')
continue;
if (isset($field['dynamic_save']))
if ($field['dynamic_save'] === false)
continue;
// Get value from fields
$field_value = null;
switch ($field['input_type']) {
case 'checkbox':
$field_value = $_POST[$field['model_field']] ? true : false;
break;
case 'image':
$file = $_FILES[$field['model_field']];
if (isset($file)) {
$path = upload_file($file, $this->model_сlass_name . '/', 'image');
if (!empty($path)) {
$field_value = $path;
}
}
break;
case 'datetime-local':
$datetime_text = $_POST[$field['model_field']] ?? null;
$datetime = new CustomDateTime();
if ($datetime_text)
$datetime = new CustomDateTime($datetime_text);
$field_value = $datetime;
break;
default:
$field_value = $_POST[$field['model_field']] ?? null;
break;
}
// Set value to object
$object->{'field_' . $field['model_field']} = $field_value;
}
try {
$this->before_save($object);
$this->save($object);
// If object is new, redirect to his own page
if ($this->context['is_new']) {
$url = get_permalink($this->object_router_name, [$object->get_id()]);
redirect_to($url);
}
$this->context['success_message'] = "The " . $this->verbose_name . " has been saved";
} catch (ValidationError $ex) {
$this->context['error_form'] = $ex;
} catch (\Exception $ex) {
if (DEBUG_MODE)
$this->context['error_message'] = $ex->getMessage();
else
$this->context['error_message'] = "Unexpected Error";
}
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers;
class AdminBanController extends Abstract\AdminSingleController
{
protected $model_сlass_name = "Lycoreco\Apps\Users\Models\BanlistModel";
protected $field_title = 'field_created_at';
protected $object_router_name = 'admin:ban';
protected $verbose_name = "ban";
protected $fields = array(
[
'model_field' => 'user_id',
'input_type' => 'number',
'dynamic_save' => false,
'input_label' => 'User id',
'input_attrs' => ['required', 'disabled']
],
[
'model_field' => 'reason',
'input_type' => 'text',
'input_label' => 'Reason',
],
[
'model_field' => 'end_at',
'input_type' => 'datetime-local',
'input_label' => 'End at',
'input_attrs' => ['required']
],
[
'model_field' => 'created_at',
'input_type' => 'text',
'dynamic_save' => false,
'input_label' => 'Created at',
'input_attrs' => ['disabled']
],
);
protected function save(&$object)
{
$is_new = !$object->is_saved();
$object->save();
if($is_new)
$object->send_email_notification();
}
protected function get_model()
{
$model = parent::get_model();
if($this->context['is_new'])
$model->field_user_id = (int)$this->url_context['url_1'];
return $model;
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers;
class AdminBanListController extends Abstract\AdminListController
{
protected $model_сlass_name = "Lycoreco\Apps\Users\Models\BanlistModel";
protected $table_fields = array(
'Reason' => 'field_reason',
'Created at' => 'field_created_at',
'End at' => 'field_end_at',
'Active?' => 'is_active()',
);
protected $single_router_name = 'admin:ban';
protected $verbose_name = "ban";
protected $verbose_name_multiply = "banlist";
protected $sort_by = ['-obj.end_at'];
public function custom_filter_fields()
{
$user_id = $this->url_context['url_1'];
return array(
[
'name' => 'obj.user_id',
'type' => '=',
'value' => $user_id
]
);
}
}

View File

@ -0,0 +1,94 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers;
use Lycoreco\Includes\Routing\HttpExceptions;
use Lycoreco\Apps\Users\Models\UserModel;
/**
* Controller to delete every object model using className and id
*/
class AdminDeleteController extends Abstract\AdminBaseController
{
protected $template_name = APPS_PATH . '/Admin/Templates/delete.php';
/**
* @return null|UserModel
*/
protected function get_model()
{
if (isset($this->__model))
return $this->__model;
$id = $this->url_context['url_2'];
$model_class = '';
switch ($this->url_context['url_1']) {
case 'users':
$model_class = "Lycoreco\Apps\Users\Models\UserModel";
break;
case 'user-banlist':
$model_class = "Lycoreco\Apps\Users\Models\BanlistModel";
break;
default:
return null;
}
return $model_class::get(array(
[
'name' => 'obj.id',
'type' => '=',
'value' => $id
]
));
}
public function get_context_data()
{
$context = parent::get_context_data();
$model = $this->get_model();
if (empty($model))
throw new HttpExceptions\NotFound404();
// Display field to show what's model
$field = '';
$back_url = '';
switch ($this->url_context['url_1']) {
case 'users':
$back_url = get_permalink('admin:user', [$model->get_id()]);
$field = 'field_username';
break;
case 'user-banlist':
$back_url = get_permalink('admin:ban', [$model->get_id()]);
$field = 'field_reason';
break;
}
$context['back_url'] = $back_url;
$context['model'] = $model;
$context['field'] = $field;
return $context;
}
protected function post()
{
$model = $this->get_model();
if (empty($model))
throw new HttpExceptions\NotFound404();
$model->delete();
// Redirect after delete
$type_model = $this->url_context['url_1'];
switch ($type_model) {
case 'users':
$link = get_permalink('admin:user-list');
break;
default:
$link = get_permalink('admin:home');
break;
}
redirect_to($link);
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers;
class AdminHomeController extends Abstract\AdminBaseController
{
protected $template_name = APPS_PATH . '/Admin/Templates/home.php';
public function get_context_data()
{
$context = parent::get_context_data();
$context['last_orders'] = [];
$datetime_month_ago = new \DateTime();
$datetime_month_ago->modify("-1 month");
return $context;
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers;
use Lycoreco\Includes\Model\ValidationError;
use Lycoreco\Apps\Users\Models\UserModel;
class AdminUserController extends Abstract\AdminSingleController
{
protected $model_сlass_name = "Lycoreco\Apps\Users\Models\UserModel";
protected $field_title = 'field_username';
protected $verbose_name = 'user';
protected $object_router_name = 'admin:user';
protected $component_widgets = ['the_user_banlist'];
protected $fields = array(
[
'model_field' => 'username',
'input_type' => 'text',
'input_attrs' => ['required']
],
[
'model_field' => 'email',
'input_type' => 'email',
'input_label' => 'E-mail',
'input_attrs' => ['required']
],
[
'model_field' => 'password',
'input_type' => 'text',
'dynamic_save' => false,
'input_attrs' => ['required']
],
[
'model_field' => 'is_admin',
'input_type' => 'checkbox',
'input_label' => 'Is admin'
],
[
'model_field' => 'is_active',
'input_type' => 'checkbox',
'input_label' => 'Is active'
],
[
'model_field' => 'register_at',
'input_type' => 'text',
'dynamic_save' => false,
'input_label' => 'Register at',
'input_attrs' => ['disabled']
],
'<hr>',
[
'model_field' => 'fname',
'input_type' => 'text',
'input_label' => 'First name',
'input_attrs' => ['required']
],
[
'model_field' => 'lname',
'input_type' => 'text',
'input_label' => 'Last name',
'input_attrs' => ['required']
]
);
/**
* Some code for updating object before save
* @param UserModel $object
* @return void
*/
protected function before_save(&$object)
{
$password = $_POST['password'] ?? '';
// If password doesn't changed
if ($password === $object->field_password && $this->context['is_new'] == false)
return;
$result_valid = UserModel::valid_password($password);
// If password is valid - hash password and save to object
if ($result_valid === true) {
$password_hash = UserModel::password_hash($password);
$object->field_password = $password_hash;
}
// if not, throw ValidationError
else {
throw new ValidationError($result_valid);
}
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers;
class AdminUserListController extends Abstract\AdminListController
{
protected $model_сlass_name = "Lycoreco\Apps\Users\Models\UserModel";
protected $table_fields = array(
'Username' => 'field_username',
'Public Name' => 'get_public_name()',
'E-mail ' => 'field_email',
'Status ' => 'get_role()',
'Banned' => 'is_banned()'
);
protected $single_router_name = 'admin:user';
protected $create_router_name = 'admin:user-new';
protected $verbose_name = "user";
protected $verbose_name_multiply = "users";
}

View File

@ -0,0 +1,4 @@
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,67 @@
<?php
use Lycoreco\Includes\Routing\Router;
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo get_title_website($title); ?></title>
<link rel="shortcut icon" type="image/png" href="<?php echo ASSETS_PATH . '/favicon.png' ?>">
<!-- Google fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Play:wght@400;700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
<!-- Google fonts/ -->
<!-- Font Awesome -->
<link href="<?php echo ASSETS_PATH . '/fontawesome-free-6.6.0-web/css/fontawesome.min.css' ?>" rel="stylesheet" />
<link href="<?php echo ASSETS_PATH . '/fontawesome-free-6.6.0-web/css/brands.min.css' ?>" rel="stylesheet" />
<link href="<?php echo ASSETS_PATH . '/fontawesome-free-6.6.0-web/css/solid.min.css' ?>" rel="stylesheet" />
<!-- Font Awesome/ -->
<link rel="stylesheet" href="<?php echo ASSETS_PATH . '/css/reset.css' ?>">
<link rel="stylesheet" href="<?php echo ASSETS_PATH . '/css/style.css' ?>">
<link rel="stylesheet" href="<?php echo ASSETS_PATH . '/css/admin.css' ?>">
</head>
<body>
<header class="header-admin">
<div class="logo">
FridgeBites Admin
</div>
<div class="header-admin__control">
<div class="username">
Hello, <span><?php echo CURRENT_USER->field_username ?></span>
</div>
<div class="links">
<a href="<?php the_permalink('index:home') ?>">View site</a>
|
<a href="<?php the_permalink('users:logout') ?>">Log Out</a>
</div>
</div>
</header>
<div class="wrapper-admin">
<aside class="admin-sidebar">
<a href="<?php the_permalink('admin:product-new') ?>" class="btn btn-primary"><i class="fa-solid fa-plus"></i> New Product</a>
<hr>
<ul class="admin-sidebar__list">
<li>
<a class="<?php echo Router::$current_router_name == 'admin:home' ? "active" : "" ?>" href="<?php the_permalink('admin:home') ?>">
<i class="fa-solid fa-house"></i> Dashboard
</a>
</li>
<li>
<a class="<?php echo Router::$current_router_name == 'admin:user-list' ? "active" : "" ?>" href="<?php the_permalink('admin:user-list') ?>">
<i class="fa-solid fa-users"></i> Users
</a>
</li>
</ul>
</aside>
<div class="wrapper-admin__content">

View File

@ -0,0 +1,13 @@
<?php the_admin_header('Dashboard') ?>
<div class="admin-container delete">
<h1 class="p-title">Delete Object</h1>
<div class="meta-text">Do you really want to delete "<?php echo $context['model']->{$context['field']} ?>"(<?php echo $context['model']->get_id() ?>) from the "<?php echo $context['model']->get_table_name() ?>" table?</div>
<form method="post" class="btn-control">
<a href="<?php echo $context['back_url'] ?>" class="btn btn-primary">Back</a>
<button type="submit" class="btn btn-danger">Delete</button>
</form>
</div>
<?php the_admin_footer() ?>

View File

@ -0,0 +1,93 @@
<?php the_admin_header('Dashboard') ?>
<div class="admin-container">
<section>
<h2>Stats per month</h2>
<div id="dashboard-stats">
<div class="dashboard-stats__item top-sales">
<div class="icon">
<i class="fa-solid fa-bag-shopping"></i>
</div>
<div class="info">
<div class="label">Total sales</div>
<div class="value">0$</div>
</div>
</div>
<div class="dashboard-stats__item profit">
<div class="icon">
<i class="fa-solid fa-money-bill-trend-up"></i>
</div>
<div class="info">
<div class="label">Profit</div>
<div class="value">0$</div>
</div>
</div>
<div class="dashboard-stats__item orders">
<div class="icon">
<i class="fa-solid fa-cart-shopping"></i>
</div>
<div class="info">
<div class="label">Orders</div>
<div class="value">0</div>
</div>
</div>
<div class="dashboard-stats__item new-users">
<div class="icon">
<i class="fa-solid fa-user"></i>
</div>
<div class="info">
<div class="label">New users</div>
<div class="value">0</div>
</div>
</div>
</div>
</section>
<section>
<h2>Quick tools</h2>
<div id="quicktools">
<a href="<?php the_permalink('admin:product-new') ?>" class="btn">
<i class="fa-solid fa-plus"></i> New Product
</a>
<form action="<?php the_permalink('admin:product-list') ?>" method="get">
<div class="input">
<input type="text" name="s" placeholder="Search for products">
<button type="submit"><i class="fa-solid fa-magnifying-glass"></i></button>
</div>
</form>
</div>
</section>
<h2>Latest orders</h2>
<table class="admin-table">
<thead>
<tr>
<th>Order number</th>
<th>Method</th>
<th>Total price</th>
<th>Buyer</th>
<th>Created at</th>
</tr>
</thead>
<tbody>
<?php foreach ($context['last_orders'] as $order): ?>
<tr>
<td><a href="<?php the_permalink('admin:order', [$order->get_id()]) ?>"><?php echo $order->field_order_number ?></a></td>
<td><?php echo $order->field_method ?></td>
<td><?php echo $order->get_total_price() ?></td>
<td><?php echo $order->get_buyer_username() ?></td>
<td><?php echo $order->field_created_at ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php the_admin_footer() ?>

View File

@ -0,0 +1,71 @@
<?php the_admin_header(ucfirst($context['verbose_name_multiply'])) ?>
<div class="admin-container">
<h1 class="p-title"><?php echo ucfirst($context['verbose_name_multiply']) ?></h1>
<section>
<div id="quicktools">
<?php if(isset($context['create_router_name'])): ?>
<a href="<?php the_permalink($context['create_router_name']) ?>" class="btn">
<i class="fa-solid fa-plus"></i> New <?php echo ucfirst($context['verbose_name']) ?>
</a>
<?php else: ?>
<span></span>
<?php endif; ?>
<form method="get">
<div class="input">
<input type="text" name="s" placeholder="Search for <?php echo $context['verbose_name_multiply'] ?>" value="<?php echo isset($_GET['s']) ? $_GET['s'] : '' ?>">
<button type="submit"><i class="fa-solid fa-magnifying-glass"></i></button>
</div>
</form>
</div>
</section>
<section class="admin-list">
<table class="admin-table">
<thead>
<tr>
<?php foreach($context['table_fields'] as $field): ?>
<th><?php echo $field['field_title'] ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php foreach($context['objects'] as $object): ?>
<tr>
<?php for($i = 0; $i < count($context['table_fields']); $i++) {
$field_value = '';
if($context['table_fields'][$i]['is_func'])
$field_value = $object->{$context['table_fields'][$i]['field_name']}();
else
$field_value = $object->{$context['table_fields'][$i]['field_name']};
?>
<td>
<?php if($i == 0): ?>
<a href="<?php the_permalink($context['single_router_name'], [$object->get_id()]) ?>"><?php the_safe($field_value) ?></a>
<?php else: ?>
<?php
if(gettype($field_value) == 'boolean')
echo $field_value ? 'Yes' : 'No';
else
the_safe($field_value);
?>
<?php endif; ?>
</td>
<?php } ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php the_pagination($context['count'], $context['elem_per_page'], $context['page']) ?>
<?php if(empty($context['objects'])): ?>
<div class="nothing">
Nothing was found
</div>
<?php endif; ?>
</section>
</div>
<?php the_admin_footer() ?>

View File

@ -0,0 +1,142 @@
<?php the_admin_header(
$context['is_new'] ? 'New ' . $context['verbose_name'] : get_the_safe($context['edit_title']));
$disabled_attr = $context['can_save'] ? '' : 'disabled';
?>
<div class="admin-container">
<h1 class="p-title">
<?php if($context['object']->is_saved()): ?>
<?php the_safe($context['edit_title']) ?>
<?php else: ?>
New <?php echo $context['verbose_name'] ?>
<?php endif; ?>
</h1>
<form class="form admin-single" method="post" enctype="multipart/form-data">
<div class="admin-single__form">
<?php
if(isset($context['error_message']))
the_alert($context['error_message'], 'warning', 'form-alert');
if(isset($context['success_message']))
the_alert($context['success_message'], 'success', 'form-alert');
?>
<?php foreach($context['fields'] as $field): ?>
<?php
// If field is html string
if(gettype($field) == 'string') {
echo $field;
continue;
}
?>
<!-- Label for every field (except checkbox) -->
<?php
$label = isset($field['input_label']) ? $field['input_label'] : ucfirst($field['model_field']);
if($field['input_type'] != 'checkbox'): ?>
<label for="<?php echo $field['model_field'] ?>">
<?php echo $label ?> <?php echo in_array('required', $field['input_attrs'] ?? array()) ? '<span>*</span>' : '' ?>
</label>
<?php endif; ?>
<?php
// Input field
$attrs_str = isset($field['input_attrs']) ? implode(" ", $field['input_attrs']) : '';
$val = $context['object']->{'field_' . $field['model_field']} ?? '';
switch($field['input_type']) {
case 'text':
case 'number':
case 'email':
case 'color':
case 'datetime-local':
?>
<div class="input <?php echo $attrs_str . ' ' . $disabled_attr . ' ' . $field['input_type'] ?>">
<input type="<?php echo $field['input_type'] ?>" id="<?php echo $field['model_field'] ?>" name="<?php echo $field['model_field'] ?>" value="<?php the_safe($val) ?>" <?php echo $attrs_str . ' ' . $disabled_attr ?> step="0.01">
</div>
<?php
break;
case 'checkbox':
?>
<div class="input-checkbox <?php echo $attrs_str ?>">
<input type="checkbox" id="<?php echo $field['model_field'] ?>" name="<?php echo $field['model_field'] ?>" value="on" <?php echo $attrs_str ?> <?php echo $val ? 'checked' : '' ?>>
<label for="<?php echo $field['model_field'] ?>"><?php echo $label ?></label>
</div>
<?php
break;
case 'textarea':
?>
<div class="input <?php echo $attrs_str . ' ' . $disabled_attr ?>">
<textarea name="<?php echo $field['model_field'] ?>" id="<?php echo $field['model_field'] ?>" <?php echo $attrs_str . ' ' . $disabled_attr ?> rows="7"><?php the_safe($val) ?></textarea>
</div>
<?php
break;
case 'select':
?>
<div class="input-select <?php echo $attrs_str . ' ' . $disabled_attr ?>">
<select name="<?php echo $field['model_field'] ?>" id="<?php echo $field['model_field'] ?>" <?php echo $attrs_str . ' ' . $disabled_attr ?>>
<?php foreach($field['input_values'] as $option): ?>
<option value="<?php echo $option[0] ?>" <?php echo $val == $option[0] ? 'selected' : '' ?>>
<?php echo $option[1] ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php
break;
case 'image':
?>
<div class="input-file">
<input type="file" id="<?php echo $field['model_field'] ?>" name="<?php echo $field['model_field'] ?>" accept="image/*" <?php echo $attrs_str . ' ' . $disabled_attr ?>>
<?php if(!empty($val)): ?>
<div class="image">
<img src="<?php echo MEDIA_URL . $val ?>" alt="">
</div>
<?php endif; ?>
</div>
<?php
break;
}?>
<?php endforeach; ?>
</div>
<div class="admin-single__info">
<div class="admin-block">
<div class="admin-block__title">
<?php echo $context['object']->is_saved() ? "Edit " . $context['verbose_name'] : "New " . $context['verbose_name'] ?>
</div>
<div class="admin-block__content">
<div class="btn-control">
<?php if($context['object']->is_saved()): ?>
<a href="<?php the_permalink('admin:delete', [str_replace('_', '-', $context['object']->get_table_name()), $context['object']->get_id()]) ?>" class="btn btn-danger" type="submit">Delete</a>
<?php else: ?>
<span></span>
<?php endif; ?>
<button class="btn btn-primary" type="submit" <?php echo $disabled_attr ?>>Save</button>
</div>
</div>
</div>
<?php
if(isset($context['error_form']))
$context['error_form']->display_error();
?>
<?php
if($context['object']->is_saved()) {
foreach ($context['component_widgets'] as $widget) {
$widget($context['object']);
}
}
?>
</div>
</form>
</div>
<?php the_admin_footer() ?>

View File

@ -0,0 +1,26 @@
<div class="admin-block">
<div class="admin-block__title">Banlist</div>
<div class="admin-block__content">
<div class="admin-block__table">
<?php if (!empty($banlist)): ?>
<?php foreach ($banlist as $ban): ?>
<div class="row">
<div class="column"><a href="<?php the_permalink('admin:ban', [$ban->get_id()]) ?>"><?php echo $ban->field_reason ?></a></div>
<?php if($ban->is_active()): ?>
Active
<?php else: ?>
Inactive
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php else: ?>
<div class="nothing">Banlist empty</div>
<?php endif ?>
</div>
<div class="btn-control">
<a href="<?php the_permalink('admin:banlist', [$user->get_id()]) ?>" class="btn">Show all</a>
<a href="<?php the_permalink('admin:ban-new', [$user->get_id()]) ?>" class="btn btn-primary">New ban</a>
</div>
</div>
</div>

33
apps/Admin/components.php Normal file
View File

@ -0,0 +1,33 @@
<?php
use Lycoreco\Apps\Users\Models\{
UserModel,
BanlistModel
};
function the_admin_header(string $title)
{
require_once APPS_PATH . '/Admin/Templates/components/admin-header.php';
}
function the_admin_footer()
{
require_once APPS_PATH . '/Admin/Templates/components/admin-footer.php';
}
/*
* Widgets in the single object page
*/
function the_user_banlist(UserModel $user)
{
$banlist = BanlistModel::filter(array(
[
'name' => 'obj.user_id',
'type' => '=',
'value' => $user->get_id()
]
),
['-obj.end_at']);
require APPS_PATH . '/Admin/Templates/widgets/user_banlist.php';
}

25
apps/Admin/urls.php Normal file
View File

@ -0,0 +1,25 @@
<?php
use Lycoreco\Apps\Admin\Controllers;
use Lycoreco\Includes\Routing\Path;
$admin_urls = [
// Dashboard
new Path('/admin', new Controllers\AdminHomeController(), 'home'),
// Lists
new Path('/admin/users', new Controllers\AdminUserListController(), 'user-list'),
////// Single object ///////
// User
new Path('/admin/user/new', new Controllers\AdminUserController(true), 'user-new'),
new Path('/admin/user/[:int]', new Controllers\AdminUserController(false), 'user'),
// Ban
new Path('/admin/user/[:int]/banlist', new Controllers\AdminBanListController(), 'banlist'),
new Path('/admin/user/[:int]/ban/new', new Controllers\AdminBanController(true), 'ban-new'),
new Path('/admin/ban/[:int]', new Controllers\AdminBanController(false), 'ban'),
// Dynamic delete for every object type
new Path('/admin/[:string]/[:int]/delete', new Controllers\AdminDeleteController(), 'delete')
];

View File

@ -0,0 +1,45 @@
<?php
namespace Lycoreco\Apps\Ajax\Controllers;
use Lycoreco\Includes\BaseController;
class AjaxController extends BaseController
{
protected $template_name = APPS_PATH . '/Ajax/Templates/ajax-result.php';
protected function restrict() {}
public function get_context_data()
{
require_once APPS_PATH . '/Ajax/ajax-actions.php';
$context['result'] = "";
$json = file_get_contents('php://input');
$data = json_decode($json, true);
$action = $data['action'] ?? false;
// If request from other site
if (!in_array($_SERVER['HTTP_HOST'], ALLOWED_HOSTS)) {
$context['result'] = get_ajax_error("403 Forbidden", 403);
return $context;
}
// if don't receive action method
if (empty($action)) {
$context['result'] = get_ajax_error("The action field indicating the function is not specified");
return $context;
}
$action = "ajax_" . $action;
try {
$context['result'] = $action($data['args']);
} catch (\Exception $ex) {
$context['result'] = get_ajax_error($ex->getMessage());
}
return $context;
}
}

View File

@ -0,0 +1,2 @@
<?php
echo $context['result'];

View File

@ -0,0 +1,47 @@
<?php
function get_ajax_error($message, $error_code = 500)
{
http_response_code($error_code);
$error = array();
$error['error'] = $message;
return json_encode($error, JSON_PRETTY_PRINT);
}
/**
* Ajax actions
*/
function ajax_search($args) {
$search_query = $args['query'];
$result = [];
$data = [
[
'id' => 2,
'name' => 'Genshin Impact'
],
[
'id' => 3,
'name' => 'Zenless zone zero'
],
[
'id' => 4,
'name' => 'Honkai Star Rail'
],
[
'id' => 5,
'name' => 'Honkai Impact'
],
];
$result['results'] = [];
foreach ($data as $key => $value) {
if(str_contains($value['name'], $search_query))
$result['results'][] = $value;
}
sleep(3);
return json_encode($result, JSON_PRETTY_PRINT);
}

8
apps/Ajax/urls.php Normal file
View File

@ -0,0 +1,8 @@
<?php
use Lycoreco\Includes\Routing\Path;
use Lycoreco\Apps\Ajax\Controllers\AjaxController;
$ajax_urls = array(
new Path('/ajax', new AjaxController(), 'main'),
);

View File

@ -0,0 +1,10 @@
<?php
namespace Lycoreco\Apps\Index\Controllers;
use Lycoreco\Includes\BaseController;
class ErrorController extends BaseController
{
protected $template_name = APPS_PATH . '/Index/Templates/error.php';
}

View File

@ -0,0 +1,10 @@
<?php
namespace Lycoreco\Apps\Index\Controllers;
use Lycoreco\Includes\BaseController;
class HomepageController extends BaseController
{
protected $template_name = APPS_PATH . '/Index/Templates/index.php';
}

View File

@ -0,0 +1,23 @@
<?php
$error = $context['error_model'];
the_header(
$error->get_http_error(),
'',
'error',
[
['robots', 'nofollow, noindex']
]);
/**
* @var PageError
*/
?>
<div class="error-page">
<div class="error-code"><?php echo $error->get_http_error() ?></div>
<div class="error-message"><?php echo $error->getMessage() ?></div>
</div>
<?php the_footer() ?>

View File

@ -0,0 +1,18 @@
<?php the_header(
'Welcome',
'Discover and purchase top-rated games and software to elevate your entertainment and productivity. Explore our curated selection for the best deals and exclusive offers!',
'frontpage',
[
['robots', 'nofollow, noindex'],
['keywords', 'keys, programms, games, xbox, pc, playstation']
]);
?>
<div class="container">
Index page
</div>
<?php the_footer(array(
ASSETS_PATH . '/swiper/swiper-bundle.min.js',
ASSETS_PATH . '/js/index.js',
)); ?>

8
apps/Index/urls.php Normal file
View File

@ -0,0 +1,8 @@
<?php
use Lycoreco\Apps\Index\Controllers;
use Lycoreco\Includes\Routing\Path;
$index_urls = [
new Path('', new Controllers\HomepageController(), 'home'),
];

View File

@ -0,0 +1,53 @@
<?php
namespace Lycoreco\Apps\Users\Controllers;
use Lycoreco\Apps\Users\Models\{
ActivateCodeModel,
UserModel
};
use Lycoreco\Includes\Routing\HttpExceptions;
use Lycoreco\Includes\BaseController;
class ActivationController extends BaseController
{
protected $template_name = APPS_PATH . '/Users/Templates/activation.php';
protected function get_model(): ActivateCodeModel
{
if (isset($this->__model))
return $this->__model;
$this->__model = ActivateCodeModel::get(array(
[
'name' => 'obj.activate_slug',
'type' => '=',
'value' => $this->url_context['url_1'] // slug
]
));
if(empty($this->__model))
throw new HttpExceptions\NotFound404('The account activation link is invalid');
return $this->__model;
}
public function get_context_data() {
$context = parent::get_context_data();
$activation_model = $this->get_model();
$user = UserModel::get(array(
[
'name' => 'obj.id',
'type' => '=',
'value' => $activation_model->field_user_id
]
));
if($user->field_is_active)
throw new HttpExceptions\BadRequest400("The account already activated");
$user->field_is_active = true;
$user->save();
return $context;
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Lycoreco\Apps\Users\Controllers;
use Lycoreco\Includes\Model\ValidationError;
use Lycoreco\Includes\BaseController;
class EditContactInfoController extends BaseController
{
protected $template_name = APPS_PATH . '/Users/Templates/sect_information/edit_information.php';
protected $allow_role = 'user';
protected function post()
{
$username = $_POST['username'] ?? '';
$fname = $_POST['fname'] ?? '';
$lname = $_POST['lname'] ?? '';
$curr_user = CURRENT_USER;
$curr_user->field_username = $username;
$curr_user->field_fname = $fname;
$curr_user->field_lname = $lname;
try {
$curr_user->save();
redirect_to(get_permalink('users:profile'));
} catch (ValidationError $ex) {
$this->context['error_form'] = $ex;
return;
} catch (\Exception $ex) {
$this->context['error_message'] = 'Unexpected error';
return;
}
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Lycoreco\Apps\Users\Controllers;
use Lycoreco\Includes\Model\ValidationError;
use Lycoreco\Includes\BaseController;
class EditEmailController extends BaseController
{
protected $template_name = APPS_PATH . '/Users/Templates/sect_information/edit_email.php';
protected $allow_role = 'user';
protected function post()
{
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
$curr_user = CURRENT_USER;
$curr_user->field_email = $email;
try {
if ($curr_user->password_verify($password)) {
$curr_user->save();
redirect_to(get_permalink('users:profile'));
} else {
$this->context['error_message'] = 'Invalid password';
}
} catch (ValidationError $ex) {
$this->context['error_form'] = $ex;
return;
} catch (\Exception $ex) {
$this->context['error_message'] = 'Unexpected error';
return;
}
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace Lycoreco\Apps\Users\Controllers;
use Lycoreco\Apps\Users\Models\UserModel;
use Lycoreco\Includes\Model\ValidationError;
use Lycoreco\Includes\BaseController;
class EditPasswordController extends BaseController
{
protected $template_name = APPS_PATH . '/Users/Templates/sect_information/edit_password.php';
protected $allow_role = 'user';
protected function post()
{
$old_password = $_POST['old_password'] ?? '';
$new_password = $_POST['new_password'] ?? '';
$repeat_password = $_POST['repeat_password'] ?? '';
// Check password
if (!CURRENT_USER->password_verify($old_password)) {
$this->context['error_form'] = new ValidationError(['You entered your old password incorrectly.']);
return;
}
if ($new_password != $repeat_password) {
$this->context['error_form'] = new ValidationError(['Passwords don\'t match']);
return;
}
try {
$curr_user = CURRENT_USER;
if (UserModel::valid_password($new_password) !== true) {
$errors = UserModel::valid_password($new_password);
throw new ValidationError($errors);
}
// After validations, save new password
$curr_user->field_password = UserModel::password_hash($new_password);
$curr_user->save();
redirect_to(get_permalink('users:profile'));
} catch (ValidationError $ex) {
$this->context['error_form'] = $ex;
return;
} catch (\Exception $ex) {
$this->context['error_message'] = 'Unexpected error';
return;
}
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace Lycoreco\Apps\Users\Controllers;
use Lycoreco\Apps\Users\Models\UserModel;
use Lycoreco\Apps\Users\Models\RecoveryPassModel;
use Lycoreco\Includes\BaseController;
class ForgotPasswordController extends BaseController
{
protected $template_name = APPS_PATH . '/Users/Templates/forgot_password.php';
protected function distinct()
{
// If current user is authorized, redirect to homepage
if (!empty(CURRENT_USER)) {
$home = get_permalink('index:home');
redirect_to($home);
}
}
protected function post()
{
require_once APPS_PATH . '/Users/functions.php';
$email = $_POST['email'];
if (!isset($email)) {
$this->context['error_message'] = 'Enter your e-mail address';
return;
}
$user = UserModel::get(array(
[
'name' => 'obj.email',
'type' => '=',
'value' => $email
]
));
// If user is exists
if ($user) {
try {
// Check if there was a previous request.
if (!RecoveryPassModel::is_cooldown_available($user->get_id())) {
$this->context['error_message'] = 'You have already sent a request recently. Check your email or wait ' . RecoveryPassModel::get_cooldown_modifier() . '.';
return;
}
// Create recovery code
$recovery_model = new RecoveryPassModel(array(
'user_id' => $user->get_id(),
'recovery_slug' => generate_uuid()
));
$recovery_model->save();
$link_to_recovery = get_permalink('users:reset', [$recovery_model->field_recovery_slug]);
// E-mail template for recovery
$body = get_recovery_email_template(
$user->get_public_name(),
$link_to_recovery
);
$altBody = get_recovery_email_alt_template(
$user->get_public_name(),
$link_to_recovery
);;
send_email(
'Reset Password',
$body,
$altBody,
$user->field_email,
$user->get_public_name()
);
$this->context['success_message'] = 'The email was sent successfully';
} catch (\Exception $ex) {
$this->context['error_message'] = 'Unknown error. Please try again.';
}
} else {
$this->context['error_message'] = 'The user with this e-mail was not found.';
}
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace Lycoreco\Apps\Users\Controllers;
use Lycoreco\Apps\Users\Models\{
UserModel,
ActivateCodeModel,
BanlistModel
};
use Lycoreco\Includes\BaseController;
class LoginController extends BaseController
{
protected $template_name = APPS_PATH . '/Users/Templates/login.php';
protected function post()
{
$username = $_POST['username'] ?? null;
$password = $_POST['password'] ?? null;
if (!empty($username) && !empty($password)) {
// Try to find user with $username
$user = UserModel::get(
array(
[
'name' => 'obj.username',
'type' => '=',
'value' => $username
],
[
'name' => 'obj.email',
'type' => '=',
'value' => $username
]
),
array(),
'OR'
);
// If user not found
if (empty($user)) {
$this->context['error_message'] = 'User not found';
return;
}
// If user is not activate
if (!$user->field_is_active) {
ActivateCodeModel::send_activation($user);
$this->context['error_message'] = 'Your account has not been activated. An activation email has been sent to your email address.';
return;
}
if ($user->is_banned()) {
$this->context['error_message'] = 'Your account has been banned. ';
$ban = BanlistModel::get(array(
[
'name' => 'obj.user_id',
'type' => '=',
'value' => $user->get_id()
]
), ['-obj.end_at']);
$ban_comment = "Reason: $ban->field_reason. Until to $ban->field_end_at";
$this->context['error_message'] .= $ban_comment;
return;
}
// Check password is correct or not
$is_correct_pass = $user->password_verify($password);
if ($is_correct_pass === true) {
set_auth_user($user->get_id());
redirect_to(get_permalink('index:home'));
} else {
$this->context['error_message'] = 'You entered an incorrect password.';
return;
}
}
}
protected function distinct()
{
// If current user is authorized, redirect to homepage
if (!empty(CURRENT_USER)) {
$home = get_permalink('index:home');
redirect_to($home);
}
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Lycoreco\Apps\Users\Controllers;
use Lycoreco\Includes\BaseController;
class LogoutController extends BaseController
{
protected $template_name = APPS_PATH . '/Users/Templates/logout.php';
protected function distinct()
{
logout();
redirect_to(get_permalink('index:home'));
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace Lycoreco\Apps\Users\Controllers;
use Lycoreco\Includes\BaseController;
class ProfileController extends BaseController
{
protected $template_name = APPS_PATH . '/Users/Templates/profile.php';
protected $allow_role = 'user';
}

View File

@ -0,0 +1,62 @@
<?php
namespace Lycoreco\Apps\Users\Controllers;
use Lycoreco\Apps\Users\Models\UserModel;
use Lycoreco\Includes\BaseController;
use Lycoreco\Includes\Model\ValidationError;
class RegisterController extends BaseController
{
protected $template_name = APPS_PATH . '/Users/Templates/register.php';
protected function distinct()
{
// If current user is authorized, redirect to homepage
if (!empty(CURRENT_USER)) {
$home = get_permalink('index:home');
redirect_to($home);
}
}
protected function post()
{
$username = $_POST['username'] ?? null;
$email = $_POST['email'] ?? null;
$pass = $_POST['password'] ?? null;
$repeat = $_POST['repeat'] ?? null;
$fname = $_POST['fname'] ?? '';
$lname = $_POST['lname'] ?? '';
// If password is not valid
if (UserModel::valid_password($pass) !== true) {
$context['error_form'] = new ValidationError(UserModel::valid_password($pass));
return;
}
if ($pass != $repeat) {
$context['error_form'] = new ValidationError(["Passwords don't match"]);
return;
}
$pass_hash = UserModel::password_hash($pass);
$new_user = new UserModel(array(
'username' => $username,
'email' => $email,
'password' => $pass_hash,
'fname' => $fname,
'lname' => $lname
));
try {
$new_user->save();
set_auth_user($new_user->get_id());
redirect_to(get_permalink('index:home'));
} catch (ValidationError $ex) {
$this->context['error_form'] = $ex;
return;
} catch (\Exception $ex) {
$this->context['error_message'] = 'Unexpected error';
return;
}
}
}

View File

@ -0,0 +1,99 @@
<?php
namespace Lycoreco\Apps\Users\Controllers;
use Lycoreco\Apps\Users\Models\UserModel;
use Lycoreco\Apps\Users\Models\RecoveryPassModel;
use Lycoreco\Includes\Model\ValidationError;
use Lycoreco\Includes\BaseController;
class ResetPasswordController extends BaseController
{
protected $template_name = APPS_PATH . '/Users/Templates/reset_password.php';
protected function get_model()
{
if (isset($this->__model))
return $this->__model;
$this->__model = RecoveryPassModel::get(array(
[
'name' => 'obj.recovery_slug',
'type' => '=',
'value' => $this->url_context['url_1']
]
));
return $this->__model;
}
protected function distinct()
{
$recovery_model = $this->get_model();
// Check that recovery_model is available or not
if ($recovery_model) {
if (!$recovery_model->is_available())
$this->context['not_available'] = 'The link is no longer valid. Please try again';
} else {
$this->context['not_available'] = 'The link does not exist';
}
}
protected function post()
{
require_once APPS_PATH . '/Users/functions.php';
$pass = $_POST['password'] ?? null;
$repeat = $_POST['repeat'] ?? null;
$recovery_model = $this->get_model();
// Validate password
if (empty($pass) || empty($repeat)) {
$this->context['error_form'] = new ValidationError(["You did not provide a password in the fields."]);
return;
}
if ($pass != $repeat) {
$this->context['error_form'] = new ValidationError(["Passwords don't match"]);
return;
}
if (UserModel::valid_password($pass) !== true) {
$this->context['error_form'] = new ValidationError(UserModel::valid_password($pass));
return;
}
if (!$recovery_model->is_available())
return;
// Set new password
$pass_hash = UserModel::password_hash($pass);
if ($recovery_model) {
$user = UserModel::get(array(
[
'name' => 'obj.id',
'type' => '=',
'value' => $recovery_model->field_user_id
]
));
// Save new password
$user->field_password = $pass_hash;
$user->save();
// Set RecoveryModel as used
$recovery_model->field_is_used = true;
$recovery_model->save();
// Send message to email
send_email(
'Password Updated',
get_reset_completed_email_template($user->get_public_name()),
get_reset_completed_email_alt_template($user->get_public_name()),
$user->field_email,
$user->get_public_name()
);
// Show message and do form as unavailable
$this->context['not_available'] = 'Your password has been successfully changed';
}
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace Lycoreco\Apps\Users\Models;
use Lycoreco\Includes\Model\BaseModel;
class ActivateCodeModel extends BaseModel
{
public $field_user_id;
public $field_activate_slug;
public $field_created_at = null;
static protected $table_name = 'user_activate_codes';
static protected $table_fields = [
'id' => 'int',
'user_id' => 'int',
'activate_slug' => 'string',
'created_at' => 'DateTime'
];
public static function init_table()
{
$result = db_query('CREATE TABLE ' . static::$table_name . ' (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
activate_slug VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);');
return $result;
}
public static function send_activation(UserModel $user)
{
require_once APPS_PATH . '/Users/functions.php';
if ($user->field_is_active)
return;
$activation_code = ActivateCodeModel::get([
[
'name' => 'obj.user_id',
'type' => '=',
'value' => $user->get_id()
]
]);
// If activation link does not exist - create new
if(!$activation_code) {
$activation_code = new ActivateCodeModel(array(
'user_id' => $user->get_id(),
'activate_slug' => generate_uuid()
));
$activation_code->save();
}
$link_to_activate = get_permalink('users:activate', [
$activation_code->field_activate_slug
]);
send_email(
"Activation account",
get_activation_email_template(
$user->field_username,
$link_to_activate),
get_activation_email_alt_template(
$user->field_username,
$link_to_activate),
$user->field_email,
$user->get_public_name()
);
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace Lycoreco\Apps\Users\Models;
use Lycoreco\Includes\Model\{
BaseModel,
CustomDateTime
};
use function PHPSTORM_META\map;
class BanlistModel extends BaseModel
{
public $field_user_id;
public $field_reason;
public $field_created_at = null;
public $field_end_at = null;
static protected $table_name = 'user_banlist';
static protected $table_fields = [
'id' => 'int',
'user_id' => 'int',
'reason' => 'string',
'created_at' => 'DateTime',
'end_at' => 'DateTime'
];
static protected $search_fields = ['obj.reason'];
public static function init_table()
{
$result = db_query('CREATE TABLE ' . static::$table_name . ' (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
reason VARCHAR(255) NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
end_at TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);');
return $result;
}
public function is_active()
{
return new CustomDateTime() <= $this->field_end_at;
}
public function send_email_notification()
{
require_once APPS_PATH . '/Users/functions.php';
$user = UserModel::get(array(
[
'name' => 'obj.id',
'type' => '=',
'value' => $this->field_user_id
]
));
send_email(
"Account banned",
get_ban_template($user->field_username, $this),
get_ban_alt_template($user->field_username, $this),
$user->field_email,
$user->get_public_name()
);
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace Lycoreco\Apps\Users\Models;
use Lycoreco\Includes\Model\BaseModel;
class RecoveryPassModel extends BaseModel
{
public $field_user_id;
public $field_recovery_slug;
public $field_created_at = null;
public $field_is_used = false;
static protected $cooldownModifier = '10 minutes';
static protected $table_name = 'recovery_password';
static protected $table_fields = [
'id' => 'int',
'user_id' => 'int',
'recovery_slug' => 'string',
'created_at' => 'DateTime',
'is_used' => 'bool'
];
public function is_available()
{
// Recovery is already used
if ($this->field_is_used)
return false;
// Invalid after 10 minutes
$datetime_recovery = new \DateTime($this->field_created_at);
$datetime_recovery->modify('+' . static::$cooldownModifier);
$current_datetime = new \DateTime();
if ($datetime_recovery < $current_datetime) {
return false;
} else {
return true;
}
}
public static function get_cooldown_modifier()
{
return static::$cooldownModifier;
}
/**
* Check if the email was sent 10 minutes earlier. If so, the user needs to wait for the cooldown.
* @return bool
*/
public static function is_cooldown_available($user_id)
{
$cooldownTime = new \DateTime();
$cooldownTime->modify('-' . static::$cooldownModifier);
$lastRecoveryModel = RecoveryPassModel::get(
array(
[
'name' => 'obj.created_at',
'type' => '>=',
'value' => $cooldownTime->format('Y-m-d H:i:s')
],
[
'name' => 'obj.user_id',
'type' => '=',
'value' => $user_id
]
)
);
if ($lastRecoveryModel)
return false;
else
return true;
}
public static function init_table()
{
$result = db_query('CREATE TABLE ' . static::$table_name . ' (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
recovery_slug VARCHAR(255) UNIQUE NOT NULL,
is_used TINYINT(1) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);');
return $result;
}
}

View File

@ -0,0 +1,169 @@
<?php
namespace Lycoreco\Apps\Users\Models;
use Lycoreco\Includes\Model\BaseModel;
class UserModel extends BaseModel
{
public $field_username;
public $field_email;
public $field_fname;
public $field_lname;
public $field_password;
public $field_is_admin = false;
public $field_is_active = false;
public $field_register_at = null;
protected $is_banned = false;
static protected $additional_fields = array(
[
'field' => [
'(EXISTS (
SELECT 1 FROM user_banlist b
WHERE b.user_id = obj.id AND b.end_at > NOW()
)) AS is_banned'
]
]
);
static protected $table_name = 'users';
static protected $table_fields = [
'id' => 'int',
'username' => 'string',
'email' => 'string',
'fname' => 'string',
'lname' => 'string',
'password' => 'string',
'is_admin' => 'bool',
'is_active' => 'bool',
'register_at' => 'DateTime'
];
static protected $search_fields = ['obj.username', 'obj.email', 'obj.fname', 'obj.lname'];
public static function init_table()
{
$result = db_query('CREATE TABLE ' . static::$table_name . ' (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(20) UNIQUE NOT NULL,
email VARCHAR(40) UNIQUE NOT NULL,
fname VARCHAR(20) NOT NULL,
lname VARCHAR(20) NOT NULL,
password VARCHAR(255) NOT NULL,
is_admin TINYINT(1) DEFAULT 0,
is_active TINYINT(1) DEFAULT 0,
register_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);');
return $result;
}
public function get_role()
{
if ($this->field_is_admin)
return 'Admin';
else
return 'User';
}
public function is_banned()
{
return (bool)$this->is_banned;
}
/**
* Get public user name
* if user has first name, we get first name
* if user has first and last name, we get first and last name
* if user hasn't first name, we get username
* @return string
*/
public function get_public_name()
{
if (empty($this->field_fname))
return $this->field_username;
$full_name = $this->field_fname;
if ($this->field_lname)
$full_name .= " " . $this->field_lname;
return $full_name;
}
public static function password_hash($password)
{
return password_hash($password, PASSWORD_DEFAULT);
}
public function password_verify($password)
{
return password_verify($password, $this->field_password);
}
public static function valid_password($password)
{
$errors = [];
$password_len = strlen($password);
if ($password_len < 4 || $password_len > 10)
$errors[] = 'The password is between 4 and 10 characters long.';
if (strpos($password, ' '))
$errors[] = 'Spaces are not allowed.';
if (empty($errors))
return true;
return $errors;
}
public function valid()
{
$errors = array();
// Check if exists another user with same username/email
$exists_user = UserModel::get(
array(
[
'name' => 'obj.username',
'type' => '=',
'value' => $this->field_username,
],
[
'name' => 'obj.email',
'type' => '=',
'value' => $this->field_email
]
),
array(),
'OR'
);
if (!empty($exists_user)) {
if ($exists_user->get_id() !== $this->get_id()) {
if ($exists_user->field_email == $this->field_email)
$errors[] = 'The user with this email already exists';
if ($exists_user->field_username == $this->field_username)
$errors[] = 'The user with this username already exists';
}
}
// Check length
$username_len = strlen($this->field_username);
$email_len = strlen($this->field_username);
$fname_len = strlen($this->field_username);
$lname_len = strlen($this->field_username);
if ($username_len > 20 || $username_len < 4)
$errors[] = 'The username must be between 4 and 20 characters long';
if ($email_len > 40 || $email_len < 4)
$errors[] = 'The email must be between 4 and 40 characters long';
if ($fname_len > 20 || $fname_len < 3)
$errors[] = 'The first name must be between 4 and 20 characters long';
if ($lname_len > 20 || $lname_len < 3)
$errors[] = 'The last name must be between 4 and 40 characters long';
if (empty($errors))
return true;
return $errors;
}
}

View File

@ -0,0 +1,16 @@
<?php the_header(
'Account activation',
'',
'activation',
[
['robots', 'nofollow, noindex']
])
?>
<div class="container">
Your account has been successfully activated. You can now log in.
</div>
<?php
the_footer();
?>

View File

@ -0,0 +1,23 @@
<?php
use Lycoreco\Includes\Routing\Router;
$user_nav = array(
'users:profile' => 'Information',
'users:wishlist' => 'My wishlist',
'users:orders' => 'My orders'
);
?>
<div class="profile-tabs">
<?php foreach ($user_nav as $router_name => $title): ?>
<?php if($router_name == Router::$current_router_name): ?>
<div class="btn btn-tab active"><?php echo $title ?></div>
<?php else: ?>
<a href="<?php the_permalink($router_name) ?>" class="btn btn-tab"><?php echo $title ?></a>
<?php endif; ?>
<?php endforeach; ?>
</div>

View File

@ -0,0 +1,17 @@
<?php the_email_header($preheader) ?>
<p>Hi <?php echo $username ?>,</p>
<p>Thanks for signing up for FridgeBites! To complete your registration, please confirm your email address by clicking the link below:</p>
<p>
<a href="<?php echo $url_to_activate ?>"><?php echo $url_to_activate ?></a>
</p>
<p>Once confirmed, youll get full access to your account and all the tasty features waiting for you.</p>
<p>If you didnt create an account, just ignore this email.</p>
<p>Welcome aboard!<br>The LycoReco Team</p>
<?php the_email_footer() ?>

View File

@ -0,0 +1,12 @@
Hi <?php echo $username ?>,
Thanks for signing up for FridgeBites! To complete your registration, please confirm your email address by clicking the link below:
<?php echo $url_to_activate ?>
Once confirmed, youll get full access to your account and all the tasty features waiting for you.
If you didnt create an account, just ignore this email.
Welcome aboard!
The LycoReco Team

View File

@ -0,0 +1,17 @@
<?php the_email_header($preheader) ?>
<p>Hello <?php echo $username ?>,</p>
<p>Were reaching out to let you know that your account on FridgeBites has been suspended due to a violation of our community guidelines.</p>
<p>The ban is effective immediately and prevents access to your account and all associated services.</p>
<p><b>Reason:</b> <?php echo $model->field_reason ?></p>
<p><b>End date:</b> <?php echo $model->field_end_at ?></p>
<p>We take community safety seriously and appreciate your understanding.</p>
<p>Best regards,<br>The LycoReco Team</p>
<?php the_email_footer() ?>

View File

@ -0,0 +1,14 @@
Hello <?php echo $username ?>,
Were reaching out to let you know that your account on FridgeBites has been suspended due to a violation of our community guidelines.
The ban is effective immediately and prevents access to your account and all associated services.
Reason: <?php echo $model->field_reason ?>
End date: <?php echo $model->field_end_at ?>
We take community safety seriously and appreciate your understanding.
Best regards,
The LycoReco Team

View File

@ -0,0 +1,11 @@
<?php the_email_header($preheader) ?>
<p>Hi <?php echo $username ?>,</p>
<p>We received a request to reset your password. Click the button below to proceed:</p>
<p>
<a href="<?php echo $url_to_recovery ?>"><?php echo $url_to_recovery ?></a>
</p>
<p>If you did not request a password reset, you can safely ignore this email. Your password will remain unchanged.</p>
<p>Thanks,<br>The LycoReco Team</p>
<?php the_email_footer() ?>

View File

@ -0,0 +1,10 @@
Hi <?php echo $username ?>,
We received a request to reset your password. Click the button below to proceed:
<?php echo $url_to_recovery ?>
If you did not request a password reset, you can safely ignore this email. Your password will remain unchanged.
Thanks,
The LycoReco Team

View File

@ -0,0 +1,14 @@
<?php the_email_header($preheader) ?>
<p>Hi <?php echo $username ?>,</p>
<p>We wanted to let you know that your password has been successfully updated. If you made this change, no further action is required.</p>
<p>If you did not request a password change, please contact our support team immediately to secure your account</p>
<p>For any questions or assistance, feel free to reach out to us at <a href="<?php the_permalink('contacts:form') ?>">contact form</a>.</p>
<p>Thank you for taking steps to keep your account secure.</p>
<p>Thanks,<br>The LycoReco Team</p>
<?php the_email_footer() ?>

View File

@ -0,0 +1,14 @@
Hi <?php echo $username ?>,
We wanted to let you know that your password has been successfully updated. If you made this change, no further action is required.
If you did not request a password change, please contact our support team immediately to secure your account.
For any questions or assistance, feel free to reach out to us at contact form:
<?php the_permalink('contacts:form') ?>
Thank you for taking steps to keep your account secure.
Thanks,
The LycoReco Team

View File

@ -0,0 +1,35 @@
<?php the_header(
'Password Recovery',
'',
'login-register',
[
['robots', 'nofollow, noindex']
]) ?>
<div class="container">
<div class="login__inner">
<h1 class="p-title">Password Recovery</h1>
<p>Enter your email to receive a secure link to reset your password. The link will be valid for a limited time.</p>
<form class="form" method="post">
<?php
if(isset($context['error_message']))
the_alert($context['error_message']);
if(isset($context['success_message']))
the_alert($context['success_message'], 'success');
?>
<label for="field_username">E-mail <span>*</span></label>
<div class="input">
<input type="email" name="email" id="field_username" required>
</div>
<div class="login-choice">
<a href="<?php the_permalink('users:login') ?>">Login</a>
<button class="btn btn-primary" type="submit">Send</button>
</div>
</form>
</div>
</div>
<?php the_footer() ?>

View File

@ -0,0 +1,48 @@
<?php the_header(
'Login',
'',
'login-register',
[
['robots', 'nofollow, noindex']
]) ?>
<div class="container">
<div class="login__inner">
<h1 class="p-title">Welcome to KeysShop</h1>
<form class="form" method="post">
<?php
if(isset($context['error_message']))
the_alert($context['error_message']);
?>
<label for="field_username">Username</label>
<div class="input">
<input type="text" name="username" id="field_username" required>
</div>
<label for="field_password">Password</label>
<div class="input">
<input type="password" name="password" id="field_password" required>
</div>
<div class="login-choice">
<a href="<?php the_permalink('users:forgot') ?>">Forgot password?</a>
<button class="btn btn-primary" type="submit">Login</button>
</div>
</form>
<hr>
<div class="login-text__notice">
<h2>You don't have account?</h2>
<p>Register now to buy game or program keys at low prices! </p>
<a href="<?php the_permalink('users:register') ?>" class="btn btn-primary">
Register
</a>
</div>
</div>
</div>
<?php the_footer() ?>

View File

@ -0,0 +1 @@
Logout....

View File

@ -0,0 +1,70 @@
<?php
require_once APPS_PATH . '/Users/components.php';
the_header(
get_the_safe(CURRENT_USER->field_username),
'View and manage your personal information, update account settings, and track your activity on the Profile page. Customize your preferences and keep your details up-to-date easily.',
'profile',
[
['robots', 'nofollow, noindex']
]);
?>
<div class="container">
<?php the_user_nav() ?>
<h1 class="p-title">Account information</h1>
<div class="account-block">
<h2>Contact information</h2>
<table>
<tr>
<td>Username:</td>
<td><?php the_safe(CURRENT_USER->field_username) ?></td>
</tr>
<tr>
<td>First Name:</td>
<td><?php the_safe(CURRENT_USER->field_fname ?? 'None') ?></td>
</tr>
<tr>
<td>Last Name:</td>
<td><?php the_safe(CURRENT_USER->field_lname ?? 'None') ?></td>
</tr>
<tr>
<td>Status:</td>
<td><?php echo CURRENT_USER->get_role() ?></td>
</tr>
</table>
<div class="change-user-btns">
<a href="<?php the_permalink('users:edit-info') ?>" class="btn">Edit info</a>
</div>
</div>
<div class="account-block">
<h2>Security</h2>
<table>
<tr>
<td>E-mail:</td>
<td><?php the_safe(CURRENT_USER->field_email) ?></td>
</tr>
<tr>
<td>Password:</td>
<td>**********</td>
</tr>
</table>
<div class="change-user-btns">
<a href="<?php the_permalink('users:edit-password') ?>" class="btn">Change password</a>
<a href="<?php the_permalink('users:edit-email') ?>" class="btn">Change e-mail</a>
</div>
</div>
<div class="account-btn-control">
<a href="<?php the_permalink('users:logout') ?>" class="btn btn-logout">Logout</a>
<?php if(CURRENT_USER->field_is_admin): ?>
<a href="<?php the_permalink('admin:home') ?>" class="btn btn-primary">Admin panel</a>
<?php endif ?>
</div>
</div>
<?php the_footer() ?>

View File

@ -0,0 +1,81 @@
<?php the_header(
'Register',
'',
'login-register',
[
['robots', 'nofollow, noindex']
]) ?>
<div class="container">
<div class="login__inner">
<h1 class="p-title">Create New Account</h1>
<form class="form" method="post">
<?php
if(isset($context['error_message']))
the_alert($context['error_message']);
?>
<label for="field_username">Username <span>*</span></label>
<div class="input">
<input type="text" name="username" id="field_username" required>
</div>
<label for="field_email">E-mail <span>*</span></label>
<div class="input">
<input type="email" name="email" id="field_email" required>
</div>
<label for="field_password">Password <span>*</span></label>
<div class="input">
<input type="password" name="password" id="field_password" required>
</div>
<label for="field_repeat_password">Repeat password <span>*</span></label>
<div class="input">
<input type="password" name="repeat" id="field_repeat_password" required>
</div>
<label for="field_fname">First Name</label>
<div class="input">
<input type="text" name="fname" id="field_fname">
</div>
<label for="field_lname">Last Name</label>
<div class="input">
<input type="text" name="lname" id="field_lname">
</div>
<div class="input-checkbox">
<input id="terms_agree" type="checkbox" required>
<label for="terms_agree">I Agree to the <a href="<?php the_permalink('index:terms') ?>">Terms & Conditions</a></label>
</div>
<div class="input-checkbox">
<input id="privacy_agree" type="checkbox" required>
<label for="privacy_agree">I Agree to the <a href="<?php the_permalink('index:privacy') ?>">Privacy Policy</a></label>
</div>
<?php
if(isset($context['error_form']))
$context['error_form']->display_error();
?>
<div class="login-choice">
<span></span>
<button class="btn btn-primary" type="submit">Create an account</button>
</div>
</form>
<hr>
<div class="login-text__notice">
<h2>Do you already have an account?</h2>
<p>Log in to your account to continue the purchase. </p>
<a href="<?php the_permalink('users:login') ?>" class="btn btn-primary">
Login
</a>
</div>
</div>
</div>
<?php the_footer() ?>

View File

@ -0,0 +1,50 @@
<?php the_header(
'Password Recovery',
'',
'login-register',
[
['robots', 'nofollow, noindex']
])
?>
<div class="container">
<div class="login__inner" <?php if(isset($context['not_available'])) echo 'style="text-align: center;"'; ?>>
<h1 class="p-title">Reset Password</h1>
<?php if(!isset($context['not_available'])): ?>
<p>Enter a new password for your account.</p>
<?php else: ?>
<p><?php echo $context['not_available'] ?></p>
<?php endif; ?>
<?php if(!isset($context['not_available'])): ?>
<form class="form" method="post">
<?php
if(isset($context['error_message']))
the_alert($context['error_message']);
?>
<label for="field_pass">New password <span>*</span></label>
<div class="input">
<input type="password" name="password" id="field_pass" required>
</div>
<label for="field_repeat_pass">Repeat new password <span>*</span></label>
<div class="input">
<input type="password" name="repeat" id="field_repeat_pass" required>
</div>
<?php
if(isset($context['error_form']))
$context['error_form']->display_error();
?>
<div class="login-choice">
<span></span>
<button class="btn btn-primary" type="submit">Reset password</button>
</div>
</form>
<?php endif; ?>
</div>
</div>
<?php the_footer() ?>

View File

@ -0,0 +1,41 @@
<?php the_header(
'Change email ' . CURRENT_USER->field_username,
'View and manage your personal information, update account settings, and track your activity on the Profile page. Customize your preferences and keep your details up-to-date easily.',
'profile-edit',
[
['robots', 'nofollow, noindex']
]) ?>
<div class="container">
<h1 class="p-title">Edit E-mail</h1>
<form class="form form-edit" method="post">
<?php
if(isset($context['error_message']))
the_alert($context['error_message']);
?>
<label for="field_email">E-mail <span>*</span></label>
<div class="input">
<input type="email" name="email" id="field_email" value="<?php the_safe(CURRENT_USER->field_email) ?>" required>
</div>
<label for="field_fname">Your password <span>*</span></label>
<div class="input">
<input type="password" name="password" id="field_password" required>
</div>
<?php
if(isset($context['error_form']))
$context['error_form']->display_error();
?>
<div class="btn-control">
<a class="a-back" href="<?php the_permalink('users:profile') ?>">< Back</a>
<button class="btn btn-primary" type="submit">Save</button>
</div>
</form>
</div>
<?php the_footer() ?>

View File

@ -0,0 +1,46 @@
<?php the_header(
'Edit ' . CURRENT_USER->field_username,
'View and manage your personal information, update account settings, and track your activity on the Profile page. Customize your preferences and keep your details up-to-date easily.',
'profile-edit',
[
['robots', 'nofollow, noindex']
]) ?>
<div class="container">
<h1 class="p-title">Edit contact information</h1>
<form class="form form-edit" method="post">
<?php
if(isset($context['error_message']))
the_alert($context['error_message']);
?>
<label for="field_username">Username <span>*</span></label>
<div class="input">
<input type="text" name="username" id="field_username" value="<?php the_safe(CURRENT_USER->field_username) ?>" required>
</div>
<label for="field_fname">First Name</label>
<div class="input">
<input type="text" name="fname" id="field_fname" value="<?php the_safe(CURRENT_USER->field_fname ?? '') ?>">
</div>
<label for="field_lname">Last Name</label>
<div class="input">
<input type="text" name="lname" id="field_lname" value="<?php the_safe(CURRENT_USER->field_lname ?? '') ?>">
</div>
<?php
if(isset($context['error_form']))
$context['error_form']->display_error();
?>
<div class="btn-control">
<a class="a-back" href="<?php the_permalink('users:profile') ?>">< Back</a>
<button class="btn btn-primary" type="submit">Save</button>
</div>
</form>
</div>
<?php the_footer() ?>

View File

@ -0,0 +1,46 @@
<?php the_header(
'Change password ' . CURRENT_USER->field_username,
'View and manage your personal information, update account settings, and track your activity on the Profile page. Customize your preferences and keep your details up-to-date easily.',
'profile-edit',
[
['robots', 'nofollow, noindex']
]) ?>
<div class="container">
<h1 class="p-title">Change password</h1>
<form class="form form-edit" method="post">
<?php
if(isset($context['error_message']))
the_alert($context['error_message']);
?>
<label for="field_old_password">Old Password <span>*</span></label>
<div class="input">
<input type="password" name="old_password" id="field_old_password" required>
</div>
<label for="field_new_password">New password <span>*</span></label>
<div class="input">
<input type="password" name="new_password" id="field_new_password" required>
</div>
<label for="field_repeat_password">Repeat new password <span>*</span></label>
<div class="input">
<input type="password" name="repeat_password" id="field_repeat_password" required>
</div>
<?php
if(isset($context['error_form']))
$context['error_form']->display_error();
?>
<div class="btn-control">
<a class="a-back" href="<?php the_permalink('users:profile') ?>">< Back</a>
<button class="btn btn-primary" type="submit">Save</button>
</div>
</form>
</div>
<?php the_footer() ?>

View File

@ -0,0 +1,6 @@
<?php
function the_user_nav()
{
include APPS_PATH . '/Users/Templates/components/profile-nav.php';
}

67
apps/Users/functions.php Normal file
View File

@ -0,0 +1,67 @@
<?php
// Recovery password
function get_recovery_email_template(string $username, string $url_to_recovery)
{
$preheader = 'Reset your password quickly and securely. Click the link to regain access to your account.';
ob_start();
require APPS_PATH . '/Users/Templates/email_templates/recovery_pass.php';
return ob_get_clean();
}
function get_recovery_email_alt_template(string $username, string $url_to_recovery)
{
ob_start();
require APPS_PATH . '/Users/Templates/email_templates/recovery_pass_alt.php';
return ob_get_clean();
}
// Reset password completed
function get_reset_completed_email_template(string $username)
{
$preheader = 'Your Password Has Been Successfully Updated.';
ob_start();
require APPS_PATH . '/Users/Templates/email_templates/reset_password_compl.php';
return ob_get_clean();
}
function get_reset_completed_email_alt_template(string $username)
{
ob_start();
require APPS_PATH . '/Users/Templates/email_templates/reset_password_compl_alt.php';
return ob_get_clean();
}
// Activation account
function get_activation_email_template(string $username, string $url_to_activate)
{
$preheader = 'Click the link to activate your account.';
ob_start();
require APPS_PATH . '/Users/Templates/email_templates/activate_account.php';
return ob_get_clean();
}
function get_activation_email_alt_template(string $username, string $url_to_activate)
{
ob_start();
require APPS_PATH . '/Users/Templates/email_templates/activate_account_alt.php';
return ob_get_clean();
}
// Ban
use Lycoreco\Apps\Users\Models\BanlistModel;
function get_ban_template(string $username, BanlistModel $model)
{
$preheader = 'Your account has been suspended due to a policy violation.';
ob_start();
require APPS_PATH . '/Users/Templates/email_templates/ban.php';
return ob_get_clean();
}
function get_ban_alt_template(string $username, BanlistModel $model)
{
ob_start();
require APPS_PATH . '/Users/Templates/email_templates/ban_alt.php';
return ob_get_clean();
}

26
apps/Users/urls.php Normal file
View File

@ -0,0 +1,26 @@
<?php
use Lycoreco\Apps\Users\Controllers;
use Lycoreco\Includes\Routing\Path;
$users_urls = [
// Login and register
new Path('/login', new Controllers\LoginController(), 'login'),
new Path('/logout', new Controllers\LogoutController(), 'logout'),
new Path('/register', new Controllers\RegisterController(), 'register'),
// Profile
new Path('/user', new Controllers\ProfileController(), 'profile'),
// Edit information
new Path('/user/edit/information', new Controllers\EditContactInfoController(), 'edit-info'),
new Path('/user/edit/email', new Controllers\EditEmailController(), 'edit-email'),
new Path('/user/edit/password', new Controllers\EditPasswordController(), 'edit-password'),
// Reset password
new Path('/forgot-password', new Controllers\ForgotPasswordController(), 'forgot'),
new Path('/reset-password/[:string]', new Controllers\ResetPasswordController(), 'reset'),
// Activation
new Path('/activate-account/[:string]', new Controllers\ActivationController(), 'activate')
];

219
assets/css/admin.css Normal file
View File

@ -0,0 +1,219 @@
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
}
.wrapper-admin {
display: flex;
min-height: calc(100vh - 64px);
}
.wrapper-admin__content {
width: 100%;
}
.header-admin {
background: var(--dark-block-background);
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px 10px;
}
.header-admin__control {
display: flex;
}
.header-admin__control .username {
color: var(--h-color);
font-family: var(--font-family-header);
margin-right: 10px;
}
.header-admin__control .username span {
color: var(--link-color);
}
.header-admin__control .links a {
color: var(--h-color);
text-decoration: none;
}
.admin-sidebar {
width: 100%;
max-width: 314px;
flex-shrink: 0;
background: var(--block-background);
}
.admin-sidebar .btn {
display: block;
margin: 20px 20px 20px 20px;
}
.admin-sidebar__list {
list-style: none;
}
.admin-sidebar__list a {
display: block;
padding: 10px 20px;
color: var(--h-color);
font-size: 20px;
text-decoration: none;
}
.admin-sidebar__list a:hover,
.admin-sidebar__list a.active {
background: #ffffff2e;
}
.admin-sidebar__list a i {
width: 34px;
}
.admin-sidebar__list a span {
background: #f00;
padding: 2px 7px;
font-size: 15px;
border-radius: 100%;
}
.admin-container {
margin: 0 auto;
width: 100%;
max-width: 1100px;
padding: 22px 30px;
}
.admin-container h2 {
font-size: 24px;
margin-bottom: 20px;
}
.admin-container section {
margin-bottom: 25px;
}
#dashboard-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
.dashboard-stats__item {
display: flex;
align-items: center;
color: var(--h-color);
background: var(--block-background);
border-radius: 5px;
padding: 12px 20px;
}
.dashboard-stats__item .icon {
font-size: 36px;
margin-right: 20px;
flex-shrink: 0;
}
.dashboard-stats__item .value {
font-size: 24px;
font-weight: 700;
}
.dashboard-stats__item.top-sales .icon {
color: #FF7B00;
}
.dashboard-stats__item.new-users .icon {
color: #00A3E8;
}
.dashboard-stats__item.orders .icon {
color: #BA00E8;
}
.dashboard-stats__item.profit .icon {
color: #00E842;
}
#quicktools {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 22px;
background: var(--block-background);
border-radius: 5px;
}
.admin-table {
color: var(--h-color);
margin-bottom: 15px;
font-size: 14px;
width: 100%;
text-align: center;
border-radius: 5px;
overflow: hidden;
}
.admin-table thead {
background: var(--block-background);
}
.admin-table td,
.admin-table th {
padding: 15px 10px;
}
.admin-block__content .admin-block__table {
border-radius: 5px;
overflow: hidden;
margin-bottom: 10px;
}
.admin-table tbody tr:nth-child(odd),
.admin-block__table .row:nth-child(odd) {
background: #9f9f9f;
}
.admin-table tbody tr:nth-child(even),
.admin-block__table .row:nth-child(even) {
background: #767676;
}
.admin-block__table {
color: var(--h-color);
}
.admin-block__table .row {
display: flex;
justify-content: space-between;
}
.admin-single {
display: grid;
grid-template-columns: 1fr 320px;
gap: 20px;
}
.admin-block {
overflow: hidden;
border-radius: 5px;
border: 1px solid var(--block-background);
margin-bottom: 20px;
}
.admin-block__content {
padding: 10px;
}
.admin-block__title {
font-size: 18px;
font-family: var(--font-family-header);
background: var(--block-background);
padding: 10px 10px;
color: var(--h-color);
}
.admin-block__table .row {
padding: 10px;
}
.admin-container.delete {
text-align: center;
}
.admin-container.delete .meta-text {
font-size: 18px;
margin-bottom: 15px;
}
.admin-container.delete .btn-control {
justify-content: space-around;
}
.order-stat {
display: flex;
align-items: center;
margin-bottom: 5px !important;
font-size: 18px;
justify-content: space-between;
}
.order-content .admin-block__table {
margin-top: 15px;
}
.order-stat span:first-child {
color: var(--text-color);
font-weight: 400;
font-family: var(--font-family-header);
}
.order-stat span:last-child {
color: var(--h-color);
}

94
assets/css/reset.css Normal file
View File

@ -0,0 +1,94 @@
html, body, div, span, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
abbr, address, cite, code,
del, dfn, em, img, ins, kbd, q, samp,
small, strong, sub, sup, var,
b, i,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, figcaption, figure,
footer, header, hgroup, menu, nav, section, summary,
time, mark, audio, video {
margin:0;
padding:0;
border:0;
outline:0;
font-size:100%;
vertical-align:baseline;
background:transparent;
}
body {
line-height:1;
}
article,aside,details,figcaption,figure,
footer,header,hgroup,menu,nav,section {
display:block;
}
nav ul {
list-style:none;
}
blockquote, q {
quotes:none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content:'';
content:none;
}
a {
margin:0;
padding:0;
font-size:100%;
vertical-align:baseline;
background:transparent;
}
/* change colours to suit your needs */
ins {
background-color:#ff9;
color:#000;
text-decoration:none;
}
/* change colours to suit your needs */
mark {
background-color:#ff9;
color:#000;
font-style:italic;
font-weight:bold;
}
del {
text-decoration: line-through;
}
abbr[title], dfn[title] {
border-bottom:1px dotted;
cursor:help;
}
table {
border-collapse:collapse;
border-spacing:0;
}
/* change border colour to suit your needs */
hr {
display:block;
height:1px;
border:0;
border-top:1px solid #cccccc;
margin:1em 0;
padding:0;
}
input, select {
vertical-align:middle;
}

0
assets/css/style.css Normal file
View File

View File

@ -0,0 +1,165 @@
Fonticons, Inc. (https://fontawesome.com)
--------------------------------------------------------------------------------
Font Awesome Free License
Font Awesome Free is free, open source, and GPL friendly. You can use it for
commercial projects, open source projects, or really almost whatever you want.
Full Font Awesome Free license: https://fontawesome.com/license/free.
--------------------------------------------------------------------------------
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
The Font Awesome Free download is licensed under a Creative Commons
Attribution 4.0 International License and applies to all icons packaged
as SVG and JS file types.
--------------------------------------------------------------------------------
# Fonts: SIL OFL 1.1 License
In the Font Awesome Free download, the SIL OFL license applies to all icons
packaged as web and desktop font files.
Copyright (c) 2024 Fonticons, Inc. (https://fontawesome.com)
with Reserved Font Name: "Font Awesome".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
SIL OPEN FONT LICENSE
Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting — in part or in whole — any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
--------------------------------------------------------------------------------
# Code: MIT License (https://opensource.org/licenses/MIT)
In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files.
Copyright 2024 Fonticons, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
# Attribution
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
Awesome Free files already contain embedded comments with sufficient
attribution, so you shouldn't need to do anything additional when using these
files normally.
We've kept attribution comments terse, so we ask that you do not actively work
to remove them from files, especially code. They're a great way for folks to
learn about Font Awesome.
--------------------------------------------------------------------------------
# Brand Icons
All brand icons are trademarks of their respective owners. The use of these
trademarks does not indicate endorsement of the trademark holder by Font
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
to represent the company, product, or service to which they refer.**

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,19 @@
/*!
* Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2024 Fonticons, Inc.
*/
:root, :host {
--fa-style-family-classic: 'Font Awesome 6 Free';
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free'; }
@font-face {
font-family: 'Font Awesome 6 Free';
font-style: normal;
font-weight: 400;
font-display: block;
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }
.far,
.fa-regular {
font-weight: 400; }

View File

@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2024 Fonticons, Inc.
*/
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}

View File

@ -0,0 +1,19 @@
/*!
* Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2024 Fonticons, Inc.
*/
:root, :host {
--fa-style-family-classic: 'Font Awesome 6 Free';
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free'; }
@font-face {
font-family: 'Font Awesome 6 Free';
font-style: normal;
font-weight: 900;
font-display: block;
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
.fas,
.fa-solid {
font-weight: 900; }

View File

@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2024 Fonticons, Inc.
*/
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}

View File

@ -0,0 +1,459 @@
/*!
* Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2024 Fonticons, Inc.
*/
:root, :host {
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free';
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free';
--fa-font-light: normal 300 1em/1 'Font Awesome 6 Pro';
--fa-font-thin: normal 100 1em/1 'Font Awesome 6 Pro';
--fa-font-duotone: normal 900 1em/1 'Font Awesome 6 Duotone';
--fa-font-brands: normal 400 1em/1 'Font Awesome 6 Brands';
--fa-font-sharp-solid: normal 900 1em/1 'Font Awesome 6 Sharp';
--fa-font-sharp-regular: normal 400 1em/1 'Font Awesome 6 Sharp';
--fa-font-sharp-light: normal 300 1em/1 'Font Awesome 6 Sharp';
--fa-font-sharp-thin: normal 100 1em/1 'Font Awesome 6 Sharp';
--fa-font-sharp-duotone-solid: normal 900 1em/1 'Font Awesome 6 Sharp Duotone'; }
svg:not(:root).svg-inline--fa, svg:not(:host).svg-inline--fa {
overflow: visible;
box-sizing: content-box; }
.svg-inline--fa {
display: var(--fa-display, inline-block);
height: 1em;
overflow: visible;
vertical-align: -.125em; }
.svg-inline--fa.fa-2xs {
vertical-align: 0.1em; }
.svg-inline--fa.fa-xs {
vertical-align: 0em; }
.svg-inline--fa.fa-sm {
vertical-align: -0.07143em; }
.svg-inline--fa.fa-lg {
vertical-align: -0.2em; }
.svg-inline--fa.fa-xl {
vertical-align: -0.25em; }
.svg-inline--fa.fa-2xl {
vertical-align: -0.3125em; }
.svg-inline--fa.fa-pull-left {
margin-right: var(--fa-pull-margin, 0.3em);
width: auto; }
.svg-inline--fa.fa-pull-right {
margin-left: var(--fa-pull-margin, 0.3em);
width: auto; }
.svg-inline--fa.fa-li {
width: var(--fa-li-width, 2em);
top: 0.25em; }
.svg-inline--fa.fa-fw {
width: var(--fa-fw-width, 1.25em); }
.fa-layers svg.svg-inline--fa {
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0; }
.fa-layers-text, .fa-layers-counter {
display: inline-block;
position: absolute;
text-align: center; }
.fa-layers {
display: inline-block;
height: 1em;
position: relative;
text-align: center;
vertical-align: -.125em;
width: 1em; }
.fa-layers svg.svg-inline--fa {
transform-origin: center center; }
.fa-layers-text {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
transform-origin: center center; }
.fa-layers-counter {
background-color: var(--fa-counter-background-color, #ff253a);
border-radius: var(--fa-counter-border-radius, 1em);
box-sizing: border-box;
color: var(--fa-inverse, #fff);
line-height: var(--fa-counter-line-height, 1);
max-width: var(--fa-counter-max-width, 5em);
min-width: var(--fa-counter-min-width, 1.5em);
overflow: hidden;
padding: var(--fa-counter-padding, 0.25em 0.5em);
right: var(--fa-right, 0);
text-overflow: ellipsis;
top: var(--fa-top, 0);
transform: scale(var(--fa-counter-scale, 0.25));
transform-origin: top right; }
.fa-layers-bottom-right {
bottom: var(--fa-bottom, 0);
right: var(--fa-right, 0);
top: auto;
transform: scale(var(--fa-layers-scale, 0.25));
transform-origin: bottom right; }
.fa-layers-bottom-left {
bottom: var(--fa-bottom, 0);
left: var(--fa-left, 0);
right: auto;
top: auto;
transform: scale(var(--fa-layers-scale, 0.25));
transform-origin: bottom left; }
.fa-layers-top-right {
top: var(--fa-top, 0);
right: var(--fa-right, 0);
transform: scale(var(--fa-layers-scale, 0.25));
transform-origin: top right; }
.fa-layers-top-left {
left: var(--fa-left, 0);
right: auto;
top: var(--fa-top, 0);
transform: scale(var(--fa-layers-scale, 0.25));
transform-origin: top left; }
.fa-1x {
font-size: 1em; }
.fa-2x {
font-size: 2em; }
.fa-3x {
font-size: 3em; }
.fa-4x {
font-size: 4em; }
.fa-5x {
font-size: 5em; }
.fa-6x {
font-size: 6em; }
.fa-7x {
font-size: 7em; }
.fa-8x {
font-size: 8em; }
.fa-9x {
font-size: 9em; }
.fa-10x {
font-size: 10em; }
.fa-2xs {
font-size: 0.625em;
line-height: 0.1em;
vertical-align: 0.225em; }
.fa-xs {
font-size: 0.75em;
line-height: 0.08333em;
vertical-align: 0.125em; }
.fa-sm {
font-size: 0.875em;
line-height: 0.07143em;
vertical-align: 0.05357em; }
.fa-lg {
font-size: 1.25em;
line-height: 0.05em;
vertical-align: -0.075em; }
.fa-xl {
font-size: 1.5em;
line-height: 0.04167em;
vertical-align: -0.125em; }
.fa-2xl {
font-size: 2em;
line-height: 0.03125em;
vertical-align: -0.1875em; }
.fa-fw {
text-align: center;
width: 1.25em; }
.fa-ul {
list-style-type: none;
margin-left: var(--fa-li-margin, 2.5em);
padding-left: 0; }
.fa-ul > li {
position: relative; }
.fa-li {
left: calc(-1 * var(--fa-li-width, 2em));
position: absolute;
text-align: center;
width: var(--fa-li-width, 2em);
line-height: inherit; }
.fa-border {
border-color: var(--fa-border-color, #eee);
border-radius: var(--fa-border-radius, 0.1em);
border-style: var(--fa-border-style, solid);
border-width: var(--fa-border-width, 0.08em);
padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); }
.fa-pull-left {
float: left;
margin-right: var(--fa-pull-margin, 0.3em); }
.fa-pull-right {
float: right;
margin-left: var(--fa-pull-margin, 0.3em); }
.fa-beat {
animation-name: fa-beat;
animation-delay: var(--fa-animation-delay, 0s);
animation-direction: var(--fa-animation-direction, normal);
animation-duration: var(--fa-animation-duration, 1s);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
.fa-bounce {
animation-name: fa-bounce;
animation-delay: var(--fa-animation-delay, 0s);
animation-direction: var(--fa-animation-direction, normal);
animation-duration: var(--fa-animation-duration, 1s);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); }
.fa-fade {
animation-name: fa-fade;
animation-delay: var(--fa-animation-delay, 0s);
animation-direction: var(--fa-animation-direction, normal);
animation-duration: var(--fa-animation-duration, 1s);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
.fa-beat-fade {
animation-name: fa-beat-fade;
animation-delay: var(--fa-animation-delay, 0s);
animation-direction: var(--fa-animation-direction, normal);
animation-duration: var(--fa-animation-duration, 1s);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
.fa-flip {
animation-name: fa-flip;
animation-delay: var(--fa-animation-delay, 0s);
animation-direction: var(--fa-animation-direction, normal);
animation-duration: var(--fa-animation-duration, 1s);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
.fa-shake {
animation-name: fa-shake;
animation-delay: var(--fa-animation-delay, 0s);
animation-direction: var(--fa-animation-direction, normal);
animation-duration: var(--fa-animation-duration, 1s);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-timing-function: var(--fa-animation-timing, linear); }
.fa-spin {
animation-name: fa-spin;
animation-delay: var(--fa-animation-delay, 0s);
animation-direction: var(--fa-animation-direction, normal);
animation-duration: var(--fa-animation-duration, 2s);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-timing-function: var(--fa-animation-timing, linear); }
.fa-spin-reverse {
--fa-animation-direction: reverse; }
.fa-pulse,
.fa-spin-pulse {
animation-name: fa-spin;
animation-direction: var(--fa-animation-direction, normal);
animation-duration: var(--fa-animation-duration, 1s);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-timing-function: var(--fa-animation-timing, steps(8)); }
@media (prefers-reduced-motion: reduce) {
.fa-beat,
.fa-bounce,
.fa-fade,
.fa-beat-fade,
.fa-flip,
.fa-pulse,
.fa-shake,
.fa-spin,
.fa-spin-pulse {
animation-delay: -1ms;
animation-duration: 1ms;
animation-iteration-count: 1;
transition-delay: 0s;
transition-duration: 0s; } }
@keyframes fa-beat {
0%, 90% {
transform: scale(1); }
45% {
transform: scale(var(--fa-beat-scale, 1.25)); } }
@keyframes fa-bounce {
0% {
transform: scale(1, 1) translateY(0); }
10% {
transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); }
30% {
transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); }
50% {
transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); }
57% {
transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); }
64% {
transform: scale(1, 1) translateY(0); }
100% {
transform: scale(1, 1) translateY(0); } }
@keyframes fa-fade {
50% {
opacity: var(--fa-fade-opacity, 0.4); } }
@keyframes fa-beat-fade {
0%, 100% {
opacity: var(--fa-beat-fade-opacity, 0.4);
transform: scale(1); }
50% {
opacity: 1;
transform: scale(var(--fa-beat-fade-scale, 1.125)); } }
@keyframes fa-flip {
50% {
transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } }
@keyframes fa-shake {
0% {
transform: rotate(-15deg); }
4% {
transform: rotate(15deg); }
8%, 24% {
transform: rotate(-18deg); }
12%, 28% {
transform: rotate(18deg); }
16% {
transform: rotate(-22deg); }
20% {
transform: rotate(22deg); }
32% {
transform: rotate(-12deg); }
36% {
transform: rotate(12deg); }
40%, 100% {
transform: rotate(0deg); } }
@keyframes fa-spin {
0% {
transform: rotate(0deg); }
100% {
transform: rotate(360deg); } }
.fa-rotate-90 {
transform: rotate(90deg); }
.fa-rotate-180 {
transform: rotate(180deg); }
.fa-rotate-270 {
transform: rotate(270deg); }
.fa-flip-horizontal {
transform: scale(-1, 1); }
.fa-flip-vertical {
transform: scale(1, -1); }
.fa-flip-both,
.fa-flip-horizontal.fa-flip-vertical {
transform: scale(-1, -1); }
.fa-rotate-by {
transform: rotate(var(--fa-rotate-angle, 0)); }
.fa-stack {
display: inline-block;
vertical-align: middle;
height: 2em;
position: relative;
width: 2.5em; }
.fa-stack-1x,
.fa-stack-2x {
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0;
z-index: var(--fa-stack-z-index, auto); }
.svg-inline--fa.fa-stack-1x {
height: 1em;
width: 1.25em; }
.svg-inline--fa.fa-stack-2x {
height: 2em;
width: 2.5em; }
.fa-inverse {
color: var(--fa-inverse, #fff); }
.sr-only,
.fa-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0; }
.sr-only-focusable:not(:focus),
.fa-sr-only-focusable:not(:focus) {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0; }
.svg-inline--fa .fa-primary {
fill: var(--fa-primary-color, currentColor);
opacity: var(--fa-primary-opacity, 1); }
.svg-inline--fa .fa-secondary {
fill: var(--fa-secondary-color, currentColor);
opacity: var(--fa-secondary-opacity, 0.4); }
.svg-inline--fa.fa-swap-opacity .fa-primary {
opacity: var(--fa-secondary-opacity, 0.4); }
.svg-inline--fa.fa-swap-opacity .fa-secondary {
opacity: var(--fa-primary-opacity, 1); }
.svg-inline--fa mask .fa-primary,
.svg-inline--fa mask .fa-secondary {
fill: black; }
.fad.fa-inverse,
.fa-duotone.fa-inverse {
color: var(--fa-inverse, #fff); }

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,26 @@
/*!
* Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2024 Fonticons, Inc.
*/
@font-face {
font-family: 'FontAwesome';
font-display: block;
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
@font-face {
font-family: 'FontAwesome';
font-display: block;
src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
@font-face {
font-family: 'FontAwesome';
font-display: block;
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype");
unicode-range: U+F003,U+F006,U+F014,U+F016-F017,U+F01A-F01B,U+F01D,U+F022,U+F03E,U+F044,U+F046,U+F05C-F05D,U+F06E,U+F070,U+F087-F088,U+F08A,U+F094,U+F096-F097,U+F09D,U+F0A0,U+F0A2,U+F0A4-F0A7,U+F0C5,U+F0C7,U+F0E5-F0E6,U+F0EB,U+F0F6-F0F8,U+F10C,U+F114-F115,U+F118-F11A,U+F11C-F11D,U+F133,U+F147,U+F14E,U+F150-F152,U+F185-F186,U+F18E,U+F190-F192,U+F196,U+F1C1-F1C9,U+F1D9,U+F1DB,U+F1E3,U+F1EA,U+F1F7,U+F1F9,U+F20A,U+F247-F248,U+F24A,U+F24D,U+F255-F25B,U+F25D,U+F271-F274,U+F278,U+F27B,U+F28C,U+F28E,U+F29C,U+F2B5,U+F2B7,U+F2BA,U+F2BC,U+F2BE,U+F2C0-F2C1,U+F2C3,U+F2D0,U+F2D2,U+F2D4,U+F2DC; }
@font-face {
font-family: 'FontAwesome';
font-display: block;
src: url("../webfonts/fa-v4compatibility.woff2") format("woff2"), url("../webfonts/fa-v4compatibility.ttf") format("truetype");
unicode-range: U+F041,U+F047,U+F065-F066,U+F07D-F07E,U+F080,U+F08B,U+F08E,U+F090,U+F09A,U+F0AC,U+F0AE,U+F0B2,U+F0D0,U+F0D6,U+F0E4,U+F0EC,U+F10A-F10B,U+F123,U+F13E,U+F148-F149,U+F14C,U+F156,U+F15E,U+F160-F161,U+F163,U+F175-F178,U+F195,U+F1F8,U+F219,U+F27A; }

View File

@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2024 Fonticons, Inc.
*/
@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,22 @@
/*!
* Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2024 Fonticons, Inc.
*/
@font-face {
font-family: 'Font Awesome 5 Brands';
font-display: block;
font-weight: 400;
src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
@font-face {
font-family: 'Font Awesome 5 Free';
font-display: block;
font-weight: 900;
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
@font-face {
font-family: 'Font Awesome 5 Free';
font-display: block;
font-weight: 400;
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }

View File

@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2024 Fonticons, Inc.
*/
@font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

BIN
assets/images/user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

13
assets/js/catalog.js Normal file
View File

@ -0,0 +1,13 @@
let filters = document.querySelector('.filters');
let filterOpenBtn = document.getElementById('catalog-filter-btn');
filters.addEventListener('click', (e) => {
if(e.target == filters) {
filters.classList.remove('active');
blockScroll(false);
}
});
filterOpenBtn.addEventListener('click', (e) => {
filters.classList.add('active');
blockScroll(true);
});

18
assets/js/index.js Normal file
View File

@ -0,0 +1,18 @@
new Swiper(".recent-keys", {
slidesPerView: 1,
spaceBetween: 20,
loop: true,
autoplay: {
delay: 3000,
disableOnInteraction: false,
},
breakpoints: {
630: {
slidesPerView: 2,
},
930: {
slidesPerView: 3,
}
}
});

Some files were not shown because too many files have changed in this diff Show More