Sezione Campus: layout dedicato, route protette e documentazione
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
// Accesso ai contenuti della sezione Campus, generati da `npm run sync-campus`
|
||||
// in campus-content/. I file sono letti a runtime (non a build time) e tenuti
|
||||
// in cache in memoria: cambiano solo con un nuovo deploy.
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { join, normalize, resolve } from 'node:path';
|
||||
|
||||
export interface CampusLink { slug: string; area: string; title: string }
|
||||
|
||||
export interface CampusLesson {
|
||||
slug: string;
|
||||
kind: string;
|
||||
label: string;
|
||||
titleFull: string;
|
||||
titleShort: string;
|
||||
relatore: string;
|
||||
concetti: string[];
|
||||
chapter: number;
|
||||
area: string;
|
||||
prev: CampusLink | null;
|
||||
next: CampusLink | null;
|
||||
}
|
||||
|
||||
export interface CampusChapter {
|
||||
num: number;
|
||||
title: string;
|
||||
sub: string;
|
||||
color: string;
|
||||
lessons: CampusLesson[];
|
||||
}
|
||||
|
||||
export interface CampusArea {
|
||||
slug: string;
|
||||
title: string;
|
||||
sub: string;
|
||||
color: string;
|
||||
chapters: CampusChapter[];
|
||||
}
|
||||
|
||||
export interface CampusNav { areas: CampusArea[]; count: number }
|
||||
|
||||
const CONTENT_DIR = resolve(process.env.CAMPUS_DIR ?? './campus-content');
|
||||
|
||||
let navCache: CampusNav | null = null;
|
||||
|
||||
/** Struttura della sezione, o null se i contenuti non sono stati generati. */
|
||||
export function getNav(): CampusNav | null {
|
||||
if (navCache) return navCache;
|
||||
const file = join(CONTENT_DIR, 'nav.json');
|
||||
if (!existsSync(file)) {
|
||||
console.warn(`Campus: contenuti assenti in ${CONTENT_DIR}. Eseguire "npm run sync-campus".`);
|
||||
return null;
|
||||
}
|
||||
navCache = JSON.parse(readFileSync(file, 'utf8')) as CampusNav;
|
||||
return navCache;
|
||||
}
|
||||
|
||||
export function findArea(nav: CampusNav, slug: string): CampusArea | null {
|
||||
return nav.areas.find((a) => a.slug === slug) ?? null;
|
||||
}
|
||||
|
||||
export function findLesson(nav: CampusNav, slug: string): CampusLesson | null {
|
||||
for (const area of nav.areas)
|
||||
for (const ch of area.chapters) {
|
||||
const l = ch.lessons.find((x) => x.slug === slug);
|
||||
if (l) return l;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function chapterOf(nav: CampusNav, lesson: CampusLesson): CampusChapter | null {
|
||||
for (const area of nav.areas) {
|
||||
const ch = area.chapters.find((c) => c.num === lesson.chapter);
|
||||
if (ch) return ch;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Corpo HTML di una guida; null se il frammento non esiste. */
|
||||
export function readLessonHtml(slug: string): string | null {
|
||||
const file = join(CONTENT_DIR, 'pages', `${slug}.html`);
|
||||
if (!existsSync(file)) return null;
|
||||
return readFileSync(file, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Percorso assoluto di un asset dentro campus-content/assets, oppure null se
|
||||
* il path esce dalla cartella (traversal) o il file non esiste.
|
||||
*/
|
||||
export function resolveAsset(relPath: string): string | null {
|
||||
const base = join(CONTENT_DIR, 'assets');
|
||||
const full = resolve(base, normalize(relPath));
|
||||
if (full !== base && !full.startsWith(base + '/')) return null;
|
||||
return existsSync(full) ? full : null;
|
||||
}
|
||||
Reference in New Issue
Block a user