@@ -98,41 +98,81 @@ function MiniCalendar({ selected, onChange, eventDates=new Set() }) {
) ;
) ;
}
}
// ── Mobile Date Picker (accordion month view) ─────────────────────────────── ──
// ── Mobile Filter Bar (Schedule view: keyword+type filters with month nav; Day view: calendar accordion) ──
function MobileDatePick er ( { selected , onChange , eventDates = new Set ( ) } ) {
function MobileScheduleFilt er ( { selected , onMonth Change , view , eventTypes , filterKeyword , onFilterKeyword , filterTypeId , onFilterTypeId , eventDates = new Set ( ) } ) {
// Day view: keep accordion calendar
const [ open , setOpen ] = useState ( false ) ;
const [ open , setOpen ] = useState ( false ) ;
const [ cur , setCur ] = useState ( ( ) => { const d = new Date ( selected || Date . now ( ) ) ; d . setDate ( 1 ) ; return d ; } ) ;
const y = selected . getFullYear ( ) , m = selected . getMonth ( ) ;
const y = cur . getFullYear ( ) , m = cur . getMonth ( ) , first = new Date ( y , m , 1 ) . getDay ( ) , total = daysInMonth ( y , m ) ;
const cells = [ ] ; for ( let i = 0 ; i < first ; i + + ) cells.push ( null ) ; for ( let d = 1 ; d < = total ; d + + ) cells.push ( d ) ;
const today = new Date ( ) ;
const today = new Date ( ) ;
if ( view === 'day' ) {
const first = new Date ( y , m , 1 ) . getDay ( ) , total = daysInMonth ( y , m ) ;
const cells = [ ] ; for ( let i = 0 ; i < first ; i + + ) cells.push ( null ) ; for ( let d = 1 ; d < = total ; d + + ) cells.push ( d ) ;
return (
< div style = { { borderBottom : '1px solid var(--border)' , background : 'var(--surface)' } } >
< button onClick = { ( ) => setOpen ( v => ! v ) } style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' , width : '100%' , padding : '10px 16px' , background : 'none' , border : 'none' , cursor : 'pointer' , fontSize : 14 , fontWeight : 600 , color : 'var(--text-primary)' } } >
< span > { MONTHS [ m ] } { y } < / span >
< span style = { { fontSize : 10 , transform : open ? 'rotate(180deg)' : 'none' , display : 'inline-block' , transition : 'transform 0.2s' } } > ▼ < / span >
< / button >
{ open && (
< div style = { { padding : '8px 12px 12px' , userSelect : 'none' } } >
< div style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' , marginBottom : 8 } } >
< button style = { { background : 'none' , border : 'none' , cursor : 'pointer' , padding : '4px 10px' , fontSize : 16 , color : 'var(--text-secondary)' } } onClick = { ( ) => onMonthChange ( - 1 ) } > ‹ < / button >
< button style = { { background : 'none' , border : 'none' , cursor : 'pointer' , padding : '4px 10px' , fontSize : 16 , color : 'var(--text-secondary)' } } onClick = { ( ) => onMonthChange ( 1 ) } > › < / button >
< / div >
< div style = { { display : 'grid' , gridTemplateColumns : 'repeat(7,1fr)' , gap : 2 , fontSize : 12 } } >
{ DAYS . map ( d => < div key = { d } style = { { textAlign : 'center' , fontWeight : 600 , color : 'var(--text-tertiary)' , padding : '2px 0' } } > { d [ 0 ] } < / div > ) }
{ cells . map ( ( d , i ) => {
if ( ! d ) return < div key = { i } / > ;
const date = new Date ( y , m , d ) , isSel = sameDay ( date , selected ) , isToday = sameDay ( date , today ) ;
const key = ` ${ y } - ${ String ( m + 1 ) . padStart ( 2 , '0' ) } - ${ String ( d ) . padStart ( 2 , '0' ) } ` ;
return (
< div key = { i } onClick = { ( ) => { const nd = new Date ( y , m , d ) ; onMonthChange ( 0 , nd ) ; setOpen ( false ) ; } } style = { { textAlign : 'center' , padding : '5px 2px' , borderRadius : 4 , cursor : 'pointer' , background : isSel ? 'var(--primary)' : 'transparent' , color : isSel ? 'white' : isToday ? 'var(--primary)' : 'var(--text-primary)' , fontWeight : isToday && ! isSel ? 700 : 400 , position : 'relative' } } >
{ d }
{ eventDates . has ( key ) && ! isSel && < span style = { { position : 'absolute' , bottom : 2 , left : '50%' , transform : 'translateX(-50%)' , width : 4 , height : 4 , borderRadius : '50%' , background : 'var(--primary)' , display : 'block' } } / > }
< / div >
) ;
} ) }
< / div >
< / div >
) }
< / div >
) ;
}
// Schedule view: filter bar with month nav + keyword + event type
const hasFilters = filterKeyword || filterTypeId ;
return (
return (
< div style = { { borderBottom : '1px solid var(--border)' , background : 'var(--surface )' } } >
< div style = { { background : 'var(--surface)' , b orderBottom : '1px solid var(--border)' } } >
< button onClick = { ( ) => setOpen ( v => ! v ) } style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' , width : '100%' , padding : '10px 16px' , background : 'none' , border : 'none' , cursor : 'pointer' , fontSize : 14 , fontWeight : 600 , color : 'var(--text-primary)' } } >
{ /* Month nav row */ }
< span > { MONTHS [ m ] } { y } < / span >
< div style = { { display : 'flex' , alignItems : 'center' , padding : '8px 16px 0' , gap : 8 } } >
< span style = { { fontSize : 10 , transform : open ? 'rotate(180deg)' : 'none' , display : 'inline-block' , transition : 'transform 0.2s' } } > ▼ < / spa n>
< button onClick = { ( ) => onMonthChange ( - 1 ) } style = { { background : 'none' , border : 'none' , cursor : 'pointer' , color : 'var(--text-secondary)' , fontSize : 18 , padding : '2px 6px' , lineHeight : 1 } } > ‹ < / butto n>
< / butto n>
< span style = { { flex : 1 , textAlign : 'center' , fontSize : 14 , fontWeight : 600 } } > { MONTHS [ m ] } { y } < / spa n>
{ open && (
< button onClick = { ( ) => onMonthChange ( 1 ) } style = { { background : 'none' , border : 'none' , cursor : 'pointer' , color : 'var(--text-secondary)' , fontSize : 18 , padding : '2px 6px' , lineHeight : 1 } } > › < / button >
< div style = { { padding : '8px 12px 12px' , userSelect : 'none' } } >
< / div >
< div style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' , marginBottom : 8 } } >
{ /* Filter inputs */ }
< button style = { { background : 'none' , border : 'none' , cursor : 'pointer' , padding: '4 px 10px' , fontSize : 16 , color : 'var(--text-secondary)' } } onClick = { ( ) => { const n = new Date ( cur ) ; n . setMonth ( m - 1 ) ; setCur ( n ) ; } } > ‹ < / button >
< div style = { { padding : '8px 12 px 10px' , display : 'flex' , gap : 8 , alignItems : 'center' } } >
< button style = { { background : 'none' , border : 'none' , cursor : 'pointer' , padding : '4px 10px' , fontSize : 16 , color : 'var(--text-secondary)' } } onClick = { ( ) => { const n = new Date ( cur ) ; n . setMonth ( m + 1 ) ; setCur ( n ) ; } } > › < / button >
< div style = { { flex : 1 , position : 'relative' } } >
< / div >
< svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "var(--text-tertiary)" strokeWidth = "2" style = { { position : 'absolute' , left : 9 , top : '50%' , transform : 'translateY(-50%)' , pointerEvents : 'none' } } > < circle cx = "11" cy = "11" r = "8" / > < line x1 = "21" y1 = "21" x2 = "16.65" y2 = "16.65" / > < / svg >
< div style = { { display : 'grid' , gridTemplateColumns : 'repeat(7,1fr)' , gap : 2 , fontSize : 12 } } >
< input
{ DAYS . map ( d => < div key = { d } style = { { textAlign : 'center' , fontWeight : 600 , color : 'var(--text-tertiary)' , padding : '2px 0' } } > { d [ 0 ] } < / div > ) }
value = { filterKeyword }
{ cells . map ( ( d , i ) => {
onChange = { e => onFilterKeyword ( e . target . value ) }
if ( ! d ) return < div key = { i } / > ;
placeholder = "Search events…"
const date = new Date ( y , m , d ) , isSel = selected && sameDay ( date , new Date ( selected ) ) , isToday = sameDay ( date , today ) ;
style = { { width : '100%' , padding : '7px 8px 7px 28px' , border : '1px solid var(--border)' , borderRadius : 'var(--radius)' , background : 'var(--background)' , color : 'var(--text-primary)' , fontSize : 13 , boxSizing : 'border-box' } }
const key = ` ${ y } - ${ String ( m + 1 ) . padStart ( 2 , '0' ) } - ${ String ( d ) . padStart ( 2 , '0' ) } ` ;
/ >
return (
< div key = { i } onClick = { ( ) => { onChange ( date ) ; setOpen ( false ) ; } } style = { { textAlign : 'center' , padding : '5px 2px' , borderRadius : 4 , cursor : 'pointer' , background : isSel ? 'var(--primary)' : 'transparent' , color : isSel ? 'white' : isToday ? 'var(--primary)' : 'var(--text-primary)' , fontWeight : isToday ? 700 : 400 , position : 'relative' } } >
{ d }
{ eventDates . has ( key ) && ! isSel && < span style = { { position : 'absolute' , bottom : 2 , left : '50%' , transform : 'translateX(-50%)' , width : 4 , height : 4 , borderRadius : '50%' , background : 'var(--primary)' , display : 'block' } } / > }
< / div >
) ;
} ) }
< / div >
< / div >
< / div >
) }
< select
value = { filterTypeId }
onChange = { e => onFilterTypeId ( e . target . value ) }
style = { { padding : '7px 8px' , border : '1px solid var(--border)' , borderRadius : 'var(--radius)' , background : 'var(--background)' , color : 'var(--text-primary)' , fontSize : 13 , flexShrink : 0 , maxWidth : 130 } }
>
< option value = "" > All types < / option >
{ eventTypes . map ( t => < option key = { t . id } value = { t . id } > { t . name } < / option > ) }
< / select >
{ hasFilters && (
< button onClick = { ( ) => { onFilterKeyword ( '' ) ; onFilterTypeId ( '' ) ; } } style = { { background : 'none' , border : 'none' , cursor : 'pointer' , color : 'var(--text-tertiary)' , fontSize : 18 , padding : '2px 4px' , lineHeight : 1 , flexShrink : 0 } } > ✕ < / button >
) }
< / div >
< / div >
< / div >
) ;
) ;
}
}
@@ -610,20 +650,39 @@ function BulkImportPanel({ onImported, onCancel }) {
}
}
// ── Calendar Views ────────────────────────────────────────────────────────────
// ── Calendar Views ────────────────────────────────────────────────────────────
// Parse keyword string into array of terms.
// Quoted phrases ("foo bar") count as one term; space-separated words are individual OR terms.
function parseKeywords ( raw ) {
const terms = [ ] ;
const re = /"([^"]+)"|(\S+)/g ;
let match ;
while ( ( match = re . exec ( raw ) ) !== null ) terms . push ( ( match [ 1 ] || match [ 2 ] ) . toLowerCase ( ) ) ;
return terms ;
}
function ScheduleView ( { events , selectedDate , onSelect , filterKeyword = '' , filterTypeId = '' , isMobile = false } ) {
function ScheduleView ( { events , selectedDate , onSelect , filterKeyword = '' , filterTypeId = '' , isMobile = false } ) {
const y = selectedDate . getFullYear ( ) , m = selectedDate . getMonth ( ) ;
const y = selectedDate . getFullYear ( ) , m = selectedDate . getMonth ( ) ;
const monthStart = new Date ( y , m , 1 ) , monthEnd = new Date ( y, m + 1 , 0 , 23 , 59 , 59 ) ;
const today = new Date ( ) ; toda y. setHours ( 0 , 0 , 0 , 0 ) ;
const kw = filter Keyword. toLowerCase ( ) . trim ( ) ;
const terms = parse Keywords ( filterKeyword ) ;
const hasFilters = terms . length > 0 || ! ! filterTypeId ;
// Always show from today forward (desktop and mobile).
// Desktop: when no filters, restrict to selected month for browsing context.
// Mobile: always from today forward regardless of filters.
// With any filter active: always today+future on both platforms.
const from = ( hasFilters || isMobile ) ? today : new Date ( y , m , 1 ) ;
const to = ( hasFilters || isMobile ) ? new Date ( 9999 , 11 , 31 ) : new Date ( y , m + 1 , 0 , 23 , 59 , 59 ) ;
const filtered = events . filter ( e => {
const filtered = events . filter ( e => {
const s = new Date ( e . start _at ) ;
const s = new Date ( e . start _at ) ;
if ( s < monthStart | | s > monthEnd ) return false ;
if ( s < from | | s > to ) return false ;
if ( filterTypeId && String ( e . event _type _id ) !== String ( filterTypeId ) ) return false ;
if ( filterTypeId && String ( e . event _type _id ) !== String ( filterTypeId ) ) return false ;
if ( kw && ! [
if ( terms . length > 0 ) {
e . title || '' , e . location || '' , e . description || ''
const haystack = [ e . title || '' , e . location || '' , e . description || '' ] . join ( ' ' ) . toLowerCase ( ) ;
] . some ( f => f . toLowerCase ( ) . includes ( kw ) ) ) return false ;
if ( ! terms . some ( t => haystack . includes ( t ) ) ) return false ;
}
return true ;
return true ;
} ) ;
} ) ;
if ( ! filtered . length ) return < div style = { { textAlign : 'center' , padding : '60px 20px' , color : 'var(--text-tertiary)' , fontSize : 14 } } > { kw || filterTypeId ? 'No events match your filters' : 'No events in' } { ! kw && ! filterTypeId && ` ${ MONTHS [ m ] } ${ y } ` } < / div > ;
const emptyMsg = hasFilters ? 'No events match your filters' : isMobile ? 'No upcoming events' : ` No events in ${ MONTHS [ m ] } ${ y } ` ;
if ( ! filtered . length ) return < div style = { { textAlign : 'center' , padding : '60px 20px' , color : 'var(--text-tertiary)' , fontSize : 14 } } > { emptyMsg } < / div > ;
return < > { filtered . map ( e => { const s = new Date ( e . start _at ) ; const col = e . event _type ? . colour || '#9ca3af' ;
return < > { filtered . map ( e => { const s = new Date ( e . start _at ) ; const col = e . event _type ? . colour || '#9ca3af' ;
// Desktop: original pre-v0.9.64 sizes. Mobile: compact sizes from v0.9.64
// Desktop: original pre-v0.9.64 sizes. Mobile: compact sizes from v0.9.64
const rowPad = isMobile ? '12px 14px' : '14px 20px' ;
const rowPad = isMobile ? '12px 14px' : '14px 20px' ;
@@ -691,14 +750,24 @@ function layoutEvents(evs) {
return result ;
return result ;
}
}
function DayView ( { events , selectedDate , onSelect } ) {
function DayView ( { events , selectedDate , onSelect , onSwipe } ) {
const hours = Array . from ( { length : DAY _END - DAY _START } , ( _ , i ) => i + DAY _START ) ;
const hours = Array . from ( { length : DAY _END - DAY _START } , ( _ , i ) => i + DAY _START ) ;
const day = events . filter ( e => sameDay ( new Date ( e . start _at ) , selectedDate ) ) ;
const day = events . filter ( e => sameDay ( new Date ( e . start _at ) , selectedDate ) ) ;
const scrollRef = useRef ( null ) ;
const scrollRef = useRef ( null ) ;
const touchRef = useRef ( { x : 0 , y : 0 } ) ;
useEffect ( ( ) => { if ( scrollRef . current ) scrollRef . current . scrollTop = 7 * HOUR _H ; } , [ selectedDate ] ) ;
useEffect ( ( ) => { if ( scrollRef . current ) scrollRef . current . scrollTop = 7 * HOUR _H ; } , [ selectedDate ] ) ;
const fmtHour = h => h === 0 ? '12 AM' : h < 12 ? ` $ { h } AM ` : h = = = 12 ? ' 12 PM ' : ` $ { h - 12 } PM ` ;
const fmtHour = h => h === 0 ? '12 AM' : h < 12 ? ` $ { h } AM ` : h = = = 12 ? ' 12 PM ' : ` $ { h - 12 } PM ` ;
const handleTouchStart = e = > { touchRef . current = { x : e . touches [ 0 ] . clientX , y : e . touches [ 0 ] . clientY } ; } ;
const handleTouchEnd = e => {
const dx = e . changedTouches [ 0 ] . clientX - touchRef . current . x ;
const dy = Math . abs ( e . changedTouches [ 0 ] . clientY - touchRef . current . y ) ;
// Require horizontal swipe > 60px, not too vertical, and not from left edge (< 30px = back gesture)
if ( Math . abs ( dx ) > 60 && dy < 80 && touchRef . current . x > 30 ) {
onSwipe ? . ( dx < 0 ? 1 : - 1 ) ; // left = next day, right = prev day
}
} ;
return (
return (
< div style = { { display : 'flex' , flexDirection : 'column' , height : '100%' } } >
< div style = { { display : 'flex' , flexDirection : 'column' , height : '100%' } } onTouchStart = { handleTouchStart } onTouchEnd = { handleTouchEnd } >
< div style = { { display : 'flex' , borderBottom : '1px solid var(--border)' , padding : '8px 0 8px 60px' , fontSize : 13 , fontWeight : 600 , color : 'var(--primary)' , flexShrink : 0 } } >
< div style = { { display : 'flex' , borderBottom : '1px solid var(--border)' , padding : '8px 0 8px 60px' , fontSize : 13 , fontWeight : 600 , color : 'var(--primary)' , flexShrink : 0 } } >
< div style = { { textAlign : 'center' } } > < div > { DAYS [ selectedDate . getDay ( ) ] } < / div > < div style = { { fontSize : 28 , fontWeight : 700 } } > { selectedDate . getDate ( ) } < / div > < / div >
< div style = { { textAlign : 'center' } } > < div > { DAYS [ selectedDate . getDay ( ) ] } < / div > < div style = { { fontSize : 28 , fontWeight : 700 } } > { selectedDate . getDate ( ) } < / div > < / div >
< / div >
< / div >
@@ -740,10 +809,20 @@ function WeekView({ events, selectedDate, onSelect }) {
const ws = weekStart ( selectedDate ) , days = Array . from ( { length : 7 } , ( _ , i ) => { const d = new Date ( ws ) ; d . setDate ( d . getDate ( ) + i ) ; return d ; } ) ;
const ws = weekStart ( selectedDate ) , days = Array . from ( { length : 7 } , ( _ , i ) => { const d = new Date ( ws ) ; d . setDate ( d . getDate ( ) + i ) ; return d ; } ) ;
const hours = Array . from ( { length : DAY _END - DAY _START } , ( _ , i ) => i + DAY _START ) , today = new Date ( ) ;
const hours = Array . from ( { length : DAY _END - DAY _START } , ( _ , i ) => i + DAY _START ) , today = new Date ( ) ;
const scrollRef = useRef ( null ) ;
const scrollRef = useRef ( null ) ;
const touchRef = useRef ( { x : 0 , y : 0 } ) ;
useEffect ( ( ) => { if ( scrollRef . current ) scrollRef . current . scrollTop = 7 * HOUR _H ; } , [ selectedDate ] ) ;
useEffect ( ( ) => { if ( scrollRef . current ) scrollRef . current . scrollTop = 7 * HOUR _H ; } , [ selectedDate ] ) ;
const fmtHour = h => h === 0 ? '12 AM' : h < 12 ? ` $ { h } AM ` : h = = = 12 ? ' 12 PM ' : ` $ { h - 12 } PM ` ;
const fmtHour = h => h === 0 ? '12 AM' : h < 12 ? ` $ { h } AM ` : h = = = 12 ? ' 12 PM ' : ` $ { h - 12 } PM ` ;
const handleTouchStart = e = > { touchRef . current = { x : e . touches [ 0 ] . clientX , y : e . touches [ 0 ] . clientY } ; } ;
const handleTouchEnd = e => {
const dx = e . changedTouches [ 0 ] . clientX - touchRef . current . x ;
const dy = Math . abs ( e . changedTouches [ 0 ] . clientY - touchRef . current . y ) ;
// Require horizontal swipe > 60px, not too vertical, and not from left edge (< 30px = back gesture)
if ( Math . abs ( dx ) > 60 && dy < 80 && touchRef . current . x > 30 ) {
onSwipe ? . ( dx < 0 ? 1 : - 1 ) ; // left = next day, right = prev day
}
} ;
return (
return (
< div style = { { display : 'flex' , flexDirection : 'column' , height : '100%' } } >
< div style = { { display : 'flex' , flexDirection : 'column' , height : '100%' } } onTouchStart = { handleTouchStart } onTouchEnd = { handleTouchEnd } >
{ /* Day headers */ }
{ /* Day headers */ }
< div style = { { display : 'grid' , gridTemplateColumns : '60px repeat(7,1fr)' , borderBottom : '1px solid var(--border)' , background : 'var(--surface)' , flexShrink : 0 } } >
< div style = { { display : 'grid' , gridTemplateColumns : '60px repeat(7,1fr)' , borderBottom : '1px solid var(--border)' , background : 'var(--surface)' , flexShrink : 0 } } >
< div / >
< div / >
@@ -947,10 +1026,10 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
{ /* List view filters — only shown in Schedule list view */ }
{ /* List view filters — only shown in Schedule list view */ }
{ view === 'schedule' && panel === 'calendar' && (
{ view === 'schedule' && panel === 'calendar' && (
< div style = { { padding : '0 16px 16px' } } >
< div style = { { padding : '0 16px 16px' } } >
< div className = "section-label" style = { { marginBottom : 8 } } > Search < / div >
< div className = "section-label" style = { { marginBottom : 8 } } > Search ( today & amp ; future ) < / div >
< input
< input
className = "input"
className = "input"
placeholder = " Keyword…"
placeholder = { ` Keyword… (space = OR, "phrase") ` }
value = { filterKeyword }
value = { filterKeyword }
onChange = { e => setFilterKeyword ( e . target . value ) }
onChange = { e => setFilterKeyword ( e . target . value ) }
style = { { marginBottom : 8 , fontSize : 13 } }
style = { { marginBottom : 8 , fontSize : 13 } }
@@ -1016,15 +1095,28 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
< / div >
< / div >
< / div >
< / div >
{ /* Mobile date picker */ }
{ /* Mobile filter bar — Schedule view: filters + month nav; Day view: calendar accordion */ }
{ isMobile && (
{ isMobile && panel === 'calendar' && (
< MobileDatePicker selected = { selDate } onChange = { d => { setSelDate ( d ) ; setPanel ( 'calendar' ) ; } } eventDates = { eventDates } / >
< MobileScheduleFilter
selected = { selDate }
view = { view }
eventTypes = { eventTypes }
filterKeyword = { filterKeyword }
onFilterKeyword = { setFilterKeyword }
filterTypeId = { filterTypeId }
onFilterTypeId = { setFilterTypeId }
eventDates = { eventDates }
onMonthChange = { ( dir , exactDate ) => {
if ( exactDate ) { setSelDate ( exactDate ) ; }
else { const d = new Date ( selDate ) ; d . setMonth ( d . getMonth ( ) + dir ) ; d . setDate ( 1 ) ; setSelDate ( d ) ; }
} }
/ >
) }
) }
{ /* Calendar or panel content */ }
{ /* Calendar or panel content */ }
< div style = { { flex : 1 , overflowY : 'auto' , overflowX : panel === 'eventForm' ? 'auto' : 'hidden' } } >
< div style = { { flex : 1 , overflowY : 'auto' , overflowX : panel === 'eventForm' ? 'auto' : 'hidden' } } >
{ panel === 'calendar' && view === 'schedule' && < ScheduleView events = { events } selectedDate = { selDate } onSelect = { openDetail } filterKeyword = { filterKeyword } filterTypeId = { filterTypeId } isMobile = { isMobile } / > }
{ panel === 'calendar' && view === 'schedule' && < ScheduleView events = { events } selectedDate = { selDate } onSelect = { openDetail } filterKeyword = { filterKeyword } filterTypeId = { filterTypeId } isMobile = { isMobile } / > }
{ panel === 'calendar' && view === 'day' && < DayView events = { events } selectedDate = { selDate } onSelect = { openDetail } / > }
{ panel === 'calendar' && view === 'day' && < DayView events = { events } selectedDate = { selDate } onSelect = { openDetail } onSwipe = { isMobile ? dir => { const d = new Date ( selDate ) ; d . setDate ( d . getDate ( ) + dir ) ; setSelDate ( d ) ; } : undefined } / > }
{ panel === 'calendar' && view === 'week' && < WeekView events = { events } selectedDate = { selDate } onSelect = { openDetail } / > }
{ panel === 'calendar' && view === 'week' && < WeekView events = { events } selectedDate = { selDate } onSelect = { openDetail } / > }
{ panel === 'calendar' && view === 'month' && < MonthView events = { events } selectedDate = { selDate } onSelect = { openDetail } onSelectDay = { d => { setSelDate ( d ) ; setView ( 'schedule' ) ; } } / > }
{ panel === 'calendar' && view === 'month' && < MonthView events = { events } selectedDate = { selDate } onSelect = { openDetail } onSelectDay = { d => { setSelDate ( d ) ; setView ( 'schedule' ) ; } } / > }