feat: add romhack archive site

This commit is contained in:
ginnoir
2026-06-08 21:16:04 -05:00
commit c868800495
518 changed files with 46140 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
---
import HackCard from '../components/HackCard.astro';
import FilterGroup from '../components/FilterGroup.astro';
import Layout from '../components/Layout.astro';
import { catalog, flagshipHacks, getFacetEntries, hacks } from '../lib/site-data';
const platforms = getFacetEntries('platform');
const bases = getFacetEntries('base').slice(0, 12);
const types = getFacetEntries('type');
const statuses = getFacetEntries('status');
---
<Layout title="ROM Hack Archive">
<section class="hero">
<div class="hero-copy">
<h1>ROM Hack Archive</h1>
<p>A private catalog of {catalog.count} Pokémon ROM hacks, fan-games, patches, notes, guides, and files.</p>
</div>
<div class="hero-stats" aria-label="Catalog totals">
<div><strong>{catalog.count}</strong><span>hacks</span></div>
<div><strong>{catalog.facets.platform.GBA ?? 0}</strong><span>GBA</span></div>
<div><strong>{catalog.facets.type.Expansion ?? 0}</strong><span>expansions</span></div>
</div>
</section>
<section class="flagships" aria-labelledby="flagships-heading">
<div class="section-heading">
<h2 id="flagships-heading">Flagship Picks</h2>
<p>Known anchors from the collection.</p>
</div>
<div class="flagship-grid">
{flagshipHacks.map((hack) => <HackCard hack={hack} compact />)}
</div>
</section>
<section class="catalog-shell" aria-label="Hack catalog">
<aside class="filter-rail">
<div class="filter-panel">
<label class="search-label" for="search">Search hacks</label>
<input id="search" class="search-input" type="search" placeholder="Search hacks" data-search-input />
</div>
<div class="filter-panel">
<label class="search-label" for="sort">Sort</label>
<select id="sort" class="select-input" data-sort-select>
<option value="name">Name</option>
<option value="release_date">Release date</option>
<option value="platform">Platform</option>
</select>
</div>
<FilterGroup title="Platform" name="platform" entries={platforms} />
<FilterGroup title="Base" name="base" entries={bases} />
<FilterGroup title="Type" name="type" entries={types} />
<FilterGroup title="Status" name="status" entries={statuses} />
</aside>
<div class="catalog-results">
<div class="results-bar">
<h2>All Hacks</h2>
<span data-result-count>{hacks.length} shown</span>
</div>
<div class="hack-grid" data-card-grid>
{hacks.map((hack) => <HackCard hack={hack} />)}
</div>
<p class="empty-state" data-empty-state hidden>No hacks match those filters.</p>
</div>
</section>
<script is:inline>
const grid = document.querySelector('[data-card-grid]');
const cards = Array.from(grid.querySelectorAll('[data-card]'));
const searchInput = document.querySelector('[data-search-input]');
const sortSelect = document.querySelector('[data-sort-select]');
const resultCount = document.querySelector('[data-result-count]');
const emptyState = document.querySelector('[data-empty-state]');
const filterInputs = Array.from(document.querySelectorAll('[data-filter]'));
function selected(name) {
return filterInputs
.filter((input) => input.name === name && input.checked)
.map((input) => input.value);
}
function matches(card) {
const query = searchInput.value.trim().toLowerCase();
const platform = selected('platform');
const base = selected('base');
const type = selected('type');
const status = selected('status');
const cardTypes = card.dataset.types ? card.dataset.types.split('|') : [];
if (query && !card.dataset.search.includes(query)) return false;
if (platform.length && !platform.includes(card.dataset.platform)) return false;
if (base.length && !base.includes(card.dataset.base)) return false;
if (status.length && !status.includes(card.dataset.status)) return false;
if (type.length && !type.some((value) => cardTypes.includes(value))) return false;
return true;
}
function sortCards(visibleCards) {
const key = sortSelect.value;
return visibleCards.sort((a, b) => {
if (key === 'release_date') return (b.dataset.release || '').localeCompare(a.dataset.release || '');
if (key === 'platform') {
const byPlatform = (a.dataset.platform || 'ZZZ').localeCompare(b.dataset.platform || 'ZZZ');
return byPlatform || a.dataset.title.localeCompare(b.dataset.title);
}
return a.dataset.title.localeCompare(b.dataset.title);
});
}
function updateCatalog() {
const visible = [];
for (const card of cards) {
const isVisible = matches(card);
card.hidden = !isVisible;
if (isVisible) visible.push(card);
}
for (const card of sortCards(visible)) grid.append(card);
resultCount.textContent = `${visible.length} shown`;
emptyState.hidden = visible.length !== 0;
}
searchInput.addEventListener('input', updateCatalog);
sortSelect.addEventListener('change', updateCatalog);
for (const input of filterInputs) input.addEventListener('change', updateCatalog);
</script>
</Layout>