feat: build Learning Planner evidence project
This commit is contained in:
68
src/storage.ts
Normal file
68
src/storage.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
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))
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user