mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
feat(journey/settings): warn on unsaved changes before closing modal
- Track dirty state (title/subtitle changed from original) - Intercept X button, backdrop click, and Cancel with handleClose - Show ConfirmDialog when dirty; proceed with onClose only on confirm - Add common.discardChanges and common.discard keys to all 15 locales
This commit is contained in:
@@ -34,6 +34,8 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'common.none': 'لا شيء',
|
'common.none': 'لا شيء',
|
||||||
'common.date': 'التاريخ',
|
'common.date': 'التاريخ',
|
||||||
'common.rename': 'إعادة تسمية',
|
'common.rename': 'إعادة تسمية',
|
||||||
|
'common.discardChanges': 'تجاهل التغييرات',
|
||||||
|
'common.discard': 'تجاهل',
|
||||||
'common.name': 'الاسم',
|
'common.name': 'الاسم',
|
||||||
'common.email': 'البريد الإلكتروني',
|
'common.email': 'البريد الإلكتروني',
|
||||||
'common.password': 'كلمة المرور',
|
'common.password': 'كلمة المرور',
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'common.none': 'Nenhum',
|
'common.none': 'Nenhum',
|
||||||
'common.date': 'Data',
|
'common.date': 'Data',
|
||||||
'common.rename': 'Renomear',
|
'common.rename': 'Renomear',
|
||||||
|
'common.discardChanges': 'Descartar alterações',
|
||||||
|
'common.discard': 'Descartar',
|
||||||
'common.name': 'Nome',
|
'common.name': 'Nome',
|
||||||
'common.email': 'E-mail',
|
'common.email': 'E-mail',
|
||||||
'common.password': 'Senha',
|
'common.password': 'Senha',
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'common.none': 'Žádné',
|
'common.none': 'Žádné',
|
||||||
'common.date': 'Datum',
|
'common.date': 'Datum',
|
||||||
'common.rename': 'Přejmenovat',
|
'common.rename': 'Přejmenovat',
|
||||||
|
'common.discardChanges': 'Zahodit změny',
|
||||||
|
'common.discard': 'Zahodit',
|
||||||
'common.name': 'Jméno',
|
'common.name': 'Jméno',
|
||||||
'common.email': 'E-mail',
|
'common.email': 'E-mail',
|
||||||
'common.password': 'Heslo',
|
'common.password': 'Heslo',
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'common.none': 'Keine',
|
'common.none': 'Keine',
|
||||||
'common.date': 'Datum',
|
'common.date': 'Datum',
|
||||||
'common.rename': 'Umbenennen',
|
'common.rename': 'Umbenennen',
|
||||||
|
'common.discardChanges': 'Änderungen verwerfen',
|
||||||
|
'common.discard': 'Verwerfen',
|
||||||
'common.name': 'Name',
|
'common.name': 'Name',
|
||||||
'common.email': 'E-Mail',
|
'common.email': 'E-Mail',
|
||||||
'common.password': 'Passwort',
|
'common.password': 'Passwort',
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'common.none': 'None',
|
'common.none': 'None',
|
||||||
'common.date': 'Date',
|
'common.date': 'Date',
|
||||||
'common.rename': 'Rename',
|
'common.rename': 'Rename',
|
||||||
|
'common.discardChanges': 'Discard Changes',
|
||||||
|
'common.discard': 'Discard',
|
||||||
'common.name': 'Name',
|
'common.name': 'Name',
|
||||||
'common.email': 'Email',
|
'common.email': 'Email',
|
||||||
'common.password': 'Password',
|
'common.password': 'Password',
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const es: Record<string, string> = {
|
|||||||
'common.none': 'Ninguno',
|
'common.none': 'Ninguno',
|
||||||
'common.date': 'Fecha',
|
'common.date': 'Fecha',
|
||||||
'common.rename': 'Renombrar',
|
'common.rename': 'Renombrar',
|
||||||
|
'common.discardChanges': 'Descartar cambios',
|
||||||
|
'common.discard': 'Descartar',
|
||||||
'common.name': 'Nombre',
|
'common.name': 'Nombre',
|
||||||
'common.email': 'Correo',
|
'common.email': 'Correo',
|
||||||
'common.password': 'Contraseña',
|
'common.password': 'Contraseña',
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const fr: Record<string, string> = {
|
|||||||
'common.none': 'Aucun',
|
'common.none': 'Aucun',
|
||||||
'common.date': 'Date',
|
'common.date': 'Date',
|
||||||
'common.rename': 'Renommer',
|
'common.rename': 'Renommer',
|
||||||
|
'common.discardChanges': 'Ignorer les modifications',
|
||||||
|
'common.discard': 'Ignorer',
|
||||||
'common.name': 'Nom',
|
'common.name': 'Nom',
|
||||||
'common.email': 'E-mail',
|
'common.email': 'E-mail',
|
||||||
'common.password': 'Mot de passe',
|
'common.password': 'Mot de passe',
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'common.none': 'Nincs',
|
'common.none': 'Nincs',
|
||||||
'common.date': 'Dátum',
|
'common.date': 'Dátum',
|
||||||
'common.rename': 'Átnevezés',
|
'common.rename': 'Átnevezés',
|
||||||
|
'common.discardChanges': 'Változtatások elvetése',
|
||||||
|
'common.discard': 'Elveti',
|
||||||
'common.name': 'Név',
|
'common.name': 'Név',
|
||||||
'common.email': 'E-mail',
|
'common.email': 'E-mail',
|
||||||
'common.password': 'Jelszó',
|
'common.password': 'Jelszó',
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const id: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'common.none': 'Tidak ada',
|
'common.none': 'Tidak ada',
|
||||||
'common.date': 'Tanggal',
|
'common.date': 'Tanggal',
|
||||||
'common.rename': 'Ganti nama',
|
'common.rename': 'Ganti nama',
|
||||||
|
'common.discardChanges': 'Buang perubahan',
|
||||||
|
'common.discard': 'Buang',
|
||||||
'common.name': 'Nama',
|
'common.name': 'Nama',
|
||||||
'common.email': 'Email',
|
'common.email': 'Email',
|
||||||
'common.password': 'Kata sandi',
|
'common.password': 'Kata sandi',
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'common.none': 'Nessuno',
|
'common.none': 'Nessuno',
|
||||||
'common.date': 'Data',
|
'common.date': 'Data',
|
||||||
'common.rename': 'Rinomina',
|
'common.rename': 'Rinomina',
|
||||||
|
'common.discardChanges': 'Scarta modifiche',
|
||||||
|
'common.discard': 'Scarta',
|
||||||
'common.name': 'Nome',
|
'common.name': 'Nome',
|
||||||
'common.email': 'Email',
|
'common.email': 'Email',
|
||||||
'common.password': 'Password',
|
'common.password': 'Password',
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const nl: Record<string, string> = {
|
|||||||
'common.none': 'Geen',
|
'common.none': 'Geen',
|
||||||
'common.date': 'Datum',
|
'common.date': 'Datum',
|
||||||
'common.rename': 'Hernoemen',
|
'common.rename': 'Hernoemen',
|
||||||
|
'common.discardChanges': 'Wijzigingen verwerpen',
|
||||||
|
'common.discard': 'Verwerpen',
|
||||||
'common.name': 'Naam',
|
'common.name': 'Naam',
|
||||||
'common.email': 'E-mail',
|
'common.email': 'E-mail',
|
||||||
'common.password': 'Wachtwoord',
|
'common.password': 'Wachtwoord',
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'common.none': 'Brak',
|
'common.none': 'Brak',
|
||||||
'common.date': 'Data',
|
'common.date': 'Data',
|
||||||
'common.rename': 'Zmień nazwę',
|
'common.rename': 'Zmień nazwę',
|
||||||
|
'common.discardChanges': 'Odrzuć zmiany',
|
||||||
|
'common.discard': 'Odrzuć',
|
||||||
'common.name': 'Nazwa',
|
'common.name': 'Nazwa',
|
||||||
'common.email': 'E-mail',
|
'common.email': 'E-mail',
|
||||||
'common.password': 'Hasło',
|
'common.password': 'Hasło',
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const ru: Record<string, string> = {
|
|||||||
'common.none': 'Нет',
|
'common.none': 'Нет',
|
||||||
'common.date': 'Дата',
|
'common.date': 'Дата',
|
||||||
'common.rename': 'Переименовать',
|
'common.rename': 'Переименовать',
|
||||||
|
'common.discardChanges': 'Отменить изменения',
|
||||||
|
'common.discard': 'Отменить',
|
||||||
'common.name': 'Имя',
|
'common.name': 'Имя',
|
||||||
'common.email': 'Эл. почта',
|
'common.email': 'Эл. почта',
|
||||||
'common.password': 'Пароль',
|
'common.password': 'Пароль',
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const zh: Record<string, string> = {
|
|||||||
'common.none': '无',
|
'common.none': '无',
|
||||||
'common.date': '日期',
|
'common.date': '日期',
|
||||||
'common.rename': '重命名',
|
'common.rename': '重命名',
|
||||||
|
'common.discardChanges': '放弃更改',
|
||||||
|
'common.discard': '放弃',
|
||||||
'common.name': '名称',
|
'common.name': '名称',
|
||||||
'common.email': '邮箱',
|
'common.email': '邮箱',
|
||||||
'common.password': '密码',
|
'common.password': '密码',
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const zhTw: Record<string, string> = {
|
|||||||
'common.none': '無',
|
'common.none': '無',
|
||||||
'common.date': '日期',
|
'common.date': '日期',
|
||||||
'common.rename': '重新命名',
|
'common.rename': '重新命名',
|
||||||
|
'common.discardChanges': '捨棄變更',
|
||||||
|
'common.discard': '捨棄',
|
||||||
'common.name': '名稱',
|
'common.name': '名稱',
|
||||||
'common.email': '郵箱',
|
'common.email': '郵箱',
|
||||||
'common.password': '密碼',
|
'common.password': '密碼',
|
||||||
|
|||||||
@@ -3002,6 +3002,10 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite, onRefr
|
|||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [showAddTrip, setShowAddTrip] = useState(false)
|
const [showAddTrip, setShowAddTrip] = useState(false)
|
||||||
const [unlinkTarget, setUnlinkTarget] = useState<{ trip_id: number; title: string } | null>(null)
|
const [unlinkTarget, setUnlinkTarget] = useState<{ trip_id: number; title: string } | null>(null)
|
||||||
|
const [showDiscardConfirm, setShowDiscardConfirm] = useState(false)
|
||||||
|
|
||||||
|
const isDirty = title !== journey.title || subtitle !== (journey.subtitle || '')
|
||||||
|
const handleClose = () => { if (isDirty) setShowDiscardConfirm(true); else onClose() }
|
||||||
const coverRef = useRef<HTMLInputElement>(null)
|
const coverRef = useRef<HTMLInputElement>(null)
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -3060,12 +3064,12 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite, onRefr
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-[200] flex items-end md:items-center justify-center md:p-5 overscroll-none" style={{ background: 'rgba(9,9,11,0.75)' }} onClick={onClose} onTouchMove={e => { if (e.target === e.currentTarget) e.preventDefault() }}>
|
<div className="fixed inset-0 z-[200] flex items-end md:items-center justify-center md:p-5 overscroll-none" style={{ background: 'rgba(9,9,11,0.75)' }} onClick={handleClose} onTouchMove={e => { if (e.target === e.currentTarget) e.preventDefault() }}>
|
||||||
<div className="bg-white dark:bg-zinc-900 rounded-t-2xl md:rounded-2xl shadow-[0_20px_40px_rgba(0,0,0,0.2)] max-w-[480px] w-full max-h-[85vh] md:max-h-[90vh] flex flex-col overflow-hidden" style={{ paddingBottom: 'env(safe-area-inset-bottom, 0px)' }} onClick={e => e.stopPropagation()}>
|
<div className="bg-white dark:bg-zinc-900 rounded-t-2xl md:rounded-2xl shadow-[0_20px_40px_rgba(0,0,0,0.2)] max-w-[480px] w-full max-h-[85vh] md:max-h-[90vh] flex flex-col overflow-hidden" style={{ paddingBottom: 'env(safe-area-inset-bottom, 0px)' }} onClick={e => e.stopPropagation()}>
|
||||||
|
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-zinc-200 dark:border-zinc-700">
|
<div className="flex items-center justify-between px-6 py-4 border-b border-zinc-200 dark:border-zinc-700">
|
||||||
<h2 className="text-[16px] font-bold text-zinc-900 dark:text-white">{t('journey.settings.title')}</h2>
|
<h2 className="text-[16px] font-bold text-zinc-900 dark:text-white">{t('journey.settings.title')}</h2>
|
||||||
<button onClick={onClose} className="w-8 h-8 rounded-lg flex items-center justify-center text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
<button onClick={handleClose} className="w-8 h-8 rounded-lg flex items-center justify-center text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
||||||
<X size={16} />
|
<X size={16} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -3212,7 +3216,7 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite, onRefr
|
|||||||
{journey.status === 'archived' ? <ArchiveRestore size={14} /> : <Archive size={14} />}
|
{journey.status === 'archived' ? <ArchiveRestore size={14} /> : <Archive size={14} />}
|
||||||
<span className="hidden md:inline">{journey.status === 'archived' ? t('journey.settings.reopenJourney') : t('journey.settings.endJourney')}</span>
|
<span className="hidden md:inline">{journey.status === 'archived' ? t('journey.settings.reopenJourney') : t('journey.settings.endJourney')}</span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={onClose} className="h-9 px-3.5 rounded-lg border border-zinc-200 dark:border-zinc-600 text-[13px] font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700">{t('common.cancel')}</button>
|
<button onClick={handleClose} className="h-9 px-3.5 rounded-lg border border-zinc-200 dark:border-zinc-600 text-[13px] font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700">{t('common.cancel')}</button>
|
||||||
<button onClick={handleSave} disabled={saving || !title.trim()} className="h-9 px-3.5 rounded-lg bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[13px] font-medium hover:bg-zinc-800 dark:hover:bg-zinc-100 disabled:opacity-40">
|
<button onClick={handleSave} disabled={saving || !title.trim()} className="h-9 px-3.5 rounded-lg bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 text-[13px] font-medium hover:bg-zinc-800 dark:hover:bg-zinc-100 disabled:opacity-40">
|
||||||
{saving ? t('common.saving') : t('common.save')}
|
{saving ? t('common.saving') : t('common.save')}
|
||||||
</button>
|
</button>
|
||||||
@@ -3259,6 +3263,16 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite, onRefr
|
|||||||
confirmLabel={t('common.delete')}
|
confirmLabel={t('common.delete')}
|
||||||
danger
|
danger
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={showDiscardConfirm}
|
||||||
|
onClose={() => setShowDiscardConfirm(false)}
|
||||||
|
onConfirm={() => { setShowDiscardConfirm(false); onClose() }}
|
||||||
|
title={t('common.discardChanges')}
|
||||||
|
message={t('journey.editor.discardChangesConfirm')}
|
||||||
|
confirmLabel={t('common.discard')}
|
||||||
|
danger
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user