import React, { useState, useEffect, useRef, useMemo } from 'react' import { useTranslation } from 'react-i18next' import LanguageDropdown from './LanguageDropdown.tsx' import { useSyncIndicator } from '../hooks/useSyncIndicator.js' import { fetchLogbooks, createLogbook, deleteLogbook, updateLogbookTitle, type DecryptedLogbook } from '../services/logbook.js' import { loadLogbookSearchFieldsBatch } from '../services/logbookSearchIndex.js' import { logbookMatchesFilter, type LogbookSearchFields } from '../utils/logbookFilter.js' import LogbookRoleBadge from './LogbookRoleBadge.tsx' import BetaBadge from './BetaBadge.tsx' import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js' import { getErrorMessage } from '../utils/errors.js' import { logoutUser } from '../services/auth.js' import { useDialog } from './ModalDialog.tsx' import { BookOpen, Plus, Trash2, LogOut, RefreshCw, Ship, Wifi, WifiOff, Search, X, CalendarDays, CaseSensitive, ArrowUp, ArrowDown, Upload } from 'lucide-react' import DisclaimerHeaderButton from './DisclaimerHeaderButton.tsx' import FeedbackHeaderButton from './FeedbackHeaderButton.tsx' import ProfileHeaderButton from './ProfileHeaderButton.tsx' import AdminHeaderButton from './AdminHeaderButton.tsx' import LogbookRestorePanel from './LogbookRestorePanel.tsx' interface LogbookDashboardProps { onSelectLogbook: (id: string, title: string) => void onLogout: () => void onOpenProfile: () => void onOpenAdmin?: () => void } type LogbookSortKey = 'name' | 'date' type LogbookSortDirection = 'asc' | 'desc' function sortLogbooks( items: DecryptedLogbook[], sortBy: LogbookSortKey, direction: LogbookSortDirection, locale: string ): DecryptedLogbook[] { const sorted = [...items] sorted.sort((a, b) => { let cmp = 0 if (sortBy === 'name') { cmp = a.title.localeCompare(b.title, locale, { sensitivity: 'base' }) } else { const timeA = a.lastTravelDate ? new Date(a.lastTravelDate).getTime() : new Date(a.updatedAt).getTime() const timeB = b.lastTravelDate ? new Date(b.lastTravelDate).getTime() : new Date(b.updatedAt).getTime() cmp = timeA - timeB } return direction === 'asc' ? cmp : -cmp }) return sorted } export default function LogbookDashboard({ onSelectLogbook, onLogout, onOpenProfile, onOpenAdmin }: LogbookDashboardProps) { const { t, i18n } = useTranslation() const { showConfirm } = useDialog() const [logbooks, setLogbooks] = useState([]) const [newTitle, setNewTitle] = useState('') const [editingLogbookId, setEditingLogbookId] = useState(null) const [editingTitleDraft, setEditingTitleDraft] = useState('') const titleInputRef = useRef(null) const [loading, setLoading] = useState(false) const [refreshing, setRefreshing] = useState(false) const [error, setError] = useState(null) const [filterQuery, setFilterQuery] = useState('') const [searchFieldsByLogbookId, setSearchFieldsByLogbookId] = useState>( () => new Map() ) const [sortBy, setSortBy] = useState('date') const [sortDirection, setSortDirection] = useState('desc') const filterInputRef = useRef(null) const [online, setOnline] = useState(navigator.onLine) const [showRestore, setShowRestore] = useState(false) const { pendingCount, showSpinner, showPendingWarning, connStatusClassName } = useSyncIndicator() // Listen to connectivity changes useEffect(() => { const handleOnline = () => setOnline(true) const handleOffline = () => setOnline(false) window.addEventListener('online', handleOnline) window.addEventListener('offline', handleOffline) return () => { window.removeEventListener('online', handleOnline) window.removeEventListener('offline', handleOffline) } }, []) // Load logbooks on mount useEffect(() => { loadLogbooks() }, []) useEffect(() => { const ids = logbooks.map((lb) => lb.id) if (ids.length === 0) { setSearchFieldsByLogbookId(new Map()) return } let cancelled = false void loadLogbookSearchFieldsBatch(ids).then((index) => { if (!cancelled) setSearchFieldsByLogbookId(index) }) return () => { cancelled = true } }, [logbooks]) const loadLogbooks = async (isRefresh = false) => { if (isRefresh) setRefreshing(true) else setLoading(true) setError(null) try { const data = await fetchLogbooks() setLogbooks(data) } catch (err: unknown) { setError(getErrorMessage(err, t('errors.load_failed'))) } finally { setLoading(false) setRefreshing(false) } } const handleCreate = async (e: React.FormEvent) => { e.preventDefault() if (!newTitle.trim()) return setLoading(true) setError(null) try { const created = await createLogbook(newTitle.trim()) setLogbooks((prev) => [created, ...prev]) setNewTitle('') trackPlausibleEvent(PlausibleEvents.LOGBOOK_CREATED) } catch (err: unknown) { setError(getErrorMessage(err, t('errors.save_failed'))) } finally { setLoading(false) } } const handleDelete = async (id: string, e: React.MouseEvent) => { e.stopPropagation() // Prevent selecting the logbook when clicking delete if (await showConfirm(t('dashboard.delete_confirm'), t('dashboard.delete_btn'), t('logs.confirm_yes'), t('logs.confirm_no'))) { setLoading(true) setError(null) try { await deleteLogbook(id) setLogbooks((prev) => prev.filter((lb) => lb.id !== id)) } catch (err: any) { setError(getErrorMessage(err, t('errors.delete_failed'))) } finally { setLoading(false) } } } useEffect(() => { if (editingLogbookId) { titleInputRef.current?.focus() titleInputRef.current?.select() } }, [editingLogbookId]) const startTitleEdit = (lb: DecryptedLogbook, e: React.MouseEvent) => { e.stopPropagation() setEditingLogbookId(lb.id) setEditingTitleDraft(lb.title) } const cancelTitleEdit = () => { setEditingLogbookId(null) setEditingTitleDraft('') } const commitTitleEdit = async (id: string) => { if (editingLogbookId !== id) return const lb = logbooks.find((item) => item.id === id) const trimmedTitle = editingTitleDraft.trim() cancelTitleEdit() if (!lb || !trimmedTitle || trimmedTitle === lb.title.trim()) return setLoading(true) setError(null) try { await updateLogbookTitle(id, trimmedTitle) setLogbooks((prev) => prev.map((item) => item.id === id ? { ...item, title: trimmedTitle, updatedAt: new Date().toISOString() } : item ) ) } catch (err: any) { setError(getErrorMessage(err, t('errors.save_failed'))) } finally { setLoading(false) } } const handleLogout = () => { void logoutUser() onLogout() } const ownedLogbooks = logbooks.filter((lb) => !lb.isShared) const sharedLogbooks = logbooks.filter((lb) => lb.isShared) const filterActive = filterQuery.trim().length > 0 const filteredOwnedLogbooks = useMemo( () => ownedLogbooks.filter((lb) => logbookMatchesFilter(lb, filterQuery, i18n.language, searchFieldsByLogbookId.get(lb.id)) ), [ownedLogbooks, filterQuery, i18n.language, searchFieldsByLogbookId] ) const filteredSharedLogbooks = useMemo( () => sharedLogbooks.filter((lb) => logbookMatchesFilter(lb, filterQuery, i18n.language, searchFieldsByLogbookId.get(lb.id)) ), [sharedLogbooks, filterQuery, i18n.language, searchFieldsByLogbookId] ) const sortedOwnedLogbooks = useMemo( () => sortLogbooks(filteredOwnedLogbooks, sortBy, sortDirection, i18n.language), [filteredOwnedLogbooks, sortBy, sortDirection, i18n.language] ) const sortedSharedLogbooks = useMemo( () => sortLogbooks(filteredSharedLogbooks, sortBy, sortDirection, i18n.language), [filteredSharedLogbooks, sortBy, sortDirection, i18n.language] ) const filteredLogbookCount = sortedOwnedLogbooks.length + sortedSharedLogbooks.length const renderLogbookCard = (lb: DecryptedLogbook) => { const isEditingTitle = editingLogbookId === lb.id return (
{!isEditingTitle && (
)} ) } const renderLogbookSection = ( title: string, items: DecryptedLogbook[], hint?: string ) => (

{title}

{hint &&

{hint}

}
{items.map(renderLogbookCard)}
) return (
{/* Premium Dashboard Header */}

{t('app.name')}

{t('app.tagline')}

{/* Connection Indicator */}
0 ? 'Pending Sync' : 'Synced' : 'Offline' } > {online ? ( showSpinner ? ( <> {t('sync.status_syncing')} ) : showPendingWarning ? ( <> {t('sync.status_unsynced')} ({pendingCount}) ) : ( <> {t('sync.status_synced')} ) ) : ( <> {t('sync.status_offline')} )}
{onOpenAdmin && } {/* Logout */}
{/* Main Dashboard Layout */}
{/* Left Side: Create form */}

{t('dashboard.create_btn')}

setNewTitle(e.target.value)} disabled={loading} required />
{error &&
{error}
}
{showRestore && (
)}
{/* Right Side: Logbooks list */}

{t('dashboard.title')}

{loading && !refreshing ? (
{t('dashboard.loading')}
) : logbooks.length === 0 ? (
{t('dashboard.no_logbooks')}
) : ( <>
{filterActive && (

{t('dashboard.filter_results', { count: filteredLogbookCount })}

)}
{t('dashboard.sort_label')}
{filterActive && filteredLogbookCount === 0 ? (
{t('dashboard.filter_no_results')}
) : (
{sortedOwnedLogbooks.length > 0 && renderLogbookSection( sortedSharedLogbooks.length > 0 ? t('dashboard.section_owned') : t('dashboard.title'), sortedOwnedLogbooks )} {sortedSharedLogbooks.length > 0 && renderLogbookSection( t('dashboard.section_shared'), sortedSharedLogbooks, t('dashboard.section_shared_hint') )}
)} )}
) }