diff --git a/features/Achievements/__tests__/kanjiGraduate.test.ts b/features/Achievements/__tests__/kanjiGraduate.test.ts new file mode 100644 index 000000000..a57f4bd4f --- /dev/null +++ b/features/Achievements/__tests__/kanjiGraduate.test.ts @@ -0,0 +1,160 @@ +import { describe, it, expect } from 'vitest'; +import { ACHIEVEMENTS, type Achievement } from '../store/useAchievementStore'; +import { + KANJI_N5, + KANJI_N4, + KANJI_N3, + KANJI_N2, + KANJI_N1, + KANJI_BY_JLPT_LEVEL, +} from '../store/kanjiSets'; + +/** + * Tests for N5-N1 Graduate achievement bug fix. + * Verifies that Graduate achievements are only unlocked when ALL kanji + * for the specific JLPT level have been practiced with >= 80% accuracy, + * and that practicing only N5 kanji does NOT unlock N4-N1 Graduate achievements. + * + * Regression test for: [BUG] N4 to N1 Graduate achievements achieved before completed + */ + +const graduateAchievements = ACHIEVEMENTS.filter(a => + [ + 'n5_graduate', + 'n4_graduate', + 'n3_graduate', + 'n2_graduate', + 'n1_graduate', + ].includes(a.id), +); + +/** Build characterMastery where every kanji in the given set has the specified accuracy */ +function buildMastery( + kanjiSet: Set, + correct: number, + incorrect: number, +): Record { + const mastery: Record = {}; + for (const char of kanjiSet) { + mastery[char] = { correct, incorrect }; + } + return mastery; +} + +/** + * Mirrors the corrected checkContentMastery logic for kanji: + * - filters characterMastery to only kanji in the JLPT-level set + * - requires all kanji in the set to have been practiced + * - requires all practiced kanji to have >= value% accuracy + */ +function checkKanjiGraduate( + achievement: Achievement, + characterMastery: Record, +): boolean { + const { value, additional } = achievement.requirements; + const jlptLevel = additional?.jlptLevel as + | 'N5' + | 'N4' + | 'N3' + | 'N2' + | 'N1' + | undefined; + if (!jlptLevel) return false; + + const kanjiSet = KANJI_BY_JLPT_LEVEL[jlptLevel]; + const entries = Object.entries(characterMastery).filter(([key]) => + kanjiSet.has(key), + ); + + // All kanji of this level must be present + if (entries.length < kanjiSet.size) return false; + + for (const [, stats] of entries) { + const total = stats.correct + stats.incorrect; + if (total === 0) return false; + const accuracy = (stats.correct / total) * 100; + if (accuracy < value) return false; + } + return true; +} + +describe('Kanji Graduate Achievements', () => { + it('kanjiSets export the correct character counts', () => { + expect(KANJI_N5.size).toBe(80); + expect(KANJI_N4.size).toBe(167); + expect(KANJI_N3.size).toBe(370); + expect(KANJI_N2.size).toBe(374); + expect(KANJI_N1.size).toBe(1504); + }); + + it('KANJI_BY_JLPT_LEVEL maps levels to the correct sets', () => { + expect(KANJI_BY_JLPT_LEVEL['N5']).toBe(KANJI_N5); + expect(KANJI_BY_JLPT_LEVEL['N4']).toBe(KANJI_N4); + expect(KANJI_BY_JLPT_LEVEL['N3']).toBe(KANJI_N3); + expect(KANJI_BY_JLPT_LEVEL['N2']).toBe(KANJI_N2); + expect(KANJI_BY_JLPT_LEVEL['N1']).toBe(KANJI_N1); + }); + + it('five Graduate achievements are defined (N5 to N1)', () => { + expect(graduateAchievements).toHaveLength(5); + const ids = graduateAchievements.map(a => a.id); + expect(ids).toContain('n5_graduate'); + expect(ids).toContain('n4_graduate'); + expect(ids).toContain('n3_graduate'); + expect(ids).toContain('n2_graduate'); + expect(ids).toContain('n1_graduate'); + }); + + it('practicing ONLY N5 kanji with 80%+ accuracy does NOT unlock N4-N1 Graduate achievements', () => { + // Simulate: user practiced all N5 kanji with 90% accuracy + const mastery = buildMastery(KANJI_N5, 9, 1); // 90% accuracy + + const n4Graduate = ACHIEVEMENTS.find(a => a.id === 'n4_graduate')!; + const n3Graduate = ACHIEVEMENTS.find(a => a.id === 'n3_graduate')!; + const n2Graduate = ACHIEVEMENTS.find(a => a.id === 'n2_graduate')!; + const n1Graduate = ACHIEVEMENTS.find(a => a.id === 'n1_graduate')!; + + expect(checkKanjiGraduate(n4Graduate, mastery)).toBe(false); + expect(checkKanjiGraduate(n3Graduate, mastery)).toBe(false); + expect(checkKanjiGraduate(n2Graduate, mastery)).toBe(false); + expect(checkKanjiGraduate(n1Graduate, mastery)).toBe(false); + }); + + it('practicing ALL N5 kanji with 80%+ accuracy unlocks N5 Graduate achievement', () => { + const mastery = buildMastery(KANJI_N5, 8, 2); // exactly 80% accuracy + const n5Graduate = ACHIEVEMENTS.find(a => a.id === 'n5_graduate')!; + expect(checkKanjiGraduate(n5Graduate, mastery)).toBe(true); + }); + + it('practicing only SOME N5 kanji does NOT unlock N5 Graduate achievement', () => { + // Only 3 out of 80 N5 kanji practiced + const partialMastery: Record< + string, + { correct: number; incorrect: number } + > = {}; + let count = 0; + for (const char of KANJI_N5) { + partialMastery[char] = { correct: 9, incorrect: 1 }; + if (++count >= 3) break; + } + const n5Graduate = ACHIEVEMENTS.find(a => a.id === 'n5_graduate')!; + expect(checkKanjiGraduate(n5Graduate, partialMastery)).toBe(false); + }); + + it('practicing N5 kanji below 80% accuracy does NOT unlock N5 Graduate achievement', () => { + const mastery = buildMastery(KANJI_N5, 7, 3); // 70% accuracy + const n5Graduate = ACHIEVEMENTS.find(a => a.id === 'n5_graduate')!; + expect(checkKanjiGraduate(n5Graduate, mastery)).toBe(false); + }); + + it('each Graduate achievement requires all kanji of its specific level', () => { + // N4 Graduate should only unlock when ALL N4 kanji are mastered + const mastery = buildMastery(KANJI_N4, 9, 1); // all N4 with 90% + const n4Graduate = ACHIEVEMENTS.find(a => a.id === 'n4_graduate')!; + const n5Graduate = ACHIEVEMENTS.find(a => a.id === 'n5_graduate')!; + + expect(checkKanjiGraduate(n4Graduate, mastery)).toBe(true); + // N5 kanji not in this mastery → N5 Graduate should NOT unlock + expect(checkKanjiGraduate(n5Graduate, mastery)).toBe(false); + }); +}); diff --git a/features/Achievements/components/progress/useAchievementProgress.ts b/features/Achievements/components/progress/useAchievementProgress.ts index 9629e0e42..9003ad85e 100644 --- a/features/Achievements/components/progress/useAchievementProgress.ts +++ b/features/Achievements/components/progress/useAchievementProgress.ts @@ -2,6 +2,7 @@ import { useState, useCallback, useMemo } from 'react'; import useAchievementStore, { ACHIEVEMENTS, } from '@/features/Achievements/store/useAchievementStore'; +import { KANJI_BY_JLPT_LEVEL } from '@/features/Achievements/store/kanjiSets'; import { useStatsStore } from '@/features/Progress'; import { useClick } from '@/shared/hooks/useAudio'; import { useShallow } from 'zustand/react/shallow'; @@ -127,6 +128,17 @@ export const useAchievementProgress = () => { } current = masteredCount; target = additional.minAnswers; + } else if (contentType === 'kanji' && additional?.jlptLevel) { + const kanjiSet = KANJI_BY_JLPT_LEVEL[additional.jlptLevel]; + relevantEntries = entries.filter(([key]) => kanjiSet.has(key)); + let masteredCount = 0; + for (const [, s] of relevantEntries) { + const tot = s.correct + s.incorrect; + if (tot > 0 && (s.correct / tot) * 100 >= targetAccuracy) + masteredCount++; + } + current = masteredCount; + target = kanjiSet.size; } else { isPercentage = true; current = 0; diff --git a/features/Achievements/store/kanjiSets.ts b/features/Achievements/store/kanjiSets.ts new file mode 100644 index 000000000..306d870f8 --- /dev/null +++ b/features/Achievements/store/kanjiSets.ts @@ -0,0 +1,2525 @@ +/** + * Static kanji character sets per JLPT level. + * Extracted from public/data-kanji/*.json. + */ + +export const KANJI_N5 = new Set([ + '日', + '一', + '国', + '人', + '年', + '大', + '十', + '二', + '本', + '中', + '長', + '出', + '三', + '時', + '行', + '見', + '月', + '分', + '後', + '前', + '生', + '五', + '間', + '上', + '東', + '四', + '今', + '金', + '九', + '入', + '学', + '高', + '円', + '子', + '外', + '八', + '六', + '下', + '来', + '気', + '小', + '七', + '山', + '話', + '女', + '北', + '午', + '百', + '書', + '先', + '名', + '川', + '千', + '水', + '半', + '男', + '西', + '電', + '校', + '語', + '土', + '木', + '聞', + '食', + '車', + '何', + '南', + '万', + '毎', + '白', + '天', + '母', + '火', + '右', + '読', + '友', + '左', + '休', + '父', + '雨', +]); + +export const KANJI_N4 = new Set([ + '会', + '同', + '事', + '自', + '社', + '発', + '者', + '地', + '業', + '方', + '新', + '場', + '員', + '立', + '開', + '手', + '力', + '問', + '代', + '明', + '動', + '京', + '目', + '通', + '言', + '理', + '体', + '田', + '主', + '題', + '意', + '不', + '作', + '用', + '度', + '強', + '公', + '持', + '野', + '以', + '思', + '家', + '世', + '多', + '正', + '安', + '院', + '心', + '界', + '教', + '文', + '元', + '重', + '近', + '考', + '画', + '海', + '売', + '知', + '道', + '集', + '別', + '物', + '使', + '品', + '計', + '死', + '特', + '私', + '始', + '朝', + '運', + '終', + '台', + '広', + '住', + '無', + '真', + '有', + '口', + '少', + '町', + '料', + '工', + '建', + '空', + '急', + '止', + '送', + '切', + '転', + '研', + '足', + '究', + '楽', + '起', + '着', + '店', + '病', + '質', + '待', + '試', + '族', + '銀', + '早', + '映', + '親', + '験', + '英', + '医', + '仕', + '去', + '味', + '写', + '字', + '答', + '夜', + '音', + '注', + '帰', + '古', + '歌', + '買', + '悪', + '図', + '週', + '室', + '歩', + '風', + '紙', + '黒', + '花', + '春', + '赤', + '青', + '館', + '屋', + '色', + '走', + '秋', + '夏', + '習', + '駅', + '洋', + '旅', + '服', + '夕', + '借', + '曜', + '飲', + '肉', + '貸', + '堂', + '鳥', + '飯', + '勉', + '冬', + '昼', + '茶', + '弟', + '牛', + '魚', + '兄', + '犬', + '妹', + '姉', + '漢', +]); + +export const KANJI_N3 = new Set([ + '政', + '議', + '民', + '連', + '対', + '部', + '合', + '市', + '内', + '相', + '定', + '回', + '選', + '米', + '実', + '関', + '決', + '全', + '表', + '戦', + '経', + '最', + '現', + '調', + '化', + '当', + '約', + '首', + '法', + '性', + '的', + '要', + '制', + '治', + '務', + '成', + '期', + '取', + '都', + '和', + '機', + '平', + '加', + '受', + '続', + '進', + '数', + '記', + '初', + '指', + '権', + '支', + '産', + '点', + '報', + '済', + '活', + '原', + '共', + '得', + '解', + '交', + '資', + '予', + '向', + '際', + '勝', + '面', + '告', + '反', + '判', + '認', + '参', + '利', + '組', + '信', + '在', + '件', + '側', + '任', + '引', + '求', + '所', + '次', + '昨', + '論', + '官', + '増', + '係', + '感', + '情', + '投', + '示', + '変', + '打', + '直', + '両', + '式', + '確', + '果', + '容', + '必', + '演', + '歳', + '争', + '談', + '能', + '位', + '置', + '流', + '格', + '疑', + '過', + '局', + '放', + '常', + '状', + '球', + '職', + '与', + '供', + '役', + '構', + '割', + '身', + '費', + '付', + '由', + '説', + '難', + '優', + '夫', + '収', + '断', + '石', + '違', + '消', + '神', + '番', + '規', + '術', + '備', + '宅', + '害', + '配', + '警', + '育', + '席', + '訪', + '乗', + '残', + '想', + '声', + '助', + '労', + '例', + '然', + '限', + '追', + '商', + '葉', + '伝', + '働', + '形', + '景', + '落', + '好', + '退', + '頭', + '負', + '渡', + '失', + '差', + '末', + '守', + '若', + '種', + '美', + '命', + '福', + '望', + '非', + '観', + '察', + '段', + '横', + '深', + '申', + '様', + '財', + '港', + '識', + '呼', + '達', + '良', + '阪', + '候', + '程', + '満', + '敗', + '値', + '突', + '光', + '路', + '科', + '積', + '他', + '処', + '太', + '客', + '否', + '師', + '登', + '易', + '速', + '存', + '飛', + '殺', + '号', + '単', + '座', + '破', + '除', + '完', + '降', + '責', + '捕', + '危', + '給', + '苦', + '迎', + '園', + '具', + '辞', + '因', + '馬', + '愛', + '富', + '彼', + '未', + '舞', + '亡', + '冷', + '適', + '婦', + '寄', + '込', + '顔', + '類', + '余', + '王', + '返', + '妻', + '背', + '熱', + '宿', + '薬', + '険', + '頼', + '覚', + '船', + '途', + '許', + '抜', + '便', + '留', + '罪', + '努', + '精', + '散', + '静', + '婚', + '喜', + '浮', + '絶', + '幸', + '押', + '倒', + '等', + '老', + '曲', + '払', + '庭', + '徒', + '勤', + '遅', + '居', + '雑', + '招', + '困', + '欠', + '更', + '刻', + '賛', + '抱', + '犯', + '恐', + '息', + '遠', + '戻', + '願', + '絵', + '越', + '欲', + '痛', + '笑', + '互', + '束', + '似', + '列', + '探', + '逃', + '遊', + '迷', + '夢', + '君', + '閉', + '緒', + '折', + '草', + '暮', + '酒', + '悲', + '晴', + '掛', + '到', + '寝', + '暗', + '盗', + '吸', + '陽', + '御', + '歯', + '忘', + '雪', + '吹', + '娘', + '誤', + '洗', + '慣', + '礼', + '窓', + '昔', + '貧', + '怒', + '泳', + '祖', + '杯', + '疲', + '皆', + '鳴', + '腹', + '煙', + '眠', + '怖', + '耳', + '頂', + '箱', + '晩', + '寒', + '髪', + '忙', + '才', + '靴', + '恥', + '偶', + '偉', + '猫', + '幾', + '誰', +]); + +export const KANJI_N2 = new Set([ + '党', + '協', + '総', + '区', + '領', + '県', + '設', + '保', + '改', + '第', + '結', + '派', + '府', + '査', + '委', + '軍', + '案', + '策', + '団', + '各', + '島', + '革', + '村', + '勢', + '減', + '再', + '税', + '営', + '比', + '防', + '補', + '境', + '導', + '副', + '算', + '輸', + '述', + '線', + '農', + '州', + '武', + '象', + '域', + '額', + '欧', + '担', + '準', + '賞', + '辺', + '造', + '被', + '技', + '低', + '復', + '移', + '個', + '門', + '課', + '脳', + '極', + '含', + '蔵', + '量', + '型', + '況', + '針', + '専', + '谷', + '史', + '階', + '管', + '兵', + '接', + '細', + '効', + '丸', + '湾', + '録', + '省', + '旧', + '橋', + '岸', + '周', + '材', + '戸', + '央', + '券', + '編', + '捜', + '竹', + '超', + '並', + '療', + '採', + '森', + '競', + '介', + '根', + '販', + '歴', + '将', + '幅', + '般', + '貿', + '講', + '林', + '装', + '諸', + '劇', + '河', + '航', + '鉄', + '児', + '禁', + '印', + '逆', + '換', + '久', + '短', + '油', + '暴', + '輪', + '占', + '植', + '清', + '倍', + '均', + '億', + '圧', + '芸', + '署', + '伸', + '停', + '爆', + '陸', + '玉', + '波', + '帯', + '延', + '羽', + '固', + '則', + '乱', + '普', + '測', + '豊', + '厚', + '齢', + '囲', + '卒', + '略', + '承', + '順', + '岩', + '練', + '軽', + '了', + '庁', + '城', + '患', + '層', + '版', + '令', + '角', + '絡', + '損', + '募', + '裏', + '仏', + '績', + '築', + '貨', + '混', + '昇', + '池', + '血', + '温', + '季', + '星', + '永', + '著', + '誌', + '庫', + '刊', + '像', + '香', + '坂', + '底', + '布', + '寺', + '宇', + '巨', + '震', + '希', + '触', + '依', + '籍', + '汚', + '枚', + '複', + '郵', + '仲', + '栄', + '札', + '板', + '骨', + '傾', + '届', + '巻', + '燃', + '跡', + '包', + '駐', + '弱', + '紹', + '雇', + '替', + '預', + '焼', + '簡', + '章', + '臓', + '律', + '贈', + '照', + '薄', + '群', + '秒', + '奥', + '詰', + '双', + '刺', + '純', + '翌', + '快', + '片', + '敬', + '悩', + '泉', + '皮', + '漁', + '荒', + '貯', + '硬', + '埋', + '柱', + '祭', + '袋', + '筆', + '訓', + '浴', + '童', + '宝', + '封', + '胸', + '砂', + '塩', + '賢', + '腕', + '兆', + '床', + '毛', + '緑', + '尊', + '祝', + '柔', + '殿', + '濃', + '液', + '衣', + '肩', + '零', + '幼', + '荷', + '泊', + '黄', + '甘', + '臣', + '浅', + '掃', + '雲', + '掘', + '捨', + '軟', + '沈', + '凍', + '乳', + '恋', + '紅', + '郊', + '腰', + '炭', + '踊', + '冊', + '勇', + '械', + '菜', + '珍', + '卵', + '湖', + '喫', + '干', + '虫', + '刷', + '湯', + '溶', + '鉱', + '涙', + '匹', + '孫', + '鋭', + '枝', + '塗', + '軒', + '毒', + '叫', + '拝', + '氷', + '乾', + '棒', + '祈', + '拾', + '粉', + '糸', + '綿', + '汗', + '銅', + '湿', + '瓶', + '咲', + '召', + '缶', + '隻', + '脂', + '蒸', + '肌', + '耕', + '鈍', + '泥', + '隅', + '灯', + '辛', + '磨', + '麦', + '姓', + '筒', + '鼻', + '粒', + '詞', + '胃', + '畳', + '机', + '膚', + '濯', + '塔', + '沸', + '灰', + '菓', + '帽', + '枯', + '涼', + '舟', + '貝', + '符', + '憎', + '皿', + '肯', + '燥', + '畜', + '坊', + '挟', + '曇', + '滴', + '伺', +]); + +export const KANJI_N1 = new Set([ + '氏', + '統', + '基', + '価', + '提', + '挙', + '応', + '企', + '検', + '藤', + '沢', + '裁', + '証', + '援', + '可', + '施', + '井', + '護', + '展', + '態', + '鮮', + '視', + '条', + '幹', + '独', + '宮', + '率', + '衛', + '張', + '監', + '環', + '審', + '義', + '訴', + '株', + '姿', + '閣', + '韓', + '衆', + '評', + '岡', + '影', + '松', + '撃', + '佐', + '核', + '整', + '融', + '製', + '票', + '渉', + '響', + '推', + '請', + '器', + '士', + '討', + '攻', + '崎', + '督', + '授', + '催', + '及', + '憲', + '離', + '激', + '摘', + '系', + '批', + '郎', + '健', + '従', + '修', + '隊', + '織', + '拡', + '故', + '振', + '弁', + '就', + '異', + '献', + '厳', + '維', + '浜', + '遺', + '塁', + '邦', + '素', + '遣', + '抗', + '模', + '雄', + '益', + '緊', + '標', + '宣', + '昭', + '廃', + '伊', + '江', + '僚', + '吉', + '盛', + '皇', + '臨', + '踏', + '壊', + '債', + '興', + '源', + '儀', + '創', + '障', + '継', + '筋', + '狙', + '闘', + '葬', + '避', + '司', + '康', + '善', + '逮', + '迫', + '惑', + '崩', + '紀', + '聴', + '脱', + '級', + '博', + '締', + '救', + '執', + '房', + '撤', + '削', + '密', + '措', + '志', + '載', + '陣', + '我', + '為', + '抑', + '幕', + '染', + '奈', + '傷', + '択', + '秀', + '徴', + '弾', + '償', + '功', + '拠', + '秘', + '拒', + '刑', + '塚', + '致', + '繰', + '尾', + '描', + '鈴', + '盤', + '項', + '喪', + '伴', + '養', + '懸', + '街', + '契', + '掲', + '躍', + '棄', + '邸', + '縮', + '還', + '属', + '慮', + '枠', + '恵', + '露', + '沖', + '緩', + '節', + '需', + '射', + '購', + '揮', + '充', + '貢', + '鹿', + '却', + '端', + '賃', + '獲', + '郡', + '併', + '徹', + '貴', + '埼', + '衝', + '焦', + '奪', + '災', + '浦', + '析', + '譲', + '称', + '納', + '樹', + '挑', + '誘', + '紛', + '至', + '宗', + '促', + '慎', + '控', + '智', + '握', + '宙', + '俊', + '銭', + '渋', + '銃', + '操', + '携', + '診', + '託', + '撮', + '誕', + '侵', + '括', + '謝', + '孝', + '駆', + '透', + '津', + '壁', + '稲', + '仮', + '裂', + '敏', + '是', + '排', + '裕', + '堅', + '訳', + '芝', + '綱', + '典', + '賀', + '扱', + '顧', + '弘', + '看', + '訟', + '戒', + '祉', + '誉', + '歓', + '奏', + '勧', + '騒', + '閥', + '甲', + '縄', + '郷', + '揺', + '免', + '既', + '薦', + '隣', + '華', + '範', + '隠', + '徳', + '哲', + '杉', + '里', + '釈', + '己', + '妥', + '威', + '豪', + '熊', + '滞', + '微', + '隆', + '症', + '暫', + '忠', + '倉', + '彦', + '肝', + '喚', + '沿', + '妙', + '唱', + '阿', + '索', + '誠', + '襲', + '懇', + '俳', + '柄', + '驚', + '麻', + '李', + '浩', + '剤', + '瀬', + '趣', + '陥', + '斎', + '貫', + '仙', + '慰', + '序', + '旬', + '兼', + '聖', + '旨', + '即', + '柳', + '舎', + '偽', + '較', + '覇', + '畑', + '詳', + '抵', + '脅', + '茂', + '犠', + '旗', + '距', + '雅', + '飾', + '網', + '竜', + '詩', + '繁', + '翼', + '茨', + '潟', + '敵', + '魅', + '嫌', + '斉', + '敷', + '擁', + '圏', + '酸', + '罰', + '滅', + '礎', + '腐', + '脚', + '菱', + '潮', + '梅', + '尽', + '僕', + '桜', + '滑', + '孤', + '煕', + '炎', + '賠', + '句', + '寿', + '鋼', + '頑', + '鎖', + '彩', + '摩', + '励', + '縦', + '輝', + '蓄', + '軸', + '巡', + '稼', + '瞬', + '砲', + '噴', + '誇', + '祥', + '牲', + '秩', + '帝', + '宏', + '唆', + '阻', + '泰', + '賄', + '撲', + '堀', + '菊', + '絞', + '縁', + '唯', + '膨', + '耐', + '塾', + '漏', + '慶', + '猛', + '芳', + '懲', + '剣', + '幌', + '彰', + '棋', + '丁', + '恒', + '揚', + '冒', + '之', + '曽', + '倫', + '陳', + '憶', + '潜', + '梨', + '仁', + '克', + '岳', + '概', + '拘', + '墓', + '黙', + '須', + '偏', + '雰', + '遇', + '諮', + '狭', + '卓', + '亀', + '糧', + '梶', + '簿', + '炉', + '牧', + '殊', + '殖', + '艦', + '輩', + '穴', + '奇', + '慢', + '鶴', + '謀', + '暖', + '昌', + '拍', + '朗', + '丈', + '寛', + '覆', + '胞', + '泣', + '隔', + '浄', + '没', + '暇', + '肺', + '貞', + '靖', + '鑑', + '飼', + '陰', + '銘', + '随', + '烈', + '尋', + '渕', + '稿', + '丹', + '啓', + '也', + '丘', + '棟', + '壌', + '漫', + '玄', + '粘', + '悟', + '舗', + '妊', + '熟', + '旭', + '恩', + '騰', + '往', + '豆', + '遂', + '狂', + '栃', + '岐', + '陛', + '緯', + '培', + '衰', + '艇', + '屈', + '径', + '淡', + '抽', + '披', + '廷', + '錦', + '准', + '暑', + '磯', + '奨', + '浸', + '剰', + '胆', + '繊', + '駒', + '虚', + '孜', + '霊', + '帳', + '悔', + '諭', + '惨', + '虐', + '翻', + '墜', + '沼', + '据', + '肥', + '徐', + '糖', + '搭', + '盾', + '脈', + '滝', + '軌', + '俵', + '妨', + '盧', + '擦', + '鯨', + '荘', + '諾', + '雷', + '漂', + '懐', + '勘', + '栽', + '拐', + '笠', + '駄', + '添', + '冠', + '斜', + '鏡', + '聡', + '浪', + '亜', + '覧', + '詐', + '壇', + '勲', + '魔', + '酬', + '紫', + '曙', + '紋', + '卸', + '奮', + '趙', + '欄', + '逸', + '涯', + '拓', + '眼', + '獄', + '筑', + '尚', + '阜', + '彫', + '穏', + '顕', + '巧', + '矛', + '垣', + '欺', + '釣', + '萩', + '粧', + '葛', + '粛', + '栗', + '愚', + '嘉', + '遭', + '架', + '篠', + '鬼', + '庶', + '稚', + '菅', + '滋', + '幻', + '煮', + '姫', + '誓', + '把', + '践', + '呈', + '疎', + '仰', + '剛', + '疾', + '征', + '砕', + '謡', + '嫁', + '謙', + '后', + '嘆', + '俣', + '菌', + '鎌', + '巣', + '頻', + '琴', + '班', + '淵', + '棚', + '潔', + '酷', + '宰', + '廊', + '寂', + '辰', + '霞', + '伏', + '柏', + '碁', + '俗', + '漠', + '邪', + '晶', + '辻', + '墨', + '鎮', + '洞', + '履', + '劣', + '那', + '殴', + '娠', + '奉', + '憂', + '朴', + '亭', + '淳', + '荻', + '嶋', + '怪', + '鳩', + '柴', + '酔', + '惜', + '穫', + '佳', + '潤', + '悼', + '乏', + '該', + '赴', + '桑', + '桂', + '髄', + '虎', + '盆', + '晋', + '穂', + '壮', + '堤', + '飢', + '傍', + '疫', + '累', + '痴', + '搬', + '晃', + '癒', + '桐', + '寸', + '郭', + '尿', + '凶', + '吐', + '宴', + '鷹', + '賓', + '虜', + '陶', + '鐘', + '憾', + '畿', + '猪', + '紘', + '磁', + '弥', + '昆', + '粗', + '訂', + '芽', + '尻', + '庄', + '傘', + '敦', + '騎', + '寧', + '循', + '忍', + '磐', + '怠', + '如', + '寮', + '祐', + '鵬', + '鉛', + '珠', + '凝', + '苗', + '獣', + '哀', + '跳', + '匠', + '垂', + '蛇', + '澄', + '縫', + '僧', + '眺', + '唐', + '亘', + '呉', + '凡', + '憩', + '鄭', + '芦', + '龍', + '媛', + '溝', + '恭', + '刈', + '睡', + '錯', + '伯', + '笹', + '穀', + '柿', + '陵', + '霧', + '魂', + '弊', + '釧', + '妃', + '舶', + '餓', + '腎', + '窮', + '掌', + '麗', + '綾', + '臭', + '釜', + '悦', + '刃', + '縛', + '暦', + '宜', + '盲', + '粋', + '辱', + '毅', + '轄', + '猿', + '弦', + '嶌', + '稔', + '窒', + '炊', + '洪', + '摂', + '飽', + '函', + '冗', + '桃', + '狩', + '朱', + '渦', + '紳', + '枢', + '碑', + '鍛', + '刀', + '鼓', + '裸', + '鴨', + '猶', + '塊', + '旋', + '弓', + '幣', + '膜', + '扇', + '脇', + '腸', + '槽', + '鍋', + '慈', + '樋', + '楊', + '伐', + '駿', + '漬', + '糾', + '亮', + '墳', + '坪', + '紺', + '慌', + '娯', + '吾', + '椿', + '舌', + '羅', + '峡', + '俸', + '厘', + '峰', + '圭', + '醸', + '蓮', + '弔', + '乙', + '倶', + '汁', + '尼', + '遍', + '堺', + '衡', + '呆', + '薫', + '瓦', + '猟', + '羊', + '窪', + '款', + '閲', + '雀', + '偵', + '喝', + '敢', + '畠', + '胎', + '酵', + '憤', + '豚', + '遮', + '扉', + '硫', + '赦', + '挫', + '窃', + '泡', + '瑞', + '又', + '慨', + '紡', + '恨', + '肪', + '扶', + '戯', + '伍', + '忌', + '濁', + '奔', + '斗', + '蘭', + '蒲', + '迅', + '肖', + '鉢', + '朽', + '殻', + '享', + '秦', + '茅', + '藩', + '沙', + '輔', + '媒', + '鶏', + '禅', + '嘱', + '胴', + '粕', + '冨', + '迭', + '挿', + '湘', + '嵐', + '椎', + '灘', + '堰', + '獅', + '姜', + '絹', + '陪', + '剖', + '譜', + '郁', + '悠', + '淑', + '帆', + '暁', + '鷲', + '傑', + '楠', + '笛', + '芥', + '其', + '玲', + '奴', + '錠', + '拳', + '翔', + '遷', + '拙', + '侍', + '尺', + '峠', + '篤', + '肇', + '渇', + '榎', + '劉', + '幡', + '諏', + '叔', + '雌', + '亨', + '堪', + '叙', + '酢', + '吟', + '逓', + '痕', + '嶺', + '袖', + '甚', + '喬', + '崔', + '妖', + '琵', + '琶', + '聯', + '蘇', + '闇', + '崇', + '漆', + '岬', + '癖', + '愉', + '寅', + '捉', + '礁', + '乃', + '洲', + '屯', + '樽', + '樺', + '槙', + '薩', + '姻', + '巌', + '淀', + '麹', + '賭', + '擬', + '塀', + '唇', + '睦', + '閑', + '胡', + '幽', + '峻', + '曹', + '哨', + '詠', + '炒', + '屏', + '卑', + '侮', + '鋳', + '抹', + '尉', + '槻', + '隷', + '禍', + '蝶', + '酪', + '茎', + '汎', + '頃', + '帥', + '梁', + '逝', + '汽', + '謎', + '琢', + '箕', + '匿', + '爪', + '芭', + '逗', + '苫', + '鍵', + '襟', + '蛍', + '楢', + '蕉', + '兜', + '寡', + '琉', + '痢', + '庸', + '朋', + '坑', + '姑', + '烏', + '藍', + '僑', + '賊', + '搾', + '奄', + '臼', + '畔', + '遼', + '唄', + '孔', + '橘', + '漱', + '呂', + '桧', + '拷', + '宋', + '嬢', + '苑', + '巽', + '杜', + '渓', + '翁', + '藝', + '廉', + '牙', + '謹', + '瞳', + '湧', + '欣', + '窯', + '褒', + '醜', + '魏', + '篇', + '升', + '此', + '峯', + '殉', + '煩', + '巴', + '禎', + '枕', + '劾', + '菩', + '堕', + '丼', + '租', + '檜', + '稜', + '牟', + '桟', + '榊', + '錫', + '荏', + '惧', + '倭', + '婿', + '慕', + '廟', + '銚', + '斐', + '罷', + '矯', + '某', + '囚', + '魁', + '薮', + '虹', + '鴻', + '泌', + '於', + '赳', + '漸', + '逢', + '凧', + '鵜', + '庵', + '膳', + '蚊', + '葵', + '厄', + '藻', + '萬', + '禄', + '孟', + '鴈', + '狼', + '嫡', + '呪', + '斬', + '尖', + '翫', + '嶽', + '尭', + '怨', + '卿', + '串', + '已', + '嚇', + '巳', + '凸', + '暢', + '腫', + '粟', + '燕', + '韻', + '綴', + '埴', + '霜', + '餅', + '魯', + '硝', + '牡', + '箸', + '勅', + '芹', + '杏', + '迦', + '棺', + '儒', + '鳳', + '馨', + '斑', + '蔭', + '焉', + '慧', + '祇', + '摯', + '愁', + '鷺', + '楼', + '彬', + '袴', + '匡', + '眉', + '苅', + '讃', + '尹', + '欽', + '薪', + '湛', + '堆', + '狐', + '褐', + '鴎', + '瀋', + '挺', + '賜', + '嵯', + '雁', + '佃', + '綜', + '繕', + '狛', + '壷', + '橿', + '栓', + '翠', + '鮎', + '芯', + '蜜', + '播', + '榛', + '凹', + '艶', + '帖', + '桶', + '惣', + '股', + '匂', + '鞍', + '蔦', + '玩', + '萱', + '梯', + '雫', + '絆', + '錬', + '湊', + '蜂', + '隼', + '舵', + '渚', + '珂', + '煥', + '衷', + '逐', + '斥', + '稀', + '癌', + '峨', + '嘘', + '旛', + '篭', + '芙', + '詔', + '皐', + '雛', + '娼', + '篆', + '鮫', + '椅', + '惟', + '牌', + '宕', + '喧', + '佑', + '蒋', + '樟', + '耀', + '黛', + '叱', + '櫛', + '渥', + '挨', + '憧', + '濡', + '槍', + '宵', + '襄', + '妄', + '惇', + '蛋', + '脩', + '笘', + '宍', + '甫', + '酌', + '蚕', + '壕', + '嬉', + '囃', + '蒼', + '餌', + '簗', + '峙', + '粥', + '舘', + '銕', + '鄒', + '蜷', + '暉', + '捧', + '頒', + '只', + '肢', + '箏', + '檀', + '鵠', + '凱', + '彗', + '謄', + '諌', + '樫', + '噂', + '脊', + '牝', + '梓', + '洛', + '醍', + '砦', + '丑', + '笏', + '蕨', + '噺', + '抒', + '嗣', + '隈', + '叶', + '凄', + '汐', + '絢', + '叩', + '嫉', + '朔', + '蔡', + '膝', + '鍾', + '仇', + '伽', + '夷', + '恣', + '瞑', + '畝', + '抄', + '杭', + '寓', + '麺', + '戴', + '爽', + '裾', + '黎', + '惰', + '坐', + '鍼', + '蛮', + '塙', + '冴', + '旺', + '葦', + '礒', + '咸', + '萌', + '饗', + '歪', + '冥', + '偲', + '壱', + '瑠', + '韮', + '漕', + '杵', + '薔', + '膠', + '允', + '眞', + '蒙', + '蕃', + '呑', + '侯', + '碓', + '茗', + '麓', + '瀕', + '蒔', + '鯉', + '竪', + '弧', + '稽', + '瘤', + '澤', + '溥', + '遥', + '蹴', + '或', + '訃', + '矩', + '厦', + '冤', + '剥', + '舜', + '侠', + '贅', + '杖', + '蓋', + '畏', + '喉', + '汪', + '猷', + '瑛', + '搜', + '曼', + '附', + '彪', + '撚', + '噛', + '卯', + '桝', + '撫', + '喋', + '但', + '溢', + '闊', + '藏', + '浙', + '彭', + '淘', + '剃', + '揃', + '綺', + '徘', + '巷', + '竿', + '蟹', + '芋', + '袁', + '舩', + '拭', + '茜', + '凌', + '頬', + '厨', + '犀', + '簑', + '皓', + '甦', + '洸', + '毬', + '檄', + '姚', + '蛭', + '婆', + '叢', + '椙', + '轟', + '贋', + '洒', + '貰', + '儲', + '緋', + '諜', + '鯛', + '蓼', + '甕', + '喘', + '怜', + '溜', + '邑', + '鉾', + '倣', + '碧', + '燈', + '諦', + '煎', + '瓜', + '緻', + '哺', + '槌', + '啄', + '穣', + '嗜', + '偕', + '罵', + '酉', + '蹄', + '頚', + '胚', + '牢', + '糞', + '悌', + '吊', + '楕', + '鮭', + '乞', + '倹', + '嗅', + '詫', + '鱒', + '蔑', + '轍', + '醤', + '惚', + '廣', + '藁', + '柚', + '舛', + '縞', + '謳', + '杞', + '鱗', + '繭', + '釘', + '弛', + '狸', + '壬', + '硯', +]); + +export const KANJI_BY_JLPT_LEVEL: Record< + 'N5' | 'N4' | 'N3' | 'N2' | 'N1', + Set +> = { + N5: KANJI_N5, + N4: KANJI_N4, + N3: KANJI_N3, + N2: KANJI_N2, + N1: KANJI_N1, +}; diff --git a/features/Achievements/store/useAchievementStore.ts b/features/Achievements/store/useAchievementStore.ts index 55d996cb8..37b3ee0bf 100644 --- a/features/Achievements/store/useAchievementStore.ts +++ b/features/Achievements/store/useAchievementStore.ts @@ -1,5 +1,6 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; +import { KANJI_BY_JLPT_LEVEL } from './kanjiSets'; export type AchievementRarity = | 'common' @@ -1382,19 +1383,101 @@ export interface SessionStats { } const BASIC_HIRAGANA = new Set([ - '\u3042','\u3044','\u3046','\u3048','\u304A','\u304B','\u304D','\u304F','\u3051','\u3053', - '\u3055','\u3057','\u3059','\u305B','\u305D','\u305F','\u3061','\u3064','\u3066','\u3068', - '\u306A','\u306B','\u306C','\u306D','\u306E','\u306F','\u3072','\u3075','\u3078','\u307B', - '\u307E','\u307F','\u3080','\u3081','\u3082','\u3084','\u3086','\u3088','\u3089','\u308A', - '\u308B','\u308C','\u308D','\u308F','\u3092','\u3093', + '\u3042', + '\u3044', + '\u3046', + '\u3048', + '\u304A', + '\u304B', + '\u304D', + '\u304F', + '\u3051', + '\u3053', + '\u3055', + '\u3057', + '\u3059', + '\u305B', + '\u305D', + '\u305F', + '\u3061', + '\u3064', + '\u3066', + '\u3068', + '\u306A', + '\u306B', + '\u306C', + '\u306D', + '\u306E', + '\u306F', + '\u3072', + '\u3075', + '\u3078', + '\u307B', + '\u307E', + '\u307F', + '\u3080', + '\u3081', + '\u3082', + '\u3084', + '\u3086', + '\u3088', + '\u3089', + '\u308A', + '\u308B', + '\u308C', + '\u308D', + '\u308F', + '\u3092', + '\u3093', ]); const BASIC_KATAKANA = new Set([ - '\u30A2','\u30A4','\u30A6','\u30A8','\u30AA','\u30AB','\u30AD','\u30AF','\u30B1','\u30B3', - '\u30B5','\u30B7','\u30B9','\u30BB','\u30BD','\u30BF','\u30C1','\u30C4','\u30C6','\u30C8', - '\u30CA','\u30CB','\u30CC','\u30CD','\u30CE','\u30CF','\u30D2','\u30D5','\u30D8','\u30DB', - '\u30DE','\u30DF','\u30E0','\u30E1','\u30E2','\u30E4','\u30E6','\u30E8','\u30E9','\u30EA', - '\u30EB','\u30EC','\u30ED','\u30EF','\u30F2','\u30F3', + '\u30A2', + '\u30A4', + '\u30A6', + '\u30A8', + '\u30AA', + '\u30AB', + '\u30AD', + '\u30AF', + '\u30B1', + '\u30B3', + '\u30B5', + '\u30B7', + '\u30B9', + '\u30BB', + '\u30BD', + '\u30BF', + '\u30C1', + '\u30C4', + '\u30C6', + '\u30C8', + '\u30CA', + '\u30CB', + '\u30CC', + '\u30CD', + '\u30CE', + '\u30CF', + '\u30D2', + '\u30D5', + '\u30D8', + '\u30DB', + '\u30DE', + '\u30DF', + '\u30E0', + '\u30E1', + '\u30E2', + '\u30E4', + '\u30E6', + '\u30E8', + '\u30E9', + '\u30EA', + '\u30EB', + '\u30EC', + '\u30ED', + '\u30EF', + '\u30F2', + '\u30F3', ]); const isSingleKanji = (value: string) => /^[\u4E00-\u9FFF]$/.test(value); @@ -1484,7 +1567,11 @@ function checkContentMastery( relevantEntries = entries.filter(([key]) => BASIC_KATAKANA.has(key)); if (relevantEntries.length < BASIC_KATAKANA.size) return false; } else if (contentType === 'kanji') { - relevantEntries = entries.filter(([key]) => isSingleKanji(key)); + const jlptLevel = additional?.jlptLevel; + if (!jlptLevel) return false; + const kanjiSet = KANJI_BY_JLPT_LEVEL[jlptLevel]; + relevantEntries = entries.filter(([key]) => kanjiSet.has(key)); + if (relevantEntries.length < kanjiSet.size) return false; } else { relevantEntries = entries; } @@ -2028,4 +2115,3 @@ const useAchievementStore = create()( ); export default useAchievementStore; - diff --git a/package-lock.json b/package-lock.json index 026dc7f4b..88aedb088 100644 --- a/package-lock.json +++ b/package-lock.json @@ -201,7 +201,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -764,7 +763,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -805,7 +803,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -836,7 +833,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1571,7 +1567,6 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", "license": "MIT", - "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "6.7.2" }, @@ -1697,7 +1692,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1720,7 +1714,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1743,7 +1736,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1760,7 +1752,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1777,7 +1768,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1794,7 +1784,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1811,7 +1800,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1828,7 +1816,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1845,7 +1832,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1862,7 +1848,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1879,7 +1864,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1896,7 +1880,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1913,7 +1896,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1936,7 +1918,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1959,7 +1940,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1982,7 +1962,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2005,7 +1984,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2028,7 +2006,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2051,7 +2028,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2074,7 +2050,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2097,7 +2072,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -2117,7 +2091,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2137,7 +2110,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2157,7 +2129,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2591,7 +2562,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -5157,7 +5127,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -5495,7 +5464,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5506,7 +5474,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -5592,7 +5559,6 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -6170,7 +6136,6 @@ "integrity": "sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@testing-library/dom": "^10.4.0", "@testing-library/user-event": "^14.6.1", @@ -6308,7 +6273,6 @@ "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", @@ -6366,7 +6330,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6949,7 +6912,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -8312,7 +8274,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -8382,7 +8343,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8579,7 +8539,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -9939,7 +9898,6 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -10741,7 +10699,6 @@ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -12847,7 +12804,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.5.12.tgz", "integrity": "sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "15.5.12", "@swc/helpers": "0.5.15", @@ -13509,7 +13465,6 @@ "integrity": "sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "playwright-core": "1.58.1" }, @@ -13587,7 +13542,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13676,7 +13630,6 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -13949,7 +13902,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -14004,7 +13956,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -14016,8 +13967,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-markdown": { "version": "10.1.0", @@ -14051,7 +14001,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -14336,8 +14285,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -14638,7 +14586,6 @@ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -15249,7 +15196,6 @@ "integrity": "sha512-kjsJ0hctkTO0ipHiyv1MY39wP4tAyVM7rPQGyVMU1iQ7NYHxthiiCHhFB/szmVjXdJa58fu3ZH5cwENMn8Y5eA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1", @@ -16263,7 +16209,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16598,7 +16543,6 @@ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } @@ -16679,7 +16623,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -16793,7 +16736,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4",