Landing page editor: logo/background/subtitle, markdown details page, editable disclaimer, photo shutter

This commit is contained in:
2026-07-24 16:35:00 +10:00
parent dfa2ae12e8
commit a1234fb851
8 changed files with 373 additions and 31 deletions
+30
View File
@@ -13,6 +13,7 @@ const PLAYLIST_LIVE = path.join(LIVE_DIR, 'playlist.live.json');
const MODELS_LIVE = path.join(LIVE_DIR, 'models.live.json');
const MODELS_SEED = path.join(DATA, 'models.json');
const CHARS_LIVE = path.join(LIVE_DIR, 'characters.live.json');
const LANDING_LIVE = path.join(LIVE_DIR, 'landing.live.json');
fs.mkdirSync(LIVE_DIR, { recursive: true });
@@ -32,6 +33,24 @@ let models = readJSON(MODELS_LIVE, null) || readJSON(MODELS_SEED, { models: [] }
* faceTextureUrl, torsoTextureUrl, heightCm, scale } } } */
let characters = readJSON(CHARS_LIVE, null) || { defaults: {}, byId: {} };
/* landing: end-user facing start screen + details page. All text is editable except
* that the disclaimer is ALWAYS rendered (wording editable, presence is not). */
const LANDING_DEFAULTS = {
title: 'NEWBURY NIGHTS',
subtitle: 'Point your phone at the Newbury Crests to reveal the hidden side of the town.',
logoUrl: '',
backgroundUrl: '',
startButton: 'Start Ghost Watching',
detailsButton: 'About this build',
detailsTitle: 'About Newbury Nights',
detailsMarkdown: 'Write about your build here.\n\nUse **bold**, *italic*, and blank lines for paragraphs.',
disclaimer: 'Fan-made tribute experience. Not affiliated with, sponsored, or endorsed by the LEGO Group. LEGO® and Hidden Side™ are trademarks of the LEGO Group.',
accent: '#51eaf1',
textColor: '#e8ecff',
overlay: 0.55,
};
let landing = Object.assign({}, LANDING_DEFAULTS, readJSON(LANDING_LIVE, null) || {});
const listeners = [];
function validateScene(s) {
@@ -76,6 +95,17 @@ module.exports = {
return scene;
},
getGhosts: () => ({ ghosts, gradients }),
getLanding: () => landing,
putLanding(l) {
if (!l || typeof l !== 'object') throw new Error('landing must be object');
const next = Object.assign({}, LANDING_DEFAULTS, l);
// disclaimer may be reworded but never emptied — it protects the project
if (!String(next.disclaimer || '').trim()) next.disclaimer = LANDING_DEFAULTS.disclaimer;
landing = next;
writeJSON(LANDING_LIVE, landing);
listeners.forEach(f => f());
return landing;
},
getCharacters: () => characters,
putCharacters(c) {
if (!c || typeof c !== 'object') throw new Error('characters must be object');