mirror of
https://github.com/kennethreitz/kennethreitz.org.git
synced 2026-06-20 14:30:57 +00:00
fd970be0ba
- Responder 3.10-3.12: auto ETags, request size limits, timeouts, Prometheus metrics at /metrics; essay release count is now seven - Gallery thumbnails via /thumb route (24MB pages drop to ~400KB) - Dark-mode flash eliminated; theme applies before first paint - 404 pages suggest the closest-matching content - Long-lived caching for static assets; lazy-loaded content images - Skip link and keyboard-accessible nav dropdowns - Per-essay OG image in Article structured data - Homepage: trim the quest log clause Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
677 lines
16 KiB
HTML
677 lines
16 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block extra_head %}
|
||
<style>
|
||
/* Directory listing styles */
|
||
.directory-list {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 0;
|
||
}
|
||
|
||
.directory-item {
|
||
position: relative;
|
||
padding: 0.25rem 0;
|
||
border-bottom: 1px solid #f9f9f9;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.directory-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.directory-icon {
|
||
width: 18px;
|
||
height: 18px;
|
||
position: absolute;
|
||
left: -1.5rem;
|
||
top: 0.3rem;
|
||
}
|
||
|
||
.directory-content {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.directory-link {
|
||
color: #333;
|
||
text-decoration: none;
|
||
font-size: 1rem;
|
||
text-align: left;
|
||
flex: 1;
|
||
}
|
||
|
||
.directory-link:hover {
|
||
color: #666;
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.directory-date {
|
||
color: #999;
|
||
font-size: 0.75rem;
|
||
margin-left: auto;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
@media (max-width: 760px) {
|
||
.directory-item {
|
||
padding: 0.2rem 0;
|
||
}
|
||
|
||
.directory-icon {
|
||
width: 16px;
|
||
height: 16px;
|
||
left: -1.25rem;
|
||
top: 0.25rem;
|
||
}
|
||
|
||
.directory-link {
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.directory-date {
|
||
font-size: 0.7rem;
|
||
}
|
||
}
|
||
|
||
/* Directory navigation styles */
|
||
.directory-navigation {
|
||
margin-top: 2rem;
|
||
padding-top: 1rem;
|
||
border-top: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.parent-link {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
color: #666;
|
||
text-decoration: none;
|
||
font-size: 0.9rem;
|
||
transition: color 0.2s ease;
|
||
}
|
||
|
||
.parent-link:hover {
|
||
color: #333;
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.parent-icon {
|
||
width: 16px;
|
||
height: 16px;
|
||
}
|
||
/* Image Gallery Styles */
|
||
.image-gallery {
|
||
margin: 2rem 0;
|
||
padding: 0 2rem;
|
||
}
|
||
|
||
.gallery-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 1rem;
|
||
margin-top: 1.5rem;
|
||
}
|
||
|
||
.gallery-item {
|
||
position: relative;
|
||
aspect-ratio: 1;
|
||
overflow: hidden;
|
||
border-radius: 4px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
cursor: pointer;
|
||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||
}
|
||
|
||
.gallery-item:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
|
||
}
|
||
|
||
.gallery-thumbnail {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.gallery-item:hover .gallery-thumbnail {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
/* Lightbox Styles */
|
||
.lightbox {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.9);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.lightbox.active {
|
||
opacity: 1;
|
||
visibility: visible;
|
||
}
|
||
|
||
.lightbox-content {
|
||
position: relative;
|
||
max-width: 90%;
|
||
max-height: 90%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.lightbox-image {
|
||
max-width: 100%;
|
||
max-height: 80vh;
|
||
object-fit: contain;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.lightbox-title {
|
||
color: white;
|
||
margin-top: 1rem;
|
||
text-align: center;
|
||
font-size: 1.1rem;
|
||
font-family: et-book, Palatino, "Palatino Linotype", "Palatino LT STD", "Book Antiqua", Georgia, serif;
|
||
}
|
||
|
||
.lightbox-exif {
|
||
color: #aaa;
|
||
margin-top: 0.5rem;
|
||
text-align: center;
|
||
font-size: 0.9rem;
|
||
font-family: et-book, Palatino, "Palatino Linotype", "Palatino LT STD", "Book Antiqua", Georgia, serif;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.lightbox-exif .exif-row {
|
||
margin: 0.25rem 0;
|
||
}
|
||
|
||
.lightbox-exif .exif-label {
|
||
font-variant: small-caps;
|
||
color: #888;
|
||
margin-right: 0.5rem;
|
||
}
|
||
|
||
.lightbox-close {
|
||
position: absolute;
|
||
top: 20px;
|
||
right: 20px;
|
||
color: white;
|
||
font-size: 2rem;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
z-index: 1001;
|
||
width: 40px;
|
||
height: 40px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 50%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
transition: background 0.2s ease;
|
||
}
|
||
|
||
.lightbox-close:hover {
|
||
background: rgba(0, 0, 0, 0.8);
|
||
}
|
||
|
||
.lightbox-nav {
|
||
position: absolute;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
color: white;
|
||
font-size: 2rem;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
z-index: 1001;
|
||
width: 50px;
|
||
height: 50px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 50%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
transition: background 0.2s ease;
|
||
}
|
||
|
||
.lightbox-nav:hover {
|
||
background: rgba(0, 0, 0, 0.8);
|
||
}
|
||
|
||
.lightbox-prev {
|
||
left: 20px;
|
||
}
|
||
|
||
.lightbox-next {
|
||
right: 20px;
|
||
}
|
||
|
||
@media (max-width: 1024px) {
|
||
.gallery-grid {
|
||
grid-template-columns: repeat(3, 1fr);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 760px) {
|
||
.image-gallery {
|
||
padding: 0 1rem;
|
||
}
|
||
|
||
.gallery-grid {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.lightbox-close {
|
||
top: 10px;
|
||
right: 10px;
|
||
width: 35px;
|
||
height: 35px;
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.lightbox-nav {
|
||
width: 45px;
|
||
height: 45px;
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.lightbox-prev {
|
||
left: 10px;
|
||
}
|
||
|
||
.lightbox-next {
|
||
right: 10px;
|
||
}
|
||
}
|
||
|
||
/* Light mode overrides */
|
||
body.light-mode .directory-link {
|
||
color: #333 !important;
|
||
}
|
||
|
||
body.light-mode .directory-link:hover {
|
||
color: #666 !important;
|
||
}
|
||
|
||
body.light-mode .directory-date {
|
||
color: #999 !important;
|
||
}
|
||
|
||
body.light-mode .directory-item {
|
||
border-bottom-color: #f9f9f9 !important;
|
||
}
|
||
|
||
body.light-mode .directory-navigation {
|
||
border-top-color: #f0f0f0 !important;
|
||
}
|
||
|
||
body.light-mode .parent-link {
|
||
color: #666 !important;
|
||
}
|
||
|
||
body.light-mode .parent-link:hover {
|
||
color: #333 !important;
|
||
}
|
||
|
||
/* Dark mode styles */
|
||
body.dark-mode .directory-link {
|
||
color: #ccc;
|
||
}
|
||
|
||
body.dark-mode .directory-link:hover {
|
||
color: #fff;
|
||
}
|
||
|
||
body.dark-mode .directory-date {
|
||
color: #666;
|
||
}
|
||
|
||
body.dark-mode .directory-item {
|
||
border-bottom-color: #2a2a2a;
|
||
}
|
||
|
||
body.dark-mode .directory-navigation {
|
||
border-top-color: #2a2a2a;
|
||
}
|
||
|
||
body.dark-mode .parent-link {
|
||
color: #999;
|
||
}
|
||
|
||
body.dark-mode .parent-link:hover {
|
||
color: #ccc;
|
||
}
|
||
|
||
@media (prefers-color-scheme: dark) {
|
||
.directory-link {
|
||
color: #ccc;
|
||
}
|
||
|
||
.directory-link:hover {
|
||
color: #fff;
|
||
}
|
||
|
||
.directory-date {
|
||
color: #666;
|
||
}
|
||
|
||
.directory-item {
|
||
border-bottom-color: #2a2a2a;
|
||
}
|
||
|
||
.directory-navigation {
|
||
border-top-color: #2a2a2a;
|
||
}
|
||
|
||
.parent-link {
|
||
color: #999;
|
||
}
|
||
|
||
.parent-link:hover {
|
||
color: #ccc;
|
||
}
|
||
}
|
||
|
||
/* Directory title icon */
|
||
.directory-title-icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
margin-right: 0.5rem;
|
||
vertical-align: middle;
|
||
display: inline-block;
|
||
}
|
||
|
||
@media (max-width: 760px) {
|
||
.directory-title-icon {
|
||
width: 24px;
|
||
height: 24px;
|
||
margin-right: 0.4rem;
|
||
}
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
{% if current_path and current_path != '' %}
|
||
<h1>
|
||
{% if folder_icon %}
|
||
<img src="{{ folder_icon }}" alt="Folder icon" class="directory-title-icon">
|
||
{% endif %}
|
||
{{ title }}
|
||
</h1>
|
||
{% endif %}
|
||
|
||
{% if index_content and content_position == 'top' %}
|
||
<section class="directory-intro">
|
||
{{ index_content.content | safe }}
|
||
</section>
|
||
{% endif %}
|
||
|
||
{% if is_image_gallery and image_items %}
|
||
<section class="image-gallery">
|
||
<h3>Gallery</h3>
|
||
<div class="gallery-grid">
|
||
{% for item in image_items %}
|
||
<div class="gallery-item">
|
||
{% if item.static_path.lower().endswith(('.jpg', '.jpeg', '.png', '.webp')) %}
|
||
<img src="/thumb{{ item.static_path }}?w=640"
|
||
srcset="/thumb{{ item.static_path }}?w=320 320w, /thumb{{ item.static_path }}?w=640 640w, /thumb{{ item.static_path }}?w=1280 1280w"
|
||
sizes="(max-width: 760px) 50vw, 320px"
|
||
loading="lazy" decoding="async"
|
||
alt="{{ item.display_name }}" class="gallery-thumbnail"
|
||
data-full="{{ item.static_path }}"
|
||
data-title="{{ item.display_name }}"
|
||
data-exif="{{ item.exif | tojson | forceescape }}">
|
||
{% else %}
|
||
<img src="{{ item.static_path }}" loading="lazy" decoding="async"
|
||
alt="{{ item.display_name }}" class="gallery-thumbnail"
|
||
data-full="{{ item.static_path }}"
|
||
data-title="{{ item.display_name }}"
|
||
data-exif="{{ item.exif | tojson | forceescape }}">
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</section>
|
||
|
||
{% if items|length > image_items|length %}
|
||
<section class="directory-listing">
|
||
<h3>Other Files</h3>
|
||
<ul class="directory-list">
|
||
{% for item in items %}
|
||
{% if not item.is_image %}
|
||
<li class="directory-item">
|
||
{% if item.unique_icon %}
|
||
<img src="{{ item.unique_icon }}" alt="Icon for {{ item.display_name }}" class="directory-icon">
|
||
{% endif %}
|
||
<div class="directory-content">
|
||
<a href="{{ item.url_path }}" class="directory-link">
|
||
{{ item.display_name }}{% if item.is_dir %}/{% endif %}
|
||
</a>
|
||
{% if not item.is_dir and item.file_date %}
|
||
<span class="directory-date">{{ item.file_date }}</span>
|
||
{% endif %}
|
||
</div>
|
||
</li>
|
||
{% endif %}
|
||
{% endfor %}
|
||
</ul>
|
||
</section>
|
||
{% endif %}
|
||
|
||
{% elif items %}
|
||
<section class="directory-listing">
|
||
<h3>Contents</h3>
|
||
<ul class="directory-list">
|
||
{% for item in items %}
|
||
<li class="directory-item">
|
||
{% if item.unique_icon %}
|
||
<img src="{{ item.unique_icon }}" alt="Icon for {{ item.display_name }}" class="directory-icon">
|
||
{% endif %}
|
||
<div class="directory-content">
|
||
<a href="{{ item.url_path }}" class="directory-link">
|
||
{{ item.display_name }}{% if item.is_dir %}/{% endif %}
|
||
</a>
|
||
{% if not item.is_dir and item.file_date %}
|
||
<span class="directory-date">{{ item.file_date }}</span>
|
||
{% endif %}
|
||
</div>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</section>
|
||
{% else %}
|
||
<section class="empty-directory">
|
||
<p><em>This directory is empty.</em></p>
|
||
</section>
|
||
{% endif %}
|
||
|
||
{% if index_content and content_position == 'bottom' %}
|
||
<section class="directory-description">
|
||
<h3>About This Directory</h3>
|
||
{{ index_content.content | safe }}
|
||
</section>
|
||
{% endif %}
|
||
|
||
{% if parent_directory %}
|
||
<section class="directory-navigation">
|
||
<a href="{{ parent_directory.url }}" class="parent-link">
|
||
{% if parent_directory.icon %}
|
||
<img src="{{ parent_directory.icon }}" alt="Parent directory icon" class="parent-icon">
|
||
{% endif %}
|
||
← Back to {{ parent_directory.display_name }}
|
||
</a>
|
||
</section>
|
||
{% endif %}
|
||
|
||
<!-- Lightbox HTML -->
|
||
<div id="lightbox" class="lightbox">
|
||
<div class="lightbox-close" onclick="closeLightbox()">×</div>
|
||
<div class="lightbox-nav lightbox-prev" onclick="changeImage(-1)"><</div>
|
||
<div class="lightbox-nav lightbox-next" onclick="changeImage(1)">></div>
|
||
<div class="lightbox-content">
|
||
<img id="lightbox-image" class="lightbox-image" src="" alt="">
|
||
<div id="lightbox-title" class="lightbox-title"></div>
|
||
<div id="lightbox-exif" class="lightbox-exif"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let currentImageIndex = 0;
|
||
let images = [];
|
||
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// Initialize gallery if images exist
|
||
const galleryItems = document.querySelectorAll('.gallery-thumbnail');
|
||
|
||
if (galleryItems.length > 0) {
|
||
// Build images array
|
||
images = Array.from(galleryItems).map(img => {
|
||
let exif = {};
|
||
try {
|
||
exif = JSON.parse(img.dataset.exif || '{}');
|
||
} catch (e) {
|
||
console.error('Error parsing EXIF data:', e);
|
||
}
|
||
return {
|
||
src: img.dataset.full,
|
||
title: img.dataset.title,
|
||
alt: img.alt,
|
||
exif: exif
|
||
};
|
||
});
|
||
|
||
// Add click handlers
|
||
galleryItems.forEach((img, index) => {
|
||
img.addEventListener('click', () => openLightbox(index));
|
||
});
|
||
}
|
||
|
||
// Close lightbox on background click
|
||
document.getElementById('lightbox').addEventListener('click', function(e) {
|
||
if (e.target === this) {
|
||
closeLightbox();
|
||
}
|
||
});
|
||
|
||
// Keyboard navigation
|
||
document.addEventListener('keydown', function(e) {
|
||
const lightbox = document.getElementById('lightbox');
|
||
if (lightbox.classList.contains('active')) {
|
||
switch(e.key) {
|
||
case 'Escape':
|
||
closeLightbox();
|
||
break;
|
||
case 'ArrowLeft':
|
||
changeImage(-1);
|
||
break;
|
||
case 'ArrowRight':
|
||
changeImage(1);
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
function openLightbox(index) {
|
||
currentImageIndex = index;
|
||
updateLightboxImage();
|
||
document.getElementById('lightbox').classList.add('active');
|
||
document.body.style.overflow = 'hidden'; // Prevent background scrolling
|
||
}
|
||
|
||
function closeLightbox() {
|
||
document.getElementById('lightbox').classList.remove('active');
|
||
document.body.style.overflow = ''; // Restore scrolling
|
||
}
|
||
|
||
function changeImage(direction) {
|
||
currentImageIndex += direction;
|
||
|
||
if (currentImageIndex >= images.length) {
|
||
currentImageIndex = 0;
|
||
} else if (currentImageIndex < 0) {
|
||
currentImageIndex = images.length - 1;
|
||
}
|
||
|
||
updateLightboxImage();
|
||
}
|
||
|
||
function updateLightboxImage() {
|
||
const image = images[currentImageIndex];
|
||
const lightboxImage = document.getElementById('lightbox-image');
|
||
const lightboxTitle = document.getElementById('lightbox-title');
|
||
const lightboxExif = document.getElementById('lightbox-exif');
|
||
|
||
lightboxImage.src = image.src;
|
||
lightboxImage.alt = image.alt;
|
||
lightboxTitle.textContent = image.title;
|
||
|
||
// Build EXIF display
|
||
const exif = image.exif || {};
|
||
let exifHtml = '';
|
||
|
||
// Camera info
|
||
if (exif.camera_make || exif.camera_model) {
|
||
const camera = [exif.camera_make, exif.camera_model].filter(Boolean).join(' ');
|
||
exifHtml += `<div class="exif-row"><span class="exif-label">Camera:</span>${camera}</div>`;
|
||
}
|
||
|
||
// Lens
|
||
if (exif.lens) {
|
||
exifHtml += `<div class="exif-row"><span class="exif-label">Lens:</span>${exif.lens}</div>`;
|
||
}
|
||
|
||
// Settings (on one line if all present)
|
||
const settings = [];
|
||
if (exif.focal_length) settings.push(exif.focal_length);
|
||
if (exif.aperture) settings.push(exif.aperture);
|
||
if (exif.shutter_speed) settings.push(exif.shutter_speed);
|
||
if (exif.iso) settings.push(exif.iso);
|
||
|
||
if (settings.length > 0) {
|
||
exifHtml += `<div class="exif-row"><span class="exif-label">Settings:</span>${settings.join(' • ')}</div>`;
|
||
}
|
||
|
||
// Date taken
|
||
if (exif.date_original || exif.date_taken) {
|
||
const date = exif.date_original || exif.date_taken;
|
||
exifHtml += `<div class="exif-row"><span class="exif-label">Taken:</span>${date}</div>`;
|
||
}
|
||
|
||
// Dimensions
|
||
if (exif.width && exif.height) {
|
||
exifHtml += `<div class="exif-row"><span class="exif-label">Size:</span>${exif.width} × ${exif.height}</div>`;
|
||
}
|
||
|
||
lightboxExif.innerHTML = exifHtml;
|
||
|
||
// Update navigation visibility
|
||
const prevBtn = document.querySelector('.lightbox-prev');
|
||
const nextBtn = document.querySelector('.lightbox-next');
|
||
|
||
if (images.length <= 1) {
|
||
prevBtn.style.display = 'none';
|
||
nextBtn.style.display = 'none';
|
||
} else {
|
||
prevBtn.style.display = 'flex';
|
||
nextBtn.style.display = 'flex';
|
||
}
|
||
}
|
||
</script>
|
||
{% endblock %} |