feat: improve export/import UI with modal and integrated success message (#46)

- Replace export checkbox with modal offering 'With Images' or 'Data Only' options
- Replace styled label with proper button for file import
- Replace browser alert() with integrated success banner for import confirmation
- Add i18n translations for new modal texts (EN/DE)

The export modal provides a cleaner UX with clear explanations for each option.
The import success message now displays inline with theme-appropriate styling.
This commit is contained in:
Daniel Volz
2026-01-18 09:37:25 +01:00
committed by GitHub
parent 8d22669bef
commit bb46b26ec6
3 changed files with 111 additions and 32 deletions
+101 -32
View File
@@ -367,6 +367,8 @@ function AppContent() {
const [exporting, setExporting] = useState(false);
const [importing, setImporting] = useState(false);
const [exportIncludeImages, setExportIncludeImages] = useState(true);
const [showExportModal, setShowExportModal] = useState(false);
const [importResult, setImportResult] = useState<{medications: number, doses: number, shares: number} | null>(null);
// User dropdown state (for mobile click-based behavior)
const [userDropdownOpen, setUserDropdownOpen] = useState(false);
@@ -1168,11 +1170,12 @@ function AppContent() {
return;
}
alert(t('exportImport.importSuccess') + "\n" + t('exportImport.importSuccessDetails', {
// Show success message in UI instead of browser alert
setImportResult({
medications: data.imported?.medications || 0,
doses: data.imported?.doseHistory || 0,
shares: data.imported?.shareLinks || 0,
}));
});
// Reload all data
loadMeds();
@@ -2847,33 +2850,42 @@ function AppContent() {
</div>
<div className="setting-section">
<div className="setting-group">
{/* Import Success Message */}
{importResult && (
<div className="success-banner" style={{marginBottom: '16px', padding: '12px 16px', borderRadius: '8px', backgroundColor: 'var(--success-bg)', border: '1px solid var(--success)', color: 'var(--text-primary)'}}>
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start'}}>
<div>
<strong style={{display: 'block', marginBottom: '4px', color: 'var(--success)'}}> {t('exportImport.importSuccess')}</strong>
<span style={{fontSize: '0.9em'}}>{t('exportImport.importSuccessDetails', {
medications: importResult.medications,
doses: importResult.doses,
shares: importResult.shares
})}</span>
</div>
<button
type="button"
onClick={() => setImportResult(null)}
style={{background: 'none', border: 'none', cursor: 'pointer', fontSize: '1.2em', padding: '0', lineHeight: '1', color: 'inherit', opacity: 0.7}}
aria-label="Close"
>×</button>
</div>
</div>
)}
{/* Export */}
<div className="action-card" style={{flexDirection: 'column', alignItems: 'stretch'}}>
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px'}}>
<div className="action-card">
<div className="action-card-content">
<span className="action-card-title">{t('exportImport.exportTitle')}</span>
<span className="action-card-desc">{t('exportImport.exportDesc')}</span>
</div>
<button
type="button"
className="secondary"
onClick={() => setShowExportModal(true)}
disabled={exporting}
>
{exporting ? t('exportImport.exporting') : t('exportImport.export')}
</button>
</div>
<label className="toggle-label" style={{marginBottom: '12px'}}>
<input
type="checkbox"
checked={exportIncludeImages}
onChange={(e) => setExportIncludeImages(e.target.checked)}
/>
<span>{t('exportImport.includeImages')}</span>
<span className="info-tooltip" data-tooltip={t('exportImport.includeImagesHint')}></span>
</label>
<button
type="button"
className="secondary"
onClick={() => handleExport(exportIncludeImages)}
disabled={exporting}
style={{alignSelf: 'flex-end'}}
>
{exporting ? t('exportImport.exporting') : t('exportImport.export')}
</button>
</div>
{/* Import */}
<div className="action-card">
@@ -2881,16 +2893,22 @@ function AppContent() {
<span className="action-card-title">{t('exportImport.importTitle')}</span>
<span className="action-card-desc">{t('exportImport.importDesc')}</span>
</div>
<label className="btn secondary">
<input
type="file"
id="import-file-input"
accept=".json,application/json"
onChange={handleImportFileSelect}
disabled={importing}
style={{display: 'none'}}
/>
<button
type="button"
className="secondary"
onClick={() => document.getElementById('import-file-input')?.click()}
disabled={importing}
>
{importing ? t('exportImport.importing') : t('exportImport.import')}
<input
type="file"
accept=".json,application/json"
onChange={handleImportFileSelect}
disabled={importing}
style={{display: 'none'}}
/>
</label>
</button>
</div>
</div>
</div>
@@ -2936,6 +2954,57 @@ function AppContent() {
</div>
</div>
)}
{/* Export Options Modal */}
{showExportModal && (
<div className="modal-overlay" onClick={() => setShowExportModal(false)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()} style={{maxWidth: "450px"}}>
<button className="modal-close" onClick={() => setShowExportModal(false)}>×</button>
<h2 style={{marginBottom: "16px", paddingRight: "2rem"}}>{t('exportImport.exportOptions')}</h2>
<div style={{display: 'flex', flexDirection: 'column', gap: '12px'}}>
<button
type="button"
className="action-card"
onClick={() => {
setShowExportModal(false);
handleExport(true);
}}
disabled={exporting}
style={{textAlign: 'left', cursor: 'pointer', border: '1px solid var(--border)', borderRadius: '8px'}}
>
<div className="action-card-content" style={{flex: 1}}>
<span className="action-card-title">📦 {t('exportImport.exportWithImages')}</span>
<span className="action-card-desc">{t('exportImport.exportWithImagesDesc')}</span>
</div>
</button>
<button
type="button"
className="action-card"
onClick={() => {
setShowExportModal(false);
handleExport(false);
}}
disabled={exporting}
style={{textAlign: 'left', cursor: 'pointer', border: '1px solid var(--border)', borderRadius: '8px'}}
>
<div className="action-card-content" style={{flex: 1}}>
<span className="action-card-title">📄 {t('exportImport.exportDataOnly')}</span>
<span className="action-card-desc">{t('exportImport.exportDataOnlyDesc')}</span>
</div>
</button>
</div>
<div className="modal-footer" style={{padding: "1rem 0 0 0", borderTop: "none", justifyContent: "flex-end"}}>
<button
type="button"
className="ghost"
onClick={() => setShowExportModal(false)}
>
{t('exportImport.cancelButton')}
</button>
</div>
</div>
</div>
)}
</section>
} />
+5
View File
@@ -374,6 +374,11 @@
"sensitiveWarning": "Benachrichtigungs-URLs können Passwörter enthalten und werden im Klartext gespeichert.",
"includeImages": "Medikamentenbilder einschließen",
"includeImagesHint": "Bilder vergrößern die Datei erheblich. Deaktivieren für kleinere Exports (~50 KB statt mehrere MB).",
"exportOptions": "Export-Optionen",
"exportWithImages": "Mit Bildern",
"exportWithImagesDesc": "Vollständiges Backup mit allen Medikamentenbildern. Größere Datei.",
"exportDataOnly": "Nur Daten",
"exportDataOnlyDesc": "Kompaktes Backup ohne Bilder. Viel kleinere Datei (~50 KB).",
"confirmImport": "Alle Daten ersetzen?",
"confirmImportMessage": "Dies löscht dauerhaft alle deine aktuellen Medikamente, Einnahmehistorie, Einstellungen und Teilen-Links und ersetzt sie durch die importierten Daten.",
"confirmImportWarning": "Diese Aktion kann nicht rückgängig gemacht werden!",
+5
View File
@@ -376,6 +376,11 @@
"sensitiveWarning": "Notification URLs may contain passwords and will be stored in plain text.",
"includeImages": "Include medication images",
"includeImagesHint": "Images significantly increase file size. Uncheck for smaller exports (~50 KB instead of several MB).",
"exportOptions": "Export Options",
"exportWithImages": "With Images",
"exportWithImagesDesc": "Full backup including all medication images. Larger file size.",
"exportDataOnly": "Data Only",
"exportDataOnlyDesc": "Compact backup without images. Much smaller file size (~50 KB).",
"confirmImport": "Replace All Data?",
"confirmImportMessage": "This will permanently delete all your current medications, dose history, settings, and share links, then replace them with the imported data.",
"confirmImportWarning": "This action cannot be undone!",