@@ -27,6 +27,21 @@ function eventDateKey(event) {
2727 return toDateKey ( d ) ;
2828}
2929
30+ function toTimeSortValue ( event ) {
31+ if ( ! event ?. time ) return Number . POSITIVE_INFINITY ;
32+ const [ hour , minute ] = String ( event . time ) . split ( ':' ) . map ( Number ) ;
33+ if ( Number . isNaN ( hour ) || Number . isNaN ( minute ) ) return Number . POSITIVE_INFINITY ;
34+ return hour * 60 + minute ;
35+ }
36+
37+ function sortEventsForDay ( events ) {
38+ return [ ...events ] . sort ( ( a , b ) => {
39+ const timeDelta = toTimeSortValue ( a ) - toTimeSortValue ( b ) ;
40+ if ( timeDelta !== 0 ) return timeDelta ;
41+ return String ( a . name || '' ) . localeCompare ( String ( b . name || '' ) ) ;
42+ } ) ;
43+ }
44+
3045function formatDate ( dateStr ) {
3146 if ( ! dateStr ) return '' ;
3247 const d = new Date ( `${ dateStr } T12:00:00` ) ;
@@ -432,17 +447,18 @@ function EventPopup({ event, onClose, isAdminView, user }) {
432447
433448// ─── Event pill ───────────────────────────────────────────────────────────────
434449
435- function EventPill ( { event, onSelect, isAdminView } ) {
450+ function EventRow ( { event, onSelect, isAdminView } ) {
436451 const colors = pillColors ( event , isAdminView ) ;
437452 const badgeText = getBadgeText ( event , isAdminView ) ;
453+ const timeLabel = formatTime ( event . time ) ;
438454
439455 return (
440456 < button
441457 type = "button"
442458 onClick = { ( ) => onSelect ( event ) }
443459 className = { [
444- 'flex w-full items-start gap-1.5 rounded-md border px-1.5 py-1' ,
445- 'text-left text-[12px] sm:text-[13px] font-semibold leading-tight' ,
460+ 'flex w-full items-center gap-1.5 rounded border px-1.5 py-1' ,
461+ 'text-left text-[11px] font-medium leading-tight' ,
446462 'transition-all duration-150 hover:border-white/30 hover:brightness-110' ,
447463 colors . bg ,
448464 colors . text ,
@@ -451,16 +467,21 @@ function EventPill({ event, onSelect, isAdminView }) {
451467 title = { event . name }
452468 >
453469 < span className = { [ 'mt-1 h-1.5 w-1.5 shrink-0 rounded-full' , colors . dot ] . join ( ' ' ) } />
454- < div className = "min-w-0 flex-1" >
455- < div className = "truncate" >
456- { event . name || 'Untitled' }
457- </ div >
458- { badgeText && (
459- < div className = "truncate pt-0.5 text-[10px] opacity-80" >
460- { badgeText }
461- </ div >
470+ < div className = "min-w-0 flex-1 truncate" >
471+ { timeLabel && (
472+ < span className = "mr-1.5 font-semibold text-white/90" >
473+ { timeLabel }
474+ </ span >
462475 ) }
476+ < span className = "truncate" >
477+ { event . name || 'Untitled' }
478+ </ span >
463479 </ div >
480+ { badgeText && (
481+ < span className = "hidden rounded border border-current/30 px-1 py-0.5 text-[9px] uppercase tracking-wide opacity-75 sm:inline" >
482+ { badgeText }
483+ </ span >
484+ ) }
464485 </ button >
465486 ) ;
466487}
@@ -485,18 +506,23 @@ function DayLabels() {
485506// ─── Calendar cell ────────────────────────────────────────────────────────────
486507
487508function CalCell ( { date, isCurrentMonth, isToday, events, onSelectEvent, isAdminView } ) {
509+ const sortedEvents = sortEventsForDay ( events ) ;
510+ const mobileRows = sortedEvents . slice ( 0 , 2 ) ;
511+ const desktopOnlyRows = sortedEvents . slice ( 2 , 4 ) ;
512+ const hiddenCount = Math . max ( sortedEvents . length - 4 , 0 ) ;
513+
488514 return (
489515 < div
490516 className = { [
491- 'min-h-[120px ] sm:min-h-[130px ] border-b border-r border-slate-700/60 p-1.5 transition-colors duration-150' ,
517+ 'min-h-[106px ] sm:min-h-[124px ] border-b border-r border-slate-700/60 p-1.5 transition-colors duration-150' ,
492518 isCurrentMonth ? '' : 'opacity-30' ,
493519 isToday ? 'bg-cyan-500/[0.12]' : 'hover:bg-slate-700/35' ,
494520 ] . join ( ' ' ) }
495521 >
496- < div className = "mb-1 flex justify-end " >
522+ < div className = "mb-1 flex justify-start " >
497523 < span
498524 className = { [
499- 'flex h-6 w-6 items-center justify-center rounded-full text-sm font-semibold' ,
525+ 'flex h-6 min- w-6 items-center justify-center rounded-full px-1.5 text-xs font-semibold' ,
500526 isToday ? 'bg-cyan-400 text-slate-950' : isCurrentMonth ? 'text-slate-200' : 'text-slate-600' ,
501527 ] . join ( ' ' ) }
502528 >
@@ -505,25 +531,120 @@ function CalCell({ date, isCurrentMonth, isToday, events, onSelectEvent, isAdmin
505531 </ div >
506532
507533 < div className = "flex flex-col gap-0.5" >
508- { events . slice ( 0 , 3 ) . map ( ( ev ) => (
509- < EventPill
534+ { mobileRows . map ( ( ev ) => (
535+ < EventRow
510536 key = { ev . id }
511537 event = { ev }
512538 onSelect = { onSelectEvent }
513539 isAdminView = { isAdminView }
514540 />
515541 ) ) }
516542
517- { events . length > 3 && (
518- < span className = "pl-1 text-[10px] text-slate-400" >
519- +{ events . length - 3 } more
543+ { desktopOnlyRows . map ( ( ev ) => (
544+ < div key = { ev . id } className = "hidden sm:block" >
545+ < EventRow
546+ event = { ev }
547+ onSelect = { onSelectEvent }
548+ isAdminView = { isAdminView }
549+ />
550+ </ div >
551+ ) ) }
552+
553+ { hiddenCount > 0 && (
554+ < span className = "pl-1 text-[10px] font-medium text-slate-400" >
555+ +{ hiddenCount } more
520556 </ span >
521557 ) }
522558 </ div >
523559 </ div >
524560 ) ;
525561}
526562
563+ function MobileMonthAgenda ( { monthEvents, onSelectEvent, isAdminView } ) {
564+ if ( monthEvents . length === 0 ) {
565+ return (
566+ < div className = "px-4 py-8 text-center text-sm text-slate-400" >
567+ No events scheduled this month.
568+ </ div >
569+ ) ;
570+ }
571+
572+ const groupedEvents = monthEvents . reduce ( ( groups , item ) => {
573+ const lastGroup = groups [ groups . length - 1 ] ;
574+ if ( ! lastGroup || lastGroup . dateKey !== item . dateKey ) {
575+ groups . push ( { dateKey : item . dateKey , events : [ item . event ] } ) ;
576+ return groups ;
577+ }
578+ lastGroup . events . push ( item . event ) ;
579+ return groups ;
580+ } , [ ] ) ;
581+
582+ return (
583+ < div className = "space-y-3 px-3 pb-4 pt-2" >
584+ { groupedEvents . map ( ( { dateKey, events } ) => {
585+ const [ year , month , day ] = dateKey . split ( '-' ) ;
586+ const date = new Date ( Number ( year ) , Number ( month ) - 1 , Number ( day ) ) ;
587+ const weekday = date . toLocaleDateString ( 'en-US' , { weekday : 'short' } ) . toUpperCase ( ) ;
588+ const dayOfMonth = String ( date . getDate ( ) ) ;
589+
590+ return (
591+ < div key = { dateKey } className = "flex items-start gap-2" >
592+ < div className = "w-[44px] shrink-0 pt-1 text-center" >
593+ < div className = "text-[10px] font-bold uppercase tracking-wider text-slate-400" >
594+ { weekday }
595+ </ div >
596+ < div className = "text-xl font-semibold leading-6 text-slate-100" >
597+ { dayOfMonth }
598+ </ div >
599+ </ div >
600+
601+ < div className = "min-w-0 flex-1 space-y-1.5" >
602+ { events . map ( ( event ) => {
603+ const colors = pillColors ( event , isAdminView ) ;
604+ const badgeText = getBadgeText ( event , isAdminView ) ;
605+ const timeLabel = formatTime ( event . time ) || 'All day' ;
606+
607+ return (
608+ < button
609+ key = { `${ dateKey } -${ event . id || event . _id || event . name } ` }
610+ type = "button"
611+ onClick = { ( ) => onSelectEvent ( event ) }
612+ className = { [
613+ 'flex w-full items-end justify-between gap-3 rounded-lg border px-3 py-2.5 text-left transition-all' ,
614+ 'hover:border-white/30 hover:brightness-110' ,
615+ colors . bg ,
616+ colors . text ,
617+ colors . border ,
618+ ] . join ( ' ' ) }
619+ >
620+ < div className = "min-w-0 flex-1" >
621+ < div className = "whitespace-normal break-words text-sm font-semibold leading-snug" >
622+ { event . name || 'Untitled Event' }
623+ </ div >
624+ < div className = "pt-1 text-xs font-semibold text-white/85" >
625+ { timeLabel }
626+ </ div >
627+ </ div >
628+
629+ < div className = "min-w-[92px] text-right" >
630+ < div className = "whitespace-normal break-words text-xs opacity-85" >
631+ { event . location || '\u00A0' }
632+ </ div >
633+ < div className = "pt-1 text-[10px] uppercase tracking-wide opacity-80" >
634+ { badgeText || '\u00A0' }
635+ </ div >
636+ </ div >
637+ </ button >
638+ ) ;
639+ } ) }
640+ </ div >
641+ </ div >
642+ ) ;
643+ } ) }
644+ </ div >
645+ ) ;
646+ }
647+
527648// ─── Main export ──────────────────────────────────────────────────────────────
528649
529650export default function CalendarView ( { events, isAdminView = false , user, canCreateEvent = false } ) {
@@ -539,6 +660,9 @@ export default function CalendarView({ events, isAdminView = false, user, canCre
539660 if ( ! map [ key ] ) map [ key ] = [ ] ;
540661 map [ key ] . push ( ev ) ;
541662 } ) ;
663+ Object . keys ( map ) . forEach ( ( key ) => {
664+ map [ key ] = sortEventsForDay ( map [ key ] ) ;
665+ } ) ;
542666 return map ;
543667 } , [ events ] ) ;
544668
@@ -577,6 +701,17 @@ export default function CalendarView({ events, isAdminView = false, user, canCre
577701 . filter ( ( c ) => c . isCurrentMonth )
578702 . reduce ( ( sum , c ) => sum + c . events . length , 0 ) ;
579703
704+ const monthEvents = useMemo ( ( ) => {
705+ return cells
706+ . filter ( ( c ) => c . isCurrentMonth && c . events . length > 0 )
707+ . flatMap ( ( c ) =>
708+ c . events . map ( ( event ) => ( {
709+ event,
710+ dateKey : c . key ,
711+ } ) )
712+ ) ;
713+ } , [ cells ] ) ;
714+
580715 return (
581716 < >
582717 { selectedEvent && (
@@ -588,7 +723,7 @@ export default function CalendarView({ events, isAdminView = false, user, canCre
588723 />
589724 ) }
590725
591- < div className = "overflow-hidden rounded-xl border border-slate-500/50 bg-slate-800/85 shadow-[0_0_0_1px_rgba(148,163,184,0.05)]" >
726+ < div className = "flex h-full max-h-full flex-col overflow-hidden rounded-xl border border-slate-500/50 bg-slate-800/85 shadow-[0_0_0_1px_rgba(148,163,184,0.05)] sm:h-auto sm:max-h-none " >
592727 < div className = "flex flex-wrap items-start justify-between gap-3 border-b border-slate-600/50 px-5 py-4" >
593728 < div className = "space-y-3" >
594729 < div className = "flex flex-wrap items-center gap-3" >
@@ -675,17 +810,27 @@ export default function CalendarView({ events, isAdminView = false, user, canCre
675810 </ div >
676811 </ div >
677812
678- < DayLabels />
813+ < div className = "min-h-0 flex-1 overflow-y-auto sm:hidden" >
814+ < MobileMonthAgenda
815+ monthEvents = { monthEvents }
816+ onSelectEvent = { setSelectedEvent }
817+ isAdminView = { isAdminView }
818+ />
819+ </ div >
679820
680- < div className = "grid grid-cols-7 [&>*:nth-child(7n)]:border-r-0" >
681- { cells . map ( ( cell ) => (
682- < CalCell
683- key = { cell . key }
684- { ...cell }
685- onSelectEvent = { setSelectedEvent }
686- isAdminView = { isAdminView }
687- />
688- ) ) }
821+ < div className = "hidden sm:block" >
822+ < DayLabels />
823+
824+ < div className = "grid grid-cols-7 [&>*:nth-child(7n)]:border-r-0" >
825+ { cells . map ( ( cell ) => (
826+ < CalCell
827+ key = { cell . key }
828+ { ...cell }
829+ onSelectEvent = { setSelectedEvent }
830+ isAdminView = { isAdminView }
831+ />
832+ ) ) }
833+ </ div >
689834 </ div >
690835
691836 { isAdminView ? (
0 commit comments