Compare commits
7 Commits
b6f6ee4b70
...
b936b852e9
| Author | SHA1 | Date | |
|---|---|---|---|
| b936b852e9 | |||
| 9cb726312a | |||
| ed0a1aadb2 | |||
| 234d78d862 | |||
| a69f64de7a | |||
| 52920e8fa8 | |||
| 30d9044950 |
61
app.py
61
app.py
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
from flask import Flask, render_template, abort, jsonify, request
|
||||
from comics_data import COMICS
|
||||
from comics_data import COMICS, FULL_WIDTH_DEFAULT, PLAIN_DEFAULT, HEADER_IMAGE, COMPACT_FOOTER
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@@ -8,18 +9,68 @@ app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'your-secret-key')
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def inject_global_settings():
|
||||
"""Make global settings available to all templates"""
|
||||
return {
|
||||
'header_image': HEADER_IMAGE,
|
||||
'compact_footer': COMPACT_FOOTER
|
||||
}
|
||||
|
||||
|
||||
def is_full_width(comic):
|
||||
"""Determine if a comic should be full width based on global and per-comic settings"""
|
||||
# If comic explicitly sets full_width, use that value
|
||||
if 'full_width' in comic:
|
||||
return comic['full_width']
|
||||
# Otherwise use the global default
|
||||
return FULL_WIDTH_DEFAULT
|
||||
|
||||
|
||||
def is_plain(comic):
|
||||
"""Determine if a comic should be plain mode based on global and per-comic settings"""
|
||||
# If comic explicitly sets plain, use that value
|
||||
if 'plain' in comic:
|
||||
return comic['plain']
|
||||
# Otherwise use the global default
|
||||
return PLAIN_DEFAULT
|
||||
|
||||
|
||||
def format_comic_date(date_str):
|
||||
"""Format date string (YYYY-MM-DD) to 'Day name, Month name day, year'"""
|
||||
try:
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d')
|
||||
# Use %d and strip leading zero for cross-platform compatibility
|
||||
day = date_obj.strftime('%d').lstrip('0')
|
||||
formatted = date_obj.strftime(f'%A, %B {day}, %Y')
|
||||
return formatted
|
||||
except:
|
||||
return date_str
|
||||
|
||||
|
||||
def enrich_comic(comic):
|
||||
"""Add computed properties to comic data"""
|
||||
if comic is None:
|
||||
return None
|
||||
enriched = comic.copy()
|
||||
enriched['full_width'] = is_full_width(comic)
|
||||
enriched['plain'] = is_plain(comic)
|
||||
enriched['formatted_date'] = format_comic_date(comic['date'])
|
||||
return enriched
|
||||
|
||||
|
||||
def get_comic_by_number(number):
|
||||
"""Get a comic by its number"""
|
||||
for comic in COMICS:
|
||||
if comic['number'] == number:
|
||||
return comic
|
||||
return enrich_comic(comic)
|
||||
return None
|
||||
|
||||
|
||||
def get_latest_comic():
|
||||
"""Get the most recent comic"""
|
||||
if COMICS:
|
||||
return COMICS[-1]
|
||||
return enrich_comic(COMICS[-1])
|
||||
return None
|
||||
|
||||
|
||||
@@ -48,7 +99,7 @@ def comic(comic_id):
|
||||
def archive():
|
||||
"""Archive page showing all comics"""
|
||||
# Reverse order to show newest first
|
||||
comics = list(reversed(COMICS))
|
||||
comics = [enrich_comic(comic) for comic in reversed(COMICS)]
|
||||
return render_template('archive.html', title='Archive',
|
||||
comics=comics)
|
||||
|
||||
@@ -62,7 +113,7 @@ def about():
|
||||
@app.route('/api/comics')
|
||||
def api_comics():
|
||||
"""API endpoint - returns all comics as JSON"""
|
||||
return jsonify(COMICS)
|
||||
return jsonify([enrich_comic(comic) for comic in COMICS])
|
||||
|
||||
|
||||
@app.route('/api/comics/<int:comic_id>')
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
# Comic data
|
||||
# Edit this file to add, remove, or modify comics
|
||||
|
||||
# Global setting: Set to True to make all comics full-width by default
|
||||
# Individual comics can override this with 'full_width': False
|
||||
FULL_WIDTH_DEFAULT = False
|
||||
|
||||
# Global setting: Set to True to hide header and remove nav border by default
|
||||
# Individual comics can override this with 'plain': False
|
||||
PLAIN_DEFAULT = False
|
||||
|
||||
# Global setting: Path to header image (relative to static/images/)
|
||||
# Set to None to disable header image
|
||||
# Example: HEADER_IMAGE = 'title.jpg' will use static/images/title.jpg
|
||||
HEADER_IMAGE = 'title.jpg'
|
||||
|
||||
# Global setting: Set to True to display footer in compact mode
|
||||
# Compact mode: single line, no border, horizontal layout
|
||||
COMPACT_FOOTER = True
|
||||
|
||||
COMICS = [
|
||||
{
|
||||
'number': 1,
|
||||
@@ -8,13 +25,17 @@ COMICS = [
|
||||
'filename': 'comic-001.jpg',
|
||||
'date': '2025-01-01',
|
||||
'alt_text': 'The very first comic',
|
||||
'author_note': 'This is where your comic journey begins!'
|
||||
'author_note': 'This is where your comic journey begins!',
|
||||
'full_width': True, # Optional: override FULL_WIDTH_DEFAULT for this comic
|
||||
'plain': True, # Optional: override PLAIN_DEFAULT for this comic
|
||||
},
|
||||
{
|
||||
'number': 2,
|
||||
'filename': 'comic-002.jpg',
|
||||
'date': '2025-01-08',
|
||||
'alt_text': 'The second comic',
|
||||
'full_width': True,
|
||||
'plain': True,
|
||||
},
|
||||
{
|
||||
'number': 3,
|
||||
@@ -22,6 +43,6 @@ COMICS = [
|
||||
'filename': 'comic-003.jpg',
|
||||
'date': '2025-01-15',
|
||||
'alt_text': 'The third comic',
|
||||
'author_note': 'Things are getting interesting!'
|
||||
'author_note': 'Things are getting interesting!',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -18,6 +18,19 @@ body {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* Site Header Image */
|
||||
.site-header-image {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.site-header-image img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Header and Navigation */
|
||||
header {
|
||||
border-bottom: 3px solid #000;
|
||||
@@ -25,6 +38,22 @@ header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Header with image variant - no border, no padding, centered nav */
|
||||
header.header-with-image {
|
||||
border-bottom: none;
|
||||
padding: 1rem 0 0 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
header.header-with-image nav .container {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
header.header-with-image .nav-links a {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
nav .container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -160,6 +189,40 @@ main {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Full-width comic variant */
|
||||
.comic-container-fullwidth {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.comic-container-fullwidth .comic-header,
|
||||
.comic-container-fullwidth .comic-navigation,
|
||||
.comic-container-fullwidth .comic-transcript {
|
||||
max-width: 900px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.comic-container-fullwidth .comic-image {
|
||||
width: calc(100vw - 4rem);
|
||||
max-width: calc(100vw - 4rem);
|
||||
margin-left: calc(-1 * (50vw - 50% - 2rem));
|
||||
margin-right: calc(-1 * (50vw - 50% - 2rem));
|
||||
}
|
||||
|
||||
.comic-container-fullwidth .comic-image img {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Plain comic variant - no header, no nav border */
|
||||
.comic-container-plain .comic-navigation {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.comic-header {
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
@@ -231,15 +294,13 @@ main {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.comic-number {
|
||||
padding: 0.4rem 0.8rem;
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
border: 2px solid #000;
|
||||
.comic-date-display {
|
||||
padding: 0 0.5rem;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
margin: 0 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.85rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Comic Transcript */
|
||||
@@ -329,9 +390,8 @@ main {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.comic-number {
|
||||
padding: 0.3rem 0.6rem;
|
||||
font-size: 0.75rem;
|
||||
.comic-date-display {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.archive-grid {
|
||||
@@ -435,3 +495,78 @@ footer {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* Compact Footer Mode */
|
||||
footer.compact-footer {
|
||||
border-top: none;
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
footer.compact-footer .container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
footer.compact-footer .footer-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
footer.compact-footer .footer-section {
|
||||
flex: none;
|
||||
min-width: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
footer.compact-footer .footer-section h3 {
|
||||
font-size: 0.75rem;
|
||||
margin-bottom: 0;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
footer.compact-footer .social-links {
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
footer.compact-footer .social-links a {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
footer.compact-footer .newsletter-placeholder {
|
||||
font-size: 0.75rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
footer.compact-footer .footer-bottom {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
footer.compact-footer .footer-bottom::before {
|
||||
content: '|';
|
||||
margin-right: 1rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
footer.compact-footer .footer-bottom p {
|
||||
font-size: 0.75rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Add separator between sections in compact mode */
|
||||
footer.compact-footer .footer-section:not(:last-child)::after {
|
||||
content: '|';
|
||||
margin-left: 1rem;
|
||||
color: #666;
|
||||
}
|
||||
BIN
static/images/title.jpg
LFS
Normal file
BIN
static/images/title.jpg
LFS
Normal file
Binary file not shown.
@@ -27,12 +27,43 @@
|
||||
|
||||
// Update the page with comic data
|
||||
function displayComic(comic) {
|
||||
// Update title
|
||||
const title = comic.title || `#${comic.number}`;
|
||||
document.querySelector('.comic-header h1').textContent = title;
|
||||
|
||||
// Update date
|
||||
document.querySelector('.comic-date').textContent = comic.date;
|
||||
// Update container class for full-width option
|
||||
const container = document.querySelector('.comic-container');
|
||||
if (comic.full_width) {
|
||||
container.classList.add('comic-container-fullwidth');
|
||||
} else {
|
||||
container.classList.remove('comic-container-fullwidth');
|
||||
}
|
||||
|
||||
// Update container class for plain mode
|
||||
if (comic.plain) {
|
||||
container.classList.add('comic-container-plain');
|
||||
} else {
|
||||
container.classList.remove('comic-container-plain');
|
||||
}
|
||||
|
||||
// Show/hide header based on plain mode
|
||||
const header = document.querySelector('.comic-header');
|
||||
if (comic.plain) {
|
||||
if (header) {
|
||||
header.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
if (header) {
|
||||
header.style.display = 'block';
|
||||
} else {
|
||||
// Create header if it doesn't exist
|
||||
const newHeader = document.createElement('div');
|
||||
newHeader.className = 'comic-header';
|
||||
newHeader.innerHTML = '<h1></h1><p class="comic-date"></p>';
|
||||
container.insertBefore(newHeader, container.firstChild);
|
||||
}
|
||||
// Update title and date
|
||||
document.querySelector('.comic-header h1').textContent = title;
|
||||
document.querySelector('.comic-date').textContent = comic.date;
|
||||
}
|
||||
|
||||
// Update image and its link
|
||||
const comicImageDiv = document.querySelector('.comic-image');
|
||||
@@ -61,7 +92,7 @@
|
||||
}
|
||||
|
||||
// Update navigation buttons
|
||||
updateNavButtons(comic.number);
|
||||
updateNavButtons(comic.number, comic.formatted_date);
|
||||
|
||||
// Update page title
|
||||
document.title = `${title} - Sunday Comics`;
|
||||
@@ -95,7 +126,7 @@
|
||||
}
|
||||
|
||||
// Update navigation button states
|
||||
function updateNavButtons(currentNumber) {
|
||||
function updateNavButtons(currentNumber, formattedDate) {
|
||||
const navButtons = document.querySelector('.nav-buttons');
|
||||
|
||||
// First button
|
||||
@@ -118,8 +149,10 @@
|
||||
prevBtn.onclick = null;
|
||||
}
|
||||
|
||||
// Comic number display
|
||||
navButtons.children[2].textContent = `Comic #${currentNumber}`;
|
||||
// Comic date display
|
||||
if (formattedDate) {
|
||||
navButtons.children[2].textContent = formattedDate;
|
||||
}
|
||||
|
||||
// Next button
|
||||
const nextBtn = navButtons.children[3];
|
||||
@@ -156,7 +189,10 @@
|
||||
const currentNumber = parseInt(document.querySelector('.comic-container').dataset.comicNumber || 0);
|
||||
|
||||
if (currentNumber && totalComics) {
|
||||
updateNavButtons(currentNumber);
|
||||
// Get the formatted date from the DOM (already rendered by server)
|
||||
const dateDisplay = document.querySelector('.comic-date-display');
|
||||
const formattedDate = dateDisplay ? dateDisplay.textContent : null;
|
||||
updateNavButtons(currentNumber, formattedDate);
|
||||
updateComicImageLink(currentNumber);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,12 +33,20 @@
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
{% if header_image %}
|
||||
<div class="site-header-image">
|
||||
<img src="{{ url_for('static', filename='images/' + header_image) }}" alt="Sunday Comics Header">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<header{% if header_image %} class="header-with-image"{% endif %}>
|
||||
<nav>
|
||||
<div class="container">
|
||||
{% if not header_image %}
|
||||
<div class="nav-brand">
|
||||
<a href="{{ url_for('index') }}">Sunday Comics</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<ul class="nav-links">
|
||||
<li><a href="{{ url_for('index') }}" {% if request.endpoint == 'index' %}class="active"{% endif %}>Latest</a></li>
|
||||
<li><a href="{{ url_for('archive') }}" {% if request.endpoint == 'archive' %}class="active"{% endif %}>Archive</a></li>
|
||||
@@ -54,7 +62,7 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<footer{% if compact_footer %} class="compact-footer"{% endif %}>
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
{% block og_image %}{{ request.url_root }}static/images/thumbs/{{ comic.filename }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="comic-container" data-comic-number="{{ comic.number }}" data-total-comics="{{ total_comics }}">
|
||||
<div class="comic-container{% if comic.full_width %} comic-container-fullwidth{% endif %}{% if comic.plain %} comic-container-plain{% endif %}" data-comic-number="{{ comic.number }}" data-total-comics="{{ total_comics }}">
|
||||
{% if not comic.plain %}
|
||||
<div class="comic-header">
|
||||
<h1>{{ comic.title if comic.title else '#' ~ comic.number }}</h1>
|
||||
<p class="comic-date">{{ comic.date }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="comic-image">
|
||||
{% if comic.number < total_comics %}
|
||||
@@ -35,7 +37,7 @@
|
||||
<span class="btn btn-nav btn-disabled">Previous</span>
|
||||
{% endif %}
|
||||
|
||||
<span class="comic-number">Comic #{{ comic.number }}</span>
|
||||
<span class="comic-date-display">{{ comic.formatted_date }}</span>
|
||||
|
||||
{% if comic.number < total_comics %}
|
||||
<a href="{{ url_for('comic', comic_id=comic.number + 1) }}" class="btn btn-nav">Next</a>
|
||||
|
||||
Reference in New Issue
Block a user