Implement full-screen monthly calendar UI

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 <noreply@anthropic.com>
This commit is contained in:
Connor Johnstone
2025-08-28 16:42:19 -04:00
parent b7b351416d
commit f6fa745775
4 changed files with 371 additions and 68 deletions

View File

@@ -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) {

View File

@@ -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! {
<div class="calendar-view">
<h2>{"Welcome to your Calendar!"}</h2>
<p>{"You are now authenticated and can access your calendar."}</p>
// Temporary counter demo - will be replaced with calendar functionality
<div class="demo-section">
<h3>{"Demo Counter"}</h3>
<button {onclick}>{ "Click me!" }</button>
<p>{ format!("Counter: {}", *counter) }</p>
</div>
<div class="calendar-placeholder">
<p>{"Calendar functionality will be implemented here."}</p>
<p>{"This will include:"}</p>
<ul>
<li>{"Calendar view with events"}</li>
<li>{"Integration with CalDAV server"}</li>
<li>{"Event creation and editing"}</li>
<li>{"Synchronization with Baikal server"}</li>
</ul>
</div>
<Calendar events={events} />
</div>
}
}

186
src/components/calendar.rs Normal file
View File

@@ -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<NaiveDate, Vec<String>>,
}
#[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! {
<div class="calendar">
<div class="calendar-header">
<button class="nav-button" onclick={prev_month}>{""}</button>
<h2 class="month-year">{format!("{} {}", get_month_name(current_month.month()), current_month.year())}</h2>
<button class="nav-button" onclick={next_month}>{""}</button>
</div>
<div class="calendar-grid">
// Weekday headers
<div class="weekday-header">{"Sun"}</div>
<div class="weekday-header">{"Mon"}</div>
<div class="weekday-header">{"Tue"}</div>
<div class="weekday-header">{"Wed"}</div>
<div class="weekday-header">{"Thu"}</div>
<div class="weekday-header">{"Fri"}</div>
<div class="weekday-header">{"Sat"}</div>
// Days from previous month (grayed out)
{
days_from_prev_month.iter().map(|day| {
html! {
<div class="calendar-day prev-month">{*day}</div>
}
}).collect::<Html>()
}
// 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! {
<div class={classes!(classes)}>
<div class="day-number">{day}</div>
{
if !events.is_empty() {
html! {
<div class="event-indicators">
{
events.iter().take(3).map(|event| {
html! { <div class="event-dot" title={event.clone()}></div> }
}).collect::<Html>()
}
{
if events.len() > 3 {
html! { <div class="more-events">{format!("+{}", events.len() - 3)}</div> }
} else {
html! {}
}
}
</div>
}
} else {
html! {}
}
}
</div>
}
}).collect::<Html>()
}
{ render_next_month_days(days_from_prev_month.len(), days_in_month) }
</div>
</div>
}
}
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! {
<div class="calendar-day next-month">{day}</div>
}
}).collect::<Html>()
}
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<u32> {
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"
}
}

View File

@@ -1,5 +1,7 @@
pub mod login;
pub mod register;
pub mod calendar;
pub use login::Login;
pub use register::Register;
pub use register::Register;
pub use calendar::Calendar;