Merge pull request 'TIST-17: added single recipe page' (#15) from TIST-17 into develop

Reviewed-on: #15
This commit is contained in:
steve_dekart 2025-06-29 12:25:44 +02:00
commit 420d0bfc6c
9 changed files with 598 additions and 52 deletions

View File

@ -2,7 +2,9 @@
namespace Lycoreco\Apps\Recipes\Controllers;
use Lycoreco\Apps\Recipes\Models\IngredientInRecipeModel;
use Lycoreco\Apps\Recipes\Models\RecipeModel;
use Lycoreco\Apps\Users\Models\UserModel;
use Lycoreco\Includes\BaseController;
use Lycoreco\Includes\Routing\HttpExceptions;
@ -33,8 +35,26 @@ class SingleRecipeController extends BaseController
public function get_context_data()
{
$context = parent::get_context_data();
$recipe = $this->get_model();
$context['recipe'] = $this->get_model();
$context['recipe'] = $recipe;
$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()
]
));
return $context;
}

View File

@ -47,4 +47,9 @@ class IngredientInRecipeModel extends BaseModel
{
return $this->field_amount . ' ' . $this->ingredient_unit;
}
public function __toString()
{
return $this->ingredient_name;
}
}

View File

@ -76,10 +76,20 @@ class RecipeModel extends BaseModel
{
return ucfirst($this->field_status);
}
public function get_image_url() {
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";
}
}

View File

@ -31,7 +31,7 @@ the_header(
<i class="fa-solid fa-magnifying-glass search-icon"></i>
<input type="text" class="search-input" placeholder="Search categories...">
</div>
<!-- All labels and ids are the same for template design purposes (all labels point to same checkbox) -->
<div class="filters-checkboxes">
<ul>
<?php foreach ($context['categories'] as $cat):

View File

@ -1,3 +1,133 @@
<pre>
<?php var_dump($context['recipe']); ?>
</pre>
<?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', 'terms, use, conditions']
]
);
?>
<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 class="btn btn-secondary btn-small hover-anim" title="Add To Favorites">
<i class="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>
</div>
<?php the_footer(array(
ASSETS_PATH . '/js/single.js',
)); ?>

View File

@ -662,7 +662,6 @@ hr {
label span {
color: #015847;
/* Example: make the asterisk red */
font-weight: bold;
}
@ -676,7 +675,7 @@ label span {
.btn-secondary {
background-color: var(--button-secondary);
height: 35px;
cursor: pointer;
max-width: 225px;
color: var(--common-text);
}
@ -801,7 +800,6 @@ label span {
overflow-y: auto;
margin-top: 10px;
margin-bottom: 15px;
}
.filters-checkboxes ul li {
@ -813,6 +811,215 @@ label span {
.filters-form .btn {
width: 200px;
}
.single-recipe {
margin-top: 46px;
}
.single-recipe-title .title {
text-align: start;
}
.single-recipe-info {
display: flex;
}
.single-recipe-info__image {
width: 350px;
height: 260px;
overflow: hidden;
border-radius: 10px;
margin-right: 25px;
}
.single-recipe-info__image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.single-recipe-data {
margin-bottom: 20px;
}
.single-recipe-data__item {
display: flex;
align-items: baseline;
margin-bottom: 6px;
}
.single-recipe-data__item .data-name {
color: var(--meta-color);
margin-right: 10px;
flex: 0 0 120px;
text-align: left;
}
.single-recipe-data__item .data {
max-width: 500px;
}
.day-select {
position: relative;
width: 230px;
border: 1px solid #00775F;
padding: 0;
}
.day-select-wrapper {
position: relative;
display: inline-block;
}
.btn-ctrl {
display: flex;
align-items: center;
}
.select-icon {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
background-color: #008C71;
color: #fff;
padding: 10px;
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
}
.btn-small {
width: 35px;
text-align: center;
padding: 0;
border: 2px solid var(--input-border);
}
.button-ctrl {
display: flex;
justify-content: space-between;
align-items: center;
text-align: center;
}
.custom-select-dropdown {
position: absolute;
top: 110%;
left: 0;
z-index: 10;
background: white;
border: 1px solid var(--input-border);
border-radius: 10px;
width: 100%;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
.dropdown-item {
font-size: 16px;
padding: 8px 12px;
cursor: pointer;
border-radius: 10px;
}
.custom-select-dropdown .dropdown-item:last-child {
border-top: 1px solid var(--input-border);
}
.dropdown-selected {
background-color: #00775F;
color: #fff;
}
.qr-popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #fff;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
z-index: 9999;
}
#qrcode img {
margin-bottom: 15px;
}
.hidden {
display: none !important;
}
#overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
z-index: 9998;
}
.btn-small,
.btn-small i {
line-height: 30px;
}
.single-recipe-content h2 {
text-align: start;
}
.single-recipe-content {
display: flex;
width: 100%;
justify-content: space-between;
align-items: flex-start;
margin-top: 30px;
gap: 40px;
/* flex-wrap: wrap; */
}
.single-instructions{
width: 50%;
font-size: 16px;
}
.single-ingredients{
width: 50%;
}
.ingredients-table{
width: 400px;
border-radius: 10px;
overflow: hidden;
border: 1px solid #b3b3b3;
border-collapse: separate;
}
.ingredients-table th{
background-color: #008B70;
color: #fff;
padding: 10px 20px;
text-align: start;
}
.ingredients-table td{
padding: 10px 20px;
border-top: 2px solid #b3b3b3;
}
@media (max-width: 768px) {
.catalog-items {
@ -824,7 +1031,104 @@ label span {
flex-direction: column;
}
.single-recipe-content {
flex-wrap: wrap;
flex-direction: column;
gap: 20px;
padding: 10px;
}
.single-instructions,
.single-ingredients {
width: 100%;
}
.ingredients-table {
width: 100%;
}
.single-recipe-info {
flex-direction: column;
align-items: center;
text-align: center;
}
.single-recipe-info__image {
width: 100%;
max-width: 350px;
height: auto;
margin-right: 0;
margin-bottom: 20px;
}
.single-recipe-info__image img {
width: 100%;
height: auto;
}
.single-recipe-info__details {
width: 100%;
padding: 0 15px;
}
.single-recipe-title .title {
text-align: center;
font-size: 24px;
}
.single-recipe-data__item {
flex-direction: column;
align-items: flex-start;
margin-bottom: 10px;
}
.single-recipe-data__item .data-name {
margin-right: 0;
margin-bottom: 2px;
flex: none;
text-align: left;
}
.button-ctrl {
flex-direction: column;
align-items: center;
gap: 12px;
}
.small-btns{
display: flex;
justify-content: center;
gap: 10px;
}
.day-select-wrapper,
.btn-small,
.btn-small i {
width: auto;
}
.btn-small{
width: 35px;
}
.custom-select-dropdown {
width: 100%;
left: 0;
max-width: 230px;
}
.qr-popup {
width: 90%;
}
}
@media (max-width: 900px) {
@ -904,22 +1208,26 @@ label span {
max-width: 100%;
}
}
@media (max-width: 1200px) {
.catalog {
flex-direction: column-reverse;
align-items: center;
margin-bottom: 46px;
}
.filters {
position: static;
margin-bottom: 46px;
}
.catalog-items {
margin-right: 0;
justify-items: center;
align-items: center;
padding: 5px;
}
.catalog-recipe {
display: flex;
flex-direction: column;
@ -927,15 +1235,16 @@ label span {
justify-content: center;
width: 200px;
}
#food-3d {
display: none;
}
}
@media (max-width: 465px) {
.catalog-recipe {
width: 150px;
}
#food-3d {
display: none;
}
}
@media (max-width: 400px) {

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

@ -0,0 +1,71 @@
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');
options.forEach(option => {
option.addEventListener('click', () => {
const selectedValue = option.getAttribute('data-value');
options.forEach(opt => opt.classList.remove('dropdown-selected'));
if (selectedValue === 'remove') {
toggleBtn.textContent = 'Add to list';
} else {
toggleBtn.textContent = option.textContent;
option.classList.add('dropdown-selected');
}
dropdown.classList.add('hidden');
alert(`You selected: ${selectedValue}`);
});
});
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");
}
});

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