From f6fa7457754dae6a57f10bc01b6a85200548ee70 Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Thu, 28 Aug 2025 16:42:19 -0400 Subject: [PATCH] Implement full-screen monthly calendar UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added a comprehensive monthly calendar component with modern styling: - Full monthly view with proper date calculations - Current day highlighting and navigation - Responsive design for all screen sizes - Event indicator support for future integration - Takes up most of screen space as requested 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- index.html | 213 ++++++++++++++++++++++++++++++------- src/app.rs | 36 ++----- src/components/calendar.rs | 186 ++++++++++++++++++++++++++++++++ src/components/mod.rs | 4 +- 4 files changed, 371 insertions(+), 68 deletions(-) create mode 100644 src/components/calendar.rs diff --git a/index.html b/index.html index 4fdf8c6..7e2ed74 100644 --- a/index.html +++ b/index.html @@ -183,58 +183,197 @@ /* Calendar View */ .calendar-view { + height: calc(100vh - 140px); /* Full height minus header and padding */ + display: flex; + flex-direction: column; + } + + /* Calendar Component */ + .calendar { background: white; - padding: 2rem; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); + border-radius: 12px; + box-shadow: 0 4px 16px rgba(0,0,0,0.1); + overflow: hidden; + flex: 1; + display: flex; + flex-direction: column; } - .calendar-view h2 { - color: #333; - margin-bottom: 1rem; - } - - .demo-section { - margin: 2rem 0; - padding: 1rem; - background: #f8f9fa; - border-radius: 4px; - border-left: 4px solid #667eea; - } - - .demo-section h3 { - margin-bottom: 1rem; - color: #333; - } - - .demo-section button { - background-color: #007bff; + .calendar-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.5rem 2rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; + } + + .month-year { + font-size: 1.8rem; + font-weight: 600; + margin: 0; + } + + .nav-button { + background: rgba(255,255,255,0.2); border: none; - padding: 0.5rem 1rem; - border-radius: 4px; + color: white; + font-size: 1.5rem; + font-weight: bold; + width: 40px; + height: 40px; + border-radius: 50%; cursor: pointer; - margin-right: 1rem; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s; } - .demo-section button:hover { - background-color: #0056b3; + .nav-button:hover { + background: rgba(255,255,255,0.3); } - .calendar-placeholder { - margin-top: 2rem; + .calendar-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + flex: 1; + background: white; + } + + .weekday-header { + background: #f8f9fa; padding: 1rem; - background: #e9ecef; - border-radius: 4px; + text-align: center; + font-weight: 600; + color: #666; + border-bottom: 1px solid #e9ecef; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.5px; } - .calendar-placeholder ul { - margin: 1rem 0; - padding-left: 2rem; + .calendar-day { + border: 1px solid #f0f0f0; + padding: 0.75rem; + min-height: 100px; + display: flex; + flex-direction: column; + cursor: pointer; + transition: background-color 0.2s; + position: relative; } - .calendar-placeholder li { - margin: 0.5rem 0; + .calendar-day:hover { + background-color: #f8f9ff; + } + + .calendar-day.current-month { + background: white; + } + + .calendar-day.prev-month, + .calendar-day.next-month { + background: #fafafa; + color: #ccc; + } + + .calendar-day.today { + background: #e3f2fd; + border: 2px solid #2196f3; + } + + .calendar-day.has-events { + background: #fff3e0; + } + + .calendar-day.today.has-events { + background: #e1f5fe; + } + + .day-number { + font-weight: 600; + font-size: 1.1rem; + margin-bottom: 0.5rem; + } + + .calendar-day.today .day-number { + color: #1976d2; + } + + .event-indicators { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; + } + + .event-dot { + background: #ff9800; + height: 6px; + border-radius: 3px; + margin-bottom: 1px; + } + + .more-events { + font-size: 0.7rem; + color: #666; + margin-top: 2px; + font-weight: 500; + } + + /* Responsive Design */ + @media (max-width: 768px) { + .calendar-header { + padding: 1rem; + } + + .month-year { + font-size: 1.4rem; + } + + .nav-button { + width: 35px; + height: 35px; + font-size: 1.2rem; + } + + .weekday-header { + padding: 0.5rem; + font-size: 0.8rem; + } + + .calendar-day { + min-height: 70px; + padding: 0.5rem; + } + + .day-number { + font-size: 1rem; + } + + .app-main { + padding: 1rem; + } + + .calendar-view { + height: calc(100vh - 120px); + } + } + + @media (max-width: 480px) { + .calendar-day { + min-height: 60px; + padding: 0.25rem; + } + + .weekday-header { + padding: 0.5rem 0.25rem; + } + + .day-number { + font-size: 0.9rem; + } } @media (max-width: 768px) { diff --git a/src/app.rs b/src/app.rs index 694d188..76457cb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,7 +1,8 @@ use yew::prelude::*; use yew_router::prelude::*; use gloo_storage::{LocalStorage, Storage}; -use crate::components::{Login, Register}; +use crate::components::{Login, Register, Calendar}; +use std::collections::HashMap; #[derive(Clone, Routable, PartialEq)] enum Route { @@ -104,37 +105,12 @@ pub fn App() -> Html { #[function_component] fn CalendarView() -> Html { - let counter = use_state(|| 0); - let onclick = { - let counter = counter.clone(); - move |_| { - let value = *counter + 1; - counter.set(value); - } - }; - + // Sample events for demonstration + let events = HashMap::new(); + html! {
-

{"Welcome to your Calendar!"}

-

{"You are now authenticated and can access your calendar."}

- - // Temporary counter demo - will be replaced with calendar functionality -
-

{"Demo Counter"}

- -

{ format!("Counter: {}", *counter) }

-
- -
-

{"Calendar functionality will be implemented here."}

-

{"This will include:"}

-
    -
  • {"Calendar view with events"}
  • -
  • {"Integration with CalDAV server"}
  • -
  • {"Event creation and editing"}
  • -
  • {"Synchronization with Baikal server"}
  • -
-
+
} } \ No newline at end of file diff --git a/src/components/calendar.rs b/src/components/calendar.rs new file mode 100644 index 0000000..667b781 --- /dev/null +++ b/src/components/calendar.rs @@ -0,0 +1,186 @@ +use yew::prelude::*; +use chrono::{Datelike, Local, NaiveDate, Duration, Weekday}; +use std::collections::HashMap; + +#[derive(Properties, PartialEq)] +pub struct CalendarProps { + #[prop_or_default] + pub events: HashMap>, +} + +#[function_component] +pub fn Calendar(props: &CalendarProps) -> Html { + let today = Local::now().date_naive(); + let current_month = use_state(|| today); + + let first_day_of_month = current_month.with_day(1).unwrap(); + let days_in_month = get_days_in_month(*current_month); + let first_weekday = first_day_of_month.weekday(); + let days_from_prev_month = get_days_from_previous_month(*current_month, first_weekday); + + let prev_month = { + let current_month = current_month.clone(); + Callback::from(move |_| { + let prev = *current_month - Duration::days(1); + let first_of_prev = prev.with_day(1).unwrap(); + current_month.set(first_of_prev); + }) + }; + + let next_month = { + let current_month = current_month.clone(); + Callback::from(move |_| { + let next = if current_month.month() == 12 { + NaiveDate::from_ymd_opt(current_month.year() + 1, 1, 1).unwrap() + } else { + NaiveDate::from_ymd_opt(current_month.year(), current_month.month() + 1, 1).unwrap() + }; + current_month.set(next); + }) + }; + + html! { +
+
+ +

{format!("{} {}", get_month_name(current_month.month()), current_month.year())}

+ +
+ +
+ // Weekday headers +
{"Sun"}
+
{"Mon"}
+
{"Tue"}
+
{"Wed"}
+
{"Thu"}
+
{"Fri"}
+
{"Sat"}
+ + // Days from previous month (grayed out) + { + days_from_prev_month.iter().map(|day| { + html! { +
{*day}
+ } + }).collect::() + } + + // Days of current month + { + (1..=days_in_month).map(|day| { + let date = current_month.with_day(day).unwrap(); + let is_today = date == today; + let events = props.events.get(&date).cloned().unwrap_or_default(); + + let mut classes = vec!["calendar-day", "current-month"]; + if is_today { + classes.push("today"); + } + if !events.is_empty() { + classes.push("has-events"); + } + + html! { +
+
{day}
+ { + if !events.is_empty() { + html! { +
+ { + events.iter().take(3).map(|event| { + html! {
} + }).collect::() + } + { + if events.len() > 3 { + html! {
{format!("+{}", events.len() - 3)}
} + } else { + html! {} + } + } +
+ } + } else { + html! {} + } + } +
+ } + }).collect::() + } + + { render_next_month_days(days_from_prev_month.len(), days_in_month) } +
+
+ } +} + +fn render_next_month_days(prev_days_count: usize, current_days_count: u32) -> Html { + let total_slots = 42; // 6 rows x 7 days + let used_slots = prev_days_count + current_days_count as usize; + let remaining_slots = if used_slots < total_slots { total_slots - used_slots } else { 0 }; + + (1..=remaining_slots).map(|day| { + html! { +
{day}
+ } + }).collect::() +} + +fn get_days_in_month(date: NaiveDate) -> u32 { + NaiveDate::from_ymd_opt( + if date.month() == 12 { date.year() + 1 } else { date.year() }, + if date.month() == 12 { 1 } else { date.month() + 1 }, + 1 + ) + .unwrap() + .pred_opt() + .unwrap() + .day() +} + +fn get_days_from_previous_month(current_month: NaiveDate, first_weekday: Weekday) -> Vec { + let days_before = match first_weekday { + Weekday::Sun => 0, + Weekday::Mon => 1, + Weekday::Tue => 2, + Weekday::Wed => 3, + Weekday::Thu => 4, + Weekday::Fri => 5, + Weekday::Sat => 6, + }; + + if days_before == 0 { + vec![] + } else { + // Calculate the previous month + let prev_month = if current_month.month() == 1 { + NaiveDate::from_ymd_opt(current_month.year() - 1, 12, 1).unwrap() + } else { + NaiveDate::from_ymd_opt(current_month.year(), current_month.month() - 1, 1).unwrap() + }; + + let prev_month_days = get_days_in_month(prev_month); + ((prev_month_days - days_before as u32 + 1)..=prev_month_days).collect() + } +} + +fn get_month_name(month: u32) -> &'static str { + match month { + 1 => "January", + 2 => "February", + 3 => "March", + 4 => "April", + 5 => "May", + 6 => "June", + 7 => "July", + 8 => "August", + 9 => "September", + 10 => "October", + 11 => "November", + 12 => "December", + _ => "Invalid" + } +} \ No newline at end of file diff --git a/src/components/mod.rs b/src/components/mod.rs index 74df60b..ba99ef4 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,5 +1,7 @@ pub mod login; pub mod register; +pub mod calendar; pub use login::Login; -pub use register::Register; \ No newline at end of file +pub use register::Register; +pub use calendar::Calendar; \ No newline at end of file