Compare commits

..

7 Commits

Author SHA1 Message Date
mi
b936b852e9 💄 compact footer 2025-11-07 21:51:44 +10:00
mi
9cb726312a 💄 header image 2025-11-07 21:33:47 +10:00
mi
ed0a1aadb2 date on all pages 2025-11-07 18:55:15 +10:00
mi
234d78d862 plain mode 2025-11-07 18:37:45 +10:00
mi
a69f64de7a global option 2025-11-07 18:21:01 +10:00
mi
52920e8fa8 apply appropriate width 2025-11-07 18:16:44 +10:00
mi
30d9044950 Optional full width comics. 2025-11-07 18:14:09 +10:00
7 changed files with 286 additions and 30 deletions

61
app.py
View File

@@ -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>')

View File

@@ -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!',
},
]

View File

@@ -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

Binary file not shown.

View File

@@ -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
// 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);
}

View File

@@ -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">

View File

@@ -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>