UI Improvements and TV mode

This commit is contained in:
2026-03-30 17:15:46 -05:00
parent 7eb6160b5a
commit 735db3baf3
8 changed files with 398 additions and 115 deletions
+60 -24
View File
@@ -53,6 +53,10 @@ $events = hubCalendarAgendaEvents($rangeStart, $rangeEnd, $people, $familySettin
[, $tzLocal] = familyHubCalendarContext($familySettings);
$todayYmd = familyHubTodayYmdInTz($tzLocal);
$twoWayMerge = !empty($familySettings['calendar_two_way_google']);
$isTvView = isset($_GET['view']) && (string) $_GET['view'] === 'tv';
$calendarBaseUrl = '?tab=calendar';
$calendarTvUrl = '?tab=calendar&view=tv';
$lastRefreshedStamp = gmdate('Y-m-d H:i') . ' UTC';
$eventsByDate = [];
foreach ($events as $ev) {
@@ -76,8 +80,31 @@ $iconByType = [
];
?>
<div id="calendar" class="tab-content">
<h2 class="mb-2">Calendar</h2>
<div id="calendar" class="tab-content<?= $isTvView ? ' calendar-tv-view' : '' ?>">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-2">
<h2 class="mb-0">Calendar</h2>
<?php if ($isTvView): ?>
<a href="<?= htmlspecialchars($calendarBaseUrl, ENT_QUOTES, 'UTF-8') ?>" class="btn btn-outline-secondary btn-sm" title="Exit TV view">
<i class="fa fa-xmark me-1" aria-hidden="true"></i>
Exit TV view
</a>
<?php else: ?>
<a href="<?= htmlspecialchars($calendarTvUrl, ENT_QUOTES, 'UTF-8') ?>" class="btn btn-outline-secondary btn-sm" title="Open TV view dashboard">
<i class="fa fa-tv me-1" aria-hidden="true"></i>
Open TV view
</a>
<?php endif; ?>
</div>
<p class="text-muted small mb-3">
<?php if ($isTvView): ?>
TV view auto-refreshes every 10 minutes.
<?php else: ?>
Open TV view for a dedicated dashboard URL that refreshes every 10 minutes.
<?php endif; ?>
</p>
<?php if ($isTvView): ?>
<p class="small text-muted mb-3">Last refreshed: <strong><?= htmlspecialchars($lastRefreshedStamp, ENT_QUOTES, 'UTF-8') ?></strong></p>
<?php endif; ?>
<?php if ($twoWayMerge): ?>
<div class="alert alert-info mb-3" role="status">
@@ -85,8 +112,30 @@ $iconByType = [
</div>
<?php endif; ?>
<div class="row g-4">
<div class="col-12 <?= $embedUrl !== null ? 'col-xl-7' : '' ?>">
<div class="row g-4 calendar-tab-layout<?= $embedUrl !== null ? ' calendar-tab-layout-split' : '' ?>">
<div class="col-12 calendar-tab-google">
<h3 class="h5 mb-3">Google Calendar</h3>
<?php if ($embedUrl === null): ?>
<?php if (!$isTvView): ?>
<p class="text-muted">Connect a shared family calendar by setting <strong>GOOGLE_CALENDAR_EMBED_CODE</strong> (embed URL or full <code>&lt;iframe&gt;</code> from Google Calendar) or <strong>GOOGLE_CALENDAR_ID</strong> in <code>.env</code>. See <a href="https://calendar.google.com/">Google Calendar</a> → calendar menu → <strong>Settings and sharing</strong> → <strong>Integrate calendar</strong>.</p>
<?php endif; ?>
<?php else: ?>
<?php if ($fromIdOnly && !$isTvView): ?>
<p class="small text-muted mb-2">Showing calendar from <code>GOOGLE_CALENDAR_ID</code>. For more control (view, height), paste the embed URL or iframe into <code>GOOGLE_CALENDAR_EMBED_CODE</code> instead.</p>
<?php endif; ?>
<div class="calendar-embed rounded border overflow-hidden bg-white">
<iframe
class="w-100 border-0 d-block"
style="height: 70vh; min-height: 360px;"
src="<?= htmlspecialchars($embedUrl, ENT_QUOTES, 'UTF-8') ?>"
loading="lazy"
title="Family calendar"
allowfullscreen
></iframe>
</div>
<?php endif; ?>
</div>
<div class="col-12 calendar-tab-agenda">
<h3 class="h5 mb-3">Family Hub agenda</h3>
<p class="text-muted small mb-3">
<?= htmlspecialchars($rangeStart, ENT_QUOTES, 'UTF-8') ?> to <?= htmlspecialchars($rangeEnd, ENT_QUOTES, 'UTF-8') ?>
@@ -129,25 +178,12 @@ $iconByType = [
</div>
<?php endif; ?>
</div>
<div class="col-12 <?= $embedUrl !== null ? 'col-xl-5' : '' ?>">
<h3 class="h5 mb-3">Google Calendar</h3>
<?php if ($embedUrl === null): ?>
<p class="text-muted">Connect a shared family calendar by setting <strong>GOOGLE_CALENDAR_EMBED_CODE</strong> (embed URL or full <code>&lt;iframe&gt;</code> from Google Calendar) or <strong>GOOGLE_CALENDAR_ID</strong> in <code>.env</code>. See <a href="https://calendar.google.com/">Google Calendar</a> → calendar menu → <strong>Settings and sharing</strong> → <strong>Integrate calendar</strong>.</p>
<?php else: ?>
<?php if ($fromIdOnly): ?>
<p class="small text-muted mb-2">Showing calendar from <code>GOOGLE_CALENDAR_ID</code>. For more control (view, height), paste the embed URL or iframe into <code>GOOGLE_CALENDAR_EMBED_CODE</code> instead.</p>
<?php endif; ?>
<div class="calendar-embed rounded border overflow-hidden bg-white">
<iframe
class="w-100 border-0 d-block"
style="height: 70vh; min-height: 360px;"
src="<?= htmlspecialchars($embedUrl, ENT_QUOTES, 'UTF-8') ?>"
loading="lazy"
title="Family calendar"
allowfullscreen
></iframe>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php if ($isTvView): ?>
<script>
window.setTimeout(function () {
window.location.reload();
}, 10 * 60 * 1000);
</script>
<?php endif; ?>
+16 -2
View File
@@ -128,14 +128,23 @@ if ($editChore) {
<?php endif; ?>
<?php if ($showChoreForm): ?>
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="card mb-4 <?= $editChore ? '' : 'border-secondary' ?>">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<span><?= $editChore ? 'Edit chore' : 'New chore' ?></span>
<?php if ($editChore): ?>
<a href="?tab=chores" class="btn btn-sm btn-outline-secondary">Cancel edit</a>
<?php else: ?>
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#choreFormCollapse" aria-expanded="false" aria-controls="choreFormCollapse">
Show form
</button>
<?php endif; ?>
</div>
<?php if ($editChore): ?>
<div class="card-body">
<?php else: ?>
<div class="collapse" id="choreFormCollapse">
<div class="card-body">
<?php endif; ?>
<form id="choreForm" class="row g-3">
<input type="hidden" name="id" id="chore_id" value="<?= $editChore ? htmlspecialchars($editChore['id'] ?? '', ENT_QUOTES, 'UTF-8') : '' ?>">
@@ -221,7 +230,12 @@ if ($editChore) {
<div class="alert d-none" id="choreFormFeedback" role="status"></div>
</div>
</form>
<?php if ($editChore): ?>
</div>
<?php else: ?>
</div>
</div>
<?php endif; ?>
</div>
<?php elseif ($activePerson !== null && $editChore !== null && !$editChoreAllowed): ?>
<div class="alert alert-warning mb-4">You cant edit this chore. Only the person who created it or a verified Head of household can.</div>
+22 -15
View File
@@ -89,22 +89,15 @@ $canReviewGroceries = $activePerson !== null
<p class="alert alert-warning">Pick a store from the list.</p>
<?php else: ?>
<div class="d-flex flex-wrap gap-2 mb-3 align-items-center">
<span class="text-muted small me-1">Show:</span>
<?php
$filtHref = static function ($f) use ($selectedStore) {
return '?tab=groceries&amp;store=' . rawurlencode($selectedStore) . '&amp;filter=' . rawurlencode($f);
};
?>
<a class="btn btn-sm <?= $filter === 'active' ? 'btn-primary' : 'btn-outline-primary' ?>" href="<?= htmlspecialchars($filtHref('active'), ENT_QUOTES, 'UTF-8') ?>">To buy</a>
<a class="btn btn-sm <?= $filter === 'purchased' ? 'btn-primary' : 'btn-outline-primary' ?>" href="<?= htmlspecialchars($filtHref('purchased'), ENT_QUOTES, 'UTF-8') ?>">Purchased</a>
<a class="btn btn-sm <?= $filter === 'pending' ? 'btn-primary' : 'btn-outline-primary' ?>" href="<?= htmlspecialchars($filtHref('pending'), ENT_QUOTES, 'UTF-8') ?>">Pending review</a>
<a class="btn btn-sm <?= $filter === 'all' ? 'btn-primary' : 'btn-outline-primary' ?>" href="<?= htmlspecialchars($filtHref('all'), ENT_QUOTES, 'UTF-8') ?>">All</a>
</div>
<?php if ($activePerson !== null): ?>
<div class="card mb-4">
<div class="card-header">Add item — <?= sanitizeInput($currentStore['name'] ?? '') ?></div>
<div class="card border-secondary mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Add item — <?= sanitizeInput($currentStore['name'] ?? '') ?></span>
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#groceryAddFormCollapse" aria-expanded="false" aria-controls="groceryAddFormCollapse">
Show form
</button>
</div>
<div class="collapse" id="groceryAddFormCollapse">
<div class="card-body">
<form id="groceryAddForm" class="row g-3">
<input type="hidden" id="grocery_store_id" value="<?= htmlspecialchars($selectedStore, ENT_QUOTES, 'UTF-8') ?>">
@@ -159,9 +152,23 @@ $canReviewGroceries = $activePerson !== null
</div>
</form>
</div>
</div>
</div>
<?php endif; ?>
<div class="d-flex flex-wrap gap-2 mb-3 align-items-center">
<span class="text-muted small me-1">Show:</span>
<?php
$filtHref = static function ($f) use ($selectedStore) {
return '?tab=groceries&store=' . rawurlencode($selectedStore) . '&filter=' . rawurlencode($f);
};
?>
<a class="btn btn-sm <?= $filter === 'active' ? 'btn-primary' : 'btn-outline-primary' ?>" href="<?= htmlspecialchars($filtHref('active'), ENT_QUOTES, 'UTF-8') ?>">To buy</a>
<a class="btn btn-sm <?= $filter === 'purchased' ? 'btn-primary' : 'btn-outline-primary' ?>" href="<?= htmlspecialchars($filtHref('purchased'), ENT_QUOTES, 'UTF-8') ?>">Purchased</a>
<a class="btn btn-sm <?= $filter === 'pending' ? 'btn-primary' : 'btn-outline-primary' ?>" href="<?= htmlspecialchars($filtHref('pending'), ENT_QUOTES, 'UTF-8') ?>">Pending review</a>
<a class="btn btn-sm <?= $filter === 'all' ? 'btn-primary' : 'btn-outline-primary' ?>" href="<?= htmlspecialchars($filtHref('all'), ENT_QUOTES, 'UTF-8') ?>">All</a>
</div>
<h3 class="h6 text-muted"><?= $filter === 'purchased' ? 'Purchased' : ($filter === 'pending' ? 'Pending review' : ($filter === 'all' ? 'All items' : 'To buy')) ?></h3>
<?php if (count($displayItems) === 0): ?>
<p class="text-muted">No items in this view.</p>
+1
View File
@@ -124,6 +124,7 @@ $permanenceOptions = [
<div class="col-12">
<div class="alert d-none" id="calendarSettingsFeedback" role="status"></div>
</div>
</div>
</form>
</div>
</div>