438 lines
14 KiB
HTML
438 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Brightwheel Memories Archive</title>
|
|
<style>
|
|
:root {
|
|
--primary: #FF6B35;
|
|
--primary-gradient: linear-gradient(135deg, #FF6B35 0%, #FF8E53 100%);
|
|
--bg: #F8FAFC;
|
|
--card-bg: #FFFFFF;
|
|
--text: #0F172A;
|
|
--text-muted: #64748B;
|
|
--border: #E2E8F0;
|
|
--radius: 14px;
|
|
--shadow: 0 4px 20px -2px rgba(0, 0, 0, 0.06);
|
|
}
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
line-height: 1.5;
|
|
padding-bottom: 60px;
|
|
}
|
|
header {
|
|
background: #FFFFFF;
|
|
border-bottom: 1px solid var(--border);
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
.header-inner {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 16px 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
flex-wrap: wrap;
|
|
gap: 16px;
|
|
}
|
|
.hero-profile {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
.avatar {
|
|
width: 52px;
|
|
height: 52px;
|
|
border-radius: 50%;
|
|
background: var(--primary-gradient);
|
|
color: white;
|
|
font-weight: 700;
|
|
font-size: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3);
|
|
overflow: hidden;
|
|
}
|
|
.avatar img { width: 100%; height: 100%; object-fit: cover; }
|
|
.hero-text h1 { font-size: 20px; font-weight: 800; }
|
|
.hero-text p { font-size: 13px; color: var(--text-muted); }
|
|
.stats-badge {
|
|
display: flex;
|
|
gap: 12px;
|
|
}
|
|
.stat-pill {
|
|
background: #F1F5F9;
|
|
padding: 6px 14px;
|
|
border-radius: 20px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: #334155;
|
|
}
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 24px auto;
|
|
padding: 0 24px;
|
|
}
|
|
.controls-bar {
|
|
background: #FFFFFF;
|
|
padding: 16px 20px;
|
|
border-radius: var(--radius);
|
|
border: 1px solid var(--border);
|
|
box-shadow: var(--shadow);
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 14px;
|
|
align-items: center;
|
|
margin-bottom: 24px;
|
|
}
|
|
.search-input {
|
|
flex: 1;
|
|
min-width: 220px;
|
|
padding: 10px 16px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
font-size: 14px;
|
|
outline: none;
|
|
}
|
|
.search-input:focus { border-color: var(--primary); }
|
|
.filter-tabs {
|
|
display: flex;
|
|
gap: 6px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.filter-btn {
|
|
padding: 8px 14px;
|
|
border-radius: 8px;
|
|
border: 1px solid var(--border);
|
|
background: #FFFFFF;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
color: var(--text-muted);
|
|
transition: all 0.2s ease;
|
|
}
|
|
.filter-btn.active {
|
|
background: var(--primary);
|
|
color: #FFFFFF;
|
|
border-color: var(--primary);
|
|
}
|
|
.feed-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
.feed-card {
|
|
background: var(--card-bg);
|
|
border-radius: var(--radius);
|
|
border: 1px solid var(--border);
|
|
box-shadow: var(--shadow);
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
transition: transform 0.2s ease;
|
|
}
|
|
.feed-card:hover {
|
|
transform: translateY(-2px);
|
|
}
|
|
.card-media {
|
|
width: 100%;
|
|
height: 240px;
|
|
background: #0F172A;
|
|
position: relative;
|
|
cursor: pointer;
|
|
}
|
|
.card-media img, .card-media video {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
.card-body {
|
|
padding: 16px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
flex: 1;
|
|
}
|
|
.card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.type-badge {
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
padding: 4px 8px;
|
|
border-radius: 6px;
|
|
}
|
|
.type-photo { background: #FEF3C7; color: #D97706; }
|
|
.type-video { background: #FCE7F3; color: #DB2777; }
|
|
.type-food { background: #DCFCE7; color: #15803D; }
|
|
.type-nap { background: #E0E7FF; color: #4338CA; }
|
|
.type-potty { background: #E0F2FE; color: #0284C7; }
|
|
.type-milestone { background: #F3E8FF; color: #7E22CE; }
|
|
.type-checkin { background: #F1F5F9; color: #475569; }
|
|
.type-note { background: #FFFBEB; color: #B45309; }
|
|
|
|
.card-time {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
.card-title {
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
color: var(--text);
|
|
}
|
|
.card-note {
|
|
font-size: 13.5px;
|
|
color: #334155;
|
|
}
|
|
.card-details {
|
|
background: #F8FAFC;
|
|
border-radius: 8px;
|
|
padding: 8px 12px;
|
|
font-size: 12px;
|
|
color: #475569;
|
|
}
|
|
.card-footer {
|
|
margin-top: auto;
|
|
padding-top: 8px;
|
|
border-top: 1px dashed var(--border);
|
|
font-size: 11.5px;
|
|
color: var(--text-muted);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
}
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
color: var(--text-muted);
|
|
grid-column: 1 / -1;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<div class="header-inner">
|
|
<div class="hero-profile">
|
|
<div class="avatar" id="avatarBox">BW</div>
|
|
<div class="hero-text">
|
|
<h1 id="childNameTitle">Brightwheel Archive</h1>
|
|
<p id="archiveSubtitle">Offline Memory Collection</p>
|
|
</div>
|
|
</div>
|
|
<div class="stats-badge">
|
|
<div class="stat-pill" id="totalItemsPill">0 Activities</div>
|
|
<div class="stat-pill" id="totalPhotosPill">0 Media</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="container">
|
|
<div class="controls-bar">
|
|
<input type="text" id="searchInput" class="search-input" placeholder="Search captions, notes, milestones...">
|
|
<div class="filter-tabs" id="filterTabs">
|
|
<button class="filter-btn active" data-filter="all">All</button>
|
|
<button class="filter-btn" data-filter="photo">Photos & Videos</button>
|
|
<button class="filter-btn" data-filter="food">Meals</button>
|
|
<button class="filter-btn" data-filter="nap">Naps</button>
|
|
<button class="filter-btn" data-filter="potty">Potty</button>
|
|
<button class="filter-btn" data-filter="milestone">Milestones</button>
|
|
<button class="filter-btn" data-filter="checkin">Attendance</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="feed-grid" id="feedGrid">
|
|
<div class="empty-state">Loading archive data...</div>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
// Embedded or fetched manifest data
|
|
let ARCHIVE_DATA = window.__BRIGHTWHEEL_ARCHIVE__ || null;
|
|
|
|
async function init() {
|
|
if (!ARCHIVE_DATA) {
|
|
try {
|
|
const res = await fetch('activities.json');
|
|
if (res.ok) {
|
|
ARCHIVE_DATA = await res.json();
|
|
}
|
|
} catch (e) {
|
|
try {
|
|
const fallbackRes = await fetch('manifest.json');
|
|
if (fallbackRes.ok) ARCHIVE_DATA = await fallbackRes.json();
|
|
} catch (e2) {
|
|
console.warn('Could not fetch activities.json or manifest.json', e2);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!ARCHIVE_DATA || !ARCHIVE_DATA.activities) {
|
|
document.getElementById('feedGrid').innerHTML = '<div class="empty-state">No activities found in activities.json. Ensure activities.json is in the same directory.</div>';
|
|
return;
|
|
}
|
|
|
|
renderProfile();
|
|
renderFeed('all', '');
|
|
setupEvents();
|
|
}
|
|
|
|
function renderProfile() {
|
|
const student = ARCHIVE_DATA.student || {};
|
|
const name = (student.first_name || '') + ' ' + (student.last_name || '');
|
|
if (name.trim()) {
|
|
document.getElementById('childNameTitle').textContent = name.trim();
|
|
document.title = name.trim() + ' - Brightwheel Memories Archive';
|
|
}
|
|
if (student.school_name) {
|
|
document.getElementById('archiveSubtitle').textContent = student.school_name;
|
|
}
|
|
if (student.avatar_local_path) {
|
|
document.getElementById('avatarBox').innerHTML = `<img src="${student.avatar_local_path}" alt="Avatar">`;
|
|
} else if (name.trim()) {
|
|
document.getElementById('avatarBox').textContent = (student.first_name?.[0] || 'B') + (student.last_name?.[0] || 'W');
|
|
}
|
|
|
|
const acts = ARCHIVE_DATA.activities || [];
|
|
const mediaCount = acts.filter(a => a.media_local_path || a.image_url || a.video_url).length;
|
|
document.getElementById('totalItemsPill').textContent = acts.length + ' Activities';
|
|
document.getElementById('totalPhotosPill').textContent = mediaCount + ' Media Files';
|
|
}
|
|
|
|
function getNormalizedType(act) {
|
|
const t = (act.activity_type || act.action_type || act.type || '').toLowerCase();
|
|
if (t.includes('photo') || t.includes('video') || t.includes('media') || act.media_local_path || act.image_url) return 'photo';
|
|
if (t.includes('food') || t.includes('meal') || t.includes('snack') || t.includes('lunch') || t.includes('breakfast')) return 'food';
|
|
if (t.includes('nap') || t.includes('sleep')) return 'nap';
|
|
if (t.includes('potty') || t.includes('diaper')) return 'potty';
|
|
if (t.includes('milestone') || t.includes('learn') || t.includes('observation') || t.includes('kudos')) return 'milestone';
|
|
if (t.includes('checkin') || t.includes('checkout') || t.includes('attendance') || t.includes('health') || t.includes('incident')) return 'checkin';
|
|
return 'note';
|
|
}
|
|
|
|
function renderFeed(filterType, searchQuery) {
|
|
const grid = document.getElementById('feedGrid');
|
|
const acts = ARCHIVE_DATA.activities || [];
|
|
const q = searchQuery.toLowerCase().trim();
|
|
|
|
const filtered = acts.filter(a => {
|
|
const type = getNormalizedType(a);
|
|
if (filterType !== 'all') {
|
|
if (filterType === 'photo' && type !== 'photo') return false;
|
|
if (filterType === 'food' && type !== 'food') return false;
|
|
if (filterType === 'nap' && type !== 'nap') return false;
|
|
if (filterType === 'potty' && type !== 'potty') return false;
|
|
if (filterType === 'milestone' && type !== 'milestone') return false;
|
|
if (filterType === 'checkin' && type !== 'checkin') return false;
|
|
}
|
|
|
|
if (q) {
|
|
const matchStr = (
|
|
(a.note || '') + ' ' +
|
|
(a.description || '') + ' ' +
|
|
(a.teacher_name || '') + ' ' +
|
|
(a.activity_type || '') + ' ' +
|
|
(a.title || '') + ' ' +
|
|
(a.created_at || '')
|
|
).toLowerCase();
|
|
if (!matchStr.includes(q)) return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
if (filtered.length === 0) {
|
|
grid.innerHTML = '<div class="empty-state">No matching activities found for current filter.</div>';
|
|
return;
|
|
}
|
|
|
|
grid.innerHTML = filtered.map(act => {
|
|
const type = getNormalizedType(act);
|
|
const dateStr = act.created_at || act.event_date || '';
|
|
const formattedDate = dateStr ? new Date(dateStr).toLocaleString(undefined, {
|
|
weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit'
|
|
}) : '';
|
|
|
|
const mediaPath = act.media_local_path || act.video_url || act.image_url;
|
|
const isVideo = (act.video_url || mediaPath?.endsWith('.mp4') || act.activity_type === 'ac_video' || act.activity_type === 'video');
|
|
|
|
let mediaHtml = '';
|
|
if (mediaPath) {
|
|
if (isVideo) {
|
|
mediaHtml = `<div class="card-media"><video src="${mediaPath}" controls preload="metadata"></video></div>`;
|
|
} else {
|
|
mediaHtml = `<div class="card-media"><img src="${mediaPath}" alt="Activity Media" loading="lazy"></div>`;
|
|
}
|
|
}
|
|
|
|
let detailLines = [];
|
|
if (act.food_type || act.amount_eaten) {
|
|
detailLines.push(`🍽️ <strong>${act.food_type || 'Meal'}</strong>: ${act.amount_eaten || 'Eaten'}`);
|
|
}
|
|
if (act.nap_start || act.nap_end || act.duration_minutes) {
|
|
detailLines.push(`😴 <strong>Nap</strong>: ${act.duration_minutes ? act.duration_minutes + ' mins' : (act.nap_start || '') + ' - ' + (act.nap_end || '')}`);
|
|
}
|
|
if (act.potty_type) {
|
|
detailLines.push(`🚽 <strong>Potty</strong>: ${act.potty_type}`);
|
|
}
|
|
if (act.milestone_name) {
|
|
detailLines.push(`🌟 <strong>Milestone</strong>: ${act.milestone_name}`);
|
|
}
|
|
if (act.attendance_info) {
|
|
detailLines.push(`📋 <strong>Attendance</strong>: ${act.attendance_info}`);
|
|
}
|
|
|
|
const displayTitle = act.action_summary || act.title || act.activity_type;
|
|
|
|
return `
|
|
<div class="feed-card">
|
|
${mediaHtml}
|
|
<div class="card-body">
|
|
<div class="card-header">
|
|
<span class="type-badge type-${type}">${act.action_subtype || act.activity_type || type}</span>
|
|
<span class="card-time">${formattedDate}</span>
|
|
</div>
|
|
<div class="card-title">${displayTitle}</div>
|
|
${act.note ? `<div class="card-note">${act.note}</div>` : ''}
|
|
${detailLines.length > 0 ? `<div class="card-details">${detailLines.join('<br>')}</div>` : ''}
|
|
<div class="card-footer">
|
|
<span>${act.teacher_name ? 'Logged by ' + act.teacher_name : 'Staff'}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function setupEvents() {
|
|
let currentFilter = 'all';
|
|
const searchInput = document.getElementById('searchInput');
|
|
|
|
document.getElementById('filterTabs').addEventListener('click', (e) => {
|
|
const btn = e.target.closest('.filter-btn');
|
|
if (!btn) return;
|
|
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
currentFilter = btn.dataset.filter;
|
|
renderFeed(currentFilter, searchInput.value);
|
|
});
|
|
|
|
searchInput.addEventListener('input', () => {
|
|
renderFeed(currentFilter, searchInput.value);
|
|
});
|
|
}
|
|
|
|
window.addEventListener('DOMContentLoaded', init);
|
|
</script>
|
|
</body>
|
|
</html>
|