Add clean program page URLs
This commit is contained in:
parent
fbaf88941c
commit
911eeb0685
3 changed files with 40 additions and 16 deletions
20
README.md
20
README.md
|
|
@ -9,16 +9,16 @@ machine-language payload is required.
|
||||||
|
|
||||||
| Program | Type | Controls | Description | Run locally |
|
| Program | Type | Controls | Description | Run locally |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| [Meteor Dodge](meteor-dodge/meteor-dodge.bas) | Arcade | `A`, `D`, `Q` | Dodge falling meteors and build your score. | [Run program](http://localhost:8134/?program=meteor-dodge%2Fmeteor-dodge.prg) |
|
| [Meteor Dodge](meteor-dodge/meteor-dodge.bas) | Arcade | `A`, `D`, `Q` | Dodge falling meteors and build your score. | [Run program](http://localhost:8134/program/meteor-dodge) |
|
||||||
| [Code Breaker](code-breaker/code-breaker.bas) | Logic | `0`-`9`, `Q` | Deduce a four-digit secret in ten guesses. | [Run program](http://localhost:8134/?program=code-breaker%2Fcode-breaker.prg) |
|
| [Code Breaker](code-breaker/code-breaker.bas) | Logic | `0`-`9`, `Q` | Deduce a four-digit secret in ten guesses. | [Run program](http://localhost:8134/program/code-breaker) |
|
||||||
| [Submarine Hunt](submarine-hunt/submarine-hunt.bas) | Strategy | Row, Column, `Q` | Find three hidden submarines with limited sonar shots. | [Run program](http://localhost:8134/?program=submarine-hunt%2Fsubmarine-hunt.prg) |
|
| [Submarine Hunt](submarine-hunt/submarine-hunt.bas) | Strategy | Row, Column, `Q` | Find three hidden submarines with limited sonar shots. | [Run program](http://localhost:8134/program/submarine-hunt) |
|
||||||
| [Lunar Lander](lunar-lander/lunar-lander.bas) | Simulation | `0`-`9`, `Q` | Choose engine burns and touch down gently. | [Run program](http://localhost:8134/?program=lunar-lander%2Flunar-lander.prg) |
|
| [Lunar Lander](lunar-lander/lunar-lander.bas) | Simulation | `0`-`9`, `Q` | Choose engine burns and touch down gently. | [Run program](http://localhost:8134/program/lunar-lander) |
|
||||||
| [Memory Matrix](memory-matrix/memory-matrix.bas) | Memory | `1`, `2`, `3`, `4`, `Q` | Repeat a growing sequence of flashing quadrants. | [Run program](http://localhost:8134/?program=memory-matrix%2Fmemory-matrix.prg) |
|
| [Memory Matrix](memory-matrix/memory-matrix.bas) | Memory | `1`, `2`, `3`, `4`, `Q` | Repeat a growing sequence of flashing quadrants. | [Run program](http://localhost:8134/program/memory-matrix) |
|
||||||
| [Robot Maze](robot-maze/robot-maze.bas) | Puzzle | `W`, `A`, `S`, `D`, `Q` | Navigate a freshly generated maze to its exit. | [Run program](http://localhost:8134/?program=robot-maze%2Frobot-maze.prg) |
|
| [Robot Maze](robot-maze/robot-maze.bas) | Puzzle | `W`, `A`, `S`, `D`, `Q` | Navigate a freshly generated maze to its exit. | [Run program](http://localhost:8134/program/robot-maze) |
|
||||||
| [Music Box](music-box/music-box.bas) | Music | `A`-`K`, `1`, `2`, `3`, `Q` | Play the SID chip as a small computer keyboard. | [Run program](http://localhost:8134/?program=music-box%2Fmusic-box.prg) |
|
| [Music Box](music-box/music-box.bas) | Music | `A`-`K`, `1`, `2`, `3`, `Q` | Play the SID chip as a small computer keyboard. | [Run program](http://localhost:8134/program/music-box) |
|
||||||
| [Pixel Painter](pixel-painter/pixel-painter.bas) | Creative | Cursor keys, Space, `C`, `Q` | Draw character-cell pictures on the screen. | [Run program](http://localhost:8134/?program=pixel-painter%2Fpixel-painter.prg) |
|
| [Pixel Painter](pixel-painter/pixel-painter.bas) | Creative | Cursor keys, Space, `C`, `Q` | Draw character-cell pictures on the screen. | [Run program](http://localhost:8134/program/pixel-painter) |
|
||||||
| [Stock Trader](stock-trader/stock-trader.bas) | Simulation | `B`, `S`, `W`, `Q` | Trade a volatile stock over twenty market days. | [Run program](http://localhost:8134/?program=stock-trader%2Fstock-trader.prg) |
|
| [Stock Trader](stock-trader/stock-trader.bas) | Simulation | `B`, `S`, `W`, `Q` | Trade a volatile stock over twenty market days. | [Run program](http://localhost:8134/program/stock-trader) |
|
||||||
| [Starfield](starfield/starfield.bas) | Demo | `1`, `2`, `3`, `Q` | Watch and control a layered animated starfield. | [Run program](http://localhost:8134/?program=starfield%2Fstarfield.prg) |
|
| [Starfield](starfield/starfield.bas) | Demo | `1`, `2`, `3`, `Q` | Watch and control a layered animated starfield. | [Run program](http://localhost:8134/program/starfield) |
|
||||||
|
|
||||||
## Running the programs
|
## Running the programs
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ function renderPrograms() {
|
||||||
for (const program of programs) {
|
for (const program of programs) {
|
||||||
const card = document.createElement('article');
|
const card = document.createElement('article');
|
||||||
card.className = 'program-card';
|
card.className = 'program-card';
|
||||||
const programUrl = `/?program=${encodeURIComponent(program.path)}`;
|
const programUrl = `/program/${encodeURIComponent(program.slug)}`;
|
||||||
card.addEventListener('click', event => {
|
card.addEventListener('click', event => {
|
||||||
if (!event.target.closest('a, button')) location.href = programUrl;
|
if (!event.target.closest('a, button')) location.href = programUrl;
|
||||||
});
|
});
|
||||||
|
|
@ -134,10 +134,11 @@ async function loadPrograms() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startEmulator(programPath) {
|
function startEmulator(program) {
|
||||||
|
const programPath = program.path;
|
||||||
library.hidden = true;
|
library.hidden = true;
|
||||||
player.hidden = false;
|
player.hidden = false;
|
||||||
const title = titleFromPath(programPath);
|
const title = program.title || titleFromPath(programPath);
|
||||||
document.title = `${title} — C64 BASIC Collection`;
|
document.title = `${title} — C64 BASIC Collection`;
|
||||||
document.querySelector('#program-title').textContent = title;
|
document.querySelector('#program-title').textContent = title;
|
||||||
|
|
||||||
|
|
@ -164,6 +165,23 @@ function startEmulator(programPath) {
|
||||||
document.body.append(loader);
|
document.body.append(loader);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadProgramPage(slug) {
|
||||||
|
library.hidden = true;
|
||||||
|
player.hidden = false;
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/programs', { cache: 'no-store' });
|
||||||
|
if (!response.ok) throw new Error('The program list could not be loaded.');
|
||||||
|
const { programs } = await response.json();
|
||||||
|
const program = programs.find(entry => entry.slug === slug);
|
||||||
|
if (!program) throw new Error('This program does not exist.');
|
||||||
|
startEmulator(program);
|
||||||
|
} catch (error) {
|
||||||
|
const message = document.querySelector('#emulator-error');
|
||||||
|
message.hidden = false;
|
||||||
|
message.textContent = error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
refreshButton.addEventListener('click', loadPrograms);
|
refreshButton.addEventListener('click', loadPrograms);
|
||||||
searchInput.addEventListener('input', renderPrograms);
|
searchInput.addEventListener('input', renderPrograms);
|
||||||
typeFilter.addEventListener('change', renderPrograms);
|
typeFilter.addEventListener('change', renderPrograms);
|
||||||
|
|
@ -186,7 +204,11 @@ function setView(view) {
|
||||||
}
|
}
|
||||||
|
|
||||||
setView(localStorage.getItem('program-view') === 'list' ? 'list' : 'grid');
|
setView(localStorage.getItem('program-view') === 'list' ? 'list' : 'grid');
|
||||||
const programPath = new URLSearchParams(location.search).get('program');
|
const legacyProgramPath = new URLSearchParams(location.search).get('program');
|
||||||
if (programPath) startEmulator(programPath);
|
const programPage = location.pathname.match(/^\/program\/([a-z0-9]+(?:-[a-z0-9]+)*)\/?$/);
|
||||||
|
if (legacyProgramPath) {
|
||||||
|
const slug = legacyProgramPath.split('/')[0];
|
||||||
|
location.replace(`/program/${encodeURIComponent(slug)}`);
|
||||||
|
} else if (programPage) loadProgramPage(decodeURIComponent(programPage[1]));
|
||||||
else loadPrograms();
|
else loadPrograms();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ function fetchPrograms() {
|
||||||
const sourceFilePath = resolve(programDirectory, sourcePath);
|
const sourceFilePath = resolve(programDirectory, sourcePath);
|
||||||
return {
|
return {
|
||||||
...program,
|
...program,
|
||||||
|
slug: program.path.split('/')[0],
|
||||||
source: sourceFilePath.startsWith(programDirectory + sep) && existsSync(sourceFilePath) ? sourcePath : null,
|
source: sourceFilePath.startsWith(programDirectory + sep) && existsSync(sourceFilePath) ? sourcePath : null,
|
||||||
size: statSync(filePath).size
|
size: statSync(filePath).size
|
||||||
};
|
};
|
||||||
|
|
@ -72,7 +73,8 @@ function sendSource(response, programPath, headOnly = false) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function serveStatic(response, pathname, headOnly = false) {
|
function serveStatic(response, pathname, headOnly = false) {
|
||||||
const requested = pathname === '/' ? 'index.html' : pathname.slice(1);
|
const isProgramPage = /^\/program\/[a-z0-9]+(?:-[a-z0-9]+)*\/?$/.test(pathname);
|
||||||
|
const requested = pathname === '/' || isProgramPage ? 'index.html' : pathname.slice(1);
|
||||||
const filePath = normalize(join(publicDirectory, requested));
|
const filePath = normalize(join(publicDirectory, requested));
|
||||||
if (!filePath.startsWith(publicDirectory) || !existsSync(filePath) || !statSync(filePath).isFile()) {
|
if (!filePath.startsWith(publicDirectory) || !existsSync(filePath) || !statSync(filePath).isFile()) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue