From bc6a013234f8c8a1e4fb8a06f0343e1bf3aef5ec Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Mar 2026 12:29:37 +0000 Subject: [PATCH 1/3] fix: Resolve naturfag crash caused by unconditional nounbank fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The migration module had a top-level `await fetchCoreBank('de', 'nounbank')` that ran unconditionally when imported, causing a 404 crash on non-language courses like naturfag that have no German vocabulary data. Changes: - Move nounbank fetch from top-level await into convertKnownWordsToNewFormat() so it only runs during actual migration and gracefully handles 404 - Make migrateProgressData() async to support the lazy fetch - Add naturfag-vg1 to the curriculum registry with proper nb language config so getTargetLanguageCode() doesn't fall back to 'de' - Add 'nb' case to shell.js enforceLanguageConsistency() so naturfag pages don't get force-switched to the German curriculum ๐Ÿค– Generated with Papertek โ€” Framework for Education Co-Authored-By: Claude https://claude.ai/code/session_017rtsVGshceLbjsQaDx7HyA --- public/js/layout/shell.js | 1 + public/js/progress/curriculum-registry.js | 32 +++++++++++++++++++++++ public/js/progress/migration.js | 22 +++++++++++----- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/public/js/layout/shell.js b/public/js/layout/shell.js index a4533f9..b6cc3cf 100644 --- a/public/js/layout/shell.js +++ b/public/js/layout/shell.js @@ -221,6 +221,7 @@ export class AppShell { switch (this.language) { case 'es': defaultId = 'spansk1-vg1'; break; case 'fr': defaultId = 'fransk1-vg1'; break; + case 'nb': defaultId = 'naturfag-vg1'; break; default: defaultId = 'tysk1-vg1'; } setActiveCurriculum(defaultId); diff --git a/public/js/progress/curriculum-registry.js b/public/js/progress/curriculum-registry.js index b430972..4b22b49 100644 --- a/public/js/progress/curriculum-registry.js +++ b/public/js/progress/curriculum-registry.js @@ -75,6 +75,23 @@ const FRENCH_LANGUAGE_CONFIG = { characterNormalization: { 'รฉ': 'e', 'รจ': 'e', 'รช': 'e', 'รซ': 'e', 'ร ': 'a', 'รข': 'a', 'รน': 'u', 'รป': 'u', 'รด': 'o', 'รฎ': 'i', 'รฏ': 'i', 'รง': 'c', 'ล“': 'oe', 'รฆ': 'ae' } }; +const NATURFAG_LANGUAGE_CONFIG = { + code: 'nb', + grammar: { + genderCount: 0, + articles: {}, + genderLabels: {}, + genderColors: {}, + pronouns: [] + }, + dataKeys: { + target: 'naturfag', + native: 'norsk' + }, + specialChars: [], + characterNormalization: {} +}; + export const CURRICULUM_REGISTRY = { 'us-8': { id: 'us-8', @@ -213,6 +230,21 @@ export const CURRICULUM_REGISTRY = { startButtonText: 'Start med spansk', languageConfig: SPANISH_LANGUAGE_CONFIG }, + 'naturfag-vg1': { + id: 'naturfag-vg1', + filePrefix: 'nf', + folderName: 'naturfag-vg1', + chapters: 2, + lessonsPerChapter: 2, + title: 'Naturfag VG1 โ€” Biologi', + description: 'Celler, fotosyntese og energi', + contentPath: '../../content/naturfag', + languageDir: 'naturfag', + paths: { + homeLink: '../index.html' + }, + languageConfig: NATURFAG_LANGUAGE_CONFIG + }, 'fransk1-vg1': { id: 'fransk1-vg1', filePrefix: 'fra1', diff --git a/public/js/progress/migration.js b/public/js/progress/migration.js index 787ef65..5a3ef3d 100644 --- a/public/js/progress/migration.js +++ b/public/js/progress/migration.js @@ -16,7 +16,8 @@ import { clearOldExerciseKeys } from '../exercises/storage-utils.js'; // Import core vocabulary from external API (for migration checks - no translations needed) import { fetchCoreBank } from '../vocabulary/vocab-api-client.js'; import { getTargetLanguageCode, genusToArticle } from '../core/language-utils.js'; -const nounBank = await fetchCoreBank(getTargetLanguageCode(), 'nounbank'); +// nounBank is fetched lazily inside convertKnownWordsToNewFormat() to avoid +// crashing non-language courses (e.g. naturfag) that have no nounbank.json. import { auth, isAuthAvailable } from '../auth/firebase-client.js'; // ================================================================= @@ -175,13 +176,22 @@ function calculateAchievements(isAuthenticated, lessonId, migrationDate) { /** * Konverterer gamle known words til nytt format med artikler for substantiver * @param {string[]} oldKnownWords - Array med kjente ord (uten artikler) - * @returns {string[]} Array med kjente ord (med artikler for substantiver) + * @returns {Promise} Array med kjente ord (med artikler for substantiver) */ -function convertKnownWordsToNewFormat(oldKnownWords) { +async function convertKnownWordsToNewFormat(oldKnownWords) { if (!Array.isArray(oldKnownWords)) { return []; } + // Fetch nounbank lazily โ€” returns {} silently for courses without vocab + let nounBank = {}; + try { + nounBank = await fetchCoreBank(getTargetLanguageCode(), 'nounbank'); + } catch { + // Non-language courses (e.g. naturfag) have no nounbank โ€” skip article conversion + console.log('[Migration] No nounbank available โ€” skipping article conversion'); + } + const convertedWords = []; oldKnownWords.forEach(word => { @@ -213,7 +223,7 @@ function convertKnownWordsToNewFormat(oldKnownWords) { * @param {boolean} isAuthenticated - Om brukeren er logget inn * @returns {boolean} True hvis migrering var vellykket */ -export function migrateProgressData(isAuthenticated) { +export async function migrateProgressData(isAuthenticated) { try { // Hent gammel data const oldKnownWords = safeStorage.get('vocab-trainer-known-words', []); @@ -223,7 +233,7 @@ export function migrateProgressData(isAuthenticated) { console.log('๐Ÿ“– Kjente ord (gammelt format):', oldKnownWords.length); // Konverter kjente ord til nytt format (med artikler for substantiver) - const convertedKnownWords = convertKnownWordsToNewFormat(oldKnownWords); + const convertedKnownWords = await convertKnownWordsToNewFormat(oldKnownWords); console.log('๐Ÿ“– Kjente ord (nytt format):', convertedKnownWords.length); // Opprett ny datastruktur @@ -537,7 +547,7 @@ export async function runMigrationIfNeeded() { // Run progress data migration (runs for both authenticated and anonymous users) if (needsMigration()) { console.log('๐Ÿ“ฆ Gammel progresjonsdata funnet. Starter migrering...'); - const success = migrateProgressData(isAuthenticated); + const success = await migrateProgressData(isAuthenticated); if (success) { showMigrationNotification(isAuthenticated); From 04b930dd67180848e792da5ddb8ded29c8e07367 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Mar 2026 12:39:03 +0000 Subject: [PATCH 2/3] refactor: Remove hardcoded German defaults from engine to support non-language courses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The framework assumed German ('de') and 'tysk1-vg1' as defaults throughout the engine, causing crashes and incorrect behavior for non-language courses like naturfag. Changes across 13 files: - Replace all `|| 'de'` fallbacks with `|| 'nb'` (neutral UI locale) - Replace hardcoded 'tysk1-vg1' defaults in store.js with dynamic _defaultCurriculum() that reads from the curriculum registry - Use config.contentPath/config.languageDir for content path resolution instead of hardcoded language-to-directory mappings - Make getCurriculumConfig() fallback to first registered curriculum instead of assuming tysk1-vg1 - Wrap top-level vocab fetches in try/catch (test.js, gender-trainer, verb-trainer) so non-language courses don't crash on missing banks - Make content-loader.js skip vocabulary bank loading entirely when target language equals native language (non-language courses) - Fix AppShell constructor to use neutral defaults for title and theme ๐Ÿค– Generated with Papertek โ€” Framework for Education Co-Authored-By: Claude https://claude.ai/code/session_017rtsVGshceLbjsQaDx7HyA --- public/js/core/language-utils.js | 2 +- public/js/dialog/ord-battle.js | 2 +- .../js/exercises/embedded-gender-trainer.js | 3 +- .../js/exercises/embedded-verb-trainer-v2.js | 3 +- public/js/layout/shell.js | 9 +- public/js/progress/curriculum-registry.js | 2 +- public/js/progress/curriculum.js | 6 +- public/js/progress/store.js | 13 ++- public/js/progress/ui.js | 10 +- public/js/test.js | 11 +- public/js/utils/content-loader.js | 104 ++++++++---------- public/js/utils/resource-content-loader.js | 36 +++--- public/js/vocab-trainer-multi/flashcards.js | 5 +- 13 files changed, 105 insertions(+), 101 deletions(-) diff --git a/public/js/core/language-utils.js b/public/js/core/language-utils.js index bc40fee..474566d 100644 --- a/public/js/core/language-utils.js +++ b/public/js/core/language-utils.js @@ -195,7 +195,7 @@ export function getNativeKey() { */ export function getTargetLanguageCode() { const config = getLanguageConfig(); - return config.code || 'de'; + return config.code || 'nb'; } /** diff --git a/public/js/dialog/ord-battle.js b/public/js/dialog/ord-battle.js index 52d6e68..a8df1a4 100644 --- a/public/js/dialog/ord-battle.js +++ b/public/js/dialog/ord-battle.js @@ -64,7 +64,7 @@ async function loadVocabularyBanks() { } const config = getCurriculumConfig(curriculumId); - const langCode = config?.languageConfig?.code || 'de'; + const langCode = config?.languageConfig?.code || 'nb'; const nativeCode = getTranslationLangCode() === 'en' ? 'en' : 'nb'; const transPair = `${langCode}-${nativeCode}`; diff --git a/public/js/exercises/embedded-gender-trainer.js b/public/js/exercises/embedded-gender-trainer.js index 4019450..8b7eca7 100644 --- a/public/js/exercises/embedded-gender-trainer.js +++ b/public/js/exercises/embedded-gender-trainer.js @@ -14,7 +14,8 @@ // Import core vocabulary from external API (genus, word - no translations needed for gender trainer) import { fetchCoreBank } from '../vocabulary/vocab-api-client.js'; import { getTargetLanguageCode, genusToArticle } from '../core/language-utils.js'; -const nounBank = await fetchCoreBank(getTargetLanguageCode(), 'nounbank'); +let nounBank = {}; +try { nounBank = await fetchCoreBank(getTargetLanguageCode(), 'nounbank'); } catch { /* No vocab for non-language courses */ } import { saveData, loadData, trackExerciseCompletion } from '../progress/index.js'; diff --git a/public/js/exercises/embedded-verb-trainer-v2.js b/public/js/exercises/embedded-verb-trainer-v2.js index ecf9e78..b8c5287 100644 --- a/public/js/exercises/embedded-verb-trainer-v2.js +++ b/public/js/exercises/embedded-verb-trainer-v2.js @@ -12,7 +12,8 @@ import { fetchCoreBank } from '../vocabulary/vocab-api-client.js'; import { getTargetLanguageCode } from '../core/language-utils.js'; -const verbbank = await fetchCoreBank(getTargetLanguageCode(), 'verbbank'); +let verbbank = {}; +try { verbbank = await fetchCoreBank(getTargetLanguageCode(), 'verbbank'); } catch { /* No vocab for non-language courses */ } // Dynamic verb classification import โ€” loads language-specific file at runtime let verbClassification = {}; diff --git a/public/js/layout/shell.js b/public/js/layout/shell.js index b6cc3cf..655ccc9 100644 --- a/public/js/layout/shell.js +++ b/public/js/layout/shell.js @@ -12,9 +12,10 @@ import { checkMediaStatus, downloadMedia, deleteMedia, getStorageInfo, formatByt export class AppShell { constructor(config) { - this.appTitle = config.appTitle || (config.language === 'es' ? "Hablamos Espaรฑol 1" : (config.language === 'fr' ? "Nous parlons Franรงais 1" : "Wir sprechen Deutsch 1")); - this.language = config.language || "de"; // 'de' or 'es' - this.theme = config.theme || (this.language === 'es' ? 'spanish' : (this.language === 'fr' ? 'french' : 'german')); + const defaultTitles = { 'es': "Hablamos Espaรฑol 1", 'fr': "Nous parlons Franรงais 1", 'de': "Wir sprechen Deutsch 1" }; + this.appTitle = config.appTitle || defaultTitles[config.language] || config.language || 'Papertek'; + this.language = config.language || "nb"; + this.theme = config.theme || { 'es': 'spanish', 'fr': 'french', 'de': 'german' }[this.language] || 'default'; this.uiLanguage = config.uiLanguage || 'no'; // 'no' or 'en' - UI language for translations // Control whether this is a lesson page (minimal header) or index page (full header) @@ -210,7 +211,7 @@ export class AppShell { enforceLanguageConsistency() { const activeId = getActiveCurriculum(); const activeConfig = getCurriculumConfig(activeId); - const activeLangCode = activeConfig?.languageConfig?.code || 'de'; // Default to 'de' if missing + const activeLangCode = activeConfig?.languageConfig?.code || 'nb'; // If the current saved curriculum does not match the Portal's language... if (activeLangCode !== this.language) { diff --git a/public/js/progress/curriculum-registry.js b/public/js/progress/curriculum-registry.js index 4b22b49..31d407c 100644 --- a/public/js/progress/curriculum-registry.js +++ b/public/js/progress/curriculum-registry.js @@ -267,5 +267,5 @@ export const CURRICULUM_REGISTRY = { * Accessor for easy registry lookup */ export function getCurriculumConfig(id) { - return CURRICULUM_REGISTRY[id] || CURRICULUM_REGISTRY['tysk1-vg1']; + return CURRICULUM_REGISTRY[id] || Object.values(CURRICULUM_REGISTRY)[0] || {}; } diff --git a/public/js/progress/curriculum.js b/public/js/progress/curriculum.js index 806dfa1..7aadc93 100644 --- a/public/js/progress/curriculum.js +++ b/public/js/progress/curriculum.js @@ -33,7 +33,7 @@ export function getCurrentCurriculum() { */ export function getCurriculumConfig(curriculumId = null) { const id = curriculumId || getActiveCurriculum(); - return CURRICULUM_CONFIG[id] || CURRICULUM_CONFIG['tysk1-vg1']; + return CURRICULUM_CONFIG[id] || Object.values(CURRICULUM_CONFIG)[0] || {}; } /** @@ -57,8 +57,8 @@ export function updateLessonLinks() { const maxChapters = config.chapters; // Determine language folder from config - const langCode = config.languageConfig?.code || 'de'; - const languageDir = langCode === 'fr' ? 'french' : (langCode === 'es' ? 'spanish' : 'german'); + const langCode = config.languageConfig?.code || 'nb'; + const languageDir = config.languageDir || { 'de': 'german', 'es': 'spanish', 'fr': 'french' }[langCode] || langCode; // Oppdater alle leksjonslenkene const lessonLinks = document.querySelectorAll('.leksjon-link[data-leksjon-id]'); diff --git a/public/js/progress/store.js b/public/js/progress/store.js index a1ed6b3..0894ea3 100644 --- a/public/js/progress/store.js +++ b/public/js/progress/store.js @@ -10,6 +10,9 @@ import { safeStorage } from '../error-handler.js'; import { CURRICULUM_CONFIG } from './config.js'; +/** Get the first registered curriculum ID as default (avoids hardcoding 'tysk1-vg1'). */ +const _defaultCurriculum = () => Object.keys(CURRICULUM_CONFIG)[0] || 'default'; + /** * Lagrer data til localStorage med feilhรฅndtering * @param {string} key - Nรธkkel @@ -39,13 +42,13 @@ export function getFullProgressData() { return { studentProfile: { name: "Student", - activeCurriculum: 'tysk1-vg1', + activeCurriculum: _defaultCurriculum(), currentGrade: 'vg1', startYear: new Date().getFullYear(), migrated: false }, progressByCurriculum: { - 'tysk1-vg1': {} + [_defaultCurriculum()]: {} }, knownWords: [], vocabTestHistory: [] @@ -54,12 +57,12 @@ export function getFullProgressData() { // Ensure data has required structure (for backwards compatibility) if (!data.progressByCurriculum) { - data.progressByCurriculum = { 'tysk1-vg1': {} }; + data.progressByCurriculum = { [_defaultCurriculum()]: {} }; } if (!data.studentProfile) { data.studentProfile = { name: "Student", - activeCurriculum: 'tysk1-vg1', + activeCurriculum: _defaultCurriculum(), currentGrade: 'vg1', startYear: new Date().getFullYear(), migrated: false @@ -89,7 +92,7 @@ export function saveFullProgressData(data) { */ export function getActiveCurriculum() { const data = getFullProgressData(); - return data.studentProfile?.activeCurriculum || 'tysk1-vg1'; + return data.studentProfile?.activeCurriculum || _defaultCurriculum(); } /** diff --git a/public/js/progress/ui.js b/public/js/progress/ui.js index bb6ced6..55d9a48 100644 --- a/public/js/progress/ui.js +++ b/public/js/progress/ui.js @@ -106,8 +106,9 @@ export function setupContinueButton() { const isSubdirectory = window.location.pathname.includes('/tysk/') || window.location.pathname.includes('/spansk/') || window.location.pathname.includes('/fransk/'); const langConfig = config.languageConfig; - const langCode = langConfig?.code || 'de'; - const langFolder = config.languageDir || (langCode === 'es' ? 'spanish' : (langCode === 'fr' ? 'french' : 'german')); + const langCode = langConfig?.code || 'nb'; + const langDirs = { 'de': 'german', 'es': 'spanish', 'fr': 'french' }; + const langFolder = config.languageDir || langDirs[langCode] || langCode; const pathPrefix = isSubdirectory ? '../' : ''; // Build base URL @@ -145,8 +146,9 @@ export function renderLessonList(lessonsData, chapterTitles) { const isSubdirectory = window.location.pathname.includes('/tysk/') || window.location.pathname.includes('/spansk/') || window.location.pathname.includes('/fransk/'); const langConfig = config.languageConfig; - const langCode = langConfig?.code || 'de'; - const langFolder = config.languageDir || (langCode === 'es' ? 'spanish' : (langCode === 'fr' ? 'french' : 'german')); + const langCode = langConfig?.code || 'nb'; + const langDirs = { 'de': 'german', 'es': 'spanish', 'fr': 'french' }; + const langFolder = config.languageDir || langDirs[langCode] || langCode; const pathPrefix = isSubdirectory ? '../' : ''; // Use default chapters if chapterTitles is missing (fallback) diff --git a/public/js/test.js b/public/js/test.js index de93d60..56cb436 100644 --- a/public/js/test.js +++ b/public/js/test.js @@ -16,8 +16,15 @@ try { console.warn(`No question bank found for ${langDir}. Tests will be empty.`); } -const verbbank = await fetchCoreBank(langCode, 'verbbank'); -const nounBank = await fetchCoreBank(langCode, 'nounbank'); +let verbbank = {}, nounBank = {}; +try { + [verbbank, nounBank] = await Promise.all([ + fetchCoreBank(langCode, 'verbbank'), + fetchCoreBank(langCode, 'nounbank') + ]); +} catch { + console.warn(`No vocabulary banks available for ${langCode}. Vocab-based questions will be skipped.`); +} /** * ============================================================================= diff --git a/public/js/utils/content-loader.js b/public/js/utils/content-loader.js index 31c42e9..845521d 100644 --- a/public/js/utils/content-loader.js +++ b/public/js/utils/content-loader.js @@ -101,15 +101,10 @@ export async function loadContent(externalConfig) { const config = externalConfig || getCurriculumConfig(curriculumId); - // Determine content path: - // - International pages load from german-international/ - // - Spanish pages load from spanish/ - // - Standard pages load from german/ + // Determine content path from curriculum config let contentPath; if (isInternational) { contentPath = '../../content/german-international'; - } else if (config.languageConfig?.code === 'es') { - contentPath = '../../content/spanish'; } else { contentPath = config.contentPath || '../../content/german'; } @@ -117,77 +112,70 @@ export async function loadContent(externalConfig) { // Determine content file suffix based on page type // International pages use 'international-a1' suffix, others use curriculum ID const contentSuffix = isInternational ? 'international-a1' : config.id; - const fallbackSuffix = isInternational ? 'international-a1' : 'tysk1-vg1'; + const fallbackSuffix = isInternational ? 'international-a1' : config.id; let lessonsModule; try { lessonsModule = await import(`${contentPath}/lessons-data-${contentSuffix}.js`); } catch (e) { - console.warn(`Could not load lessons-data-${contentSuffix}.js, falling back to ${fallbackSuffix}`); - lessonsModule = await import(`${contentPath}/lessons-data-${fallbackSuffix}.js`); + console.warn(`Could not load lessons-data-${contentSuffix}.js`); + lessonsModule = { lessonsData: {}, chapterTitles: {} }; } - // Dynamic load with fallback for shared content types + // Dynamic load with fallback โ€” modules return empty data if not found let grammarModule, pronunciationModule, cultureModule; + const emptyModule = { grammarData: {}, pronunciationData: {}, cultureData: {} }; try { grammarModule = await import(`${contentPath}/grammar-data-${contentSuffix}.js`); } - catch (e) { - console.warn(`Could not load grammar-data-${contentSuffix}.js, falling back to ${fallbackSuffix}`); - grammarModule = await import(`${contentPath}/grammar-data-${fallbackSuffix}.js`); - } + catch (e) { grammarModule = emptyModule; } try { pronunciationModule = await import(`${contentPath}/pronunciation-data-${contentSuffix}.js`); } - catch (e) { - console.warn(`Could not load pronunciation-data-${contentSuffix}.js, falling back to ${fallbackSuffix}`); - pronunciationModule = await import(`${contentPath}/pronunciation-data-${fallbackSuffix}.js`); - } + catch (e) { pronunciationModule = emptyModule; } try { cultureModule = await import(`${contentPath}/culture-data-${contentSuffix}.js`); } - catch (e) { - console.warn(`Could not load culture-data-${contentSuffix}.js, falling back to ${fallbackSuffix}`); - cultureModule = await import(`${contentPath}/culture-data-${fallbackSuffix}.js`); - } + catch (e) { cultureModule = emptyModule; } - const vocabDataModule = await import(`${contentPath}/vocabulary-data.js`); + let vocabDataModule; + try { vocabDataModule = await import(`${contentPath}/vocabulary-data.js`); } + catch (e) { vocabDataModule = { vocabularyData: {} }; } // Load characters data - const charactersModule = await import(`${contentPath}/characters-data.js`); + let charactersModule; + try { charactersModule = await import(`${contentPath}/characters-data.js`); } + catch (e) { charactersModule = {}; } - // Load vocabulary from external API (core + translations) - // Uses ISO 639-1 codes: de (German), es (Spanish), fr (French), nb (Norwegian), en (English) - const langCode = config.languageConfig?.code || 'de'; + // Load vocabulary from external API (only for foreign-language courses) + const langCode = config.languageConfig?.code || 'nb'; const nativeCode = nativeLang || 'nb'; - const transPair = `${langCode}-${nativeCode}`; - - const bankNames = ['nounbank', 'verbbank', 'generalbank', 'adjectivebank', 'numbersbank', 'phrasesbank', 'pronounsbank', 'articlesbank']; - - // Load core and translation banks in parallel from external API - const [coreBanks, transBanks] = await Promise.all([ - Promise.all(bankNames.map(b => fetchCoreBank(langCode, b))), - Promise.all(bankNames.map(b => fetchTranslationBank(transPair, b))) - ]); - - // Merge core with translations - const [nounCore, verbCore, generalCore, adjCore, numbersCore, phrasesCore, pronounsCore, articlesCore] = coreBanks; - const [nounTrans, verbTrans, generalTrans, adjTrans, numbersTrans, phrasesTrans, pronounsTrans, articlesTrans] = transBanks; - - const nounBank = mergeVocabulary(nounCore, nounTrans, nativeLang); - const verbbank = mergeVocabulary(verbCore, verbTrans, nativeLang); - const generalBank = mergeVocabulary(generalCore, generalTrans, nativeLang); - const adjectiveBank = mergeVocabulary(adjCore, adjTrans, nativeLang); - const numbersBank = mergeVocabulary(numbersCore, numbersTrans, nativeLang); - const phrasesBank = mergeVocabulary(phrasesCore, phrasesTrans, nativeLang); - const pronounsBank = mergeVocabulary(pronounsCore, pronounsTrans, nativeLang); - const articlesBank = mergeVocabulary(articlesCore, articlesTrans, nativeLang); - - // Combine into wordBank (general + numbers + phrases + pronouns + articles) - const wordBank = { - ...generalBank, - ...numbersBank, - ...phrasesBank, - ...pronounsBank, - ...articlesBank - }; + const hasVocab = langCode !== nativeCode; // Skip vocab for same-language courses (e.g. naturfag) + + let nounBank = {}, verbbank = {}, wordBank = {}, adjectiveBank = {}; + + if (hasVocab) { + const transPair = `${langCode}-${nativeCode}`; + const bankNames = ['nounbank', 'verbbank', 'generalbank', 'adjectivebank', 'numbersbank', 'phrasesbank', 'pronounsbank', 'articlesbank']; + + // Load core and translation banks in parallel from external API + const [coreBanks, transBanks] = await Promise.all([ + Promise.all(bankNames.map(b => fetchCoreBank(langCode, b))), + Promise.all(bankNames.map(b => fetchTranslationBank(transPair, b))) + ]); + + // Merge core with translations + const [nounCore, verbCore, generalCore, adjCore, numbersCore, phrasesCore, pronounsCore, articlesCore] = coreBanks; + const [nounTrans, verbTrans, generalTrans, adjTrans, numbersTrans, phrasesTrans, pronounsTrans, articlesTrans] = transBanks; + + nounBank = mergeVocabulary(nounCore, nounTrans, nativeLang); + verbbank = mergeVocabulary(verbCore, verbTrans, nativeLang); + const generalBank = mergeVocabulary(generalCore, generalTrans, nativeLang); + adjectiveBank = mergeVocabulary(adjCore, adjTrans, nativeLang); + const numbersBank = mergeVocabulary(numbersCore, numbersTrans, nativeLang); + const phrasesBank = mergeVocabulary(phrasesCore, phrasesTrans, nativeLang); + const pronounsBank = mergeVocabulary(pronounsCore, pronounsTrans, nativeLang); + const articlesBank = mergeVocabulary(articlesCore, articlesTrans, nativeLang); + + wordBank = { ...generalBank, ...numbersBank, ...phrasesBank, ...pronounsBank, ...articlesBank }; + } const content = { lessonsData: lessonsModule.lessonsData, diff --git a/public/js/utils/resource-content-loader.js b/public/js/utils/resource-content-loader.js index 53f9b5f..5458c49 100644 --- a/public/js/utils/resource-content-loader.js +++ b/public/js/utils/resource-content-loader.js @@ -22,16 +22,16 @@ const resourceCache = {}; * @returns {string} Content folder path (relative to this module's location) */ function getContentPath(config) { - const langCode = config.languageConfig?.code || 'de'; - - // Path is relative from public/js/utils/ to public/content/ - if (langCode === 'fr') { - return '../../content/french'; - } - if (langCode === 'es') { - return '../../content/spanish'; + // Use explicit contentPath from curriculum config when available + if (config.contentPath) { + return config.contentPath; } - return '../../content/german'; + + // Fallback: derive from language code for known languages + const langCode = config.languageConfig?.code || 'nb'; + const langDirs = { 'de': 'german', 'es': 'spanish', 'fr': 'french' }; + const dir = langDirs[langCode] || langCode; + return `../../content/${dir}`; } /** @@ -42,7 +42,7 @@ function getContentPath(config) { function getCurriculaForLanguage(langCode) { return Object.keys(CURRICULUM_REGISTRY).filter(id => { const config = CURRICULUM_REGISTRY[id]; - return (config.languageConfig?.code || 'de') === langCode; + return (config.languageConfig?.code || 'nb') === langCode; }); } @@ -54,7 +54,7 @@ export async function loadGrammarWordlist() { const curriculumId = getActiveCurriculum(); const config = getCurriculumConfig(curriculumId); const contentPath = getContentPath(config); - const langCode = config.languageConfig?.code || 'de'; + const langCode = config.languageConfig?.code || 'nb'; const cacheKey = `grammar-wordlist-${langCode}`; if (resourceCache[cacheKey]) { @@ -93,7 +93,7 @@ export async function loadAllGrammarData() { console.warn(`Could not load grammar-data-${curriculumId}.js, trying fallback:`, error); // Try fallback for German - if ((config.languageConfig?.code || 'de') === 'de') { + if ((config.languageConfig?.code || 'nb') === 'de') { try { const fallback = await import(`${contentPath}/grammar-data-tysk1-vg1.js`); resourceCache[cacheKey] = fallback.grammarData; @@ -128,7 +128,7 @@ export async function loadAllCultureData() { console.warn(`Could not load culture-data-${curriculumId}.js, trying fallback:`, error); // Try fallback for German - if ((config.languageConfig?.code || 'de') === 'de') { + if ((config.languageConfig?.code || 'nb') === 'de') { try { const fallback = await import(`${contentPath}/culture-data-tysk1-vg1.js`); resourceCache[cacheKey] = fallback.cultureData; @@ -163,7 +163,7 @@ export async function loadAllPronunciationData() { console.warn(`Could not load pronunciation-data-${curriculumId}.js, trying fallback:`, error); // Try fallback for German - if ((config.languageConfig?.code || 'de') === 'de') { + if ((config.languageConfig?.code || 'nb') === 'de') { try { const fallback = await import(`${contentPath}/pronunciation-data-tysk1-vg1.js`); resourceCache[cacheKey] = fallback.pronunciationData; @@ -183,7 +183,7 @@ export async function loadAllPronunciationData() { export function getCurrentLanguageName() { const curriculumId = getActiveCurriculum(); const config = getCurriculumConfig(curriculumId); - const langCode = config.languageConfig?.code || 'de'; + const langCode = config.languageConfig?.code || 'nb'; const names = { 'de': 'tysk', @@ -203,7 +203,7 @@ export function getCurrentLanguageName() { export function getHomePageUrl() { const curriculumId = getActiveCurriculum(); const config = getCurriculumConfig(curriculumId); - const langCode = config.languageConfig?.code || 'de'; + const langCode = config.languageConfig?.code || 'nb'; // Resource pages are in public/, home pages are in public/tysk/ or public/spansk/ const homeUrls = { @@ -213,7 +213,7 @@ export function getHomePageUrl() { 'en': 'engelsk/index.html' }; - return homeUrls[langCode] || 'tysk/index.html'; + return homeUrls[langCode] || config.paths?.homeLink || 'index.html'; } /** @@ -224,7 +224,7 @@ export function getHomePageUrl() { export function getResourcePageTitles(pageType) { const curriculumId = getActiveCurriculum(); const config = getCurriculumConfig(curriculumId); - const langCode = config.languageConfig?.code || 'de'; + const langCode = config.languageConfig?.code || 'nb'; const titles = { 'de': { diff --git a/public/js/vocab-trainer-multi/flashcards.js b/public/js/vocab-trainer-multi/flashcards.js index 5ffea76..629a5cd 100644 --- a/public/js/vocab-trainer-multi/flashcards.js +++ b/public/js/vocab-trainer-multi/flashcards.js @@ -18,9 +18,10 @@ export function renderFlashcards(container, context, mode = 'normal') { const { vocabulary, knownWords, saveKnownWords, adapter, config, isLessonMode } = context; // Determine language code for audio paths (ISO 639-1: de, es, fr) - const langCode = config?.languageConfig?.code || 'de'; + const langCode = config?.languageConfig?.code || 'nb'; // Legacy language name for content/ symlink paths - const language = langCode === 'es' ? 'spanish' : (langCode === 'fr' ? 'french' : 'german'); + const langDirs = { 'de': 'german', 'es': 'spanish', 'fr': 'french' }; + const language = config?.languageDir || langDirs[langCode] || langCode; // 1. Filter words based on mode let allAvailableWords; From bea522832036f3931c3617d60a3fc4f132eaeb8e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Mar 2026 13:12:43 +0000 Subject: [PATCH 3/3] feat: Add frontpage template system with 3 selectable layouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The framework now supports multiple frontpage layouts that teachers can choose when creating a new educational app, making it suitable for both language courses and subject-based courses like science/math/history. Templates: - classic โ€” Language course style (curriculum selector, vocab trainer link) - subject โ€” Subject course style (chapter card grid, hero section) - minimal โ€” Simple compact list layout (clean rows) Changes: - templates/frontpage/{classic,subject,minimal}.html โ€” Three HTML templates - ui.js โ€” Added renderSubjectLayout() and renderMinimalLayout() renderers that dispatch based on data-frontpage-template attribute on - shell.js โ€” Skip shell header/tools rendering for subject/minimal templates (they handle their own layout) - papertek.css โ€” Added 4 named color themes (science/green, humanities/indigo, math/pink, social/orange) via data-theme attribute - create-edu-app.js โ€” Added frontpage template and color theme prompts, generates index.html from selected template, auto-suggests 'subject' for same-language courses - papertek.config.template.js โ€” Added frontpageTemplate option ๐Ÿค– Generated with Papertek โ€” Framework for Education Co-Authored-By: Claude --- cli/create-edu-app.js | 51 ++++++- public/js/layout/shell.js | 18 ++- public/js/progress/ui.js | 107 +++++++++++++++ public/papertek.css | 56 ++++++++ templates/frontpage/classic.html | 28 ++++ templates/frontpage/minimal.html | 112 ++++++++++++++++ templates/frontpage/subject.html | 183 ++++++++++++++++++++++++++ templates/papertek.config.template.js | 7 + 8 files changed, 548 insertions(+), 14 deletions(-) create mode 100644 templates/frontpage/classic.html create mode 100644 templates/frontpage/minimal.html create mode 100644 templates/frontpage/subject.html diff --git a/cli/create-edu-app.js b/cli/create-edu-app.js index b6d1261..dbe3eba 100644 --- a/cli/create-edu-app.js +++ b/cli/create-edu-app.js @@ -158,7 +158,23 @@ async function main() { // 6. CEFR level const cefrLevel = args.cefr || await prompt.ask('CEFR level', 'A1'); - // 7. Output directory + // 7. Frontpage template + console.log('\n Available frontpage templates:'); + console.log(' classic - Language course style (curriculum selector, vocab trainer)'); + console.log(' subject - Subject course style (chapter card grid, hero section)'); + console.log(' minimal - Simple compact list layout'); + const isLanguageCourse = langCode !== uiLang; + const defaultTemplate = isLanguageCourse ? 'classic' : 'subject'; + const frontpageTemplate = args.template || await prompt.ask('Frontpage template', defaultTemplate); + + // 8. Color theme (for subject/minimal templates) + let colorTheme = ''; + if (frontpageTemplate !== 'classic') { + console.log('\n Available color themes: default (amber), science (green), humanities (indigo), math (pink), social (orange)'); + colorTheme = args.theme || await prompt.ask('Color theme', 'default'); + } + + // 9. Output directory const slug = slugify(courseName); const defaultDir = `./${slug}`; const outputDir = args.output || await prompt.ask('Output directory', defaultDir); @@ -210,7 +226,7 @@ async function main() { // โ”€โ”€ Generate papertek.config.js โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const configContent = generateConfig({ courseName, langCode, langPreset, uiLang, - chapters, lessonsPerCh, cefrLevel, slug, curriculumId, + chapters, lessonsPerCh, cefrLevel, slug, curriculumId, frontpageTemplate, }); writeFileSync(join(projectRoot, 'papertek.config.js'), configContent); @@ -228,6 +244,13 @@ async function main() { const packageJson = generatePackageJson({ slug, courseName }); writeFileSync(join(projectRoot, 'package.json'), JSON.stringify(packageJson, null, 2) + '\n'); + // โ”€โ”€ Copy frontpage templates โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const frontpageSrc = join(FRAMEWORK_ROOT, 'templates/frontpage'); + if (existsSync(frontpageSrc)) { + copyDirSync(frontpageSrc, join(projectRoot, 'templates/frontpage')); + console.log(' โœ… Copied frontpage templates'); + } + // โ”€โ”€ Copy schemas โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const schemasDir = join(FRAMEWORK_ROOT, 'schemas'); if (existsSync(schemasDir)) { @@ -269,9 +292,21 @@ async function main() { // โ”€โ”€ Generate .gitignore โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ writeFileSync(join(projectRoot, '.gitignore'), generateGitignore()); - // โ”€โ”€ Generate index.html โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - writeFileSync(join(projectRoot, 'public/index.html'), generateIndexHtml({ courseName, chapters, lessonsPerCh })); - console.log(' โœ… Generated public/index.html'); + // โ”€โ”€ Generate index.html from frontpage template โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const templateFile = join(FRAMEWORK_ROOT, 'templates/frontpage', `${frontpageTemplate}.html`); + let indexHtml; + if (existsSync(templateFile)) { + indexHtml = readFileSync(templateFile, 'utf-8') + .replace(/\{\{COURSE_NAME\}\}/g, courseName) + .replace(/\{\{COURSE_DESCRIPTION\}\}/g, `${chapters} kapitler`) + .replace(/\{\{UI_LANG\}\}/g, uiLang === 'nb' ? 'no' : uiLang) + .replace(/\{\{CURRICULUM_ID\}\}/g, curriculumId) + .replace(/\{\{COLOR_THEME\}\}/g, colorTheme && colorTheme !== 'default' ? colorTheme : ''); + } else { + indexHtml = generateIndexHtml({ courseName, chapters, lessonsPerCh }); + } + writeFileSync(join(projectRoot, 'public/index.html'), indexHtml); + console.log(` โœ… Generated public/index.html (template: ${frontpageTemplate})`); // โ”€โ”€ Init git โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ try { @@ -301,7 +336,7 @@ async function main() { // โ”€โ”€ Generators โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -function generateConfig({ courseName, langCode, langPreset, uiLang, chapters, lessonsPerCh, cefrLevel, slug, curriculumId }) { +function generateConfig({ courseName, langCode, langPreset, uiLang, chapters, lessonsPerCh, cefrLevel, slug, curriculumId, frontpageTemplate }) { const articlesStr = Object.entries(langPreset.articles || {}) .map(([k, v]) => `${k}: '${v}'`).join(', '); const genderLabelsStr = Object.entries(langPreset.genderLabels || {}) @@ -371,12 +406,14 @@ export default { fetchAudio: true, }, + frontpageTemplate: '${frontpageTemplate || 'classic'}', + features: { offlineMode: true, cloudSync: false, teacherDashboard: false, classroomGames: false, - vocabTrainer: true, + vocabTrainer: ${langCode !== uiLang}, spacedRepetition: true, wordTooltips: true, specialCharKeyboard: ${langPreset.specialCharacters.length > 0}, diff --git a/public/js/layout/shell.js b/public/js/layout/shell.js index 655ccc9..c4037c5 100644 --- a/public/js/layout/shell.js +++ b/public/js/layout/shell.js @@ -48,18 +48,22 @@ export class AppShell { this.renderStructure(); // Only render full header and install button on index/home pages, not lesson pages + const frontpageTemplate = document.body.dataset.frontpageTemplate; if (!this.isLessonPage) { - this.renderHeader(); - this.renderInstallButton(); - this.renderToolsSection(); - this.renderFooterButtons(); + // Subject/minimal templates handle their own layout โ€” skip shell header/tools + if (!frontpageTemplate || frontpageTemplate === 'classic') { + this.renderHeader(); + this.renderInstallButton(); + this.renderToolsSection(); + this.renderFooterButtons(); + + // Initialize Curriculum Selector logic + this.setupCurriculumSelector(); + } // Setup functionality setupDebugTools(); - // Initialize Curriculum Selector logic - this.setupCurriculumSelector(); - // Trigger curriculum content caching after a short delay // (let the page render first, then cache in background) setTimeout(() => { diff --git a/public/js/progress/ui.js b/public/js/progress/ui.js index 55d9a48..4bbcd5c 100644 --- a/public/js/progress/ui.js +++ b/public/js/progress/ui.js @@ -135,6 +135,15 @@ export function renderLessonList(lessonsData, chapterTitles) { const container = document.getElementById('lessons-container'); if (!container) return; + // Dispatch to template-specific renderer if applicable + const template = document.body.dataset.frontpageTemplate; + if (template === 'subject') { + return renderSubjectLayout(container, lessonsData, chapterTitles); + } + if (template === 'minimal') { + return renderMinimalLayout(container, lessonsData, chapterTitles); + } + container.innerHTML = ''; const config = getCurriculumConfig(getActiveCurriculum()); @@ -429,6 +438,104 @@ function renderTestProgress(completedTests, requiredTests) { return html; } +/** + * Subject template renderer โ€” chapter grid cards + */ +function renderSubjectLayout(container, lessonsData, chapterTitles) { + container.innerHTML = ''; + + const config = getCurriculumConfig(getActiveCurriculum()); + const filePrefix = config.filePrefix; + const folderName = config.folderName; + const langCode = config.languageConfig?.code || 'nb'; + const langDirs = { 'de': 'german', 'es': 'spanish', 'fr': 'french' }; + const langFolder = config.languageDir || langDirs[langCode] || langCode; + const progress = getProgressData(); + + const chapters = chapterTitles ? Object.keys(chapterTitles).map(Number).sort((a, b) => a - b) : []; + + chapters.forEach(chapterId => { + const chapterTitle = chapterTitles[chapterId]; + const chapterLessons = Object.keys(lessonsData) + .filter(id => id.startsWith(`${chapterId}-`)) + .sort(); + + if (chapterLessons.length === 0) return; + + // Calculate progress for this chapter + let completedLessons = 0; + chapterLessons.forEach(id => { + const lp = progress[id]; + if (lp?.achievements?.leksjon) completedLessons++; + }); + const progressPct = chapterLessons.length > 0 ? Math.round((completedLessons / chapterLessons.length) * 100) : 0; + + // First lesson URL for the chapter card link + const firstLessonId = chapterLessons[0]; + const lessonUrl = `content/${langFolder}/lessons/${folderName}/${filePrefix}-${firstLessonId}.html`; + + const card = document.createElement('a'); + card.href = lessonUrl; + card.className = 'chapter-card'; + card.dataset.chapterId = String(chapterId); + card.innerHTML = ` +
Kapittel ${chapterId}
+

${chapterTitle}

+

${lessonsData[firstLessonId]?.learningGoals?.[0] || ''}

+
${chapterLessons.length} leksjoner${completedLessons > 0 ? ` · ${completedLessons} fullfort` : ''}
+
+ `; + container.appendChild(card); + }); +} + +/** + * Minimal template renderer โ€” compact row list + */ +function renderMinimalLayout(container, lessonsData, chapterTitles) { + container.innerHTML = ''; + + const config = getCurriculumConfig(getActiveCurriculum()); + const filePrefix = config.filePrefix; + const folderName = config.folderName; + const langCode = config.languageConfig?.code || 'nb'; + const langDirs = { 'de': 'german', 'es': 'spanish', 'fr': 'french' }; + const langFolder = config.languageDir || langDirs[langCode] || langCode; + + const chapters = chapterTitles ? Object.keys(chapterTitles).map(Number).sort((a, b) => a - b) : []; + + chapters.forEach(chapterId => { + const chapterTitle = chapterTitles[chapterId]; + const chapterLessons = Object.keys(lessonsData) + .filter(id => id.startsWith(`${chapterId}-`)) + .sort(); + + if (chapterLessons.length === 0) return; + + const heading = document.createElement('div'); + heading.className = 'chapter-heading'; + heading.textContent = `Kapittel ${chapterId}: ${chapterTitle}`; + container.appendChild(heading); + + chapterLessons.forEach(lessonId => { + const lesson = lessonsData[lessonId]; + const displayId = lessonId.replace('-', '.'); + const lessonUrl = `content/${langFolder}/lessons/${folderName}/${filePrefix}-${lessonId}.html`; + + const row = document.createElement('a'); + row.href = lessonUrl; + row.className = 'lesson-row leksjon-link'; + row.dataset.leksjonId = lessonId; + row.innerHTML = ` + ${displayId} + ${lesson.targetTitle || lesson.dialog?.title || 'Uten tittel'} + + `; + container.appendChild(row); + }); + }); +} + // Set up event listeners for achievements and curriculum events if (typeof document !== 'undefined') { // Listen for progress updates from achievements.js diff --git a/public/papertek.css b/public/papertek.css index ad21c35..341ded8 100644 --- a/public/papertek.css +++ b/public/papertek.css @@ -221,6 +221,62 @@ input, button, textarea, select { font: inherit; } a { color: var(--color-info-600); text-decoration: none; } a:hover { text-decoration: underline; } + + /* โ”€โ”€ Named Color Themes โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ + /* Apply via data-theme="science" on or */ + + [data-theme="science"] { + --color-primary-50: #ecfdf5; + --color-primary-100: #d1fae5; + --color-primary-200: #a7f3d0; + --color-primary-300: #6ee7b7; + --color-primary-400: #34d399; + --color-primary-500: #10b981; + --color-primary-600: #059669; + --color-primary-700: #047857; + --color-primary-800: #065f46; + --color-primary-900: #064e3b; + } + + [data-theme="humanities"] { + --color-primary-50: #eef2ff; + --color-primary-100: #e0e7ff; + --color-primary-200: #c7d2fe; + --color-primary-300: #a5b4fc; + --color-primary-400: #818cf8; + --color-primary-500: #6366f1; + --color-primary-600: #4f46e5; + --color-primary-700: #4338ca; + --color-primary-800: #3730a3; + --color-primary-900: #312e81; + } + + [data-theme="math"] { + --color-primary-50: #fdf2f8; + --color-primary-100: #fce7f3; + --color-primary-200: #fbcfe8; + --color-primary-300: #f9a8d4; + --color-primary-400: #f472b6; + --color-primary-500: #ec4899; + --color-primary-600: #db2777; + --color-primary-700: #be185d; + --color-primary-800: #9d174d; + --color-primary-900: #831843; + } + + [data-theme="social"] { + --color-primary-50: #fff7ed; + --color-primary-100: #ffedd5; + --color-primary-200: #fed7aa; + --color-primary-300: #fdba74; + --color-primary-400: #fb923c; + --color-primary-500: #f97316; + --color-primary-600: #ea580c; + --color-primary-700: #c2410c; + --color-primary-800: #9a3412; + --color-primary-900: #7c2d12; + } + } diff --git a/templates/frontpage/classic.html b/templates/frontpage/classic.html new file mode 100644 index 0000000..f49cbff --- /dev/null +++ b/templates/frontpage/classic.html @@ -0,0 +1,28 @@ + + + + + + + {{COURSE_NAME}} + + + + + + + + + +
+ +
+
+ + + + diff --git a/templates/frontpage/minimal.html b/templates/frontpage/minimal.html new file mode 100644 index 0000000..eb07895 --- /dev/null +++ b/templates/frontpage/minimal.html @@ -0,0 +1,112 @@ + + + + + + + {{COURSE_NAME}} + + + + + + + + + + +
+ + +
+

{{COURSE_NAME}}

+

{{COURSE_DESCRIPTION}}

+
+ + + + + +
+
+
+ + +
+ +
+ +
+ + + + diff --git a/templates/frontpage/subject.html b/templates/frontpage/subject.html new file mode 100644 index 0000000..9befafc --- /dev/null +++ b/templates/frontpage/subject.html @@ -0,0 +1,183 @@ + + + + + + + {{COURSE_NAME}} + + + + + + + + + + +
+ + +
+

{{COURSE_NAME}}

+

{{COURSE_DESCRIPTION}}

+
+ + + + + +
+
+
+ + +
+

+ Verktoy og ressurser +

+
+ +
+
+ + +
+ +
+ +
+ + + + diff --git a/templates/papertek.config.template.js b/templates/papertek.config.template.js index 5e41978..7a2e68c 100644 --- a/templates/papertek.config.template.js +++ b/templates/papertek.config.template.js @@ -71,6 +71,13 @@ export default { }, }, + // โ”€โ”€โ”€ Frontpage Layout โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Available templates: 'classic' | 'subject' | 'minimal' + // classic โ€” Language course style (curriculum selector, vocab trainer link) + // subject โ€” Subject course style (chapter card grid, hero section) + // minimal โ€” Simple list layout (compact rows, clean header) + frontpageTemplate: '{{FRONTPAGE_TEMPLATE}}', + // โ”€โ”€โ”€ Feature Flags โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ features: { offlineMode: true,