69 lines
2.7 KiB
TypeScript
69 lines
2.7 KiB
TypeScript
import { createSampleData } from './sampleData'
|
|
import type { AppStateV1, LearningGoal, LearningTask, LoadResult, Priority, TaskStatus } from './types'
|
|
|
|
export const STORAGE_KEY = 'learning-planner.state'
|
|
|
|
const priorities: Priority[] = ['low', 'medium', 'high']
|
|
const statuses: TaskStatus[] = ['todo', 'in-progress', 'done']
|
|
|
|
function isString(value: unknown): value is string {
|
|
return typeof value === 'string'
|
|
}
|
|
|
|
function isGoal(value: unknown): value is LearningGoal {
|
|
if (!value || typeof value !== 'object') return false
|
|
const goal = value as Record<string, unknown>
|
|
return ['id', 'title', 'description', 'category', 'targetDate', 'color', 'createdAt'].every((key) => isString(goal[key]))
|
|
}
|
|
|
|
function isTask(value: unknown): value is LearningTask {
|
|
if (!value || typeof value !== 'object') return false
|
|
const task = value as Record<string, unknown>
|
|
return (
|
|
['id', 'goalId', 'title', 'notes', 'dueDate', 'createdAt'].every((key) => isString(task[key])) &&
|
|
priorities.includes(task.priority as Priority) &&
|
|
statuses.includes(task.status as TaskStatus) &&
|
|
typeof task.estimatedMinutes === 'number' &&
|
|
Number.isFinite(task.estimatedMinutes) &&
|
|
task.estimatedMinutes >= 0
|
|
)
|
|
}
|
|
|
|
export function parseState(value: unknown): AppStateV1 {
|
|
if (!value || typeof value !== 'object') throw new Error('The backup must contain an object.')
|
|
const state = value as Record<string, unknown>
|
|
if (state.version !== 1) throw new Error('This backup version is not supported.')
|
|
if (!Array.isArray(state.goals) || !state.goals.every(isGoal)) throw new Error('The backup contains invalid goals.')
|
|
if (!Array.isArray(state.tasks) || !state.tasks.every(isTask)) throw new Error('The backup contains invalid tasks.')
|
|
const goalIds = new Set(state.goals.map((goal) => goal.id))
|
|
if (state.tasks.some((task) => !goalIds.has(task.goalId))) throw new Error('A task refers to a goal that does not exist.')
|
|
return state as unknown as AppStateV1
|
|
}
|
|
|
|
export const storageAdapter = {
|
|
load(storage: Pick<Storage, 'getItem'> = localStorage): LoadResult {
|
|
try {
|
|
const raw = storage.getItem(STORAGE_KEY)
|
|
if (!raw) return { state: createSampleData() }
|
|
return { state: parseState(JSON.parse(raw)) }
|
|
} catch (error) {
|
|
return {
|
|
state: createSampleData(),
|
|
warning: `Saved data could not be read. Sample data was restored. ${error instanceof Error ? error.message : ''}`.trim(),
|
|
}
|
|
}
|
|
},
|
|
|
|
save(state: AppStateV1, storage: Pick<Storage, 'setItem'> = localStorage): void {
|
|
storage.setItem(STORAGE_KEY, JSON.stringify(parseState(state)))
|
|
},
|
|
|
|
export(state: AppStateV1): string {
|
|
return JSON.stringify(parseState(state), null, 2)
|
|
},
|
|
|
|
import(raw: string): AppStateV1 {
|
|
return parseState(JSON.parse(raw))
|
|
},
|
|
}
|