learning_ai_clock/web/src/lib/export.ts

201 lines
5.3 KiB
TypeScript

// ── 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<string> {
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);
});
}