Files
learning-planner/src/storage.ts

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))
},
}