diff --git a/src/components/week_view.rs b/src/components/week_view.rs index 5c71ea2..1e2bf3e 100644 --- a/src/components/week_view.rs +++ b/src/components/week_view.rs @@ -40,84 +40,145 @@ pub fn week_view(props: &WeekViewProps) -> Html { "#3B82F6".to_string() }; + // Generate time labels - 24 hours plus the final midnight boundary + let mut time_labels: Vec = (0..24).map(|hour| { + if hour == 0 { + "12 AM".to_string() + } else if hour < 12 { + format!("{} AM", hour) + } else if hour == 12 { + "12 PM".to_string() + } else { + format!("{} PM", hour - 12) + } + }).collect(); + + // Add the final midnight boundary to show where the day ends + time_labels.push("12 AM".to_string()); + html! { -
- // Weekday headers -
{"Sun"}
-
{"Mon"}
-
{"Tue"}
-
{"Wed"}
-
{"Thu"}
-
{"Fri"}
-
{"Sat"}
+
+ // Header with weekday names and dates +
+
+ { + week_days.iter().map(|date| { + let is_today = *date == props.today; + let weekday_name = get_weekday_name(date.weekday()); + + html! { +
+
{weekday_name}
+
{date.day()}
+
+ } + }).collect::() + } +
- // Week days - { - week_days.iter().map(|date| { - let is_today = *date == props.today; - let day_events = props.events.get(date).cloned().unwrap_or_default(); - - html! { -
+
+ // Time labels +
+ { + time_labels.iter().enumerate().map(|(index, time)| { + let is_final = index == time_labels.len() - 1; + html! { +
+ {time} +
} - } - > -
{date.day()}
-
- { - day_events.iter().map(|event| { - let event_color = get_event_color(event); - let is_refreshing = props.refreshing_event_uid.as_ref() == Some(&event.uid); - - let onclick = { - let on_event_click = props.on_event_click.clone(); - let event = event.clone(); - Callback::from(move |_: MouseEvent| { - on_event_click.emit(event.clone()); - }) - }; - - let oncontextmenu = { - if let Some(callback) = &props.on_event_context_menu { + }).collect::() + } +
+ + // Day columns +
+ { + week_days.iter().map(|date| { + let is_today = *date == props.today; + let day_events = props.events.get(date).cloned().unwrap_or_default(); + + html! { +
- {event.summary.as_ref().unwrap_or(&"Untitled".to_string())} -
} - }).collect::() + > + // Time slot backgrounds - 24 full hour slots + 1 boundary slot + { + (0..24).map(|_hour| { + html! { +
+
+
+
+ } + }).collect::() + } + // Final boundary slot to match the final time label +
+ + // Events positioned absolutely +
+ { + day_events.iter().map(|event| { + let event_color = get_event_color(event); + let is_refreshing = props.refreshing_event_uid.as_ref() == Some(&event.uid); + + let onclick = { + let on_event_click = props.on_event_click.clone(); + let event = event.clone(); + Callback::from(move |_: MouseEvent| { + on_event_click.emit(event.clone()); + }) + }; + + let oncontextmenu = { + if let Some(callback) = &props.on_event_context_menu { + let callback = callback.clone(); + let event = event.clone(); + Some(Callback::from(move |e: web_sys::MouseEvent| { + e.prevent_default(); + callback.emit((e, event.clone())); + })) + } else { + None + } + }; + + // For now, position events at top of day (we'll improve this later with actual time positioning) + html! { +
+ {event.summary.as_ref().unwrap_or(&"Untitled".to_string())} +
+ } + }).collect::() + } +
+
} -
-
- } - }).collect::() - } + }).collect::() + } +
+
+
} } @@ -134,4 +195,16 @@ fn get_start_of_week(date: NaiveDate) -> NaiveDate { Weekday::Sat => 6, }; 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", + } } \ No newline at end of file diff --git a/styles.css b/styles.css index 1a0e6b2..75fca46 100644 --- a/styles.css +++ b/styles.css @@ -462,7 +462,189 @@ body { 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 { display: grid; grid-template-columns: repeat(7, 1fr);