@@ -395,6 +395,75 @@ function DeleteDialog({ prompt, onCancel, onConfirm }: DeleteDialogProps) {
395395 )
396396}
397397
398+ interface OverlapEntry {
399+ id : number
400+ name : string
401+ ics_url : string
402+ sync_all : boolean
403+ keep_local : boolean
404+ }
405+
406+ interface OverlapWarning {
407+ overlapping : OverlapEntry [ ]
408+ pendingSubmit : ( ) => void
409+ }
410+
411+ function OverlapWarningDialog ( {
412+ warning,
413+ onCancel,
414+ onConfirm,
415+ } : {
416+ warning : OverlapWarning | null
417+ onCancel : ( ) => void
418+ onConfirm : ( ) => void
419+ } ) {
420+ if ( ! warning ) return null
421+ return (
422+ < div className = "app-dialog show" onClick = { onCancel } >
423+ < div className = "app-dialog-modal" onClick = { e => e . stopPropagation ( ) } >
424+ < div className = "app-dialog-header" >
425+ < h3 > Duplicate Calendar Target</ h3 >
426+ </ div >
427+ < div className = "app-dialog-body" >
428+ < p > Another destination already syncs to this CalDAV calendar:</ p >
429+ { warning . overlapping . map ( d => (
430+ < div
431+ key = { d . id }
432+ style = { {
433+ marginBottom : 8 ,
434+ padding : '8px 12px' ,
435+ background : 'var(--card-bg)' ,
436+ borderRadius : 4 ,
437+ border : '1px solid var(--card-border)' ,
438+ } }
439+ >
440+ < strong > { d . name } </ strong >
441+ < br />
442+ < span style = { { fontSize : 13 , opacity : 0.7 } } > { d . ics_url } </ span >
443+ < br />
444+ < span style = { { fontSize : 12 , opacity : 0.6 } } >
445+ sync_all: { d . sync_all ? 'on' : 'off' } , keep_local: { d . keep_local ? 'on' : 'off' }
446+ </ span >
447+ </ div >
448+ ) ) }
449+ < p style = { { marginTop : 12 , fontSize : 13 } } >
450+ Multiple destinations targeting the same calendar operate independently. If keep_local
451+ is disabled on any destination, it may delete events uploaded by another.
452+ </ p >
453+ < div className = "dialog-actions" >
454+ < button className = "app-btn app-btn-subtle" onClick = { onCancel } >
455+ Cancel
456+ </ button >
457+ < button className = "app-btn app-btn-primary" onClick = { onConfirm } >
458+ Continue Anyway
459+ </ button >
460+ </ div >
461+ </ div >
462+ </ div >
463+ </ div >
464+ )
465+ }
466+
398467// --- Component ---
399468
400469export default function Home ( ) {
@@ -431,6 +500,9 @@ export default function Home() {
431500 // Delete confirmation
432501 const [ deletePrompt , setDeletePrompt ] = useState < DeletePrompt | null > ( null )
433502
503+ // Overlap warning
504+ const [ overlapWarning , setOverlapWarning ] = useState < OverlapWarning | null > ( null )
505+
434506 // ── Data fetching ──────────────────────────────────────────────
435507
436508 const fetchSources = useCallback ( async ( ) => {
@@ -606,17 +678,39 @@ export default function Home() {
606678
607679 async function submitDest ( e : React . FormEvent ) {
608680 e . preventDefault ( )
609- const url = editingDest ? `/api/destinations/${ editingDest . id } ` : '/api/destinations'
610- const method = editingDest ? 'PUT' : 'POST'
611681 const { sync_interval_hours, sync_interval_minutes, sync_interval_seconds, ...rest } = destForm
612682 const body = {
613683 ...rest ,
614684 sync_interval_secs : toSecs ( sync_interval_hours , sync_interval_minutes , sync_interval_seconds ) ,
615685 }
616- await apiSubmit ( url , method , body , ( ) => {
617- closeDestDialog ( )
618- fetchDestinations ( )
619- } )
686+
687+ const doSubmit = async ( ) => {
688+ const url = editingDest ? `/api/destinations/${ editingDest . id } ` : '/api/destinations'
689+ const method = editingDest ? 'PUT' : 'POST'
690+ await apiSubmit ( url , method , body , ( ) => {
691+ closeDestDialog ( )
692+ fetchDestinations ( )
693+ } )
694+ setOverlapWarning ( null )
695+ }
696+
697+ try {
698+ const params = new URLSearchParams ( {
699+ caldav_url : destForm . caldav_url ,
700+ calendar_name : destForm . calendar_name ,
701+ } )
702+ if ( editingDest ) params . set ( 'exclude_id' , String ( editingDest . id ) )
703+ const res = await api . get ( `/api/destinations/check-overlap?${ params } ` )
704+ const data = res . data as { overlapping : OverlapEntry [ ] }
705+ if ( data . overlapping ?. length ) {
706+ setOverlapWarning ( { overlapping : data . overlapping , pendingSubmit : doSubmit } )
707+ return
708+ }
709+ } catch {
710+ // If overlap check fails, proceed without warning
711+ }
712+
713+ await doSubmit ( )
620714 }
621715
622716 // ── Delete handler ─────────────────────────────────────────────
@@ -1088,6 +1182,11 @@ export default function Home() {
10881182 onCancel = { ( ) => setDeletePrompt ( null ) }
10891183 onConfirm = { handleDeleteConfirm }
10901184 />
1185+ < OverlapWarningDialog
1186+ warning = { overlapWarning }
1187+ onCancel = { ( ) => setOverlapWarning ( null ) }
1188+ onConfirm = { ( ) => overlapWarning ?. pendingSubmit ( ) }
1189+ />
10911190 </ div >
10921191 )
10931192}
0 commit comments