Merge pull request 'Completed web programming project' (#44) from develop into master
Reviewed-on: #44 Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
9
.htaccess
Normal 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>
|
||||
@ -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:
|
||||
|
||||
- [\[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\] 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
|
||||
|
||||
|
||||
@ -38,6 +38,7 @@ abstract class AdminSingleController extends AdminBaseController
|
||||
protected $field_title = 'field_id';
|
||||
protected $edit_title_template = 'Edit [:verbose] "[:field]"';
|
||||
protected $can_save = true;
|
||||
protected $is_new = false;
|
||||
|
||||
/**
|
||||
* Function names with $object attribute.
|
||||
@ -53,6 +54,7 @@ abstract class AdminSingleController extends AdminBaseController
|
||||
*/
|
||||
public function __construct($is_new = false)
|
||||
{
|
||||
$this->is_new = $is_new;
|
||||
$this->context['is_new'] = $is_new;
|
||||
}
|
||||
|
||||
@ -137,7 +139,7 @@ abstract class AdminSingleController extends AdminBaseController
|
||||
case 'image':
|
||||
$file = $_FILES[$field['model_field']];
|
||||
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)) {
|
||||
$field_value = $path;
|
||||
|
||||
21
apps/Admin/Controllers/AdminCategoryController.php
Normal 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']
|
||||
]
|
||||
);
|
||||
}
|
||||
17
apps/Admin/Controllers/AdminCategoryListController.php
Normal 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";
|
||||
}
|
||||
@ -9,6 +9,55 @@ use Lycoreco\Apps\Users\Models\UserModel;
|
||||
*/
|
||||
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';
|
||||
|
||||
/**
|
||||
@ -22,17 +71,15 @@ class AdminDeleteController extends Abstract\AdminBaseController
|
||||
$id = $this->url_context['url_2'];
|
||||
$model_class = '';
|
||||
|
||||
switch ($this->url_context['url_1']) {
|
||||
case 'users':
|
||||
$model_class = "Lycoreco\Apps\Users\Models\UserModel";
|
||||
foreach (self::DELETE_MODELS as $delete_model) {
|
||||
if($this->url_context['url_1'] == $delete_model['url_name']) {
|
||||
$model_class = $delete_model['model'];
|
||||
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(
|
||||
[
|
||||
'name' => 'obj.id',
|
||||
@ -52,16 +99,12 @@ class AdminDeleteController extends Abstract\AdminBaseController
|
||||
// Display field to show what's model
|
||||
$field = '';
|
||||
$back_url = '';
|
||||
switch ($this->url_context['url_1']) {
|
||||
case 'users':
|
||||
$back_url = get_permalink('admin:user', [$model->get_id()]);
|
||||
$field = 'field_username';
|
||||
break;
|
||||
|
||||
case 'user-banlist':
|
||||
$back_url = get_permalink('admin:ban', [$model->get_id()]);
|
||||
$field = 'field_reason';
|
||||
break;
|
||||
foreach (self::DELETE_MODELS as $delete_model) {
|
||||
if($this->url_context['url_1'] == $delete_model['url_name']) {
|
||||
$back_url = get_permalink($delete_model['back_to'], [$model->get_id()]);
|
||||
$field = $delete_model['field_display'];
|
||||
}
|
||||
}
|
||||
|
||||
$context['back_url'] = $back_url;
|
||||
@ -80,15 +123,15 @@ class AdminDeleteController extends Abstract\AdminBaseController
|
||||
$model->delete();
|
||||
|
||||
// Redirect after delete
|
||||
$type_model = $this->url_context['url_1'];
|
||||
switch ($type_model) {
|
||||
case 'users':
|
||||
$link = get_permalink('admin:user-list');
|
||||
break;
|
||||
default:
|
||||
$link = get_permalink('admin:home');
|
||||
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;
|
||||
}
|
||||
}
|
||||
redirect_to($link);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
<?php
|
||||
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
|
||||
{
|
||||
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->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;
|
||||
|
||||
78
apps/Admin/Controllers/AdminRecipeController.php
Normal 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()
|
||||
];
|
||||
}
|
||||
}
|
||||
20
apps/Admin/Controllers/AdminRecipeListController.php
Normal 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";
|
||||
}
|
||||
44
apps/Admin/Controllers/AdminReviewControler.php
Normal 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']
|
||||
]
|
||||
);
|
||||
}
|
||||
17
apps/Admin/Controllers/AdminReviewListController.php
Normal 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";
|
||||
}
|
||||
27
apps/Admin/Controllers/IngredientController.php
Normal 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'
|
||||
]
|
||||
);
|
||||
}
|
||||
18
apps/Admin/Controllers/IngredientListController.php
Normal 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";
|
||||
}
|
||||
50
apps/Admin/Controllers/IngredientRecipeRelController.php
Normal 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()
|
||||
];
|
||||
}
|
||||
}
|
||||
28
apps/Admin/Controllers/IngredientRecipeRelListController.php
Normal 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
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,19 +1,56 @@
|
||||
<?php
|
||||
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>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?php echo get_title_website($title); ?></title>
|
||||
<link rel="shortcut icon" type="image/png" href="<?php echo ASSETS_PATH . '/favicon.png' ?>">
|
||||
<link rel="shortcut icon" type="image/x-icon" href="<?php echo ASSETS_PATH . '/images/favicon.ico' ?>">
|
||||
|
||||
<!-- Google fonts -->
|
||||
<!-- Google fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Play:wght@400;700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
|
||||
<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 -->
|
||||
@ -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/admin.css' ?>">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="header-admin">
|
||||
<div class="logo">
|
||||
<div class="logo-admin">
|
||||
FridgeBites Admin
|
||||
</div>
|
||||
<div class="header-admin__control">
|
||||
@ -36,32 +74,27 @@ use Lycoreco\Includes\Routing\Router;
|
||||
Hello, <span><?php echo CURRENT_USER->field_username ?></span>
|
||||
</div>
|
||||
<div class="links">
|
||||
<a href="<?php the_permalink('index:home') ?>">View site</a>
|
||||
<a href="<?php the_permalink('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>
|
||||
</header>
|
||||
|
||||
<div class="wrapper-admin">
|
||||
<aside class="admin-sidebar">
|
||||
<a href="<?php the_permalink('admin:product-new') ?>" class="btn btn-primary"><i class="fa-solid fa-plus"></i> New Product</a>
|
||||
<a href="<?php the_permalink('admin:recipe-new') ?>" class="btn btn-primary"><i
|
||||
class="fa-solid fa-plus"></i> New recipe</a>
|
||||
<hr>
|
||||
<ul class="admin-sidebar__list">
|
||||
<?php foreach ($sidebar_links as $link): ?>
|
||||
<li>
|
||||
<a class="<?php echo Router::$current_router_name == 'admin:home' ? "active" : "" ?>" href="<?php the_permalink('admin:home') ?>">
|
||||
<i class="fa-solid fa-house"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="<?php echo Router::$current_router_name == 'admin:user-list' ? "active" : "" ?>" href="<?php the_permalink('admin:user-list') ?>">
|
||||
<i class="fa-solid fa-users"></i> Users
|
||||
<a class="<?php echo Router::$current_router_name == $link['router_name'] ? "active" : "" ?>"
|
||||
href="<?php the_permalink($link['router_name']) ?>">
|
||||
<i class="<?= $link['icon'] ?>"></i> <?= $link['name'] ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</aside>
|
||||
<div class="wrapper-admin__content">
|
||||
|
||||
|
||||
|
||||
|
||||
@ -2,36 +2,36 @@
|
||||
|
||||
<div class="admin-container">
|
||||
<section>
|
||||
<h2>Stats per month</h2>
|
||||
<h2 class="title">Stats per month</h2>
|
||||
|
||||
<div id="dashboard-stats">
|
||||
<div class="dashboard-stats__item top-sales">
|
||||
<div class="icon">
|
||||
<i class="fa-solid fa-bag-shopping"></i>
|
||||
<i class="fa-solid fa-bowl-food"></i>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="label">Total sales</div>
|
||||
<div class="value">0$</div>
|
||||
<div class="label">New recipes</div>
|
||||
<div class="value"><?= $context['recipes_count'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-stats__item profit">
|
||||
<div class="icon">
|
||||
<i class="fa-solid fa-money-bill-trend-up"></i>
|
||||
<i class="fa-solid fa-message"></i>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="label">Profit</div>
|
||||
<div class="value">0$</div>
|
||||
<div class="label">New reviews</div>
|
||||
<div class="value"><?= $context['reviews_count'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-stats__item orders">
|
||||
<div class="icon">
|
||||
<i class="fa-solid fa-cart-shopping"></i>
|
||||
<i class="fa-solid fa-user-slash"></i>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="label">Orders</div>
|
||||
<div class="value">0</div>
|
||||
<div class="label">Total bans</div>
|
||||
<div class="value"><?= $context['ban_count'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -41,49 +41,47 @@
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="label">New users</div>
|
||||
<div class="value">0</div>
|
||||
<div class="value"><?= $context['user_count'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Quick tools</h2>
|
||||
<h2 class="title">Quick tools</h2>
|
||||
|
||||
<div id="quicktools">
|
||||
<a href="<?php the_permalink('admin:product-new') ?>" class="btn">
|
||||
<i class="fa-solid fa-plus"></i> New Product
|
||||
<a href="<?php the_permalink('admin:recipe-new') ?>" class="btn btn-secondary hover-anim">
|
||||
<i class="fa-solid fa-plus"></i> New recipe
|
||||
</a>
|
||||
<form action="<?php the_permalink('admin:product-list') ?>" method="get">
|
||||
<div class="input">
|
||||
<input type="text" name="s" placeholder="Search for products">
|
||||
<button type="submit"><i class="fa-solid fa-magnifying-glass"></i></button>
|
||||
<form action="<?php the_permalink('admin:recipe-list') ?>" method="get">
|
||||
<div class="input-admin">
|
||||
<input type="text" name="s" placeholder="Search for recipes">
|
||||
<button class="btn btn-secondary hover-anim" type="submit"><i class="fa-solid fa-magnifying-glass"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<h2>Latest orders</h2>
|
||||
<h2 class="title">Latest resipes</h2>
|
||||
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order number</th>
|
||||
<th>Method</th>
|
||||
<th>Total price</th>
|
||||
<th>Buyer</th>
|
||||
<th>Created at</th>
|
||||
<th>Title</th>
|
||||
<th>Price</th>
|
||||
<th>Status</th>
|
||||
<th>Created At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($context['last_orders'] as $order): ?>
|
||||
<?php foreach ($context['latest_recipes'] as $recipe): ?>
|
||||
<tr>
|
||||
<td><a href="<?php the_permalink('admin:order', [$order->get_id()]) ?>"><?php echo $order->field_order_number ?></a></td>
|
||||
<td><?php echo $order->field_method ?></td>
|
||||
<td><?php echo $order->get_total_price() ?></td>
|
||||
<td><?php echo $order->get_buyer_username() ?></td>
|
||||
<td><?php echo $order->field_created_at ?></td>
|
||||
<td><a href="<?php the_permalink('admin:recipe', [$recipe->get_id()]) ?>"><?= $recipe->field_title ?></a></td>
|
||||
<td><?= $recipe->get_price() ?></td>
|
||||
<td><?= $recipe->get_status() ?></td>
|
||||
<td><?= $recipe->field_created_at ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
||||
@ -1,21 +1,21 @@
|
||||
<?php the_admin_header(ucfirst($context['verbose_name_multiply'])) ?>
|
||||
|
||||
<div class="admin-container">
|
||||
<h1 class="p-title"><?php echo ucfirst($context['verbose_name_multiply']) ?></h1>
|
||||
<h1 class="title"><?php echo ucfirst($context['verbose_name_multiply']) ?></h1>
|
||||
|
||||
<section>
|
||||
<div id="quicktools">
|
||||
<?php if(isset($context['create_router_name'])): ?>
|
||||
<a href="<?php the_permalink($context['create_router_name']) ?>" class="btn">
|
||||
<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']) ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span></span>
|
||||
<?php endif; ?>
|
||||
<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'] : '' ?>">
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -5,7 +5,7 @@ $disabled_attr = $context['can_save'] ? '' : 'disabled';
|
||||
?>
|
||||
|
||||
<div class="admin-container">
|
||||
<h1 class="p-title">
|
||||
<h1 class="title">
|
||||
<?php if($context['object']->is_saved()): ?>
|
||||
<?php the_safe($context['edit_title']) ?>
|
||||
<?php else: ?>
|
||||
@ -112,12 +112,12 @@ $disabled_attr = $context['can_save'] ? '' : 'disabled';
|
||||
<div class="admin-block__content">
|
||||
<div class="btn-control">
|
||||
<?php if($context['object']->is_saved()): ?>
|
||||
<a href="<?php the_permalink('admin:delete', [str_replace('_', '-', $context['object']->get_table_name()), $context['object']->get_id()]) ?>" class="btn btn-danger" type="submit">Delete</a>
|
||||
<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: ?>
|
||||
<span></span>
|
||||
<?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>
|
||||
|
||||
22
apps/Admin/Templates/widgets/ingredients_in_recipe.php
Normal 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>
|
||||
@ -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>
|
||||
6
apps/Admin/Templates/widgets/recipe_author.php
Normal 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>
|
||||
@ -19,8 +19,8 @@
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<div class="btn-control">
|
||||
<a href="<?php the_permalink('admin:banlist', [$user->get_id()]) ?>" class="btn">Show all</a>
|
||||
<a href="<?php the_permalink('admin:ban-new', [$user->get_id()]) ?>" class="btn btn-primary">New ban</a>
|
||||
<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 hover-anim">New ban</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -4,6 +4,11 @@ use Lycoreco\Apps\Users\Models\{
|
||||
UserModel,
|
||||
BanlistModel
|
||||
};
|
||||
use Lycoreco\Apps\Recipes\Models\{
|
||||
RecipeModel,
|
||||
IngredientModel,
|
||||
IngredientInRecipeModel
|
||||
};
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
@ -9,6 +9,10 @@ $admin_urls = [
|
||||
|
||||
// Lists
|
||||
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 ///////
|
||||
// User
|
||||
@ -20,6 +24,26 @@ $admin_urls = [
|
||||
new Path('/admin/user/[:int]/ban/new', new Controllers\AdminBanController(true), 'ban-new'),
|
||||
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
|
||||
new Path('/admin/[:string]/[:int]/delete', new Controllers\AdminDeleteController(), 'delete')
|
||||
];
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace Lycoreco\Apps\Ajax\Controllers;
|
||||
|
||||
use Lycoreco\Includes\BaseController;
|
||||
use Lycoreco\Includes\Model\ValidationError;
|
||||
|
||||
class AjaxController extends BaseController
|
||||
{
|
||||
@ -15,10 +16,7 @@ class AjaxController extends BaseController
|
||||
require_once APPS_PATH . '/Ajax/ajax-actions.php';
|
||||
$context['result'] = "";
|
||||
|
||||
$json = file_get_contents('php://input');
|
||||
$data = json_decode($json, true);
|
||||
|
||||
$action = $data['action'] ?? false;
|
||||
$action = $_POST['action'] ?? false;
|
||||
|
||||
// If request from other site
|
||||
if (!in_array($_SERVER['HTTP_HOST'], ALLOWED_HOSTS)) {
|
||||
@ -34,9 +32,16 @@ class AjaxController extends BaseController
|
||||
$action = "ajax_" . $action;
|
||||
|
||||
try {
|
||||
$context['result'] = $action($data['args']);
|
||||
} catch (\Exception $ex) {
|
||||
$context['result'] = $action();
|
||||
}
|
||||
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());
|
||||
return $context;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
<?php
|
||||
echo $context['result'];
|
||||
echo json_encode($context['result'], JSON_PRETTY_PRINT);
|
||||
@ -1,5 +1,12 @@
|
||||
<?php
|
||||
|
||||
use Lycoreco\Apps\Recipes\Models\{
|
||||
IngredientModel,
|
||||
RecipeModel,
|
||||
RecipeUserMenu,
|
||||
FavoriteModel
|
||||
};
|
||||
|
||||
function get_ajax_error($message, $error_code = 500)
|
||||
{
|
||||
http_response_code($error_code);
|
||||
@ -7,41 +14,186 @@ function get_ajax_error($message, $error_code = 500)
|
||||
$error = array();
|
||||
$error['error'] = $message;
|
||||
|
||||
return json_encode($error, JSON_PRETTY_PRINT);
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajax actions
|
||||
*/
|
||||
function ajax_search($args) {
|
||||
$search_query = $args['query'];
|
||||
|
||||
$result = [];
|
||||
$data = [
|
||||
[
|
||||
'id' => 2,
|
||||
'name' => 'Genshin Impact'
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'name' => 'Zenless zone zero'
|
||||
],
|
||||
[
|
||||
'id' => 4,
|
||||
'name' => 'Honkai Star Rail'
|
||||
],
|
||||
[
|
||||
'id' => 5,
|
||||
'name' => 'Honkai Impact'
|
||||
],
|
||||
];
|
||||
$result['results'] = [];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if(str_contains($value['name'], $search_query))
|
||||
$result['results'][] = $value;
|
||||
function ajax_search()
|
||||
{
|
||||
$search_query = $_POST['query'] ?? null;
|
||||
if (!isset($search_query)) {
|
||||
return get_ajax_error("Missing 'query' parameter.", 400);
|
||||
}
|
||||
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;
|
||||
}
|
||||
@ -2,9 +2,51 @@
|
||||
|
||||
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;
|
||||
|
||||
require_once(INCLUDES_PATH . '/Const/recipes.php');
|
||||
|
||||
class HomepageController extends BaseController
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Lycoreco\Includes\Routing\HttpExceptions\PageError;
|
||||
|
||||
$error = $context['error_model'];
|
||||
|
||||
the_header(
|
||||
@ -7,17 +10,19 @@ the_header(
|
||||
'error',
|
||||
[
|
||||
['robots', 'nofollow, noindex']
|
||||
]);
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
/**
|
||||
* @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-message"><?php echo $error->getMessage() ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php the_footer() ?>
|
||||
@ -1,4 +1,6 @@
|
||||
<?php the_header(
|
||||
<?php
|
||||
require_once APPS_PATH . '/Recipes/components.php';
|
||||
the_header(
|
||||
'Welcome',
|
||||
"Discover delicious recipes using the ingredients you already have. Fridgebites helps you create tasty meals with what's in your fridge",
|
||||
'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="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">
|
||||
<h2 class="title">Latest Recipes Added</h2>
|
||||
<!-- Slider main container -->
|
||||
@ -22,42 +34,21 @@
|
||||
<!-- Additional required wrapper -->
|
||||
<div class="swiper-wrapper">
|
||||
<!-- Slides -->
|
||||
|
||||
<?php foreach($context['latest_recipes'] as $recipe): ?>
|
||||
<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">
|
||||
<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 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>
|
||||
</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>
|
||||
<p class="recipe-info__title"><?= $recipe->field_title ?></p>
|
||||
<p class="recipe-info__meta"><i class="fa-solid fa-user"></i> <?= $recipe->author_username ?></p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</div>
|
||||
<!-- If we need pagination -->
|
||||
@ -76,87 +67,14 @@
|
||||
|
||||
<div class="swiper categories-swiper">
|
||||
<div class="swiper-wrapper">
|
||||
<?php foreach($context['categories'] as $cat): ?>
|
||||
<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>
|
||||
</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 href="<?= get_permalink('recipes:catalog') . '?category=' . $cat->get_id() ?>" class="category hover-anim">
|
||||
<p class="category-title"><?= $cat->field_name ?></p>
|
||||
|
||||
</a>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="swiper-button-prev"></div>
|
||||
@ -168,240 +86,58 @@
|
||||
<div class="recent-reviews">
|
||||
<h2 class="title">Recent User Reviews</h2>
|
||||
<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-img">
|
||||
<img src="media/recipe1.jpeg" alt="reviewed recipe">
|
||||
<img src="<?= $review->get_recipe_image() ?>" alt="reviewed recipe">
|
||||
</div>
|
||||
<h3 class="review-title-text">Just Like Mom's</h3>
|
||||
<h3 class="review-title-text"><?= $review->recipe_title ?></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>
|
||||
<p><?= $review->get_excerpt() ?></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>
|
||||
</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>
|
||||
<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> <?= $review->get_date() ?></p>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if(CURRENT_USER): ?>
|
||||
<?php if($context['usermenu_recipe_prefetch']): ?>
|
||||
<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">
|
||||
<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>
|
||||
<?php
|
||||
foreach ($context['usermenu_recipe_prefetch'] as $recipe_pref) {
|
||||
the_product_recipes_item($recipe_pref['origin'], $recipe_pref['relations']);
|
||||
}
|
||||
?>
|
||||
</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>
|
||||
<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>
|
||||
<?php else: ?>
|
||||
<div class="daily-meals">
|
||||
<h2 class="title">No meals added for <?= date("l"); ?></h2>
|
||||
<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>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
</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(
|
||||
ASSETS_PATH . '/swiper/swiper-bundle.min.js',
|
||||
ASSETS_PATH . '/js/index.js',
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
<?php
|
||||
namespace Lycoreco\Apps\Receipts\Models;
|
||||
|
||||
use Lycoreco\Includes\Model\BaseModel;
|
||||
|
||||
class CategoryModel extends BaseModel
|
||||
{
|
||||
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
<?php
|
||||
namespace Lycoreco\Apps\Receipts\Models;
|
||||
|
||||
use Lycoreco\Includes\Model\BaseModel;
|
||||
|
||||
class IngredientInReceiptModel extends BaseModel
|
||||
{
|
||||
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
<?php
|
||||
namespace Lycoreco\Apps\Receipts\Models;
|
||||
|
||||
use Lycoreco\Includes\Model\BaseModel;
|
||||
|
||||
class IngredientModel extends BaseModel
|
||||
{
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Lycoreco\Includes\Routing\Path;
|
||||
63
apps/Recipes/Controllers/CatalogController.php
Normal 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;
|
||||
}
|
||||
}
|
||||
27
apps/Recipes/Controllers/DailyMealsController.php
Normal 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;
|
||||
}
|
||||
}
|
||||
46
apps/Recipes/Controllers/ExportPdfController.php
Normal 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;
|
||||
}
|
||||
}
|
||||
50
apps/Recipes/Controllers/FavoritesController.php
Normal 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;
|
||||
}
|
||||
}
|
||||
133
apps/Recipes/Controllers/SingleRecipeController.php
Normal 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;
|
||||
}
|
||||
}
|
||||
80
apps/Recipes/Controllers/SingleSubmitController.php
Normal 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;
|
||||
}
|
||||
}
|
||||
35
apps/Recipes/Models/CategoryModel.php
Normal 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;
|
||||
}
|
||||
}
|
||||
47
apps/Recipes/Models/FavoriteModel.php
Normal 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;
|
||||
}
|
||||
}
|
||||
55
apps/Recipes/Models/IngredientInRecipeModel.php
Normal 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;
|
||||
}
|
||||
}
|
||||
38
apps/Recipes/Models/IngredientModel.php
Normal 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;
|
||||
}
|
||||
}
|
||||
158
apps/Recipes/Models/RecipeModel.php
Normal 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;
|
||||
}
|
||||
}
|
||||
93
apps/Recipes/Models/RecipeUserMenu.php
Normal 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;
|
||||
}
|
||||
}
|
||||
121
apps/Recipes/Models/ReviewsModel.php
Normal 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;
|
||||
}
|
||||
}
|
||||
97
apps/Recipes/Templates/catalog.php
Normal 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(); ?>
|
||||
15
apps/Recipes/Templates/components/catalog-item.php
Normal 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>
|
||||
18
apps/Recipes/Templates/components/recipe-ings-item.php
Normal 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>
|
||||
43
apps/Recipes/Templates/daily-meals.php
Normal 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(); ?>
|
||||
1
apps/Recipes/Templates/export-pdf.php
Normal file
@ -0,0 +1 @@
|
||||
<?php $context['fpdf']->Output('I', 'recipe.pdf') ?>
|
||||
36
apps/Recipes/Templates/favorites.php
Normal 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();
|
||||
?>
|
||||
122
apps/Recipes/Templates/single-submit.php
Normal 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',
|
||||
)); ?>
|
||||
194
apps/Recipes/Templates/single.php
Normal 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> <?= $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',
|
||||
)); ?>
|
||||
111
apps/Recipes/Utils/RecipePDF.php
Normal 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();
|
||||
}
|
||||
}
|
||||
12
apps/Recipes/components.php
Normal 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
@ -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'),
|
||||
];
|
||||
@ -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;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
font-family: var(--common-font);
|
||||
font-size: 14px;
|
||||
color: var(--common-text);
|
||||
}
|
||||
|
||||
.wrapper-admin {
|
||||
display: flex;
|
||||
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
.wrapper-admin__content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-admin {
|
||||
background: var(--dark-block-background);
|
||||
height: 64px;
|
||||
|
||||
height: 80px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -22,99 +48,127 @@ body {
|
||||
|
||||
padding: 15px 10px;
|
||||
}
|
||||
|
||||
.header-admin__control {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.header-admin__control .username {
|
||||
color: var(--h-color);
|
||||
font-family: var(--font-family-header);
|
||||
color: var(--common-text);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
font-family: var(--common-font);
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.header-admin__control .username span {
|
||||
color: var(--link-color);
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
.header-admin__control .links a {
|
||||
color: var(--h-color);
|
||||
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
font-family: var(--common-font);
|
||||
color: var(--common-text);
|
||||
}
|
||||
|
||||
.admin-sidebar {
|
||||
width: 100%;
|
||||
max-width: 314px;
|
||||
flex-shrink: 0;
|
||||
background: var(--block-background);
|
||||
background: var(--panel-background);
|
||||
}
|
||||
|
||||
.admin-sidebar .btn {
|
||||
display: block;
|
||||
margin: 20px 20px 20px 20px;
|
||||
}
|
||||
|
||||
.admin-sidebar__list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.admin-sidebar__list a {
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
color: var(--h-color);
|
||||
color: var(--panel-text);
|
||||
font-size: 20px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.admin-sidebar__list a:hover,
|
||||
.admin-sidebar__list a.active {
|
||||
background: #ffffff2e;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.admin-sidebar__list a i {
|
||||
width: 34px;
|
||||
}
|
||||
|
||||
.admin-sidebar__list a span {
|
||||
background: #f00;
|
||||
padding: 2px 7px;
|
||||
font-size: 15px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.admin-container {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 1100px;
|
||||
padding: 22px 30px;
|
||||
}
|
||||
|
||||
.admin-container h2 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.admin-container section {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
#dashboard-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.dashboard-stats__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
color: var(--h-color);
|
||||
background: var(--block-background);
|
||||
color: var(--common-text);
|
||||
background: var(--panel-background);
|
||||
border-radius: 5px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.dashboard-stats__item .icon {
|
||||
font-size: 36px;
|
||||
margin-right: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dashboard-stats__item .value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dashboard-stats__item.top-sales .icon {
|
||||
color: #FF7B00;
|
||||
}
|
||||
|
||||
.dashboard-stats__item.new-users .icon {
|
||||
color: #00A3E8;
|
||||
}
|
||||
|
||||
.dashboard-stats__item.orders .icon {
|
||||
color: #BA00E8;
|
||||
}
|
||||
|
||||
.dashboard-stats__item.profit .icon {
|
||||
color: #00E842;
|
||||
}
|
||||
@ -125,11 +179,12 @@ body {
|
||||
justify-content: space-between;
|
||||
|
||||
padding: 14px 22px;
|
||||
background: var(--block-background);
|
||||
border-radius: 5px;
|
||||
background: var(--panel-background);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.admin-table {
|
||||
color: var(--h-color);
|
||||
color: var(--common-text);
|
||||
|
||||
margin-bottom: 15px;
|
||||
font-size: 14px;
|
||||
@ -138,67 +193,93 @@ body {
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-table thead {
|
||||
background: var(--block-background);
|
||||
background: #0DBB99;
|
||||
color: #eaf8eb;
|
||||
}
|
||||
|
||||
.admin-table td,
|
||||
.admin-table th {
|
||||
padding: 15px 10px;
|
||||
}
|
||||
|
||||
.admin-table a {
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
.admin-block__content .admin-block__table {
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.admin-table tbody tr:nth-child(odd),
|
||||
.admin-block__table .row:nth-child(odd) {
|
||||
background: #9f9f9f;
|
||||
background: #eaf8eb;
|
||||
}
|
||||
|
||||
.admin-table tbody tr:nth-child(even),
|
||||
.admin-block__table .row:nth-child(even) {
|
||||
background: #767676;
|
||||
background: #b6f1ba;
|
||||
}
|
||||
|
||||
.admin-block__table {
|
||||
color: var(--h-color);
|
||||
color: var(--common-text);
|
||||
}
|
||||
|
||||
.admin-block__table .row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.admin-single {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 320px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.admin-block {
|
||||
overflow: hidden;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--block-background);
|
||||
border: 1px solid #015847;
|
||||
margin-bottom: 20px;
|
||||
background: #f1fff2
|
||||
}
|
||||
|
||||
.admin-block__content {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.admin-block__title {
|
||||
font-size: 18px;
|
||||
font-family: var(--font-family-header);
|
||||
background: var(--block-background);
|
||||
font-family: var(--common-font);
|
||||
background-color: #015847;
|
||||
padding: 10px 10px;
|
||||
color: var(--h-color);
|
||||
color: #eaf8eb;
|
||||
}
|
||||
|
||||
.admin-block__table .row {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.admin-block__table a {
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
.admin-container.delete {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.admin-container.delete .meta-text {
|
||||
font-size: 18px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.admin-container.delete .btn-control {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.order-stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -206,14 +287,90 @@ body {
|
||||
font-size: 18px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.order-content .admin-block__table {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.order-stat span:first-child {
|
||||
color: var(--text-color);
|
||||
color: var(--common-text);
|
||||
font-weight: 400;
|
||||
font-family: var(--font-family-header);
|
||||
font-family: var(--common-font);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
23
assets/css/single-submit.css
Normal 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;
|
||||
}
|
||||
1079
assets/css/style.css
BIN
assets/food_3d.glb
Normal file
@ -8,7 +8,7 @@
|
||||
* @param {Function} onError
|
||||
*/
|
||||
function sendAjax(action,
|
||||
args = [ ],
|
||||
args = [],
|
||||
onLoad = () => { },
|
||||
onSuccess = (data) => { },
|
||||
onError = (error) => { }) {
|
||||
@ -43,6 +43,87 @@ function showToastify(message, type = "info") {
|
||||
gravity: "bottom",
|
||||
position: "left",
|
||||
className: type,
|
||||
duration: 3000 })
|
||||
duration: 3000
|
||||
})
|
||||
.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
@ -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
@ -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');
|
||||
});
|
||||
68
assets/js/threejs-scene.js
Normal 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
@ -20,7 +20,7 @@
|
||||
|
||||
<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 . '/qrcode/qrcode.min.js' ?>"></script>
|
||||
<?php foreach($scripts as $script): ?>
|
||||
<script src="<?php echo $script ?>"></script>
|
||||
<?php endforeach; ?>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@ -11,14 +12,16 @@
|
||||
<!-- Meta tags -->
|
||||
<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] ?>">
|
||||
<?php endforeach; ?>
|
||||
|
||||
<!-- Google fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=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/ -->
|
||||
|
||||
<!-- 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/style.css' ?>">
|
||||
</head>
|
||||
|
||||
<body class="<?php echo $body_class ?>">
|
||||
<header class="header">
|
||||
<div class="container">
|
||||
<div class="header-inner">
|
||||
<div class="logo">
|
||||
<a href="<?php the_permalink("index:home")?>">
|
||||
<img src="<?php echo ASSETS_PATH . '/images/fridgeLogo.png'?>" alt="fridgeBitesLogo" class="logo-img">
|
||||
<a href="<?php the_permalink("index:home") ?>">
|
||||
<img src="<?php echo ASSETS_PATH . '/images/fridgeLogo.png' ?>" alt="fridgeBitesLogo"
|
||||
class="logo-img">
|
||||
</a>
|
||||
</div>
|
||||
<nav class="nav">
|
||||
<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="#" class="nav-link">RECIPES</a></li>
|
||||
<li class="nav-item"><a href="#" class="nav-link">FAVORITES</a></li>
|
||||
<li class="nav-item"><a href="#" class="nav-link">MEAL A DAY</a></li>
|
||||
<li class="nav-item"><a href="#" class="nav-link">SUBMIT RECIPE</a></li>
|
||||
<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("recipes:catalog") ?>"
|
||||
class="nav-link">RECIPES</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>
|
||||
</nav>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</header>
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"phpmailer/phpmailer": "^6.10"
|
||||
"phpmailer/phpmailer": "^6.10",
|
||||
"setasign/fpdf": "^1.8"
|
||||
}
|
||||
}
|
||||
|
||||
48
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "a475eab76e37559e0cc7396ccf30c49e",
|
||||
"content-hash": "a6db4a4cd2450cf3fbd46a573661b495",
|
||||
"packages": [
|
||||
{
|
||||
"name": "phpmailer/phpmailer",
|
||||
@ -86,6 +86,52 @@
|
||||
}
|
||||
],
|
||||
"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": [],
|
||||
|
||||
BIN
fridgebitesDocs2.pdf
Normal file
BIN
fridgebitesDocs3
Normal file
@ -133,13 +133,13 @@ function the_pagination(int $count, int $elem_per_page, int $current_page)
|
||||
continue;
|
||||
$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; ?>
|
||||
|
||||
<!-- Current page -->
|
||||
<?php if ($current_page > 0 && $current_page <= $total_pages): ?>
|
||||
<li>
|
||||
<div class="btn active page-btn"><?php echo $current_page ?></div>
|
||||
<div class="btn btn-primary"><?php echo $current_page ?></div>
|
||||
</li>
|
||||
<?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++):
|
||||
$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; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
@ -252,4 +252,54 @@ function send_email(string $subject, string $body, string $altBody, string $to_a
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
5
includes/Const/recipes.php
Normal 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'
|
||||
));
|
||||
@ -344,7 +344,7 @@ abstract class BaseModel
|
||||
else
|
||||
return false;
|
||||
}
|
||||
static function count($fields, $search, $additional_fields = array())
|
||||
static function count($fields, $search = '', $additional_fields = array())
|
||||
{
|
||||
$filter_result = static::filter(
|
||||
$fields,
|
||||
@ -364,6 +364,9 @@ abstract class BaseModel
|
||||
else
|
||||
return $filter_result[0]->func_total_count;
|
||||
}
|
||||
public function getAssocArr() {
|
||||
return get_object_vars($this);
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
@ -436,7 +439,7 @@ abstract class BaseModel
|
||||
/**
|
||||
* Return model from Mysql result
|
||||
* @param array $pdo_result pdo resut FETCH_MODE = FETCH_ASSOC
|
||||
* @return array
|
||||
* @return array(self)
|
||||
*/
|
||||
protected static function createObjectsFromQuery(array $pdo_result)
|
||||
{
|
||||
|
||||
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 114 KiB |
BIN
media/recipes/caesar_686a6c042a944.jpg
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
media/recipes/chana_685f868b8fb94.jpg
Normal file
|
After Width: | Height: | Size: 385 KiB |
BIN
media/recipes/garlic_686a6a714284a.jpg
Normal file
|
After Width: | Height: | Size: 305 KiB |
BIN
media/recipes/images_68626081274a5.jpeg
Normal file
|
After Width: | Height: | Size: 11 KiB |
5
urls.php
@ -1,15 +1,18 @@
|
||||
<?php
|
||||
use Lycoreco\Apps\Index\Controllers\ErrorController;
|
||||
use Lycoreco\Includes\Routing\Router;
|
||||
|
||||
require APPS_PATH . '/Index/urls.php';
|
||||
require APPS_PATH . '/Users/urls.php';
|
||||
require APPS_PATH . '/Admin/urls.php';
|
||||
require APPS_PATH . '/Ajax/urls.php';
|
||||
require APPS_PATH . '/Recipes/urls.php';
|
||||
|
||||
Router::includes($index_urls, "index");
|
||||
Router::includes($users_urls, 'users');
|
||||
Router::includes($admin_urls, 'admin');
|
||||
Router::includes($ajax_urls, 'ajax');
|
||||
Router::includes($recipes_urls, 'recipes');
|
||||
|
||||
// Router::set_error_controller('default', new ErrorController())
|
||||
Router::set_error_controller('default', new ErrorController());
|
||||
?>
|
||||