Latest Recipes Added
@@ -22,42 +34,21 @@
@@ -76,87 +67,14 @@
@@ -168,240 +86,58 @@
Recent User Reviews
-
+
+
-
+
-
Just Like Mom's
+
= $review->recipe_title ?>
-
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...
+
= $review->get_excerpt() ?>
-
-
-
-
-
-
-
Just Like Mom's
-
-
-
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...
-
-
-
-
-
-
-
-
Just Like Mom's
-
-
-
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...
-
-
-
-
-
-
-
-
Just Like Mom's
-
-
-
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...
-
-
-
-
-
-
-
-
Just Like Mom's
-
-
-
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...
-
-
-
-
-
-
-
-
Just Like Mom's
-
-
-
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...
-
-
+
+
+
-
Your Menu for Monday
+
Your Menu for = date("l"); ?>
+
+
+
No meals added for = date("l"); ?>
+
You have not added any recipes for = date("l");?>, please go to the catalog and add recipes.
+
+
+
+
+
+
+
'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;
+ }
+}
diff --git a/apps/Recipes/Controllers/DailyMealsController.php b/apps/Recipes/Controllers/DailyMealsController.php
new file mode 100644
index 0000000..dad7262
--- /dev/null
+++ b/apps/Recipes/Controllers/DailyMealsController.php
@@ -0,0 +1,27 @@
+__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;
+ }
+}
diff --git a/apps/Recipes/Controllers/FavoritesController.php b/apps/Recipes/Controllers/FavoritesController.php
new file mode 100644
index 0000000..c5047d6
--- /dev/null
+++ b/apps/Recipes/Controllers/FavoritesController.php
@@ -0,0 +1,50 @@
+ '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;
+ }
+}
\ No newline at end of file
diff --git a/apps/Recipes/Controllers/SingleRecipeController.php b/apps/Recipes/Controllers/SingleRecipeController.php
new file mode 100644
index 0000000..0ba4ebe
--- /dev/null
+++ b/apps/Recipes/Controllers/SingleRecipeController.php
@@ -0,0 +1,133 @@
+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;
+ }
+}
diff --git a/apps/Recipes/Controllers/SingleSubmitController.php b/apps/Recipes/Controllers/SingleSubmitController.php
new file mode 100644
index 0000000..39df223
--- /dev/null
+++ b/apps/Recipes/Controllers/SingleSubmitController.php
@@ -0,0 +1,80 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/apps/Recipes/Models/CategoryModel.php b/apps/Recipes/Models/CategoryModel.php
new file mode 100644
index 0000000..93b0663
--- /dev/null
+++ b/apps/Recipes/Models/CategoryModel.php
@@ -0,0 +1,35 @@
+ '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;
+ }
+}
\ No newline at end of file
diff --git a/apps/Recipes/Models/FavoriteModel.php b/apps/Recipes/Models/FavoriteModel.php
new file mode 100644
index 0000000..83c3071
--- /dev/null
+++ b/apps/Recipes/Models/FavoriteModel.php
@@ -0,0 +1,47 @@
+ '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;
+ }
+}
\ No newline at end of file
diff --git a/apps/Recipes/Models/IngredientInRecipeModel.php b/apps/Recipes/Models/IngredientInRecipeModel.php
new file mode 100644
index 0000000..7ecc9ea
--- /dev/null
+++ b/apps/Recipes/Models/IngredientInRecipeModel.php
@@ -0,0 +1,55 @@
+ [
+ '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;
+ }
+}
\ No newline at end of file
diff --git a/apps/Recipes/Models/IngredientModel.php b/apps/Recipes/Models/IngredientModel.php
new file mode 100644
index 0000000..424eb0f
--- /dev/null
+++ b/apps/Recipes/Models/IngredientModel.php
@@ -0,0 +1,38 @@
+ '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;
+ }
+}
\ No newline at end of file
diff --git a/apps/Recipes/Models/RecipeModel.php b/apps/Recipes/Models/RecipeModel.php
new file mode 100644
index 0000000..0f56be8
--- /dev/null
+++ b/apps/Recipes/Models/RecipeModel.php
@@ -0,0 +1,158 @@
+ [
+ '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;
+ }
+}
diff --git a/apps/Recipes/Models/RecipeUserMenu.php b/apps/Recipes/Models/RecipeUserMenu.php
new file mode 100644
index 0000000..4141b05
--- /dev/null
+++ b/apps/Recipes/Models/RecipeUserMenu.php
@@ -0,0 +1,93 @@
+ '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;
+ }
+}
diff --git a/apps/Recipes/Models/ReviewsModel.php b/apps/Recipes/Models/ReviewsModel.php
new file mode 100644
index 0000000..2ef4c69
--- /dev/null
+++ b/apps/Recipes/Models/ReviewsModel.php
@@ -0,0 +1,121 @@
+ '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;
+ }
+}
\ No newline at end of file
diff --git a/apps/Recipes/Templates/catalog.php b/apps/Recipes/Templates/catalog.php
new file mode 100644
index 0000000..6419310
--- /dev/null
+++ b/apps/Recipes/Templates/catalog.php
@@ -0,0 +1,97 @@
+
+
+
\ No newline at end of file
diff --git a/apps/Recipes/Templates/components/catalog-item.php b/apps/Recipes/Templates/components/catalog-item.php
new file mode 100644
index 0000000..659be4b
--- /dev/null
+++ b/apps/Recipes/Templates/components/catalog-item.php
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
= $recipe->field_title ?>
+
+
+
+
\ No newline at end of file
diff --git a/apps/Recipes/Templates/components/recipe-ings-item.php b/apps/Recipes/Templates/components/recipe-ings-item.php
new file mode 100644
index 0000000..1dfcb7a
--- /dev/null
+++ b/apps/Recipes/Templates/components/recipe-ings-item.php
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
= $recipe->field_title ?>
+ = $recipe->field_estimated_time ?> mins to make
+
+
+
+
+ = $ingredients[$i]->ingredient_name ?>
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/Recipes/Templates/daily-meals.php b/apps/Recipes/Templates/daily-meals.php
new file mode 100644
index 0000000..fa18f80
--- /dev/null
+++ b/apps/Recipes/Templates/daily-meals.php
@@ -0,0 +1,43 @@
+
+
+
+ $recipe_prefetches) {
+?>
+
+
+
Your Menu for = ucfirst($day) ?>
+
+
+
+
+
+
+
+
No meals added for = ucfirst($day); ?>
+
You have not added any recipes for = ucfirst($day);?>, please go to the catalog and add recipes.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/Recipes/Templates/export-pdf.php b/apps/Recipes/Templates/export-pdf.php
new file mode 100644
index 0000000..42d52f9
--- /dev/null
+++ b/apps/Recipes/Templates/export-pdf.php
@@ -0,0 +1 @@
+Output('I', 'recipe.pdf') ?>
\ No newline at end of file
diff --git a/apps/Recipes/Templates/favorites.php b/apps/Recipes/Templates/favorites.php
new file mode 100644
index 0000000..d35800a
--- /dev/null
+++ b/apps/Recipes/Templates/favorites.php
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+ Nothing to show
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/Recipes/Templates/single-submit.php b/apps/Recipes/Templates/single-submit.php
new file mode 100644
index 0000000..429ea6b
--- /dev/null
+++ b/apps/Recipes/Templates/single-submit.php
@@ -0,0 +1,122 @@
+
+
+
+
+
+
Submit a Recipe
+
+
+
+
+
+
+
+
+
Ingredient Name *
+
+
+
+
Unit of measure *
+
+
+
+
Add new ingredient
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/Recipes/Templates/single.php b/apps/Recipes/Templates/single.php
new file mode 100644
index 0000000..148bd79
--- /dev/null
+++ b/apps/Recipes/Templates/single.php
@@ -0,0 +1,194 @@
+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'],
+ ]
+);
+?>
+
+
= $context['recipe']->get_id() ?>
+
+
+
+
+
+
+
+
+
+
+
field_title; ?>
+
+
+
+ Category:
+ = $context['recipe']->category_name ?>
+
+
+ Author:
+ = $context['author']->field_username ?>
+
+
+ Estimated Price:
+ get_price(); ?>
+
+
+ Time To Make:
+ get_time(); ?>
+
+
+ Ingredients:
+ = join(", ", $context['ingredients']) ?>
+
+
+ Date Created:
+ field_created_at; ?>
+
+
+
+
+
+
+
+
Instructions
+
+ = $context['recipe']->get_html_instruction(); ?>
+
+
+
Ingredients
+
+
+
+ Name
+ Count
+
+
+
+
+
+ = $ing; ?>
+ = $ing->get_count(); ?>
+
+
+
+
+
+
+
+
Reviews
+
Average Rating: = $context['reviews_average'] ?>
+
+
+
All Reviews
+
+
Nothing to show
+
+
+
+
+
= $review->field_rating ?> = $review->field_title ?>
+
+ = $review->get_html_content() ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/Recipes/Utils/RecipePDF.php b/apps/Recipes/Utils/RecipePDF.php
new file mode 100644
index 0000000..7943851
--- /dev/null
+++ b/apps/Recipes/Utils/RecipePDF.php
@@ -0,0 +1,111 @@
+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();
+ }
+}
diff --git a/apps/Recipes/components.php b/apps/Recipes/components.php
new file mode 100644
index 0000000..a56faab
--- /dev/null
+++ b/apps/Recipes/components.php
@@ -0,0 +1,12 @@
+ { },
- onSuccess = (data) => { },
+function sendAjax(action,
+ args = [],
+ onLoad = () => { },
+ onSuccess = (data) => { },
onError = (error) => { }) {
-
+
onLoad();
-
+
fetch('/ajax', {
method: 'POST',
headers: {
@@ -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}`);
- }
-
- onSuccess(data);
- })
- .catch(error => {
- onError(error);
- });
+ .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);
+ });
}
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 += `
+
+ `;
+ });
+ searchResults.hidden = false;
+ } else {
+ searchResults.innerHTML = `
No recipes found
`;
+ searchResults.hidden = false;
+ }
+ }, 300);
+});
+
+document.addEventListener('click', function (event) {
+ if (!searchResults.contains(event.target) && event.target !== searchInput) {
+ searchResults.hidden = true;
+ }
+});
\ No newline at end of file
diff --git a/assets/js/single-submit.js b/assets/js/single-submit.js
new file mode 100644
index 0000000..843dc46
--- /dev/null
+++ b/assets/js/single-submit.js
@@ -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 += `
+
${ingredientName}
+
+
+
+
+
+ `;
+ 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 = `
No recipes found
`;
+ searchIngDropdown.hidden = false;
+ }
+ }, 300);
+});
\ No newline at end of file
diff --git a/assets/js/single.js b/assets/js/single.js
new file mode 100644
index 0000000..c8754d7
--- /dev/null
+++ b/assets/js/single.js
@@ -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');
+});
\ No newline at end of file
diff --git a/assets/js/threejs-scene.js b/assets/js/threejs-scene.js
new file mode 100644
index 0000000..ec7e5b9
--- /dev/null
+++ b/assets/js/threejs-scene.js
@@ -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();
\ No newline at end of file
diff --git a/assets/qrcode/qrcode.min.js b/assets/qrcode/qrcode.min.js
new file mode 100644
index 0000000..993e88f
--- /dev/null
+++ b/assets/qrcode/qrcode.min.js
@@ -0,0 +1 @@
+var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c
=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push(' ');g.push(" ")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}();
\ No newline at end of file
diff --git a/components/templates/layout/footer.php b/components/templates/layout/footer.php
index 1004d29..494f5cc 100644
--- a/components/templates/layout/footer.php
+++ b/components/templates/layout/footer.php
@@ -20,7 +20,7 @@
-
+
diff --git a/components/templates/layout/header.php b/components/templates/layout/header.php
index b7717b9..4395f58 100644
--- a/components/templates/layout/header.php
+++ b/components/templates/layout/header.php
@@ -1,5 +1,6 @@
+
@@ -11,14 +12,16 @@
-
-
+
+
-
+
@@ -36,28 +39,46 @@
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/composer.json b/composer.json
index b6b3fe3..db4b3e6 100644
--- a/composer.json
+++ b/composer.json
@@ -9,6 +9,7 @@
}
},
"require": {
- "phpmailer/phpmailer": "^6.10"
+ "phpmailer/phpmailer": "^6.10",
+ "setasign/fpdf": "^1.8"
}
}
diff --git a/composer.lock b/composer.lock
index 462c06e..84bf483 100644
--- a/composer.lock
+++ b/composer.lock
@@ -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": [],
diff --git a/fridgebitesDocs2.pdf b/fridgebitesDocs2.pdf
new file mode 100644
index 0000000..71e935d
Binary files /dev/null and b/fridgebitesDocs2.pdf differ
diff --git a/fridgebitesDocs3 b/fridgebitesDocs3
new file mode 100644
index 0000000..13f5f8c
Binary files /dev/null and b/fridgebitesDocs3 differ
diff --git a/functions.php b/functions.php
index 21b8b83..0159aa7 100644
--- a/functions.php
+++ b/functions.php
@@ -133,13 +133,13 @@ function the_pagination(int $count, int $elem_per_page, int $current_page)
continue;
$GET['page'] = $i;
?>
-
+
0 && $current_page <= $total_pages): ?>
-
+
@@ -147,7 +147,7 @@ function the_pagination(int $count, int $elem_per_page, int $current_page)
-
+
@@ -252,4 +252,54 @@ function send_email(string $subject, string $body, string $altBody, string $to_a
$mail->send();
}
-?>
\ No newline at end of file
+
+/**
+ * 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);
+}
diff --git a/includes/Const/recipes.php b/includes/Const/recipes.php
new file mode 100644
index 0000000..19b83d5
--- /dev/null
+++ b/includes/Const/recipes.php
@@ -0,0 +1,5 @@
+ 'monday', 2 => 'tuesday', 3 => 'wednesday', 4 => 'thursday', 5 => 'friday', 6 => 'saturday', 0 => 'sunday'
+));
\ No newline at end of file
diff --git a/includes/Model/BaseModel.php b/includes/Model/BaseModel.php
index d924618..238842f 100644
--- a/includes/Model/BaseModel.php
+++ b/includes/Model/BaseModel.php
@@ -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)
{
diff --git "a/media/Lycoreco\\Apps\\Recipes\\Models\\RecipeModel/chana-masala-recipe_684f4743a2a80.jpg" "b/media/Lycoreco\\Apps\\Recipes\\Models\\RecipeModel/chana-masala-recipe_684f4743a2a80.jpg"
new file mode 100644
index 0000000..63d9ff9
Binary files /dev/null and "b/media/Lycoreco\\Apps\\Recipes\\Models\\RecipeModel/chana-masala-recipe_684f4743a2a80.jpg" differ
diff --git "a/media/Lycoreco\\Apps\\Recipes\\Models\\RecipeModel/images_684f027f3a8da.jpeg" "b/media/Lycoreco\\Apps\\Recipes\\Models\\RecipeModel/images_684f027f3a8da.jpeg"
new file mode 100644
index 0000000..d74d844
Binary files /dev/null and "b/media/Lycoreco\\Apps\\Recipes\\Models\\RecipeModel/images_684f027f3a8da.jpeg" differ
diff --git a/media/recipes/Simply-Recipes-Spaghetti-Aglio-e-Olio-LEAD-2-c8e7e8c6edb04a8691463c6ea8cd4ba1_6862601986ee2.jpg b/media/recipes/Simply-Recipes-Spaghetti-Aglio-e-Olio-LEAD-2-c8e7e8c6edb04a8691463c6ea8cd4ba1_6862601986ee2.jpg
new file mode 100644
index 0000000..c19273b
Binary files /dev/null and b/media/recipes/Simply-Recipes-Spaghetti-Aglio-e-Olio-LEAD-2-c8e7e8c6edb04a8691463c6ea8cd4ba1_6862601986ee2.jpg differ
diff --git a/media/recipes/caesar_686a6c042a944.jpg b/media/recipes/caesar_686a6c042a944.jpg
new file mode 100644
index 0000000..a8009aa
Binary files /dev/null and b/media/recipes/caesar_686a6c042a944.jpg differ
diff --git a/media/recipes/chana_685f868b8fb94.jpg b/media/recipes/chana_685f868b8fb94.jpg
new file mode 100644
index 0000000..c0380f7
Binary files /dev/null and b/media/recipes/chana_685f868b8fb94.jpg differ
diff --git a/media/recipes/garlic_686a6a714284a.jpg b/media/recipes/garlic_686a6a714284a.jpg
new file mode 100644
index 0000000..5b8866e
Binary files /dev/null and b/media/recipes/garlic_686a6a714284a.jpg differ
diff --git a/media/recipes/images_68626081274a5.jpeg b/media/recipes/images_68626081274a5.jpeg
new file mode 100644
index 0000000..5093915
Binary files /dev/null and b/media/recipes/images_68626081274a5.jpeg differ
diff --git a/urls.php b/urls.php
index b07374a..2b2d536 100644
--- a/urls.php
+++ b/urls.php
@@ -1,15 +1,18 @@
\ No newline at end of file