Anti-waste: single-row compare bar, trend cards with arrows, rotating food facts
This commit is contained in:
+110
-114
@@ -385,12 +385,12 @@ body {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Header row: title-row + grade */
|
||||
/* Header row */
|
||||
.aw-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
.aw-title-row {
|
||||
display: flex;
|
||||
@@ -398,7 +398,7 @@ body {
|
||||
gap: 6px;
|
||||
}
|
||||
.aw-title {
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
@@ -413,7 +413,6 @@ body {
|
||||
}
|
||||
.aw-live-on {
|
||||
background: var(--success);
|
||||
box-shadow: 0 0 0 0 var(--success);
|
||||
animation: aw-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
.aw-live-off { background: #9ca3af; }
|
||||
@@ -423,27 +422,17 @@ body {
|
||||
100% { box-shadow: 0 0 0 0 rgba(34,197,94,0); }
|
||||
}
|
||||
|
||||
.aw-grade-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.aw-grade-label {
|
||||
font-size: 0.68rem;
|
||||
color: var(--text-light);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
/* Grade badge */
|
||||
.aw-grade {
|
||||
display: inline-block;
|
||||
min-width: 32px;
|
||||
min-width: 30px;
|
||||
text-align: center;
|
||||
padding: 2px 7px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
cursor: default;
|
||||
}
|
||||
.aw-grade-ap { background: #16a34a; }
|
||||
.aw-grade-a { background: #22c55e; }
|
||||
@@ -451,56 +440,55 @@ body {
|
||||
.aw-grade-c { background: #fb923c; }
|
||||
.aw-grade-d { background: #dc2626; }
|
||||
|
||||
/* Comparison bars */
|
||||
.aw-compare { display: flex; flex-direction: column; gap: 5px; margin-bottom: 8px; }
|
||||
.aw-compare-row { display: flex; align-items: center; gap: 6px; }
|
||||
.aw-compare-lbl {
|
||||
min-width: 70px;
|
||||
/* ── Single-row comparison bar ──────────────────────────── */
|
||||
.aw-cmp-wrap { margin-bottom: 7px; }
|
||||
.aw-cmp-row-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-light);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.aw-you-lbl { color: var(--success); font-weight: 700; }
|
||||
.aw-bar-track {
|
||||
flex: 1;
|
||||
height: 9px;
|
||||
border-radius: 5px;
|
||||
.aw-cmp-lbl-you { color: var(--success); }
|
||||
.aw-cmp-lbl-you strong { font-size: 0.8rem; }
|
||||
.aw-cmp-lbl-avg { color: var(--text-light); text-align: right; }
|
||||
.aw-cmp-lbl-avg strong { color: var(--text); font-size: 0.8rem; }
|
||||
.aw-cmp-track {
|
||||
position: relative;
|
||||
height: 8px;
|
||||
background: var(--bg-main);
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.aw-bar-you {
|
||||
height: 100%;
|
||||
.aw-cmp-you-fill {
|
||||
position: absolute;
|
||||
left: 0; top: 0; bottom: 0;
|
||||
background: var(--success);
|
||||
border-radius: 5px;
|
||||
transition: width 0.6s cubic-bezier(.4,0,.2,1);
|
||||
min-width: 2px;
|
||||
border-radius: 4px;
|
||||
transition: width 0.7s cubic-bezier(.4,0,.2,1);
|
||||
min-width: 3px;
|
||||
}
|
||||
.aw-bar-avg {
|
||||
height: 100%;
|
||||
background: #d1d5db;
|
||||
border-radius: 5px;
|
||||
transition: width 0.6s cubic-bezier(.4,0,.2,1);
|
||||
min-width: 2px;
|
||||
.aw-cmp-avg-tick {
|
||||
position: absolute;
|
||||
top: -2px; bottom: -2px;
|
||||
width: 3px;
|
||||
background: #f59e0b;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 0 2px var(--bg-card);
|
||||
transform: translateX(-50%);
|
||||
transition: left 0.7s cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
.aw-compare-pct { font-size: 0.74rem; font-weight: 700; color: var(--text-light); min-width: 28px; text-align: right; }
|
||||
.aw-you-pct { color: var(--success); }
|
||||
|
||||
/* Status message */
|
||||
.aw-status {
|
||||
font-size: 0.76rem;
|
||||
/* Inline status below bar */
|
||||
.aw-status-inline {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
margin-bottom: 7px;
|
||||
border-left: 3px solid transparent;
|
||||
margin: 0 0 6px;
|
||||
padding: 0;
|
||||
}
|
||||
.aw-status-good { background: #f0fdf4; border-color: var(--success); color: #166534; }
|
||||
.aw-status-bad { background: #fef2f2; border-color: var(--danger); color: #991b1b; }
|
||||
.aw-status-ok { background: #fffbeb; border-color: var(--warning); color: #92400e; }
|
||||
.aw-status-good { color: #16a34a; }
|
||||
.aw-status-bad { color: #dc2626; }
|
||||
.aw-status-ok { color: #b45309; }
|
||||
|
||||
/* Savings badges */
|
||||
/* ── Savings badges ─────────────────────────────────────── */
|
||||
.aw-savings-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -511,9 +499,9 @@ body {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 8px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.72rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
@@ -521,73 +509,81 @@ body {
|
||||
.aw-badge-meals { background: #f0fdf4; color: #166534; border-color: #86efac; }
|
||||
.aw-badge-co2 { background: #eff6ff; color: #1e40af; border-color: #93c5fd; }
|
||||
|
||||
/* Trend mini chart */
|
||||
.aw-trend-section { margin-top: 2px; }
|
||||
.aw-trend-title {
|
||||
/* ── Trend mini-cards ───────────────────────────────────── */
|
||||
.aw-trend-cards {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
.aw-tcard {
|
||||
flex: 1;
|
||||
padding: 5px 4px 4px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.aw-tcard-good { background: #f0fdf4; border-color: #86efac; }
|
||||
.aw-tcard-ok { background: #fffbeb; border-color: #fde68a; }
|
||||
.aw-tcard-bad { background: #fef2f2; border-color: #fca5a5; }
|
||||
.aw-tcard-empty { background: var(--bg-main); border-color: var(--border); opacity: 0.45; }
|
||||
.aw-tc-label {
|
||||
display: block;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.57rem;
|
||||
color: var(--text-light);
|
||||
margin-bottom: 1px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.aw-tc-rate {
|
||||
display: block;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.aw-trend-bars {
|
||||
.aw-tcard-good .aw-tc-rate { color: #16a34a; }
|
||||
.aw-tcard-ok .aw-tc-rate { color: #b45309; }
|
||||
.aw-tcard-bad .aw-tc-rate { color: #dc2626; }
|
||||
.aw-tcard-empty .aw-tc-rate { color: var(--text-light); }
|
||||
.aw-tc-minibar { height: 3px; background: rgba(0,0,0,.08); border-radius: 2px; overflow: hidden; }
|
||||
.aw-tc-minibar > div { height: 100%; border-radius: 2px; }
|
||||
.aw-tcard-good .aw-tc-minibar > div { background: var(--success); }
|
||||
.aw-tcard-ok .aw-tc-minibar > div { background: #f59e0b; }
|
||||
.aw-tcard-bad .aw-tc-minibar > div { background: var(--danger); }
|
||||
.aw-tcard-empty .aw-tc-minibar > div { background: #d1d5db; }
|
||||
.aw-tc-arrow { font-size: 0.9rem; flex-shrink: 0; padding: 0 1px; }
|
||||
.aw-arrow-good { color: var(--success); }
|
||||
.aw-arrow-bad { color: var(--danger); }
|
||||
.aw-arrow-ok { color: var(--text-light); }
|
||||
|
||||
/* ── Rotating food-fact box ─────────────────────────────── */
|
||||
.aw-fact-rotator {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-end;
|
||||
height: 44px;
|
||||
align-items: flex-start;
|
||||
gap: 5px;
|
||||
padding: 5px 8px;
|
||||
background: linear-gradient(135deg, #f0f9ff, #e0f2fe);
|
||||
border-radius: 7px;
|
||||
border-left: 2px solid #38bdf8;
|
||||
margin-bottom: 5px;
|
||||
min-height: 26px;
|
||||
}
|
||||
.aw-trend-col {
|
||||
.aw-fact-icon { font-size: 0.76rem; flex-shrink: 0; padding-top: 1px; }
|
||||
.aw-fact-text {
|
||||
font-size: 0.7rem;
|
||||
color: #0369a1;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
.aw-trend-col.aw-trend-empty { opacity: 0.35; }
|
||||
.aw-trend-rate {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
.aw-rate-good { color: var(--success); }
|
||||
.aw-rate-ok { color: var(--warning); }
|
||||
.aw-rate-bad { color: var(--danger); }
|
||||
.aw-trend-bar-wrap {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
max-height: 24px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-main);
|
||||
min-height: 4px;
|
||||
}
|
||||
.aw-trend-bar-fill {
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
transition: height 0.5s ease;
|
||||
}
|
||||
.aw-tbar-good { background: var(--success); }
|
||||
.aw-tbar-ok { background: var(--warning); }
|
||||
.aw-tbar-bad { background: var(--danger); }
|
||||
.aw-trend-label {
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-light);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
.aw-fact-fade { opacity: 0; }
|
||||
|
||||
/* Source footnote */
|
||||
.aw-source {
|
||||
font-size: 0.6rem;
|
||||
font-size: 0.58rem;
|
||||
color: #9ca3af;
|
||||
margin-top: 5px;
|
||||
margin-top: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
+118
-54
@@ -2085,8 +2085,41 @@ const WASTE_BENCHMARKS = {
|
||||
de: { avgWasteRate: 20, avgKgMonth: 6.5, costPerKg: 7.7, currency: '€', countryKey: 'antiwaste.country_de' },
|
||||
en: { avgWasteRate: 30, avgKgMonth: 9.2, costPerKg: 8.5, currency: '$', countryKey: 'antiwaste.country_en' },
|
||||
};
|
||||
const _AW_KG_PER_EVENT = 0.5; // estimated avg kg per out/waste transaction
|
||||
const _AW_KG_PER_EVENT = 0.5;
|
||||
let _awRefreshTimer = null;
|
||||
let _awFactTimer = null;
|
||||
|
||||
// Food facts per language — displayed in rotating carousel inside the card
|
||||
const AW_FACTS = {
|
||||
it: [
|
||||
"In Italia ogni famiglia spreca in media 5,2 kg di cibo al mese",
|
||||
"Il 30% del cibo prodotto nel mondo viene sprecato ogni anno",
|
||||
"Sprecare 1 kg di manzo genera ~27 kg di CO₂ nell'atmosfera",
|
||||
"Frutta e verdura sono gli alimenti più sprecati nelle famiglie italiane",
|
||||
"Un italiano spende in media ~€450/anno in cibo mai consumato",
|
||||
"L'8% delle emissioni mondiali di gas serra proviene dallo spreco alimentare",
|
||||
"Ridurre lo spreco del 50% potrebbe sfamare 800 milioni di persone in più",
|
||||
"1 kg di pane sprecato = 1.300 litri d'acqua consumati inutilmente",
|
||||
],
|
||||
de: [
|
||||
"Deutsche Haushalte werfen pro Person rund 82 kg Lebensmittel pro Jahr weg",
|
||||
"Weltweit endet etwa 30% aller produzierten Lebensmittel als Abfall",
|
||||
"Lebensmittelverschwendung verursacht 8% der globalen Treibhausgase",
|
||||
"Obst und Gemüse werden in deutschen Haushalten am häufigsten weggeworfen",
|
||||
"Ein Deutscher zahlt im Schnitt ~€300/Jahr für ungenutzte Lebensmittel",
|
||||
"1 kg verschwendetes Rindfleisch ≈ 27 kg CO₂-Emissionen",
|
||||
"Halbierung der Lebensmittelverschwendung könnte 800 Mio. Menschen ernähren",
|
||||
],
|
||||
en: [
|
||||
"30–40% of the US food supply is wasted each year (USDA 2021)",
|
||||
"Food waste accounts for 8% of global greenhouse gas emissions",
|
||||
"Americans spend ~$1,800/year on food they never eat",
|
||||
"Fruits & vegetables are the most wasted food items worldwide",
|
||||
"Wasting 1 lb of beef emits as much CO₂ as driving 20 miles",
|
||||
"Halving food waste globally could feed 800 million more people",
|
||||
"If food waste were a country, it'd be the world's 3rd largest emitter",
|
||||
],
|
||||
};
|
||||
|
||||
/** Fetch fresh stats and re-render the anti-waste section. */
|
||||
function _awFetchAndRender() {
|
||||
@@ -2112,9 +2145,34 @@ function _updateAwLiveDot(online) {
|
||||
/** Start/stop the 60-second auto-refresh based on connectivity. */
|
||||
function _startAntiWasteAutoRefresh() {
|
||||
clearInterval(_awRefreshTimer);
|
||||
if (navigator.onLine) {
|
||||
_awRefreshTimer = setInterval(_awFetchAndRender, 60_000);
|
||||
if (navigator.onLine) _awRefreshTimer = setInterval(_awFetchAndRender, 60_000);
|
||||
}
|
||||
|
||||
/** Build one trend mini-card. */
|
||||
function _awTrendCard(rate, label, maxRate) {
|
||||
if (rate === null) {
|
||||
return `<div class="aw-tcard aw-tcard-empty">
|
||||
<span class="aw-tc-label">${label}</span>
|
||||
<span class="aw-tc-rate">–</span>
|
||||
<div class="aw-tc-minibar"><div style="width:0"></div></div>
|
||||
</div>`;
|
||||
}
|
||||
const cls = rate <= 8 ? 'good' : rate <= 20 ? 'ok' : 'bad';
|
||||
const barPct = Math.max(4, Math.round((rate / Math.max(maxRate, 5)) * 100));
|
||||
return `<div class="aw-tcard aw-tcard-${cls}">
|
||||
<span class="aw-tc-label">${label}</span>
|
||||
<span class="aw-tc-rate">${rate}%</span>
|
||||
<div class="aw-tc-minibar"><div style="width:${barPct}%"></div></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/** Arrow between two consecutive trend values. */
|
||||
function _awTrendArrow(prev, curr) {
|
||||
if (prev === null || curr === null) return null;
|
||||
const d = curr - prev;
|
||||
if (d <= -3) return { sym: '↓', cls: 'aw-arrow-good' };
|
||||
if (d >= 3) return { sym: '↑', cls: 'aw-arrow-bad' };
|
||||
return { sym: '→', cls: 'aw-arrow-ok' };
|
||||
}
|
||||
|
||||
function _renderAntiWasteSection(used30, wasted30, usedP30, wastedP30, usedP60, wastedP60, isOnline = navigator.onLine) {
|
||||
@@ -2125,8 +2183,6 @@ function _renderAntiWasteSection(used30, wasted30, usedP30, wastedP30, usedP60,
|
||||
|
||||
const bm = WASTE_BENCHMARKS[_currentLang] || WASTE_BENCHMARKS['it'];
|
||||
const country = t(bm.countryKey);
|
||||
|
||||
// Waste rates (0–100 %)
|
||||
const myRate = Math.round((wasted30 / total30) * 100);
|
||||
const avgRate = bm.avgWasteRate;
|
||||
|
||||
@@ -2138,15 +2194,15 @@ function _renderAntiWasteSection(used30, wasted30, usedP30, wastedP30, usedP60,
|
||||
else if (myRate <= 25) { grade = 'C'; gradeClass = 'c'; }
|
||||
else { grade = 'D'; gradeClass = 'd'; }
|
||||
|
||||
// Estimated savings vs average person
|
||||
// Savings vs average
|
||||
const avgWastedEvents = total30 * (avgRate / 100);
|
||||
const savedEvents = Math.max(0, avgWastedEvents - wasted30);
|
||||
const savedKg = +(savedEvents * _AW_KG_PER_EVENT).toFixed(1);
|
||||
const savedMoney = Math.round(savedKg * bm.costPerKg);
|
||||
const savedMeals = Math.round(savedKg * 2.5); // ~400 g per meal
|
||||
const savedCO2 = +(savedKg * 2.5).toFixed(1); // ~2.5 kg CO2 / kg food wasted
|
||||
const savedMeals = Math.round(savedKg * 2.5);
|
||||
const savedCO2 = +(savedKg * 2.5).toFixed(1);
|
||||
|
||||
// Status message
|
||||
// Status
|
||||
let statusMsg, statusCls;
|
||||
if (myRate < avgRate) {
|
||||
statusMsg = t('antiwaste.better').replace('{country}', country).replace('{diff}', avgRate - myRate);
|
||||
@@ -2159,44 +2215,35 @@ function _renderAntiWasteSection(used30, wasted30, usedP30, wastedP30, usedP60,
|
||||
statusCls = 'aw-status-ok';
|
||||
}
|
||||
|
||||
// Comparison bars (scaled to max of the two rates, min 5 for visual)
|
||||
const maxRate = Math.max(myRate, avgRate, 5);
|
||||
const myBarPct = Math.round((myRate / maxRate) * 100);
|
||||
const avgBarPct = Math.round((avgRate / maxRate) * 100);
|
||||
// Single-row compare bar: both values scaled relative to max+30% headroom
|
||||
const scale = Math.max(myRate, avgRate, 5) * 1.35;
|
||||
const youFillPct = +((myRate / scale) * 100).toFixed(1);
|
||||
const avgTickPct = +((avgRate / scale) * 100).toFixed(1);
|
||||
|
||||
// Trend (3 monthly buckets: m2=oldest … m0=current)
|
||||
// Trend cards
|
||||
const totals = [usedP60 + wastedP60, usedP30 + wastedP30, total30];
|
||||
const rates60 = totals.map((tot, i) => {
|
||||
const rates = totals.map((tot, i) => {
|
||||
const w = [wastedP60, wastedP30, wasted30][i];
|
||||
return tot > 0 ? Math.round((w / tot) * 100) : null;
|
||||
});
|
||||
const trendLabels = [t('antiwaste.months_ago_2'), t('antiwaste.months_ago_1'), t('antiwaste.this_month')];
|
||||
const maxTrend = Math.max(...rates60.filter(r => r !== null), 5);
|
||||
const hasTrendData = rates60[0] !== null || rates60[1] !== null;
|
||||
const labels = [t('antiwaste.months_ago_2'), t('antiwaste.months_ago_1'), t('antiwaste.this_month')];
|
||||
const maxTrend = Math.max(...rates.filter(r => r !== null), 5);
|
||||
const hasTrend = rates[0] !== null || rates[1] !== null;
|
||||
|
||||
const trendBars = rates60.map((rate, i) => {
|
||||
if (rate === null) {
|
||||
return `<div class="aw-trend-col aw-trend-empty">
|
||||
<span class="aw-trend-rate">–</span>
|
||||
<div class="aw-trend-bar-wrap"><div class="aw-trend-bar-fill" style="height:2px"></div></div>
|
||||
<span class="aw-trend-label">${trendLabels[i]}</span>
|
||||
</div>`;
|
||||
}
|
||||
const hPct = Math.max(6, Math.round((rate / maxTrend) * 100));
|
||||
const cls = rate <= 8 ? 'good' : rate <= 20 ? 'ok' : 'bad';
|
||||
return `<div class="aw-trend-col">
|
||||
<span class="aw-trend-rate aw-rate-${cls}">${rate}%</span>
|
||||
<div class="aw-trend-bar-wrap"><div class="aw-trend-bar-fill aw-tbar-${cls}" style="height:${hPct}%"></div></div>
|
||||
<span class="aw-trend-label">${trendLabels[i]}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
const arr1 = _awTrendArrow(rates[0], rates[1]);
|
||||
const arr2 = _awTrendArrow(rates[1], rates[2]);
|
||||
const arrowHtml = a => a ? `<span class="aw-tc-arrow ${a.cls}">${a.sym}</span>` : '';
|
||||
|
||||
// Savings badges — inline, compact
|
||||
// Badges
|
||||
const badges = [];
|
||||
if (savedMoney > 0) badges.push(`<span class="aw-badge aw-badge-money">💰 ${bm.currency}${savedMoney}</span>`);
|
||||
if (savedMeals > 0) badges.push(`<span class="aw-badge aw-badge-meals">🥗 ${savedMeals} ${t('antiwaste.meals')}</span>`);
|
||||
if (savedCO2 > 0) badges.push(`<span class="aw-badge aw-badge-co2">🌍 −${savedCO2} kg CO₂</span>`);
|
||||
|
||||
// Random starting fact
|
||||
const facts = AW_FACTS[_currentLang] || AW_FACTS['it'];
|
||||
const factIdx = Math.floor(Math.random() * facts.length);
|
||||
|
||||
const liveCls = isOnline ? 'aw-live-on' : 'aw-live-off';
|
||||
const liveTip = isOnline ? t('antiwaste.live_on') : t('antiwaste.live_off');
|
||||
|
||||
@@ -2206,37 +2253,54 @@ function _renderAntiWasteSection(used30, wasted30, usedP30, wastedP30, usedP60,
|
||||
<span class="aw-live-dot ${liveCls}" title="${liveTip}"></span>
|
||||
<h3 class="aw-title">${t('antiwaste.title')}</h3>
|
||||
</div>
|
||||
<div class="aw-grade-wrap">
|
||||
<span class="aw-grade-label">${t('antiwaste.grade_label')}</span>
|
||||
<span class="aw-grade aw-grade-${gradeClass}">${grade}</span>
|
||||
</div>
|
||||
<span class="aw-grade aw-grade-${gradeClass}" title="${t('antiwaste.grade_label')}">${grade}</span>
|
||||
</div>
|
||||
|
||||
<div class="aw-compare">
|
||||
<div class="aw-compare-row">
|
||||
<span class="aw-compare-lbl aw-you-lbl">${t('antiwaste.you')}</span>
|
||||
<div class="aw-bar-track"><div class="aw-bar-you" style="width:${myBarPct}%"></div></div>
|
||||
<span class="aw-compare-pct aw-you-pct">${myRate}%</span>
|
||||
<div class="aw-cmp-wrap">
|
||||
<div class="aw-cmp-row-labels">
|
||||
<span class="aw-cmp-lbl-you">${t('antiwaste.you')} <strong>${myRate}%</strong></span>
|
||||
<span class="aw-cmp-lbl-avg"><strong>${avgRate}%</strong> ${country}</span>
|
||||
</div>
|
||||
<div class="aw-compare-row">
|
||||
<span class="aw-compare-lbl">${country}</span>
|
||||
<div class="aw-bar-track"><div class="aw-bar-avg" style="width:${avgBarPct}%"></div></div>
|
||||
<span class="aw-compare-pct">${avgRate}%</span>
|
||||
<div class="aw-cmp-track">
|
||||
<div class="aw-cmp-you-fill" style="width:${youFillPct}%"></div>
|
||||
<div class="aw-cmp-avg-tick" style="left:${avgTickPct}%"></div>
|
||||
</div>
|
||||
<p class="aw-status-inline ${statusCls}">${statusMsg}</p>
|
||||
</div>
|
||||
|
||||
<div class="aw-status ${statusCls}">${statusMsg}</div>
|
||||
|
||||
${badges.length > 0 ? `<div class="aw-savings-row">${badges.join('')}</div>` : ''}
|
||||
|
||||
${hasTrendData ? `
|
||||
<div class="aw-trend-section">
|
||||
<span class="aw-trend-title">${t('antiwaste.trend_title')}</span>
|
||||
<div class="aw-trend-bars">${trendBars}</div>
|
||||
${hasTrend ? `<div class="aw-trend-cards">
|
||||
${_awTrendCard(rates[0], labels[0], maxTrend)}
|
||||
${arrowHtml(arr1)}
|
||||
${_awTrendCard(rates[1], labels[1], maxTrend)}
|
||||
${arrowHtml(arr2)}
|
||||
${_awTrendCard(rates[2], labels[2], maxTrend)}
|
||||
</div>` : ''}
|
||||
|
||||
<div class="aw-fact-rotator">
|
||||
<span class="aw-fact-icon">💡</span>
|
||||
<span id="aw-fact-text" class="aw-fact-text">${facts[factIdx]}</span>
|
||||
</div>
|
||||
|
||||
<div class="aw-source">${t('antiwaste.source')}</div>
|
||||
`;
|
||||
|
||||
// Rotate facts every 6 s with CSS fade
|
||||
if (_awFactTimer) clearInterval(_awFactTimer);
|
||||
if (facts.length > 1) {
|
||||
let idx = factIdx;
|
||||
_awFactTimer = setInterval(() => {
|
||||
const el = document.getElementById('aw-fact-text');
|
||||
if (!el) { clearInterval(_awFactTimer); return; }
|
||||
el.classList.add('aw-fact-fade');
|
||||
setTimeout(() => {
|
||||
idx = (idx + 1) % facts.length;
|
||||
el.textContent = facts[idx];
|
||||
el.classList.remove('aw-fact-fade');
|
||||
}, 420);
|
||||
}, 6000);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== DASHBOARD =====
|
||||
|
||||
Reference in New Issue
Block a user