import { useState } from 'react'; import type { ChangeEvent } from 'react'; import type { Word } from '../types/vocabulary'; export type ImportPayload = Word[] | { words: Word[]; progress?: unknown[]; stats?: unknown }; interface ImportModalProps { isOpen: boolean; onClose: () => void; onImport: (payload: ImportPayload) => Promise | void; } const normalizeWord = (input: Partial, index: number): Word | null => { const french = input.french?.trim(); const english = input.english?.trim(); if (!french || !english) { return null; } return { id: input.id || `imported-${Date.now()}-${index}`, french, english, pronunciation: input.pronunciation?.trim() || '', ttsText: input.ttsText?.trim() || french, chinese: input.chinese?.trim() || '', partOfSpeech: input.partOfSpeech?.trim() || '', example: input.example?.trim() || '', exampleTranslation: input.exampleTranslation?.trim() || '', examplePronunciation: input.examplePronunciation?.trim() || '', notes: input.notes?.trim() || '', audioUrl: input.audioUrl?.trim() || '', category: input.category?.trim() || 'General', difficulty: input.difficulty || 'beginner', tags: input.tags || [], }; }; const parseCsvLine = (line: string) => line.split(',').map((part) => part.trim().replace(/^"|"$/g, '')); const parseTags = (value: string | undefined) => value ?.split(/[|;]+/) .map((item) => item.trim()) .filter(Boolean) || []; export function ImportModal({ isOpen, onClose, onImport }: ImportModalProps) { const [content, setContent] = useState(''); const [error, setError] = useState(''); const [isImporting, setIsImporting] = useState(false); if (!isOpen) return null; const finishImport = async (payload: ImportPayload) => { setError(''); setIsImporting(true); try { await onImport(payload); setContent(''); onClose(); } catch (err) { setError(err instanceof Error ? err.message : 'Import failed. Please check the file format.'); } finally { setIsImporting(false); } }; const parseContent = async (text: string) => { try { const json = JSON.parse(text); if (Array.isArray(json)) { const words = json .map((item, index) => normalizeWord(item as Partial, index)) .filter(Boolean) as Word[]; if (words.length === 0) { throw new Error('The JSON word list is empty.'); } await finishImport(words); return; } if (json && typeof json === 'object' && Array.isArray((json as { words?: unknown[] }).words)) { const backup = json as { words: Partial[]; progress?: unknown[]; stats?: unknown }; const words = backup.words .map((item, index) => normalizeWord(item, index)) .filter(Boolean) as Word[]; if (words.length === 0) { throw new Error('The backup does not contain any valid words.'); } await finishImport({ words, progress: Array.isArray(backup.progress) ? backup.progress : undefined, stats: backup.stats, }); return; } throw new Error('Unsupported JSON structure.'); } catch (jsonError) { const lines = text .trim() .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean); if (lines.length === 0) { throw jsonError instanceof Error ? jsonError : new Error('There is no importable content.'); } const header = parseCsvLine(lines[0]).map((value) => value.toLowerCase()); const hasHeader = header.includes('french') || header.includes('english'); const dataLines = hasHeader ? lines.slice(1) : lines; const words = dataLines .map((line, index) => { const parts = parseCsvLine(line); if (hasHeader) { return normalizeWord( { french: parts[header.indexOf('french')] || '', english: parts[header.indexOf('english')] || '', chinese: parts[header.indexOf('chinese')] || '', pronunciation: parts[header.indexOf('pronunciation')] || '', ttsText: parts[header.indexOf('ttstext')] || '', partOfSpeech: parts[header.indexOf('partofspeech')] || '', category: parts[header.indexOf('category')] || 'General', example: parts[header.indexOf('example')] || '', exampleTranslation: parts[header.indexOf('exampletranslation')] || '', examplePronunciation: parts[header.indexOf('examplepronunciation')] || '', notes: parts[header.indexOf('notes')] || '', audioUrl: parts[header.indexOf('audiourl')] || '', difficulty: (parts[header.indexOf('difficulty')] as Word['difficulty']) || 'beginner', tags: parseTags(parts[header.indexOf('tags')]), }, index, ); } return normalizeWord( { french: parts[0] || '', english: parts[1] || '', pronunciation: parts[2] || '', category: parts[3] || 'General', difficulty: (parts[4] as Word['difficulty']) || 'beginner', chinese: parts[5] || '', example: parts[6] || '', exampleTranslation: parts[7] || '', tags: parseTags(parts[8]), }, index, ); }) .filter(Boolean) as Word[]; if (words.length === 0) { throw jsonError instanceof Error ? jsonError : new Error('Unable to parse the provided content.'); } await finishImport(words); } }; const handleFileUpload = async (event: ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = async (loadEvent) => { const text = String(loadEvent.target?.result || ''); setContent(text); await parseContent(text); }; reader.readAsText(file); }; const handleSubmit = async () => { if (!content.trim()) { setError('Please paste content or choose a file first.'); return; } await parseContent(content); }; return (

Import vocabulary

Supports CSV, JSON word lists, and full backup files with words, progress, and stats.

or paste content