Compare commits

..

118 Commits

Author SHA1 Message Date
4a0b0fe804 Merge pull request 'Completed web programming project' (#44) from develop into master
Reviewed-on: #44
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
2025-07-07 19:00:17 +02:00
9a28d6b322 fixed reviews adaptive 2025-07-07 13:01:32 +02:00
8356c80ca3 Merge branch 'develop' of https://gitea.steve-dekart.xyz/web_programming/fridge_bites into develop 2025-07-07 12:58:17 +02:00
03fa67da84 Minor changer 2025-07-07 12:58:06 +02:00
7c69e94afa TIST-44: added documentation 2025-07-07 09:36:51 +02:00
3e0f72c4f6 fixed keywords on single item 2025-07-06 15:14:08 +02:00
2860cd182f Merge branch 'develop' of https://gitea.steve-dekart.xyz/web_programming/fridge_bites into develop 2025-07-06 14:43:57 +02:00
552970f8b0 updated 2025-07-06 14:43:45 +02:00
59f5e7b6b0 Merge pull request 'TIST-50: fixed adaptive for submit recipe page' (#43) from TIST-50 into develop
Reviewed-on: #43
2025-07-06 14:40:30 +02:00
8e4996651e TIST-50: fixed adaptive for submit recipe page 2025-07-06 14:39:06 +02:00
7012c9aa85 Merge pull request 'TIST-16: Add new recipes by user' (#42) from TIST-16 into develop
Reviewed-on: #42
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
2025-07-06 14:32:59 +02:00
e2cc848fec Merge branch 'develop' of https://gitea.steve-dekart.xyz/web_programming/fridge_bites into TIST-16 2025-07-06 14:31:28 +02:00
b21465e89f Merge pull request 'TIST-49: fixed display issues of warning and success alerts' (#41) from TIST-49 into develop
Reviewed-on: #41
2025-07-06 14:31:23 +02:00
5cd2030848 Merge branch 'develop' of https://gitea.steve-dekart.xyz/web_programming/fridge_bites into TIST-16 2025-07-06 14:31:08 +02:00
6dc6cd98ea Added functional for submit recipe 2025-07-06 14:30:47 +02:00
db6f9538e1 TIST-49: fixed display issues of warning and success alerts 2025-07-06 13:57:56 +02:00
d0db460f28 Merge pull request 'TIST-45: added custom error page' (#40) from TIST-45 into develop
Reviewed-on: #40
2025-07-06 13:51:53 +02:00
df65db797b Merge pull request 'TIST-48: added reset button in catalog filters' (#39) from TIST-48 into develop
Reviewed-on: #39
2025-07-06 13:51:46 +02:00
c56bfdc80e Merge pull request 'TIST-47: fixed display issues with pagination' (#38) from TIST-47 into develop
Reviewed-on: #38
2025-07-06 13:51:40 +02:00
cbcb3f1c5a Merge pull request 'TIST-46: fixed display issues with recent reviews and daily recipes' (#37) from TIST-46 into develop
Reviewed-on: #37
2025-07-06 13:51:16 +02:00
924fb17927 TIST-45: added custom error page 2025-07-06 13:48:20 +02:00
ba7a1eac31 TIST-48: added reset button in catalog filters 2025-07-06 13:34:59 +02:00
97895c35d2 TIST-47: fixed display issues with pagination 2025-07-06 13:23:27 +02:00
ffaef7e153 Merge branch 'develop' of gitea.steve-dekart.xyz:web_programming/fridge_bites into TIST-46 2025-07-06 13:07:08 +02:00
931596354b TIST-46: fixed display issues with recent reviews and daily recipes 2025-07-06 13:06:57 +02:00
2a97a3b2c7 Merge pull request 'TIST-22: added submit recipe page and create ingredient modal' (#36) from TIST-22 into develop
Reviewed-on: #36
Reviewed-by: steve_dekart <stevedekart2020@gmail.com>
2025-07-06 12:57:33 +02:00
bb8cbbb274 added link to submit to header 2025-07-06 11:34:39 +02:00
36e7435bd4 Merge branch 'develop' of gitea.steve-dekart.xyz:web_programming/fridge_bites into TIST-22 2025-07-06 11:20:51 +02:00
26cf158ec5 TIST-22: added single recipe submit page and new ingredient modal 2025-07-06 11:20:45 +02:00
e2fe1a4173 Merge pull request 'TIST-32: Reviews model and publish as pending' (#35) from TIST-32 into develop
Reviewed-on: #35
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
2025-07-06 09:55:55 +02:00
9fc579fc56 Added review model and user can publish it 2025-07-06 09:53:02 +02:00
9f2d2a13d2 Merge pull request 'TIST-41: added reviews and reviews form to singe recipe page' (#32) from TIST-41 into develop
Reviewed-on: #32
Reviewed-by: steve_dekart <stevedekart2020@gmail.com>
2025-07-05 20:53:06 +02:00
1974d44a35 Merge branch 'develop' of gitea.steve-dekart.xyz:web_programming/fridge_bites into TIST-41 2025-07-05 20:52:46 +02:00
f336917b62 Merge pull request 'TIST-39: added ajax to daily meals select' (#33) from TIST-39 into develop
Reviewed-on: #33
Reviewed-by: steve_dekart <stevedekart2020@gmail.com>
2025-07-05 20:52:28 +02:00
4ccb8ac05e fixed display issue with select dropdown 2025-07-05 20:51:43 +02:00
a6a7fbade6 Merge branch 'develop' of gitea.steve-dekart.xyz:web_programming/fridge_bites into TIST-39 2025-07-05 20:36:37 +02:00
9a3d2da412 Merge pull request 'TIST-43: Fill index page, favorites and meal of day' (#34) from TIST-43 into develop
Reviewed-on: #34
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
2025-07-05 20:36:04 +02:00
81a88b93d5 Added in_usermenu field for RecipeModel 2025-07-05 20:35:34 +02:00
b854c92377 Filled index, meals and favorites page. Added pagination for recipes 2025-07-05 20:20:33 +02:00
3f67fe27d0 TIST-39: added ajax to daily meals select 2025-07-05 20:19:45 +02:00
6ef6930766 Merge branch 'develop' of gitea.steve-dekart.xyz:web_programming/fridge_bites into TIST-41 2025-07-04 21:33:08 +02:00
f1bebeee48 TIST-41: added reviews and reviews form to singe recipe page 2025-07-04 21:32:57 +02:00
9a66bb2b6f Merge pull request 'TIST-36: Prefetch related models for a list of model instances' (#31) from TIST-36 into develop
Reviewed-on: #31
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
2025-07-04 20:52:55 +02:00
69f47cdc42 Merge branch 'develop' of https://gitea.steve-dekart.xyz/web_programming/fridge_bites into TIST-36 2025-07-04 20:52:31 +02:00
ec6f7bcabd Merge pull request 'TIST-38: fixed width of single recipe info details' (#30) from TIST-38 into develop
Reviewed-on: #30
Reviewed-by: steve_dekart <stevedekart2020@gmail.com>
2025-07-04 20:52:18 +02:00
15f3b42d84 Added prefetch_related function to relate origin models with their relations 2025-07-04 20:49:37 +02:00
d846de0182 TIST-38: fixed width of single recipe info details 2025-07-04 19:54:48 +02:00
07c4c49cf0 Merge pull request 'TIST-40: added search functionality using fetch' (#28) from TIST-40 into develop
Reviewed-on: #28
Reviewed-by: steve_dekart <stevedekart2020@gmail.com>
2025-07-03 22:41:54 +02:00
caa48d3b62 added href to search results, changed to use global hidden atribute, added click outside of search to hide results 2025-07-03 22:40:09 +02:00
44863aecbd Merge branch 'develop' of gitea.steve-dekart.xyz:web_programming/fridge_bites into TIST-40 2025-07-03 22:30:18 +02:00
34df639446 Merge pull request 'TIST-42: Add id or url to item for search action on ajax' (#29) from TIST-42 into develop
Reviewed-on: #29
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
2025-07-03 22:27:51 +02:00
2651014595 Added id fields for search ingredients ajax action 2025-07-03 22:23:42 +02:00
572b7cf3f4 Added url, id, image_url fields for search ajax action 2025-07-03 22:21:24 +02:00
a6a759b8f9 TIST-40: added search functionality using fetch 2025-07-03 15:36:57 +02:00
80a23a5080 Merge pull request 'TIST-37: Send ajax request to add favorite' (#27) from TIST-37 into develop
Reviewed-on: #27
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
2025-07-02 22:35:06 +02:00
f0ed2f0712 added files 2025-07-02 22:33:45 +02:00
6f306beb4b Added favorite button functional 2025-07-02 22:32:15 +02:00
cc0125d997 Merge pull request 'TIST-35: Favorite property on ProductModel' (#26) from TIST-35 into develop
Reviewed-on: #26
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
2025-07-02 20:45:28 +02:00
c1dc0389e9 Merge branch 'develop' of https://gitea.steve-dekart.xyz/web_programming/fridge_bites into TIST-35 2025-07-02 20:45:01 +02:00
aa0e153f65 Merge pull request 'TIST-23: added static favorites page and controller' (#25) from TIST-23 into develop
Reviewed-on: #25
Reviewed-by: steve_dekart <stevedekart2020@gmail.com>
2025-07-02 20:44:44 +02:00
ae65ec769a Added new property for Recipe 2025-07-02 20:41:41 +02:00
36b0f58638 TIST-23: added static favorites page and controller 2025-07-01 15:35:50 +02:00
1f43f408cc Merge pull request 'TIST-34: Fix display issues on Admin page for select and catalog' (#24) from TIST-34 into develop
Reviewed-on: #24
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
2025-06-30 22:22:19 +02:00
d0ceea9a60 fixed minor issues 2025-06-30 22:19:15 +02:00
216a1f8b7a Merge pull request 'TIST-31: Favorites model and ajax endpoint' (#23) from TIST-31 into develop
Reviewed-on: #23
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
2025-06-30 22:14:10 +02:00
e5b2a7fd62 Merge branch 'develop' of https://gitea.steve-dekart.xyz/web_programming/fridge_bites into TIST-31 2025-06-30 22:13:14 +02:00
29d9f21d71 Merge pull request 'TIST-21: added static daily meals page' (#22) from TIST-21 into develop
Reviewed-on: #22
Reviewed-by: steve_dekart <stevedekart2020@gmail.com>
2025-06-30 22:12:39 +02:00
7dfdc37a90 removed unused 'use' from daily recipes controller 2025-06-30 22:09:56 +02:00
2caecfe4a4 Fixed eng mistakes 2 2025-06-30 21:57:10 +02:00
797d9fd334 Fixed eng mistakes 2025-06-30 21:56:20 +02:00
f66b0f6a1e Added FavoriteModel on db and created new endpoint for ajax 2025-06-30 21:51:00 +02:00
46adce3c77 added static daily meals page 2025-06-29 18:03:04 +02:00
3e9fdd36bb Merge pull request 'TIST-28: Create ajax endpoint to create new ingredient and search ingredients' (#20) from TIST-28 into develop
Reviewed-on: #20
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
2025-06-29 17:35:27 +02:00
f588e5f608 Merge pull request 'TIST-30: fixed catalog anchor textt decorations and removed display grid from filters, removed images from categories in index and updated design' (#21) from TIST-30 into develop
Reviewed-on: #21
Reviewed-by: steve_dekart <stevedekart2020@gmail.com>
2025-06-29 17:33:27 +02:00
fc357f4eb5 TIST-30: fixed catalog anchor textt decorations and removed display grid from filters, removed images from categories in index and updated design 2025-06-29 15:43:04 +02:00
95e41b813f Added ajax endpoints to search and create ingredient 2025-06-29 15:02:57 +02:00
c35cf57eab Merge pull request 'TIST-24: Fixed small issues on admin page' (#19) from TIST-24 into develop
Reviewed-on: #19
Reviewed-by: steve_dekart <stevedekart2020@gmail.com>
2025-06-29 13:43:48 +02:00
f556d9948c fixed more small issues on admin page 2025-06-29 13:23:34 +02:00
David Katrinka
184d7f43cb TIST-20: Fixed small issues on admin page 2025-06-29 12:27:12 +02:00
420d0bfc6c Merge pull request 'TIST-17: added single recipe page' (#15) from TIST-17 into develop
Reviewed-on: #15
2025-06-29 12:25:44 +02:00
David Katrinka
76d4011c41 fixed all issues other than modal 2025-06-29 12:11:07 +02:00
David Katrinka
c58a94d23c TIST-17: added single recipe page 2025-06-29 12:11:07 +02:00
703a327f39 Merge pull request 'TIST-29: ThreeJS' (#18) from TIST-29 into develop
Reviewed-on: #18
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
2025-06-29 11:58:00 +02:00
3959a8c55a Added 3d model and customized first block on index page 2025-06-29 11:39:17 +02:00
b6cc9e7a02 Merge pull request 'TIST-27: UserMenu model and ajax endpoint to add' (#17) from TIST-27 into develop
Reviewed-on: #17
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
2025-06-29 10:03:39 +02:00
4b2cac054f Fixed const name 2025-06-29 00:53:28 +02:00
c3967b9abc Added UserMenu ajax endpoint 2025-06-29 00:51:43 +02:00
bb00e19385 Updated README 2025-06-28 11:47:16 +02:00
dba1d5c8ae Merge pull request 'TIST-26: Added export recipe to pdf endpoint' (#14) from TIST-26 into develop
Reviewed-on: #14
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
2025-06-28 11:28:27 +02:00
6259cc3688 Added export recipe to pdf endpoint 2025-06-28 11:25:24 +02:00
a2c2b31597 Merge pull request 'TIST-25: filter form on catalog page' (#13) from TIST-25 into develop
Reviewed-on: #13
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
2025-06-28 08:47:23 +02:00
783e951570 Merge pull request 'TIST-12: Create endpoint for search in ajax' (#12) from TIST-12 into develop
Reviewed-on: #12
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
2025-06-28 08:43:57 +02:00
d78de57b84 Merge pull request 'TIST-15: Add additional information for dashboard' (#11) from TIST-15 into develop
Reviewed-on: #11
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
2025-06-28 08:43:47 +02:00
51899db9fb Added filter on the page 2025-06-28 08:41:09 +02:00
c374c50ca0 Added ajax search and fixed body type for request on ajax 2025-06-28 00:39:25 +02:00
b66f2c6709 Added information on dashboard 2025-06-28 00:10:15 +02:00
b6970ebef7 Fixed path issue during upload image by model 2025-06-27 22:44:05 +02:00
318a40c222 Merge pull request 'TIST-11: Create filter with ManyToMany relation between Recipe and Ingredients' (#10) from TIST-11 into develop
Reviewed-on: #10
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
2025-06-22 20:40:14 +02:00
0a7ce228a2 Create filter with ManyToMany relation between Recipe and Ingredients 2025-06-21 23:51:26 +02:00
5b1d4e60da Merge pull request 'TIST-20: added recipes page, addded recipes page to header links, added display: grid to daily meals' (#9) from TIST-20 into develop
Reviewed-on: #9
Reviewed-by: steve_dekart <stevedekart2020@gmail.com>
2025-06-19 18:18:43 +02:00
David Katrinka
3ad1eada3d TIST-20: added recipes page, addded recipes page to header links, added display: grid to daily meals 2025-06-18 19:36:35 +02:00
32c0529a4e Merge pull request 'TIST-14: admin panel frontend' (#7) from TIST-14 into develop
Reviewed-on: #7
Reviewed-by: steve_dekart <stevedekart2020@gmail.com>
2025-06-17 18:58:08 +02:00
David Katrinka
871f656007 fixed admin widgets css 2025-06-17 17:31:23 +02:00
58bd2bee3c Merge pull request 'Create pages for recipe on Admin Panel' (#8) from TIST-19 into develop
Reviewed-on: #8
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
2025-06-16 21:59:12 +02:00
abbf111b4e Create pages for recipe on Admin Panel 2025-06-16 21:46:01 +02:00
David Katrinka
945aa9d18c admin panel frontend 2025-06-16 19:45:30 +02:00
e860e0c338 Merge pull request 'added frontend for search input and user/login button' (#6) from TIST-13 into develop
Reviewed-on: #6
2025-06-15 21:45:29 +02:00
b628b32301 Merge pull request 'fixed recent reviews and daily recipes adaptive' (#5) from TIST-10 into develop
Reviewed-on: #5
2025-06-15 21:45:18 +02:00
28592fd2d6 Merge pull request 'TIST-9: updated font size for faq' (#4) from TIST-9 into develop
Reviewed-on: #4
2025-06-15 21:38:45 +02:00
David Katrinka
9faa333c2e added frontend for search input and user/login button 2025-06-15 20:55:22 +02:00
David Katrinka
6dadf22b1e fixed recent reviews and daily recipes adaptive 2025-06-15 20:23:06 +02:00
David Katrinka
9b45f69a8f TIST-9: updated font size for faq 2025-06-15 20:14:08 +02:00
82b4c98f29 Merge pull request 'Create models for the recipe app and connect them to the database' (#3) from TIST-5 into develop
Reviewed-on: #3
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
2025-06-15 18:25:22 +02:00
597d46e2ab Create models for the recipe app and connect them to the database 2025-06-15 18:08:00 +02:00
ed9cbb1d36 Merge pull request 'TIST 6: adaptive for header and footer' (#2) from TIST-6 into develop
Reviewed-on: #2
2025-06-14 20:16:24 +02:00
David Katrinka
1b4abcd062 TIST 6: adaptive for header and footer 2025-06-14 16:41:44 +02:00
28b4eb6127 Merge pull request 'TIST-7' (#1) from TIST-7 into develop
Reviewed-on: #1
2025-06-14 14:48:59 +02:00
22103ffeb1 Fixed english names 2025-06-14 14:41:41 +02:00
86 changed files with 4418 additions and 634 deletions

9
.htaccess Normal file
View File

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

View File

@ -22,8 +22,11 @@ The FridgeBites was developed almost entirely from scratch using PHP, without re
The website is built entirely from scratch using pure PHP, without relying on any frameworks. This was done as an experimental project to enhance personal skills. Despite this, some libraries were used to add specific functionality:
- [\[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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,55 @@ use Lycoreco\Apps\Users\Models\UserModel;
*/
class AdminDeleteController extends Abstract\AdminBaseController
{
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,14 +123,14 @@ 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');
$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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,23 +1,60 @@
<?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 -->
<link href="<?php echo ASSETS_PATH . '/fontawesome-free-6.6.0-web/css/fontawesome.min.css' ?>" rel="stylesheet" />
<!-- Font Awesome -->
<link href="<?php echo ASSETS_PATH . '/fontawesome-free-6.6.0-web/css/fontawesome.min.css' ?>" rel="stylesheet" />
<link href="<?php echo ASSETS_PATH . '/fontawesome-free-6.6.0-web/css/brands.min.css' ?>" rel="stylesheet" />
<link href="<?php echo ASSETS_PATH . '/fontawesome-free-6.6.0-web/css/solid.min.css' ?>" rel="stylesheet" />
<!-- Font Awesome/ -->
@ -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('users:logout') ?>">Log Out</a>
<a href="<?php the_permalink('index:home') ?>" class="hover-anim">View site</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">
<li>
<a class="<?php echo Router::$current_router_name == 'admin:home' ? "active" : "" ?>" href="<?php the_permalink('admin:home') ?>">
<i class="fa-solid fa-house"></i> Dashboard
</a>
</li>
<li>
<a class="<?php echo Router::$current_router_name == 'admin:user-list' ? "active" : "" ?>" href="<?php the_permalink('admin:user-list') ?>">
<i class="fa-solid fa-users"></i> Users
</a>
</li>
<?php foreach ($sidebar_links as $link): ?>
<li>
<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">

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

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

View File

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

View File

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

View File

@ -19,8 +19,8 @@
<?php endif ?>
</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>

View File

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

View File

@ -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')
];

View File

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

View File

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

View File

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

View File

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

View File

@ -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
*/
/**
* @var PageError
*/
?>
<div class="error-page">
<div class="error-code"><?php echo $error->get_http_error() ?></div>
<div class="error-message"><?php echo $error->getMessage() ?></div>
<div 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() ?>

View File

@ -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>
</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>
<?php
foreach ($context['usermenu_recipe_prefetch'] 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 <?= 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',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@ -1,20 +1,46 @@
:root {
--panel-text: #008b70;
--input-background: #f7f7f7;
--input-border: #b3b3b3;
--input-placeholder: #b3b3b3;
--panel-background: #eaf8eb;
--input-text-color: #000;
--title-color: #015847;
--meta-color: #727272;
--common-text: #000;
--panel-title-color: #000;
--button-primary: #0DBB99;
--button-secondary: #ECECEC;
--title-font: 'Roboto Slab', sans-serif;
--common-font: 'Roboto Condensed', serif;
}
* {
box-sizing: border-box;
}
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;
}

View File

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

File diff suppressed because it is too large Load Diff

BIN
assets/food_3d.glb Normal file

Binary file not shown.

View File

@ -8,7 +8,7 @@
* @param {Function} onError
*/
function sendAjax(action,
args = [ ],
args = [],
onLoad = () => { },
onSuccess = (data) => { },
onError = (error) => { }) {
@ -25,17 +25,17 @@ function sendAjax(action,
'args': args
})
})
.then(response => response.json().then(data => ({ data, response })))
.then(({ data, response }) => {
if (!response.ok) {
throw new Error(data.error || `HTTP error! Status: ${response.status}`);
}
.then(response => response.json().then(data => ({ data, response })))
.then(({ data, response }) => {
if (!response.ok) {
throw new Error(data.error || `HTTP error! Status: ${response.status}`);
}
onSuccess(data);
})
.catch(error => {
onError(error);
});
onSuccess(data);
})
.catch(error => {
onError(error);
});
}
function showToastify(message, type = "info") {
Toastify({
@ -43,6 +43,87 @@ function showToastify(message, type = "info") {
gravity: "bottom",
position: "left",
className: type,
duration: 3000 })
.showToast();
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
View File

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

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

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

View File

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

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

File diff suppressed because one or more lines are too long

View File

@ -20,7 +20,7 @@
<script src="<?php echo ASSETS_PATH . '/js/main.js' ?>"></script>
<script src="<?php echo ASSETS_PATH . '/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; ?>

View File

@ -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): ?>
<meta name="<?php echo $tag[0] ?>" content="<?php echo $tag[1] ?>">
<?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,28 +39,46 @@
<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>
<div class="page">
<div class="page">

View File

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

48
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"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

Binary file not shown.

BIN
fridgebitesDocs3 Normal file

Binary file not shown.

View File

@ -133,13 +133,13 @@ function the_pagination(int $count, int $elem_per_page, int $current_page)
continue;
$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);
}

View File

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

View File

@ -344,7 +344,7 @@ abstract class BaseModel
else
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)
{

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,15 +1,18 @@
<?php
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());
?>