merge develop: nutrition analysis + screensaver pie charts
This commit is contained in:
@@ -5631,6 +5631,248 @@ body.cooking-mode-active .app-header {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
/* ── Screensaver nutrition panel ── */
|
||||
.screensaver-nutrition {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 90vw;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
.ss-nutr-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
.ss-nutr-title {
|
||||
color: rgba(255,255,255,0.75);
|
||||
font-size: 1.3rem;
|
||||
font-weight: 300;
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
}
|
||||
.ss-nutr-charts {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 36px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
.ss-nutr-chart-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.ss-nutr-chart-label {
|
||||
color: rgba(255,255,255,0.5);
|
||||
font-size: .85rem;
|
||||
text-align: center;
|
||||
}
|
||||
/* 3D animated pie */
|
||||
.ss-pie3d {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
border-radius: 50%;
|
||||
background: var(--pie-bg, conic-gradient(#4ade80 0deg 90deg, #60a5fa 90deg 180deg, #fbbf24 180deg 270deg, #334155 270deg 360deg));
|
||||
transform: perspective(250px) rotateX(40deg) scale(0.82);
|
||||
box-shadow: 0 12px 32px rgba(0,0,0,0.6), 0 4px 0 rgba(255,255,255,0.06);
|
||||
transition: transform 0.8s cubic-bezier(.34,1.56,.64,1), box-shadow 0.8s;
|
||||
will-change: transform;
|
||||
}
|
||||
.ss-pie3d.ss-pie3d-ready {
|
||||
transform: perspective(250px) rotateX(30deg) scale(1);
|
||||
animation: ss-pie-spin 18s linear infinite;
|
||||
}
|
||||
@keyframes ss-pie-spin {
|
||||
from { transform: perspective(250px) rotateX(30deg) rotate(0deg) scale(1); }
|
||||
to { transform: perspective(250px) rotateX(30deg) rotate(360deg) scale(1); }
|
||||
}
|
||||
.ss-nutr-legend {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
min-width: 130px;
|
||||
}
|
||||
.ss-leg-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: rgba(255,255,255,0.65);
|
||||
font-size: .8rem;
|
||||
}
|
||||
.ss-leg-dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ss-leg-pct {
|
||||
margin-left: auto;
|
||||
color: rgba(255,255,255,0.45);
|
||||
}
|
||||
/* Score donut column */
|
||||
.ss-nutr-scores-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
.ss-donut-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.ss-donut-label {
|
||||
color: rgba(255,255,255,0.45);
|
||||
font-size: .75rem;
|
||||
}
|
||||
/* CSS-only ring donut using conic-gradient */
|
||||
.ss-donut-ring {
|
||||
position: relative;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
background: conic-gradient(var(--color, #4ade80) calc(var(--val, 0) * 1%), rgba(255,255,255,0.08) calc(var(--val, 0) * 1%));
|
||||
transform: perspective(120px) rotateX(30deg) scale(0.7);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.5);
|
||||
transition: transform 0.7s cubic-bezier(.34,1.56,.64,1), background 0.9s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.ss-donut-ring.ss-donut-ready {
|
||||
transform: perspective(120px) rotateX(25deg) scale(1);
|
||||
animation: ss-donut-bob 4s ease-in-out infinite;
|
||||
}
|
||||
@keyframes ss-donut-bob {
|
||||
0%,100% { transform: perspective(120px) rotateX(25deg) scale(1) translateY(0); }
|
||||
50% { transform: perspective(120px) rotateX(25deg) scale(1) translateY(-4px); }
|
||||
}
|
||||
.ss-donut-ring::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
.ss-donut-text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: rgba(255,255,255,0.8);
|
||||
font-size: .8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Dashboard nutrition card ── */
|
||||
.nutr-card {
|
||||
background: var(--bg-card, #fff);
|
||||
border-radius: 14px;
|
||||
padding: 16px;
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.nutr-body {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
margin: 12px 0;
|
||||
}
|
||||
.nutr-pie-wrap {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.nutr-pie-3d {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
transform: perspective(180px) rotateX(38deg) scale(0.82);
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.18);
|
||||
transition: transform 0.7s cubic-bezier(.34,1.56,.64,1);
|
||||
will-change: transform;
|
||||
}
|
||||
.nutr-pie-3d.nutr-pie-ready {
|
||||
transform: perspective(180px) rotateX(28deg) scale(1);
|
||||
animation: nutr-pie-spin 22s linear infinite;
|
||||
}
|
||||
@keyframes nutr-pie-spin {
|
||||
from { transform: perspective(180px) rotateX(28deg) rotate(0deg) scale(1); }
|
||||
to { transform: perspective(180px) rotateX(28deg) rotate(360deg) scale(1); }
|
||||
}
|
||||
.nutr-pie-center {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.nutr-pie-total {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
.nutr-pie-label {
|
||||
font-size: .6rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.nutr-legend {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.nutr-leg-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: .78rem;
|
||||
}
|
||||
.nutr-leg-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.nutr-leg-pct {
|
||||
margin-left: auto;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
/* Score bars */
|
||||
.nutr-scores {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.nutr-score-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: .78rem;
|
||||
}
|
||||
.nutr-score-label { flex: 0 0 80px; white-space: nowrap; }
|
||||
.nutr-score-track {
|
||||
flex: 1;
|
||||
height: 7px;
|
||||
border-radius: 4px;
|
||||
background: rgba(0,0,0,0.07);
|
||||
overflow: hidden;
|
||||
}
|
||||
.nutr-score-fill {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
width: 0%;
|
||||
transition: width 1s ease;
|
||||
}
|
||||
.nutr-score-val { flex: 0 0 32px; text-align: right; font-weight: 600; }
|
||||
|
||||
/* ===== SETUP WIZARD ===== */
|
||||
.setup-wizard-content {
|
||||
max-width: 480px;
|
||||
|
||||
+339
-23
@@ -2772,6 +2772,205 @@ function _renderAntiWasteSection(used30, wasted30, usedP30, wastedP30, usedP60,
|
||||
}
|
||||
}
|
||||
|
||||
// ===== NUTRITION ANALYSIS SECTION =====
|
||||
// Alternates with waste-chart-section every hour (randomised offset)
|
||||
|
||||
// Colour palette for pie slices (matches category colours)
|
||||
const _NUTR_COLORS = {
|
||||
'frutta': '#4ade80', 'verdura': '#22d3ee',
|
||||
'carne': '#f87171', 'pesce': '#60a5fa',
|
||||
'latticini': '#fbbf24', 'pasta': '#a78bfa',
|
||||
'pane': '#fb923c', 'cereali': '#f472b6',
|
||||
'bevande': '#34d399', 'condimenti':'#94a3b8',
|
||||
'surgelati': '#818cf8', 'conserve': '#e879f9',
|
||||
'snack': '#fcd34d', 'altro': '#64748b',
|
||||
};
|
||||
|
||||
let _nutriData = null; // cached result from last inventory fetch
|
||||
let _insightFlipTimer = null; // setInterval handle for waste/nutrition alternation
|
||||
|
||||
/**
|
||||
* Compute nutrition-related metrics from the current inventory array.
|
||||
* Returns null if not enough data.
|
||||
*/
|
||||
function _buildNutritionData(inventory) {
|
||||
if (!inventory || inventory.length === 0) return null;
|
||||
|
||||
// Category distribution (product count)
|
||||
const catCounts = {};
|
||||
for (const item of inventory) {
|
||||
const cat = mapToLocalCategory(item.category || '', item.name || '');
|
||||
catCounts[cat] = (catCounts[cat] || 0) + 1;
|
||||
}
|
||||
const total = Object.values(catCounts).reduce((s, v) => s + v, 0);
|
||||
|
||||
// Sorted slices for pie
|
||||
const slices = Object.entries(catCounts)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([cat, count]) => ({
|
||||
cat,
|
||||
count,
|
||||
pct: Math.round(count / total * 100),
|
||||
color: _NUTR_COLORS[cat] || '#64748b',
|
||||
icon: CATEGORY_ICONS[cat] || '📦',
|
||||
}));
|
||||
|
||||
// Health score 0-100 based on category mix
|
||||
// + points for fruit/veg/fish; - for snacks/sweets
|
||||
const healthyCats = ['frutta','verdura','pesce','carne'];
|
||||
const unhealthyCats = ['snack','bevande'];
|
||||
const healthyCount = healthyCats.reduce((s, c) => s + (catCounts[c] || 0), 0);
|
||||
const unhealthyCount= unhealthyCats.reduce((s, c) => s + (catCounts[c] || 0), 0);
|
||||
const healthScore = Math.min(100, Math.max(0,
|
||||
Math.round(50 + (healthyCount / Math.max(total, 1)) * 50 - (unhealthyCount / Math.max(total, 1)) * 30)
|
||||
));
|
||||
|
||||
// Variety score: number of distinct categories / max(16)
|
||||
const varietyScore = Math.min(100, Math.round(Object.keys(catCounts).length / 16 * 100));
|
||||
|
||||
// Freshness score: % products with expiry date set
|
||||
const withExpiry = inventory.filter(i => i.expiry_date).length;
|
||||
const freshnessScore = Math.round(withExpiry / Math.max(total, 1) * 100);
|
||||
|
||||
// Balance: fraction of fresh (frigo+freezer) vs shelf-stable (dispensa)
|
||||
const fresh = inventory.filter(i => i.location === 'frigo' || i.location === 'freezer').length;
|
||||
const fresh_pct = Math.round(fresh / Math.max(total, 1) * 100);
|
||||
|
||||
return { slices, total, healthScore, varietyScore, freshnessScore, fresh_pct };
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the nutrition analysis card into #nutrition-section.
|
||||
*/
|
||||
function _renderNutritionSection(inventory) {
|
||||
const section = document.getElementById('nutrition-section');
|
||||
if (!section) return;
|
||||
const data = _buildNutritionData(inventory);
|
||||
if (!data) { section.style.display = 'none'; return; }
|
||||
_nutriData = data;
|
||||
|
||||
const { slices, total, healthScore, varietyScore, freshnessScore, fresh_pct } = data;
|
||||
const top5 = slices.slice(0, 5);
|
||||
|
||||
// Build conic-gradient for pie
|
||||
let deg = 0;
|
||||
const stops = top5.map(s => {
|
||||
const end = deg + s.pct * 3.6;
|
||||
const stop = `${s.color} ${deg.toFixed(1)}deg ${end.toFixed(1)}deg`;
|
||||
deg = end;
|
||||
return stop;
|
||||
});
|
||||
if (deg < 360) stops.push(`#334155 ${deg.toFixed(1)}deg 360deg`);
|
||||
const gradient = `conic-gradient(from 0deg, ${stops.join(', ')})`;
|
||||
|
||||
// Score colour
|
||||
const scoreColor = healthScore >= 70 ? '#4ade80' : healthScore >= 45 ? '#fbbf24' : '#f87171';
|
||||
const scoreLabel = healthScore >= 70 ? '😄 Ottimo' : healthScore >= 45 ? '🙂 Discreto' : '😬 Migliorabile';
|
||||
|
||||
section.innerHTML = `
|
||||
<div class="nutr-card">
|
||||
<div class="aw-header">
|
||||
<div class="aw-title-row">
|
||||
<span class="aw-live-dot aw-live-on"></span>
|
||||
<h3 class="aw-title">🥗 Analisi Alimentare</h3>
|
||||
</div>
|
||||
<span class="aw-grade" style="background:${scoreColor};font-size:.75rem;padding:4px 10px">${scoreLabel}</span>
|
||||
</div>
|
||||
|
||||
<div class="nutr-body">
|
||||
<!-- 3D animated pie -->
|
||||
<div class="nutr-pie-wrap">
|
||||
<div class="nutr-pie-3d" id="nutr-pie" style="background:${gradient}"></div>
|
||||
<div class="nutr-pie-center">
|
||||
<span class="nutr-pie-total">${total}</span>
|
||||
<span class="nutr-pie-label">prodotti</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="nutr-legend">
|
||||
${top5.map(s => `
|
||||
<div class="nutr-leg-row">
|
||||
<span class="nutr-leg-dot" style="background:${s.color}"></span>
|
||||
<span class="nutr-leg-icon">${s.icon}</span>
|
||||
<span class="nutr-leg-name">${s.cat}</span>
|
||||
<span class="nutr-leg-pct">${s.pct}%</span>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Score bar row -->
|
||||
<div class="nutr-scores">
|
||||
${_nutrScoreBar('🌿 Salute', healthScore, '#4ade80')}
|
||||
${_nutrScoreBar('🎨 Varietà', varietyScore, '#60a5fa')}
|
||||
${_nutrScoreBar('❄️ Freschi', fresh_pct, '#22d3ee')}
|
||||
</div>
|
||||
|
||||
<div class="aw-source">Basato su ${total} prodotti in dispensa · EverShelf</div>
|
||||
</div>`;
|
||||
|
||||
// Trigger pie animation after render
|
||||
requestAnimationFrame(() => {
|
||||
const pie = document.getElementById('nutr-pie');
|
||||
if (pie) setTimeout(() => pie.classList.add('nutr-pie-ready'), 60);
|
||||
});
|
||||
}
|
||||
|
||||
function _nutrScoreBar(label, val, color) {
|
||||
return `<div class="nutr-score-row">
|
||||
<span class="nutr-score-label">${label}</span>
|
||||
<div class="nutr-score-track">
|
||||
<div class="nutr-score-fill" style="width:0%;background:${color}" data-target="${val}"></div>
|
||||
</div>
|
||||
<span class="nutr-score-val">${val}%</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the waste ↔ nutrition alternation on the dashboard.
|
||||
* One is shown, the other hidden; they swap every hour (+ random phase offset).
|
||||
*/
|
||||
let _insightPhase = null; // 'waste' | 'nutrition'
|
||||
|
||||
function _startInsightAlternation(inventory) {
|
||||
clearInterval(_insightFlipTimer);
|
||||
// Pick initial panel based on current minute: even hour → waste, odd → nutrition
|
||||
const isNutritionTurn = Math.floor(Date.now() / 3_600_000) % 2 === 1;
|
||||
_insightPhase = isNutritionTurn ? 'nutrition' : 'waste';
|
||||
_applyInsightPhase();
|
||||
// Flip every hour
|
||||
_insightFlipTimer = setInterval(() => {
|
||||
_insightPhase = _insightPhase === 'waste' ? 'nutrition' : 'waste';
|
||||
_applyInsightPhase();
|
||||
}, 3_600_000);
|
||||
}
|
||||
|
||||
function _applyInsightPhase() {
|
||||
const wasteEl = document.getElementById('waste-chart-section');
|
||||
const nutrEl = document.getElementById('nutrition-section');
|
||||
if (!wasteEl || !nutrEl) return;
|
||||
const showNutr = _insightPhase === 'nutrition' && nutrEl.innerHTML.trim() !== '';
|
||||
const showWaste = _insightPhase === 'waste' && wasteEl.innerHTML.trim() !== '';
|
||||
// Fade-swap
|
||||
[wasteEl, nutrEl].forEach(el => { el.style.opacity = '0'; el.style.transition = 'opacity .6s'; });
|
||||
setTimeout(() => {
|
||||
wasteEl.style.display = showWaste ? 'block' : 'none';
|
||||
nutrEl.style.display = showNutr ? 'block' : 'none';
|
||||
// If neither ready yet, keep waste visible
|
||||
if (!showWaste && !showNutr) wasteEl.style.display = 'block';
|
||||
requestAnimationFrame(() => {
|
||||
wasteEl.style.opacity = '1';
|
||||
nutrEl.style.opacity = '1';
|
||||
// Animate score bars when nutrition becomes visible
|
||||
if (showNutr) {
|
||||
nutrEl.querySelectorAll('.nutr-score-fill').forEach(bar => {
|
||||
bar.style.width = (bar.dataset.target || 0) + '%';
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 620);
|
||||
}
|
||||
|
||||
// ===== DASHBOARD =====
|
||||
async function loadDashboard() {
|
||||
try {
|
||||
@@ -2885,6 +3084,13 @@ async function loadDashboard() {
|
||||
);
|
||||
_startAntiWasteAutoRefresh();
|
||||
|
||||
// Nutrition section — built from the full inventory list
|
||||
try {
|
||||
const invForNutr = (await api('inventory_list')).inventory || [];
|
||||
_renderNutritionSection(invForNutr);
|
||||
} catch(_e) {}
|
||||
_startInsightAlternation();
|
||||
|
||||
// Opened (partially used products with known package capacity)
|
||||
const openedSection = document.getElementById('alert-opened');
|
||||
const openedList = document.getElementById('opened-list');
|
||||
@@ -11388,10 +11594,9 @@ function activateScreensaver() {
|
||||
requestAnimationFrame(() => overlay.classList.add('visible'));
|
||||
updateScreensaverClock();
|
||||
_screensaverClockInterval = setInterval(updateScreensaverClock, 1000);
|
||||
// Load data and start facts
|
||||
// Load data and start fact/nutrition rotation
|
||||
loadScreensaverData().then(() => {
|
||||
showNextScreensaverFact();
|
||||
_screensaverFactInterval = setInterval(showNextScreensaverFact, SCREENSAVER_FACT_DURATION);
|
||||
_startScreensaverRotation();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11427,6 +11632,11 @@ function dismissScreensaver(targetPage) {
|
||||
if (!_screensaverActive) return;
|
||||
clearInterval(_screensaverClockInterval);
|
||||
clearInterval(_screensaverFactInterval);
|
||||
clearInterval(_ssRotationTimer);
|
||||
const nutrEl = document.getElementById('screensaver-nutrition');
|
||||
if (nutrEl) { nutrEl.style.display = 'none'; nutrEl.innerHTML = ''; }
|
||||
const factEl = document.getElementById('screensaver-fact');
|
||||
if (factEl) { factEl.classList.remove('visible'); }
|
||||
const overlay = document.getElementById('screensaver');
|
||||
overlay.classList.remove('visible');
|
||||
setTimeout(() => {
|
||||
@@ -11442,6 +11652,121 @@ function dismissScreensaver(targetPage) {
|
||||
}, 400);
|
||||
}
|
||||
|
||||
// Handle for screensaver rotation timer
|
||||
let _ssRotationTimer = null;
|
||||
let _ssSlot = 0; // 0=fact, 1=nutrition, 2=fact, 3=nutrition …
|
||||
|
||||
/**
|
||||
* Start the screensaver content rotation:
|
||||
* Every SCREENSAVER_FACT_DURATION ms flip between fact text and nutrition charts.
|
||||
*/
|
||||
function _startScreensaverRotation() {
|
||||
clearInterval(_ssRotationTimer);
|
||||
_ssSlot = 0;
|
||||
_showScreensaverSlot(0);
|
||||
_screensaverFactInterval = _ssRotationTimer = setInterval(() => {
|
||||
_ssSlot = (_ssSlot + 1) % 4; // 4 steps: fact, nutr, fact, nutr (with repeats for more facts)
|
||||
_showScreensaverSlot(_ssSlot);
|
||||
}, SCREENSAVER_FACT_DURATION);
|
||||
}
|
||||
|
||||
function _showScreensaverSlot(slot) {
|
||||
const factEl = document.getElementById('screensaver-fact');
|
||||
const nutrEl = document.getElementById('screensaver-nutrition');
|
||||
if (!factEl || !nutrEl) return;
|
||||
const showNutr = slot % 2 === 1; // odd slots = nutrition
|
||||
// Fade out both
|
||||
factEl.classList.remove('visible');
|
||||
nutrEl.style.opacity = '0';
|
||||
nutrEl.style.transition = 'opacity 1.5s ease';
|
||||
setTimeout(() => {
|
||||
if (!_screensaverActive) return;
|
||||
if (showNutr) {
|
||||
factEl.style.display = 'none';
|
||||
nutrEl.style.display = 'flex';
|
||||
_renderScreensaverNutrition();
|
||||
requestAnimationFrame(() => { nutrEl.style.opacity = '1'; });
|
||||
} else {
|
||||
nutrEl.style.display = 'none';
|
||||
factEl.style.display = '';
|
||||
factEl.textContent = generateScreensaverFact();
|
||||
requestAnimationFrame(() => { factEl.classList.add('visible'); });
|
||||
}
|
||||
}, 1600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render animated 3D-style pie charts inside the screensaver.
|
||||
* Shows: category distribution, health score, freshness.
|
||||
*/
|
||||
function _renderScreensaverNutrition() {
|
||||
const el = document.getElementById('screensaver-nutrition');
|
||||
if (!el) return;
|
||||
// Use cached nutrition data from dashboard if available, else build from screensaver inventory
|
||||
const inv = (_screensaverData && _screensaverData.inventory) || [];
|
||||
const data = (_nutriData && _nutriData.slices) ? _nutriData : _buildNutritionData(inv);
|
||||
if (!data) { el.style.display = 'none'; return; }
|
||||
|
||||
const { slices, total, healthScore, varietyScore, freshnessScore, fresh_pct } = data;
|
||||
const top4 = slices.slice(0, 4);
|
||||
|
||||
// Build conic-gradient
|
||||
let deg = 0;
|
||||
const stops = top4.map(s => {
|
||||
const end = deg + s.pct * 3.6;
|
||||
const stop = `${s.color} ${deg.toFixed(1)}deg ${end.toFixed(1)}deg`;
|
||||
deg = end;
|
||||
return stop;
|
||||
});
|
||||
if (deg < 360) stops.push(`rgba(255,255,255,0.08) ${deg.toFixed(1)}deg 360deg`);
|
||||
const gradient = `conic-gradient(from 0deg, ${stops.join(', ')})`;
|
||||
|
||||
// Three mini donut charts: categories, health, freshness
|
||||
const healthColor = healthScore >= 70 ? '#4ade80' : healthScore >= 45 ? '#fbbf24' : '#f87171';
|
||||
const freshColor = freshnessScore >= 70 ? '#22d3ee' : freshnessScore >= 40 ? '#60a5fa' : '#94a3b8';
|
||||
const varColor = varietyScore >= 70 ? '#a78bfa' : varietyScore >= 40 ? '#fbbf24' : '#64748b';
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="ss-nutr-wrap">
|
||||
<div class="ss-nutr-title">🥗 La tua dispensa oggi</div>
|
||||
<div class="ss-nutr-charts">
|
||||
<!-- Main category pie -->
|
||||
<div class="ss-nutr-chart-block">
|
||||
<div class="ss-pie3d" id="ss-pie-main" style="--pie-bg:${gradient}"></div>
|
||||
<div class="ss-nutr-chart-label">${total} prodotti</div>
|
||||
<div class="ss-nutr-legend">
|
||||
${top4.map(s => `<div class="ss-leg-row"><span style="background:${s.color}" class="ss-leg-dot"></span><span>${s.icon} ${s.cat}</span><span class="ss-leg-pct">${s.pct}%</span></div>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Score donuts -->
|
||||
<div class="ss-nutr-scores-col">
|
||||
${_ssDonut('❤️ Salute', healthScore, healthColor)}
|
||||
${_ssDonut('🎨 Varietà', varietyScore, varColor)}
|
||||
${_ssDonut('❄️ Freschi', fresh_pct, freshColor)}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Trigger animations
|
||||
requestAnimationFrame(() => {
|
||||
const pie = document.getElementById('ss-pie-main');
|
||||
if (pie) setTimeout(() => pie.classList.add('ss-pie3d-ready'), 80);
|
||||
el.querySelectorAll('.ss-donut-ring').forEach(ring => {
|
||||
const val = parseInt(ring.dataset.val || 0);
|
||||
setTimeout(() => { ring.style.setProperty('--val', val); ring.classList.add('ss-donut-ready'); }, 200);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _ssDonut(label, val, color) {
|
||||
return `<div class="ss-donut-wrap">
|
||||
<div class="ss-donut-ring" data-val="${val}" style="--color:${color};--val:0">
|
||||
<span class="ss-donut-text">${val}%</span>
|
||||
</div>
|
||||
<div class="ss-donut-label">${label}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Load all data needed for screensaver facts
|
||||
async function loadScreensaverData() {
|
||||
try {
|
||||
@@ -11699,49 +12024,40 @@ function generateScreensaverFact() {
|
||||
}
|
||||
|
||||
// --- Time-of-day greetings & suggestions ---
|
||||
facts.push(() => `${greeting}! Se vuoi che ti preparo una ricetta, tocca qui.`);
|
||||
facts.push(() => `${greeting}! La tua dispensa è sotto controllo. 😊`);
|
||||
if (hour >= 6 && hour < 10) {
|
||||
facts.push(() => `Buongiorno! Pronto per la colazione? ☕`);
|
||||
if (byCategory['pane']) facts.push(() => `Buongiorno! Hai del pane per la colazione. 🍞`);
|
||||
if (byCategory['latticini']) facts.push(() => `C'è del latte in frigo per il cappuccino? ☕🥛`);
|
||||
if (byCategory['frutta']) facts.push(() => `Buongiorno! Una bella frutta fresca per iniziare bene. 🍎`);
|
||||
}
|
||||
if (hour >= 11 && hour < 14) {
|
||||
facts.push(() => `È quasi ora di pranzo! Cosa cuciniamo? 🍽️`);
|
||||
if (byCategory['pasta']) facts.push(() => `Ora di pranzo… Un bel piatto di pasta? 🍝`);
|
||||
if (byCategory['verdura']) facts.push(() => `Un'insalata fresca per pranzo? Hai ${byCategory['verdura'].length} verdure! 🥗`);
|
||||
}
|
||||
if (hour >= 17 && hour < 21) {
|
||||
facts.push(() => `Buona sera! Hai pensato alla cena? 🍽️`);
|
||||
if (byCategory['carne']) facts.push(() => `Per cena potresti usare la carne che hai. 🥩`);
|
||||
if (byCategory['pesce']) facts.push(() => `Che ne dici di pesce per cena? 🐟`);
|
||||
if (expiringThisWeek.length > 0) facts.push(() => `Hai ${expiringThisWeek.length} prodotti in scadenza questa settimana — usali stasera!`);
|
||||
}
|
||||
if (hour >= 21 || hour < 6) {
|
||||
facts.push(() => `Buonanotte! Domani controlla le scadenze. 🌙`);
|
||||
if (expiringSoon.length > 0) facts.push(() => `Buonanotte! Domani ricordati di usare: ${expiringSoon.slice(0,2).map(i=>i.name).join(', ')}.`);
|
||||
}
|
||||
|
||||
// --- Weekly stats ---
|
||||
const recentIn = stats.recent_in || 0;
|
||||
const recentOut = stats.recent_out || 0;
|
||||
if (recentIn > 0) {
|
||||
facts.push(() => `Questa settimana hai aggiunto ${recentIn} prodotti.`);
|
||||
}
|
||||
if (recentOut > 0) {
|
||||
facts.push(() => `Questa settimana hai consumato ${recentOut} prodotti.`);
|
||||
}
|
||||
if (recentIn > 0 && recentOut > 0) {
|
||||
facts.push(() => `Bilancio settimanale: +${recentIn} entrati, -${recentOut} usciti.`);
|
||||
facts.push(() => `Bilancio settimana: +${recentIn} aggiunti, −${recentOut} consumati.`);
|
||||
} else if (recentIn > 0) {
|
||||
facts.push(() => `Questa settimana hai aggiunto ${recentIn} prodotti.`);
|
||||
} else if (recentOut > 0) {
|
||||
facts.push(() => `Questa settimana hai consumato ${recentOut} prodotti. Ottimo!`);
|
||||
}
|
||||
|
||||
// --- Tips & curiosità (statici ma ruotano) ---
|
||||
facts.push(() => `💡 Lo sapevi? I prodotti in freezer durano molto più a lungo della data di scadenza.`);
|
||||
facts.push(() => `💡 I prodotti in freezer durano molto più a lungo della data di scadenza.`);
|
||||
facts.push(() => `💡 Il pane congelato mantiene la fragranza per settimane.`);
|
||||
facts.push(() => `💡 Le uova si conservano fino a 3-4 settimane dopo la data preferita.`);
|
||||
facts.push(() => `💡 Lo yogurt chiuso in frigo dura spesso 1-2 settimane oltre la scadenza.`);
|
||||
facts.push(() => `💡 Per evitare sprechi, usa prima i prodotti con scadenza più vicina.`);
|
||||
facts.push(() => `💡 Per evitare sprechi, usa prima i prodotti con scadenza più vicina (FIFO).`);
|
||||
facts.push(() => `💡 La carne in freezer può durare fino a 6 mesi senza problemi.`);
|
||||
facts.push(() => `💡 Le verdure fresche durano di più se conservate nel cassetto del frigo.`);
|
||||
facts.push(() => `💡 Controlla regolarmente la dispensa per evitare doppioni nella spesa.`);
|
||||
facts.push(() => `💡 I latticini vanno conservati nella parte più fredda del frigo.`);
|
||||
facts.push(() => `💡 Non ricongelare mai un alimento già scongelato. Cucinalo subito!`);
|
||||
facts.push(() => `💡 Un frigo ordinato ti fa risparmiare tempo e denaro.`);
|
||||
facts.push(() => `💡 Le conserve aperte vanno in frigo e consumate in pochi giorni.`);
|
||||
|
||||
+8
-3
@@ -11,7 +11,7 @@
|
||||
<title>EverShelf</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="icon" type="image/png" href="assets/img/logo/logo_icon.png">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=20260506b">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=20260506c">
|
||||
<!-- QuaggaJS for barcode scanning -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
|
||||
<!-- @xenova/transformers: ES-module bootstrap that exposes a lazy category-classifier as window._categoryPipelinePromise -->
|
||||
@@ -152,8 +152,11 @@
|
||||
<div id="expired-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Anti-Waste Report Card (content fully rendered by JS) -->
|
||||
<div id="waste-chart-section" style="display:none"></div>
|
||||
<!-- Anti-Waste Report Card + Nutrition Analysis (alternating, content rendered by JS) -->
|
||||
<div id="dashboard-insight-wrap" style="position:relative">
|
||||
<div id="waste-chart-section" style="display:none"></div>
|
||||
<div id="nutrition-section" style="display:none"></div>
|
||||
</div>
|
||||
|
||||
<!-- Alert for soonest expiring items -->
|
||||
<div class="alert-section" id="alert-expiring" style="display:none">
|
||||
@@ -1289,6 +1292,8 @@
|
||||
<div class="screensaver-content">
|
||||
<div class="screensaver-clock" id="screensaver-clock"></div>
|
||||
<div id="screensaver-mealplan" class="screensaver-mealplan" style="display:none"></div>
|
||||
<!-- Nutrition pie charts panel (shown alternately with fact) -->
|
||||
<div id="screensaver-nutrition" class="screensaver-nutrition" style="display:none"></div>
|
||||
<div class="screensaver-fact" id="screensaver-fact"></div>
|
||||
</div>
|
||||
<div class="screensaver-shortcuts">
|
||||
|
||||
Reference in New Issue
Block a user