// ── Timer & Routine Export / Import ────────────────────────── // Export all timers + routines as JSON, import from JSON file import type { Timer } from './timer-engine'; import type { Routine } from './routines'; // ── Types ───────────────────────────────────────────────────── export interface ExportData { version: 1; exportedAt: number; app: 'chronomind'; timers: Timer[]; routines?: Routine[]; } export interface ImportResult { success: boolean; imported: number; skipped: number; errors: string[]; } // ── Export ───────────────────────────────────────────────────── /** * Export timers as a downloadable JSON blob. */ export function exportTimers(timers: Timer[]): ExportData { return { version: 1, exportedAt: Date.now(), app: 'chronomind', timers: timers.map((t) => ({ ...t })), // shallow clone }; } /** * Export timers + routines as a downloadable JSON blob. */ export function exportAll(timers: Timer[], routines: Routine[]): ExportData { return { version: 1, exportedAt: Date.now(), app: 'chronomind', timers: timers.map((t) => ({ ...t })), routines: routines.map((r) => ({ ...r, steps: r.steps.map((s) => ({ ...s })) })), }; } /** * Trigger a browser download of the export data. */ export function downloadExport(timers: Timer[]): void { const data = exportTimers(timers); const json = JSON.stringify(data, null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `chronomind-export-${new Date().toISOString().slice(0, 10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } /** * Trigger a browser download of timers + routines. */ export function downloadExportAll(timers: Timer[], routines: Routine[]): void { const data = exportAll(timers, routines); const json = JSON.stringify(data, null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `chronomind-export-${new Date().toISOString().slice(0, 10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // ── Import ──────────────────────────────────────────────────── /** * Validate and parse an import file. */ export function parseImportData(jsonStr: string): ExportData | null { try { const data = JSON.parse(jsonStr); if (!data || data.app !== 'chronomind' || !Array.isArray(data.timers)) { return null; } return data as ExportData; } catch { return null; } } /** * Import timers from parsed export data, deduplicating by ID. */ export function importTimers( exportData: ExportData, existingTimers: Timer[] ): ImportResult { const errors: string[] = []; let imported = 0; let skipped = 0; const existingIds = new Set(existingTimers.map((t) => t.id)); const newTimers: Timer[] = []; for (const timer of exportData.timers) { // Basic validation if (!timer.id || !timer.type || !timer.label) { errors.push(`Invalid timer: missing required fields (id, type, or label)`); skipped++; continue; } if (existingIds.has(timer.id)) { skipped++; continue; } // Validate required numeric fields if (typeof timer.createdAt !== 'number' || typeof timer.targetTime !== 'number') { errors.push(`Timer "${timer.label}": invalid timestamps`); skipped++; continue; } newTimers.push(timer); imported++; } return { success: errors.length === 0, imported, skipped, errors, }; } /** * Import routines from parsed export data, deduplicating by ID. */ export function importRoutines( exportData: ExportData, existingRoutines: Routine[] ): ImportResult { if (!exportData.routines || exportData.routines.length === 0) { return { success: true, imported: 0, skipped: 0, errors: [] }; } const errors: string[] = []; let imported = 0; let skipped = 0; const existingIds = new Set(existingRoutines.map((r) => r.id)); for (const routine of exportData.routines) { if (!routine.id || !routine.name || !Array.isArray(routine.steps)) { errors.push(`Invalid routine: missing required fields (id, name, or steps)`); skipped++; continue; } if (existingIds.has(routine.id)) { skipped++; continue; } imported++; } return { success: errors.length === 0, imported, skipped, errors, }; } /** * Read a File object and return the parsed content as string. */ export function readFileAsText(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result as string); reader.onerror = () => reject(new Error('Failed to read file')); reader.readAsText(file); }); }