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>
This commit is contained in:
steve_dekart 2025-07-06 14:32:59 +02:00
commit 7012c9aa85
10 changed files with 278 additions and 36 deletions

View File

@ -44,6 +44,11 @@ class CatalogController extends BaseController
'is_having' => true
);
}
$fields[] = array(
'name' => 'obj.status',
'type' => '=',
'value' => 'publish'
);
$context['recipes_count'] = RecipeModel::count($fields);
$context['recipes'] = RecipeModel::filter(

View File

@ -2,13 +2,71 @@
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 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();

View File

@ -25,7 +25,7 @@ class CategoryModel extends BaseModel
public static function get_cat_values()
{
$cat_list = self::filter(array());
$cat_list = self::filter(array(), count: 200);
$result = array();
foreach($cat_list as $cat) {
$result[] = [ $cat->get_id(), $cat->field_name ];

View File

@ -1,4 +1,5 @@
<?php
namespace Lycoreco\Apps\Recipes\Models;
use Lycoreco\Includes\Model\BaseModel;
@ -39,9 +40,7 @@ class RecipeModel extends BaseModel
'join_table' => 'users us ON us.id = obj.author_id'
],
[
'field' => [
],
'field' => [],
'join_table' => 'recipe_ingredients tb2 ON tb2.recipe_id = obj.id'
]
);
@ -57,21 +56,22 @@ class RecipeModel extends BaseModel
'status' => 'string',
'created_at' => 'DateTime'
];
protected static function get_additional_fields(){
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) {
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"
"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) {
if (CURRENT_USER) {
$add_fields = array_merge($add_fields, array(
[
"field" => [
@ -106,7 +106,7 @@ class RecipeModel extends BaseModel
}
public function get_absolute_url()
{
return get_permalink('recipes:single', [ $this->get_id() ]);
return get_permalink('recipes:single', [$this->get_id()]);
}
public function get_price()
{
@ -129,7 +129,30 @@ class RecipeModel extends BaseModel
return nl2br(trim($this->field_instruction));
}
public function get_time(){
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

@ -9,12 +9,21 @@ the_header(
]
);
?>
<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>
<form action="" class="single-submit-form" method="post" enctype="multipart/form-data">
<?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>
@ -26,28 +35,34 @@ the_header(
</div>
<label for="ingredients-input">Ingredients</label><span>*</span>
<button type="button" id="add-ingredient-btn" class="btn btn-primary hover-anim add-ingredient-btn">Add new
ingredient</button>
<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 class="input">
select 2 goes here
</div>
<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">
@ -56,12 +71,12 @@ the_header(
<label for="time-input">Estimated Time (minutes)</label><span>*</span>
<div class="input">
<input type="text" id="time-input" name="est-time" placeholder="Enter the recipe estimated time" required>
<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="text" id="price-input" name="est-price" placeholder="Enter the recipe estimated price"
<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>
@ -82,6 +97,23 @@ the_header(
<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>

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

View File

@ -1076,6 +1076,16 @@ input[type="checkbox"]{
overflow: hidden;
border: 1px solid #b3b3b3;
border-collapse: separate;
margin-bottom: 20px;
}
.ingredients-table i {
color: #f00;
cursor: pointer;
margin-left: 10px;
}
.ingredients-table td:nth-child(2) {
display: flex;
align-items: center;
}
.ingredients-table th {
@ -1248,7 +1258,7 @@ label {
}
.add-ingredient-btn{
margin-bottom: 25px;
margin-bottom: 0px;
}
.single-submit-form span{

View File

@ -53,3 +53,94 @@ ingredientSubmitBtn.addEventListener('click', async (e) => {
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);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB