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
|
return all_snapshots
|
||||||
|
|
||||||
async def format_snapshot_html(
|
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:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Format a single snapshot as HTML.
|
Format a single snapshot as HTML with parent-friendly design.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
snapshot: Snapshot dictionary from API
|
snapshot: Snapshot dictionary from API
|
||||||
|
index: Index for alternating accent colors
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
HTML string for the snapshot
|
HTML string for the snapshot
|
||||||
@@ -314,10 +315,9 @@ class SnapshotDownloader:
|
|||||||
snapshot_id = snapshot.get("id", "unknown")
|
snapshot_id = snapshot.get("id", "unknown")
|
||||||
content = snapshot.get("notes", "") # Don't escape HTML in notes field
|
content = snapshot.get("notes", "") # Don't escape HTML in notes field
|
||||||
start_time = snapshot.get("startTime", "")
|
start_time = snapshot.get("startTime", "")
|
||||||
snapshot_type = snapshot.get("type", "Snapshot")
|
|
||||||
|
|
||||||
# Format dates
|
# Format dates in friendly format
|
||||||
start_date = self.format_date(start_time) if start_time else "Unknown"
|
start_date = self.format_date_short(start_time) if start_time else ""
|
||||||
|
|
||||||
# Extract additional information
|
# Extract additional information
|
||||||
author = snapshot.get("author", {})
|
author = snapshot.get("author", {})
|
||||||
@@ -326,58 +326,49 @@ class SnapshotDownloader:
|
|||||||
author_name = (
|
author_name = (
|
||||||
html.escape(f"{author_forename} {author_surname}".strip())
|
html.escape(f"{author_forename} {author_surname}".strip())
|
||||||
if author
|
if author
|
||||||
else "Unknown"
|
else ""
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extract child information (if any)
|
# Extract child information (if any)
|
||||||
child = snapshot.get("child", {})
|
child = snapshot.get("child", {})
|
||||||
child_forename = child.get("forename", "") if child else ""
|
child_forename = child.get("forename", "") if child else ""
|
||||||
child_name = (
|
child_name = html.escape(child_forename) if child_forename else ""
|
||||||
html.escape(
|
|
||||||
f"{child.get('forename', '')} {child.get('surname', '')}".strip()
|
|
||||||
)
|
|
||||||
if child
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create title in format: "Child Forename by Author Forename Surname"
|
# Create friendly title
|
||||||
if child_forename and author_forename:
|
if child_forename:
|
||||||
title = html.escape(
|
title = html.escape(child_forename)
|
||||||
f"{child_forename} by {author_forename} {author_surname}".strip()
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
title = html.escape(f"Snapshot {snapshot_id}")
|
title = "Memory"
|
||||||
|
|
||||||
# Extract location/activity information
|
# Extract location/activity information
|
||||||
activity = snapshot.get("activity", {})
|
activity = snapshot.get("activity", {})
|
||||||
activity_name = html.escape(activity.get("name", "")) if activity else ""
|
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"""
|
html_content = f"""
|
||||||
<div class="snapshot" id="snapshot-{snapshot_id}">
|
<article class="memory-card" id="snapshot-{snapshot_id}" style="--accent: {accent}">
|
||||||
<div class="snapshot-header">
|
<div class="memory-header">
|
||||||
<h3 class="snapshot-title">{title}</h3>
|
<div class="memory-title-section">
|
||||||
<div class="snapshot-meta">
|
<h3 class="memory-title">{title}</h3>
|
||||||
<span class="snapshot-id">ID: {snapshot_id}</span>
|
{f'<span class="memory-date">📅 {start_date}</span>' if start_date else ''}
|
||||||
<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>'}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="snapshot-content">
|
<div class="memory-details">
|
||||||
{f'<div class="snapshot-author">👤 Author: {author_name}</div>' if author_name != "Unknown" else ""}
|
{f'<span class="memory-tag teacher">👩🏫 {author_name}</span>' if author_name else ''}
|
||||||
{f'<div class="snapshot-child">👶 Child: {child_name}</div>' if child_name else ""}
|
{f'<span class="memory-tag activity">🎯 {activity_name}</span>' if activity_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>
|
</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()
|
return html_content.strip()
|
||||||
@@ -385,26 +376,24 @@ class SnapshotDownloader:
|
|||||||
async def format_snapshot_media(
|
async def format_snapshot_media(
|
||||||
self, snapshot: Dict[str, Any], session: aiohttp.ClientSession
|
self, snapshot: Dict[str, Any], session: aiohttp.ClientSession
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Format media attachments for a snapshot."""
|
"""Format media attachments for a snapshot with parent-friendly design."""
|
||||||
media_html = ""
|
media_html = ""
|
||||||
|
|
||||||
# Check for media (images and other files)
|
# Check for media (images and other files)
|
||||||
media = snapshot.get("media", [])
|
media = snapshot.get("media", [])
|
||||||
images = [m for m in media if m.get("type") == "image"]
|
images = [m for m in media if m.get("type") == "image"]
|
||||||
if images:
|
if images:
|
||||||
media_html += '<div class="snapshot-images">\n'
|
media_html += '<div class="memory-photos">\n'
|
||||||
media_html += "<h4>📸 Images:</h4>\n"
|
media_html += '<div class="photo-grid">\n'
|
||||||
media_html += '<div class="image-grid">\n'
|
|
||||||
|
|
||||||
for image in images:
|
for image in images:
|
||||||
# Download the image file
|
# Download the image file
|
||||||
local_path = await self.download_media_file(session, image)
|
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:
|
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' <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"
|
media_html += "</div>\n"
|
||||||
else:
|
else:
|
||||||
# Fallback to API URL if download failed
|
# Fallback to API URL if download failed
|
||||||
@@ -414,12 +403,8 @@ class SnapshotDownloader:
|
|||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
if image_url:
|
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' <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"
|
||||||
|
|
||||||
media_html += "</div>\n</div>\n"
|
media_html += "</div>\n</div>\n"
|
||||||
@@ -427,18 +412,16 @@ class SnapshotDownloader:
|
|||||||
# Check for non-image media as attachments
|
# Check for non-image media as attachments
|
||||||
attachments = [m for m in media if m.get("type") != "image"]
|
attachments = [m for m in media if m.get("type") != "image"]
|
||||||
if attachments:
|
if attachments:
|
||||||
media_html += '<div class="snapshot-attachments">\n'
|
media_html += '<div class="memory-attachments">\n'
|
||||||
media_html += "<h4>📎 Attachments:</h4>\n"
|
|
||||||
media_html += '<ul class="attachment-list">\n'
|
media_html += '<ul class="attachment-list">\n'
|
||||||
|
|
||||||
for attachment in attachments:
|
for attachment in attachments:
|
||||||
# Download the attachment file
|
# Download the attachment file
|
||||||
local_path = await self.download_media_file(session, attachment)
|
local_path = await self.download_media_file(session, attachment)
|
||||||
attachment_name = html.escape(attachment.get("fileName", "Attachment"))
|
attachment_name = html.escape(attachment.get("fileName", "File"))
|
||||||
attachment_type = attachment.get("mimeType", "unknown")
|
|
||||||
|
|
||||||
if local_path:
|
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:
|
else:
|
||||||
# Fallback to API URL if download failed
|
# Fallback to API URL if download failed
|
||||||
attachment_url = (
|
attachment_url = (
|
||||||
@@ -447,57 +430,29 @@ class SnapshotDownloader:
|
|||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
if attachment_url:
|
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:
|
else:
|
||||||
media_html += (
|
media_html += f" <li>📎 {attachment_name}</li>\n"
|
||||||
f" <li>{attachment_name} ({attachment_type})</li>\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
media_html += "</ul>\n</div>\n"
|
media_html += "</ul>\n</div>\n"
|
||||||
|
|
||||||
return media_html
|
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:
|
def format_date(self, date_string: str) -> str:
|
||||||
"""Format a date string for display."""
|
"""Format a date string for parent-friendly display."""
|
||||||
try:
|
try:
|
||||||
# Try to parse ISO format date
|
# Try to parse ISO format date
|
||||||
dt = datetime.fromisoformat(date_string.replace("Z", "+00:00"))
|
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:
|
except:
|
||||||
return date_string
|
return date_string
|
||||||
|
|
||||||
@@ -595,7 +550,7 @@ class SnapshotDownloader:
|
|||||||
async def generate_html_template(
|
async def generate_html_template(
|
||||||
self, snapshots: List[Dict[str, Any]], date_from: str, date_to: str
|
self, snapshots: List[Dict[str, Any]], date_from: str, date_to: str
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Generate the complete HTML template."""
|
"""Generate the complete HTML template with parent-friendly design."""
|
||||||
|
|
||||||
# Generate individual snapshot HTML
|
# Generate individual snapshot HTML
|
||||||
snapshots_html = ""
|
snapshots_html = ""
|
||||||
@@ -610,18 +565,24 @@ class SnapshotDownloader:
|
|||||||
# Authenticate session for media downloads
|
# Authenticate session for media downloads
|
||||||
await self.authenticate()
|
await self.authenticate()
|
||||||
|
|
||||||
for snapshot in snapshots:
|
for index, snapshot in enumerate(snapshots):
|
||||||
snapshot_html = await self.format_snapshot_html(snapshot, session)
|
snapshot_html = await self.format_snapshot_html(snapshot, session, index)
|
||||||
snapshots_html += snapshot_html
|
snapshots_html += snapshot_html
|
||||||
snapshots_html += "\n\n"
|
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_template = f"""<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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>
|
<style>
|
||||||
{self.get_css_styles()}
|
{self.get_css_styles()}
|
||||||
</style>
|
</style>
|
||||||
@@ -629,28 +590,22 @@ class SnapshotDownloader:
|
|||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header class="page-header">
|
<header class="page-header">
|
||||||
<h1>ParentZone Snapshots Backup</h1>
|
<div class="header-icon">🌟</div>
|
||||||
<div class="date-range">
|
<h1>My Child's Memories</h1>
|
||||||
<strong>Period:</strong> {date_from} to {date_to}
|
<p class="date-range">{friendly_date_range}</p>
|
||||||
</div>
|
<div class="memories-count">📚 {len(snapshots)} precious moment{"s" if len(snapshots) != 1 else ""}</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>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav class="navigation">
|
<nav class="search-bar">
|
||||||
<button onclick="toggleAllDetails()">Toggle All Details</button>
|
<input type="text" id="searchBox" placeholder="🔍 Search memories..." onkeyup="searchSnapshots()">
|
||||||
<input type="text" id="searchBox" placeholder="Search snapshots..." onkeyup="searchSnapshots()">
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main class="snapshots-container">
|
<main class="memories-container">
|
||||||
{snapshots_html}
|
{snapshots_html}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="page-footer">
|
<footer class="page-footer">
|
||||||
<p>Generated by ParentZone Snapshot Downloader</p>
|
<p>💝 Memories preserved with love</p>
|
||||||
<p>Total snapshots: {len(snapshots)} | Pages fetched: {self.stats["pages_fetched"]}</p>
|
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -661,9 +616,24 @@ class SnapshotDownloader:
|
|||||||
</html>"""
|
</html>"""
|
||||||
|
|
||||||
return html_template
|
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:
|
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 """
|
return """
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -672,321 +642,269 @@ class SnapshotDownloader:
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: 'Nunito', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: #495057;
|
color: #4A4A4A;
|
||||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 50%, #f1f3f4 100%);
|
background: linear-gradient(135deg, #FFF5F5 0%, #FFF9E6 50%, #F0FFF4 100%);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
max-width: 900px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px;
|
padding: 40px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.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;
|
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 {
|
.page-header h1 {
|
||||||
color: #495057;
|
font-size: 2.2rem;
|
||||||
margin-bottom: 10px;
|
font-weight: 700;
|
||||||
font-size: 2.5em;
|
color: #2D3748;
|
||||||
font-weight: 600;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-range {
|
.date-range {
|
||||||
font-size: 1.2em;
|
font-size: 1.1rem;
|
||||||
color: #6c757d;
|
color: #718096;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats {
|
.memories-count {
|
||||||
display: flex;
|
display: inline-block;
|
||||||
justify-content: center;
|
background: white;
|
||||||
gap: 20px;
|
padding: 10px 24px;
|
||||||
flex-wrap: wrap;
|
border-radius: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #667EEA;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-item {
|
.search-bar {
|
||||||
color: #495057;
|
margin-bottom: 30px;
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation {
|
.search-bar input {
|
||||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
width: 100%;
|
||||||
padding: 20px;
|
padding: 16px 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;
|
|
||||||
border: none;
|
border: none;
|
||||||
padding: 10px 20px;
|
border-radius: 16px;
|
||||||
border-radius: 12px;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
font-family: inherit;
|
||||||
font-size: 1em;
|
background: white;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation button:hover {
|
.search-bar input:focus {
|
||||||
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 {
|
|
||||||
outline: none;
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot {
|
.memory-card {
|
||||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
background: white;
|
||||||
border-radius: 15px;
|
border-radius: 20px;
|
||||||
padding: 25px;
|
padding: 24px;
|
||||||
box-shadow: 0 2px 10px rgba(96, 125, 139, 0.1);
|
box-shadow: 0 2px 16px rgba(0,0,0,0.06);
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
border-left: 5px solid var(--accent, #FFE4E1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot:hover {
|
.memory-card:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-3px);
|
||||||
box-shadow: 0 4px 20px rgba(96, 125, 139, 0.15);
|
box-shadow: 0 8px 30px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-header {
|
.memory-header {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 16px;
|
||||||
border-bottom: 2px solid #e8eaf0;
|
|
||||||
padding-bottom: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-title {
|
.memory-title-section {
|
||||||
color: #495057;
|
|
||||||
font-size: 1.8em;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snapshot-meta {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
color: #6c757d;
|
gap: 12px;
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-content > div {
|
.memory-title {
|
||||||
margin-bottom: 15px;
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2D3748;
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-author, .snapshot-child, .snapshot-activity {
|
.memory-date {
|
||||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
font-size: 0.95rem;
|
||||||
padding: 10px;
|
color: #718096;
|
||||||
border-radius: 12px;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-description {
|
.memory-details {
|
||||||
background: linear-gradient(135deg, #fafbfc 0%, #f0f8ff 100%);
|
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;
|
padding: 20px;
|
||||||
border-radius: 12px;
|
border-radius: 16px;
|
||||||
border-left: 4px solid #6c757d;
|
line-height: 1.8;
|
||||||
|
color: #4A5568;
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-description p {
|
.memory-notes p {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 12px;
|
||||||
line-height: 1.6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-description p:last-child {
|
.memory-notes p:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-description br {
|
.memory-notes em {
|
||||||
display: block;
|
color: #A0AEC0;
|
||||||
margin: 10px 0;
|
|
||||||
content: " ";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-description strong {
|
.memory-photos {
|
||||||
font-weight: bold;
|
margin-top: 20px;
|
||||||
color: #495057;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-description em {
|
.photo-grid {
|
||||||
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 {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
gap: 15px;
|
gap: 16px;
|
||||||
margin-top: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-item {
|
.photo-item {
|
||||||
text-align: center;
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-item img {
|
.photo-item img {
|
||||||
max-width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
border-radius: 12px;
|
display: block;
|
||||||
box-shadow: 0 2px 8px rgba(96, 125, 139, 0.1);
|
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
object-fit: contain;
|
object-fit: cover;
|
||||||
background: linear-gradient(135deg, #fafbfc 0%, #f0f8ff 100%);
|
background: #F7FAFC;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-caption {
|
.photo-item:hover img {
|
||||||
margin-top: 5px;
|
transform: scale(1.02);
|
||||||
font-size: 0.9em;
|
|
||||||
color: #6c757d;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-meta {
|
.memory-attachments {
|
||||||
margin-top: 3px;
|
margin-top: 16px;
|
||||||
font-size: 0.8em;
|
|
||||||
color: #95a5a6;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snapshot-attachments {
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-list {
|
.attachment-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding-left: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-list li {
|
.attachment-list li {
|
||||||
padding: 8px 0;
|
padding: 10px 0;
|
||||||
border-bottom: 1px solid #e8eaf0;
|
border-bottom: 1px solid #EDF2F7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-list li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-list a {
|
.attachment-list a {
|
||||||
color: #495057;
|
color: #667EEA;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-list a:hover {
|
.attachment-list a:hover {
|
||||||
|
color: #5A67D8;
|
||||||
text-decoration: underline;
|
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 {
|
.page-footer {
|
||||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
margin-top: 50px;
|
||||||
padding: 20px;
|
|
||||||
border-radius: 15px;
|
|
||||||
margin-top: 30px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0 2px 10px rgba(96, 125, 139, 0.1);
|
color: #A0AEC0;
|
||||||
color: #6c757d;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
@media (max-width: 600px) {
|
||||||
color: #495057;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.container {
|
.container {
|
||||||
padding: 10px;
|
padding: 24px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header h1 {
|
.page-header h1 {
|
||||||
font-size: 2em;
|
font-size: 1.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation {
|
.memory-card {
|
||||||
flex-direction: column;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats {
|
.memory-title {
|
||||||
flex-direction: column;
|
font-size: 1.3rem;
|
||||||
gap: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-meta {
|
.memory-title-section {
|
||||||
flex-direction: column;
|
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:
|
def get_javascript_functions(self) -> str:
|
||||||
"""Get JavaScript functions for the HTML file."""
|
"""Get JavaScript functions for the HTML file."""
|
||||||
return """
|
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() {
|
function searchSnapshots() {
|
||||||
const searchTerm = document.getElementById('searchBox').value.toLowerCase();
|
const searchTerm = document.getElementById('searchBox').value.toLowerCase();
|
||||||
const snapshots = document.querySelectorAll('.snapshot');
|
const memories = document.querySelectorAll('.memory-card');
|
||||||
|
let visibleCount = 0;
|
||||||
|
|
||||||
snapshots.forEach(snapshot => {
|
memories.forEach(memory => {
|
||||||
const text = snapshot.textContent.toLowerCase();
|
const text = memory.textContent.toLowerCase();
|
||||||
if (text.includes(searchTerm)) {
|
if (text.includes(searchTerm)) {
|
||||||
snapshot.style.display = 'block';
|
memory.style.display = 'block';
|
||||||
|
visibleCount++;
|
||||||
} else {
|
} 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() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Add click handlers for snapshot titles to make them collapsible
|
const memories = document.querySelectorAll('.memory-card');
|
||||||
const titles = document.querySelectorAll('.snapshot-title');
|
memories.forEach((memory, index) => {
|
||||||
titles.forEach(title => {
|
memory.style.opacity = '0';
|
||||||
title.style.cursor = 'pointer';
|
memory.style.transform = 'translateY(20px)';
|
||||||
title.addEventListener('click', function() {
|
memory.style.transition = 'opacity 0.4s ease, transform 0.4s ease';
|
||||||
const content = this.closest('.snapshot').querySelector('.snapshot-content');
|
|
||||||
if (content.style.display === 'none') {
|
setTimeout(() => {
|
||||||
content.style.display = 'block';
|
memory.style.opacity = '1';
|
||||||
this.style.opacity = '1';
|
memory.style.transform = 'translateY(0)';
|
||||||
} else {
|
}, index * 80);
|
||||||
content.style.display = 'none';
|
|
||||||
this.style.opacity = '0.7';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user