UI Improvements and TV mode
This commit is contained in:
parent
7eb6160b5a
commit
735db3baf3
@ -14,10 +14,51 @@ body {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
main {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
background: linear-gradient(135deg, var(--person-accent, #4a90e2) 0%, #2c5282 100%);
|
||||
}
|
||||
|
||||
/* Header top: title (left) · balance + persona (right from md up); mobile: second row = persona menu */
|
||||
.app-header-top {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
column-gap: 0.75rem;
|
||||
row-gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-header-brand {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-header-actions {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
justify-self: end;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.app-header-persona-mobile {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.app-header-persona-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-header-top {
|
||||
row-gap: 0.35rem;
|
||||
}
|
||||
}
|
||||
|
||||
.persona-chip {
|
||||
min-height: 2.75rem;
|
||||
touch-action: manipulation;
|
||||
@ -28,6 +69,11 @@ body {
|
||||
box-shadow: 0 0 0 0.15rem rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.persona-chip-mobile.active {
|
||||
font-weight: 600;
|
||||
background-color: rgba(74, 144, 226, 0.14);
|
||||
}
|
||||
|
||||
.family-hub-body .card {
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
@ -65,26 +111,74 @@ body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Tab Styles */
|
||||
.tabs {
|
||||
/* Header tab nav */
|
||||
.app-header.app-header-with-tabs .tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.35rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
@media (min-width: 768px) {
|
||||
.app-header.app-header-with-tabs .app-header-top {
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.app-header.app-header-with-tabs .tabs {
|
||||
margin-top: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.app-header .tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
min-height: 2.5rem;
|
||||
padding: 0.45rem 0.9rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
margin-right: 5px;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: white;
|
||||
border-color: var(--border-color);
|
||||
border-bottom: 1px solid white;
|
||||
margin-bottom: -1px;
|
||||
body.header-tone-dark .app-header .tab {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-color: rgba(255, 255, 255, 0.28);
|
||||
}
|
||||
|
||||
body.header-tone-dark .app-header .tab:hover,
|
||||
body.header-tone-dark .app-header .tab:focus-visible {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
body.header-tone-dark .app-header .tab.active {
|
||||
color: #1f2a37;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border-color: rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
body.header-tone-light .app-header .tab {
|
||||
color: #1f2a37;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-color: rgba(31, 42, 55, 0.25);
|
||||
}
|
||||
|
||||
body.header-tone-light .app-header .tab:hover,
|
||||
body.header-tone-light .app-header .tab:focus-visible {
|
||||
color: #111827;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border-color: rgba(31, 42, 55, 0.4);
|
||||
}
|
||||
|
||||
body.header-tone-light .app-header .tab.active {
|
||||
color: #fff;
|
||||
background: #1f2a37;
|
||||
border-color: #1f2a37;
|
||||
}
|
||||
|
||||
/* Mobile Responsive */
|
||||
@ -92,14 +186,22 @@ body {
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
flex-direction: column;
|
||||
|
||||
.app-header h1.h3 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.tab {
|
||||
margin-right: 0;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.user-balance {
|
||||
max-width: 58vw;
|
||||
}
|
||||
|
||||
.app-header .tabs {
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.app-header .tab {
|
||||
padding: 0.4rem 0.8rem;
|
||||
min-height: 2.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,3 +212,28 @@ body {
|
||||
.calendar-agenda-today {
|
||||
background-color: rgba(74, 144, 226, 0.06);
|
||||
}
|
||||
|
||||
/* Calendar tab: Google column first; side-by-side 70% / 30% from xl up when embed is configured */
|
||||
@media (min-width: 1200px) {
|
||||
.calendar-tab-layout-split > .calendar-tab-google {
|
||||
flex: 0 0 70%;
|
||||
max-width: 70%;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.calendar-tab-layout-split > .calendar-tab-agenda {
|
||||
flex: 0 0 30%;
|
||||
max-width: 30%;
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-tv-view .calendar-embed iframe {
|
||||
min-height: 70vh;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.calendar-tv-view .calendar-embed iframe {
|
||||
min-height: 78vh;
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,33 +9,72 @@
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="assets/css/style.css">
|
||||
</head>
|
||||
<body class="family-hub-body" style="--person-accent: <?= htmlspecialchars($favoriteColor ?? '#4a90e2', ENT_QUOTES, 'UTF-8') ?>;">
|
||||
<header class="app-header text-white p-3 mb-0">
|
||||
<body class="family-hub-body <?= htmlspecialchars($headerThemeClass ?? 'header-tone-dark', ENT_QUOTES, 'UTF-8') ?>" style="--person-accent: <?= htmlspecialchars($favoriteColor ?? '#4a90e2', ENT_QUOTES, 'UTF-8') ?>;">
|
||||
<?php $hideTopHeader = !empty($isCalendarTvView); ?>
|
||||
<?php if (!$hideTopHeader): ?>
|
||||
<header class="app-header app-header-with-tabs text-white pt-3 px-3 pb-2 mb-0">
|
||||
<div class="container">
|
||||
<div class="d-flex flex-column flex-md-row align-items-start align-items-md-center justify-content-between gap-3">
|
||||
<h1 class="h3 mb-0">Family Hub</h1>
|
||||
<div class="d-flex flex-column align-items-start align-items-md-end gap-2 ms-md-auto">
|
||||
<?php
|
||||
$hdrSym = trim((string) ($familySettings['currency_symbol'] ?? '★'));
|
||||
$hdrName = trim((string) ($familySettings['currency_name'] ?? ''));
|
||||
$hdrBal = 0.0;
|
||||
if ($activePerson !== null) {
|
||||
$hdrBal = is_numeric($activePerson['currency_balance'] ?? null)
|
||||
? (float) $activePerson['currency_balance']
|
||||
: 0.0;
|
||||
}
|
||||
?>
|
||||
<?php if ($activePerson !== null && count($people) > 0): ?>
|
||||
<div class="user-balance badge rounded-pill bg-light text-dark px-3 py-2 text-wrap text-start" title="Current balance for the selected profile">
|
||||
<span class="text-muted small">Balance</span><br>
|
||||
<strong class="fs-6"><?= htmlspecialchars(number_format($hdrBal, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?></strong>
|
||||
<span class="ms-1"><?= htmlspecialchars($hdrSym, ENT_QUOTES, 'UTF-8') ?><?php if ($hdrName !== ''): ?><span class="text-muted small"> <?= htmlspecialchars($hdrName, ENT_QUOTES, 'UTF-8') ?></span><?php endif; ?></span>
|
||||
<?php
|
||||
$hdrSym = trim((string) ($familySettings['currency_symbol'] ?? '★'));
|
||||
$hdrName = trim((string) ($familySettings['currency_name'] ?? ''));
|
||||
$hdrBal = 0.0;
|
||||
if ($activePerson !== null) {
|
||||
$hdrBal = is_numeric($activePerson['currency_balance'] ?? null)
|
||||
? (float) $activePerson['currency_balance']
|
||||
: 0.0;
|
||||
}
|
||||
?>
|
||||
<div class="app-header-top">
|
||||
<h1 class="h3 mb-0 app-header-brand">Family Hub</h1>
|
||||
<div class="app-header-actions d-flex align-items-center gap-2 flex-wrap justify-content-end">
|
||||
<?php if (count($people) === 0): ?>
|
||||
<span class="small opacity-75 text-end">Add people in Family settings.</span>
|
||||
<?php else: ?>
|
||||
<?php if ($activePerson !== null): ?>
|
||||
<div class="user-balance app-header-balance badge rounded-pill bg-light text-dark px-3 py-2 text-wrap text-start" title="Current balance for the selected profile">
|
||||
<span class="text-muted small">Balance</span><br>
|
||||
<strong class="fs-6"><?= htmlspecialchars(number_format($hdrBal, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?></strong>
|
||||
<span class="ms-1"><?= htmlspecialchars($hdrSym, ENT_QUOTES, 'UTF-8') ?><?php if ($hdrName !== ''): ?><span class="text-muted small"> <?= htmlspecialchars($hdrName, ENT_QUOTES, 'UTF-8') ?></span><?php endif; ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="persona-switcher d-none d-md-flex flex-wrap gap-2 align-items-center justify-content-end" role="toolbar" aria-label="Who is using the hub">
|
||||
<?php foreach ($people as $p): ?>
|
||||
<?php
|
||||
$pid = $p['id'] ?? '';
|
||||
$pname = $p['name'] ?? 'Unknown';
|
||||
$isActive = $activePerson && ($activePerson['id'] ?? '') === $pid;
|
||||
$needsPin = personRequiresPinToActivate($p) ? '1' : '0';
|
||||
$roleLabel = $p['role'] ?? '';
|
||||
?>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm persona-chip <?= $isActive ? 'btn-light active' : 'btn-outline-light' ?>"
|
||||
data-person-id="<?= htmlspecialchars($pid, ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-needs-pin="<?= $needsPin ?>"
|
||||
data-person-name="<?= htmlspecialchars($pname, ENT_QUOTES, 'UTF-8') ?>"
|
||||
>
|
||||
<?= sanitizeInput($pname) ?>
|
||||
<?php if ($roleLabel === ROLE_HEAD): ?>
|
||||
<span class="visually-hidden">(Head of household)</span>
|
||||
<i class="fa fa-house-chimney-user ms-1" aria-hidden="true"></i>
|
||||
<?php elseif ($roleLabel === ROLE_ADULT): ?>
|
||||
<i class="fa fa-person ms-1" aria-hidden="true"></i>
|
||||
<?php elseif ($roleLabel === ROLE_CHILD): ?>
|
||||
<i class="fa fa-child ms-1" aria-hidden="true"></i>
|
||||
<?php endif; ?>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="persona-switcher d-flex flex-wrap gap-2 align-items-center" role="toolbar" aria-label="Who is using the hub">
|
||||
<?php if (count($people) === 0): ?>
|
||||
<span class="small opacity-75">Add people in Family settings.</span>
|
||||
<?php else: ?>
|
||||
</div>
|
||||
|
||||
<?php if (count($people) > 0): ?>
|
||||
<div class="dropdown d-md-none app-header-persona-mobile">
|
||||
<button class="btn btn-light btn-sm dropdown-toggle w-100 text-start" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fa fa-user-group me-1" aria-hidden="true"></i>
|
||||
<?= $activePerson !== null ? sanitizeInput((string) ($activePerson['name'] ?? 'Select family member')) : 'Select family member' ?>
|
||||
</button>
|
||||
<ul class="dropdown-menu w-100">
|
||||
<?php foreach ($people as $p): ?>
|
||||
<?php
|
||||
$pid = $p['id'] ?? '';
|
||||
@ -44,30 +83,45 @@
|
||||
$needsPin = personRequiresPinToActivate($p) ? '1' : '0';
|
||||
$roleLabel = $p['role'] ?? '';
|
||||
?>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm persona-chip <?= $isActive ? 'btn-light active' : 'btn-outline-light' ?>"
|
||||
data-person-id="<?= htmlspecialchars($pid, ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-needs-pin="<?= $needsPin ?>"
|
||||
data-person-name="<?= htmlspecialchars($pname, ENT_QUOTES, 'UTF-8') ?>"
|
||||
>
|
||||
<?= sanitizeInput($pname) ?>
|
||||
<?php if ($roleLabel === ROLE_HEAD): ?>
|
||||
<span class="visually-hidden">(Head of household)</span>
|
||||
<i class="fa fa-house-chimney-user ms-1" aria-hidden="true"></i>
|
||||
<?php elseif ($roleLabel === ROLE_ADULT): ?>
|
||||
<i class="fa fa-person ms-1" aria-hidden="true"></i>
|
||||
<?php elseif ($roleLabel === ROLE_CHILD): ?>
|
||||
<i class="fa fa-child ms-1" aria-hidden="true"></i>
|
||||
<?php endif; ?>
|
||||
</button>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item persona-chip persona-chip-mobile <?= $isActive ? 'active' : '' ?>"
|
||||
data-person-id="<?= htmlspecialchars($pid, ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-needs-pin="<?= $needsPin ?>"
|
||||
data-person-name="<?= htmlspecialchars($pname, ENT_QUOTES, 'UTF-8') ?>"
|
||||
>
|
||||
<?= sanitizeInput($pname) ?>
|
||||
<?php if ($roleLabel === ROLE_HEAD): ?>
|
||||
<i class="fa fa-house-chimney-user ms-1" aria-hidden="true"></i>
|
||||
<?php elseif ($roleLabel === ROLE_ADULT): ?>
|
||||
<i class="fa fa-person ms-1" aria-hidden="true"></i>
|
||||
<?php elseif ($roleLabel === ROLE_CHILD): ?>
|
||||
<i class="fa fa-child ms-1" aria-hidden="true"></i>
|
||||
<?php endif; ?>
|
||||
</button>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
$_nav = isset($navTabs) && is_array($navTabs) && $navTabs !== [] ? $navTabs : ($TABS ?? []);
|
||||
?>
|
||||
<?php if ($_nav !== []): ?>
|
||||
<nav class="tabs" aria-label="Sections">
|
||||
<?php foreach ($_nav as $tabId => $tab): ?>
|
||||
<a href="?tab=<?= htmlspecialchars($tabId, ENT_QUOTES, 'UTF-8') ?>" class="tab <?= ($activeTab ?? '') === $tabId ? 'active' : '' ?>">
|
||||
<i class="fa fa-<?= htmlspecialchars($tab['icon'], ENT_QUOTES, 'UTF-8') ?>" aria-hidden="true"></i>
|
||||
<span><?= htmlspecialchars($tab['title'], ENT_QUOTES, 'UTF-8') ?></span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</header>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="modal fade" id="hohPinModal" tabindex="-1" aria-labelledby="hohPinModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
|
||||
@ -57,6 +57,18 @@ function personRequiresPinToActivate(?array $person): bool {
|
||||
return ($person['role'] ?? '') === ROLE_HEAD && !empty($person['pin_hash']);
|
||||
}
|
||||
|
||||
/**
|
||||
* True when at least one person is recorded as Head of household (setup complete).
|
||||
*/
|
||||
function familyHubHasHeadOfHousehold(array $people): bool {
|
||||
foreach ($people as $p) {
|
||||
if (($p['role'] ?? '') === ROLE_HEAD) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function assertHoHCanManagePeople(array $people): void {
|
||||
if (count($people) === 0) {
|
||||
return;
|
||||
|
||||
52
index.php
52
index.php
@ -19,11 +19,40 @@ if (isset($TABS['currency'])) {
|
||||
$TABS['currency']['title'] = currencyTabLabel($familySettings);
|
||||
}
|
||||
|
||||
$hasHeadOfHousehold = familyHubHasHeadOfHousehold($people);
|
||||
$showSettingsTab = !$hasHeadOfHousehold
|
||||
|| (
|
||||
$activePerson !== null
|
||||
&& ($activePerson['role'] ?? '') === ROLE_HEAD
|
||||
&& isHohVerified()
|
||||
);
|
||||
|
||||
$navTabs = $TABS;
|
||||
if (!$showSettingsTab) {
|
||||
unset($navTabs['settings']);
|
||||
}
|
||||
|
||||
$activeTab = isset($_GET['tab']) ? (string) $_GET['tab'] : 'chores';
|
||||
if (!isset($TABS[$activeTab])) {
|
||||
$activeTab = 'chores';
|
||||
}
|
||||
|
||||
if (
|
||||
isset($_GET['tab'])
|
||||
&& (string) $_GET['tab'] === 'settings'
|
||||
&& !$showSettingsTab
|
||||
) {
|
||||
header('Location: ?tab=chores', true, 303);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!isset($navTabs[$activeTab])) {
|
||||
$firstNav = array_key_first($navTabs);
|
||||
$activeTab = is_string($firstNav) ? $firstNav : 'chores';
|
||||
}
|
||||
|
||||
$isCalendarTvView = $activeTab === 'calendar' && isset($_GET['view']) && (string) $_GET['view'] === 'tv';
|
||||
|
||||
$familyHubApiBase = familyHubWebApiBase();
|
||||
|
||||
$favoriteColor = '#4a90e2';
|
||||
@ -34,20 +63,23 @@ if (
|
||||
) {
|
||||
$favoriteColor = (string) $activePerson['favoriteColor'];
|
||||
}
|
||||
$headerThemeClass = 'header-tone-dark';
|
||||
if (preg_match('/^#([0-9A-Fa-f]{6})$/', $favoriteColor, $m)) {
|
||||
$hex = $m[1];
|
||||
$r = hexdec(substr($hex, 0, 2));
|
||||
$g = hexdec(substr($hex, 2, 2));
|
||||
$b = hexdec(substr($hex, 4, 2));
|
||||
$relativeLuminance = (0.2126 * $r + 0.7152 * $g + 0.0722 * $b) / 255;
|
||||
if ($relativeLuminance >= 0.62) {
|
||||
$headerThemeClass = 'header-tone-light';
|
||||
}
|
||||
}
|
||||
|
||||
include 'includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="container">
|
||||
<div class="tabs">
|
||||
<?php foreach ($TABS as $tabId => $tab): ?>
|
||||
<a href="?tab=<?= htmlspecialchars($tabId, ENT_QUOTES, 'UTF-8') ?>" class="tab <?= $activeTab === $tabId ? 'active' : '' ?>">
|
||||
<i class="fa fa-<?= htmlspecialchars($tab['icon'], ENT_QUOTES, 'UTF-8') ?>"></i> <?= htmlspecialchars($tab['title'], ENT_QUOTES, 'UTF-8') ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="<?= $isCalendarTvView ? 'container-fluid px-3 py-3' : 'container' ?>">
|
||||
<div class="tab-content" id="dashboardContentArea">
|
||||
<?php include 'tabs/' . $activeTab . '.php'; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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><iframe></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><iframe></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; ?>
|
||||
|
||||
@ -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 can’t edit this chore. Only the person who created it or a verified Head of household can.</div>
|
||||
|
||||
@ -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&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>
|
||||
|
||||
<?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>
|
||||
|
||||
@ -124,6 +124,7 @@ $permanenceOptions = [
|
||||
<div class="col-12">
|
||||
<div class="alert d-none" id="calendarSettingsFeedback" role="status"></div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user