Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 44 additions & 7 deletions cli/create-edu-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import { createInterface } from 'readline';
import { resolve, dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { mkdirSync, writeFileSync, copyFileSync, existsSync, readdirSync, statSync, readFileSync } from 'fs';

Check warning on line 17 in cli/create-edu-app.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Replace `·mkdirSync,·writeFileSync,·copyFileSync,·existsSync,·readdirSync,·statSync,·readFileSync·` with `⏎··mkdirSync,⏎··writeFileSync,⏎··copyFileSync,⏎··existsSync,⏎··readdirSync,⏎··statSync,⏎··readFileSync,⏎`
import { execSync } from 'child_process';

const __filename = fileURLToPath(import.meta.url);
Expand All @@ -24,49 +24,49 @@
// ── Language presets ──────────────────────────────────────────────
const LANGUAGE_PRESETS = {
de: {
code: 'de', name: 'German', nativeName: 'Deutsch',

Check warning on line 27 in cli/create-edu-app.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Replace `·name:·'German',` with `⏎····name:·'German',⏎···`
genderSystem: 'three',
articles: { m: 'der', f: 'die', n: 'das', pl: 'die' },
genderLabels: { m: 'Maskulin', f: 'Feminin', n: 'Neutrum' },
genderColors: { m: '#3B82F6', f: '#EC4899', n: '#10B981' },
specialCharacters: ['ä', 'ö', 'ü', 'ß'],
characterNormalization: { 'ö': 'oe', 'ä': 'ae', 'ü': 'ue', 'ß': 'ss' },

Check warning on line 33 in cli/create-edu-app.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Replace `'ö':·'oe',·'ä':·'ae',·'ü':·'ue',·'ß'` with `ö:·'oe',·ä:·'ae',·ü:·'ue',·ß`
pronouns: ['ich', 'du', 'er/sie/es', 'wir', 'ihr', 'sie/Sie'],
ttsVoice: 'de-DE',
languageDir: 'german',
},
fr: {
code: 'fr', name: 'French', nativeName: 'Français',

Check warning on line 39 in cli/create-edu-app.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Replace `·name:·'French',` with `⏎····name:·'French',⏎···`
genderSystem: 'two',
articles: { m: 'le', f: 'la', pl: 'les' },
genderLabels: { m: 'Masculin', f: 'Féminin' },
genderColors: { m: '#3B82F6', f: '#EC4899' },
specialCharacters: ['é', 'è', 'ê', 'ë', 'à', 'â', 'ù', 'û', 'ô', 'î', 'ï', 'ç', 'œ'],
characterNormalization: { 'é': 'e', 'è': 'e', 'ê': 'e', 'ë': 'e', 'à': 'a', 'â': 'a', 'ù': 'u', 'û': 'u', 'ô': 'o', 'î': 'i', 'ï': 'i', 'ç': 'c', 'œ': 'oe' },

Check warning on line 45 in cli/create-edu-app.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Replace `·'é':·'e',·'è':·'e',·'ê':·'e',·'ë':·'e',·'à':·'a',·'â':·'a',·'ù':·'u',·'û':·'u',·'ô':·'o',·'î':·'i',·'ï':·'i',·'ç':·'c',·'œ':·'oe'` with `⏎······é:·'e',⏎······è:·'e',⏎······ê:·'e',⏎······ë:·'e',⏎······à:·'a',⏎······â:·'a',⏎······ù:·'u',⏎······û:·'u',⏎······ô:·'o',⏎······î:·'i',⏎······ï:·'i',⏎······ç:·'c',⏎······œ:·'oe',⏎···`
pronouns: ['je', 'tu', 'il/elle/on', 'nous', 'vous', 'ils/elles'],
ttsVoice: 'fr-FR',
languageDir: 'french',
},
es: {
code: 'es', name: 'Spanish', nativeName: 'Español',

Check warning on line 51 in cli/create-edu-app.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Replace `·name:·'Spanish',` with `⏎····name:·'Spanish',⏎···`
genderSystem: 'two',
articles: { m: 'el', f: 'la', pl: 'los/las' },
genderLabels: { m: 'Masculino', f: 'Femenino' },
genderColors: { m: '#3B82F6', f: '#EC4899' },
specialCharacters: ['á', 'é', 'í', 'ó', 'ú', 'ü', 'ñ', '¿', '¡'],
characterNormalization: { 'á': 'a', 'é': 'e', 'í': 'i', 'ó': 'o', 'ú': 'u', 'ü': 'u', 'ñ': 'n' },

Check warning on line 57 in cli/create-edu-app.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Replace `'á':·'a',·'é':·'e',·'í':·'i',·'ó':·'o',·'ú':·'u',·'ü':·'u',·'ñ'` with `á:·'a',·é:·'e',·í:·'i',·ó:·'o',·ú:·'u',·ü:·'u',·ñ`
pronouns: ['yo', 'tú', 'él/ella/usted', 'nosotros', 'vosotros', 'ellos/ustedes'],
ttsVoice: 'es-ES',
languageDir: 'spanish',
},
nb: {
code: 'nb', name: 'Norwegian Bokmål', nativeName: 'Norsk bokmål',

Check warning on line 63 in cli/create-edu-app.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Replace `·name:·'Norwegian·Bokmål',` with `⏎····name:·'Norwegian·Bokmål',⏎···`
genderSystem: 'three',
articles: { m: 'en', f: 'ei', n: 'et', pl: '' },
genderLabels: { m: 'Hankjønn', f: 'Hunkjønn', n: 'Intetkjønn' },
genderColors: { m: '#3B82F6', f: '#EC4899', n: '#10B981' },
specialCharacters: ['æ', 'ø', 'å'],
characterNormalization: { 'æ': 'ae', 'ø': 'o', 'å': 'a' },

Check warning on line 69 in cli/create-edu-app.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Replace `'æ':·'ae',·'ø':·'o',·'å'` with `æ:·'ae',·ø:·'o',·å`
pronouns: ['jeg', 'du', 'han/hun/det', 'vi', 'dere', 'de'],
ttsVoice: 'nb-NO',
languageDir: 'norwegian',
Expand All @@ -77,7 +77,7 @@
function createPrompt() {
const rl = createInterface({ input: process.stdin, output: process.stdout });
return {
ask: (question, defaultVal) => new Promise(resolve => {

Check warning on line 80 in cli/create-edu-app.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Insert `⏎·····`
const suffix = defaultVal ? ` [${defaultVal}]` : '';
rl.question(` ${question}${suffix}: `, answer => {
resolve(answer.trim() || defaultVal || '');
Expand All @@ -89,12 +89,12 @@

// ── Copy directory recursively ───────────────────────────────────
function copyDirSync(src, dest, filter = () => true) {
if (!existsSync(src)) return;

Check failure on line 92 in cli/create-edu-app.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Expected { after 'if' condition
mkdirSync(dest, { recursive: true });
for (const entry of readdirSync(src)) {
const srcPath = join(src, entry);
const destPath = join(dest, entry);
if (!filter(srcPath, entry)) continue;

Check failure on line 97 in cli/create-edu-app.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Expected { after 'if' condition
if (statSync(srcPath).isDirectory()) {
copyDirSync(srcPath, destPath, filter);
} else {
Expand Down Expand Up @@ -158,7 +158,23 @@
// 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);
Expand Down Expand Up @@ -210,7 +226,7 @@
// ── 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);

Expand All @@ -228,6 +244,13 @@
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)) {
Expand Down Expand Up @@ -269,9 +292,21 @@
// ── 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 {
Expand All @@ -288,20 +323,20 @@
console.log(' ║ ✅ Project created successfully! ║');
console.log(' ╚══════════════════════════════════════════════╝');
console.log();
console.log(` Next steps:`);

Check failure on line 326 in cli/create-edu-app.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Strings must use singlequote
console.log(` cd ${outputDir}`);
console.log(` npm install`);

Check failure on line 328 in cli/create-edu-app.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Strings must use singlequote
console.log(` npm run dev`);

Check failure on line 329 in cli/create-edu-app.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Strings must use singlequote
console.log();
console.log(` Then open Claude Code and start building content:`);

Check failure on line 331 in cli/create-edu-app.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Strings must use singlequote
console.log(` /create-lesson 1-1`);

Check failure on line 332 in cli/create-edu-app.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Strings must use singlequote
console.log(` /create-exercises 1-1`);

Check failure on line 333 in cli/create-edu-app.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Strings must use singlequote
console.log();
}

// ── 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 || {})
Expand Down Expand Up @@ -371,12 +406,14 @@
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},
Expand Down Expand Up @@ -542,9 +579,9 @@
exerciseData[`${ch}-${l}`] = {
exercises: [
{
id: `aufgabeA`,

Check failure on line 582 in cli/create-edu-app.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Strings must use singlequote
type: 'fill-in',
title: `Exercise A`,

Check failure on line 584 in cli/create-edu-app.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Strings must use singlequote
badges: [],
description: 'Complete the sentences.',
items: [
Expand Down
2 changes: 1 addition & 1 deletion public/js/core/language-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export function getNativeKey() {
*/
export function getTargetLanguageCode() {
const config = getLanguageConfig();
return config.code || 'de';
return config.code || 'nb';
}

/**
Expand Down
2 changes: 1 addition & 1 deletion public/js/dialog/ord-battle.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;

Expand Down
3 changes: 2 additions & 1 deletion public/js/exercises/embedded-gender-trainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
3 changes: 2 additions & 1 deletion public/js/exercises/embedded-verb-trainer-v2.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down
28 changes: 17 additions & 11 deletions public/js/layout/shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -47,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(() => {
Expand Down Expand Up @@ -210,7 +215,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) {
Expand All @@ -221,6 +226,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);
Expand Down
34 changes: 33 additions & 1 deletion public/js/progress/curriculum-registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -235,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] || {};
}
6 changes: 3 additions & 3 deletions public/js/progress/curriculum.js
Original file line number Diff line number Diff line change
Expand Up @@ -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] || {};
}

/**
Expand All @@ -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]');
Expand Down
22 changes: 16 additions & 6 deletions public/js/progress/migration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

// =================================================================
Expand Down Expand Up @@ -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<string[]>} 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 => {
Expand Down Expand Up @@ -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', []);
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down
13 changes: 8 additions & 5 deletions public/js/progress/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: []
Expand All @@ -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
Expand Down Expand Up @@ -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();
}

/**
Expand Down
Loading