From 09a1326c7e81009f8c9fe9d250af1c63f4bb7e0e Mon Sep 17 00:00:00 2001 From: Ramon Date: Sun, 19 Jul 2026 11:55:25 +0200 Subject: [PATCH] feat: meer personage-accessoires incl. zonnebril (v0.4.28-beta) - Nieuwe hoofdaccessoires: zonnebril, koptelefoon, feesthoedje. - Nieuwe lichaamsaccessoires: ketting, rugtas (bandjes over de schouders, zichtbaar van voren). - Server-validatie (src/api.js) en vertalingen (nl+en) meegewerkt met dezelfde nieuwe waarden - anders zou de client een optie tonen die de server als "ongeldig hoofdaccessoire" afwijst. - Geen wijziging aan de bestaande pet/kroon-onderdrukkingsregel nodig: zonnebril/koptelefoon/feesthoedje bedekken het bovenhoofd niet volledig, dus haar blijft er gewoon zichtbaar naast/onder. - Geverifieerd: alle select-opties komen aan in de instellingen-editor, een PUT met de nieuwe waarden slaagt (bestaande server-validatie accepteert ze nu), en alle soort/accessoire-combinaties oogden goed in screenshot (zonnebril op de dino, koptelefoon op het monster, feesthoedje + ketting samen, rugtas op het dier). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_014EPxzBVRXZZnBPvSAaPAbJ --- VERSION | 2 +- public/js/avatar.js | 33 +++++++++++++++++++++++++++++++-- public/js/core.js | 6 +++++- src/api.js | 4 ++-- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/VERSION b/VERSION index e85208e..f63d1be 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.27-beta \ No newline at end of file +0.4.28-beta \ No newline at end of file diff --git a/public/js/avatar.js b/public/js/avatar.js index cdecdae..af23903 100644 --- a/public/js/avatar.js +++ b/public/js/avatar.js @@ -7,8 +7,8 @@ "use strict"; const AVATAR_SPECIES = ["dino", "monster", "animal"]; const AVATAR_HAIR = ["none", "spiky", "curly", "ponytail", "mohawk"]; -const AVATAR_HEAD_ACC = ["none", "cap", "crown", "glasses", "bow"]; -const AVATAR_BODY_ACC = ["none", "scarf", "cape", "badge"]; +const AVATAR_HEAD_ACC = ["none", "cap", "crown", "glasses", "sunglasses", "bow", "headphones", "partyhat"]; +const AVATAR_BODY_ACC = ["none", "scarf", "cape", "badge", "necklace", "backpack"]; const AVATAR_DEFAULT = Object.freeze({ species: "dino", bodyColor: "#3fae6a", hairStyle: "none", hairColor: "#5b3a29", accessoryHead: "none", accessoryBody: "none", @@ -147,6 +147,27 @@ const AVATAR_HEAD_ACC_MARKUP = { bow: () => ` `, + sunglasses: () => ` + + + + + + + `, + headphones: () => ` + + + + + `, + partyhat: () => ` + + + + + + `, }; const AVATAR_BODY_ACC_MARKUP = { @@ -158,6 +179,14 @@ const AVATAR_BODY_ACC_MARKUP = { `, badge: () => ``, + necklace: () => ` + + `, + backpack: () => ` + + + + `, }; /* een pet/kroon dekt het hele bovenhoofd af - loshangend haar eronder tonen diff --git a/public/js/core.js b/public/js/core.js index a59766d..b18968c 100644 --- a/public/js/core.js +++ b/public/js/core.js @@ -2,7 +2,7 @@ "use strict"; /* version — shown until /api/version resolves (or if the fetch fails, e.g. offline). Kept in sync by hand with the VERSION file at the repo root on every release. */ -const VERSION = "0.4.27-beta"; +const VERSION = "0.4.28-beta"; (function(){ const tag = document.getElementById("verTag"); tag.textContent = "v"+VERSION; @@ -169,7 +169,9 @@ const I18N = { avatarHair_none:"Geen", avatarHair_spiky:"Piekerig", avatarHair_curly:"Krullen", avatarHair_ponytail:"Staartje", avatarHair_mohawk:"Hanenkam", avatarAccHead:"Hoofdaccessoire", avatarAccBody:"Lichaamsaccessoire", avatarAcc_none:"Geen", avatarAcc_cap:"Petje", avatarAcc_crown:"Kroon", avatarAcc_glasses:"Bril", avatarAcc_bow:"Strik", + avatarAcc_sunglasses:"Zonnebril", avatarAcc_headphones:"Koptelefoon", avatarAcc_partyhat:"Feesthoedje", avatarAcc_scarf:"Sjaal", avatarAcc_cape:"Cape", avatarAcc_badge:"Insigne", + avatarAcc_necklace:"Ketting", avatarAcc_backpack:"Rugtas", avatarSave:"Opslaan", avatarSaved:"Personage opgeslagen ✓", clock24Hour:"24-uursklok", widgetSettings:"Instellingen", clockWelcome:"Oefenen met de klok", clockWelcomeHint:"Kies bovenaan niveau 1, 2 of 3. Kies bij Instellingen hoe de opdracht wordt getoond en start daarna.", clockSetTitle:"Zet de wijzers op de goede tijd", clockReadTitle:"Bekijk de opdracht en maak de tijd", clockChooseTitle:"Kies het juiste tijdblok", clockYourTask:"Jouw opdracht", clockLinkHands:"Grote wijzer draait mee", clockTypeHint:"Voer de tijd in als UU:MM.", clockLevel1Hint:"Niveau 1: hele en halve uren", clockLevel2Hint:"Niveau 2: stappen van vijf minuten", clockLevel3Hint:"Niveau 3: alle minuten, met optioneel 24 uur en dagdelen", clockBuildHint:"Sleep vier cijfers naar de vakken en maak UU:MM.", clockDigitGhost:"?", clockDigit:"Cijfer {digit}", clockIncomplete:"Maak eerst de volledige tijd af.", clockCorrect:"Goed gedaan! Kies de volgende opdracht. 🌟", clockTryAgain:"Bijna! Probeer het nog een keer.", clockStartTask:"Start opdracht", clockNextTask:"Volgende opdracht", clockLevel:"Niveau", clockLevelHour:"Hele uren", clockLevelQuarter:"Kwartieren", clockLevelFive:"Vijf minuten", clockLevelMinute:"Iedere minuut", clockLearningStyle:"Opdracht tonen", clockStyleAnalogue:"Analoge klok", clockStyleDigital:"Grote digitale tijd", clockStyleWords:"In woorden", clockStyleBoth:"Klok, cijfers en woorden", clockDragHint:"Sleep de wijzers om de tijd te veranderen.", clockGame:"Spel", clockGameSet:"Klok zetten", clockGameRead:"Tijd aflezen", clockGameChoose:"Kies de tijd", clockStart:"Kies ‘Nieuwe opdracht’ om te beginnen", clockRead:"Welke tijd zie je? Stel je antwoord hieronder in.", clockChoose:"Welke digitale tijd hoort bij deze klok?", clockTry:"Nog niet. Het goede antwoord is {time}.", clockScore:"Score: {score} · reeks: {streak}", clockWordsHour:"{hour} uur", clockWordsQuarterPast:"kwart over {hour}", clockWordsHalf:"half {hour}", clockWordsQuarterTo:"kwart voor {hour}", clockWordsPast:"{minutes} over {hour}", clockWordsTo:"{minutes} voor {hour}", clockMorning:"in de ochtend", clockAfternoon:"in de middag", clockEvening:"in de avond", clockNight:"in de nacht", fractionRows:"Rijen", fractionLabels:"Breuken tonen", fractionHint:"Tik in een rij tot waar je de breuk wilt kleuren.", fractionSelected:"Gekozen breuk: {fraction}", fractionClear:"Leegmaken", clockTask:"Oefenopdracht", clockSet:"Zet de klok op {time}", clockHour:"Uur", clockMinute:"Minuten", placeNumber:"Getal", placeGame:"Spel", placeGameExplore:"Verkennen", placeGameBuild:"Bouwen", placeLevel:"Niveau", placeLevel1Hint:"Niveau 1: getallen tot 100 (T en E)", placeLevel2Hint:"Niveau 2: getallen tot 1000 (H, T en E)", placeLevel3Hint:"Niveau 3: getallen tot 1.000.000 (HD, TD, D, H, T en E)", placeBuildTitle:"Bouw het getal", placeYourTask:"Jouw opdracht", placeBuildHint:"Sleep de cijfers naar de vakken en bouw het getal.", placeDigitGhost:"?", placeIncomplete:"Maak eerst het hele getal af.", placeCorrect:"Goed gedaan! Kies de volgende opdracht. 🌟", placeTryAgain:"Bijna! Probeer het nog een keer.", placeNextTask:"Volgende opdracht", placeScore:"Score: {score} · reeks: {streak}", moneyMake:"Maak € {amount}", wordtypeSentence:"Zin", wordtypeHint:"Tik: blauw zelfstandig naamwoord · groen werkwoord · geel bijvoeglijk naamwoord", scheduleActivity:"activiteit…", scheduleEmpty:"Voeg de eerste activiteit toe.", scheduleCount:"{count} momenten op de planning", pollReset:"Opnieuw", pollQuestion:"Vraag", pollOptions:"Antwoorden (één per regel)", pollTotal:"{count} stemmen", moodTotal:"{count} kinderen hebben gekozen", moodVote:"Kies {face}", galleryFavorites:"Favorieten", galleryFavorite:"Favoriet maken", galleryEmpty:"Geen widgets gevonden.", gallerySearch:"Zoek widgets…", templateMath:"Rekenles", templateLanguage:"Taalles", templateDay:"Dagstart", exportBoard:"Exporteren", importBoard:"Importeren", importError:"Dit is geen geldig Teach-bordbestand.", widgetDuplicate:"Dupliceren", widgetLock:"Vastzetten", widgetUnlock:"Losmaken", widgetPreview:"Voorbeeld zonder bediening", undo:"Ongedaan maken", redo:"Opnieuw", @@ -501,7 +503,9 @@ const I18N = { avatarHair_none:"None", avatarHair_spiky:"Spiky", avatarHair_curly:"Curly", avatarHair_ponytail:"Ponytail", avatarHair_mohawk:"Mohawk", avatarAccHead:"Head accessory", avatarAccBody:"Body accessory", avatarAcc_none:"None", avatarAcc_cap:"Cap", avatarAcc_crown:"Crown", avatarAcc_glasses:"Glasses", avatarAcc_bow:"Bow", + avatarAcc_sunglasses:"Sunglasses", avatarAcc_headphones:"Headphones", avatarAcc_partyhat:"Party hat", avatarAcc_scarf:"Scarf", avatarAcc_cape:"Cape", avatarAcc_badge:"Badge", + avatarAcc_necklace:"Necklace", avatarAcc_backpack:"Backpack", avatarSave:"Save", avatarSaved:"Character saved ✓", clock24Hour:"24-hour clock", widgetSettings:"Settings", clockWelcome:"Practise telling time", clockWelcomeHint:"Choose level 1, 2 or 3 at the top. In Settings, choose how the task is shown, then start.", clockSetTitle:"Move the hands to the correct time", clockReadTitle:"View the task and make the time", clockChooseTitle:"Choose the correct time block", clockYourTask:"Your task", clockLinkHands:"Move the minute hand along", clockTypeHint:"Enter the time as HH:MM.", clockLevel1Hint:"Level 1: whole and half hours", clockLevel2Hint:"Level 2: five-minute steps", clockLevel3Hint:"Level 3: every minute, with optional 24-hour time and day parts", clockBuildHint:"Drag four digits into the slots to make HH:MM.", clockDigitGhost:"?", clockDigit:"Digit {digit}", clockIncomplete:"Complete the full time first.", clockCorrect:"Well done! Choose the next task. 🌟", clockTryAgain:"Almost! Try again.", clockStartTask:"Start task", clockNextTask:"Next task", clockLevel:"Level", clockLevelHour:"Whole hours", clockLevelQuarter:"Quarter hours", clockLevelFive:"Five minutes", clockLevelMinute:"Every minute", clockLearningStyle:"Show task as", clockStyleAnalogue:"Analogue clock", clockStyleDigital:"Large digital time", clockStyleWords:"In words", clockStyleBoth:"Clock, digits and words", clockDragHint:"Drag the hands to change the time.", clockGame:"Game", clockGameSet:"Set the clock", clockGameRead:"Read the time", clockGameChoose:"Choose the time", clockStart:"Choose ‘New task’ to begin", clockRead:"What time is it? Set your answer below.", clockChoose:"Which digital time matches this clock?", clockTry:"Not yet. The correct answer is {time}.", clockScore:"Score: {score} · streak: {streak}", clockWordsHour:"{hour} o'clock", clockWordsQuarterPast:"quarter past {hour}", clockWordsHalf:"half past {hour}", clockWordsQuarterTo:"quarter to {hour}", clockWordsPast:"{minutes} past {hour}", clockWordsTo:"{minutes} to {hour}", clockMorning:"in the morning", clockAfternoon:"in the afternoon", clockEvening:"in the evening", clockNight:"at night", fractionRows:"Rows", fractionLabels:"Show fractions", fractionHint:"Tap a row up to the part you want to colour.", fractionSelected:"Selected fraction: {fraction}", fractionClear:"Clear", clockTask:"Practice task", clockSet:"Set the clock to {time}", clockHour:"Hour", clockMinute:"Minutes", placeNumber:"Number", placeGame:"Game", placeGameExplore:"Explore", placeGameBuild:"Build", placeLevel:"Level", placeLevel1Hint:"Level 1: numbers up to 100 (T and E)", placeLevel2Hint:"Level 2: numbers up to 1000 (H, T and E)", placeLevel3Hint:"Level 3: numbers up to 1,000,000 (HD, TD, D, H, T and E)", placeBuildTitle:"Build the number", placeYourTask:"Your task", placeBuildHint:"Drag the digits into the slots to build the number.", placeDigitGhost:"?", placeIncomplete:"Complete the whole number first.", placeCorrect:"Well done! Choose the next task. 🌟", placeTryAgain:"Almost! Try again.", placeNextTask:"Next task", placeScore:"Score: {score} · streak: {streak}", moneyMake:"Make € {amount}", wordtypeSentence:"Sentence", wordtypeHint:"Tap: blue noun · green verb · yellow adjective", scheduleActivity:"activity…", scheduleEmpty:"Add the first activity.", scheduleCount:"{count} moments in the schedule", pollReset:"Reset", pollQuestion:"Question", pollOptions:"Answers (one per line)", pollTotal:"{count} votes", moodTotal:"{count} children have chosen", moodVote:"Choose {face}", galleryFavorites:"Favourites", galleryFavorite:"Add favourite", galleryEmpty:"No widgets found.", gallerySearch:"Search widgets…", templateMath:"Maths lesson", templateLanguage:"Language lesson", templateDay:"Day start", exportBoard:"Export", importBoard:"Import", importError:"This is not a valid Teach board file.", widgetDuplicate:"Duplicate", widgetLock:"Lock", widgetUnlock:"Unlock", widgetPreview:"Preview without controls", undo:"Undo", redo:"Redo", diff --git a/src/api.js b/src/api.js index 1b95bf6..234dc20 100644 --- a/src/api.js +++ b/src/api.js @@ -196,8 +196,8 @@ export default async function api(app) { // geen rol-beperkte PERMISSIONS-sleutel nodig, altijd de eigen rij. const AVATAR_SPECIES = ['dino', 'monster', 'animal']; const AVATAR_HAIR = ['none', 'spiky', 'curly', 'ponytail', 'mohawk']; - const AVATAR_HEAD_ACC = ['none', 'cap', 'crown', 'glasses', 'bow']; - const AVATAR_BODY_ACC = ['none', 'scarf', 'cape', 'badge']; + const AVATAR_HEAD_ACC = ['none', 'cap', 'crown', 'glasses', 'sunglasses', 'bow', 'headphones', 'partyhat']; + const AVATAR_BODY_ACC = ['none', 'scarf', 'cape', 'badge', 'necklace', 'backpack']; const isHexColor = (v) => typeof v === 'string' && /^#[0-9a-fA-F]{6}$/.test(v); app.put('/me/avatar', async (req, reply) => { need(req, reply);