diff --git a/assets/css/style.css b/assets/css/style.css index 3458c43..92380a9 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -780,6 +780,86 @@ body.header-tone-light .app-header .tab.active { } } +/* TV dashboard: dark room mode (wall display) */ +body.calendar-tv-dark { + color-scheme: dark; + background-color: #0f172a; + color: #e2e8f0; + --text-color: #e2e8f0; + --border-color: #334155; + --secondary-color: #1e293b; +} + +body.calendar-tv-dark main { + background-color: #0f172a; +} + +body.calendar-tv-dark .calendar-tv-view h2, +body.calendar-tv-dark .calendar-tv-view h3 { + color: #f1f5f9; +} + +body.calendar-tv-dark .calendar-tv-view .text-muted { + color: #94a3b8 !important; +} + +body.calendar-tv-dark .calendar-tv-view .calendar-embed { + background-color: #1e293b; + border-color: #334155 !important; +} + +body.calendar-tv-dark .calendar-tv-view .calendar-embed.bg-white { + background-color: #1e293b !important; +} + +body.calendar-tv-dark .calendar-tv-view .list-group-item { + background-color: rgba(30, 41, 59, 0.75); + color: #e2e8f0; + border-color: #334155 !important; +} + +body.calendar-tv-dark .calendar-tv-view .list-group-item-action:hover, +body.calendar-tv-dark .calendar-tv-view .list-group-item-action:focus { + background-color: rgba(51, 65, 85, 0.65); + color: #f8fafc; +} + +body.calendar-tv-dark .calendar-tv-view .calendar-agenda-today { + background-color: color-mix(in srgb, var(--fh-primary, #4a90e2) 24%, #1e293b); + border-color: var(--fh-primary, #60a5fa) !important; +} + +body.calendar-tv-dark .calendar-tv-view .calendar-agenda-today .text-primary { + color: var(--fh-primary, #93c5fd) !important; +} + +body.calendar-tv-dark .calendar-tv-view .text-primary { + color: var(--fh-primary, #93c5fd) !important; +} + +body.calendar-tv-dark .calendar-tv-view .btn-outline-secondary { + color: #cbd5e1; + border-color: #64748b; +} + +body.calendar-tv-dark .calendar-tv-view .btn-outline-secondary:hover, +body.calendar-tv-dark .calendar-tv-view .btn-outline-secondary:focus { + background-color: #334155; + border-color: #94a3b8; + color: #f8fafc; +} + +body.calendar-tv-dark .calendar-tv-view code { + background-color: #334155; + color: #e2e8f0; +} + +body.calendar-tv-dark .calendar-tv-view .badge.bg-secondary { + background-color: #475569 !important; + color: #f1f5f9; +} + + /* Meal detail (recipe) page: sections and flow */ .meal-detail-header { border-bottom: 1px solid var(--border-color); diff --git a/assets/js/main.js b/assets/js/main.js index 4c6889c..8eb4c36 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -8,6 +8,30 @@ var apiBase = typeof window.familyHubApiBase === 'string' ? window.familyHubApiBase : '/api'; + /** + * Avoids JSON.parse on empty bodies (Firefox: "unexpected end of data at line 1 column 1") + * and surfaces clearer errors when PHP returns HTML or nothing. + */ + function parseJsonResponseText(res, text) { + var trimmed = (text || '').trim(); + if (trimmed === '') { + var emptyErr = new Error( + !res.ok ? (res.statusText || 'Request failed') : 'Empty response from server.' + ); + emptyErr.payload = {}; + emptyErr.status = res.status; + throw emptyErr; + } + try { + return JSON.parse(trimmed); + } catch (e) { + var parseErr = new Error('Server did not return JSON. Check server logs or network tab.'); + parseErr.payload = trimmed.slice(0, 500); + parseErr.status = res.status; + throw parseErr; + } + } + function postJson(path, body) { return fetch(apiBase + path, { method: 'POST', @@ -15,7 +39,8 @@ credentials: 'same-origin', body: JSON.stringify(body || {}) }).then(function (res) { - return res.json().then(function (data) { + return res.text().then(function (text) { + var data = parseJsonResponseText(res, text); if (!res.ok) { var err = new Error(data.error || res.statusText || 'Request failed'); err.payload = data; @@ -775,11 +800,15 @@ function readMealsLibrary() { var el = document.getElementById('mealsLibraryJson'); - if (!el || !el.textContent) { + if (!el) { + return []; + } + var raw = (el.textContent || '').trim(); + if (raw === '') { return []; } try { - return JSON.parse(el.textContent); + return JSON.parse(raw); } catch (e) { return []; } @@ -1032,7 +1061,8 @@ var safeId = String(mealId); fetch(apiBase + '/meal_export.php?mealId=' + encodeURIComponent(safeId), { credentials: 'same-origin' }) .then(function (res) { - return res.json().then(function (data) { + return res.text().then(function (text) { + var data = parseJsonResponseText(res, text); return { res: res, data: data }; }); }) @@ -1083,7 +1113,11 @@ var reader = new FileReader(); reader.onload = function () { try { - mealImportParsed = JSON.parse(String(reader.result || '')); + var raw = String(reader.result || '').trim(); + if (raw === '') { + throw new Error('File is empty.'); + } + mealImportParsed = JSON.parse(raw); $prev.text(JSON.stringify(mealImportParsed, null, 2)); $('#btnMealImportSubmit').prop('disabled', false); if ($fb.length) { diff --git a/env.example b/env.example index 76e34f6..d61af11 100644 --- a/env.example +++ b/env.example @@ -3,7 +3,9 @@ GOOGLE_CLIENT_ID=your_client_id_here GOOGLE_CLIENT_SECRET=your_client_secret_here GOOGLE_REDIRECT_URI=http://localhost/family-hub/auth/google/callback -# Google Calendar +# Google Calendar (Calendar tab embed). Replace placeholders—leaving your_calendar_id_here hides the embed. +# Easiest: set GOOGLE_CALENDAR_ID to the Calendar ID from Google Calendar → Settings → Integrate calendar. +# Optional: paste the embed URL or full