Reworked snapshot template output
All checks were successful
Build Docker Image / build (push) Successful in 32s
All checks were successful
Build Docker Image / build (push) Successful in 32s
This commit is contained in:
@@ -299,13 +299,14 @@ class SnapshotDownloader:
|
||||
return all_snapshots
|
||||
|
||||
async def format_snapshot_html(
|
||||
self, snapshot: Dict[str, Any], session: aiohttp.ClientSession
|
||||
self, snapshot: Dict[str, Any], session: aiohttp.ClientSession, index: int = 0
|
||||
) -> str:
|
||||
"""
|
||||
Format a single snapshot as HTML.
|
||||
Format a single snapshot as HTML with parent-friendly design.
|
||||
|
||||
Args:
|
||||
snapshot: Snapshot dictionary from API
|
||||
index: Index for alternating accent colors
|
||||
|
||||
Returns:
|
||||
HTML string for the snapshot
|
||||
@@ -314,10 +315,9 @@ class SnapshotDownloader:
|
||||
snapshot_id = snapshot.get("id", "unknown")
|
||||
content = snapshot.get("notes", "") # Don't escape HTML in notes field
|
||||
start_time = snapshot.get("startTime", "")
|
||||
snapshot_type = snapshot.get("type", "Snapshot")
|
||||
|
||||
# Format dates
|
||||
start_date = self.format_date(start_time) if start_time else "Unknown"
|
||||
# Format dates in friendly format
|
||||
start_date = self.format_date_short(start_time) if start_time else ""
|
||||
|
||||
# Extract additional information
|
||||
author = snapshot.get("author", {})
|
||||
@@ -326,58 +326,49 @@ class SnapshotDownloader:
|
||||
author_name = (
|
||||
html.escape(f"{author_forename} {author_surname}".strip())
|
||||
if author
|
||||
else "Unknown"
|
||||
else ""
|
||||
)
|
||||
|
||||
# Extract child information (if any)
|
||||
child = snapshot.get("child", {})
|
||||
child_forename = child.get("forename", "") if child else ""
|
||||
child_name = (
|
||||
html.escape(
|
||||
f"{child.get('forename', '')} {child.get('surname', '')}".strip()
|
||||
)
|
||||
if child
|
||||
else ""
|
||||
)
|
||||
child_name = html.escape(child_forename) if child_forename else ""
|
||||
|
||||
# Create title in format: "Child Forename by Author Forename Surname"
|
||||
if child_forename and author_forename:
|
||||
title = html.escape(
|
||||
f"{child_forename} by {author_forename} {author_surname}".strip()
|
||||
)
|
||||
# Create friendly title
|
||||
if child_forename:
|
||||
title = html.escape(child_forename)
|
||||
else:
|
||||
title = html.escape(f"Snapshot {snapshot_id}")
|
||||
title = "Memory"
|
||||
|
||||
# Extract location/activity information
|
||||
activity = snapshot.get("activity", {})
|
||||
activity_name = html.escape(activity.get("name", "")) if activity else ""
|
||||
|
||||
# Build HTML
|
||||
# Soft accent colors for cards
|
||||
accent_colors = ["#FFE4E1", "#E0F7FA", "#FFF8E1", "#F3E5F5", "#E8F5E9"]
|
||||
accent = accent_colors[index % len(accent_colors)]
|
||||
|
||||
# Build parent-friendly HTML
|
||||
html_content = f"""
|
||||
<div class="snapshot" id="snapshot-{snapshot_id}">
|
||||
<div class="snapshot-header">
|
||||
<h3 class="snapshot-title">{title}</h3>
|
||||
<div class="snapshot-meta">
|
||||
<span class="snapshot-id">ID: {snapshot_id}</span>
|
||||
<span class="snapshot-type">Type: {snapshot_type}</span>
|
||||
<span class="snapshot-date">Date: {start_date}</span>
|
||||
{f'<span class="snapshot-signed">{"✓ Signed" if snapshot.get("signed", False) else "⏳ Pending"}</span>'}
|
||||
<article class="memory-card" id="snapshot-{snapshot_id}" style="--accent: {accent}">
|
||||
<div class="memory-header">
|
||||
<div class="memory-title-section">
|
||||
<h3 class="memory-title">{title}</h3>
|
||||
{f'<span class="memory-date">📅 {start_date}</span>' if start_date else ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="snapshot-content">
|
||||
{f'<div class="snapshot-author">👤 Author: {author_name}</div>' if author_name != "Unknown" else ""}
|
||||
{f'<div class="snapshot-child">👶 Child: {child_name}</div>' if child_name else ""}
|
||||
{f'<div class="snapshot-activity">🎯 Activity: {activity_name}</div>' if activity_name else ""}
|
||||
|
||||
<div class="snapshot-description">
|
||||
<div class="notes-content">{content if content else "<em>No description provided</em>"}</div>
|
||||
</div>
|
||||
|
||||
{await self.format_snapshot_media(snapshot, session)}
|
||||
{self.format_snapshot_metadata(snapshot)}
|
||||
<div class="memory-details">
|
||||
{f'<span class="memory-tag teacher">👩🏫 {author_name}</span>' if author_name else ''}
|
||||
{f'<span class="memory-tag activity">🎯 {activity_name}</span>' if activity_name else ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="memory-content">
|
||||
<div class="memory-notes">{content if content else "<em>No notes for this memory</em>"}</div>
|
||||
</div>
|
||||
|
||||
{await self.format_snapshot_media(snapshot, session)}
|
||||
</article>
|
||||
"""
|
||||
|
||||
return html_content.strip()
|
||||
@@ -385,26 +376,24 @@ class SnapshotDownloader:
|
||||
async def format_snapshot_media(
|
||||
self, snapshot: Dict[str, Any], session: aiohttp.ClientSession
|
||||
) -> str:
|
||||
"""Format media attachments for a snapshot."""
|
||||
"""Format media attachments for a snapshot with parent-friendly design."""
|
||||
media_html = ""
|
||||
|
||||
# Check for media (images and other files)
|
||||
media = snapshot.get("media", [])
|
||||
images = [m for m in media if m.get("type") == "image"]
|
||||
if images:
|
||||
media_html += '<div class="snapshot-images">\n'
|
||||
media_html += "<h4>📸 Images:</h4>\n"
|
||||
media_html += '<div class="image-grid">\n'
|
||||
media_html += '<div class="memory-photos">\n'
|
||||
media_html += '<div class="photo-grid">\n'
|
||||
|
||||
for image in images:
|
||||
# Download the image file
|
||||
local_path = await self.download_media_file(session, image)
|
||||
image_name = html.escape(image.get("fileName", "Image"))
|
||||
image_name = html.escape(image.get("fileName", "Photo"))
|
||||
|
||||
if local_path:
|
||||
media_html += '<div class="image-item">\n'
|
||||
media_html += '<div class="photo-item">\n'
|
||||
media_html += f' <img src="{local_path}" alt="{image_name}" loading="lazy">\n'
|
||||
media_html += f' <p class="image-meta">Updated: {self.format_date(image.get("updated", ""))}</p>\n'
|
||||
media_html += "</div>\n"
|
||||
else:
|
||||
# Fallback to API URL if download failed
|
||||
@@ -414,12 +403,8 @@ class SnapshotDownloader:
|
||||
else ""
|
||||
)
|
||||
if image_url:
|
||||
media_html += '<div class="image-item">\n'
|
||||
media_html += '<div class="photo-item">\n'
|
||||
media_html += f' <img src="{image_url}" alt="{image_name}" loading="lazy">\n'
|
||||
media_html += (
|
||||
f' <p class="image-caption">{image_name} (online)</p>\n'
|
||||
)
|
||||
media_html += f' <p class="image-meta">Updated: {self.format_date(image.get("updated", ""))}</p>\n'
|
||||
media_html += "</div>\n"
|
||||
|
||||
media_html += "</div>\n</div>\n"
|
||||
@@ -427,18 +412,16 @@ class SnapshotDownloader:
|
||||
# Check for non-image media as attachments
|
||||
attachments = [m for m in media if m.get("type") != "image"]
|
||||
if attachments:
|
||||
media_html += '<div class="snapshot-attachments">\n'
|
||||
media_html += "<h4>📎 Attachments:</h4>\n"
|
||||
media_html += '<div class="memory-attachments">\n'
|
||||
media_html += '<ul class="attachment-list">\n'
|
||||
|
||||
for attachment in attachments:
|
||||
# Download the attachment file
|
||||
local_path = await self.download_media_file(session, attachment)
|
||||
attachment_name = html.escape(attachment.get("fileName", "Attachment"))
|
||||
attachment_type = attachment.get("mimeType", "unknown")
|
||||
attachment_name = html.escape(attachment.get("fileName", "File"))
|
||||
|
||||
if local_path:
|
||||
media_html += f' <li><a href="{local_path}" target="_blank">{attachment_name} ({attachment_type})</a></li>\n'
|
||||
media_html += f' <li><a href="{local_path}" target="_blank">📎 {attachment_name}</a></li>\n'
|
||||
else:
|
||||
# Fallback to API URL if download failed
|
||||
attachment_url = (
|
||||
@@ -447,57 +430,29 @@ class SnapshotDownloader:
|
||||
else ""
|
||||
)
|
||||
if attachment_url:
|
||||
media_html += f' <li><a href="{attachment_url}" target="_blank">{attachment_name} ({attachment_type}) - online</a></li>\n'
|
||||
media_html += f' <li><a href="{attachment_url}" target="_blank">📎 {attachment_name}</a></li>\n'
|
||||
else:
|
||||
media_html += (
|
||||
f" <li>{attachment_name} ({attachment_type})</li>\n"
|
||||
)
|
||||
media_html += f" <li>📎 {attachment_name}</li>\n"
|
||||
|
||||
media_html += "</ul>\n</div>\n"
|
||||
|
||||
return media_html
|
||||
|
||||
def format_snapshot_metadata(self, snapshot: Dict[str, Any]) -> str:
|
||||
"""Format additional metadata for a snapshot."""
|
||||
metadata_html = '<div class="snapshot-metadata">\n'
|
||||
metadata_html += "<h4>ℹ️ Additional Information:</h4>\n"
|
||||
metadata_html += '<div class="metadata-grid">\n'
|
||||
|
||||
# Add any additional fields that might be interesting
|
||||
metadata_fields = [
|
||||
("code", "Code"),
|
||||
("signed", "Signed Status"),
|
||||
("type", "Type"),
|
||||
]
|
||||
|
||||
for field, label in metadata_fields:
|
||||
value = snapshot.get(field)
|
||||
if value:
|
||||
if isinstance(value, list):
|
||||
value = ", ".join(str(v) for v in value)
|
||||
metadata_html += '<div class="metadata-item">\n'
|
||||
metadata_html += (
|
||||
f" <strong>{label}:</strong> {html.escape(str(value))}\n"
|
||||
)
|
||||
metadata_html += "</div>\n"
|
||||
|
||||
# Raw JSON data (collapsed by default)
|
||||
metadata_html += '<details class="raw-data">\n'
|
||||
metadata_html += "<summary>🔍 Raw JSON Data</summary>\n"
|
||||
metadata_html += '<pre class="json-data">\n'
|
||||
metadata_html += html.escape(json.dumps(snapshot, indent=2, default=str))
|
||||
metadata_html += "\n</pre>\n"
|
||||
metadata_html += "</details>\n"
|
||||
|
||||
metadata_html += "</div>\n</div>\n"
|
||||
return metadata_html
|
||||
|
||||
def format_date(self, date_string: str) -> str:
|
||||
"""Format a date string for display."""
|
||||
"""Format a date string for parent-friendly display."""
|
||||
try:
|
||||
# Try to parse ISO format date
|
||||
dt = datetime.fromisoformat(date_string.replace("Z", "+00:00"))
|
||||
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||
# Format as "Monday, January 5, 2024 at 2:30 PM"
|
||||
return dt.strftime("%A, %B %d, %Y at %I:%M %p").replace(" 0", " ")
|
||||
except:
|
||||
return date_string
|
||||
|
||||
def format_date_short(self, date_string: str) -> str:
|
||||
"""Format a date string for compact display."""
|
||||
try:
|
||||
dt = datetime.fromisoformat(date_string.replace("Z", "+00:00"))
|
||||
return dt.strftime("%B %d, %Y")
|
||||
except:
|
||||
return date_string
|
||||
|
||||
@@ -595,7 +550,7 @@ class SnapshotDownloader:
|
||||
async def generate_html_template(
|
||||
self, snapshots: List[Dict[str, Any]], date_from: str, date_to: str
|
||||
) -> str:
|
||||
"""Generate the complete HTML template."""
|
||||
"""Generate the complete HTML template with parent-friendly design."""
|
||||
|
||||
# Generate individual snapshot HTML
|
||||
snapshots_html = ""
|
||||
@@ -610,18 +565,24 @@ class SnapshotDownloader:
|
||||
# Authenticate session for media downloads
|
||||
await self.authenticate()
|
||||
|
||||
for snapshot in snapshots:
|
||||
snapshot_html = await self.format_snapshot_html(snapshot, session)
|
||||
for index, snapshot in enumerate(snapshots):
|
||||
snapshot_html = await self.format_snapshot_html(snapshot, session, index)
|
||||
snapshots_html += snapshot_html
|
||||
snapshots_html += "\n\n"
|
||||
|
||||
# Create the complete HTML document
|
||||
# Format dates for display
|
||||
friendly_date_range = self._format_date_range_friendly(date_from, date_to)
|
||||
|
||||
# Create the complete HTML document with parent-friendly design
|
||||
html_template = f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ParentZone Snapshots Backup - {date_from} to {date_to}</title>
|
||||
<title>My Child's Memories - {friendly_date_range}</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
{self.get_css_styles()}
|
||||
</style>
|
||||
@@ -629,28 +590,22 @@ class SnapshotDownloader:
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="page-header">
|
||||
<h1>ParentZone Snapshots Backup</h1>
|
||||
<div class="date-range">
|
||||
<strong>Period:</strong> {date_from} to {date_to}
|
||||
</div>
|
||||
<div class="stats">
|
||||
<span class="stat-item">Total Snapshots: <strong>{len(snapshots)}</strong></span>
|
||||
<span class="stat-item">Generated: <strong>{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</strong></span>
|
||||
</div>
|
||||
<div class="header-icon">🌟</div>
|
||||
<h1>My Child's Memories</h1>
|
||||
<p class="date-range">{friendly_date_range}</p>
|
||||
<div class="memories-count">📚 {len(snapshots)} precious moment{"s" if len(snapshots) != 1 else ""}</div>
|
||||
</header>
|
||||
|
||||
<nav class="navigation">
|
||||
<button onclick="toggleAllDetails()">Toggle All Details</button>
|
||||
<input type="text" id="searchBox" placeholder="Search snapshots..." onkeyup="searchSnapshots()">
|
||||
<nav class="search-bar">
|
||||
<input type="text" id="searchBox" placeholder="🔍 Search memories..." onkeyup="searchSnapshots()">
|
||||
</nav>
|
||||
|
||||
<main class="snapshots-container">
|
||||
<main class="memories-container">
|
||||
{snapshots_html}
|
||||
</main>
|
||||
|
||||
<footer class="page-footer">
|
||||
<p>Generated by ParentZone Snapshot Downloader</p>
|
||||
<p>Total snapshots: {len(snapshots)} | Pages fetched: {self.stats["pages_fetched"]}</p>
|
||||
<p>💝 Memories preserved with love</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -662,8 +617,23 @@ class SnapshotDownloader:
|
||||
|
||||
return html_template
|
||||
|
||||
def _format_date_range_friendly(self, date_from: str, date_to: str) -> str:
|
||||
"""Format a date range in a parent-friendly way."""
|
||||
try:
|
||||
start = datetime.strptime(date_from, '%Y-%m-%d')
|
||||
end = datetime.strptime(date_to, '%Y-%m-%d')
|
||||
|
||||
if start.year == end.year and start.month == end.month:
|
||||
return f"{start.strftime('%B')} {start.day} - {end.day}, {start.year}"
|
||||
elif start.year == end.year:
|
||||
return f"{start.strftime('%B %d')} - {end.strftime('%B %d')}, {start.year}"
|
||||
else:
|
||||
return f"{start.strftime('%B %d, %Y')} - {end.strftime('%B %d, %Y')}"
|
||||
except ValueError:
|
||||
return f"{date_from} to {date_to}"
|
||||
|
||||
def get_css_styles(self) -> str:
|
||||
"""Get CSS styles for the HTML file."""
|
||||
"""Get CSS styles for the HTML file with parent-friendly design."""
|
||||
return """
|
||||
* {
|
||||
margin: 0;
|
||||
@@ -672,321 +642,269 @@ class SnapshotDownloader:
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-family: 'Nunito', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #495057;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 50%, #f1f3f4 100%);
|
||||
color: #4A4A4A;
|
||||
background: linear-gradient(135deg, #FFF5F5 0%, #FFF9E6 50%, #F0FFF4 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 4px 20px rgba(108, 117, 125, 0.15);
|
||||
border: 2px solid #dee2e6;
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 16px;
|
||||
animation: gentle-bounce 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes gentle-bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: #495057;
|
||||
margin-bottom: 10px;
|
||||
font-size: 2.5em;
|
||||
font-weight: 600;
|
||||
font-size: 2.2rem;
|
||||
font-weight: 700;
|
||||
color: #2D3748;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.date-range {
|
||||
font-size: 1.2em;
|
||||
color: #6c757d;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.1rem;
|
||||
color: #718096;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
.memories-count {
|
||||
display: inline-block;
|
||||
background: white;
|
||||
padding: 10px 24px;
|
||||
border-radius: 24px;
|
||||
font-weight: 600;
|
||||
color: #667EEA;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
color: #495057;
|
||||
font-size: 1.1em;
|
||||
.search-bar {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
padding: 20px;
|
||||
border-radius: 15px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 10px rgba(96, 125, 139, 0.1);
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.navigation button {
|
||||
background: linear-gradient(135deg, #6c757d 0%, #495057 100%);
|
||||
color: white;
|
||||
.search-bar input {
|
||||
width: 100%;
|
||||
padding: 16px 20px;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
border-radius: 16px;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
background: white;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.navigation button:hover {
|
||||
background: linear-gradient(135deg, #495057 0%, #343a40 100%);
|
||||
}
|
||||
|
||||
.navigation input {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.navigation input:focus {
|
||||
.search-bar input:focus {
|
||||
outline: none;
|
||||
border-color: #6c757d;
|
||||
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.snapshots-container {
|
||||
.search-bar input::placeholder {
|
||||
color: #A0AEC0;
|
||||
}
|
||||
|
||||
.memories-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.snapshot {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 2px 10px rgba(96, 125, 139, 0.1);
|
||||
.memory-card {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 16px rgba(0,0,0,0.06);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
border-left: 5px solid var(--accent, #FFE4E1);
|
||||
}
|
||||
|
||||
.snapshot:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 20px rgba(96, 125, 139, 0.15);
|
||||
.memory-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 30px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.snapshot-header {
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #e8eaf0;
|
||||
padding-bottom: 15px;
|
||||
.memory-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.snapshot-title {
|
||||
color: #495057;
|
||||
font-size: 1.8em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.snapshot-meta {
|
||||
.memory-title-section {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
color: #6c757d;
|
||||
font-size: 0.9em;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.snapshot-content > div {
|
||||
margin-bottom: 15px;
|
||||
.memory-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #2D3748;
|
||||
}
|
||||
|
||||
.snapshot-author, .snapshot-child, .snapshot-activity {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
padding: 10px;
|
||||
border-radius: 12px;
|
||||
.memory-date {
|
||||
font-size: 0.95rem;
|
||||
color: #718096;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.snapshot-description {
|
||||
background: linear-gradient(135deg, #fafbfc 0%, #f0f8ff 100%);
|
||||
.memory-details {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.memory-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 14px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.memory-tag.teacher {
|
||||
background: #E8F5E9;
|
||||
color: #2E7D32;
|
||||
}
|
||||
|
||||
.memory-tag.activity {
|
||||
background: #FFF3E0;
|
||||
color: #E65100;
|
||||
}
|
||||
|
||||
.memory-content {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.memory-notes {
|
||||
background: linear-gradient(135deg, #FAFBFC 0%, #F7FAFC 100%);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border-left: 4px solid #6c757d;
|
||||
border-radius: 16px;
|
||||
line-height: 1.8;
|
||||
color: #4A5568;
|
||||
}
|
||||
|
||||
.snapshot-description p {
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.6;
|
||||
.memory-notes p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.snapshot-description p:last-child {
|
||||
.memory-notes p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.snapshot-description br {
|
||||
display: block;
|
||||
margin: 10px 0;
|
||||
content: " ";
|
||||
.memory-notes em {
|
||||
color: #A0AEC0;
|
||||
}
|
||||
|
||||
.snapshot-description strong {
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
.memory-photos {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.snapshot-description em {
|
||||
font-style: italic;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.snapshot-description .notes-content {
|
||||
/* Container for HTML notes content */
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.snapshot-description span[style] {
|
||||
/* Preserve inline styles from the notes HTML */
|
||||
}
|
||||
|
||||
.snapshot-images {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
.photo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 10px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
text-align: center;
|
||||
.photo-item {
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.image-item img {
|
||||
max-width: 100%;
|
||||
.photo-item img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(96, 125, 139, 0.1);
|
||||
display: block;
|
||||
max-height: 400px;
|
||||
object-fit: contain;
|
||||
background: linear-gradient(135deg, #fafbfc 0%, #f0f8ff 100%);
|
||||
object-fit: cover;
|
||||
background: #F7FAFC;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.image-caption {
|
||||
margin-top: 5px;
|
||||
font-size: 0.9em;
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
.photo-item:hover img {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.image-meta {
|
||||
margin-top: 3px;
|
||||
font-size: 0.8em;
|
||||
color: #95a5a6;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.snapshot-attachments {
|
||||
margin: 20px 0;
|
||||
.memory-attachments {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.attachment-list li {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #e8eaf0;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #EDF2F7;
|
||||
}
|
||||
|
||||
.attachment-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.attachment-list a {
|
||||
color: #495057;
|
||||
color: #667EEA;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.attachment-list a:hover {
|
||||
color: #5A67D8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.snapshot-metadata {
|
||||
margin-top: 20px;
|
||||
background: linear-gradient(135deg, #fafbfc 0%, #f0f8ff 100%);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.metadata-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.metadata-item {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.raw-data {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.raw-data summary {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.json-data {
|
||||
background: #2c3e50;
|
||||
color: #ecf0f1;
|
||||
padding: 15px;
|
||||
border-radius: 12px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.9em;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.page-footer {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
padding: 20px;
|
||||
border-radius: 15px;
|
||||
margin-top: 30px;
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 10px rgba(96, 125, 139, 0.1);
|
||||
color: #6c757d;
|
||||
color: #A0AEC0;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: #495057;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: 600px) {
|
||||
.container {
|
||||
padding: 10px;
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 2em;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
flex-direction: column;
|
||||
.memory-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
.memory-title {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.snapshot-meta {
|
||||
.memory-title-section {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.photo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -994,45 +912,34 @@ class SnapshotDownloader:
|
||||
def get_javascript_functions(self) -> str:
|
||||
"""Get JavaScript functions for the HTML file."""
|
||||
return """
|
||||
function toggleAllDetails() {
|
||||
const details = document.querySelectorAll('details');
|
||||
const allOpen = Array.from(details).every(detail => detail.open);
|
||||
|
||||
details.forEach(detail => {
|
||||
detail.open = !allOpen;
|
||||
});
|
||||
}
|
||||
|
||||
function searchSnapshots() {
|
||||
const searchTerm = document.getElementById('searchBox').value.toLowerCase();
|
||||
const snapshots = document.querySelectorAll('.snapshot');
|
||||
const memories = document.querySelectorAll('.memory-card');
|
||||
let visibleCount = 0;
|
||||
|
||||
snapshots.forEach(snapshot => {
|
||||
const text = snapshot.textContent.toLowerCase();
|
||||
memories.forEach(memory => {
|
||||
const text = memory.textContent.toLowerCase();
|
||||
if (text.includes(searchTerm)) {
|
||||
snapshot.style.display = 'block';
|
||||
memory.style.display = 'block';
|
||||
visibleCount++;
|
||||
} else {
|
||||
snapshot.style.display = 'none';
|
||||
memory.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add smooth scrolling for internal links
|
||||
// Add smooth fade-in animation on load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Add click handlers for snapshot titles to make them collapsible
|
||||
const titles = document.querySelectorAll('.snapshot-title');
|
||||
titles.forEach(title => {
|
||||
title.style.cursor = 'pointer';
|
||||
title.addEventListener('click', function() {
|
||||
const content = this.closest('.snapshot').querySelector('.snapshot-content');
|
||||
if (content.style.display === 'none') {
|
||||
content.style.display = 'block';
|
||||
this.style.opacity = '1';
|
||||
} else {
|
||||
content.style.display = 'none';
|
||||
this.style.opacity = '0.7';
|
||||
}
|
||||
});
|
||||
const memories = document.querySelectorAll('.memory-card');
|
||||
memories.forEach((memory, index) => {
|
||||
memory.style.opacity = '0';
|
||||
memory.style.transform = 'translateY(20px)';
|
||||
memory.style.transition = 'opacity 0.4s ease, transform 0.4s ease';
|
||||
|
||||
setTimeout(() => {
|
||||
memory.style.opacity = '1';
|
||||
memory.style.transform = 'translateY(0)';
|
||||
}, index * 80);
|
||||
});
|
||||
});
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user