(function () { 'use strict'; function setText(element, value) { if (element) element.textContent = value == null ? '' : String(value); } document.addEventListener('click', function (event) { var button = event.target.closest('[data-recipe-share]'); if (!button) return; var url = button.getAttribute('data-share-url') || window.location.href; var title = button.getAttribute('data-share-title') || document.title; var status = button.parentNode ? button.parentNode.querySelector('.kalia-share-status') : null; if (navigator.share) { navigator.share({ title: title, text: title, url: url }).then(function () { setText(status, 'Ricetta condivisa.'); }).catch(function () {}); return; } if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(url).then(function () { setText(status, 'Link copiato.'); }).catch(function () { setText(status, 'Copia il link dalla barra del browser.'); }); return; } setText(status, 'Copia il link dalla barra del browser.'); }); function postRecipe(data) { return fetch('/it', { method: 'POST', body: data, credentials: 'same-origin' }) .then(function (response) { return response.json(); }) .then(function (payload) { if (!payload.success) throw new Error(payload.message || 'Richiesta non completata.'); return payload.data; }); } function requestMedia(recipeId, kind, token) { var data = new FormData(); data.append('action', 'recipes/media'); data.append('csrf_token', token); data.append('ricetta_id', String(recipeId)); data.append('tipo', kind); return postRecipe(data); } document.addEventListener('click', function (event) { var button = event.target.closest('[data-recipe-media]'); if (!button) return; var card = button.closest('[data-recipe-card]'); var container = button.closest('[data-recipe-csrf]'); if (!card || !container) return; var kind = button.getAttribute('data-recipe-media'); var recipeId = button.getAttribute('data-recipe-id'); var mediaStatus = card.querySelector('[data-recipe-media-status]'); button.disabled = true; setText(mediaStatus, kind === 'immagine' ? 'Creazione immagine in corso...' : 'Creazione audio in corso...'); requestMedia(recipeId, kind, container.getAttribute('data-recipe-csrf')) .then(function (data) { if (kind === 'immagine') { var picture = card.querySelector('[data-recipe-image]'); if (picture) { picture.src = data.path; picture.alt = card.querySelector('h2').textContent; } } else { var emptyAudio = card.querySelector('[data-recipe-audio-empty]'); if (emptyAudio) emptyAudio.parentNode.removeChild(emptyAudio); var mediaActions = card.querySelector('.kalia-recipe-media-actions'); var audio = document.createElement('audio'); audio.controls = true; audio.preload = 'none'; audio.src = data.path; audio.setAttribute('aria-label', 'Ascolta ' + (card.querySelector('h2') ? card.querySelector('h2').textContent : 'la ricetta')); audio.setAttribute('data-recipe-audio', ''); mediaActions.insertBefore(audio, button); var voiceNote = document.createElement('small'); voiceNote.className = 'text-secondary'; setText(voiceNote, 'Voce generata con AI.'); mediaActions.insertBefore(voiceNote, button); var listenSlot = card.querySelector('[data-recipe-listen-slot]'); if (listenSlot && !listenSlot.querySelector('a')) { var listenLink = document.createElement('a'); listenLink.className = 'btn btn-outline-kalia'; listenLink.href = '/it/ascolta-ricetta?codice=' + encodeURIComponent(card.getAttribute('data-recipe-code')); var listenIcon = document.createElement('i'); listenIcon.className = 'fa-solid fa-arrow-up-right-from-square me-1'; listenIcon.setAttribute('aria-hidden', 'true'); listenLink.appendChild(listenIcon); listenLink.appendChild(document.createTextNode('Apri la pagina di ascolto')); listenSlot.appendChild(listenLink); } } setText(mediaStatus, kind === 'immagine' ? 'Immagine pronta.' : 'Audio pronto.'); button.parentNode.removeChild(button); }) .catch(function (error) { setText(mediaStatus, error.message || 'Generazione non riuscita.'); button.disabled = false; }); }); document.addEventListener('submit', function (event) { var emailForm = event.target; if (!emailForm.matches('[data-recipe-email]')) return; event.preventDefault(); var emailButton = emailForm.querySelector('button[type="submit"]'); var emailStatus = emailForm.querySelector('.kalia-share-status'); emailButton.disabled = true; setText(emailStatus, 'Invio in corso...'); postRecipe(new FormData(emailForm)) .then(function (data) { var recipient = data && data.destinatario ? data.destinatario : emailForm.elements.destinatario.value.trim(); var modalElement = document.getElementById('recipe-email-sent-modal'); var modalMessage = document.getElementById('recipe-email-sent-message'); if (modalElement && modalMessage && window.bootstrap) { setText(modalMessage, 'Spedito a ' + recipient); setText(emailStatus, ''); var sourceModalElement = emailForm.closest('.modal'); if (sourceModalElement && sourceModalElement !== modalElement) { sourceModalElement.addEventListener('hidden.bs.modal', function () { window.bootstrap.Modal.getOrCreateInstance(modalElement).show(); }, { once: true }); window.bootstrap.Modal.getOrCreateInstance(sourceModalElement).hide(); } else { window.bootstrap.Modal.getOrCreateInstance(modalElement).show(); } } else { setText(emailStatus, 'Spedito a ' + recipient); } }) .catch(function (error) { setText(emailStatus, error.message || 'Invio non riuscito.'); }) .then(function () { emailButton.disabled = false; }); }); document.addEventListener('click', function (event) { var openButton = event.target.closest('[data-recipe-open]'); if (!openButton) return; var card = openButton.closest('[data-recipe-card]'); var details = card ? card.querySelector('details') : null; if (details) { details.open = true; details.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); function normalizeRecipeSearch(value) { var normalized = String(value || '').toLocaleLowerCase('it'); return normalized.normalize ? normalized.normalize('NFD').replace(/[\u0300-\u036f]/g, '') : normalized; } var personalSearch = document.getElementById('personal-recipe-search'); function filterPersonalRecipes() { if (!personalSearch) return; var terms = normalizeRecipeSearch(personalSearch.value).trim().split(/\s+/).filter(Boolean); var cards = document.querySelectorAll('[data-recipe-card]'); var visible = 0; Array.prototype.forEach.call(cards, function (card) { var title = card.querySelector('h2'); var details = card.querySelector('details'); var haystack = normalizeRecipeSearch((title ? title.textContent : '') + ' ' + (details ? details.textContent : '')); var matches = terms.every(function (term) { return haystack.indexOf(term) !== -1; }); card.hidden = !matches; if (matches) visible++; }); setText(document.getElementById('personal-recipe-count'), visible + (visible === 1 ? ' ricetta' : ' ricette')); var empty = document.getElementById('personal-recipe-no-results'); if (empty) { empty.hidden = visible !== 0; if (!cards.length) setText(empty, 'Non hai piu ricette personali.'); } } if (personalSearch) { personalSearch.addEventListener('input', filterPersonalRecipes); var hashCard = document.getElementById(window.location.hash.slice(1)); if (hashCard && hashCard.hasAttribute('data-recipe-card')) { var hashDetails = hashCard.querySelector('details'); if (hashDetails) hashDetails.open = true; } } document.addEventListener('click', function (event) { var deleteButton = event.target.closest('[data-recipe-delete]'); if (!deleteButton) return; var card = deleteButton.closest('[data-recipe-card]'); var container = deleteButton.closest('[data-recipe-csrf]'); if (!card || !container) return; var name = card.querySelector('h2') ? card.querySelector('h2').textContent : 'questa ricetta'; if (!window.confirm('Eliminare "' + name + '"? La ricetta, la sua immagine e il suo audio non saranno piu disponibili.')) return; var status = card.querySelector('[data-recipe-delete-status]'); var data = new FormData(); data.append('action', 'recipes/delete'); data.append('csrf_token', container.getAttribute('data-recipe-csrf')); data.append('ricetta_id', card.getAttribute('data-recipe-id')); deleteButton.disabled = true; setText(status, 'Eliminazione in corso...'); postRecipe(data).then(function (response) { card.parentNode.removeChild(card); filterPersonalRecipes(); var feedback = document.getElementById('personal-recipe-feedback'); if (feedback) { feedback.className = response.media_errors ? 'alert alert-warning' : 'alert alert-success'; feedback.hidden = false; setText(feedback, response.media_errors ? 'Ricetta eliminata, ma alcuni file non sono stati rimossi. Contatta l assistenza.' : 'Ricetta eliminata.'); } }).catch(function (error) { setText(status, error.message || 'Eliminazione non riuscita.'); deleteButton.disabled = false; }); }); var form = document.getElementById('recipe-generator-form'); if (!form) return; var submit = document.getElementById('recipe-generator-submit'); var status = document.getElementById('recipe-generator-status'); var result = document.getElementById('recipe-generator-result'); var modeButtons = form.querySelectorAll('[data-recipe-mode]'); var mode = 'text'; var photoInputs = form.querySelectorAll('[data-recipe-photo-input]'); var photoPreview = document.getElementById('recipe-photo-preview'); var photoPreviewImage = document.getElementById('recipe-photo-preview-image'); var photoFileName = document.getElementById('recipe-photo-file-name'); var photoStatus = document.getElementById('recipe-photo-status'); var recognizedIngredients = ''; var selectedPhoto = null; var photoPreviewUrl = ''; var photoPending = false; var generationPending = false; var generationExhausted = false; var quotaLabel = document.getElementById('recipe-quota-label'); var photoModalElement = document.getElementById('recipe-photo-loading'); var photoModal = photoModalElement && window.bootstrap ? window.bootstrap.Modal.getOrCreateInstance(photoModalElement) : null; var progressElement = document.getElementById('recipe-generation-loading'); var progressModal = progressElement && window.bootstrap ? window.bootstrap.Modal.getOrCreateInstance(progressElement) : null; if (photoModalElement && photoModal) { photoModalElement.addEventListener('shown.bs.modal', function () { if (!photoPending) photoModal.hide(); }); } Array.prototype.forEach.call(modeButtons, function (button) { button.addEventListener('click', function () { mode = button.getAttribute('data-recipe-mode'); Array.prototype.forEach.call(modeButtons, function (item) { var active = item === button; item.classList.toggle('is-active', active); item.setAttribute('aria-pressed', active ? 'true' : 'false'); }); Array.prototype.forEach.call(form.querySelectorAll('[data-recipe-panel]'), function (panel) { panel.hidden = panel.getAttribute('data-recipe-panel') !== mode; }); }); }); function clearRecipePhoto() { selectedPhoto = null; Array.prototype.forEach.call(photoInputs, function (input) { input.value = ''; }); if (photoPreviewUrl) window.URL.revokeObjectURL(photoPreviewUrl); photoPreviewUrl = ''; if (photoPreviewImage) photoPreviewImage.removeAttribute('src'); if (photoPreview) photoPreview.hidden = true; recognizedIngredients = ''; setText(photoFileName, ''); setText(photoStatus, 'La foto verra analizzata solo dopo la selezione.'); } var photoRemove = document.getElementById('recipe-photo-remove'); if (photoRemove) photoRemove.addEventListener('click', clearRecipePhoto); Array.prototype.forEach.call(photoInputs, function (input) { input.addEventListener('change', function () { if (!input.files || !input.files.length) return; var file = input.files[0]; if (file.size <= 0 || file.size > 8 * 1024 * 1024 || ['image/jpeg', 'image/png', 'image/webp'].indexOf(file.type) === -1) { clearRecipePhoto(); setText(photoStatus, 'Usa una foto JPG, PNG o WebP di massimo 8 MB.'); return; } if (photoPending) { input.value = ''; setText(photoStatus, 'Attendi il riconoscimento della foto in corso.'); return; } selectedPhoto = file; Array.prototype.forEach.call(photoInputs, function (other) { if (other !== input) other.value = ''; }); if (photoPreviewUrl) window.URL.revokeObjectURL(photoPreviewUrl); photoPreviewUrl = window.URL.createObjectURL(file); if (photoPreviewImage) photoPreviewImage.src = photoPreviewUrl; if (photoPreview) photoPreview.hidden = false; recognizedIngredients = ''; setText(photoFileName, file.name); photoPending = true; setText(photoStatus, 'Riconosco gli ingredienti nella foto...'); if (photoModal) photoModal.show(); var data = new FormData(); data.append('action', 'recipes/photo-ingredients'); data.append('csrf_token', form.elements.csrf_token.value); data.append('recipe_image', file); postRecipe(data).then(function (response) { if (selectedPhoto !== file) return; recognizedIngredients = String(response.ingredienti || '').trim(); if (quotaLabel && response.quota && !response.quota.unlimited) setText(quotaLabel, 'Ti restano ' + response.quota.remaining + ' utilizzi AI gratuiti. Uno verra usato per creare questa ricetta.'); setText(photoStatus, recognizedIngredients ? 'Foto analizzata. Controlla porzioni e calorie, poi crea la ricetta. La foto non viene conservata.' : "Non ho riconosciuto ingredienti. Prova con un'altra foto."); }).catch(function (error) { if (selectedPhoto === file) { recognizedIngredients = ''; setText(photoStatus, error.message || 'Riconoscimento non riuscito.'); } }).then(function () { photoPending = false; if (photoModal) photoModal.hide(); }); }); }); function setProgressStep(step) { if (!progressElement) return; Array.prototype.forEach.call(progressElement.querySelectorAll('[data-recipe-step]'), function (item) { var number = parseInt(item.getAttribute('data-recipe-step'), 10); item.classList.toggle('is-done', number < step); item.classList.toggle('is-active', number === step); }); } function addList(items, ordered) { var list = document.createElement(ordered ? 'ol' : 'ul'); items.forEach(function (item) { var li = document.createElement('li'); if (typeof item === 'string') { setText(li, item); } else { var amount = item.quantita == null ? '' : ' - ' + formatRecipeAmount(item.quantita, item.unita || ''); setText(li, (item.nome || '') + amount); } list.appendChild(li); }); return list; } function formatRecipeAmount(quantity, unit) { var amount = Number(quantity); if (!isFinite(amount)) return ''; var label = String(unit || '').trim(); if (amount >= 1000 && /^(g|gr|grammo|grammi)$/i.test(label)) { amount /= 1000; label = 'kg'; } return amount.toLocaleString('it-IT', { maximumFractionDigits: 3 }) + (label ? ' ' + label : ''); } function renderRecipe(recipe, recipeId, imagePath) { while (result.firstChild) result.removeChild(result.firstChild); var card = document.createElement('article'); card.className = 'card border-0 shadow-sm'; var body = document.createElement('div'); body.className = 'card-body p-4'; var eyebrow = document.createElement('span'); eyebrow.className = 'kalia-eyebrow'; setText(eyebrow, 'RICETTA CREATA E SALVATA'); var heading = document.createElement('h2'); heading.className = 'h3 mt-2'; setText(heading, recipe.nome); var image = document.createElement('img'); image.className = 'kalia-personal-recipe-image'; image.src = imagePath || '/assets/img/ricetta-personale-placeholder.png'; image.alt = imagePath ? recipe.nome : 'Ingredienti freschi'; var intro = document.createElement('p'); intro.className = 'text-secondary'; setText(intro, recipe.introduzione); var kcal = document.createElement('strong'); kcal.className = 'kalia-recipe-kcal d-block mb-3'; setText(kcal, Math.round(recipe.calorie_kcal_porzione || 0) + ' kcal per porzione'); var grid = document.createElement('div'); grid.className = 'row g-4'; var ingredientsColumn = document.createElement('div'); ingredientsColumn.className = 'col-lg-5'; var ingredientsTitle = document.createElement('h3'); ingredientsTitle.className = 'h5'; setText(ingredientsTitle, 'Ingredienti'); ingredientsColumn.appendChild(ingredientsTitle); ingredientsColumn.appendChild(addList(recipe.ingredienti || [], false)); var stepsColumn = document.createElement('div'); stepsColumn.className = 'col-lg-7'; var stepsTitle = document.createElement('h3'); stepsTitle.className = 'h5'; setText(stepsTitle, 'Preparazione'); stepsColumn.appendChild(stepsTitle); stepsColumn.appendChild(addList(recipe.procedimento || [], true)); grid.appendChild(ingredientsColumn); grid.appendChild(stepsColumn); body.appendChild(eyebrow); body.appendChild(heading); body.appendChild(intro); body.appendChild(kcal); body.appendChild(grid); var mediaInfo = document.createElement('p'); mediaInfo.className = 'small text-secondary mt-3'; setText(mediaInfo, imagePath ? 'Immagine pronta. Creo la versione audio...' : 'Immagine non disponibile. Creo la versione audio...'); var audio = document.createElement('audio'); audio.controls = true; audio.preload = 'none'; audio.hidden = true; var voiceNote = document.createElement('small'); voiceNote.className = 'text-secondary'; setText(voiceNote, 'Voce generata con AI.'); voiceNote.hidden = true; var savedLink = document.createElement('a'); savedLink.href = '/it/mie-ricette'; savedLink.className = 'btn btn-outline-kalia mt-3 d-block'; setText(savedLink, 'Apri le mie ricette per inviare la scheda'); body.appendChild(mediaInfo); body.appendChild(audio); body.appendChild(voiceNote); body.appendChild(savedLink); var imageLink = document.createElement('a'); imageLink.href = '/it/mie-ricette#ricetta-' + encodeURIComponent(recipeId); imageLink.setAttribute('aria-label', 'Apri la ricetta salvata'); imageLink.appendChild(image); card.appendChild(imageLink); card.appendChild(body); result.appendChild(card); result.hidden = false; return { image: image, audio: audio, voiceNote: voiceNote, info: mediaInfo }; } form.addEventListener('submit', function (event) { event.preventDefault(); if (generationPending || generationExhausted) return; if (mode === 'photo' && (photoPending || !selectedPhoto || !recognizedIngredients)) { status.className = 'alert alert-warning'; status.hidden = false; setText(status, photoPending ? "Attendi l'analisi della foto." : 'Seleziona una foto riconoscibile prima di creare la ricetta.'); return; } var request = new FormData(form); if (mode === 'photo') { request.delete('ingredienti'); request.append('ingredienti', recognizedIngredients); } generationPending = true; submit.disabled = true; status.className = 'alert alert-info'; status.hidden = false; setText(status, 'Kalia sta preparando la ricetta e la sua immagine...'); result.hidden = true; setProgressStep(2); if (progressModal) progressModal.show(); postRecipe(request) .then(function (data) { setProgressStep(3); if (data.quota && !data.quota.unlimited) { if (quotaLabel) setText(quotaLabel, 'Ti restano ' + data.quota.remaining + ' utilizzi AI gratuiti. La foto ne richiede due.'); if (!data.quota.allowed) { generationExhausted = true; submit.type = 'button'; submit.setAttribute('data-premium-feature', 'Crea la tua ricetta con AI'); submit.setAttribute('data-premium-kind', 'recipe'); submit.setAttribute('data-premium-description', 'Hai esaurito gli utilizzi AI gratuiti. Le ricette salvate restano disponibili; con Premium puoi crearne di nuove.'); setText(submit, 'Crea la tua ricetta con AI'); } } var imageReady = !!data.immagine; status.className = imageReady ? 'alert alert-success' : 'alert alert-warning'; setText(status, imageReady ? 'Ricetta creata con immagine e salvata. La versione audio e in preparazione.' : "Ricetta salvata, ma l'immagine non e disponibile in questo momento."); var media = renderRecipe(data.recipe, data.id, data.immagine); var recipeId = data.id; var csrf = form.elements.csrf_token.value; window.setTimeout(function () { if (progressModal) progressModal.hide(); result.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 550); requestMedia(recipeId, 'audio', csrf) .then(function (audioData) { media.audio.src = audioData.path; media.audio.hidden = false; media.voiceNote.hidden = false; setText(media.info, imageReady ? 'Immagine e audio disponibili nella scheda salvata.' : 'Audio pronto; immagine non disponibile.'); }) .catch(function () { setText(media.info, imageReady ? "Immagine pronta. Puoi creare l'audio da Le mie ricette." : 'Immagine e audio non disponibili in questo momento.'); }); }) .catch(function (error) { if (progressModal) progressModal.hide(); status.className = 'alert alert-danger'; setText(status, error.message || 'Non e stato possibile creare la ricetta.'); }) .then(function () { submit.disabled = false; generationPending = false; }); }); })();