---
import Layout from '../layouts/Layout.astro';
import { readdir, readFile } from 'fs/promises';
import { join, basename, extname } from 'path';
// Get world info and images from the symlinked content
let worldTitle = 'World';
let images: Array<{
src: string;
alt: string;
title: string;
category: string;
description?: string;
entryLink?: string;
entryType?: 'entry' | 'taxonomy' | 'overview';
}> = [];
try {
// Read world overview for title
const overviewPath = join(process.cwd(), 'src/content/overview', 'world-overview.md');
const rawOverview = await readFile(overviewPath, 'utf-8');
const titleMatch = rawOverview.match(/^# (.+)$/m);
if (titleMatch) {
worldTitle = titleMatch[1];
}
// During development, we need to find the world's images directory
// In production, images will be copied to the site
try {
// First, try to find the world directory by resolving the symlink
const overviewSymlinkPath = join(process.cwd(), 'src/content/overview');
const { realpathSync } = await import('fs');
let worldPath = '';
try {
const realOverviewPath = realpathSync(overviewSymlinkPath);
// The world directory is the parent of the overview directory
worldPath = join(realOverviewPath, '..');
} catch {
// Symlink resolution failed
}
// Get images from the world's images directory
const worldImagesPath = worldPath ? join(worldPath, 'images') : '';
const fs = await import('fs');
if (worldImagesPath && fs.existsSync(worldImagesPath)) {
// Get all image files recursively
async function getImagesRecursively(dirPath: string, category: string = ''): Promise<void> {
const items = await readdir(dirPath, { withFileTypes: true });
for (const item of items) {
const fullPath = join(dirPath, item.name);
if (item.isDirectory()) {
// Recursively process subdirectories
const subCategory = category ? `${category}/${item.name}` : item.name;
await getImagesRecursively(fullPath, subCategory);
} else if (item.isFile() && ['.png', '.jpg', '.jpeg', '.gif', '.webp'].includes(extname(item.name).toLowerCase())) {
// This is an image file
const relativePath = `/images/${category ? category + '/' : ''}${item.name}`;
const fileName = basename(item.name, extname(item.name));
const displayName = fileName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
const categoryName = category || 'General';
const categoryDisplay = categoryName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
// Determine the source link and type
let entryLink: string | undefined;
let entryType: 'entry' | 'taxonomy' | 'overview' | undefined;
let description = `${displayName} from ${categoryDisplay}`;
if (fileName.includes('world-overview')) {
// This is a world overview image
entryLink = '/';
entryType = 'overview';
description = `${displayName} - Click to view world overview`;
} else if (fileName.endsWith('-taxonomy')) {
// This is a taxonomy overview image
const taxonomyName = fileName.replace('-taxonomy', '');
entryLink = `/taxonomies/${taxonomyName}`;
entryType = 'taxonomy';
description = `${displayName} - Click to view ${categoryDisplay} taxonomy`;
} else if (category && category !== 'General') {
// This should be an entry image - check if corresponding entry exists
try {
const entriesPath = join(process.cwd(), 'src/content/entries', category);
const entryFiles = await readdir(entriesPath);
const matchingEntry = entryFiles.find(entryFile => {
const entryName = basename(entryFile, '.md');
return entryName === fileName;
});
if (matchingEntry) {
entryLink = `/taxonomies/${category}/entries/${fileName}`;
entryType = 'entry';
description = `${displayName} - Click to view entry`;
}
} catch {
// Entry directory doesn't exist or other error
}
}
images.push({
src: relativePath,
alt: displayName,
title: displayName,
category: categoryDisplay,
description,
entryLink,
entryType
});
}
}
}
await getImagesRecursively(worldImagesPath);
}
} catch (error) {
console.error('Error loading gallery images:', error);
}
} catch (error) {
console.error('Error reading world content:', error);
}
// Sort images by category, then by title
images.sort((a, b) => {
if (a.category === b.category) {
return a.title.localeCompare(b.title);
}
return a.category.localeCompare(b.category);
});
// Group images by category
const imagesByCategory = images.reduce((acc, img) => {
if (!acc[img.category]) {
acc[img.category] = [];
}
acc[img.category].push(img);
return acc;
}, {} as Record<string, typeof images>);
---
<Layout title={`Gallery - ${worldTitle}`} description={`Visual gallery of ${worldTitle}`}>
<div style="padding: 2rem; padding-bottom: 0;">
<div class="world-header">
<nav style="margin-bottom: 1rem;">
<a href="/" style="color: #666; text-decoration: none;">Home</a>
<span style="color: #ccc; margin: 0 0.5rem;">›</span>
<span style="color: #333;">Gallery</span>
</nav>
<h1>Gallery</h1>
<p style="color: #666;">Visual exploration of {worldTitle}</p>
</div>
</div>
<div style="padding: 2rem;">
{images.length === 0 ? (
<div style="text-align: center; padding: 4rem; color: #666;">
<h2>No images found</h2>
<p>Generate some images for your world entries to see them here!</p>
</div>
) : (
<>
<div style="margin-bottom: 2rem;">
<p style="color: #666; font-size: 0.9em;">
Showing {images.length} images across {Object.keys(imagesByCategory).length} categories
</p>
</div>
{Object.entries(imagesByCategory).map(([category, categoryImages]) => (
<section style="margin-bottom: 3rem;">
<h2 style="color: #333; border-bottom: 2px solid #eee; padding-bottom: 0.5rem; margin-bottom: 1.5rem;">
{category} ({categoryImages.length})
</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 2rem;">
{categoryImages.map(image => (
<div style="position: relative;">
<div style="border: 1px solid #e0e0e0; border-radius: 8px; overflow: hidden; background: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.1); transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;"
onmouseover="this.style.transform='translateY(-4px)'; this.style.boxShadow='0 6px 16px rgba(0,0,0,0.2)'; this.style.borderColor='#007acc';"
onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 4px rgba(0,0,0,0.1)'; this.style.borderColor='#e0e0e0';">
<div style="aspect-ratio: 16/9; overflow: hidden; position: relative;">
<img
src={image.src}
alt={image.alt}
style="width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s ease, filter 0.2s ease; cursor: pointer; filter: brightness(1);"
onclick={`openFullscreen(${JSON.stringify(image)}, ${images.indexOf(image)})`}
onmouseover="this.style.transform='scale(1.05)'; this.style.filter='brightness(1.1)';"
onmouseout="this.style.transform='scale(1)'; this.style.filter='brightness(1)';"
loading="lazy"
/>
<div style="position: absolute; top: 8px; right: 8px; background: rgba(0,0,0,0.8); color: white; padding: 6px 10px; border-radius: 6px; font-size: 0.85em; opacity: 0.9; transition: opacity 0.2s ease, transform 0.2s ease; box-shadow: 0 2px 8px rgba(0,0,0,0.3);"
onmouseover="this.style.opacity='1'; this.style.transform='scale(1.05)';"
onmouseout="this.style.opacity='0.9'; this.style.transform='scale(1)';">
🔍 Click to expand
</div>
</div>
<div style="padding: 1rem;">
<h3 style="margin: 0 0 0.5rem 0; font-size: 1.1rem; color: #333;">
{image.entryLink ? (
<a href={image.entryLink} style="text-decoration: none; color: inherit;">
{image.title}
{image.entryType === 'entry' && <span style="color: #007acc; font-size: 0.8em; margin-left: 0.5rem;">→</span>}
{image.entryType === 'taxonomy' && <span style="color: #007acc; font-size: 0.8em; margin-left: 0.5rem;">📁</span>}
{image.entryType === 'overview' && <span style="color: #007acc; font-size: 0.8em; margin-left: 0.5rem;">🏠</span>}
</a>
) : image.title}
</h3>
{image.description && (
<p style="margin: 0; color: #666; font-size: 0.9em; line-height: 1.4;">
{image.description}
</p>
)}
</div>
</div>
</div>
))}
</div>
</section>
))}
</>
)}
</div>
<!-- Fullscreen Modal -->
<div id="fullscreenModal" style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.95); z-index: 1000; justify-content: center; align-items: center;">
<div style="position: relative; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;">
<!-- Close button -->
<button id="closeModal" style="position: absolute; top: 20px; right: 20px; background: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.3); color: white; padding: 10px 15px; border-radius: 50%; cursor: pointer; font-size: 1.2rem; z-index: 1001; transition: background 0.2s ease;"
onmouseover="this.style.background='rgba(255,255,255,0.3)';"
onmouseout="this.style.background='rgba(255,255,255,0.2)';">
✕
</button>
<!-- Previous button -->
<button id="prevButton" style="position: absolute; left: 20px; top: 50%; transform: translateY(-50%); background: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.3); color: white; padding: 15px 20px; border-radius: 50%; cursor: pointer; font-size: 1.5rem; z-index: 1001; transition: background 0.2s ease;"
onmouseover="this.style.background='rgba(255,255,255,0.3)';"
onmouseout="this.style.background='rgba(255,255,255,0.2)';">
‹
</button>
<!-- Next button -->
<button id="nextButton" style="position: absolute; right: 20px; top: 50%; transform: translateY(-50%); background: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.3); color: white; padding: 15px 20px; border-radius: 50%; cursor: pointer; font-size: 1.5rem; z-index: 1001; transition: background 0.2s ease;"
onmouseover="this.style.background='rgba(255,255,255,0.3)';"
onmouseout="this.style.background='rgba(255,255,255,0.2)';">
›
</button>
<!-- Image container with image-focused layout -->
<div style="display: flex; align-items: center; max-width: 98vw; max-height: 95vh; gap: 1.5rem;">
<!-- Compact description panel on left -->
<div id="imageInfo" style="background: rgba(0,0,0,0.9); color: white; padding: 1.5rem; border-radius: 10px; width: 280px; flex-shrink: 0; backdrop-filter: blur(15px); box-shadow: 0 8px 25px rgba(0,0,0,0.6);">
<h3 id="imageTitle" style="margin: 0 0 0.8rem 0; font-size: 1.2rem; color: #fff; line-height: 1.3;"></h3>
<p id="imageDescription" style="margin: 0 0 1rem 0; color: #ccc; font-size: 0.9rem; line-height: 1.4;"></p>
<div id="imageActions" style="margin-bottom: 1rem;"></div>
<div style="font-size: 0.8rem; color: #999; border-top: 1px solid rgba(255,255,255,0.15); padding-top: 0.8rem;">
<div style="margin-bottom: 0.4rem;"><span id="imageCounter"></span></div>
<div style="font-size: 0.75rem;">ESC to close • ← → navigate</div>
</div>
</div>
<!-- Large image taking up most space -->
<div style="flex: 1; display: flex; justify-content: center; align-items: center; height: 95vh;">
<img id="fullscreenImage" style="max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 8px; box-shadow: 0 20px 50px rgba(0,0,0,0.7);" />
</div>
</div>
</div>
</div>
</Layout>
<style>
@media (max-width: 768px) {
.gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
}
}
@media (max-width: 480px) {
.gallery-grid {
grid-template-columns: 1fr;
}
}
</style>
<script define:vars={{ images }}>
let currentImageIndex = 0;
let allImages = images;
function openFullscreen(image, index) {
currentImageIndex = index;
showFullscreenImage();
document.getElementById('fullscreenModal').style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closeFullscreen() {
document.getElementById('fullscreenModal').style.display = 'none';
document.body.style.overflow = 'auto';
}
function showFullscreenImage() {
const image = allImages[currentImageIndex];
const modal = document.getElementById('fullscreenModal');
const img = document.getElementById('fullscreenImage');
const title = document.getElementById('imageTitle');
const description = document.getElementById('imageDescription');
const actions = document.getElementById('imageActions');
const counter = document.getElementById('imageCounter');
img.src = image.src;
img.alt = image.alt;
title.textContent = image.title;
description.textContent = image.description || '';
counter.textContent = `${currentImageIndex + 1} of ${allImages.length}`;
// Add source link if available
if (image.entryLink) {
actions.innerHTML = `
<a href="${image.entryLink}" style="color: #4CAF50; text-decoration: none; background: rgba(76,175,80,0.25); padding: 8px 14px; border-radius: 6px; border: 1px solid rgba(76,175,80,0.4); transition: all 0.2s ease; display: inline-block; font-weight: 500; font-size: 0.85rem;"
onmouseover="this.style.background='rgba(76,175,80,0.35)'; this.style.transform='translateY(-1px)'; this.style.boxShadow='0 3px 8px rgba(76,175,80,0.3)';"
onmouseout="this.style.background='rgba(76,175,80,0.25)'; this.style.transform='translateY(0)'; this.style.boxShadow='none';">
${image.entryType === 'entry' ? '→ View Entry' : image.entryType === 'taxonomy' ? '📁 View Taxonomy' : '🏠 View Overview'}
</a>
`;
} else {
actions.innerHTML = '';
}
// Update navigation button states
document.getElementById('prevButton').style.opacity = currentImageIndex > 0 ? '1' : '0.3';
document.getElementById('nextButton').style.opacity = currentImageIndex < allImages.length - 1 ? '1' : '0.3';
}
function nextImage() {
if (currentImageIndex < allImages.length - 1) {
currentImageIndex++;
showFullscreenImage();
}
}
function prevImage() {
if (currentImageIndex > 0) {
currentImageIndex--;
showFullscreenImage();
}
}
// Event listeners
document.getElementById('closeModal').addEventListener('click', closeFullscreen);
document.getElementById('nextButton').addEventListener('click', nextImage);
document.getElementById('prevButton').addEventListener('click', prevImage);
// Keyboard navigation
document.addEventListener('keydown', function(e) {
const modal = document.getElementById('fullscreenModal');
if (modal.style.display === 'flex') {
switch(e.key) {
case 'Escape':
closeFullscreen();
break;
case 'ArrowLeft':
prevImage();
break;
case 'ArrowRight':
nextImage();
break;
}
}
});
// Close on background click
document.getElementById('fullscreenModal').addEventListener('click', function(e) {
if (e.target === this) {
closeFullscreen();
}
});
// Make functions globally available
window.openFullscreen = openFullscreen;
</script>