Completed web programming project #44

Merged
steve_dekart merged 117 commits from develop into master 2025-07-07 19:00:18 +02:00
86 changed files with 4418 additions and 634 deletions

9
.htaccess Normal file
View File

@ -0,0 +1,9 @@
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>

View File

@ -22,8 +22,11 @@ The FridgeBites was developed almost entirely from scratch using PHP, without re
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: 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. - [\[PHP\] PHPMailer](https://github.com/PHPMailer/PHPMailer): A library for sending emails, used for password recovery functionality.
- [\[PHP\] FPHP](https://www.fpdf.org/): library which allows to generate PDF files with pure PHP, that is to say without using the PDFlib library.
- [\[JS\] Swiper.js](https://github.com/nolimits4web/swiper): A highly customizable library for creating sliders and carousels. - [\[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. - [\[JS\] Toastify.js](https://github.com/apvarun/toastify-js): A lightweight library for creating beautiful and customizable toast notifications.
- [\[JS\] Three.js](https://github.com/mrdoob/three.js/): project is to create an easy-to-use, lightweight, cross-browser, general-purpose 3D library.
- [\[JS\] qrcode.js](https://davidshimjs.github.io/qrcodejs/): javascript library for making QRCode. QRCode.js supports Cross-browser with HTML5 Canvas and table tag in DOM. QRCode.js has no dependencies.
## Installing ## Installing

View File

@ -38,6 +38,7 @@ abstract class AdminSingleController extends AdminBaseController
protected $field_title = 'field_id'; protected $field_title = 'field_id';
protected $edit_title_template = 'Edit [:verbose] "[:field]"'; protected $edit_title_template = 'Edit [:verbose] "[:field]"';
protected $can_save = true; protected $can_save = true;
protected $is_new = false;
/** /**
* Function names with $object attribute. * Function names with $object attribute.
@ -53,6 +54,7 @@ abstract class AdminSingleController extends AdminBaseController
*/ */
public function __construct($is_new = false) public function __construct($is_new = false)
{ {
$this->is_new = $is_new;
$this->context['is_new'] = $is_new; $this->context['is_new'] = $is_new;
} }
@ -137,7 +139,7 @@ abstract class AdminSingleController extends AdminBaseController
case 'image': case 'image':
$file = $_FILES[$field['model_field']]; $file = $_FILES[$field['model_field']];
if (isset($file)) { if (isset($file)) {
$path = upload_file($file, $this->model_сlass_name . '/', 'image'); $path = upload_file($file, $this->model_сlass_name::get_table_name() . '/', 'image');
if (!empty($path)) { if (!empty($path)) {
$field_value = $path; $field_value = $path;

View File

@ -0,0 +1,21 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers;
use Lycoreco\Includes\Model\ValidationError;
use Lycoreco\Apps\Recipes\Models\CategoryModel;
class AdminCategoryController extends Abstract\AdminSingleController
{
protected $model_сlass_name = "Lycoreco\Apps\Recipes\Models\CategoryModel";
protected $field_title = 'field_name';
protected $verbose_name = 'category';
protected $object_router_name = 'admin:category';
protected $fields = array(
[
'model_field' => 'name',
'input_type' => 'text',
'input_attrs' => ['required']
]
);
}

View File

@ -0,0 +1,17 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers;
use Lycoreco\Includes\Model\ValidationError;
use Lycoreco\Apps\Recipes\Models\CategoryModel;
class AdminCategoryListController extends Abstract\AdminListController
{
protected $model_сlass_name = "Lycoreco\Apps\Recipes\Models\CategoryModel";
protected $table_fields = array(
'Name' => 'field_name'
);
protected $single_router_name = 'admin:category';
protected $create_router_name = 'admin:category-new';
protected $verbose_name = "category";
protected $verbose_name_multiply = "categories";
}

View File

@ -9,6 +9,55 @@ use Lycoreco\Apps\Users\Models\UserModel;
*/ */
class AdminDeleteController extends Abstract\AdminBaseController class AdminDeleteController extends Abstract\AdminBaseController
{ {
const DELETE_MODELS = [
[
'url_name' => 'users',
'model' => 'Lycoreco\Apps\Users\Models\UserModel',
'field_display' => 'field_username',
'back_to' => 'admin:user',
'success_to' => 'admin:user-list'
],
[
'url_name' => 'user-banlist',
'model' => 'Lycoreco\Apps\Users\Models\BanlistModel',
'field_display' => 'field_reason',
'back_to' => 'admin:ban'
],
[
'url_name' => 'recipes',
'model' => 'Lycoreco\Apps\Recipes\Models\RecipeModel',
'field_display' => 'field_title',
'back_to' => 'admin:recipe',
'success_to' => 'admin:recipe-list'
],
[
'url_name' => 'ingredients',
'model' => 'Lycoreco\Apps\Recipes\Models\IngredientModel',
'field_display' => 'field_name',
'back_to' => 'admin:ingredient',
'success_to' => 'admin:ingredient-list'
],
[
'url_name' => 'categories',
'model' => 'Lycoreco\Apps\Recipes\Models\CategoryModel',
'field_display' => 'field_name',
'back_to' => 'admin:category',
'success_to' => 'admin:category-list'
],
[
'url_name' => 'recipe-ingredients',
'model' => 'Lycoreco\Apps\Recipes\Models\IngredientInRecipeModel',
'field_display' => 'ingredient_name',
'back_to' => 'admin:ing-cat-rel',
],
[
'url_name' => 'recipe-reviews',
'model' => 'Lycoreco\Apps\Recipes\Models\ReviewsModel',
'field_display' => 'field_title',
'back_to' => 'admin:review',
],
];
protected $template_name = APPS_PATH . '/Admin/Templates/delete.php'; protected $template_name = APPS_PATH . '/Admin/Templates/delete.php';
/** /**
@ -22,17 +71,15 @@ class AdminDeleteController extends Abstract\AdminBaseController
$id = $this->url_context['url_2']; $id = $this->url_context['url_2'];
$model_class = ''; $model_class = '';
switch ($this->url_context['url_1']) { foreach (self::DELETE_MODELS as $delete_model) {
case 'users': if($this->url_context['url_1'] == $delete_model['url_name']) {
$model_class = "Lycoreco\Apps\Users\Models\UserModel"; $model_class = $delete_model['model'];
break; break;
case 'user-banlist':
$model_class = "Lycoreco\Apps\Users\Models\BanlistModel";
break;
default:
return null;
} }
}
if(empty($model_class))
return null;
return $model_class::get(array( return $model_class::get(array(
[ [
'name' => 'obj.id', 'name' => 'obj.id',
@ -52,16 +99,12 @@ class AdminDeleteController extends Abstract\AdminBaseController
// Display field to show what's model // Display field to show what's model
$field = ''; $field = '';
$back_url = ''; $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': foreach (self::DELETE_MODELS as $delete_model) {
$back_url = get_permalink('admin:ban', [$model->get_id()]); if($this->url_context['url_1'] == $delete_model['url_name']) {
$field = 'field_reason'; $back_url = get_permalink($delete_model['back_to'], [$model->get_id()]);
break; $field = $delete_model['field_display'];
}
} }
$context['back_url'] = $back_url; $context['back_url'] = $back_url;
@ -80,15 +123,15 @@ class AdminDeleteController extends Abstract\AdminBaseController
$model->delete(); $model->delete();
// Redirect after 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'); $link = get_permalink('admin:home');
foreach (self::DELETE_MODELS as $delete_model) {
if($this->url_context['url_1'] == $delete_model['url_name']) {
if(isset($delete_model['success_to'])) {
$link = get_permalink($delete_model['success_to']);
}
break; break;
} }
}
redirect_to($link); redirect_to($link);
} }
} }

View File

@ -1,6 +1,12 @@
<?php <?php
namespace Lycoreco\Apps\Admin\Controllers; namespace Lycoreco\Apps\Admin\Controllers;
use Lycoreco\Apps\Recipes\Models\IngredientModel;
use Lycoreco\Apps\Recipes\Models\RecipeModel;
use Lycoreco\Apps\Recipes\Models\ReviewsModel;
use Lycoreco\Apps\Users\Models\BanlistModel;
use Lycoreco\Apps\Users\Models\UserModel;
class AdminHomeController extends Abstract\AdminBaseController class AdminHomeController extends Abstract\AdminBaseController
{ {
protected $template_name = APPS_PATH . '/Admin/Templates/home.php'; protected $template_name = APPS_PATH . '/Admin/Templates/home.php';
@ -13,6 +19,40 @@ class AdminHomeController extends Abstract\AdminBaseController
$datetime_month_ago = new \DateTime(); $datetime_month_ago = new \DateTime();
$datetime_month_ago->modify("-1 month"); $datetime_month_ago->modify("-1 month");
$datetime_month_ago_text = $datetime_month_ago->format('Y-m-d H:i:s' . '\'');
$context['user_count'] = UserModel::count(array(
[
'name' => 'obj.register_at',
'type' => '>=',
'value' => $datetime_month_ago_text
]
));
$context['recipes_count'] = RecipeModel::count(array(
[
'name' => 'obj.created_at',
'type' => '>=',
'value' => $datetime_month_ago_text
]
));
$context['ban_count'] = BanlistModel::count(array(
[
'name' => 'obj.created_at',
'type' => '>=',
'value' => $datetime_month_ago_text
]
));
$context['reviews_count'] = ReviewsModel::count(array(
[
'name' => 'obj.created_at',
'type' => '>=',
'value' => $datetime_month_ago_text
]
));
$context['latest_recipes'] = RecipeModel::filter(
array(),
['-obj.created_at']
);
return $context; return $context;

View File

@ -0,0 +1,78 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers;
use Lycoreco\Includes\Model\ValidationError;
use Lycoreco\Apps\Recipes\Models\{
RecipeModel,
CategoryModel
};
class AdminRecipeController extends Abstract\AdminSingleController
{
protected $model_сlass_name = "Lycoreco\Apps\Recipes\Models\RecipeModel";
protected $field_title = 'field_title';
protected $verbose_name = 'recipe';
protected $object_router_name = 'admin:recipe';
protected $component_widgets = ['the_recipe_author', 'the_recipe_ingredients'];
protected $fields = array(
[
'model_field' => 'title',
'input_type' => 'text',
'input_attrs' => ['required']
],
[
'model_field' => 'instruction',
'input_type' => 'textarea',
'input_attrs' => ['required']
],
[
'model_field' => 'image_url',
'input_type' => 'image',
'input_label' => 'Image',
],
[
'model_field' => 'estimated_time',
'input_type' => 'number',
'input_attrs' => ['required'],
'input_label' => 'Estimated time (min)'
],
[
'model_field' => 'estimated_price',
'input_type' => 'number',
'input_attrs' => ['required'],
'input_label' => 'Estimated price ($)'
],
[
'model_field' => 'status',
'input_type' => 'select',
'input_attrs' => ['required'],
'input_values' => RecipeModel::STATUS
],
[
'model_field' => 'created_at',
'input_type' => 'text',
'dynamic_save' => false,
'input_label' => 'Created at',
'input_attrs' => ['disabled']
]
);
protected function before_save(&$object)
{
if($this->is_new)
{
$object->field_author_id = CURRENT_USER->get_id();
}
}
public function __construct($is_new = false) {
parent::__construct($is_new);
$this->fields[] = [
'model_field' => 'category_id',
'input_type' => 'select',
'input_label' => 'Categories',
'input_attrs' => ['required'],
'input_values' => CategoryModel::get_cat_values()
];
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers;
use Lycoreco\Includes\Model\ValidationError;
use Lycoreco\Apps\Recipes\Models\RecipeModel;
class AdminRecipeListController extends Abstract\AdminListController
{
protected $model_сlass_name = "Lycoreco\Apps\Recipes\Models\RecipeModel";
protected $table_fields = array(
'Title' => 'field_title',
'Price' => 'get_price()',
'Status ' => 'get_status()',
'Creaated at' => 'field_created_at',
);
protected $single_router_name = 'admin:recipe';
protected $create_router_name = 'admin:recipe-new';
protected $verbose_name = "recipe";
protected $verbose_name_multiply = "recipes";
}

View File

@ -0,0 +1,44 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers;
use Lycoreco\Apps\Recipes\Models\ReviewsModel;
class AdminReviewControler extends Abstract\AdminSingleController
{
protected $model_сlass_name = "Lycoreco\Apps\Recipes\Models\ReviewsModel";
protected $field_title = 'field_title';
protected $verbose_name = 'review';
protected $object_router_name = 'admin:review';
protected $fields = array(
[
'model_field' => 'title',
'input_type' => 'text',
'input_attrs' => ['required']
],
[
'model_field' => 'rating',
'input_type' => 'number',
'input_attrs' => ['required']
],
[
'model_field' => 'content',
'input_type' => 'textarea',
'input_attrs' => ['required']
],
[
'model_field' => 'status',
'input_type' => 'select',
'input_attrs' => ['required'],
'input_values' => ReviewsModel::STATUS
],
[
'model_field' => 'created_at',
'input_type' => 'text',
'dynamic_save' => false,
'input_label' => 'Created at',
'input_attrs' => ['disabled']
]
);
}

View File

@ -0,0 +1,17 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers;
class AdminReviewListController extends Abstract\AdminListController
{
protected $model_сlass_name = "Lycoreco\Apps\Recipes\Models\ReviewsModel";
protected $table_fields = array(
'Title' => 'field_title',
'Rating' => 'field_rating',
'Status ' => 'get_status()',
'Creaated at' => 'field_created_at',
);
protected $single_router_name = 'admin:review';
protected $verbose_name = "review";
protected $verbose_name_multiply = "reviews";
}

View File

@ -0,0 +1,27 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers;
use Lycoreco\Includes\Model\ValidationError;
use Lycoreco\Apps\Recipes\Models\IngredientModel;
class IngredientController extends Abstract\AdminSingleController
{
protected $model_сlass_name = "Lycoreco\Apps\Recipes\Models\IngredientModel";
protected $field_title = 'field_name';
protected $verbose_name = 'ingredient';
protected $object_router_name = 'admin:ingredient';
protected $fields = array(
[
'model_field' => 'name',
'input_type' => 'text',
'input_attrs' => ['required']
],
[
'model_field' => 'unit_name',
'input_type' => 'text',
'input_attrs' => ['required'],
'input_label' => 'Unit name'
]
);
}

View File

@ -0,0 +1,18 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers;
use Lycoreco\Includes\Model\ValidationError;
use Lycoreco\Apps\Recipes\Models\IngredientModel;
class IngredientListController extends Abstract\AdminListController
{
protected $model_сlass_name = "Lycoreco\Apps\Recipes\Models\IngredientModel";
protected $table_fields = array(
'Name' => 'field_name',
'Unit' => 'field_unit_name'
);
protected $single_router_name = 'admin:ingredient';
protected $create_router_name = 'admin:ingredient-new';
protected $verbose_name = "ingredient";
protected $verbose_name_multiply = "ingredients";
}

View File

@ -0,0 +1,50 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers;
use Lycoreco\Apps\Recipes\Models\{
IngredientInRecipeModel,
IngredientModel
};
class IngredientRecipeRelController extends Abstract\AdminSingleController
{
protected $model_сlass_name = "Lycoreco\Apps\Recipes\Models\IngredientInRecipeModel";
protected $field_title = 'ingredient_name';
protected $object_router_name = 'admin:ing-cat-rel';
protected $verbose_name = "ingredient in recipe";
protected $component_widgets = ['the_recipe_ingredients_relation'];
protected $fields = array(
[
'model_field' => 'recipe_id',
'input_type' => 'number',
'dynamic_save' => false,
'input_label' => 'Recipe id',
'input_attrs' => ['required', 'disabled']
],
[
'model_field' => 'amount',
'input_type' => 'text',
'input_attrs' => ['required']
],
);
protected function get_model()
{
$model = parent::get_model();
if($this->context['is_new'])
$model->field_recipe_id = (int)$this->url_context['url_1'];
return $model;
}
public function __construct($is_new = false) {
parent::__construct($is_new);
$this->fields[] = [
'model_field' => 'ingredient_id',
'input_type' => 'select',
'input_label' => 'Ingedient',
'input_attrs' => ['required'],
'input_values' => IngredientModel::get_ing_values()
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers;
class IngredientRecipeRelListController extends Abstract\AdminListController
{
protected $model_сlass_name = "Lycoreco\Apps\Recipes\Models\IngredientInRecipeModel";
protected $table_fields = array(
'Name' => 'ingredient_name',
'Amount' => 'get_count()'
);
protected $single_router_name = 'admin:ing-cat-rel';
protected $verbose_name = "ingredients in recipe";
protected $verbose_name_multiply = "ingredients in recipe";
public function custom_filter_fields()
{
$recipe_id = $this->url_context['url_1'];
return array(
[
'name' => 'obj.recipe_id',
'type' => '=',
'value' => $recipe_id
]
);
}
}

View File

@ -1,19 +1,56 @@
<?php <?php
use Lycoreco\Includes\Routing\Router; use Lycoreco\Includes\Routing\Router;
$sidebar_links = [
[
'name' => 'Dashboard',
'icon' => 'fa-solid fa-house',
'router_name' => 'admin:home',
],
[
'name' => 'Users',
'icon' => 'fa-solid fa-users',
'router_name' => 'admin:user-list',
],
[
'name' => 'Recipes',
'icon' => 'fa-solid fa-bowl-food',
'router_name' => 'admin:recipe-list'
],
[
'name' => 'Categories',
'icon' => 'fa-solid fa-tag',
'router_name' => 'admin:category-list'
],
[
'name' => 'Ingredients',
'icon' => 'fa-solid fa-carrot',
'router_name' => 'admin:ingredient-list'
],
[
'name' => 'Reviews',
'icon' => 'fa-solid fa-comment',
'router_name' => 'admin:review-list'
]
];
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo get_title_website($title); ?></title> <title><?php echo get_title_website($title); ?></title>
<link rel="shortcut icon" type="image/png" href="<?php echo ASSETS_PATH . '/favicon.png' ?>"> <link rel="shortcut icon" type="image/x-icon" href="<?php echo ASSETS_PATH . '/images/favicon.ico' ?>">
<!-- Google fonts -->
<!-- Google fonts --> <!-- Google fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <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"> <link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:ital,wght@0,100..900;1,100..900&family=Roboto+Slab:wght@100..900&display=swap"
rel="stylesheet">
<!-- Google fonts/ -->
<!-- Google fonts/ --> <!-- Google fonts/ -->
<!-- Font Awesome --> <!-- Font Awesome -->
@ -26,9 +63,10 @@ use Lycoreco\Includes\Routing\Router;
<link rel="stylesheet" href="<?php echo ASSETS_PATH . '/css/style.css' ?>"> <link rel="stylesheet" href="<?php echo ASSETS_PATH . '/css/style.css' ?>">
<link rel="stylesheet" href="<?php echo ASSETS_PATH . '/css/admin.css' ?>"> <link rel="stylesheet" href="<?php echo ASSETS_PATH . '/css/admin.css' ?>">
</head> </head>
<body> <body>
<header class="header-admin"> <header class="header-admin">
<div class="logo"> <div class="logo-admin">
FridgeBites Admin FridgeBites Admin
</div> </div>
<div class="header-admin__control"> <div class="header-admin__control">
@ -36,32 +74,27 @@ use Lycoreco\Includes\Routing\Router;
Hello, <span><?php echo CURRENT_USER->field_username ?></span> Hello, <span><?php echo CURRENT_USER->field_username ?></span>
</div> </div>
<div class="links"> <div class="links">
<a href="<?php the_permalink('index:home') ?>">View site</a> <a href="<?php the_permalink('index:home') ?>" class="hover-anim">View site</a>
| |
<a href="<?php the_permalink('users:logout') ?>">Log Out</a> <a href="<?php the_permalink('users:logout') ?>" class="hover-anim">Log Out</a>
</div> </div>
</div> </div>
</header> </header>
<div class="wrapper-admin"> <div class="wrapper-admin">
<aside class="admin-sidebar"> <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> <a href="<?php the_permalink('admin:recipe-new') ?>" class="btn btn-primary"><i
class="fa-solid fa-plus"></i> New recipe</a>
<hr> <hr>
<ul class="admin-sidebar__list"> <ul class="admin-sidebar__list">
<?php foreach ($sidebar_links as $link): ?>
<li> <li>
<a class="<?php echo Router::$current_router_name == 'admin:home' ? "active" : "" ?>" href="<?php the_permalink('admin:home') ?>"> <a class="<?php echo Router::$current_router_name == $link['router_name'] ? "active" : "" ?>"
<i class="fa-solid fa-house"></i> Dashboard href="<?php the_permalink($link['router_name']) ?>">
</a> <i class="<?= $link['icon'] ?>"></i> <?= $link['name'] ?>
</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> </a>
</li> </li>
<?php endforeach; ?>
</ul> </ul>
</aside> </aside>
<div class="wrapper-admin__content"> <div class="wrapper-admin__content">

View File

@ -2,36 +2,36 @@
<div class="admin-container"> <div class="admin-container">
<section> <section>
<h2>Stats per month</h2> <h2 class="title">Stats per month</h2>
<div id="dashboard-stats"> <div id="dashboard-stats">
<div class="dashboard-stats__item top-sales"> <div class="dashboard-stats__item top-sales">
<div class="icon"> <div class="icon">
<i class="fa-solid fa-bag-shopping"></i> <i class="fa-solid fa-bowl-food"></i>
</div> </div>
<div class="info"> <div class="info">
<div class="label">Total sales</div> <div class="label">New recipes</div>
<div class="value">0$</div> <div class="value"><?= $context['recipes_count'] ?></div>
</div> </div>
</div> </div>
<div class="dashboard-stats__item profit"> <div class="dashboard-stats__item profit">
<div class="icon"> <div class="icon">
<i class="fa-solid fa-money-bill-trend-up"></i> <i class="fa-solid fa-message"></i>
</div> </div>
<div class="info"> <div class="info">
<div class="label">Profit</div> <div class="label">New reviews</div>
<div class="value">0$</div> <div class="value"><?= $context['reviews_count'] ?></div>
</div> </div>
</div> </div>
<div class="dashboard-stats__item orders"> <div class="dashboard-stats__item orders">
<div class="icon"> <div class="icon">
<i class="fa-solid fa-cart-shopping"></i> <i class="fa-solid fa-user-slash"></i>
</div> </div>
<div class="info"> <div class="info">
<div class="label">Orders</div> <div class="label">Total bans</div>
<div class="value">0</div> <div class="value"><?= $context['ban_count'] ?></div>
</div> </div>
</div> </div>
@ -41,49 +41,47 @@
</div> </div>
<div class="info"> <div class="info">
<div class="label">New users</div> <div class="label">New users</div>
<div class="value">0</div> <div class="value"><?= $context['user_count'] ?></div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<section> <section>
<h2>Quick tools</h2> <h2 class="title">Quick tools</h2>
<div id="quicktools"> <div id="quicktools">
<a href="<?php the_permalink('admin:product-new') ?>" class="btn"> <a href="<?php the_permalink('admin:recipe-new') ?>" class="btn btn-secondary hover-anim">
<i class="fa-solid fa-plus"></i> New Product <i class="fa-solid fa-plus"></i> New recipe
</a> </a>
<form action="<?php the_permalink('admin:product-list') ?>" method="get"> <form action="<?php the_permalink('admin:recipe-list') ?>" method="get">
<div class="input"> <div class="input-admin">
<input type="text" name="s" placeholder="Search for products"> <input type="text" name="s" placeholder="Search for recipes">
<button type="submit"><i class="fa-solid fa-magnifying-glass"></i></button> <button class="btn btn-secondary hover-anim" type="submit"><i class="fa-solid fa-magnifying-glass"></i></button>
</div> </div>
</form> </form>
</div> </div>
</section> </section>
<h2>Latest orders</h2> <h2 class="title">Latest resipes</h2>
<table class="admin-table"> <table class="admin-table">
<thead> <thead>
<tr> <tr>
<th>Order number</th> <th>Title</th>
<th>Method</th> <th>Price</th>
<th>Total price</th> <th>Status</th>
<th>Buyer</th> <th>Created At</th>
<th>Created at</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($context['last_orders'] as $order): ?> <?php foreach ($context['latest_recipes'] as $recipe): ?>
<tr> <tr>
<td><a href="<?php the_permalink('admin:order', [$order->get_id()]) ?>"><?php echo $order->field_order_number ?></a></td> <td><a href="<?php the_permalink('admin:recipe', [$recipe->get_id()]) ?>"><?= $recipe->field_title ?></a></td>
<td><?php echo $order->field_method ?></td> <td><?= $recipe->get_price() ?></td>
<td><?php echo $order->get_total_price() ?></td> <td><?= $recipe->get_status() ?></td>
<td><?php echo $order->get_buyer_username() ?></td> <td><?= $recipe->field_created_at ?></td>
<td><?php echo $order->field_created_at ?></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>

View File

@ -1,21 +1,21 @@
<?php the_admin_header(ucfirst($context['verbose_name_multiply'])) ?> <?php the_admin_header(ucfirst($context['verbose_name_multiply'])) ?>
<div class="admin-container"> <div class="admin-container">
<h1 class="p-title"><?php echo ucfirst($context['verbose_name_multiply']) ?></h1> <h1 class="title"><?php echo ucfirst($context['verbose_name_multiply']) ?></h1>
<section> <section>
<div id="quicktools"> <div id="quicktools">
<?php if(isset($context['create_router_name'])): ?> <?php if(isset($context['create_router_name'])): ?>
<a href="<?php the_permalink($context['create_router_name']) ?>" class="btn"> <a href="<?php the_permalink($context['create_router_name']) ?>" class="btn btn-secondary hover-anim">
<i class="fa-solid fa-plus"></i> New <?php echo ucfirst($context['verbose_name']) ?> <i class="fa-solid fa-plus"></i> New <?php echo ucfirst($context['verbose_name']) ?>
</a> </a>
<?php else: ?> <?php else: ?>
<span></span> <span></span>
<?php endif; ?> <?php endif; ?>
<form method="get"> <form method="get">
<div class="input"> <div class="input-admin">
<input type="text" name="s" placeholder="Search for <?php echo $context['verbose_name_multiply'] ?>" value="<?php echo isset($_GET['s']) ? $_GET['s'] : '' ?>"> <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> <button type="submit" class="hover-anim btn btn-secondary"><i class="fa-solid fa-magnifying-glass"></i></button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -5,7 +5,7 @@ $disabled_attr = $context['can_save'] ? '' : 'disabled';
?> ?>
<div class="admin-container"> <div class="admin-container">
<h1 class="p-title"> <h1 class="title">
<?php if($context['object']->is_saved()): ?> <?php if($context['object']->is_saved()): ?>
<?php the_safe($context['edit_title']) ?> <?php the_safe($context['edit_title']) ?>
<?php else: ?> <?php else: ?>
@ -112,12 +112,12 @@ $disabled_attr = $context['can_save'] ? '' : 'disabled';
<div class="admin-block__content"> <div class="admin-block__content">
<div class="btn-control"> <div class="btn-control">
<?php if($context['object']->is_saved()): ?> <?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> <a href="<?php the_permalink('admin:delete', [str_replace('_', '-', $context['object']->get_table_name()), $context['object']->get_id()]) ?>" class="btn btn-secondary hover-anim" type="submit">Delete</a>
<?php else: ?> <?php else: ?>
<span></span> <span></span>
<?php endif; ?> <?php endif; ?>
<button class="btn btn-primary" type="submit" <?php echo $disabled_attr ?>>Save</button> <button class="btn btn-primary hover-anim" type="submit" <?php echo $disabled_attr ?>>Save</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,22 @@
<div class="admin-block">
<div class="admin-block__title">Ingredients</div>
<div class="admin-block__content">
<div class="admin-block__table">
<?php if (!empty($ingredients)): ?>
<?php foreach ($ingredients as $ing): ?>
<div class="row">
<div class="column"><a href="<?php the_permalink('admin:ing-cat-rel', [$ing->get_id()]) ?>"><?php echo $ing->ingredient_name ?></a></div>
<div class="column"><?php echo $ing->get_count() ?></div>
</div>
<?php endforeach; ?>
<?php else: ?>
<div class="nothing">No ingredients</div>
<?php endif ?>
</div>
<div class="btn-control">
<a href="<?php the_permalink('admin:ing-cat-rel-list', [$recipe->get_id()]) ?>" class="btn btn-secondary hover-anim">Show all</a>
<a href="<?php the_permalink('admin:ing-cat-rel-new', [$recipe->get_id()]) ?>" class="btn btn-primary hover-anim">Add ingredient</a>
</div>
</div>
</div>

View File

@ -0,0 +1,10 @@
<div class="admin-block">
<div class="admin-block__title">Model relations</div>
<div class="admin-block__content">
<div class="admin-block__subtitle">Ingredient: </div>
<div><a href="<?php the_permalink('admin:ingredient', array($ingredient->get_id())) ?>"><?= $ingredient->field_name ?></a></div>
<div class="admin-block__subtitle">Recipe: </div>
<div><a href="<?php the_permalink('admin:recipe', array($recipe->get_id())) ?>"><?= $recipe->field_title ?></a></div>
</div>
</div>

View File

@ -0,0 +1,6 @@
<div class="admin-block">
<div class="admin-block__title">Author</div>
<div class="admin-block__content">
<a href="<?php the_permalink('admin:user', [$author->get_id()]) ?>" class="recipe-author"><?php echo $author->field_username ?></a>
</div>
</div>

View File

@ -19,8 +19,8 @@
<?php endif ?> <?php endif ?>
</div> </div>
<div class="btn-control"> <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:banlist', [$user->get_id()]) ?>" class="btn btn-secondary hover-anim">Show all</a>
<a href="<?php the_permalink('admin:ban-new', [$user->get_id()]) ?>" class="btn btn-primary">New ban</a> <a href="<?php the_permalink('admin:ban-new', [$user->get_id()]) ?>" class="btn btn-primary hover-anim">New ban</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -4,6 +4,11 @@ use Lycoreco\Apps\Users\Models\{
UserModel, UserModel,
BanlistModel BanlistModel
}; };
use Lycoreco\Apps\Recipes\Models\{
RecipeModel,
IngredientModel,
IngredientInRecipeModel
};
function the_admin_header(string $title) function the_admin_header(string $title)
{ {
@ -31,3 +36,48 @@ function the_user_banlist(UserModel $user)
require APPS_PATH . '/Admin/Templates/widgets/user_banlist.php'; require APPS_PATH . '/Admin/Templates/widgets/user_banlist.php';
} }
function the_recipe_author(RecipeModel $recipe)
{
$author = UserModel::get(array(
[
'name' => 'obj.id',
'type' => '=',
'value' => $recipe->field_author_id
]
));
require APPS_PATH . '/Admin/Templates/widgets/recipe_author.php';
}
function the_recipe_ingredients(RecipeModel $recipe)
{
$ingredients = IngredientInRecipeModel::filter(array(
[
'name' => 'obj.recipe_id',
'type' => '=',
'value' => $recipe->get_id()
]
));
require APPS_PATH . '/Admin/Templates/widgets/ingredients_in_recipe.php';
}
function the_recipe_ingredients_relation(IngredientInRecipeModel $relation)
{
$ingredient = IngredientModel::get(array(
[
'name' => 'obj.id',
'type' => '=',
'value' => $relation->field_ingredient_id
]
));
$recipe = RecipeModel::get(array(
[
'name' => 'obj.id',
'type' => '=',
'value' => $relation->field_recipe_id
]
));
require APPS_PATH . '/Admin/Templates/widgets/ingredients_in_recipe_relation.php';
}

View File

@ -9,6 +9,10 @@ $admin_urls = [
// Lists // Lists
new Path('/admin/users', new Controllers\AdminUserListController(), 'user-list'), new Path('/admin/users', new Controllers\AdminUserListController(), 'user-list'),
new Path('/admin/recipes',new Controllers\AdminRecipeListController(), 'recipe-list'),
new Path('/admin/categories',new Controllers\AdminCategoryListController(), 'category-list'),
new Path('/admin/ingredients',new Controllers\IngredientListController(), 'ingredient-list'),
new Path('/admin/reviews', new Controllers\AdminReviewListController(), 'review-list'),
////// Single object /////// ////// Single object ///////
// User // User
@ -20,6 +24,26 @@ $admin_urls = [
new Path('/admin/user/[:int]/ban/new', new Controllers\AdminBanController(true), 'ban-new'), new Path('/admin/user/[:int]/ban/new', new Controllers\AdminBanController(true), 'ban-new'),
new Path('/admin/ban/[:int]', new Controllers\AdminBanController(false), 'ban'), new Path('/admin/ban/[:int]', new Controllers\AdminBanController(false), 'ban'),
// Recipe
new Path('/admin/recipe/[:int]', new Controllers\AdminRecipeController(), 'recipe'),
new Path('/admin/recipe/new', new Controllers\AdminRecipeController(true), 'recipe-new'),
// Category
new Path('/admin/category/[:int]', new Controllers\AdminCategoryController(), 'category'),
new Path('/admin/category/new', new Controllers\AdminCategoryController(true), 'category-new'),
// Ingredient
new Path('/admin/ingredient/[:int]', new Controllers\IngredientController(), 'ingredient'),
new Path('/admin/ingredient/new', new Controllers\IngredientController(true), 'ingredient-new'),
// Reviews
new Path('/admin/review/[:int]', new Controllers\AdminReviewControler(), 'review'),
// Recipe ingedient relation
new Path('/admin/recipe/[:int]/ingredients', new Controllers\IngredientRecipeRelListController(), 'ing-cat-rel-list'),
new Path('/admin/recipe/ingredient/[:int]', new Controllers\IngredientRecipeRelController(), 'ing-cat-rel'),
new Path('/admin/recipe/[:int]/ingredient/new', new Controllers\IngredientRecipeRelController(true), 'ing-cat-rel-new'),
// Dynamic delete for every object type // Dynamic delete for every object type
new Path('/admin/[:string]/[:int]/delete', new Controllers\AdminDeleteController(), 'delete') new Path('/admin/[:string]/[:int]/delete', new Controllers\AdminDeleteController(), 'delete')
]; ];

View File

@ -3,6 +3,7 @@
namespace Lycoreco\Apps\Ajax\Controllers; namespace Lycoreco\Apps\Ajax\Controllers;
use Lycoreco\Includes\BaseController; use Lycoreco\Includes\BaseController;
use Lycoreco\Includes\Model\ValidationError;
class AjaxController extends BaseController class AjaxController extends BaseController
{ {
@ -15,10 +16,7 @@ class AjaxController extends BaseController
require_once APPS_PATH . '/Ajax/ajax-actions.php'; require_once APPS_PATH . '/Ajax/ajax-actions.php';
$context['result'] = ""; $context['result'] = "";
$json = file_get_contents('php://input'); $action = $_POST['action'] ?? false;
$data = json_decode($json, true);
$action = $data['action'] ?? false;
// If request from other site // If request from other site
if (!in_array($_SERVER['HTTP_HOST'], ALLOWED_HOSTS)) { if (!in_array($_SERVER['HTTP_HOST'], ALLOWED_HOSTS)) {
@ -34,9 +32,16 @@ class AjaxController extends BaseController
$action = "ajax_" . $action; $action = "ajax_" . $action;
try { try {
$context['result'] = $action($data['args']); $context['result'] = $action();
} catch (\Exception $ex) { }
catch (ValidationError $ex) {
$context['result'] = get_ajax_error($ex->getMessage(), 400);
return $context;
}
catch (\Exception $ex) {
http_response_code(500);
$context['result'] = get_ajax_error($ex->getMessage()); $context['result'] = get_ajax_error($ex->getMessage());
return $context;
} }

View File

@ -1,2 +1,2 @@
<?php <?php
echo $context['result']; echo json_encode($context['result'], JSON_PRETTY_PRINT);

View File

@ -1,5 +1,12 @@
<?php <?php
use Lycoreco\Apps\Recipes\Models\{
IngredientModel,
RecipeModel,
RecipeUserMenu,
FavoriteModel
};
function get_ajax_error($message, $error_code = 500) function get_ajax_error($message, $error_code = 500)
{ {
http_response_code($error_code); http_response_code($error_code);
@ -7,41 +14,186 @@ function get_ajax_error($message, $error_code = 500)
$error = array(); $error = array();
$error['error'] = $message; $error['error'] = $message;
return json_encode($error, JSON_PRETTY_PRINT); return $error;
} }
/** /**
* Ajax actions * Ajax actions
*/ */
function ajax_search($args) { function ajax_search()
$search_query = $args['query']; {
$search_query = $_POST['query'] ?? null;
$result = []; if (!isset($search_query)) {
$data = [ return get_ajax_error("Missing 'query' parameter.", 400);
[
'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); if (!CURRENT_USER) {
return get_ajax_error('You are not authorized', 401);
}
$result = array();
return json_encode($result, JSON_PRETTY_PRINT); $recipes = array();
$recipe_models = RecipeModel::filter(
count: 5,
search: $search_query
);
foreach ($recipe_models as $recipe_model) {
$recipe = $recipe_model->getAssocArr();
$recipe['image_url'] = $recipe_model->get_image_url();
$recipe['url'] = $recipe_model->get_absolute_url();
$recipes[] = $recipe;
}
$result['count'] = count($recipes);
$result['result'] = $recipes;
return $result;
}
function ajax_usermenu()
{
$recipe_id = $_POST['recipe_id'] ?? null;
$dayofweek = $_POST['dayofweek'] ?? null;
if (!CURRENT_USER) {
return get_ajax_error('You are not authorized', 401);
}
if (!isset($recipe_id)) {
return get_ajax_error("Missing 'recipe_id' parameter.", 400);
}
if (!isset($dayofweek)) {
return get_ajax_error("Missing 'dayofweek' parameter.", 400);
}
$result = array();
$user_menu = RecipeUserMenu::get(array(
[
'name' => 'obj.recipe_id',
'type' => '=',
'value' => $recipe_id
],
[
'name' => 'obj.user_id',
'type' => '=',
'value' => CURRENT_USER->get_id()
]
));
// If user choose optiopn 'remove'
if($dayofweek == 'remove') {
if($user_menu) {
$user_menu->delete();
$result['success'] = 'This recipe was removed from your list';
return $result;
}
else {
return get_ajax_error("This recipe in your menu is not exists", 400);
}
}
// If not exists, add new recipe in user menu
if(!$user_menu) {
$user_menu = new RecipeUserMenu();
$user_menu->field_recipe_id = $recipe_id;
$user_menu->field_user_id = CURRENT_USER->get_id();
}
$user_menu->field_dayofweek = $dayofweek;
$user_menu->save();
$result['success'] = 'You have successfully added the recipe to your menu.';
return $result;
}
function ajax_create_ingredient()
{
$ingredient_name = $_POST['name'] ?? null;
$ingredient_unit = $_POST['unit'] ?? null;
if (!CURRENT_USER) {
return get_ajax_error('You are not authorized', 401);
}
if (!isset($ingredient_name)) {
return get_ajax_error("Missing 'name' parameter.", 400);
}
if (!isset($ingredient_unit)) {
return get_ajax_error("Missing 'unit' parameter.", 400);
}
$ingredient = new IngredientModel();
$ingredient->field_name = $ingredient_name;
$ingredient->field_unit_name = $ingredient_unit;
$ingredient->save();
$result = array();
$result['success'] = 'You have successfully added new ingredient';
return $result;
}
function ajax_search_ingredient()
{
$search_query = $_POST['query'] ?? null;
if (!isset($search_query)) {
return get_ajax_error("Missing 'query' parameter.", 400);
}
$result = array();
$ingredients = IngredientModel::filter(
count: 5,
search: $search_query
);
$result['count'] = count($ingredients);
$result['result'] = array_map(function($ing) {
return $ing->getAssocArr();
}, $ingredients);
return $result;
}
function ajax_favorites()
{
$recipe_id = $_POST['recipe_id'] ?? null;
$type = $_POST['type'] ?? 'add'; // add, remove
if (!CURRENT_USER) {
return get_ajax_error('You are not authorized', 401);
}
if (!isset($recipe_id)) {
return get_ajax_error("Missing 'recipe_id' parameter.", 400);
}
if($type != 'add' && $type != 'remove')
{
return get_ajax_error("'type' parameter can be only 'add' or 'remove'", 400);
}
$result = array();
$favorite = FavoriteModel::get(array(
[
'name' => 'obj.recipe_id',
'type' => '=',
'value' => $recipe_id
],
[
'name' => 'obj.user_id',
'type' => '=',
'value' => CURRENT_USER->get_id()
]
));
if($type == 'remove') {
if($favorite) {
$favorite->delete();
$result['success'] = 'This recipe was removed from your favorite list';
return $result;
}
else {
return get_ajax_error("This recipe from your favorites list does not exist.", 400);
}
}
if(!$favorite)
$favorite = new FavoriteModel();
$favorite->field_user_id = CURRENT_USER->get_id();
$favorite->field_recipe_id = $recipe_id;
$favorite->save();
$result['success'] = 'You have successfully added the recipe to your favorites list';
return $result;
} }

View File

@ -2,9 +2,51 @@
namespace Lycoreco\Apps\Index\Controllers; namespace Lycoreco\Apps\Index\Controllers;
use Lycoreco\Apps\Recipes\Models\CategoryModel;
use Lycoreco\Apps\Recipes\Models\RecipeModel;
use Lycoreco\Apps\Recipes\Models\RecipeUserMenu;
use Lycoreco\Apps\Recipes\Models\ReviewsModel;
use Lycoreco\Includes\BaseController; use Lycoreco\Includes\BaseController;
require_once(INCLUDES_PATH . '/Const/recipes.php');
class HomepageController extends BaseController class HomepageController extends BaseController
{ {
protected $template_name = APPS_PATH . '/Index/Templates/index.php'; protected $template_name = APPS_PATH . '/Index/Templates/index.php';
public function get_context_data() {
$context = parent::get_context_data();
$context['latest_recipes'] = RecipeModel::filter(array(
[
'name' => 'obj.status',
'type' => '=',
'value' => 'publish'
]),
['-obj.created_at'],
3
);
$context['categories'] = CategoryModel::filter();
$dayNumber = date("w");
$dayofweek = DAYS_OF_WEEK[$dayNumber];
$context['reviews'] = ReviewsModel::filter(array(
[
'name' => 'obj.status',
'type' => '=',
'value' => 'publish'
]),
['-obj.created_at'],
6
);
if(CURRENT_USER) {
$context['usermenu_recipe_prefetch'] = RecipeUserMenu::get_prefetch_recipes(CURRENT_USER, $dayofweek);
}
else {
$context['usermenu_recipe_prefetch'] = [ ];
}
return $context;
}
} }

View File

@ -1,4 +1,7 @@
<?php <?php
use Lycoreco\Includes\Routing\HttpExceptions\PageError;
$error = $context['error_model']; $error = $context['error_model'];
the_header( the_header(
@ -7,17 +10,19 @@ the_header(
'error', 'error',
[ [
['robots', 'nofollow, noindex'] ['robots', 'nofollow, noindex']
]); ]
);
/** /**
* @var PageError * @var PageError
*/ */
?> ?>
<div class="error-page"> <div class="container">
<div class="error-page">
<div class="error-code"><?php echo $error->get_http_error() ?></div> <div class="error-code"><?php echo $error->get_http_error() ?></div>
<div class="error-message"><?php echo $error->getMessage() ?></div> <div class="error-message"><?php echo $error->getMessage() ?></div>
</div>
</div> </div>
<?php the_footer() ?> <?php the_footer() ?>

View File

@ -1,4 +1,6 @@
<?php the_header( <?php
require_once APPS_PATH . '/Recipes/components.php';
the_header(
'Welcome', 'Welcome',
"Discover delicious recipes using the ingredients you already have. Fridgebites helps you create tasty meals with what's in your fridge", "Discover delicious recipes using the ingredients you already have. Fridgebites helps you create tasty meals with what's in your fridge",
'frontpage', 'frontpage',
@ -8,13 +10,23 @@
] ]
); );
?> ?>
<div class="welcome">
<div class="container">
<div class="welcome__inner">
<div class="text-container">
<div class="quote">
<h2><span class="black-qoute-word">Your</span> Fridge.</h2>
<h2><span class="black-qoute-word">Your</span> Rules.</h2>
<h2><span class="black-qoute-word">Our</span> Recipes.</h2>
</div>
<p>Discover delicious recipes tailored to exactly what you have on hand no extra shopping trips, no wasted food, just tasty meals made easy.</p>
</div>
<canvas id="food-3d"></canvas>
</div>
</div>
</div>
<div class="container"> <div class="container">
<div class="quote">
<h2><span class="black-qoute-word">Your</span> Fridge. <span class="black-qoute-word">Your</span> Rules.
<span class="black-qoute-word">Our</span> Recipes.
</h2>
</div>
<div class="latest-recipes"> <div class="latest-recipes">
<h2 class="title">Latest Recipes Added</h2> <h2 class="title">Latest Recipes Added</h2>
<!-- Slider main container --> <!-- Slider main container -->
@ -22,42 +34,21 @@
<!-- Additional required wrapper --> <!-- Additional required wrapper -->
<div class="swiper-wrapper"> <div class="swiper-wrapper">
<!-- Slides --> <!-- Slides -->
<?php foreach($context['latest_recipes'] as $recipe): ?>
<div class="swiper-slide"> <div class="swiper-slide">
<a href="#" class="recent-recipe hover-anim"> <a href="<?= $recipe->get_absolute_url() ?>" class="recent-recipe hover-anim">
<div class="recipe-img"> <div class="recipe-img">
<img src="media/recipe1.png" alt="Recipe 1" class="recipe-img__img"> <img src="<?= $recipe->get_image_url() ?>" alt="Recipe 1" class="recipe-img__img">
</div> </div>
<div class="recipe-info"> <div class="recipe-info">
<p class="recipe-info__title">Spaghetti Bolognese</p> <p class="recipe-info__title"><?= $recipe->field_title ?></p>
<p class="recipe-info__meta"><i class="fa-solid fa-user"></i> GreenDavid004</p> <p class="recipe-info__meta"><i class="fa-solid fa-user"></i> <?= $recipe->author_username ?></p>
</div>
</a>
</div>
<div class="swiper-slide">
<a href="#" class="recent-recipe hover-anim">
<div class="recipe-img">
<img src="media/recipe1.jpeg" alt="Recipe 1" class="recipe-img__img">
</div>
<div class="recipe-info">
<p class="recipe-info__title">Spaghetti Bolognese</p>
<p class="recipe-info__meta"><i class="fa-solid fa-user"></i> GreenDavid004</p>
</div>
</a>
</div>
<div class="swiper-slide">
<a href="#" class="recent-recipe hover-anim">
<div class="recipe-img">
<img src="media/recipe1.jpeg" alt="Recipe 1" class="recipe-img__img">
</div>
<div class="recipe-info">
<p class="recipe-info__title">Spaghetti Bolognese</p>
<p class="recipe-info__meta"><i class="fa-solid fa-user"></i> GreenDavid004</p>
</div> </div>
</a> </a>
</div> </div>
<?php endforeach; ?>
</div> </div>
<!-- If we need pagination --> <!-- If we need pagination -->
@ -76,87 +67,14 @@
<div class="swiper categories-swiper"> <div class="swiper categories-swiper">
<div class="swiper-wrapper"> <div class="swiper-wrapper">
<?php foreach($context['categories'] as $cat): ?>
<div class="swiper-slide"> <div class="swiper-slide">
<a href="#" class="category hover-anim"> <a href="<?= get_permalink('recipes:catalog') . '?category=' . $cat->get_id() ?>" class="category hover-anim">
<div class="category-img"> <p class="category-title"><?= $cat->field_name ?></p>
<img src="media/category1.png" alt="Category 1" class="category-img__img">
</div>
<p class="category-title">Mexican</p>
</a>
</div>
<div class="swiper-slide">
<a href="#" class="category hover-anim">
<div class="category-img">
<img src="media/category1.png" alt="Category 1" class="category-img__img">
</div>
<p class="category-title">Mexican</p>
</a>
</div>
<div class="swiper-slide">
<a href="#" class="category hover-anim">
<div class="category-img">
<img src="media/category1.png" alt="Category 1" class="category-img__img">
</div>
<p class="category-title">Mexican</p>
</a>
</div>
<div class="swiper-slide">
<a href="#" class="category hover-anim">
<div class="category-img">
<img src="media/category1.png" alt="Category 1" class="category-img__img">
</div>
<p class="category-title">Mexican</p>
</a>
</div>
<div class="swiper-slide">
<a href="#" class="category hover-anim">
<div class="category-img">
<img src="media/category1.png" alt="Category 1" class="category-img__img">
</div>
<p class="category-title">Mexican</p>
</a>
</div>
<div class="swiper-slide">
<a href="#" class="category hover-anim">
<div class="category-img">
<img src="media/category1.png" alt="Category 1" class="category-img__img">
</div>
<p class="category-title">Mexican</p>
</a>
</div>
<div class="swiper-slide">
<a href="#" class="category hover-anim">
<div class="category-img">
<img src="media/category1.png" alt="Category 1" class="category-img__img">
</div>
<p class="category-title">Mexican</p>
</a>
</div>
<div class="swiper-slide">
<a href="#" class="category hover-anim">
<div class="category-img">
<img src="media/category1.png" alt="Category 1" class="category-img__img">
</div>
<p class="category-title">Mexican</p>
</a>
</div>
<div class="swiper-slide">
<a href="#" class="category hover-anim">
<div class="category-img">
<img src="media/category1.png" alt="Category 1" class="category-img__img">
</div>
<p class="category-title">Mexican</p>
</a> </a>
</div> </div>
<?php endforeach; ?>
</div> </div>
<div class="swiper-button-prev"></div> <div class="swiper-button-prev"></div>
@ -168,240 +86,58 @@
<div class="recent-reviews"> <div class="recent-reviews">
<h2 class="title">Recent User Reviews</h2> <h2 class="title">Recent User Reviews</h2>
<div class="reviews-grid"> <div class="reviews-grid">
<a href="#" class="recent-review hover-anim"> <?php foreach($context['reviews'] as $review): ?>
<a href="<?php the_permalink('recipes:single', [$review->field_recipe_id]) ?>" class="recent-review hover-anim">
<div class="review-title"> <div class="review-title">
<div class="review-img"> <div class="review-img">
<img src="media/recipe1.jpeg" alt="reviewed recipe"> <img src="<?= $review->get_recipe_image() ?>" alt="reviewed recipe">
</div> </div>
<h3 class="review-title-text">Just Like Mom's</h3> <h3 class="review-title-text"><?= $review->recipe_title ?></h3>
</div> </div>
<div class="review-text"> <div class="review-text">
<p>I made this spaghetti last night and it was absolutely delicious! The sauce was rich and <p><?= $review->get_excerpt() ?></p>
flavorful—just the right balance of garlic, herbs, and tomato. I added a pinch of chili
flakes for a little kick, and it turned out perfect...</p>
</div> </div>
<div class="review-meta meta"> <div class="review-meta meta">
<p class="review-author"><i class="fa-solid fa-user"></i> GreenDavid004</p> <p class="review-author"><i class="fa-solid fa-user"></i> <?= $review->author_username ?></p>
<p class="review-date"><i class="fa-solid fa-calendar-days"></i> 2023-10-01</p> <p class="review-date"><i class="fa-solid fa-calendar-days"></i> <?= $review->get_date() ?></p>
</div>
</a>
<a href="#" class="recent-review hover-anim">
<div class="review-title">
<div class="review-img">
<img src="media/recipe1.jpeg" alt="reviewed recipe">
</div>
<h3 class="review-title-text">Just Like Mom's</h3>
</div>
<div class="review-text">
<p>I made this spaghetti last night and it was absolutely delicious! The sauce was rich and
flavorful—just the right balance of garlic, herbs, and tomato. I added a pinch of chili
flakes for a little kick, and it turned out perfect...</p>
</div>
<div class="review-meta meta">
<p class="review-author"><i class="fa-solid fa-user"></i> GreenDavid004</p>
<p class="review-date"><i class="fa-solid fa-calendar-days"></i> 2023-10-01</p>
</div>
</a><a href="#" class="recent-review hover-anim">
<div class="review-title">
<div class="review-img">
<img src="media/recipe1.jpeg" alt="reviewed recipe">
</div>
<h3 class="review-title-text">Just Like Mom's</h3>
</div>
<div class="review-text">
<p>I made this spaghetti last night and it was absolutely delicious! The sauce was rich and
flavorful—just the right balance of garlic, herbs, and tomato. I added a pinch of chili
flakes for a little kick, and it turned out perfect...</p>
</div>
<div class="review-meta meta">
<p class="review-author"><i class="fa-solid fa-user"></i> GreenDavid004</p>
<p class="review-date"><i class="fa-solid fa-calendar-days"></i> 2023-10-01</p>
</div>
</a><a href="#" class="recent-review hover-anim">
<div class="review-title">
<div class="review-img">
<img src="media/recipe1.jpeg" alt="reviewed recipe">
</div>
<h3 class="review-title-text">Just Like Mom's</h3>
</div>
<div class="review-text">
<p>I made this spaghetti last night and it was absolutely delicious! The sauce was rich and
flavorful—just the right balance of garlic, herbs, and tomato. I added a pinch of chili
flakes for a little kick, and it turned out perfect...</p>
</div>
<div class="review-meta meta">
<p class="review-author"><i class="fa-solid fa-user"></i> GreenDavid004</p>
<p class="review-date"><i class="fa-solid fa-calendar-days"></i> 2023-10-01</p>
</div>
</a><a href="#" class="recent-review hover-anim">
<div class="review-title">
<div class="review-img">
<img src="media/recipe1.jpeg" alt="reviewed recipe">
</div>
<h3 class="review-title-text">Just Like Mom's</h3>
</div>
<div class="review-text">
<p>I made this spaghetti last night and it was absolutely delicious! The sauce was rich and
flavorful—just the right balance of garlic, herbs, and tomato. I added a pinch of chili
flakes for a little kick, and it turned out perfect...</p>
</div>
<div class="review-meta meta">
<p class="review-author"><i class="fa-solid fa-user"></i> GreenDavid004</p>
<p class="review-date"><i class="fa-solid fa-calendar-days"></i> 2023-10-01</p>
</div>
</a><a href="#" class="recent-review hover-anim">
<div class="review-title">
<div class="review-img">
<img src="media/recipe1.jpeg" alt="reviewed recipe">
</div>
<h3 class="review-title-text">Just Like Mom's</h3>
</div>
<div class="review-text">
<p>I made this spaghetti last night and it was absolutely delicious! The sauce was rich and
flavorful—just the right balance of garlic, herbs, and tomato. I added a pinch of chili
flakes for a little kick, and it turned out perfect...</p>
</div>
<div class="review-meta meta">
<p class="review-author"><i class="fa-solid fa-user"></i> GreenDavid004</p>
<p class="review-date"><i class="fa-solid fa-calendar-days"></i> 2023-10-01</p>
</div> </div>
</a> </a>
<?php endforeach; ?>
</div> </div>
</div> </div>
<?php if(CURRENT_USER): ?>
<?php if($context['usermenu_recipe_prefetch']): ?>
<div class="daily-meals"> <div class="daily-meals">
<h2 class="title">Your Menu for Monday</h2> <h2 class="title">Your Menu for <?= date("l"); ?></h2>
<div class="daily-meals-grid"> <div class="daily-meals-grid">
<a href="#" class="daily-meal hover-anim"> <?php
<div class="meal-img"> foreach ($context['usermenu_recipe_prefetch'] as $recipe_pref) {
<img src="media/recipe1.jpeg" alt="meal-img"> the_product_recipes_item($recipe_pref['origin'], $recipe_pref['relations']);
</div> }
<div class="daily-meal-info"> ?>
<div class="daily-meal-title">
<h3>Goulash</h3>
<span class="meta"><i class="fa-regular fa-clock"></i>120mins to make</span>
</div>
<div class="daily-meal-ingredients">
<ul class="ingredients-list">
<li>Ground beef</li>
<li>Tomato sauce</li>
<li>Onion</li>
<li>Macaroni</li>
<li>Garlic</li>
<li>Cheese</li>
</ul>
</div> </div>
</div> </div>
</a> <?php else: ?>
<a href="#" class="daily-meal hover-anim"> <div class="daily-meals">
<div class="meal-img"> <h2 class="title">No meals added for <?= date("l"); ?></h2>
<img src="media/recipe1.jpeg" alt="meal-img"> <p class="no-daily-meals-msg">You have not added any recipes for <?= date("l");?>, please go to the catalog and add recipes.</p>
</div>
<div class="daily-meal-info">
<div class="daily-meal-title">
<h3>Goulash</h3>
<span class="meta"><i class="fa-regular fa-clock"></i>120mins to make</span>
</div>
<div class="daily-meal-ingredients">
<ul class="ingredients-list">
<li>Ground beef</li>
<li>Tomato sauce</li>
<li>Onion</li>
<li>Macaroni</li>
<li>Garlic</li>
<li>Cheese</li>
</ul>
</div>
</div>
</a>
<a href="#" class="daily-meal hover-anim">
<div class="meal-img">
<img src="media/recipe1.jpeg" alt="meal-img">
</div>
<div class="daily-meal-info">
<div class="daily-meal-title">
<h3>Goulash</h3>
<span class="meta"><i class="fa-regular fa-clock"></i>120mins to make</span>
</div>
<div class="daily-meal-ingredients">
<ul class="ingredients-list">
<li>Ground beef</li>
<li>Tomato sauce</li>
<li>Onion</li>
<li>Macaroni</li>
<li>Garlic</li>
<li>Cheese</li>
</ul>
</div>
</div>
</a>
<a href="#" class="daily-meal hover-anim">
<div class="meal-img">
<img src="media/recipe1.jpeg" alt="meal-img">
</div>
<div class="daily-meal-info">
<div class="daily-meal-title">
<h3>Goulash</h3>
<span class="meta"><i class="fa-regular fa-clock"></i>120mins to make</span>
</div>
<div class="daily-meal-ingredients">
<ul class="ingredients-list">
<li>Ground beef</li>
<li>Tomato sauce</li>
<li>Onion</li>
<li>Macaroni</li>
<li>Garlic</li>
<li>Cheese</li>
</ul>
</div>
</div>
</a>
<a href="#" class="daily-meal hover-anim">
<div class="meal-img">
<img src="media/recipe1.jpeg" alt="meal-img">
</div>
<div class="daily-meal-info">
<div class="daily-meal-title">
<h3>Goulash</h3>
<span class="meta"><i class="fa-regular fa-clock"></i>120mins to make</span>
</div>
<div class="daily-meal-ingredients">
<ul class="ingredients-list">
<li>Ground beef</li>
<li>Tomato sauce</li>
<li>Onion</li>
<li>Macaroni</li>
<li>Garlic</li>
<li>Cheese</li>
</ul>
</div>
</div>
</a>
<a href="#" class="daily-meal hover-anim">
<div class="meal-img">
<img src="media/recipe1.jpeg" alt="meal-img">
</div>
<div class="daily-meal-info">
<div class="daily-meal-title">
<h3>Goulash</h3>
<span class="meta"><i class="fa-regular fa-clock"></i>120mins to make</span>
</div>
<div class="daily-meal-ingredients">
<ul class="ingredients-list">
<li>Ground beef</li>
<li>Tomato sauce</li>
<li>Onion</li>
<li>Macaroni</li>
<li>Garlic</li>
<li>Cheese</li>
</ul>
</div>
</div>
</a>
</div>
</div> </div>
<?php endif; ?>
<?php endif; ?>
</div> </div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@v0.149.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@v0.149.0/examples/jsm/"
}
}
</script>
<script type="module" src="<?= ASSETS_PATH . '/js/threejs-scene.js' ?>"></script>
<?php the_footer(array( <?php the_footer(array(
ASSETS_PATH . '/swiper/swiper-bundle.min.js', ASSETS_PATH . '/swiper/swiper-bundle.min.js',
ASSETS_PATH . '/js/index.js', ASSETS_PATH . '/js/index.js',

View File

@ -1,9 +0,0 @@
<?php
namespace Lycoreco\Apps\Receipts\Models;
use Lycoreco\Includes\Model\BaseModel;
class CategoryModel extends BaseModel
{
}

View File

@ -1,9 +0,0 @@
<?php
namespace Lycoreco\Apps\Receipts\Models;
use Lycoreco\Includes\Model\BaseModel;
class IngredientInReceiptModel extends BaseModel
{
}

View File

@ -1,9 +0,0 @@
<?php
namespace Lycoreco\Apps\Receipts\Models;
use Lycoreco\Includes\Model\BaseModel;
class IngredientModel extends BaseModel
{
}

View File

@ -1,16 +0,0 @@
<?php
namespace Lycoreco\Apps\Receipts\Models;
use Lycoreco\Includes\Model\BaseModel;
class ReceiptModel extends BaseModel
{
public $field_title;
public $field_instruction;
public $field_estimated_time;
public $field_estimated_price;
public $field_category_id;
public $field_author_id;
public $field_status;
public $field_created_at;
}

View File

@ -1,3 +0,0 @@
<?php
use Lycoreco\Includes\Routing\Path;

View File

@ -0,0 +1,63 @@
<?php
namespace Lycoreco\Apps\Recipes\Controllers;
use Lycoreco\Apps\Recipes\Models\{
CategoryModel,
IngredientModel,
RecipeModel
};
use Lycoreco\Includes\BaseController;
define('CATALOG_MAX_RECIPES', 10);
class CatalogController extends BaseController
{
protected $template_name = APPS_PATH . '/Recipes/Templates/catalog.php';
public function get_context_data()
{
$context = parent::get_context_data();
$context["page"] = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$context['categories'] = CategoryModel::filter(sort_by: ['obj.name'], count: 200);
$context['ingredients'] = IngredientModel::filter(sort_by: ['obj.name'], count: 200);
// GET request to filter catalog
$fields = array();
$category_id = isset($_GET['category']) ? $_GET['category'] : null;
if ($category_id) {
$fields[] = array(
'name' => 'obj.category_id',
'type' => '=',
'value' => $category_id
);
}
$ingredient_ids = isset($_GET['ingredient']) ? $_GET['ingredient'] : null;
if ($ingredient_ids) {
$fields[] = array(
'name' => 'tb2.ingredient_id',
'type' => 'IN',
'value' => $ingredient_ids,
'is_having' => true
);
}
$fields[] = array(
'name' => 'obj.status',
'type' => '=',
'value' => 'publish'
);
$context['recipes_count'] = RecipeModel::count($fields);
$context['recipes'] = RecipeModel::filter(
$fields,
['-obj.created_at'],
CATALOG_MAX_RECIPES,
offset: calc_page_offset(CATALOG_MAX_RECIPES, $context['page'])
);
return $context;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Lycoreco\Apps\Recipes\Controllers;
use Lycoreco\Apps\Recipes\Models\RecipeUserMenu;
use Lycoreco\Includes\BaseController;
require_once(INCLUDES_PATH . '/Const/recipes.php');
class DailyMealsController extends BaseController
{
protected $template_name = APPS_PATH . '/Recipes/Templates/daily-meals.php';
protected $allow_role = 'user';
function get_context_data() {
$context = parent::get_context_data();
$context['weeks'] = array();
foreach (DAYS_OF_WEEK as $week) {
$context['weeks'][$week] = RecipeUserMenu::get_prefetch_recipes(CURRENT_USER, $week);
}
return $context;
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Lycoreco\Apps\Recipes\Controllers;
use FPDF;
use Lycoreco\Apps\Recipes\Models\RecipeModel;
use Lycoreco\Apps\Recipes\Utils\RecipePDF;
use Lycoreco\Includes\BaseController;
use Lycoreco\Includes\Routing\HttpExceptions;
class ExportPdfController extends BaseController
{
protected $template_name = APPS_PATH . '/Recipes/Templates/export-pdf.php';
protected function get_model()
{
if (isset($this->__model))
return $this->__model;
$this->__model = RecipeModel::get(
array(
[
'name' => 'obj.id',
'type' => '=',
'value' => $this->url_context['url_1']
]
)
);
if (empty($this->__model))
throw new HttpExceptions\NotFound404('Recipe not found');
return $this->__model;
}
public function get_context_data()
{
$context = parent::get_context_data();
$recipe = $this->get_model();
$fpdf = new RecipePDF($recipe);
$fpdf->PrintRecipe();
$context['fpdf'] = $fpdf;
return $context;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Lycoreco\Apps\Recipes\Controllers;
use Lycoreco\Apps\Recipes\Models\FavoriteModel;
use Lycoreco\Apps\Recipes\Models\RecipeModel;
use Lycoreco\Includes\BaseController;
define('FAVORITES_MAX_RECIPES', 500);
class FavoritesController extends BaseController
{
protected $template_name = APPS_PATH . '/Recipes/Templates/favorites.php';
protected $allow_role = 'user';
public function get_context_data()
{
$context = parent::get_context_data();
$favorite_recipes = FavoriteModel::filter(array(
[
'name' => 'obj.user_id',
'type' => '=',
'value' => CURRENT_USER->get_id()
]
));
$fav_ids = array_map(function($recipe) {
return $recipe->field_recipe_id;
}, $favorite_recipes);
if(!empty($fav_ids)) {
$context['recipes'] = RecipeModel::filter(
array(
[
'name' => 'obj.id',
'type' => 'IN',
'value' => $fav_ids
]
),
['-obj.created_at'],
FAVORITES_MAX_RECIPES
);
}
else {
$context['recipes'] = [];
}
return $context;
}
}

View File

@ -0,0 +1,133 @@
<?php
namespace Lycoreco\Apps\Recipes\Controllers;
use Exception;
use Lycoreco\Apps\Recipes\Models\IngredientInRecipeModel;
use Lycoreco\Apps\Recipes\Models\RecipeModel;
use Lycoreco\Apps\Recipes\Models\ReviewsModel;
use Lycoreco\Apps\Users\Models\UserModel;
use Lycoreco\Includes\BaseController;
use Lycoreco\Includes\Model\ValidationError;
use Lycoreco\Includes\Routing\HttpExceptions;
class SingleRecipeController extends BaseController
{
protected $template_name = APPS_PATH . '/Recipes/Templates/single.php';
protected function post()
{
if(!CURRENT_USER) {
$this->context['reviews_error'] = 'You are not authorized';
return;
}
$rating = $_POST['rating-select'] ?? null;
$title = $_POST['review-title'] ?? null;
$content = $_POST['review-body-input'] ?? null;
$recipe = $this->get_model();
$review = new ReviewsModel();
$review->field_title = $title;
$review->field_content = $content;
$review->field_rating = $rating;
$review->field_status = 'pending';
$review->field_user_id = CURRENT_USER->get_id();
$review->field_recipe_id = $recipe->get_id();
try {
$review->save();
$this->context['display_review_form'] = false;
}
catch(ValidationError $ex) {
$this->context['reviews_error'] = $ex->getMessage();
}
catch(Exception $ex) {
$this->context['reviews_error'] = 'Unexpected error';
}
}
protected function get_model()
{
if (isset($this->__model))
return $this->__model;
$this->__model = RecipeModel::get(
array(
[
'name' => 'obj.id',
'type' => '=',
'value' => $this->url_context['url_1']
]
)
);
if (empty($this->__model))
throw new HttpExceptions\NotFound404('Recipe not found');
return $this->__model;
}
public function get_context_data()
{
$context = parent::get_context_data();
$recipe = $this->get_model();
$context['recipe'] = $recipe;
$context['display_review_form'] = true;
$context['author'] = UserModel::get(array(
[
'name' => 'obj.id',
'type' => '=',
'value' => $recipe->field_author_id
]
));
$context['ingredients'] = IngredientInRecipeModel::filter(array(
[
'name' => 'obj.recipe_id',
'type' => '=',
'value' => $recipe->get_id()
]
));
$context['reviews'] = ReviewsModel::filter(array(
[
'name' => 'obj.recipe_id',
'type' => '=',
'value' => $recipe->get_id()
],
[
'name' => 'obj.status',
'type' => '=',
'value' => 'publish'
],
));
$context['reviews_average'] = '0.0';
if(!empty($context['reviews'])) {
$sum = array_sum(array_column($context['reviews'], 'field_rating'));
$count = count($context['reviews']);
$context['reviews_average'] = number_format($sum / $count, 1);
}
if(CURRENT_USER) {
$review_exists = ReviewsModel::count(array(
[
'name' => 'obj.recipe_id',
'type' => '=',
'value' => $recipe->get_id()
],
[
'name' => 'obj.user_id',
'type' => '=',
'value' => CURRENT_USER->get_id()
]
));
if($review_exists > 0)
$context['display_review_form'] = false;
}
return $context;
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace Lycoreco\Apps\Recipes\Controllers;
use Exception;
use Lycoreco\Apps\Recipes\Models\RecipeModel;
use Lycoreco\Includes\BaseController;
use Lycoreco\Apps\Recipes\Models\CategoryModel;
use Lycoreco\Apps\Recipes\Models\IngredientInRecipeModel;
use Lycoreco\Includes\Model\ValidationError;
class SingleSubmitController extends BaseController
{
protected $template_name = APPS_PATH . '/Recipes/Templates/single-submit.php';
protected $allow_role = 'user';
protected function post()
{
$title = $_POST['title'] ?? '';
$description = $_POST['description'] ?? '';
$image = $_FILES['image'] ?? null;
$estimated_time = $_POST['est-time'] ?? 0;
$estimated_price = $_POST['est-price'] ?? 0;
$category_id = $_POST['category'] ?? null;
$ingredient_ids = $_POST['ing-id'] ?? [];
$ingredient_counts = $_POST['ing-count'] ?? [];
$recipe = new RecipeModel();
$recipe->field_title = $title;
$recipe->field_instruction = $description;
if($image) {
$file_url = upload_file($image, RecipeModel::get_table_name() . '/', 'image');
$recipe->field_image_url = $file_url;
}
$recipe->field_estimated_time = $estimated_time;
$recipe->field_estimated_price = $estimated_price;
$recipe->field_category_id = $category_id;
$recipe->field_author_id = CURRENT_USER->get_id();
$recipe->field_status = 'pending';
try {
$recipe->save();
}
catch (ValidationError $ex) {
$this->context['error_message'] = $ex->getMessage();
}
catch (Exception $ex) {
$this->context['error_message'] = "Unexpected error";
}
for ($i = 0; $i < count($ingredient_ids); $i++) {
$ing_id = $ingredient_ids[$i];
$ing_count = (int) $ingredient_counts[$i];
if($ing_count <= 0)
continue;
$relation = new IngredientInRecipeModel();
$relation->field_ingredient_id = $ing_id;
$relation->field_amount = $ing_count;
$relation->field_recipe_id = $recipe->get_id();
$relation->save();
}
redirect_to($recipe->get_absolute_url());
}
public function get_context_data()
{
$context = parent::get_context_data();
$context['category_options'] = CategoryModel::get_cat_values();
return $context;
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Lycoreco\Apps\Recipes\Models;
use Lycoreco\Includes\Model\BaseModel;
class CategoryModel extends BaseModel
{
public $field_name;
static protected $search_fields = ['obj.name'];
static protected $table_name = 'categories';
static protected $table_fields = [
'id' => 'int',
'name' => 'string',
];
public static function init_table()
{
$result = db_query('CREATE TABLE ' . static::$table_name . ' (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL
);');
return $result;
}
public static function get_cat_values()
{
$cat_list = self::filter(array(), count: 200);
$result = array();
foreach($cat_list as $cat) {
$result[] = [ $cat->get_id(), $cat->field_name ];
}
return $result;
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace Lycoreco\Apps\Recipes\Models;
use Lycoreco\Includes\Model\BaseModel;
class FavoriteModel extends BaseModel
{
public $field_recipe_id;
public $field_user_id;
static protected $table_name = 'recipe_favorites';
static protected $table_fields = [
'id' => 'int',
'recipe_id' => 'int',
'user_id' => 'int',
];
public static function init_table()
{
$result = db_query('CREATE TABLE ' . static::$table_name . ' (
id INT AUTO_INCREMENT PRIMARY KEY,
recipe_id INT NOT NULL,
user_id INT NOT NULL,
FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);');
return $result;
}
public function valid()
{
$recipe = RecipeModel::get(array(
[
'name' => 'obj.id',
'type' => '=',
'value' => $this->field_recipe_id
]
));
if(!$recipe)
return ['Recipe does not exist'];
return true;
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Lycoreco\Apps\Recipes\Models;
use Lycoreco\Includes\Model\BaseModel;
class IngredientInRecipeModel extends BaseModel
{
public $field_ingredient_id;
public $field_recipe_id;
public $field_amount;
static protected $search_fields = ['tb1.name'];
public $ingredient_name;
public $ingredient_unit;
static protected $additional_fields = array(
[
'field' => [
'tb1.name AS ingredient_name',
'tb1.unit_name AS ingredient_unit'
],
'join_table' => 'ingredients tb1 ON tb1.id = obj.ingredient_id'
]
);
static protected $table_name = 'recipe_ingredients';
static protected $table_fields = [
'id' => 'int',
'ingredient_id' => 'int',
'recipe_id' => 'int',
'amount' => 'int'
];
public static function init_table()
{
$result = db_query('CREATE TABLE ' . static::$table_name . ' (
id INT AUTO_INCREMENT PRIMARY KEY,
ingredient_id INT NOT NULL,
recipe_id INT NOT NULL,
amount INT NOT NULL,
FOREIGN KEY (ingredient_id) REFERENCES ingredients(id) ON DELETE CASCADE,
FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE
);');
return $result;
}
public function get_count()
{
return $this->field_amount . ' ' . $this->ingredient_unit;
}
public function __toString()
{
return $this->ingredient_name;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Lycoreco\Apps\Recipes\Models;
use Lycoreco\Includes\Model\BaseModel;
class IngredientModel extends BaseModel
{
public $field_name;
public $field_unit_name;
static protected $search_fields = ['obj.name'];
static protected $table_name = 'ingredients';
static protected $table_fields = [
'id' => 'int',
'name' => 'string',
'unit_name' => 'string'
];
public static function init_table()
{
$result = db_query('CREATE TABLE ' . static::$table_name . ' (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
unit_name VARCHAR(20) NULL
);');
return $result;
}
public static function get_ing_values()
{
$ing_list = self::filter(array(), count: 200);
$result = array();
foreach($ing_list as $ing) {
$result[] = [ $ing->get_id(), $ing->field_name . ' ('. $ing->field_unit_name .')' ];
}
return $result;
}
}

View File

@ -0,0 +1,158 @@
<?php
namespace Lycoreco\Apps\Recipes\Models;
use Lycoreco\Includes\Model\BaseModel;
class RecipeModel extends BaseModel
{
public $field_title;
public $field_instruction;
public $field_image_url;
public $field_estimated_time;
public $field_estimated_price;
public $field_category_id;
public $field_author_id;
public $field_status;
public $field_created_at;
public $category_name;
public $author_username;
public $is_in_favorite = 0;
public $in_usermenu = false;
const STATUS = [['publish', 'Publish'], ['pending', 'Pending']];
static protected $search_fields = ['obj.title'];
static protected $table_name = 'recipes';
static protected $additional_fields = array(
[
'field' => [
'tb1.name AS category_name'
],
'join_table' => 'categories tb1 ON tb1.id = obj.category_id'
],
[
'field' => [
'us.username AS author_username'
],
'join_table' => 'users us ON us.id = obj.author_id'
],
[
'field' => [],
'join_table' => 'recipe_ingredients tb2 ON tb2.recipe_id = obj.id'
]
);
static protected $table_fields = [
'id' => 'int',
'title' => 'string',
'instruction' => 'string',
'image_url' => 'string',
'estimated_time' => 'int',
'estimated_price' => 'float',
'category_id' => 'int',
'author_id' => 'int',
'status' => 'string',
'created_at' => 'DateTime'
];
protected static function get_additional_fields()
{
$add_fields = parent::get_additional_fields();
// If user is authorized, we also check product in thw wishlist
if (CURRENT_USER) {
$add_fields = array_merge($add_fields, array(
[
"field" => [
"MAX(CASE WHEN fav.user_id = " . CURRENT_USER->get_id() . " THEN 1 ELSE 0 END) AS is_in_favorite"
],
"join_table" => "recipe_favorites fav ON fav.recipe_id = obj.id"
]
));
}
if (CURRENT_USER) {
$add_fields = array_merge($add_fields, array(
[
"field" => [
"MAX(m.dayofweek) AS in_usermenu"
],
"join_table" => "recipe_usermenu m ON m.recipe_id = obj.id AND m.user_id = " . CURRENT_USER->get_id()
]
));
}
return $add_fields;
}
public static function init_table()
{
$result = db_query('CREATE TABLE ' . static::$table_name . ' (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
instruction TEXT NOT NULL,
image_url VARCHAR(255) NULL,
estimated_time INT NOT NULL,
estimated_price INT NOT NULL,
category_id INT NOT NULL,
author_id INT NOT NULL,
status VARCHAR(20) NOT NULL CHECK (status IN (\'publish\', \'pending\')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
);');
return $result;
}
public function get_absolute_url()
{
return get_permalink('recipes:single', [$this->get_id()]);
}
public function get_price()
{
return $this->field_estimated_price . '$';
}
public function get_status()
{
return ucfirst($this->field_status);
}
public function get_image_url()
{
if ($this->field_image_url)
return MEDIA_URL . $this->field_image_url;
return null;
}
public function get_html_instruction(): string
{
return nl2br(trim($this->field_instruction));
}
public function get_time()
{
return $this->field_estimated_time . " minutes";
}
public function valid()
{
$errors = [];
if(mb_strlen($this->field_title) <= 3)
$errors[] = 'Title field must be more than 3 symbols';
if($this->field_estimated_time <= 0)
$errors[] = "Estimated time must be more than 0 minutes";
if($this->field_estimated_price <= 0)
$errors[] = "Estimated price must be more than 0$";
if($this->field_category_id === null)
$errors[] = "Recipe must have category";
if(!empty($errors))
return $errors;
return true;
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace Lycoreco\Apps\Recipes\Models;
use Lycoreco\Apps\Users\Models\UserModel;
use Lycoreco\Includes\Model\BaseModel;
class RecipeUserMenu extends BaseModel
{
public $field_dayofweek;
public $field_recipe_id;
public $field_user_id;
public $field_created_at;
static protected $table_name = 'recipe_usermenu';
static protected $table_fields = [
'id' => 'int',
'dayofweek' => 'string',
'recipe_id' => 'int',
'user_id' => 'int',
'created_at' => 'DateTime'
];
public static function init_table()
{
$result = db_query('CREATE TABLE ' . static::$table_name . ' (
id INT AUTO_INCREMENT PRIMARY KEY,
dayofweek VARCHAR(150) NOT NULL,
recipe_id INT NOT NULL,
user_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);');
return $result;
}
public function valid()
{
require_once INCLUDES_PATH . '/Const/recipes.php';
if (!in_array($this->field_dayofweek, DAYS_OF_WEEK))
return ['Day of Week is not valid'];
$recipe = RecipeModel::get(array(
[
'name' => 'obj.id',
'type' => '=',
'value' => $this->field_recipe_id
]
));
if (!$recipe)
return ['Recipe does not exist'];
return true;
}
public static function get_prefetch_recipes(UserModel $user, string $week) {
$usermenus = RecipeUserMenu::filter(array(
[
'name' => 'obj.user_id',
'type' => '=',
'value' => $user->get_id(),
],
[
'name' => 'obj.dayofweek',
'type' => '=',
'value' => $week
]
), count: 100);
if(empty($usermenus))
return [];
$usermenu_ids = array_map(function($usermenu) {
return $usermenu->field_recipe_id;
}, $usermenus);
$recipes = RecipeModel::filter(array(
[
'name' => 'obj.id',
'type' => 'IN',
'value' => $usermenu_ids
]
));
$prefetch_recipes_ings = prefetch_related($recipes, 'Recipes:IngredientInRecipeModel', 'recipe_id');
return $prefetch_recipes_ings;
}
}

View File

@ -0,0 +1,121 @@
<?php
namespace Lycoreco\Apps\Recipes\Models;
use DateTime;
use Lycoreco\Includes\Model\BaseModel;
class ReviewsModel extends BaseModel
{
const STATUS = [['publish', 'Publish'], ['pending', 'Pending']];
public $field_title;
public $field_content;
public $field_rating;
public $field_status;
public $field_recipe_id;
public $field_user_id;
public $field_created_at;
public $author_username;
public $recipe_title;
public $recipe_image;
static protected $search_fields = ['obj.title'];
static protected $table_name = 'recipe_reviews';
static protected $table_fields = [
'id' => 'int',
'title' => 'string',
'content' => 'string',
'rating' => 'int',
'status' => 'string',
'recipe_id' => 'int',
'user_id' => 'int',
'created_at' => 'DateTime',
];
static protected $additional_fields = array(
[
'field' => [
'us.username AS author_username'
],
'join_table' => 'users us ON us.id = obj.user_id'
],
[
'field' => [
're.title AS recipe_title',
're.image_url AS recipe_image'
],
'join_table' => 'recipes re ON re.id = obj.recipe_id'
]
);
public static function init_table()
{
$result = db_query('CREATE TABLE ' . static::$table_name . ' (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
rating INT NOT NULL,
status VARCHAR(20) NOT NULL CHECK (status IN (\'publish\', \'pending\')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
recipe_id INT NOT NULL,
user_id INT NOT NULL,
FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);');
return $result;
}
public function get_html_content(): string
{
return nl2br(trim($this->field_content));
}
public function get_status()
{
return ucfirst($this->field_status);
}
public function get_date()
{
$date = new DateTime($this->field_created_at);
return $date->format('d.m.Y');
}
public function get_excerpt()
{
$max_length = 100;
$excerpt = mb_substr($this->field_content, 0, $max_length);
if(mb_strlen($this->field_content) > $max_length)
$excerpt .= '...';
return $excerpt;
}
public function get_recipe_image()
{
$recipe = new RecipeModel();
$recipe->field_image_url = $this->recipe_image;
return $recipe->get_image_url();
}
public function valid() {
$errors = array();
if(empty($this->field_title))
$errors[] = 'Title field is empty';
if(empty($this->field_content))
$errors[] = 'Content field is empty';
$this->field_rating = (int)$this->field_rating;
if($this->field_rating < 1 || $this->field_rating > 5) {
$errors[] = 'Rating must be between 1 and 5';
}
if (empty($errors))
return true;
return $errors;
}
}

View File

@ -0,0 +1,97 @@
<?php
require_once APPS_PATH . '/Recipes/components.php';
the_header(
'Recipes',
'Explore our delicious recipes and find your next favorite dish.',
'recipes-page',
[
['keywords', 'recipes, cooking, food, cuisine'],
]
);
?>
<div class="container">
<div class="catalog">
<div class="catalog-items__inner">
<div class="catalog-items">
<?php
foreach ($context['recipes'] as $recipe) {
the_product_item($recipe);
}
?>
</div>
<?php the_pagination($context['recipes_count'], CATALOG_MAX_RECIPES, $context["page"]); ?>
</div>
<div class="filters-container">
<div class="filters">
<div class="filters-inner">
<form class="filters-form" method="get">
<div class="categories-filter">
<h3 class="filters__title">
Categories</h3>
<div class="filters__search">
<i class="fa-solid fa-magnifying-glass search-icon"></i>
<input type="text" class="search-input" placeholder="Search categories...">
</div>
<div class="filters-checkboxes">
<ul>
<?php foreach ($context['categories'] as $cat):
$field_id = 'cat_' . $cat->get_id();
$is_checked = false;
$category = $_GET['category'] ?? null;
if ($category == $cat->get_id())
$is_checked = true;
?>
<li>
<input id="<?= $field_id ?>" type="radio" name="category"
value="<?= $cat->get_id() ?>" <?= $is_checked ? 'checked' : '' ?>>
<label for="<?= $field_id ?>"><?= $cat->field_name ?></label>
</li>
<?php endforeach; ?>
</ul>
</div>
</div>
<div class="ingredients-filter">
<h3 class="filters__title">
Ingredients</h3>
<div class="filters__search">
<i class="fa-solid fa-magnifying-glass search-icon"></i>
<input type="text" class="search-input" placeholder="Search Ingredients...">
</div>
<div class="filters-checkboxes">
<ul>
<?php foreach ($context['ingredients'] as $ing):
$field_id = 'ing_' . $ing->get_id();
$is_checked = false;
$ingredients = $_GET['ingredient'] ?? null;
if ($ingredients) {
if (in_array($ing->get_id(), $ingredients))
$is_checked = true;
}
?>
<li>
<input id="<?= $field_id ?>" type="checkbox" name="ingredient[]"
value="<?= $ing->get_id() ?>" <?= $is_checked ? 'checked' : '' ?>>
<label for="<?= $field_id ?>"><?= $ing->field_name ?></label>
</li>
<?php endforeach; ?>
</ul>
</div>
</div>
<button class="btn btn-primary hover-anim" type="submit">
Apply
</button>
<a class="btn btn-secondary hover-anim" type="button" href="<?php the_permalink('recipes:catalog') ?>">
Reset
</a>
</form>
</div>
</div>
</div>
</div>
</div>
<?php the_footer(); ?>

View File

@ -0,0 +1,15 @@
<a href="<?= $recipe->get_absolute_url() ?>" class="catalog-recipe hover-anim">
<div class="catalog-recipe__image">
<img src="<?= $recipe->get_image_url() ?>" alt="<?= $recipe->field_title ?>">
</div>
<div class="catalog-recipe__info">
<div class="catalog-recipe__title">
<h3><?= $recipe->field_title ?></h3>
</div>
<div class="catalog-recipe__meta meta">
<div class="catalog-recipe__category">
<?= $recipe->category_name ?>
</div>
</div>
</div>
</a>

View File

@ -0,0 +1,18 @@
<a href="<?= $recipe->get_absolute_url() ?>" class="daily-meal hover-anim">
<div class="meal-img">
<img src="<?= $recipe->get_image_url() ?>" alt="meal-img">
</div>
<div class="daily-meal-info">
<div class="daily-meal-title">
<h3><?= $recipe->field_title ?></h3>
<span class="meta"><i class="fa-regular fa-clock"></i> <?= $recipe->field_estimated_time ?> mins to make</span>
</div>
<div class="daily-meal-ingredients">
<ul class="ingredients-list">
<?php for ($i=0; $i < count($ingredients) && $i <= 6; $i++): ?>
<li><?= $ingredients[$i]->ingredient_name ?></li>
<?php endfor; ?>
</ul>
</div>
</div>
</a>

View File

@ -0,0 +1,43 @@
<?php
require_once APPS_PATH . '/Recipes/components.php';
require_once(INCLUDES_PATH . '/Const/recipes.php');
the_header(
'Daily Meals',
'What do you want to eat today?',
'daily-meals-page',
[
['keywords', 'recipes, cooking, food, cuisine'],
]
);
?>
<div class="container">
<div class="daily-recipes">
<?php
foreach ($context['weeks'] as $day => $recipe_prefetches) {
?>
<?php if(!empty($recipe_prefetches)): ?>
<div class="daily-meals">
<h2 class="title">Your Menu for <?= ucfirst($day) ?></h2>
<div class="daily-meals-grid">
<?php
foreach ($recipe_prefetches as $recipe_pref) {
the_product_recipes_item($recipe_pref['origin'], $recipe_pref['relations']);
}
?>
</div>
</div>
<?php else: ?>
<div class="daily-meals">
<h2 class="title">No meals added for <?= ucfirst($day); ?></h2>
<p class="no-daily-meals-msg">You have not added any recipes for <?= ucfirst($day);?>, please go to the catalog and add recipes.</p>
</div>
<?php endif; ?>
<?php
}
?>
</div>
</div>
<?php the_footer(); ?>

View File

@ -0,0 +1 @@
<?php $context['fpdf']->Output('I', 'recipe.pdf') ?>

View File

@ -0,0 +1,36 @@
<?php
require_once APPS_PATH . '/Recipes/components.php';
the_header(
'Favorites',
'Here are your favorite recipes. Enjoy cooking!',
'favorites-page',
[
['keywords', 'favorites, recipes, cooking, food'],
]
);
?>
<div class="container">
<div class="favorites">
<?php if(!empty($context['recipes'])): ?>
<div class="favorites-items">
<?php
foreach ($context['recipes'] as $recipe) {
the_product_item($recipe);
}
?>
</div>
<?php else: ?>
<div class="nothing">
Nothing to show
</div>
<?php endif; ?>
</div>
</div>
<?php
the_footer();
?>

View File

@ -0,0 +1,122 @@
<?php
the_header(
'Submit a Recipe',
'Share your culinary creations with the world! Fill out the form below to submit your recipe for review and publication.',
'submit-recipe-body',
[
['keywords', 'recipe, submit, share, cooking, culinary'],
]
);
?>
<link rel="stylesheet" href="<?php echo ASSETS_PATH . '/css/single-submit.css' ?>">
<div class="container">
<div class="submit-recipe">
<h1 class="title">Submit a Recipe</h1>
</div>
<?php
if(isset($context['success_message']))
the_alert($context['success_message'], 'success');
if(isset($context['error_message']))
the_alert($context['error_message'], 'warning');
?>
<form class="single-submit-form" method="post" enctype="multipart/form-data">
<label for="title-input">Title</label><span>*</span>
<div class="input">
<input type="text" id="title-input" name="title" placeholder="Enter the recipe title" required>
</div>
<label for="description-input">Description</label><span>*</span>
<div class="input">
<textarea id="description-input" name="description" placeholder="Describe your recipe in detail"
required></textarea>
</div>
<label for="ingredients-input">Ingredients</label><span>*</span>
<table class="ingredients-table">
<thead>
<tr>
<th>Name</th>
<th>Count</th>
</tr>
</thead>
<tbody class="ing-table-rows">
</tbody>
<tfoot>
<tr>
<td>
<div id="search-ingredient">
<input type="text" placeholder="Search ingredients...">
<div class="custom-select-dropdown" hidden>
<div class="dropdown-item hover-anim">Vegetable Oil</div>
</div>
</div>
</td>
<td>
<button type="button" id="add-ingredient-btn" class="btn btn-primary hover-anim add-ingredient-btn">Create new</button>
</td>
</tr>
</tfoot>
</table>
<label for="image-input">Image</label><span>*</span>
<div class="input-file">
<input type="file" id="image-input" name="image" accept="image/*" required>
</div>
<label for="time-input">Estimated Time (minutes)</label><span>*</span>
<div class="input">
<input type="number" id="time-input" name="est-time" placeholder="Enter the recipe estimated time" required>
</div>
<label for="price-input">Estimated Price ($)</label><span>*</span>
<div class="input">
<input type="number" id="price-input" name="est-price" placeholder="Enter the recipe estimated price"
required>
</div>
<label for="category-select">Category</label><span>*</span>
<div class="input-select">
<select id="category-select" name="category" required>
<option value="">Select a category</option>
<?php foreach ($context['category_options'] as $category): ?>
<option value="<?php echo $category[0]; ?>">
<?php echo $category[1]; ?>
</option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary hover-anim">Submit Recipe</button>
</form>
<div id="overlay" class="hidden"></div>
<div class="ing-modal hidden" id="ingredient-modal">
<div id="new-ingredient">
<label for="ing-name-input">Ingredient Name</label><span>*</span>
<div class="input">
<input type="text" id="ing-name-input" name="title" placeholder="Enter the ingredient name"
required>
</div>
<label for="unit-input">Unit of measure</label><span>*</span>
<div class="input">
<input type="text" id="ing-unit-input" name="title" placeholder="Enter the unit of measure"
required>
</div>
<button type="button" id="new-ingredient-submit" class="btn btn-primary hover-anim">Add new ingredient</button>
</div>
</div>
</div>
<?php the_footer(array(
ASSETS_PATH . '/js/single-submit.js',
)); ?>

View File

@ -0,0 +1,194 @@
<?php
require_once(INCLUDES_PATH . '/Const/recipes.php');
the_header(
$context['recipe']->field_title,
'This is a single recipe page where you can view the details of the recipe, including ingredients, instructions, and more.',
'recipe',
[
['keywords', 'recipes, cooking, food, cuisine'],
]
);
?>
<div id="recipe-id" hidden><?= $context['recipe']->get_id() ?></div>
<div id="in-usermenu" hidden><?= $context['recipe']->in_usermenu; ?></div>
<div class="container">
<div class="single-recipe">
<div class="single-recipe-info">
<div class="single-recipe-info__image">
<img src="<?= $context['recipe']->get_image_url(); ?>"
alt="<?php echo $context['recipe']->field_title; ?>">
</div>
<div class="single-recipe-info__details">
<div class="single-recipe-title">
<h1 class="title"><?php echo $context['recipe']->field_title; ?></h1>
</div>
<div class="single-recipe-data">
<div class="single-recipe-data__item">
<span class="data-name">Category: </span>
<span class="data"><?= $context['recipe']->category_name ?></span>
</div>
<div class="single-recipe-data__item">
<span class="data-name">Author: </span>
<span class="data"><?= $context['author']->field_username ?></span>
</div>
<div class="single-recipe-data__item">
<span class="data-name">Estimated Price: </span>
<span class="data"><?php echo $context['recipe']->get_price(); ?></span>
</div>
<div class="single-recipe-data__item">
<span class="data-name">Time To Make: </span>
<span class="data"><?php echo $context['recipe']->get_time(); ?></span>
</div>
<div class="single-recipe-data__item">
<span class="data-name">Ingredients: </span>
<span class="data"><?= join(", ", $context['ingredients']) ?></span>
</div>
<div class="single-recipe-data__item">
<span class="data-name">Date Created: </span>
<span class="data"><?php echo $context['recipe']->field_created_at; ?></span>
</div>
</div>
<div class="button-ctrl">
<select class="hidden" name="daily-meal-select" id="daily-meal-day">
<?php foreach (DAYS_OF_WEEK as $day): ?>
<option value="<?= $day ?>"><?= ucfirst($day) ?></option>
<?php endforeach; ?>
<option value="remove">Remove From List</option>
</select>
<div class="day-select-wrapper">
<div class="hover-anim">
<button type="button" href="#" id="day-select" class="btn btn-primary day-select">Add to
list</button>
<i class="fa-solid fa-list select-icon"></i>
</div>
<div id="custom-select-dropdown" class="custom-select-dropdown hidden">
<?php foreach (DAYS_OF_WEEK as $day): ?>
<div class="dropdown-item hover-anim" data-value="<?= $day ?>">
<?= ucfirst($day) ?>
</div>
<?php endforeach; ?>
<div class="dropdown-item hover-anim" data-value="remove">Remove From List</div>
</div>
</div>
<div class="small-btns">
<button id="favorite-btn"
class="btn btn-secondary btn-small hover-anim <?= $context['recipe']->is_in_favorite ? 'active' : '' ?>"
title="Add To Favorites">
<i
class="<?= $context['recipe']->is_in_favorite ? 'fa-solid' : 'fa-regular' ?> fa-heart"></i>
</button>
<a class="btn btn-secondary btn-small hover-anim"
href="<?php the_permalink('recipes:export-pdf', [$context['recipe']->get_id()]); ?>"
target="_blank" title="Export As PDF">
<i class="fa-solid fa-download"></i>
</a>
<button class="btn btn-secondary btn-small hover-anim" id="qr-btn" title="Get QR Code">
<i class="fa-solid fa-qrcode"></i>
</button>
</div>
<div id="overlay" class="hidden"></div>
<div class="qr-popup hidden">
<div id="qrcode"></div>
<a id="downloadLink" class="btn btn-primary hover-anim" href="#" download="recipe-qr.png">
Download QR Code
</a>
</div>
</div>
</div>
</div>
<div class="single-recipe-content">
<div class="single-instructions">
<h2 class="title">Instructions</h2>
<?= $context['recipe']->get_html_instruction(); ?>
</div>
<div class="single-ingredients">
<h2 class="title">Ingredients</h2>
<table class="ingredients-table">
<thead>
<tr>
<th>Name</th>
<th>Count</th>
</tr>
</thead>
<tbody>
<?php
foreach ($context['ingredients'] as $ing) {
?>
<tr>
<td><?= $ing; ?></td>
<td><?= $ing->get_count(); ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
<div class="single-recipe-reviews">
<h2 class="title">Reviews</h2>
<h3 class="rating">Average Rating: <?= $context['reviews_average'] ?> <i class="fa-regular fa-star"></i></h3>
<div class="reviews">
<div class="review-list">
<h4 class="subtitle">All Reviews</h4>
<?php if(empty($context['reviews'])): ?>
<div class="nothing">Nothing to show</div>
<?php endif; ?>
<?php foreach ($context['reviews'] as $review): ?>
<div class="review">
<span class="review-title"><?= $review->field_rating ?> <i class="fa-solid fa-star"></i>&nbsp;&nbsp;<?= $review->field_title ?></span>
<div class="review-body">
<?= $review->get_html_content() ?>
</div>
<div class="single-review-meta">
<div class="review-meta__user meta">
<i class="fa-regular fa-user"></i> <?= $review->author_username ?>
</div>
<div class="review-meta__date meta"><?= $review->get_date() ?></div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php if($context['display_review_form']): ?>
<div class="review-form">
<h4 class="subtitle">Your Review</h4>
<?php if(isset($context['reviews_error'])) {
the_alert($context['reviews_error'], 'warning');
} ?>
<form id="review-form" class="review-form__form" method="post" action="#">
<div class="rating-selection">
<label for="rating-select">Choose rating: </label>
<select name="rating-select" id="rating select">
<option>Rating</option>
<option value="1">1 Star</option>
<option value="2">2 Star </option>
<option value="3">3 Star</option>
<option value="4">4 Star</option>
<option value="5">5 Star</option>
</select>
</div>
<div class="input">
<input type="text" name="review-title" id="review-title-input" placeholder="Review Title"
required>
</div>
<textarea name="review-body-input" id="review-body-input" placeholder="Write your review here"></textarea>
<button type="submit" class="btn btn-primary hover-anim">Submit</button>
</form>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php the_footer(array(
ASSETS_PATH . '/js/single.js',
)); ?>

View File

@ -0,0 +1,111 @@
<?php
namespace Lycoreco\Apps\Recipes\Utils;
use FPDF;
use Lycoreco\Apps\Recipes\Models\IngredientInRecipeModel;
use Lycoreco\Apps\Recipes\Models\RecipeModel;
class RecipePDF extends FPDF
{
public RecipeModel $recipe;
public function __construct(RecipeModel $recipe)
{
parent::__construct('P', 'mm', 'A4');
$this->recipe = $recipe;
}
function Header()
{
$recipe = $this->recipe;
// Background
$this->SetTextColor(0, 139, 112);
$this->SetFont('Arial', 'B', 16);
$this->Image(BASE_PATH . '/assets/images/fridgeLogo.png', 10, 6, 20);
$this->Cell(80);
$this->Cell(30, 10, $recipe->field_title, 0, 0, 'C');
$this->Ln(8);
$this->SetFont('Arial', '', 12);
$this->SetTextColor(0, 0, 0);
$this->Cell(80);
$this->Cell(30, 10, $recipe->category_name, 0, 0, 'C');
$this->Ln(20);
}
function Footer()
{
// Position at 1.5 cm from bottom
$this->SetY(-15);
// Arial italic 8
$this->SetFont('Arial', 'I', 8);
// Text color in gray
$this->SetTextColor(128);
// Page number
$this->Cell(0, 3, 'Page ' . $this->PageNo(), 0, 0, 'C');
$this->Ln();
$this->Cell(0, 10, 'LycoReco', 0, 0, 'C');
}
function RecipeTable()
{
$header = array('Ingredient', 'Count');
$ingredients = IngredientInRecipeModel::filter(array(
[
'name' => 'obj.recipe_id',
'type' => '=',
'value' => $this->recipe->get_id()
]
));
// Colors, line width and bold font
$this->SetFillColor(0, 139, 122);
$this->SetTextColor(255);
$this->SetDrawColor(179, 179, 179);
$this->SetLineWidth(.3);
$this->SetFont('', 'B');
// Header
$w = array(80, 35);
$this->Cell(35);
for ($i = 0; $i < count($header); $i++)
$this->Cell($w[$i], 7, $header[$i], 1, 0, 'C', true);
$this->Ln();
// Body
$this->SetFillColor(224,235,255);
$this->SetTextColor(0);
$this->SetFont('');
foreach ($ingredients as $ing) {
$this->Cell(35);
$this->Cell($w[0], 6, $ing->ingredient_name, 'LR', 0, 'L', false);
$this->Cell($w[1], 6, $ing->get_count(), 'LR', 0, 'L', false);
$this->Ln();
}
$this->Cell(35);
$this->Cell(array_sum($w),0,'','T');
$this->Ln(10);
}
function RecipeContent()
{
$recipe = $this->recipe;
$this->SetFont('Times', '', 14);
$this->SetTextColor(0);
$this->MultiCell(0, 5, txt: $recipe->field_instruction);
$this->Ln();
$this->SetFont('', 'I');
$this->Cell(0, 5, '(Bon appetit!)');
}
public function PrintRecipe()
{
$this->SetTitle($this->recipe->field_title);
$this->AddPage();
$this->RecipeTable();
$this->RecipeContent();
}
}

View File

@ -0,0 +1,12 @@
<?php
use Lycoreco\Apps\Recipes\Models\RecipeModel;
function the_product_item(RecipeModel $recipe)
{
include APPS_PATH . '/Recipes/Templates/components/catalog-item.php';
}
function the_product_recipes_item(RecipeModel $recipe, array $ingredients)
{
include APPS_PATH . '/Recipes/Templates/components/recipe-ings-item.php';
}

13
apps/Recipes/urls.php Normal file
View File

@ -0,0 +1,13 @@
<?php
use Lycoreco\Apps\Recipes\Controllers;
use Lycoreco\Includes\Routing\Path;
$recipes_urls = [
new Path('/recipe/[:int]', new Controllers\SingleRecipeController(), 'single'),
new Path('/recipe/[:int]/export-pdf', new Controllers\ExportPdfController(), 'export-pdf'),
new Path('/catalog', new Controllers\CatalogController(), 'catalog'),
new Path('/daily-meals', new Controllers\DailyMealsController, 'daily-meals'),
new Path('/favorites', new Controllers\FavoritesController(), 'favorites'),
new Path('/submit', new Controllers\SingleSubmitController(), 'single-submit'),
];

View File

@ -1,20 +1,46 @@
:root {
--panel-text: #008b70;
--input-background: #f7f7f7;
--input-border: #b3b3b3;
--input-placeholder: #b3b3b3;
--panel-background: #eaf8eb;
--input-text-color: #000;
--title-color: #015847;
--meta-color: #727272;
--common-text: #000;
--panel-title-color: #000;
--button-primary: #0DBB99;
--button-secondary: #ECECEC;
--title-font: 'Roboto Slab', sans-serif;
--common-font: 'Roboto Condensed', serif;
}
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
body { body {
min-height: 100vh; min-height: 100vh;
font-family: var(--common-font);
font-size: 14px;
color: var(--common-text);
} }
.wrapper-admin { .wrapper-admin {
display: flex; display: flex;
min-height: calc(100vh - 64px); min-height: calc(100vh - 64px);
} }
.wrapper-admin__content { .wrapper-admin__content {
width: 100%; width: 100%;
} }
.header-admin { .header-admin {
background: var(--dark-block-background);
height: 64px; height: 80px;
display: flex; display: flex;
align-items: center; align-items: center;
@ -22,99 +48,127 @@ body {
padding: 15px 10px; padding: 15px 10px;
} }
.header-admin__control { .header-admin__control {
display: flex; display: flex;
} }
.header-admin__control .username { .header-admin__control .username {
color: var(--h-color); color: var(--common-text);
font-family: var(--font-family-header); font-size: 16px;
font-weight: 500;
font-family: var(--common-font);
margin-right: 10px; margin-right: 10px;
} }
.header-admin__control .username span { .header-admin__control .username span {
color: var(--link-color); color: var(--title-color);
} }
.header-admin__control .links a { .header-admin__control .links a {
color: var(--h-color);
text-decoration: none; text-decoration: none;
font-size: 16px;
font-weight: 500;
font-family: var(--common-font);
color: var(--common-text);
} }
.admin-sidebar { .admin-sidebar {
width: 100%; width: 100%;
max-width: 314px; max-width: 314px;
flex-shrink: 0; flex-shrink: 0;
background: var(--block-background); background: var(--panel-background);
} }
.admin-sidebar .btn { .admin-sidebar .btn {
display: block; display: block;
margin: 20px 20px 20px 20px; margin: 20px 20px 20px 20px;
} }
.admin-sidebar__list { .admin-sidebar__list {
list-style: none; list-style: none;
} }
.admin-sidebar__list a { .admin-sidebar__list a {
display: block; display: block;
padding: 10px 20px; padding: 10px 20px;
color: var(--h-color); color: var(--panel-text);
font-size: 20px; font-size: 20px;
text-decoration: none; text-decoration: none;
} }
.admin-sidebar__list a:hover, .admin-sidebar__list a:hover,
.admin-sidebar__list a.active { .admin-sidebar__list a.active {
background: #ffffff2e; color: #000;
} }
.admin-sidebar__list a i { .admin-sidebar__list a i {
width: 34px; width: 34px;
} }
.admin-sidebar__list a span { .admin-sidebar__list a span {
background: #f00; background: #f00;
padding: 2px 7px; padding: 2px 7px;
font-size: 15px; font-size: 15px;
border-radius: 100%; border-radius: 100%;
} }
.admin-container { .admin-container {
margin: 0 auto; margin: 0 auto;
width: 100%; width: 100%;
max-width: 1100px; max-width: 1100px;
padding: 22px 30px; padding: 22px 30px;
} }
.admin-container h2 { .admin-container h2 {
font-size: 24px; font-size: 24px;
margin-bottom: 20px; margin-bottom: 20px;
} }
.admin-container section { .admin-container section {
margin-bottom: 25px; margin-bottom: 25px;
} }
#dashboard-stats { #dashboard-stats {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 20px; gap: 20px;
} }
.dashboard-stats__item { .dashboard-stats__item {
display: flex; display: flex;
align-items: center; align-items: center;
color: var(--h-color); color: var(--common-text);
background: var(--block-background); background: var(--panel-background);
border-radius: 5px; border-radius: 5px;
padding: 12px 20px; padding: 12px 20px;
} }
.dashboard-stats__item .icon { .dashboard-stats__item .icon {
font-size: 36px; font-size: 36px;
margin-right: 20px; margin-right: 20px;
flex-shrink: 0; flex-shrink: 0;
} }
.dashboard-stats__item .value { .dashboard-stats__item .value {
font-size: 24px; font-size: 24px;
font-weight: 700; font-weight: 700;
} }
.dashboard-stats__item.top-sales .icon { .dashboard-stats__item.top-sales .icon {
color: #FF7B00; color: #FF7B00;
} }
.dashboard-stats__item.new-users .icon { .dashboard-stats__item.new-users .icon {
color: #00A3E8; color: #00A3E8;
} }
.dashboard-stats__item.orders .icon { .dashboard-stats__item.orders .icon {
color: #BA00E8; color: #BA00E8;
} }
.dashboard-stats__item.profit .icon { .dashboard-stats__item.profit .icon {
color: #00E842; color: #00E842;
} }
@ -125,11 +179,12 @@ body {
justify-content: space-between; justify-content: space-between;
padding: 14px 22px; padding: 14px 22px;
background: var(--block-background); background: var(--panel-background);
border-radius: 5px; border-radius: 10px;
} }
.admin-table { .admin-table {
color: var(--h-color); color: var(--common-text);
margin-bottom: 15px; margin-bottom: 15px;
font-size: 14px; font-size: 14px;
@ -138,67 +193,93 @@ body {
border-radius: 5px; border-radius: 5px;
overflow: hidden; overflow: hidden;
} }
.admin-table thead { .admin-table thead {
background: var(--block-background); background: #0DBB99;
color: #eaf8eb;
} }
.admin-table td, .admin-table td,
.admin-table th { .admin-table th {
padding: 15px 10px; padding: 15px 10px;
} }
.admin-table a {
color: var(--title-color);
}
.admin-block__content .admin-block__table { .admin-block__content .admin-block__table {
border-radius: 5px; border-radius: 5px;
overflow: hidden; overflow: hidden;
margin-bottom: 10px; margin-bottom: 10px;
} }
.admin-table tbody tr:nth-child(odd), .admin-table tbody tr:nth-child(odd),
.admin-block__table .row:nth-child(odd) { .admin-block__table .row:nth-child(odd) {
background: #9f9f9f; background: #eaf8eb;
} }
.admin-table tbody tr:nth-child(even), .admin-table tbody tr:nth-child(even),
.admin-block__table .row:nth-child(even) { .admin-block__table .row:nth-child(even) {
background: #767676; background: #b6f1ba;
} }
.admin-block__table { .admin-block__table {
color: var(--h-color); color: var(--common-text);
} }
.admin-block__table .row { .admin-block__table .row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.admin-single { .admin-single {
display: grid; display: grid;
grid-template-columns: 1fr 320px; grid-template-columns: 1fr 320px;
gap: 20px; gap: 20px;
} }
.admin-block { .admin-block {
overflow: hidden; overflow: hidden;
border-radius: 5px; border-radius: 5px;
border: 1px solid var(--block-background); border: 1px solid #015847;
margin-bottom: 20px; margin-bottom: 20px;
background: #f1fff2
} }
.admin-block__content { .admin-block__content {
padding: 10px; padding: 10px;
} }
.admin-block__title { .admin-block__title {
font-size: 18px; font-size: 18px;
font-family: var(--font-family-header); font-family: var(--common-font);
background: var(--block-background); background-color: #015847;
padding: 10px 10px; padding: 10px 10px;
color: var(--h-color); color: #eaf8eb;
} }
.admin-block__table .row { .admin-block__table .row {
padding: 10px; padding: 10px;
} }
.admin-block__table a {
color: var(--title-color);
}
.admin-container.delete { .admin-container.delete {
text-align: center; text-align: center;
} }
.admin-container.delete .meta-text { .admin-container.delete .meta-text {
font-size: 18px; font-size: 18px;
margin-bottom: 15px; margin-bottom: 15px;
} }
.admin-container.delete .btn-control { .admin-container.delete .btn-control {
justify-content: space-around; justify-content: space-around;
} }
.order-stat { .order-stat {
display: flex; display: flex;
align-items: center; align-items: center;
@ -206,14 +287,90 @@ body {
font-size: 18px; font-size: 18px;
justify-content: space-between; justify-content: space-between;
} }
.order-content .admin-block__table { .order-content .admin-block__table {
margin-top: 15px; margin-top: 15px;
} }
.order-stat span:first-child { .order-stat span:first-child {
color: var(--text-color); color: var(--common-text);
font-weight: 400; font-weight: 400;
font-family: var(--font-family-header); font-family: var(--common-font);
} }
.order-stat span:last-child { .order-stat span:last-child {
color: var(--h-color); color: var(--title-color);
}
.header-admin {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background-color: var(--panel-background);
}
.logo-admin {
color: var(--title-color);
font-family: var(--title-font);
font-size: 20px;
font-weight: 700;
}
.input-admin input {
background: var(--input-background);
border: 1px solid var(--input-border);
color: var(--input-text-color);
padding: 10px;
font-size: 16px;
border-radius: 10px;
box-sizing: border-box;
}
.input-admin button {
border: 1px solid var(--input-border);
}
.admin-container .title {
text-align: start;
}
.input-admin button {
padding: 10px;
border-radius: 10px;
}
.admin-block__content .btn-control {
display: flex;
justify-content: space-between;
}
.admin-single__form img {
height: 260px;
width: 350;
border-radius: 10px;
margin-bottom: 20px;
}
.admin-single__form input[type="file"] {
margin-bottom: 20px;
}
.recipe-author {
color: var(--title-color);
}
.admin-single__form .input-checkbox {
accent-color: #0DBB99;
} }

View File

@ -0,0 +1,23 @@
.ingredients-table {
overflow: auto;
}
.ingredients-table .input {
margin-bottom: 0;
}
.ingredients-table .input input[type="number"] {
height: auto;
padding: 5px;
width: 100px;
}
#search-ingredient {
position: relative;
}
#search-ingredient input {
border: 0;
padding: 5px 0;
border: 0 !important;
outline: none !important;
}
.custom-select-dropdown .dropdown-item:last-child {
border-top: 0;
}

File diff suppressed because it is too large Load Diff

BIN
assets/food_3d.glb Normal file

Binary file not shown.

View File

@ -8,7 +8,7 @@
* @param {Function} onError * @param {Function} onError
*/ */
function sendAjax(action, function sendAjax(action,
args = [ ], args = [],
onLoad = () => { }, onLoad = () => { },
onSuccess = (data) => { }, onSuccess = (data) => { },
onError = (error) => { }) { onError = (error) => { }) {
@ -43,6 +43,87 @@ function showToastify(message, type = "info") {
gravity: "bottom", gravity: "bottom",
position: "left", position: "left",
className: type, className: type,
duration: 3000 }) duration: 3000
})
.showToast(); .showToast();
} }
document.addEventListener('DOMContentLoaded', function () {
const toggle = document.getElementById('menu-toggle');
const nav = document.querySelector('.nav');
const icon = document.getElementById('menu-icon');
toggle.addEventListener('click', function () {
nav.classList.toggle('nav-open');
if (nav.classList.contains('nav-open')) {
icon.classList.remove('fa-bars');
icon.classList.add('fa-xmark');
} else {
icon.classList.remove('fa-xmark');
icon.classList.add('fa-bars');
}
});
});
const searchInput = document.getElementById('search-input');
const searchResults = document.getElementById('search-results');
let searchTimeout;
searchInput.addEventListener('input', function () {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(async () => {
let searchValue = this.value.trim();
if (searchValue.length < 3) {
searchResults.innerHTML = '';
searchResults.hidden = true;
return;
}
const formData = new FormData();
formData.append('action', 'search');
formData.append('query', searchValue);
const response = await fetch('/ajax', {
method: 'POST',
body: formData
});
const json = await response.json();
if (!response.ok) {
const message = json.error || 'Something went wrong.';
showToastify(message, 'error');
return;
}
const results = json.result;
searchResults.innerHTML = '';
if (results.length > 0) {
results.forEach(result => {
searchResults.innerHTML += `
<div class="search-result-item hover-anim">
<a href="${result.url}" class="search-result-link">
${result.field_title}
</a>
</div>
`;
});
searchResults.hidden = false;
} else {
searchResults.innerHTML = `<div class="search-result-item">No recipes found</div>`;
searchResults.hidden = false;
}
}, 300);
});
document.addEventListener('click', function (event) {
if (!searchResults.contains(event.target) && event.target !== searchInput) {
searchResults.hidden = true;
}
});

146
assets/js/single-submit.js Normal file
View File

@ -0,0 +1,146 @@
const addIngredientBtn = document.getElementById('add-ingredient-btn');
const overlay = document.getElementById('overlay');
const ingModal = document.getElementById('ingredient-modal');
addIngredientBtn.addEventListener('click', () => {
ingModal.classList.remove('hidden');
overlay.classList.remove('hidden');
});
document.addEventListener('click', (e) => {
if (!ingModal.contains(e.target) && !addIngredientBtn.contains(e.target)) {
ingModal.classList.add('hidden');
overlay.classList.add('hidden');
}
});
const ingredientSubmitBtn = document.getElementById('new-ingredient-submit');
const ingredientName = document.getElementById('ing-name-input');
const ingredientUnit = document.getElementById('ing-unit-input');
ingredientSubmitBtn.addEventListener('click', async (e) => {
const name = ingredientName.value.trim();
const unit = ingredientUnit.value.trim();
if (!name || !unit) {
showToastify('Please fill in all fields.', 'error');
return;
}
const formData = new FormData();
formData.append('action', 'create_ingredient');
formData.append('name', name);
formData.append('unit', unit);
const response = await fetch('/ajax', {
method: 'POST',
body: formData
});
const json = await response.json();
if (!response.ok) {
const message = json.error;
showToastify(message, 'error');
return;
}
showToastify(json.success, 'success');
ingredientName.value = '';
ingredientUnit.value = '';
ingModal.classList.add('hidden');
overlay.classList.add('hidden');
});
const searchIngredientWrapper = document.getElementById('search-ingredient');
const searchIngInput = searchIngredientWrapper.querySelector('input');
const searchIngDropdown = searchIngredientWrapper.querySelector('.custom-select-dropdown');
const tableIngRows = document.querySelector('.ing-table-rows');
const ingredientsAdded = new Map();
searchIngInput.addEventListener('input', function () {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(async () => {
let searchValue = this.value.trim();
if (searchValue.length < 3) {
searchIngInput.innerHTML = '';
searchIngDropdown.hidden = true;
return;
}
const formData = new FormData();
formData.append('action', 'search_ingredient');
formData.append('query', searchValue);
const response = await fetch('/ajax', {
method: 'POST',
body: formData
});
const json = await response.json();
if (!response.ok) {
const message = json.error || 'Something went wrong.';
showToastify(message, 'error');
return;
}
const results = json.result;
searchIngDropdown.innerHTML = '';
if (results.length > 0) {
results.forEach(ingredient => {
const ingredientName = `${ingredient.field_name} (${ingredient.field_unit_name})`;
const option = document.createElement('div');
option.className = "dropdown-item hover-anim";
option.textContent = ingredientName;
option.addEventListener('click', (e) => {
const row = document.createElement('tr');
if(ingredientsAdded.get(ingredient.id) != undefined) {
showToastify("This field already exists.", "error");
return;
}
row.innerHTML += `
<td>${ingredientName}</td>
<td>
<input class="ing-id" type="number" name="ing-id[]" hidden />
<div class="input ing-name"><input type="number" name="ing-count[]" value="1" /></div>
<i class="fa-solid fa-trash"></i>
</td>
`;
row.querySelector(".ing-id").value = ingredient.id;
const rowDeleteBtn = row.querySelector('i');
rowDeleteBtn.addEventListener('click', (e) => {
ingredientsAdded.delete(ingredient.id);
row.remove();
});
ingredientsAdded.set(ingredient.id, ingredientName);
tableIngRows.append(row);
searchIngInput.value = '';
searchIngDropdown.hidden = true;
});
searchIngDropdown.append(option);
});
searchIngDropdown.hidden = false;
} else {
searchIngDropdown.innerHTML = `<div class="search-result-item">No recipes found</div>`;
searchIngDropdown.hidden = false;
}
}, 300);
});

140
assets/js/single.js Normal file
View File

@ -0,0 +1,140 @@
const recipeId = document.getElementById('recipe-id').textContent;
const toggleBtn = document.getElementById('day-select');
const dropdown = document.getElementById('custom-select-dropdown');
toggleBtn.addEventListener('click', () => {
dropdown.classList.toggle('hidden');
});
document.addEventListener('click', (e) => {
if (!dropdown.contains(e.target) && !toggleBtn.contains(e.target)) {
dropdown.classList.add('hidden');
}
});
const options = document.querySelectorAll('.dropdown-item');
const inUsermenu = document.getElementById('in-usermenu').textContent;
if (inUsermenu) {
options.forEach(option => {
if (option.getAttribute('data-value') === inUsermenu && option.getAttribute('data-value') !== 'remove') {
option.classList.add('dropdown-selected');
toggleBtn.textContent = option.textContent;
}
});
}
options.forEach(option => {
option.addEventListener('click', async (e) => {
const selectedValue = option.getAttribute('data-value');
options.forEach(opt => opt.classList.remove('dropdown-selected'));
const formData = new FormData();
formData.append('action', 'usermenu');
formData.append('recipe_id', recipeId);
formData.append('dayofweek', selectedValue);
const response = await fetch('/ajax', {
method: 'POST',
body: formData
});
const json = await response.json();
if (!response.ok) {
const message = json.error;
showToastify(message, 'error');
return;
}
if (selectedValue === 'remove') {
toggleBtn.textContent = 'Add to list';
} else {
toggleBtn.textContent = option.textContent;
option.classList.add('dropdown-selected');
}
dropdown.classList.add('hidden');
showToastify(json.success, 'success');
});
});
const qrContainer = document.getElementById("qrcode");
const downloadLink = document.getElementById("downloadLink");
const qrBtn = document.getElementById("qr-btn");
const qrPopup = document.querySelector(".qr-popup");
const overlay = document.getElementById('overlay');
qrBtn.addEventListener("click", () => {
qrPopup.classList.toggle("hidden")
overlay.classList.toggle("hidden")
qrContainer.innerHTML = "";
new QRCode(qrContainer, {
text: window.location.href,
width: 200,
height: 200
});
setTimeout(() => {
const qrImg = qrContainer.querySelector("img");
if (qrImg) {
downloadLink.href = qrImg.src;
}
}, 500);
});
document.addEventListener('click', (e) => {
if (!qrPopup.contains(e.target) && !qrBtn.contains(e.target)) {
qrPopup.classList.add("hidden");
overlay.classList.add("hidden");
}
});
const favoriteBtn = document.getElementById('favorite-btn');
const favoriteIcon = favoriteBtn.querySelector('i');
favoriteBtn.addEventListener('click', async (e) => {
const isFavorite = favoriteBtn.classList.contains('active');
const type = isFavorite ? 'remove' : 'add';
const formData = new FormData();
formData.append('action', 'favorites');
formData.append('recipe_id', recipeId);
formData.append('type', type);
favoriteBtn.disabled = true;
const response = await fetch('/ajax', {
method: 'POST',
body: formData
});
favoriteBtn.disabled = false;
const json = await response.json();
if (!response.ok) {
const message = json.error;
showToastify(message, 'error');
return;
}
favoriteBtn.classList.toggle('active');
if (type == 'add') {
favoriteIcon.classList.remove('fa-regular');
favoriteIcon.classList.add('fa-solid');
} else {
favoriteIcon.classList.add('fa-regular');
favoriteIcon.classList.remove('fa-solid');
}
showToastify(json.success, 'success');
});

View File

@ -0,0 +1,68 @@
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
// Сцена, камера, рендерер
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xeaf8eb);
const width = 430;
const height = 350;
const camera = new THREE.PerspectiveCamera(
75,width / height, 0.1, 1000
);
camera.position.set(9, 3, 3); // Слегка сверху и сбоку
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true, canvas: document.querySelector('#food-3d'), });
renderer.setSize(width, height, false);
// Еда
const loader = new GLTFLoader();
let foodModel;
loader.load('/assets/food_3d.glb', function(gltf) {
foodModel = gltf.scene;
foodModel.position.set(0, -1, 1.5);
foodModel.scale.set(25, 25, 30);
scene.add(foodModel);
}, function (xhr) {
// called while loading is progressing
console.log((xhr.loaded / xhr.total * 100) + '% loaded');
},
function (error) {
// called when loading has errors
console.log('An error happened', error);
});
// Свет (не обязателен для MeshNormalMaterial, но пригодится для других)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.4);
directionalLight.position.set(3, 5, 2);
directionalLight.castShadow = false;
scene.add(directionalLight);
const hemisphereLight = new THREE.HemisphereLight(0xaaaaaa, 0x444444, 0.6);
scene.add(hemisphereLight);
let mouseX = 0;
let mouseY = 0;
window.addEventListener('mousemove', (event) => {
// Нормализуем координаты мыши в [-1, 1]
mouseX = (event.clientX / window.innerWidth) * 2 - 1;
mouseY = -((event.clientY / window.innerHeight) * 2 - 1);
});
function animate() {
requestAnimationFrame(animate);
foodModel.position.z = 1.5 + mouseX * 0.7;
foodModel.position.y = -1 + mouseY * -0.7;
renderer.render(scene, camera);
}
animate();

1
assets/qrcode/qrcode.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -20,7 +20,7 @@
<script src="<?php echo ASSETS_PATH . '/js/main.js' ?>"></script> <script src="<?php echo ASSETS_PATH . '/js/main.js' ?>"></script>
<script src="<?php echo ASSETS_PATH . '/toastify/toastify-js.js' ?>"></script> <script src="<?php echo ASSETS_PATH . '/toastify/toastify-js.js' ?>"></script>
<script src="<?php echo ASSETS_PATH . '/qrcode/qrcode.min.js' ?>"></script>
<?php foreach($scripts as $script): ?> <?php foreach($scripts as $script): ?>
<script src="<?php echo $script ?>"></script> <script src="<?php echo $script ?>"></script>
<?php endforeach; ?> <?php endforeach; ?>

View File

@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -11,14 +12,16 @@
<!-- Meta tags --> <!-- Meta tags -->
<meta name="robots" content="nofollow, noindex"> <meta name="robots" content="nofollow, noindex">
<?php foreach($meta_tags as $tag): ?> <?php foreach ($meta_tags as $tag): ?>
<meta name="<?php echo $tag[0] ?>" content="<?php echo $tag[1] ?>"> <meta name="<?php echo $tag[0] ?>" content="<?php echo $tag[1] ?>">
<?php endforeach; ?> <?php endforeach; ?>
<!-- Google fonts --> <!-- Google fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:ital,wght@0,100..900;1,100..900&family=Roboto+Slab:wght@100..900&display=swap" rel="stylesheet"> <link
href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:ital,wght@0,100..900;1,100..900&family=Roboto+Slab:wght@100..900&display=swap"
rel="stylesheet">
<!-- Google fonts/ --> <!-- Google fonts/ -->
<!-- Font Awesome --> <!-- Font Awesome -->
@ -36,27 +39,45 @@
<link rel="stylesheet" href="<?php echo ASSETS_PATH . '/css/reset.css' ?>"> <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/style.css' ?>">
</head> </head>
<body class="<?php echo $body_class ?>"> <body class="<?php echo $body_class ?>">
<header class="header"> <header class="header">
<div class="container"> <div class="container">
<div class="header-inner"> <div class="header-inner">
<div class="logo"> <div class="logo">
<a href="<?php the_permalink("index:home")?>"> <a href="<?php the_permalink("index:home") ?>">
<img src="<?php echo ASSETS_PATH . '/images/fridgeLogo.png'?>" alt="fridgeBitesLogo" class="logo-img"> <img src="<?php echo ASSETS_PATH . '/images/fridgeLogo.png' ?>" alt="fridgeBitesLogo"
class="logo-img">
</a> </a>
</div> </div>
<nav class="nav"> <nav class="nav">
<ul class="nav-list"> <ul class="nav-list">
<li class="nav-item"><a href="<?php the_permalink("index:home")?>" class="nav-link">HOME</a></li> <li class="nav-item"><a href="<?php the_permalink("index:home") ?>" class="nav-link">HOME</a>
<li class="nav-item"><a href="#" class="nav-link">RECIPES</a></li> </li>
<li class="nav-item"><a href="#" class="nav-link">FAVORITES</a></li> <li class="nav-item"><a href="<?php the_permalink("recipes:catalog") ?>"
<li class="nav-item"><a href="#" class="nav-link">MEAL A DAY</a></li> class="nav-link">RECIPES</a></li>
<li class="nav-item"><a href="#" class="nav-link">SUBMIT RECIPE</a></li> <li class="nav-item"><a href="<?php the_permalink("recipes:favorites") ?>"
class="nav-link">FAVORITES</a></li>
<li class="nav-item"><a href="<?php the_permalink("recipes:daily-meals") ?>"
class="nav-link">MEAL A DAY</a></li>
<li class="nav-item"><a href="<?php the_permalink("recipes:single-submit") ?>" class="nav-link">SUBMIT RECIPE</a></li>
</ul> </ul>
</nav> </nav>
<div class="search-and-login"> <div class="search-and-login">
placeholder <i class="fa-solid fa-magnifying-glass search-icon"></i>
<input type="text" id="search-input" class="search-input" placeholder="Search recipes...">
<div class="search-results" id="search-results" hidden>
</div> </div>
<a href="<?php the_permalink(CURRENT_USER ? 'users:profile' : 'users:login') ?>"
class="login-link hover-anim">
<i class="fa-regular fa-user"></i>
</a>
</div>
<button id="menu-toggle" class="menu-toggle" aria-label="Toggle navigation">
<i id="menu-icon" class="fa-solid fa-bars"></i>
</button>
</div> </div>
</div> </div>
</header> </header>

View File

@ -9,6 +9,7 @@
} }
}, },
"require": { "require": {
"phpmailer/phpmailer": "^6.10" "phpmailer/phpmailer": "^6.10",
"setasign/fpdf": "^1.8"
} }
} }

48
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "a475eab76e37559e0cc7396ccf30c49e", "content-hash": "a6db4a4cd2450cf3fbd46a573661b495",
"packages": [ "packages": [
{ {
"name": "phpmailer/phpmailer", "name": "phpmailer/phpmailer",
@ -86,6 +86,52 @@
} }
], ],
"time": "2025-04-24T15:19:31+00:00" "time": "2025-04-24T15:19:31+00:00"
},
{
"name": "setasign/fpdf",
"version": "1.8.6",
"source": {
"type": "git",
"url": "https://github.com/Setasign/FPDF.git",
"reference": "0838e0ee4925716fcbbc50ad9e1799b5edfae0a0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Setasign/FPDF/zipball/0838e0ee4925716fcbbc50ad9e1799b5edfae0a0",
"reference": "0838e0ee4925716fcbbc50ad9e1799b5edfae0a0",
"shasum": ""
},
"require": {
"ext-gd": "*",
"ext-zlib": "*"
},
"type": "library",
"autoload": {
"classmap": [
"fpdf.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Olivier Plathey",
"email": "oliver@fpdf.org",
"homepage": "http://fpdf.org/"
}
],
"description": "FPDF is a PHP class which allows to generate PDF files with pure PHP. F from FPDF stands for Free: you may use it for any kind of usage and modify it to suit your needs.",
"homepage": "http://www.fpdf.org",
"keywords": [
"fpdf",
"pdf"
],
"support": {
"source": "https://github.com/Setasign/FPDF/tree/1.8.6"
},
"time": "2023-06-26T14:44:25+00:00"
} }
], ],
"packages-dev": [], "packages-dev": [],

BIN
fridgebitesDocs2.pdf Normal file

Binary file not shown.

BIN
fridgebitesDocs3 Normal file

Binary file not shown.

View File

@ -133,13 +133,13 @@ function the_pagination(int $count, int $elem_per_page, int $current_page)
continue; continue;
$GET['page'] = $i; $GET['page'] = $i;
?> ?>
<li><a class="btn page-btn" href="<?php the_GET_request($GET) ?>"><?php echo $i ?></a></li> <li><a class="btn btn-secondary" href="<?php the_GET_request($GET) ?>"><?php echo $i ?></a></li>
<?php endfor; ?> <?php endfor; ?>
<!-- Current page --> <!-- Current page -->
<?php if ($current_page > 0 && $current_page <= $total_pages): ?> <?php if ($current_page > 0 && $current_page <= $total_pages): ?>
<li> <li>
<div class="btn active page-btn"><?php echo $current_page ?></div> <div class="btn btn-primary"><?php echo $current_page ?></div>
</li> </li>
<?php endif ?> <?php endif ?>
@ -147,7 +147,7 @@ function the_pagination(int $count, int $elem_per_page, int $current_page)
<?php for ($i = $current_page + 1; $i <= $total_pages && $i <= $current_page + 2; $i++): <?php for ($i = $current_page + 1; $i <= $total_pages && $i <= $current_page + 2; $i++):
$GET['page'] = $i; $GET['page'] = $i;
?> ?>
<li><a class="btn page-btn" href="<?php the_GET_request($GET) ?>"><?php echo $i ?></a></li> <li><a class="btn btn-secondary" href="<?php the_GET_request($GET) ?>"><?php echo $i ?></a></li>
<?php endfor; ?> <?php endfor; ?>
</ul> </ul>
</nav> </nav>
@ -252,4 +252,54 @@ function send_email(string $subject, string $body, string $altBody, string $to_a
$mail->send(); $mail->send();
} }
?>
/**
* Collects related objects for each origin object based on a given foreign key.
*
* Returns an array in the following format:
* [
* [
* 'origin' => $object, // the origin object
* 'relations' => $objects // related objects from the target model
* ],
* ...
* ]
*
* @param array $objects Array of origin objects (their IDs will be used for matching)
* @param string $model_name_format Target model name in "app:model" format to fetch related objects from
* @param string $field_key Foreign key in the related model that refers to the origin object ID
* @return array An array of relation mappings between origin and related objects
*/
function prefetch_related(array $objects, string $model_name_format, string $field_key)
{
[$app, $model] = explode(':', $model_name_format);
$model_name = "Lycoreco\Apps\\" . $app . "\Models\\" . $model;
if (!class_exists($model_name)) {
throw new InvalidArgumentException("Model class $model_name does not exist.");
}
$keys = $keys = array_map(fn($obj) => $obj->get_id(), $objects);
if (empty($keys))
return [];
$related_objects = $model_name::filter(array(
[
'name' => 'obj.' . $field_key,
'type' => 'IN',
'value' => $keys
]
));
return array_map(function ($object) use ($related_objects, $field_key) {
$rels = array_filter($related_objects, function ($rel) use ($object, $field_key) {
return $rel->{'field_' . $field_key} == $object->get_id();
});
return [
'origin' => $object,
'relations' => array_values($rels)
];
}, $objects);
}

View File

@ -0,0 +1,5 @@
<?php
define('DAYS_OF_WEEK', array(
1 => 'monday', 2 => 'tuesday', 3 => 'wednesday', 4 => 'thursday', 5 => 'friday', 6 => 'saturday', 0 => 'sunday'
));

View File

@ -344,7 +344,7 @@ abstract class BaseModel
else else
return false; return false;
} }
static function count($fields, $search, $additional_fields = array()) static function count($fields, $search = '', $additional_fields = array())
{ {
$filter_result = static::filter( $filter_result = static::filter(
$fields, $fields,
@ -364,6 +364,9 @@ abstract class BaseModel
else else
return $filter_result[0]->func_total_count; return $filter_result[0]->func_total_count;
} }
public function getAssocArr() {
return get_object_vars($this);
}
public function delete() public function delete()
{ {
@ -436,7 +439,7 @@ abstract class BaseModel
/** /**
* Return model from Mysql result * Return model from Mysql result
* @param array $pdo_result pdo resut FETCH_MODE = FETCH_ASSOC * @param array $pdo_result pdo resut FETCH_MODE = FETCH_ASSOC
* @return array * @return array(self)
*/ */
protected static function createObjectsFromQuery(array $pdo_result) protected static function createObjectsFromQuery(array $pdo_result)
{ {

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,15 +1,18 @@
<?php <?php
use Lycoreco\Apps\Index\Controllers\ErrorController;
use Lycoreco\Includes\Routing\Router; use Lycoreco\Includes\Routing\Router;
require APPS_PATH . '/Index/urls.php'; require APPS_PATH . '/Index/urls.php';
require APPS_PATH . '/Users/urls.php'; require APPS_PATH . '/Users/urls.php';
require APPS_PATH . '/Admin/urls.php'; require APPS_PATH . '/Admin/urls.php';
require APPS_PATH . '/Ajax/urls.php'; require APPS_PATH . '/Ajax/urls.php';
require APPS_PATH . '/Recipes/urls.php';
Router::includes($index_urls, "index"); Router::includes($index_urls, "index");
Router::includes($users_urls, 'users'); Router::includes($users_urls, 'users');
Router::includes($admin_urls, 'admin'); Router::includes($admin_urls, 'admin');
Router::includes($ajax_urls, 'ajax'); Router::includes($ajax_urls, 'ajax');
Router::includes($recipes_urls, 'recipes');
// Router::set_error_controller('default', new ErrorController()) Router::set_error_controller('default', new ErrorController());
?> ?>