Replace native browser alerts and confirms with customized modern promise-based overlay dialogs
This commit is contained in:
@@ -4,6 +4,7 @@ import { db } from '../services/db.js'
|
||||
import { getActiveMasterKey } from '../services/auth.js'
|
||||
import { encryptJson, decryptJson } from '../services/crypto.js'
|
||||
import { syncLogbook } from '../services/sync.js'
|
||||
import { useDialog } from './ModalDialog.tsx'
|
||||
import { Users, User, Plus, Trash2, Edit2, Save, X, Check } from 'lucide-react'
|
||||
|
||||
interface CrewFormProps {
|
||||
@@ -30,6 +31,7 @@ interface DecryptedCrew {
|
||||
|
||||
export default function CrewForm({ logbookId }: CrewFormProps) {
|
||||
const { t } = useTranslation()
|
||||
const { showConfirm } = useDialog()
|
||||
|
||||
// Skipper profile state
|
||||
const [skipName, setSkipName] = useState('')
|
||||
@@ -260,7 +262,7 @@ export default function CrewForm({ logbookId }: CrewFormProps) {
|
||||
}
|
||||
|
||||
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)
|
||||
try {
|
||||
const now = new Date().toISOString()
|
||||
|
||||
@@ -7,6 +7,7 @@ import { syncLogbook } from '../services/sync.js'
|
||||
import { downloadCsv, shareCsv } from '../services/csvExport.js'
|
||||
import { downloadLogbookPagePdf } from '../services/pdfExport.js'
|
||||
import LogEntryEditor from './LogEntryEditor.tsx'
|
||||
import { useDialog } from './ModalDialog.tsx'
|
||||
import { FileText, Plus, Trash2, ChevronRight, Calendar, Download, Share2 } from 'lucide-react'
|
||||
|
||||
interface LogEntriesListProps {
|
||||
@@ -24,6 +25,7 @@ interface DecryptedEntryItem {
|
||||
|
||||
export default function LogEntriesList({ logbookId }: LogEntriesListProps) {
|
||||
const { t } = useTranslation()
|
||||
const { showConfirm } = useDialog()
|
||||
const [entries, setEntries] = useState<DecryptedEntryItem[]>([])
|
||||
const [selectedEntryId, setSelectedEntryId] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -187,7 +189,7 @@ export default function LogEntriesList({ logbookId }: LogEntriesListProps) {
|
||||
const handleDelete = async (entryId: string, e: React.MouseEvent) => {
|
||||
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)
|
||||
try {
|
||||
const now = new Date().toISOString()
|
||||
|
||||
@@ -7,6 +7,7 @@ import { syncLogbook } from '../services/sync.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 PhotoCapture from './PhotoCapture.tsx'
|
||||
import { useDialog } from './ModalDialog.tsx'
|
||||
import {
|
||||
startGpsTracking,
|
||||
stopGpsTracking,
|
||||
@@ -44,6 +45,7 @@ interface LogEvent {
|
||||
|
||||
export default function LogEntryEditor({ entryId, logbookId, onBack }: LogEntryEditorProps) {
|
||||
const { t } = useTranslation()
|
||||
const { showAlert } = useDialog()
|
||||
|
||||
// General details state
|
||||
const [date, setDate] = useState('')
|
||||
@@ -202,7 +204,7 @@ export default function LogEntryEditor({ entryId, logbookId, onBack }: LogEntryE
|
||||
})
|
||||
setTrackingActive(true)
|
||||
} 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 = () => {
|
||||
if (!navigator.geolocation) {
|
||||
alert('Geolocation is not supported by your browser')
|
||||
showAlert('Geolocation is not supported by your browser')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -254,20 +256,20 @@ export default function LogEntryEditor({ entryId, logbookId, onBack }: LogEntryE
|
||||
},
|
||||
(err) => {
|
||||
console.error('GPS capturing failed:', err)
|
||||
alert(`Failed to retrieve coordinates: ${err.message}`)
|
||||
showAlert(`Failed to retrieve coordinates: ${err.message}`)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const handleFetchWeather = async () => {
|
||||
if (!evGpsLat || !evGpsLng) {
|
||||
alert(t('settings.gps_error'))
|
||||
showAlert(t('settings.gps_error'))
|
||||
return
|
||||
}
|
||||
|
||||
const apiKey = localStorage.getItem('owm_api_key')
|
||||
if (!apiKey) {
|
||||
alert(t('settings.no_key'))
|
||||
showAlert(t('settings.no_key'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -313,10 +315,10 @@ export default function LogEntryEditor({ entryId, logbookId, onBack }: LogEntryE
|
||||
setEvWeatherIcon(data.weather[0].icon)
|
||||
}
|
||||
|
||||
alert(t('settings.weather_success'))
|
||||
showAlert(t('settings.weather_success'))
|
||||
} catch (err) {
|
||||
console.error('Weather prefilling failed:', err)
|
||||
alert(t('settings.weather_error'))
|
||||
showAlert(t('settings.weather_error'))
|
||||
} finally {
|
||||
setWeatherLoading(false)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useLiveQuery } from 'dexie-react-hooks'
|
||||
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 { BookOpen, Plus, Trash2, LogOut, Languages, RefreshCw, Ship, User, Wifi, WifiOff } from 'lucide-react'
|
||||
|
||||
interface LogbookDashboardProps {
|
||||
@@ -13,6 +14,7 @@ interface LogbookDashboardProps {
|
||||
|
||||
export default function LogbookDashboard({ onSelectLogbook, onLogout }: LogbookDashboardProps) {
|
||||
const { t, i18n } = useTranslation()
|
||||
const { showConfirm } = useDialog()
|
||||
const [logbooks, setLogbooks] = useState<DecryptedLogbook[]>([])
|
||||
const [newTitle, setNewTitle] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -76,7 +78,7 @@ export default function LogbookDashboard({ onSelectLogbook, onLogout }: LogbookD
|
||||
const handleDelete = async (id: string, e: React.MouseEvent) => {
|
||||
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)
|
||||
setError(null)
|
||||
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 { syncLogbook } from '../services/sync.js'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { useDialog } from './ModalDialog.tsx'
|
||||
import { Camera, Trash2 } from 'lucide-react'
|
||||
|
||||
interface PhotoCaptureProps {
|
||||
@@ -21,6 +22,7 @@ interface DecryptedPhoto {
|
||||
|
||||
export default function PhotoCapture({ entryId, logbookId }: PhotoCaptureProps) {
|
||||
const { t } = useTranslation()
|
||||
const { showConfirm } = useDialog()
|
||||
const [caption, setCaption] = useState('')
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -156,7 +158,7 @@ export default function PhotoCapture({ entryId, logbookId }: PhotoCaptureProps)
|
||||
}
|
||||
|
||||
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 {
|
||||
const now = new Date().toISOString()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user