fix: Daagbok-Branding, Track-Upload statt GPS-Tracker und zentrale Account-Löschung.

Ersetzt gpsTracker/Leaflet durch trackUpload, korrigiert App-Bezeichner und Login-Abstände, und macht die Account-Gefahrenzone auf dem Dashboard erreichbar.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-29 15:25:39 +02:00
co-authored by Cursor
parent 603d0bf1c4
commit 3c7aec1573
21 changed files with 261 additions and 367 deletions
@@ -0,0 +1,63 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Trash2, AlertTriangle } from 'lucide-react'
import { useDialog } from './ModalDialog.tsx'
import { deleteAccount } from '../services/auth.js'
interface AccountDangerZoneProps {
className?: string
}
export default function AccountDangerZone({ className = '' }: AccountDangerZoneProps) {
const { t } = useTranslation()
const { showConfirm, showAlert } = useDialog()
const [deleting, setDeleting] = 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) return
setDeleting(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 {
setDeleting(false)
}
}
return (
<div className={`account-danger-zone member-editor-card glass ${className}`.trim()}>
<div className="account-danger-zone__header">
<AlertTriangle size={20} className="account-danger-zone__icon" />
<h3 className="account-danger-zone__title">{t('settings.danger_zone_title')}</h3>
</div>
<p className="account-danger-zone__desc">{t('settings.danger_zone_desc')}</p>
<div className="form-actions account-danger-zone__actions">
<button
type="button"
className="btn danger"
onClick={handleDeleteAccount}
disabled={deleting}
>
<Trash2 size={16} />
{deleting ? t('settings.deleting_account') : t('settings.delete_account_btn')}
</button>
</div>
</div>
)
}
+1 -1
View File
@@ -382,7 +382,7 @@ export default function AuthOnboarding({ onAuthenticated }: AuthOnboardingProps)
return (
<div className="auth-card glass">
<div className="auth-brand">
<img src="/logo.png" alt="Kapteins Daagbox" className="auth-logo-img" />
<img src="/logo.png" alt="Kapteins Daagbok" className="auth-logo-img" />
<h1>{t('app.name')}</h1>
<p className="tagline">{t('auth.tagline')}</p>
</div>
+1 -1
View File
@@ -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)}
/>
)
}
+52 -158
View File
@@ -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<string | null>(null)
const [weatherLoading, setWeatherLoading] = useState(false)
// GPS Tracking States
const [savedTrack, setSavedTrack] = useState<SavedGpsTrack | null>(null)
// Track file upload
const [savedTrack, setSavedTrack] = useState<SavedTrack | null>(null)
const [dragOver, setDragOver] = useState(false)
const [uploadError, setUploadError] = useState<string | null>(null)
const fileInputRef = useRef<HTMLInputElement | null>(null)
const mapContainerRef = useRef<HTMLDivElement | null>(null)
const mapInstanceRef = useRef<L.Map | null>(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: '&copy; <a href="https://openstreetmap.org">OpenStreetMap</a> contributors'
}).addTo(map)
L.tileLayer('https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: 'Map data &copy; <a href="http://openseamap.org">OpenSeaMap</a> 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({
)}
</div>
{/* GPS Track Upload & Map Visualization */}
{/* Track file upload */}
<div className="form-card">
<div className="form-header">
<Navigation size={20} className="form-icon" />
<h3>{t('logs.gps_tracking_title')}</h3>
<Upload size={20} className="form-icon" />
<h3>{t('logs.track_upload_title')}</h3>
</div>
{uploadError && <div className="track-error-msg">{uploadError}</div>}
{!savedTrack ? (
/* Upload Zone when no track is loaded */
<div
className={`track-upload-zone ${dragOver ? 'dragover' : ''}`}
onDragOver={handleDragOver}
@@ -1260,52 +1162,44 @@ export default function LogEntryEditor({
onChange={handleFileChange}
disabled={saving}
/>
<Download size={36} className="track-upload-icon" />
<Upload size={36} className="track-upload-icon" />
<div className="track-upload-text">{t('logs.gps_track_upload_btn')}</div>
<div className="track-upload-subtext">{t('logs.gps_track_upload_help')}</div>
</div>
) : (
/* Map and Details when track is loaded */
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div className="track-info-header">
<div className="track-info-left">
<Navigation size={16} style={{ color: '#fbbf24' }} />
<span className="track-info-name">{savedTrack.filename || 'track'}</span>
</div>
<div className="track-info-stats">
<span style={{ marginRight: '12px' }}>
{t('logs.gps_tracking_stat_distance')}: <strong>{calculateTrackDistance(savedTrack.waypoints)} sm</strong>
</span>
<span>
{t('logs.gps_tracking_stat_waypoints')}: <strong>{savedTrack.waypoints.length}</strong>
</span>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<div className="track-info-header">
<div className="track-info-left">
<Upload size={16} style={{ color: '#fbbf24' }} />
<span className="track-info-name">{savedTrack.filename || 'track'}</span>
<span className="track-info-stats">
{savedTrack.fileType.toUpperCase()}
{savedTrack.waypoints.length > 0 && (
<> · {savedTrack.waypoints.length} {t('logs.track_upload_points')}</>
)}
</span>
</div>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
<button
type="button"
className="btn secondary"
onClick={() => downloadTrackFile(savedTrack)}
style={{ width: 'auto', padding: '6px 12px', fontSize: '13px', display: 'flex', alignItems: 'center', gap: '4px' }}
>
<Download size={14} />
{t('logs.gps_tracking_btn_gpx')}
</button>
{!readOnly && (
<button
type="button"
className="btn secondary"
onClick={() => downloadTrackFile(savedTrack)}
style={{ width: 'auto', padding: '6px 12px', fontSize: '13px', display: 'flex', alignItems: 'center', gap: '4px' }}
onClick={handleDeleteTrack}
style={{ width: 'auto', padding: '6px 12px', fontSize: '13px', display: 'flex', alignItems: 'center', gap: '4px', background: 'rgba(239, 68, 68, 0.1)', color: '#ef4444', borderColor: 'rgba(239, 68, 68, 0.2)' }}
>
<Download size={14} />
{t('logs.gps_tracking_btn_gpx')}
<Trash2 size={14} />
{t('logs.gps_track_delete')}
</button>
{!readOnly && (
<button
type="button"
className="btn secondary"
onClick={handleDeleteTrack}
style={{ width: 'auto', padding: '6px 12px', fontSize: '13px', display: 'flex', alignItems: 'center', gap: '4px', background: 'rgba(239, 68, 68, 0.1)', color: '#ef4444', borderColor: 'rgba(239, 68, 68, 0.2)' }}
>
<Trash2 size={14} />
{t('logs.gps_track_delete')}
</button>
)}
</div>
)}
</div>
{/* Leaflet Map Div */}
<div id="openseamap-container" ref={mapContainerRef} />
</div>
)}
</div>
@@ -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
)}
</section>
</main>
<section className="dashboard-account-section" aria-label={t('settings.danger_zone_title')}>
<AccountDangerZone />
</section>
</div>
)
}
+4 -52
View File
@@ -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) {
</div>
)}
{/* Danger Zone / Account Deletion */}
<div className="member-editor-card glass mt-6" style={{ borderTop: '1px solid rgba(239,68,68,0.2)', paddingTop: '24px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px' }}>
<AlertTriangle size={20} style={{ color: '#ef4444' }} />
<h3 style={{ margin: 0, color: '#ef4444', fontSize: '16px' }}>
{t('settings.danger_zone_title')}
</h3>
</div>
<p style={{ fontSize: '13.5px', color: '#94a3b8', lineHeight: '145%', margin: '0 0 16px 0' }}>
{t('settings.danger_zone_desc')}
</p>
<div className="form-actions" style={{ justifyContent: 'flex-start' }}>
<button
type="button"
className="btn danger"
onClick={handleDeleteAccount}
style={{ width: 'auto' }}
>
<Trash2 size={16} />
{t('settings.delete_account_btn')}
</button>
</div>
</div>
<AccountDangerZone className="mt-6" />
</div>
)
}