From 325e8b15e988c520e9ab95a1868e053eb2f56fee Mon Sep 17 00:00:00 2001 From: Tudor Sitaru Date: Mon, 5 Jan 2026 10:56:36 +0000 Subject: [PATCH] Reworked snapshot template output --- src/snapshot_downloader.py | 625 ++++++++++++++++--------------------- 1 file changed, 266 insertions(+), 359 deletions(-) diff --git a/src/snapshot_downloader.py b/src/snapshot_downloader.py index 5850859..8fbe820 100644 --- a/src/snapshot_downloader.py +++ b/src/snapshot_downloader.py @@ -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""" -
-
-

{title}

-
- ID: {snapshot_id} - Type: {snapshot_type} - Date: {start_date} - {f'{"✓ Signed" if snapshot.get("signed", False) else "âŗ Pending"}'} +
+
+
+

{title}

+ {f'📅 {start_date}' if start_date else ''}
-
- {f'
👤 Author: {author_name}
' if author_name != "Unknown" else ""} - {f'
đŸ‘ļ Child: {child_name}
' if child_name else ""} - {f'
đŸŽ¯ Activity: {activity_name}
' if activity_name else ""} - -
-
{content if content else "No description provided"}
-
- - {await self.format_snapshot_media(snapshot, session)} - {self.format_snapshot_metadata(snapshot)} +
+ {f'👩‍đŸĢ {author_name}' if author_name else ''} + {f'đŸŽ¯ {activity_name}' if activity_name else ''}
-
+ +
+
{content if content else "No notes for this memory"}
+
+ + {await self.format_snapshot_media(snapshot, session)} +
""" 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 += '
\n' - media_html += "

📸 Images:

\n" - media_html += '
\n' + media_html += '
\n' + media_html += '
\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 += '
\n' + media_html += '
\n' media_html += f' {image_name}\n' - media_html += f'

Updated: {self.format_date(image.get("updated", ""))}

\n' media_html += "
\n" else: # Fallback to API URL if download failed @@ -414,12 +403,8 @@ class SnapshotDownloader: else "" ) if image_url: - media_html += '
\n' + media_html += '
\n' media_html += f' {image_name}\n' - media_html += ( - f'

{image_name} (online)

\n' - ) - media_html += f'

Updated: {self.format_date(image.get("updated", ""))}

\n' media_html += "
\n" media_html += "
\n
\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 += '
\n' - media_html += "

📎 Attachments:

\n" + media_html += '
\n' media_html += '
    \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'
  • {attachment_name} ({attachment_type})
  • \n' + media_html += f'
  • 📎 {attachment_name}
  • \n' else: # Fallback to API URL if download failed attachment_url = ( @@ -447,57 +430,29 @@ class SnapshotDownloader: else "" ) if attachment_url: - media_html += f'
  • {attachment_name} ({attachment_type}) - online
  • \n' + media_html += f'
  • 📎 {attachment_name}
  • \n' else: - media_html += ( - f"
  • {attachment_name} ({attachment_type})
  • \n" - ) + media_html += f"
  • 📎 {attachment_name}
  • \n" media_html += "
\n
\n" return media_html - def format_snapshot_metadata(self, snapshot: Dict[str, Any]) -> str: - """Format additional metadata for a snapshot.""" - metadata_html = '\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""" - ParentZone Snapshots Backup - {date_from} to {date_to} + My Child's Memories - {friendly_date_range} + + + @@ -629,28 +590,22 @@ class SnapshotDownloader:
-
@@ -661,9 +616,24 @@ 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); }); }); """