Files
natreex d4765e6576 Add foundational components and logic for migration, UI design, and input handling
- Introduced foundational UI components (`Badge`, `LockCard`, `SectionHeader`, `AvatarIcon`, etc.) for flexible layouts and consistent design.
- Added migration support with the `MigrationModal` component and backend integration for exporting/importing data between Electron and Tauri.
- Extended form components with `TextAreaInput`, `OrderInput`, `ToggleField`, and `ToolbarSelect` for improved input handling.
- Updated `ScribeShell` with migration popup logic to prompt users for data migration.
- Integrated `AlertStack` for better alert handling and notification management.
- Enhanced Rust/Tauri services with migration command implementations.
- Added translations and styles for new components.
2026-04-05 12:52:54 -04:00

177 lines
7.4 KiB
TypeScript

import React, {useState} from 'react';
import {ArrowRightLeft, CheckCircle, AlertTriangle, FolderOpen, Loader2} from 'lucide-react';
import Modal from '@/components/ui/Modal';
import Button from '@/components/ui/Button';
import {useTranslations} from '@/lib/i18n';
import * as tauri from '@/lib/tauri';
interface MigrationModalProps {
onClose: () => void;
onSuccess: () => void;
}
type MigrationStep = 'intro' | 'select' | 'importing' | 'success' | 'error';
export default function MigrationModal({onClose, onSuccess}: MigrationModalProps) {
const t = useTranslations();
const [step, setStep] = useState<MigrationStep>('intro');
const [filePath, setFilePath] = useState<string>('');
const [errorMsg, setErrorMsg] = useState<string>('');
const [migratedUserId, setMigratedUserId] = useState<string>('');
async function handleCheck(): Promise<void> {
if (!filePath.trim()) return;
try {
const result: tauri.MigrationCheckResult = await tauri.checkElectronMigration(filePath.trim());
if (!result.found) {
setErrorMsg(t('migration.fileNotFound'));
setStep('error');
return;
}
if (!result.hasDb) {
setErrorMsg(t('migration.dbNotFound'));
setStep('error');
return;
}
await handleImport();
} catch (e: unknown) {
setErrorMsg(e instanceof Error ? e.message : String(e));
setStep('error');
}
}
async function handleImport(): Promise<void> {
setStep('importing');
try {
const result: tauri.MigrationResult = await tauri.importFromElectron(filePath.trim());
if (result.success) {
setMigratedUserId(result.userId || '');
setStep('success');
} else {
setErrorMsg(result.error || t('migration.importFailed'));
setStep('error');
}
} catch (e: unknown) {
setErrorMsg(e instanceof Error ? e.message : String(e));
setStep('error');
}
}
function handleSuccessClose(): void {
localStorage.setItem('electron_migration_done', 'true');
onSuccess();
}
function handleDismiss(): void {
localStorage.setItem('electron_migration_dismissed', 'true');
onClose();
}
return (
<Modal
title={t('migration.title')}
icon={ArrowRightLeft}
size="md"
onClose={onClose}
>
{step === 'intro' && (
<div className="space-y-4">
<p className="text-muted leading-relaxed">{t('migration.introText')}</p>
<div className="bg-tertiary rounded-xl p-4 space-y-2">
<p className="text-sm font-semibold text-text-primary">{t('migration.steps')}</p>
<ol className="list-decimal list-inside text-sm text-muted space-y-1">
<li>{t('migration.step1')}</li>
<li>{t('migration.step2')}</li>
<li>{t('migration.step3')}</li>
</ol>
</div>
<div className="flex justify-end gap-3 pt-2">
<Button variant="secondary" onClick={handleDismiss}>
{t('migration.later')}
</Button>
<Button variant="primary" icon={FolderOpen} onClick={function (): void {
setStep('select');
}}>
{t('migration.haveFile')}
</Button>
</div>
</div>
)}
{step === 'select' && (
<div className="space-y-4">
<p className="text-muted text-sm">{t('migration.selectText')}</p>
<div className="space-y-2">
<label className="text-sm font-medium text-text-primary">
{t('migration.filePath')}
</label>
<input
type="text"
value={filePath}
onChange={function (e: React.ChangeEvent<HTMLInputElement>): void {
setFilePath(e.target.value);
}}
placeholder="/Users/.../Desktop/eritors-migration.json"
className="w-full px-4 py-2.5 rounded-xl bg-tertiary border border-secondary text-text-primary text-sm placeholder:text-muted/50 focus:outline-none focus:border-primary"
/>
</div>
<div className="flex justify-end gap-3 pt-2">
<Button variant="secondary" onClick={function (): void {
setStep('intro');
}}>
{t('migration.back')}
</Button>
<Button variant="primary" onClick={handleCheck} disabled={!filePath.trim()}>
{t('migration.import')}
</Button>
</div>
</div>
)}
{step === 'importing' && (
<div className="flex flex-col items-center py-8 space-y-4">
<Loader2 className="w-10 h-10 text-primary animate-spin" strokeWidth={1.75}/>
<p className="text-muted">{t('migration.importing')}</p>
</div>
)}
{step === 'success' && (
<div className="space-y-4">
<div className="flex flex-col items-center py-6 space-y-3">
<CheckCircle className="w-12 h-12 text-success" strokeWidth={1.75}/>
<p className="text-text-primary font-semibold text-lg">{t('migration.successTitle')}</p>
<p className="text-muted text-sm text-center">{t('migration.successText')}</p>
</div>
<div className="flex justify-end pt-2">
<Button variant="primary" onClick={handleSuccessClose}>
{t('migration.done')}
</Button>
</div>
</div>
)}
{step === 'error' && (
<div className="space-y-4">
<div className="flex flex-col items-center py-6 space-y-3">
<AlertTriangle className="w-12 h-12 text-error" strokeWidth={1.75}/>
<p className="text-text-primary font-semibold">{t('migration.errorTitle')}</p>
<p className="text-muted text-sm text-center">{errorMsg}</p>
</div>
<div className="flex justify-end gap-3 pt-2">
<Button variant="secondary" onClick={onClose}>
{t('migration.close')}
</Button>
<Button variant="primary" onClick={function (): void {
setStep('select');
setErrorMsg('');
}}>
{t('migration.retry')}
</Button>
</div>
</div>
)}
</Modal>
);
}