Implement time-based week view with scrollable 24-hour layout
- Add comprehensive time grid structure with hourly and half-hourly divisions - Implement scrollable week view with sticky header and time labels - Create 25 time labels (12 AM through 11 PM plus boundary) with proper formatting - Add 25 matching time slot backgrounds for visual alignment - Style time labels with appropriate sizing and boundary indicators - Position events absolutely over time grid (basic positioning for now) - Set proper container heights and scrollable content area Note: Time slot alignment still needs refinement for complete 24-hour coverage 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		| @@ -40,18 +40,61 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|         "#3B82F6".to_string() |         "#3B82F6".to_string() | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     html! { |     // Generate time labels - 24 hours plus the final midnight boundary | ||||||
|         <div class="week-grid"> |     let mut time_labels: Vec<String> = (0..24).map(|hour| { | ||||||
|             // Weekday headers |         if hour == 0 { | ||||||
|             <div class="weekday-header">{"Sun"}</div> |             "12 AM".to_string() | ||||||
|             <div class="weekday-header">{"Mon"}</div> |         } else if hour < 12 { | ||||||
|             <div class="weekday-header">{"Tue"}</div> |             format!("{} AM", hour) | ||||||
|             <div class="weekday-header">{"Wed"}</div> |         } else if hour == 12 { | ||||||
|             <div class="weekday-header">{"Thu"}</div> |             "12 PM".to_string() | ||||||
|             <div class="weekday-header">{"Fri"}</div> |         } else { | ||||||
|             <div class="weekday-header">{"Sat"}</div> |             format!("{} PM", hour - 12) | ||||||
|  |         } | ||||||
|  |     }).collect(); | ||||||
|      |      | ||||||
|             // Week days |     // Add the final midnight boundary to show where the day ends | ||||||
|  |     time_labels.push("12 AM".to_string()); | ||||||
|  |  | ||||||
|  |     html! { | ||||||
|  |         <div class="week-view-container"> | ||||||
|  |             // Header with weekday names and dates | ||||||
|  |             <div class="week-header"> | ||||||
|  |                 <div class="time-gutter"></div> | ||||||
|  |                 { | ||||||
|  |                     week_days.iter().map(|date| { | ||||||
|  |                         let is_today = *date == props.today; | ||||||
|  |                         let weekday_name = get_weekday_name(date.weekday()); | ||||||
|  |                          | ||||||
|  |                         html! { | ||||||
|  |                             <div class={classes!("week-day-header", if is_today { Some("today") } else { None })}> | ||||||
|  |                                 <div class="weekday-name">{weekday_name}</div> | ||||||
|  |                                 <div class="day-number">{date.day()}</div> | ||||||
|  |                             </div> | ||||||
|  |                         } | ||||||
|  |                     }).collect::<Html>() | ||||||
|  |                 } | ||||||
|  |             </div> | ||||||
|  |              | ||||||
|  |             // Scrollable content area with time grid | ||||||
|  |             <div class="week-content"> | ||||||
|  |                 <div class="time-grid"> | ||||||
|  |                     // Time labels | ||||||
|  |                     <div class="time-labels"> | ||||||
|  |                         { | ||||||
|  |                             time_labels.iter().enumerate().map(|(index, time)| { | ||||||
|  |                                 let is_final = index == time_labels.len() - 1; | ||||||
|  |                                 html! { | ||||||
|  |                                     <div class={classes!("time-label", if is_final { Some("final-boundary") } else { None })}> | ||||||
|  |                                         {time} | ||||||
|  |                                     </div> | ||||||
|  |                                 } | ||||||
|  |                             }).collect::<Html>() | ||||||
|  |                         } | ||||||
|  |                     </div> | ||||||
|  |                      | ||||||
|  |                     // Day columns | ||||||
|  |                     <div class="week-days-grid"> | ||||||
|                         { |                         { | ||||||
|                             week_days.iter().map(|date| { |                             week_days.iter().map(|date| { | ||||||
|                                 let is_today = *date == props.today; |                                 let is_today = *date == props.today; | ||||||
| @@ -59,7 +102,7 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                  |                                  | ||||||
|                                 html! { |                                 html! { | ||||||
|                                     <div  |                                     <div  | ||||||
|                             class={classes!("calendar-day", if is_today { Some("today") } else { None })} |                                         class={classes!("week-day-column", if is_today { Some("today") } else { None })} | ||||||
|                                         oncontextmenu={ |                                         oncontextmenu={ | ||||||
|                                             if let Some(callback) = &props.on_calendar_context_menu { |                                             if let Some(callback) = &props.on_calendar_context_menu { | ||||||
|                                                 let callback = callback.clone(); |                                                 let callback = callback.clone(); | ||||||
| @@ -73,8 +116,22 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                             } |                                             } | ||||||
|                                         } |                                         } | ||||||
|                                     > |                                     > | ||||||
|                             <div class="day-number">{date.day()}</div> |                                         // Time slot backgrounds - 24 full hour slots + 1 boundary slot | ||||||
|                             <div class="day-events"> |                                         { | ||||||
|  |                                             (0..24).map(|_hour| { | ||||||
|  |                                                 html! { | ||||||
|  |                                                     <div class="time-slot"> | ||||||
|  |                                                         <div class="time-slot-half"></div> | ||||||
|  |                                                         <div class="time-slot-half"></div> | ||||||
|  |                                                     </div> | ||||||
|  |                                                 } | ||||||
|  |                                             }).collect::<Html>() | ||||||
|  |                                         } | ||||||
|  |                                         // Final boundary slot to match the final time label | ||||||
|  |                                         <div class="time-slot boundary-slot"></div> | ||||||
|  |                                          | ||||||
|  |                                         // Events positioned absolutely | ||||||
|  |                                         <div class="events-container"> | ||||||
|                                             { |                                             { | ||||||
|                                                 day_events.iter().map(|event| { |                                                 day_events.iter().map(|event| { | ||||||
|                                                     let event_color = get_event_color(event); |                                                     let event_color = get_event_color(event); | ||||||
| @@ -101,10 +158,11 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                                         } |                                                         } | ||||||
|                                                     }; |                                                     }; | ||||||
|                                                      |                                                      | ||||||
|  |                                                     // For now, position events at top of day (we'll improve this later with actual time positioning) | ||||||
|                                                     html! { |                                                     html! { | ||||||
|                                                         <div  |                                                         <div  | ||||||
|                                                 class={classes!("event-box", if is_refreshing { Some("refreshing") } else { None })} |                                                             class={classes!("week-event", if is_refreshing { Some("refreshing") } else { None })} | ||||||
|                                                 style={format!("background-color: {}", event_color)} |                                                             style={format!("background-color: {}; top: {}px;", event_color, 60)} // Placeholder positioning | ||||||
|                                                             {onclick} |                                                             {onclick} | ||||||
|                                                             {oncontextmenu} |                                                             {oncontextmenu} | ||||||
|                                                         > |                                                         > | ||||||
| @@ -119,6 +177,9 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                             }).collect::<Html>() |                             }).collect::<Html>() | ||||||
|                         } |                         } | ||||||
|                     </div> |                     </div> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -135,3 +196,15 @@ fn get_start_of_week(date: NaiveDate) -> NaiveDate { | |||||||
|     }; |     }; | ||||||
|     date - Duration::days(days_from_sunday) |     date - Duration::days(days_from_sunday) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | fn get_weekday_name(weekday: Weekday) -> &'static str { | ||||||
|  |     match weekday { | ||||||
|  |         Weekday::Sun => "Sun", | ||||||
|  |         Weekday::Mon => "Mon", | ||||||
|  |         Weekday::Tue => "Tue", | ||||||
|  |         Weekday::Wed => "Wed", | ||||||
|  |         Weekday::Thu => "Thu", | ||||||
|  |         Weekday::Fri => "Fri", | ||||||
|  |         Weekday::Sat => "Sat", | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										184
									
								
								styles.css
									
									
									
									
									
								
							
							
						
						
									
										184
									
								
								styles.css
									
									
									
									
									
								
							| @@ -462,7 +462,189 @@ body { | |||||||
|     background: white; |     background: white; | ||||||
| } | } | ||||||
|  |  | ||||||
| /* Week Grid */ | /* Week View Container */ | ||||||
|  | .week-view-container { | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     height: 100%; | ||||||
|  |     background: white; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Week Header */ | ||||||
|  | .week-header { | ||||||
|  |     display: grid; | ||||||
|  |     grid-template-columns: 80px repeat(7, 1fr); | ||||||
|  |     background: #f8f9fa; | ||||||
|  |     border-bottom: 2px solid #e9ecef; | ||||||
|  |     position: sticky; | ||||||
|  |     top: 0; | ||||||
|  |     z-index: 10; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .time-gutter { | ||||||
|  |     background: #f8f9fa; | ||||||
|  |     border-right: 1px solid #e9ecef; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-day-header { | ||||||
|  |     padding: 1rem; | ||||||
|  |     text-align: center; | ||||||
|  |     border-right: 1px solid #e9ecef; | ||||||
|  |     background: #f8f9fa; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-day-header.today { | ||||||
|  |     background: #e3f2fd; | ||||||
|  |     color: #1976d2; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .weekday-name { | ||||||
|  |     font-size: 0.9rem; | ||||||
|  |     font-weight: 600; | ||||||
|  |     color: #666; | ||||||
|  |     text-transform: uppercase; | ||||||
|  |     letter-spacing: 0.5px; | ||||||
|  |     margin-bottom: 0.25rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-day-header .day-number { | ||||||
|  |     font-size: 1.5rem; | ||||||
|  |     font-weight: 700; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-day-header.today .weekday-name { | ||||||
|  |     color: #1976d2; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Week Content */ | ||||||
|  | .week-content { | ||||||
|  |     flex: 1; | ||||||
|  |     overflow-y: auto; | ||||||
|  |     overflow-x: hidden; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .time-grid { | ||||||
|  |     display: grid; | ||||||
|  |     grid-template-columns: 80px 1fr; | ||||||
|  |     min-height: 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Time Labels */ | ||||||
|  | .time-labels { | ||||||
|  |     background: #f8f9fa; | ||||||
|  |     border-right: 1px solid #e9ecef; | ||||||
|  |     position: sticky; | ||||||
|  |     left: 0; | ||||||
|  |     z-index: 5; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .time-label { | ||||||
|  |     height: 60px; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: flex-start; | ||||||
|  |     justify-content: center; | ||||||
|  |     padding-top: 0.5rem; | ||||||
|  |     font-size: 0.75rem; | ||||||
|  |     color: #666; | ||||||
|  |     border-bottom: 1px solid #f0f0f0; | ||||||
|  |     font-weight: 500; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .time-label.final-boundary { | ||||||
|  |     height: 60px; /* Keep same height but this marks the end boundary */ | ||||||
|  |     border-bottom: 2px solid #e9ecef; /* Stronger border to show day end */ | ||||||
|  |     color: #999; /* Lighter color to indicate it's the boundary */ | ||||||
|  |     font-size: 0.7rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Week Days Grid */ | ||||||
|  | .week-days-grid { | ||||||
|  |     display: grid; | ||||||
|  |     grid-template-columns: repeat(7, 1fr); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-day-column { | ||||||
|  |     position: relative; | ||||||
|  |     border-right: 1px solid #e9ecef; | ||||||
|  |     min-height: 1500px; /* 25 time labels × 60px = 1500px total */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-day-column:last-child { | ||||||
|  |     border-right: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-day-column.today { | ||||||
|  |     background: #fafffe; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Time Slots */ | ||||||
|  | .time-slot { | ||||||
|  |     height: 60px; | ||||||
|  |     border-bottom: 1px solid #f0f0f0; | ||||||
|  |     position: relative; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .time-slot-half { | ||||||
|  |     height: 30px; | ||||||
|  |     border-bottom: 1px dotted #f5f5f5; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .time-slot-half:last-child { | ||||||
|  |     border-bottom: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .time-slot.boundary-slot { | ||||||
|  |     height: 60px; /* Match the final time label height */ | ||||||
|  |     border-bottom: 2px solid #e9ecef; /* Strong border to match final boundary */ | ||||||
|  |     background: rgba(0,0,0,0.02); /* Slightly different background to indicate boundary */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Events Container */ | ||||||
|  | .events-container { | ||||||
|  |     position: absolute; | ||||||
|  |     top: 0; | ||||||
|  |     left: 0; | ||||||
|  |     right: 0; | ||||||
|  |     bottom: 0; | ||||||
|  |     pointer-events: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Week Events */ | ||||||
|  | .week-event { | ||||||
|  |     position: absolute; | ||||||
|  |     left: 4px; | ||||||
|  |     right: 4px; | ||||||
|  |     min-height: 20px; | ||||||
|  |     background: #3B82F6; | ||||||
|  |     color: white; | ||||||
|  |     padding: 2px 6px; | ||||||
|  |     border-radius: 4px; | ||||||
|  |     font-size: 0.75rem; | ||||||
|  |     line-height: 1.3; | ||||||
|  |     cursor: pointer; | ||||||
|  |     pointer-events: auto; | ||||||
|  |     z-index: 3; | ||||||
|  |     border: 1px solid rgba(255,255,255,0.2); | ||||||
|  |     text-shadow: 0 1px 1px rgba(0,0,0,0.3); | ||||||
|  |     font-weight: 500; | ||||||
|  |     box-shadow: 0 1px 3px rgba(0,0,0,0.1); | ||||||
|  |     overflow: hidden; | ||||||
|  |     text-overflow: ellipsis; | ||||||
|  |     white-space: nowrap; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-event:hover { | ||||||
|  |     filter: brightness(1.1); | ||||||
|  |     z-index: 4; | ||||||
|  |     box-shadow: 0 2px 6px rgba(0,0,0,0.15); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-event.refreshing { | ||||||
|  |     animation: pulse 1.5s ease-in-out infinite alternate; | ||||||
|  |     border-color: #ff9800; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Legacy Week Grid (for backward compatibility) */ | ||||||
| .week-grid { | .week-grid { | ||||||
|     display: grid; |     display: grid; | ||||||
|     grid-template-columns: repeat(7, 1fr); |     grid-template-columns: repeat(7, 1fr); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Connor Johnstone
					Connor Johnstone