183 lines
4.5 KiB
TypeScript
183 lines
4.5 KiB
TypeScript
import { create } from 'zustand';
|
|
import type { Word, StudyProgress, StudyStats, DifficultyRating } from '../types/vocabulary';
|
|
import { calculateNextReview } from '../utils/srs';
|
|
import { db, initDatabase, exportData, importData } from '../db/database';
|
|
|
|
interface AppState {
|
|
words: Word[];
|
|
progress: Map<string, StudyProgress>;
|
|
stats: StudyStats;
|
|
currentWordIndex: number;
|
|
isFlipped: boolean;
|
|
isLoading: boolean;
|
|
|
|
// Actions
|
|
init: () => Promise<void>;
|
|
setWords: (words: Word[]) => void;
|
|
addWords: (words: Word[]) => Promise<void>;
|
|
flipCard: () => void;
|
|
rateWord: (rating: DifficultyRating) => Promise<void>;
|
|
nextWord: () => void;
|
|
getCurrentWord: () => Word | null;
|
|
getDueWords: () => Word[];
|
|
exportData: () => Promise<any>;
|
|
importData: (data: any) => Promise<void>;
|
|
speak: (text: string) => void;
|
|
}
|
|
|
|
export const useAppStore = create<AppState>((set, get) => ({
|
|
words: [],
|
|
progress: new Map(),
|
|
stats: {
|
|
totalWords: 0,
|
|
masteredWords: 0,
|
|
studyingWords: 0,
|
|
streakDays: 0,
|
|
todayStudied: 0,
|
|
todayNewWords: 0,
|
|
},
|
|
currentWordIndex: 0,
|
|
isFlipped: false,
|
|
isLoading: true,
|
|
|
|
init: async () => {
|
|
await initDatabase();
|
|
|
|
// Load words from DB
|
|
const wordsFromDB = await db.words.toArray();
|
|
|
|
// Load progress from DB
|
|
const progressFromDB = await db.progress.toArray();
|
|
const progressMap = new Map<string, StudyProgress>();
|
|
progressFromDB.forEach(p => progressMap.set(p.wordId, p));
|
|
|
|
// Load stats from DB
|
|
const statsFromDB = await db.stats.get('main');
|
|
|
|
set({
|
|
words: wordsFromDB,
|
|
progress: progressMap,
|
|
stats: statsFromDB || {
|
|
totalWords: wordsFromDB.length,
|
|
masteredWords: 0,
|
|
studyingWords: 0,
|
|
streakDays: 0,
|
|
todayStudied: 0,
|
|
todayNewWords: 0,
|
|
},
|
|
isLoading: false,
|
|
});
|
|
},
|
|
|
|
setWords: (words) => set({
|
|
words,
|
|
stats: { ...get().stats, totalWords: words.length }
|
|
}),
|
|
|
|
addWords: async (newWords) => {
|
|
const wordsWithDate = newWords.map(w => ({ ...w, addedAt: new Date() }));
|
|
await db.words.bulkAdd(wordsWithDate);
|
|
|
|
const allWords = await db.words.toArray();
|
|
set({
|
|
words: allWords,
|
|
stats: { ...get().stats, totalWords: allWords.length }
|
|
});
|
|
},
|
|
|
|
flipCard: () => set((state) => ({ isFlipped: !state.isFlipped })),
|
|
|
|
rateWord: async (rating) => {
|
|
const state = get();
|
|
const currentWord = state.getCurrentWord();
|
|
if (!currentWord) return;
|
|
|
|
const existingProgress = state.progress.get(currentWord.id);
|
|
const newProgress = calculateNextReview(
|
|
existingProgress || {
|
|
wordId: currentWord.id,
|
|
interval: 0,
|
|
repetitions: 0,
|
|
easeFactor: 2.5,
|
|
nextReviewDate: new Date(),
|
|
lastStudiedDate: new Date(),
|
|
},
|
|
rating
|
|
);
|
|
|
|
// Save to IndexedDB
|
|
await db.progress.put(newProgress);
|
|
|
|
const newProgressMap = new Map(state.progress);
|
|
newProgressMap.set(currentWord.id, newProgress);
|
|
|
|
const newStats = {
|
|
...state.stats,
|
|
todayStudied: state.stats.todayStudied + 1,
|
|
};
|
|
|
|
await db.stats.put({ ...newStats, id: 'main' });
|
|
|
|
set({
|
|
progress: newProgressMap,
|
|
isFlipped: false,
|
|
stats: newStats,
|
|
});
|
|
|
|
setTimeout(() => get().nextWord(), 300);
|
|
},
|
|
|
|
nextWord: () => {
|
|
const state = get();
|
|
const dueWords = state.getDueWords();
|
|
|
|
if (dueWords.length === 0) {
|
|
set({ currentWordIndex: -1 });
|
|
return;
|
|
}
|
|
|
|
const nextIndex = state.words.findIndex(w => w.id === dueWords[0].id);
|
|
set({
|
|
currentWordIndex: nextIndex,
|
|
isFlipped: false
|
|
});
|
|
},
|
|
|
|
getCurrentWord: () => {
|
|
const state = get();
|
|
if (state.currentWordIndex < 0 || state.currentWordIndex >= state.words.length) {
|
|
return null;
|
|
}
|
|
return state.words[state.currentWordIndex];
|
|
},
|
|
|
|
getDueWords: () => {
|
|
const state = get();
|
|
const now = new Date();
|
|
|
|
return state.words.filter((word) => {
|
|
const progress = state.progress.get(word.id);
|
|
if (!progress) return true;
|
|
return new Date(progress.nextReviewDate) <= now;
|
|
});
|
|
},
|
|
|
|
exportData: async () => {
|
|
return await exportData();
|
|
},
|
|
|
|
importData: async (data) => {
|
|
await importData(data);
|
|
await get().init();
|
|
},
|
|
|
|
speak: (text: string) => {
|
|
if ('speechSynthesis' in window) {
|
|
const utterance = new SpeechSynthesisUtterance(text);
|
|
utterance.lang = 'fr-FR';
|
|
utterance.rate = 0.8;
|
|
window.speechSynthesis.speak(utterance);
|
|
}
|
|
},
|
|
}));
|