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
+199
View File
@@ -0,0 +1,199 @@
import { describe, expect, it } from 'vitest';
import {
buildFacets,
applyBannerManifest,
applyGeneratedFileLinks,
normalizeCatalog,
parseReleaseDate,
slugifyStem,
sortHacks,
} from './catalog';
import type { RawCatalog } from './types';
const baseCatalog: RawCatalog = {
generated: '2026-06-08',
count: 3,
image_base: 'https://romhacks-files.ginnoir.com',
facets: {
platform: { GBA: 2, Unknown: 1 },
base: { FireRed: 1, Unknown: 2 },
status: { Complete: 2, Unknown: 1 },
type: { Difficulty: 1, Expansion: 1 },
play_status: { Unplayed: 3 },
},
hacks: [
{
stem: 'Radical Red',
title: 'Pokémon Radical Red',
platform: 'GBA',
base: 'FireRed',
status: 'Complete',
version: '4.1',
developer: 'Soupercell',
release_date: '2024',
banner: 'https://romhacks-files.ginnoir.com/_meta/radical/banner.jpg',
source: 'https://example.com/radical',
library_path: 'roms/gba/Hacks/Pokemon - Radical Red (Hack).gba',
play_status: 'Unplayed',
rating: 5,
type: ['Difficulty', 'Expansion'],
tags: ['hack'],
summary: 'Fast, difficult FireRed expansion.',
notability: 'Flagship difficulty hack.',
story: '',
features: ['Expanded Pokédex'],
links: [{ label: 'Source / info', url: 'https://example.com/radical' }],
},
{
stem: 'A FarfetchΓÇÖD Story',
title: 'Pokémon A FarfetchD Story',
platform: 'PC',
base: null,
status: 'Complete',
version: 'completed',
developer: 'KennyCatches',
release_date: '2023-08-10',
scrape_dir: 'Pokemon_A_Farfetch_D_Story',
library_path: '/storage1/labdata/romhacks/library/Pokemon_A_Farfetch_D_Story/',
play_status: 'Unplayed',
type: ['New Experience'],
tags: [],
summary: 'A short RPGXP adventure.',
notability: '',
story: '',
features: [],
links: [],
},
{
stem: 'Unknown Base',
title: 'Pokémon Unknown Base',
platform: null,
base: '—',
status: null,
version: '',
library_path: '—',
play_status: 'Unplayed',
type: [],
tags: [],
summary: '',
notability: '',
story: '',
features: [],
links: [],
},
],
};
describe('catalog normalization', () => {
it('creates deterministic URL-safe slugs from stems', () => {
expect(slugifyStem('Radical Red')).toBe('radical-red');
expect(slugifyStem('A FarfetchΓÇÖD Story')).toBe('a-farfetch-d-story');
expect(slugifyStem('Pokémon: Crystal Clear!')).toBe('pokemon-crystal-clear');
});
it('normalizes records without rendering null-like values', () => {
const catalog = normalizeCatalog(baseCatalog);
const unknown = catalog.hacks.find((hack) => hack.stem === 'Unknown Base');
expect(catalog.count).toBe(3);
expect(catalog.facets.platform).toEqual(baseCatalog.facets.platform);
expect(unknown?.platform).toBeUndefined();
expect(unknown?.base).toBeUndefined();
expect(unknown?.status).toBeUndefined();
expect(unknown?.chips.map((chip) => chip.label)).not.toContain('—');
expect(unknown?.searchText).not.toMatch(/\bnull\b|—/i);
});
it('maps ROM and library paths to internal file-server links', () => {
const catalog = normalizeCatalog(baseCatalog);
const radical = catalog.hacks.find((hack) => hack.slug === 'radical-red');
const farfetchd = catalog.hacks.find((hack) => hack.slug === 'a-farfetch-d-story');
expect(radical?.fileLinks).toContainEqual({
label: 'ROM file',
url: 'https://romhacks-files.ginnoir.com/_roms/gba/Hacks/Pokemon%20-%20Radical%20Red%20%28Hack%29.gba',
kind: 'rom',
});
expect(farfetchd?.fileLinks).toContainEqual({
label: 'Browse related files',
url: 'https://romhacks-files.ginnoir.com/Pokemon_A_Farfetch_D_Story/',
kind: 'library',
});
});
it('builds facets from normalized records when counts are not supplied', () => {
const facets = buildFacets(normalizeCatalog(baseCatalog).hacks);
expect(facets.platform.GBA).toBe(1);
expect(facets.platform.PC).toBe(1);
expect(facets.platform.Unknown).toBe(1);
expect(facets.type.Difficulty).toBe(1);
expect(facets.type.Expansion).toBe(1);
});
it('applies downloaded banner manifest paths and placeholder fallback', () => {
const catalog = normalizeCatalog(baseCatalog);
const withBanners = applyBannerManifest(catalog, {
'radical-red': '/assets/banners/radical-red.jpg',
});
const radical = withBanners.hacks.find((hack) => hack.slug === 'radical-red');
const unknown = withBanners.hacks.find((hack) => hack.slug === 'unknown-base');
expect(radical?.bannerLocal).toBe('/assets/banners/radical-red.jpg');
expect(unknown?.bannerLocal).toBe('/assets/placeholders/hack.svg');
});
it('merges generated guide links without dropping ROM or browse links', () => {
const catalog = normalizeCatalog(baseCatalog);
const withFiles = applyGeneratedFileLinks(catalog, {
'a-farfetch-d-story': [
{
label: 'guide.pdf',
url: 'https://romhacks-files.ginnoir.com/Pokemon_A_Farfetch_D_Story/guide.pdf',
kind: 'guide',
},
],
});
const farfetchd = withFiles.hacks.find((hack) => hack.slug === 'a-farfetch-d-story');
expect(farfetchd?.fileLinks).toEqual([
{
label: 'Browse related files',
url: 'https://romhacks-files.ginnoir.com/Pokemon_A_Farfetch_D_Story/',
kind: 'library',
},
{
label: 'guide.pdf',
url: 'https://romhacks-files.ginnoir.com/Pokemon_A_Farfetch_D_Story/guide.pdf',
kind: 'guide',
},
]);
});
it('sorts by name, release date, and platform', () => {
const hacks = normalizeCatalog(baseCatalog).hacks;
expect(sortHacks(hacks, 'name').map((hack) => hack.slug)).toEqual([
'a-farfetch-d-story',
'radical-red',
'unknown-base',
]);
expect(sortHacks(hacks, 'release_date').map((hack) => hack.slug)).toEqual([
'radical-red',
'a-farfetch-d-story',
'unknown-base',
]);
expect(sortHacks(hacks, 'platform').map((hack) => hack.slug)).toEqual([
'radical-red',
'a-farfetch-d-story',
'unknown-base',
]);
});
it('parses bare years and ISO dates for descending date sort', () => {
expect(parseReleaseDate('2024')).toBe(new Date('2024-01-01').getTime());
expect(parseReleaseDate('2023-08-10')).toBe(new Date('2023-08-10').getTime());
expect(parseReleaseDate(null)).toBe(0);
});
});