Replace native browser alerts and confirms with customized modern promise-based overlay dialogs
This commit is contained in:
@@ -1463,3 +1463,56 @@ body:has(.theme-cupertino) {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Custom Dialog Modals Styling */
|
||||||
|
.custom-dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(11, 12, 16, 0.75);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-dialog-card {
|
||||||
|
background: rgba(15, 23, 42, 0.85);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 28px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 420px;
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.6);
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-dialog-title {
|
||||||
|
font-size: 19px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fbbf24;
|
||||||
|
margin: 0 0 14px 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-dialog-message {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #e2e8f0;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0 0 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|||||||
+8
-1
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
import { DialogProvider } from './components/ModalDialog.tsx'
|
||||||
import AuthOnboarding from './components/AuthOnboarding.tsx'
|
import AuthOnboarding from './components/AuthOnboarding.tsx'
|
||||||
import LogbookDashboard from './components/LogbookDashboard.tsx'
|
import LogbookDashboard from './components/LogbookDashboard.tsx'
|
||||||
import VesselForm from './components/VesselForm.tsx'
|
import VesselForm from './components/VesselForm.tsx'
|
||||||
@@ -259,4 +260,10 @@ function App() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default function AppWrapper() {
|
||||||
|
return (
|
||||||
|
<DialogProvider>
|
||||||
|
<App />
|
||||||
|
</DialogProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { db } from '../services/db.js'
|
|||||||
import { getActiveMasterKey } from '../services/auth.js'
|
import { getActiveMasterKey } from '../services/auth.js'
|
||||||
import { encryptJson, decryptJson } from '../services/crypto.js'
|
import { encryptJson, decryptJson } from '../services/crypto.js'
|
||||||
import { syncLogbook } from '../services/sync.js'
|
import { syncLogbook } from '../services/sync.js'
|
||||||
|
import { useDialog } from './ModalDialog.tsx'
|
||||||
import { Users, User, Plus, Trash2, Edit2, Save, X, Check } from 'lucide-react'
|
import { Users, User, Plus, Trash2, Edit2, Save, X, Check } from 'lucide-react'
|
||||||
|
|
||||||
interface CrewFormProps {
|
interface CrewFormProps {
|
||||||
@@ -30,6 +31,7 @@ interface DecryptedCrew {
|
|||||||
|
|
||||||
export default function CrewForm({ logbookId }: CrewFormProps) {
|
export default function CrewForm({ logbookId }: CrewFormProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { showConfirm } = useDialog()
|
||||||
|
|
||||||
// Skipper profile state
|
// Skipper profile state
|
||||||
const [skipName, setSkipName] = useState('')
|
const [skipName, setSkipName] = useState('')
|
||||||
@@ -260,7 +262,7 @@ export default function CrewForm({ logbookId }: CrewFormProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteMember = async (memberId: string) => {
|
const handleDeleteMember = async (memberId: string) => {
|
||||||
if (window.confirm(t('crew.delete_confirm'))) {
|
if (await showConfirm(t('crew.delete_confirm'), t('crew.title'), t('logs.confirm_yes'), t('logs.confirm_no'))) {
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { syncLogbook } from '../services/sync.js'
|
|||||||
import { downloadCsv, shareCsv } from '../services/csvExport.js'
|
import { downloadCsv, shareCsv } from '../services/csvExport.js'
|
||||||
import { downloadLogbookPagePdf } from '../services/pdfExport.js'
|
import { downloadLogbookPagePdf } from '../services/pdfExport.js'
|
||||||
import LogEntryEditor from './LogEntryEditor.tsx'
|
import LogEntryEditor from './LogEntryEditor.tsx'
|
||||||
|
import { useDialog } from './ModalDialog.tsx'
|
||||||
import { FileText, Plus, Trash2, ChevronRight, Calendar, Download, Share2 } from 'lucide-react'
|
import { FileText, Plus, Trash2, ChevronRight, Calendar, Download, Share2 } from 'lucide-react'
|
||||||
|
|
||||||
interface LogEntriesListProps {
|
interface LogEntriesListProps {
|
||||||
@@ -24,6 +25,7 @@ interface DecryptedEntryItem {
|
|||||||
|
|
||||||
export default function LogEntriesList({ logbookId }: LogEntriesListProps) {
|
export default function LogEntriesList({ logbookId }: LogEntriesListProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { showConfirm } = useDialog()
|
||||||
const [entries, setEntries] = useState<DecryptedEntryItem[]>([])
|
const [entries, setEntries] = useState<DecryptedEntryItem[]>([])
|
||||||
const [selectedEntryId, setSelectedEntryId] = useState<string | null>(null)
|
const [selectedEntryId, setSelectedEntryId] = useState<string | null>(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
@@ -187,7 +189,7 @@ export default function LogEntriesList({ logbookId }: LogEntriesListProps) {
|
|||||||
const handleDelete = async (entryId: string, e: React.MouseEvent) => {
|
const handleDelete = async (entryId: string, e: React.MouseEvent) => {
|
||||||
e.stopPropagation() // Prevent selecting the card
|
e.stopPropagation() // Prevent selecting the card
|
||||||
|
|
||||||
if (window.confirm(t('logs.delete_confirm'))) {
|
if (await showConfirm(t('logs.delete_confirm'), t('logs.delete_entry'), t('logs.confirm_yes'), t('logs.confirm_no'))) {
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { syncLogbook } from '../services/sync.js'
|
|||||||
import { downloadLogbookPagePdf } from '../services/pdfExport.js'
|
import { downloadLogbookPagePdf } from '../services/pdfExport.js'
|
||||||
import { FileText, Save, ChevronLeft, Check, Compass, Plus, Trash2, MapPin, CloudSun, Clock, Download, Play, Square, Navigation } from 'lucide-react'
|
import { FileText, Save, ChevronLeft, Check, Compass, Plus, Trash2, MapPin, CloudSun, Clock, Download, Play, Square, Navigation } from 'lucide-react'
|
||||||
import PhotoCapture from './PhotoCapture.tsx'
|
import PhotoCapture from './PhotoCapture.tsx'
|
||||||
|
import { useDialog } from './ModalDialog.tsx'
|
||||||
import {
|
import {
|
||||||
startGpsTracking,
|
startGpsTracking,
|
||||||
stopGpsTracking,
|
stopGpsTracking,
|
||||||
@@ -44,6 +45,7 @@ interface LogEvent {
|
|||||||
|
|
||||||
export default function LogEntryEditor({ entryId, logbookId, onBack }: LogEntryEditorProps) {
|
export default function LogEntryEditor({ entryId, logbookId, onBack }: LogEntryEditorProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { showAlert } = useDialog()
|
||||||
|
|
||||||
// General details state
|
// General details state
|
||||||
const [date, setDate] = useState('')
|
const [date, setDate] = useState('')
|
||||||
@@ -202,7 +204,7 @@ export default function LogEntryEditor({ entryId, logbookId, onBack }: LogEntryE
|
|||||||
})
|
})
|
||||||
setTrackingActive(true)
|
setTrackingActive(true)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert(err.message || 'Failed to start GPS tracking')
|
showAlert(err.message || 'Failed to start GPS tracking')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +245,7 @@ export default function LogEntryEditor({ entryId, logbookId, onBack }: LogEntryE
|
|||||||
|
|
||||||
const handleGetGps = () => {
|
const handleGetGps = () => {
|
||||||
if (!navigator.geolocation) {
|
if (!navigator.geolocation) {
|
||||||
alert('Geolocation is not supported by your browser')
|
showAlert('Geolocation is not supported by your browser')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,20 +256,20 @@ export default function LogEntryEditor({ entryId, logbookId, onBack }: LogEntryE
|
|||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
console.error('GPS capturing failed:', err)
|
console.error('GPS capturing failed:', err)
|
||||||
alert(`Failed to retrieve coordinates: ${err.message}`)
|
showAlert(`Failed to retrieve coordinates: ${err.message}`)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFetchWeather = async () => {
|
const handleFetchWeather = async () => {
|
||||||
if (!evGpsLat || !evGpsLng) {
|
if (!evGpsLat || !evGpsLng) {
|
||||||
alert(t('settings.gps_error'))
|
showAlert(t('settings.gps_error'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiKey = localStorage.getItem('owm_api_key')
|
const apiKey = localStorage.getItem('owm_api_key')
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
alert(t('settings.no_key'))
|
showAlert(t('settings.no_key'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,10 +315,10 @@ export default function LogEntryEditor({ entryId, logbookId, onBack }: LogEntryE
|
|||||||
setEvWeatherIcon(data.weather[0].icon)
|
setEvWeatherIcon(data.weather[0].icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
alert(t('settings.weather_success'))
|
showAlert(t('settings.weather_success'))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Weather prefilling failed:', err)
|
console.error('Weather prefilling failed:', err)
|
||||||
alert(t('settings.weather_error'))
|
showAlert(t('settings.weather_error'))
|
||||||
} finally {
|
} finally {
|
||||||
setWeatherLoading(false)
|
setWeatherLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useLiveQuery } from 'dexie-react-hooks'
|
|||||||
import { db } from '../services/db.js'
|
import { db } from '../services/db.js'
|
||||||
import { fetchLogbooks, createLogbook, deleteLogbook, type DecryptedLogbook } from '../services/logbook.js'
|
import { fetchLogbooks, createLogbook, deleteLogbook, type DecryptedLogbook } from '../services/logbook.js'
|
||||||
import { logoutUser } from '../services/auth.js'
|
import { logoutUser } from '../services/auth.js'
|
||||||
|
import { useDialog } from './ModalDialog.tsx'
|
||||||
import { BookOpen, Plus, Trash2, LogOut, Languages, RefreshCw, Ship, User, Wifi, WifiOff } from 'lucide-react'
|
import { BookOpen, Plus, Trash2, LogOut, Languages, RefreshCw, Ship, User, Wifi, WifiOff } from 'lucide-react'
|
||||||
|
|
||||||
interface LogbookDashboardProps {
|
interface LogbookDashboardProps {
|
||||||
@@ -13,6 +14,7 @@ interface LogbookDashboardProps {
|
|||||||
|
|
||||||
export default function LogbookDashboard({ onSelectLogbook, onLogout }: LogbookDashboardProps) {
|
export default function LogbookDashboard({ onSelectLogbook, onLogout }: LogbookDashboardProps) {
|
||||||
const { t, i18n } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
|
const { showConfirm } = useDialog()
|
||||||
const [logbooks, setLogbooks] = useState<DecryptedLogbook[]>([])
|
const [logbooks, setLogbooks] = useState<DecryptedLogbook[]>([])
|
||||||
const [newTitle, setNewTitle] = useState('')
|
const [newTitle, setNewTitle] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
@@ -76,7 +78,7 @@ export default function LogbookDashboard({ onSelectLogbook, onLogout }: LogbookD
|
|||||||
const handleDelete = async (id: string, e: React.MouseEvent) => {
|
const handleDelete = async (id: string, e: React.MouseEvent) => {
|
||||||
e.stopPropagation() // Prevent selecting the logbook when clicking delete
|
e.stopPropagation() // Prevent selecting the logbook when clicking delete
|
||||||
|
|
||||||
if (window.confirm(t('dashboard.delete_confirm'))) {
|
if (await showConfirm(t('dashboard.delete_confirm'), t('dashboard.title'), t('logs.confirm_yes'), t('logs.confirm_no'))) {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import React, { createContext, useContext, useState, useRef } from 'react'
|
||||||
|
|
||||||
|
interface DialogContextType {
|
||||||
|
showAlert: (message: string, title?: string, confirmText?: string) => Promise<void>
|
||||||
|
showConfirm: (message: string, title?: string, confirmText?: string, cancelText?: string) => Promise<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
const DialogContext = createContext<DialogContextType | undefined>(undefined)
|
||||||
|
|
||||||
|
export function useDialog() {
|
||||||
|
const context = useContext(DialogContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useDialog must be used within a DialogProvider')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const [title, setTitle] = useState('')
|
||||||
|
const [message, setMessage] = useState('')
|
||||||
|
const [type, setType] = useState<'alert' | 'confirm'>('alert')
|
||||||
|
const [confirmLabel, setConfirmLabel] = useState('OK')
|
||||||
|
const [cancelLabel, setCancelLabel] = useState('Cancel')
|
||||||
|
|
||||||
|
const resolveRef = useRef<((val: any) => void) | null>(null)
|
||||||
|
|
||||||
|
const showAlert = (msg: string, headerTitle?: string, btnText?: string): Promise<void> => {
|
||||||
|
setMessage(msg)
|
||||||
|
setTitle(headerTitle || '')
|
||||||
|
setType('alert')
|
||||||
|
setConfirmLabel(btnText || 'OK')
|
||||||
|
setIsOpen(true)
|
||||||
|
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
resolveRef.current = resolve
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const showConfirm = (msg: string, headerTitle?: string, btnConfirm?: string, btnCancel?: string): Promise<boolean> => {
|
||||||
|
setMessage(msg)
|
||||||
|
setTitle(headerTitle || '')
|
||||||
|
setType('confirm')
|
||||||
|
setConfirmLabel(btnConfirm || 'Yes')
|
||||||
|
setCancelLabel(btnCancel || 'No')
|
||||||
|
setIsOpen(true)
|
||||||
|
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
resolveRef.current = resolve
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
setIsOpen(false)
|
||||||
|
if (resolveRef.current) {
|
||||||
|
resolveRef.current(type === 'confirm' ? true : undefined)
|
||||||
|
resolveRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setIsOpen(false)
|
||||||
|
if (resolveRef.current) {
|
||||||
|
resolveRef.current(false)
|
||||||
|
resolveRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogContext.Provider value={{ showAlert, showConfirm }}>
|
||||||
|
{children}
|
||||||
|
{isOpen && (
|
||||||
|
<div className="custom-dialog-overlay" onClick={type === 'alert' ? handleConfirm : undefined}>
|
||||||
|
<div className="custom-dialog-card glass scale-in" onClick={(e) => e.stopPropagation()}>
|
||||||
|
{title && <h3 className="custom-dialog-title">{title}</h3>}
|
||||||
|
<p className="custom-dialog-message">{message}</p>
|
||||||
|
<div className="custom-dialog-actions">
|
||||||
|
{type === 'confirm' && (
|
||||||
|
<button type="button" className="btn secondary" onClick={handleCancel} style={{ width: 'auto', padding: '8px 20px', margin: 0 }}>
|
||||||
|
{cancelLabel}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button type="button" className="btn primary" onClick={handleConfirm} style={{ width: 'auto', minWidth: '80px', padding: '8px 20px', margin: 0 }}>
|
||||||
|
{confirmLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { getActiveMasterKey } from '../services/auth.js'
|
|||||||
import { encryptJson, decryptJson } from '../services/crypto.js'
|
import { encryptJson, decryptJson } from '../services/crypto.js'
|
||||||
import { syncLogbook } from '../services/sync.js'
|
import { syncLogbook } from '../services/sync.js'
|
||||||
import { useLiveQuery } from 'dexie-react-hooks'
|
import { useLiveQuery } from 'dexie-react-hooks'
|
||||||
|
import { useDialog } from './ModalDialog.tsx'
|
||||||
import { Camera, Trash2 } from 'lucide-react'
|
import { Camera, Trash2 } from 'lucide-react'
|
||||||
|
|
||||||
interface PhotoCaptureProps {
|
interface PhotoCaptureProps {
|
||||||
@@ -21,6 +22,7 @@ interface DecryptedPhoto {
|
|||||||
|
|
||||||
export default function PhotoCapture({ entryId, logbookId }: PhotoCaptureProps) {
|
export default function PhotoCapture({ entryId, logbookId }: PhotoCaptureProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { showConfirm } = useDialog()
|
||||||
const [caption, setCaption] = useState('')
|
const [caption, setCaption] = useState('')
|
||||||
const [uploading, setUploading] = useState(false)
|
const [uploading, setUploading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
@@ -156,7 +158,7 @@ export default function PhotoCapture({ entryId, logbookId }: PhotoCaptureProps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = async (photoId: string) => {
|
const handleDelete = async (photoId: string) => {
|
||||||
if (window.confirm(t('logs.photo_delete_confirm'))) {
|
if (await showConfirm(t('logs.photo_delete_confirm'), t('logs.photos_title'), t('logs.confirm_yes'), t('logs.confirm_no'))) {
|
||||||
try {
|
try {
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,8 @@
|
|||||||
"photo_processing": "Wird verarbeitet...",
|
"photo_processing": "Wird verarbeitet...",
|
||||||
"no_photos": "Noch keine Fotos an diesen Reisetag angehängt.",
|
"no_photos": "Noch keine Fotos an diesen Reisetag angehängt.",
|
||||||
"photo_delete_confirm": "Sind Sie sicher, dass Sie dieses Foto unwiderruflich löschen möchten?",
|
"photo_delete_confirm": "Sind Sie sicher, dass Sie dieses Foto unwiderruflich löschen möchten?",
|
||||||
|
"confirm_yes": "Ja",
|
||||||
|
"confirm_no": "Nein",
|
||||||
"gps_tracking_title": "GPS-Routenaufzeichnung (E2E-verschlüsselt)",
|
"gps_tracking_title": "GPS-Routenaufzeichnung (E2E-verschlüsselt)",
|
||||||
"gps_tracking_status_active": "Aufzeichnung läuft",
|
"gps_tracking_status_active": "Aufzeichnung läuft",
|
||||||
"gps_tracking_status_inactive": "Aufzeichnung inaktiv",
|
"gps_tracking_status_inactive": "Aufzeichnung inaktiv",
|
||||||
|
|||||||
@@ -97,6 +97,8 @@
|
|||||||
"photo_processing": "Processing...",
|
"photo_processing": "Processing...",
|
||||||
"no_photos": "No photos attached to this journal entry yet.",
|
"no_photos": "No photos attached to this journal entry yet.",
|
||||||
"photo_delete_confirm": "Are you sure you want to permanently delete this photo?",
|
"photo_delete_confirm": "Are you sure you want to permanently delete this photo?",
|
||||||
|
"confirm_yes": "Yes",
|
||||||
|
"confirm_no": "No",
|
||||||
"gps_tracking_title": "GPS Route Tracking (E2E Encrypted)",
|
"gps_tracking_title": "GPS Route Tracking (E2E Encrypted)",
|
||||||
"gps_tracking_status_active": "Tracking Active",
|
"gps_tracking_status_active": "Tracking Active",
|
||||||
"gps_tracking_status_inactive": "Tracking Inactive",
|
"gps_tracking_status_inactive": "Tracking Inactive",
|
||||||
|
|||||||
@@ -254,7 +254,6 @@ ${trkpts}
|
|||||||
// Download GPX file client-side
|
// Download GPX file client-side
|
||||||
export function downloadGpxFile(waypoints: GpsWaypoint[], dateStr: string): void {
|
export function downloadGpxFile(waypoints: GpsWaypoint[], dateStr: string): void {
|
||||||
if (waypoints.length === 0) {
|
if (waypoints.length === 0) {
|
||||||
alert('No waypoints recorded to export.')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const gpxContent = generateGpxString(waypoints, dateStr)
|
const gpxContent = generateGpxString(waypoints, dateStr)
|
||||||
|
|||||||
Reference in New Issue
Block a user