diff --git a/client/index.html b/client/index.html
index ceb1063..0cb597d 100644
--- a/client/index.html
+++ b/client/index.html
@@ -7,7 +7,7 @@
-
+
-

+
{t('app.name')}
{t('auth.tagline')}
diff --git a/client/src/components/LogEntriesList.tsx b/client/src/components/LogEntriesList.tsx
index 248d4d9..72e9b58 100644
--- a/client/src/components/LogEntriesList.tsx
+++ b/client/src/components/LogEntriesList.tsx
@@ -275,7 +275,7 @@ export default function LogEntriesList({
readOnly={readOnly}
preloadedEntry={preloadedEntries?.find(entry => (entry.payloadId || entry.id) === selectedEntryId)}
preloadedPhotos={preloadedPhotos}
- preloadedGpsTrack={preloadedGpsTracks?.find(track => track.entryId === selectedEntryId)}
+ preloadedTrack={preloadedGpsTracks?.find(track => track.entryId === selectedEntryId)}
/>
)
}
diff --git a/client/src/components/LogEntryEditor.tsx b/client/src/components/LogEntryEditor.tsx
index e7dc886..5e6447a 100644
--- a/client/src/components/LogEntryEditor.tsx
+++ b/client/src/components/LogEntryEditor.tsx
@@ -6,20 +6,17 @@ import { getLogbookKey } from '../services/logbookKeys.js'
import { encryptJson, decryptJson } from '../services/crypto.js'
import { syncLogbook } from '../services/sync.js'
import { downloadLogbookPagePdf } from '../services/pdfExport.js'
-import { FileText, Save, ChevronLeft, Check, Compass, Plus, Trash2, MapPin, CloudSun, Clock, Download, Navigation } from 'lucide-react'
+import { FileText, Save, ChevronLeft, Check, Compass, Plus, Trash2, MapPin, CloudSun, Clock, Download, Upload } from 'lucide-react'
import PhotoCapture from './PhotoCapture.tsx'
import { useDialog } from './ModalDialog.tsx'
-import L from 'leaflet'
-import 'leaflet/dist/leaflet.css'
import {
- getDecryptedGpsTrack,
- saveUploadedGpsTrack,
- deleteGpsTrack,
+ getDecryptedTrack,
+ saveUploadedTrack,
+ deleteTrack,
downloadTrackFile,
parseTrackFile,
- type GpsWaypoint,
- type SavedGpsTrack
-} from '../services/gpsTracker.js'
+ type SavedTrack
+} from '../services/trackUpload.js'
interface LogEntryEditorProps {
entryId: string
@@ -28,7 +25,7 @@ interface LogEntryEditorProps {
readOnly?: boolean
preloadedEntry?: any
preloadedPhotos?: any[]
- preloadedGpsTrack?: any
+ preloadedTrack?: any
preloadedYacht?: any
}
@@ -58,7 +55,7 @@ export default function LogEntryEditor({
readOnly = false,
preloadedEntry,
preloadedPhotos,
- preloadedGpsTrack,
+ preloadedTrack,
preloadedYacht
}: LogEntryEditorProps) {
const { t, i18n } = useTranslation()
@@ -116,15 +113,12 @@ export default function LogEntryEditor({
const [error, setError] = useState
(null)
const [weatherLoading, setWeatherLoading] = useState(false)
- // GPS Tracking States
- const [savedTrack, setSavedTrack] = useState(null)
+ // Track file upload
+ const [savedTrack, setSavedTrack] = useState(null)
const [dragOver, setDragOver] = useState(false)
const [uploadError, setUploadError] = useState(null)
const fileInputRef = useRef(null)
- const mapContainerRef = useRef(null)
- const mapInstanceRef = useRef(null)
-
// Auto-calculate Freshwater Consumption
useEffect(() => {
const morning = parseFloat(fwMorning) || 0
@@ -236,91 +230,24 @@ export default function LogEntryEditor({
loadEntry()
}, [entryId, preloadedEntry])
- // GPS Track Loader
- const loadGpsTrack = async () => {
- if (readOnly && preloadedGpsTrack) {
- setSavedTrack(preloadedGpsTrack)
+ const loadTrack = async () => {
+ if (readOnly && preloadedTrack) {
+ setSavedTrack(preloadedTrack)
return
}
try {
- const track = await getDecryptedGpsTrack(entryId)
+ const track = await getDecryptedTrack(entryId)
setSavedTrack(track)
} catch (e) {
- console.warn('Failed to load GPS track:', e)
+ console.warn('Failed to load track file:', e)
}
}
useEffect(() => {
- loadGpsTrack()
- }, [entryId, preloadedGpsTrack])
+ loadTrack()
+ }, [entryId, preloadedTrack])
- // Leaflet Map Initialization and Rendering
- useEffect(() => {
- if (!savedTrack || !savedTrack.waypoints || savedTrack.waypoints.length === 0 || !mapContainerRef.current) {
- if (mapInstanceRef.current) {
- mapInstanceRef.current.remove()
- mapInstanceRef.current = null
- }
- return
- }
-
- const startWp = savedTrack.waypoints[0]
- const map = L.map(mapContainerRef.current).setView([startWp.lat, startWp.lng], 13)
- mapInstanceRef.current = map
-
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
- maxZoom: 19,
- attribution: '© OpenStreetMap contributors'
- }).addTo(map)
-
- L.tileLayer('https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', {
- maxZoom: 18,
- attribution: 'Map data © OpenSeaMap contributors'
- }).addTo(map)
-
- const latLngs = savedTrack.waypoints.map((wp) => [wp.lat, wp.lng] as [number, number])
-
- const polyline = L.polyline(latLngs, {
- color: '#fbbf24',
- weight: 4,
- opacity: 0.85
- }).addTo(map)
-
- map.fitBounds(polyline.getBounds(), { padding: [20, 20] })
-
- if (savedTrack.waypoints.length > 0) {
- L.circleMarker(latLngs[0], {
- radius: 8,
- fillColor: '#10b981',
- fillOpacity: 0.9,
- color: '#ffffff',
- weight: 2
- }).addTo(map).bindPopup('Start Position')
-
- if (savedTrack.waypoints.length > 1) {
- L.circleMarker(latLngs[latLngs.length - 1], {
- radius: 8,
- fillColor: '#ef4444',
- fillOpacity: 0.9,
- color: '#ffffff',
- weight: 2
- }).addTo(map).bindPopup('End Position')
- }
- }
-
- setTimeout(() => {
- map.invalidateSize()
- }, 100)
-
- return () => {
- if (mapInstanceRef.current) {
- mapInstanceRef.current.remove()
- mapInstanceRef.current = null
- }
- }
- }, [savedTrack])
-
- // GPX/KML/GeoJSON Upload Handlers
+ // Track file upload handlers
const handleFileUpload = async (file: File) => {
if (readOnly) return
setUploadError(null)
@@ -338,8 +265,8 @@ export default function LogEntryEditor({
throw new Error('No coordinates found in file. Supported formats: GPX, KML, GeoJSON.')
}
- await saveUploadedGpsTrack(logbookId, entryId, text, parsedWps, file.name, fileType)
- await loadGpsTrack()
+ await saveUploadedTrack(logbookId, entryId, text, parsedWps, file.name, fileType)
+ await loadTrack()
} catch (err: any) {
console.error('File parsing failed:', err)
setUploadError(err.message || 'Failed to parse track file.')
@@ -380,7 +307,7 @@ export default function LogEntryEditor({
return
}
try {
- await deleteGpsTrack(logbookId, entryId)
+ await deleteTrack(logbookId, entryId)
setSavedTrack(null)
setUploadError(null)
} catch (err: any) {
@@ -388,30 +315,6 @@ export default function LogEntryEditor({
}
}
- const calculateTrackDistance = (wps: GpsWaypoint[]) => {
- if (wps.length < 2) return 0
- let totalMeters = 0
- for (let i = 1; i < wps.length; i++) {
- const lat1 = wps[i - 1].lat
- const lon1 = wps[i - 1].lng
- const lat2 = wps[i].lat
- const lon2 = wps[i].lng
-
- const R = 6371e3
- const phi1 = (lat1 * Math.PI) / 180
- const phi2 = (lat2 * Math.PI) / 180
- const deltaPhi = ((lat2 - lat1) * Math.PI) / 180
- const deltaLambda = ((lon2 - lon1) * Math.PI) / 180
-
- const a =
- Math.sin(deltaPhi / 2) * Math.sin(deltaPhi / 2) +
- Math.cos(phi1) * Math.cos(phi2) * Math.sin(deltaLambda / 2) * Math.sin(deltaLambda / 2)
- const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
- totalMeters += R * c
- }
- return Number((totalMeters / 1852).toFixed(2))
- }
-
const handleGetGps = () => {
if (readOnly) return
const lookupFallback = async () => {
@@ -1234,17 +1137,16 @@ export default function LogEntryEditor({
)}
- {/* GPS Track Upload & Map Visualization */}
+ {/* Track file upload */}
-
-
{t('logs.gps_tracking_title')}
+
+ {t('logs.track_upload_title')}
{uploadError &&
{uploadError}
}
{!savedTrack ? (
- /* Upload Zone when no track is loaded */
-
+
{t('logs.gps_track_upload_btn')}
{t('logs.gps_track_upload_help')}
) : (
- /* Map and Details when track is loaded */
-
-
-
-
- {savedTrack.filename || 'track'}
-
-
-
- {t('logs.gps_tracking_stat_distance')}: {calculateTrackDistance(savedTrack.waypoints)} sm
-
-
- {t('logs.gps_tracking_stat_waypoints')}: {savedTrack.waypoints.length}
-
-
-
+
+
+
+ {savedTrack.filename || 'track'}
+
+ {savedTrack.fileType.toUpperCase()}
+ {savedTrack.waypoints.length > 0 && (
+ <> · {savedTrack.waypoints.length} {t('logs.track_upload_points')}>
+ )}
+
+
+
+
+ {!readOnly && (
- {!readOnly && (
-
- )}
-
+ )}
-
- {/* Leaflet Map Div */}
-
)}
diff --git a/client/src/components/LogbookDashboard.tsx b/client/src/components/LogbookDashboard.tsx
index 044c57a..76f7792 100644
--- a/client/src/components/LogbookDashboard.tsx
+++ b/client/src/components/LogbookDashboard.tsx
@@ -5,6 +5,7 @@ import { db } from '../services/db.js'
import { fetchLogbooks, createLogbook, deleteLogbook, type DecryptedLogbook } from '../services/logbook.js'
import { logoutUser } from '../services/auth.js'
import { useDialog } from './ModalDialog.tsx'
+import AccountDangerZone from './AccountDangerZone.tsx'
import { BookOpen, Plus, Trash2, LogOut, Languages, RefreshCw, Ship, User, Wifi, WifiOff } from 'lucide-react'
interface LogbookDashboardProps {
@@ -227,6 +228,10 @@ export default function LogbookDashboard({ onSelectLogbook, onLogout }: LogbookD
)}
+
+
)
}
diff --git a/client/src/components/SettingsForm.tsx b/client/src/components/SettingsForm.tsx
index 3979bc4..70498a5 100644
--- a/client/src/components/SettingsForm.tsx
+++ b/client/src/components/SettingsForm.tsx
@@ -1,10 +1,10 @@
import React, { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
-import { Settings as SettingsIcon, Save, Check, Users, Trash2, Copy, Link as LinkIcon, AlertTriangle } from 'lucide-react'
+import { Settings as SettingsIcon, Save, Check, Users, Trash2, Copy, Link as LinkIcon } from 'lucide-react'
import { ensureLogbookKey } from '../services/logbookKeys.js'
-import { useDialog } from './ModalDialog.tsx'
+import AccountDangerZone from './AccountDangerZone.tsx'
import PwaInstallPrompt from './PwaInstallPrompt.tsx'
-import { deleteAccount } from '../services/auth.js'
+import { useDialog } from './ModalDialog.tsx'
interface SettingsFormProps {
logbookId?: string | null
@@ -48,31 +48,6 @@ export default function SettingsForm({ logbookId }: SettingsFormProps) {
const [shareCopied, setShareCopied] = useState(false)
const [loadingShareLink, setLoadingShareLink] = useState(false)
- const handleDeleteAccount = async () => {
- const confirmed = await showConfirm(
- t('settings.delete_account_confirm_desc'),
- t('settings.delete_account_confirm_title'),
- t('settings.delete_account_confirm_yes'),
- t('settings.delete_account_confirm_no')
- )
-
- if (confirmed) {
- setSaving(true)
- try {
- const success = await deleteAccount()
- if (success) {
- window.location.reload()
- } else {
- showAlert(t('settings.delete_account_failed'))
- }
- } catch (err: any) {
- showAlert(err.message || t('settings.delete_account_failed'))
- } finally {
- setSaving(false)
- }
- }
- }
-
useEffect(() => {
if (logbookId) {
loadCollaborators()
@@ -514,30 +489,7 @@ export default function SettingsForm({ logbookId }: SettingsFormProps) {
)}
{/* Danger Zone / Account Deletion */}
-