✨ comic embed
This commit is contained in:
18
app.py
18
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/<int:comic_id>')
|
||||
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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1068,3 +1068,187 @@ footer.compact-footer .footer-section:not(:last-child)::after {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<link rel="alternate" type="application/rss+xml" title="{{ comic_name }} RSS Feed" href="{{ url_for('static', filename='feed.rss') }}">
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<body data-site-url="{{ site_url }}">
|
||||
<!-- Skip to main content link for keyboard navigation -->
|
||||
<a href="#main-content" class="skip-to-main">Skip to main content</a>
|
||||
|
||||
@@ -185,7 +185,28 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Embed Code Modal -->
|
||||
{% if embed_enabled %}
|
||||
<div id="embed-modal" class="modal" role="dialog" aria-labelledby="embed-modal-title" aria-hidden="true">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="embed-modal-title">Embed This Comic</h2>
|
||||
<button class="modal-close" aria-label="Close embed modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Copy this code to embed the comic on your website:</p>
|
||||
<textarea id="embed-code" readonly onfocus="this.select()" onclick="this.select()" aria-label="Embed code"></textarea>
|
||||
<button id="copy-embed-code" class="btn-copy" aria-label="Copy embed code to clipboard">Copy Code</button>
|
||||
<p class="embed-preview-link">Preview: <a id="embed-preview-link" href="#" target="_blank" rel="noopener noreferrer">Open embed in new window</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script src="{{ url_for('static', filename='js/comic-nav.js') }}"></script>
|
||||
{% if embed_enabled %}
|
||||
<script src="{{ url_for('static', filename='js/embed.js') }}"></script>
|
||||
{% endif %}
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -131,6 +131,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if embed_enabled %}
|
||||
<div class="comic-embed-section">
|
||||
<button class="btn-embed" id="embed-button" data-comic-number="{{ comic.number }}" aria-label="Get embed code for this comic">Share/Embed</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if comic.author_note %}
|
||||
<div class="comic-transcript">
|
||||
<h3>Author Note</h3>
|
||||
|
||||
@@ -91,6 +91,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if embed_enabled %}
|
||||
<div class="comic-embed-section">
|
||||
<button class="btn-embed" id="embed-button" data-comic-number="{{ comic.number }}" aria-label="Get embed code for this comic">Share/Embed</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if comic.author_note %}
|
||||
<div class="comic-transcript">
|
||||
<h3>Author Note</h3>
|
||||
|
||||
Reference in New Issue
Block a user