feat: implement Phase 4 (CSV export, share, sync indicators, OS themes) and add dev starter script
This commit is contained in:
@@ -4,8 +4,9 @@ import { db } from '../services/db.js'
|
||||
import { getActiveMasterKey } from '../services/auth.js'
|
||||
import { decryptJson, encryptJson } from '../services/crypto.js'
|
||||
import { syncLogbook } from '../services/sync.js'
|
||||
import { downloadCsv, shareCsv } from '../services/csvExport.js'
|
||||
import LogEntryEditor from './LogEntryEditor.tsx'
|
||||
import { FileText, Plus, Trash2, ChevronRight, Calendar } from 'lucide-react'
|
||||
import { FileText, Plus, Trash2, ChevronRight, Calendar, Download, Share2 } from 'lucide-react'
|
||||
|
||||
interface LogEntriesListProps {
|
||||
logbookId: string
|
||||
@@ -25,6 +26,7 @@ export default function LogEntriesList({ logbookId }: LogEntriesListProps) {
|
||||
const [entries, setEntries] = useState<DecryptedEntryItem[]>([])
|
||||
const [selectedEntryId, setSelectedEntryId] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [exporting, setExporting] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -74,6 +76,40 @@ export default function LogEntriesList({ logbookId }: LogEntriesListProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const handleDownloadCsv = async () => {
|
||||
setExporting(true)
|
||||
setError(null)
|
||||
try {
|
||||
const title = localStorage.getItem('active_logbook_title') || 'Logbook'
|
||||
await downloadCsv(logbookId, title)
|
||||
} catch (err: any) {
|
||||
console.error('Failed to download CSV:', err)
|
||||
setError(err.message || 'Failed to generate CSV export.')
|
||||
} finally {
|
||||
setExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleShareCsv = async () => {
|
||||
setExporting(true)
|
||||
setError(null)
|
||||
try {
|
||||
const title = localStorage.getItem('active_logbook_title') || 'Logbook'
|
||||
await shareCsv(logbookId, title)
|
||||
} catch (err: any) {
|
||||
if (err.message === 'share_unsupported') {
|
||||
const title = localStorage.getItem('active_logbook_title') || 'Logbook'
|
||||
await downloadCsv(logbookId, title)
|
||||
setError(t('logs.share_unsupported'))
|
||||
} else {
|
||||
console.error('Failed to share CSV:', err)
|
||||
setError(err.message || 'Failed to share CSV export.')
|
||||
}
|
||||
} finally {
|
||||
setExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreate = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
@@ -187,10 +223,22 @@ export default function LogEntriesList({ logbookId }: LogEntriesListProps) {
|
||||
<Calendar size={24} className="form-icon" />
|
||||
<h2>{t('logs.title')}</h2>
|
||||
</div>
|
||||
<button className="btn primary" onClick={handleCreate} style={{ width: 'auto', padding: '8px 16px' }}>
|
||||
<Plus size={16} />
|
||||
{t('logs.new_entry')}
|
||||
</button>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<button className="btn secondary" onClick={handleDownloadCsv} disabled={loading || exporting || entries.length === 0} style={{ width: 'auto', padding: '8px 16px' }} title={t('logs.export_csv')}>
|
||||
<Download size={16} />
|
||||
<span className="hide-mobile">{exporting ? t('logs.exporting') : t('logs.export_csv')}</span>
|
||||
</button>
|
||||
|
||||
<button className="btn secondary" onClick={handleShareCsv} disabled={loading || exporting || entries.length === 0} style={{ width: 'auto', padding: '8px 16px' }} title={t('logs.share_csv')}>
|
||||
<Share2 size={16} />
|
||||
<span className="hide-mobile">{t('logs.share_csv')}</span>
|
||||
</button>
|
||||
|
||||
<button className="btn primary" onClick={handleCreate} disabled={loading || exporting} style={{ width: 'auto', padding: '8px 16px' }}>
|
||||
<Plus size={16} />
|
||||
{t('logs.new_entry')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <div className="auth-error mb-4">{error}</div>}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Settings, Save, Check } from 'lucide-react'
|
||||
export default function SettingsForm() {
|
||||
const { t } = useTranslation()
|
||||
const [apiKey, setApiKey] = useState(localStorage.getItem('owm_api_key') || '')
|
||||
const [theme, setTheme] = useState(localStorage.getItem('active_theme') || 'auto')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [success, setSuccess] = useState(false)
|
||||
|
||||
@@ -15,6 +16,10 @@ export default function SettingsForm() {
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('owm_api_key', apiKey.trim())
|
||||
localStorage.setItem('active_theme', theme)
|
||||
|
||||
// Notify App of theme change
|
||||
window.dispatchEvent(new Event('theme-changed'))
|
||||
|
||||
setSaving(false)
|
||||
setSuccess(true)
|
||||
@@ -34,6 +39,7 @@ export default function SettingsForm() {
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="vessel-form mt-6">
|
||||
{/* Weather Integration card */}
|
||||
<div className="member-editor-card glass">
|
||||
<h3 style={{ marginTop: 0, marginBottom: '12px', color: '#fbbf24', fontSize: '16px' }}>
|
||||
{t('settings.owm_title')}
|
||||
@@ -58,6 +64,32 @@ export default function SettingsForm() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Theme customization card */}
|
||||
<div className="member-editor-card glass mt-4">
|
||||
<h3 style={{ marginTop: 0, marginBottom: '12px', color: '#fbbf24', fontSize: '16px' }}>
|
||||
{t('settings.theme_title')}
|
||||
</h3>
|
||||
<p style={{ fontSize: '13.5px', color: '#94a3b8', lineHeight: '145%', margin: '0 0 16px 0' }}>
|
||||
{t('settings.theme_label')}
|
||||
</p>
|
||||
|
||||
<div className="input-group">
|
||||
<select
|
||||
id="app-theme"
|
||||
className="input-text"
|
||||
value={theme}
|
||||
onChange={(e) => setTheme(e.target.value)}
|
||||
disabled={saving}
|
||||
style={{ background: 'rgba(11, 12, 16, 0.85)', color: '#f1f5f9' }}
|
||||
>
|
||||
<option value="auto">{t('settings.theme_auto')}</option>
|
||||
<option value="ocean">{t('settings.theme_ocean')}</option>
|
||||
<option value="material">{t('settings.theme_material')}</option>
|
||||
<option value="cupertino">{t('settings.theme_cupertino')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-actions mt-4">
|
||||
{success && (
|
||||
<div className="success-toast">
|
||||
|
||||
Reference in New Issue
Block a user