From 4ec1feb2a97b9ddd60823243270840df432c2115 Mon Sep 17 00:00:00 2001 From: mi Date: Sat, 15 Nov 2025 16:55:51 +1000 Subject: [PATCH] :sparkles: comic embed --- app.py | 18 +++- comics_data.py | 4 + static/css/style.css | 184 +++++++++++++++++++++++++++++++++++++++++ static/js/comic-nav.js | 53 ++++++++++++ templates/base.html | 23 +++++- templates/comic.html | 6 ++ templates/index.html | 6 ++ 7 files changed, 291 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index efcd14a..ac57e04 100644 --- a/app.py +++ b/app.py @@ -9,7 +9,7 @@ from comics_data import ( COMICS, COMIC_NAME, COPYRIGHT_NAME, SITE_URL, FULL_WIDTH_DEFAULT, PLAIN_DEFAULT, LOGO_IMAGE, LOGO_MODE, HEADER_IMAGE, FOOTER_IMAGE, BANNER_IMAGE, COMPACT_FOOTER, ARCHIVE_FULL_WIDTH, SECTIONS_ENABLED, USE_COMIC_NAV_ICONS, USE_HEADER_NAV_ICONS, USE_FOOTER_SOCIAL_ICONS, NEWSLETTER_ENABLED, - SOCIAL_INSTAGRAM, SOCIAL_YOUTUBE, SOCIAL_EMAIL, API_SPEC_LINK + SOCIAL_INSTAGRAM, SOCIAL_YOUTUBE, SOCIAL_EMAIL, API_SPEC_LINK, EMBED_ENABLED ) import markdown @@ -49,7 +49,8 @@ def inject_global_settings(): 'social_instagram': SOCIAL_INSTAGRAM, 'social_youtube': SOCIAL_YOUTUBE, 'social_email': SOCIAL_EMAIL, - 'api_spec_link': API_SPEC_LINK + 'api_spec_link': API_SPEC_LINK, + 'embed_enabled': EMBED_ENABLED } @@ -168,6 +169,19 @@ def comic(comic_id): comic=comic, total_comics=len(COMICS)) +@app.route('/embed/') +def embed(comic_id): + """Embeddable comic view - minimal layout for iframes""" + if not EMBED_ENABLED: + abort(404) + comic = get_comic_by_number(comic_id) + if not comic: + abort(404) + # Use comic title if present, otherwise use #X format + page_title = comic.get('title', f"#{comic_id}") + return render_template('embed.html', title=page_title, comic=comic) + + def group_comics_by_section(comics_list): """Group comics by section. Returns list of (section_title, comics) tuples""" if not SECTIONS_ENABLED: diff --git a/comics_data.py b/comics_data.py index 6f95fda..6ad43b1 100644 --- a/comics_data.py +++ b/comics_data.py @@ -85,6 +85,10 @@ SOCIAL_EMAIL = None # e.g., 'mailto:your@email.com' # Path is relative to static/ directory API_SPEC_LINK = None # Set to 'openapi.yml' to enable +# Global setting: Set to True to enable comic embed functionality +# When enabled, users can get embed codes to display comics on other websites +EMBED_ENABLED = True + COMICS = [ { 'number': 1, diff --git a/static/css/style.css b/static/css/style.css index a7dd522..1466b01 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1067,4 +1067,188 @@ footer.compact-footer .footer-section:not(:last-child)::after { content: '|'; margin-left: var(--space-md); color: var(--color-text-muted); +} + +/* ============================================================ + EMBED FEATURE STYLES + ============================================================ */ + +/* Embed button section */ +.comic-embed-section { + margin-top: var(--space-lg); + text-align: center; +} + +/* Embed button */ +.btn-embed { + padding: var(--space-sm) var(--space-lg); + background-color: var(--color-background); + border: var(--border-width-thin) solid var(--color-border); + color: var(--color-text); + font-family: var(--font-family); + font-size: var(--font-size-md); + cursor: pointer; + text-transform: uppercase; + letter-spacing: var(--letter-spacing-tight); + transition: background-color var(--transition-speed); +} + +.btn-embed:hover { + background-color: var(--color-hover-bg); +} + +.btn-embed:focus { + outline: 3px solid var(--color-primary); + outline-offset: 2px; +} + +/* Modal overlay */ +.modal { + display: none; /* Hidden by default */ + position: fixed; + z-index: 10000; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.6); +} + +/* Modal content box */ +.modal-content { + background-color: var(--color-background); + margin: 10% auto; + padding: 0; + border: var(--border-width-thick) solid var(--color-border); + max-width: 600px; + width: 90%; +} + +/* Modal header */ +.modal-header { + padding: var(--space-md) var(--space-lg); + border-bottom: var(--border-width-thin) solid var(--color-border); + display: flex; + justify-content: space-between; + align-items: center; +} + +.modal-header h2 { + margin: 0; + font-size: var(--font-size-xl); + text-transform: uppercase; + letter-spacing: var(--letter-spacing-tight); +} + +/* Modal close button */ +.modal-close { + background: none; + border: none; + font-size: var(--font-size-3xl); + font-weight: bold; + cursor: pointer; + color: var(--color-text); + line-height: 1; + padding: 0; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; +} + +.modal-close:hover, +.modal-close:focus { + color: var(--color-text-muted); + outline: none; +} + +/* Modal body */ +.modal-body { + padding: var(--space-lg); +} + +.modal-body p { + margin-bottom: var(--space-md); + font-size: var(--font-size-md); +} + +/* Embed code textarea */ +#embed-code { + width: 100%; + min-height: 100px; + padding: var(--space-md); + font-family: var(--font-family); + font-size: var(--font-size-sm); + border: var(--border-width-thin) solid var(--color-border); + background-color: var(--color-background); + color: var(--color-text); + resize: vertical; + margin-bottom: var(--space-md); +} + +#embed-code:focus { + outline: 3px solid var(--color-primary); + outline-offset: 2px; +} + +/* Copy button */ +.btn-copy { + padding: var(--space-sm) var(--space-lg); + background-color: var(--color-primary); + border: var(--border-width-thin) solid var(--color-border); + color: var(--color-background); + font-family: var(--font-family); + font-size: var(--font-size-md); + cursor: pointer; + text-transform: uppercase; + letter-spacing: var(--letter-spacing-tight); + width: 100%; + transition: background-color var(--transition-speed); +} + +.btn-copy:hover { + background-color: var(--color-text); +} + +.btn-copy:focus { + outline: 3px solid var(--color-primary); + outline-offset: 2px; +} + +.btn-copy.copied { + background-color: var(--color-text-muted); +} + +/* Embed preview link */ +.embed-preview-link { + margin-top: var(--space-md); + font-size: var(--font-size-sm); + text-align: center; +} + +.embed-preview-link a { + color: var(--color-text); + text-decoration: underline; +} + +.embed-preview-link a:hover { + color: var(--color-text-muted); +} + +/* Mobile responsive modal */ +@media (max-width: 768px) { + .modal-content { + margin: 20% auto; + width: 95%; + } + + .modal-header h2 { + font-size: var(--font-size-lg); + } + + #embed-code { + font-size: var(--font-size-xs); + } } \ No newline at end of file diff --git a/static/js/comic-nav.js b/static/js/comic-nav.js index da2cf31..828fa33 100644 --- a/static/js/comic-nav.js +++ b/static/js/comic-nav.js @@ -125,6 +125,11 @@ // Update URL without reload history.pushState({ comicId: comic.number }, '', `/comic/${comic.number}`); + // Dispatch custom event for other features (like embed button) + window.dispatchEvent(new CustomEvent('comicUpdated', { + detail: { comicNumber: comic.number } + })); + // Move focus to comic image for keyboard navigation accessibility const comicImageFocus = document.getElementById('comic-image-focus'); if (comicImageFocus) { @@ -202,10 +207,22 @@ firstBtn.className = isIconNav ? 'btn-icon-nav' : 'btn btn-nav'; firstBtn.onclick = (e) => { e.preventDefault(); loadComic(1); }; firstBtn.removeAttribute('aria-disabled'); + if (firstBtn.tagName === 'SPAN') { + firstBtn.setAttribute('tabindex', '0'); + firstBtn.setAttribute('role', 'button'); + firstBtn.onkeydown = (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + loadComic(1); + } + }; + } } else { firstBtn.className = isIconNav ? 'btn-icon-nav btn-icon-disabled' : 'btn btn-nav btn-disabled'; firstBtn.onclick = null; + firstBtn.onkeydown = null; firstBtn.setAttribute('aria-disabled', 'true'); + firstBtn.removeAttribute('tabindex'); } // Previous button @@ -214,10 +231,22 @@ prevBtn.className = isIconNav ? 'btn-icon-nav' : 'btn btn-nav'; prevBtn.onclick = (e) => { e.preventDefault(); loadComic(currentNumber - 1); }; prevBtn.removeAttribute('aria-disabled'); + if (prevBtn.tagName === 'SPAN') { + prevBtn.setAttribute('tabindex', '0'); + prevBtn.setAttribute('role', 'button'); + prevBtn.onkeydown = (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + loadComic(currentNumber - 1); + } + }; + } } else { prevBtn.className = isIconNav ? 'btn-icon-nav btn-icon-disabled' : 'btn btn-nav btn-disabled'; prevBtn.onclick = null; + prevBtn.onkeydown = null; prevBtn.setAttribute('aria-disabled', 'true'); + prevBtn.removeAttribute('tabindex'); } // Comic date display @@ -231,10 +260,22 @@ nextBtn.className = isIconNav ? 'btn-icon-nav' : 'btn btn-nav'; nextBtn.onclick = (e) => { e.preventDefault(); loadComic(currentNumber + 1); }; nextBtn.removeAttribute('aria-disabled'); + if (nextBtn.tagName === 'SPAN') { + nextBtn.setAttribute('tabindex', '0'); + nextBtn.setAttribute('role', 'button'); + nextBtn.onkeydown = (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + loadComic(currentNumber + 1); + } + }; + } } else { nextBtn.className = isIconNav ? 'btn-icon-nav btn-icon-disabled' : 'btn btn-nav btn-disabled'; nextBtn.onclick = null; + nextBtn.onkeydown = null; nextBtn.setAttribute('aria-disabled', 'true'); + nextBtn.removeAttribute('tabindex'); } // Latest button @@ -243,10 +284,22 @@ latestBtn.className = isIconNav ? 'btn-icon-nav' : 'btn btn-nav'; latestBtn.onclick = (e) => { e.preventDefault(); loadComic(totalComics); }; latestBtn.removeAttribute('aria-disabled'); + if (latestBtn.tagName === 'SPAN') { + latestBtn.setAttribute('tabindex', '0'); + latestBtn.setAttribute('role', 'button'); + latestBtn.onkeydown = (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + loadComic(totalComics); + } + }; + } } else { latestBtn.className = isIconNav ? 'btn-icon-nav btn-icon-disabled' : 'btn btn-nav btn-disabled'; latestBtn.onclick = null; + latestBtn.onkeydown = null; latestBtn.setAttribute('aria-disabled', 'true'); + latestBtn.removeAttribute('tabindex'); } } diff --git a/templates/base.html b/templates/base.html index 4079964..5587f6b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -37,7 +37,7 @@ {% block extra_css %}{% endblock %} - + Skip to main content @@ -185,7 +185,28 @@ {% endif %} + + {% if embed_enabled %} + + {% endif %} + + {% if embed_enabled %} + + {% endif %} {% block extra_js %}{% endblock %} \ No newline at end of file diff --git a/templates/comic.html b/templates/comic.html index 83dc7fb..722177d 100644 --- a/templates/comic.html +++ b/templates/comic.html @@ -131,6 +131,12 @@ + {% if embed_enabled %} +
+ +
+ {% endif %} + {% if comic.author_note %}

Author Note

diff --git a/templates/index.html b/templates/index.html index 45a42d1..e04a347 100644 --- a/templates/index.html +++ b/templates/index.html @@ -91,6 +91,12 @@
+ {% if embed_enabled %} +
+ +
+ {% endif %} + {% if comic.author_note %}

Author Note