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:
213
index.html
213
index.html
@@ -183,58 +183,197 @@
|
|||||||
|
|
||||||
/* Calendar View */
|
/* Calendar View */
|
||||||
.calendar-view {
|
.calendar-view {
|
||||||
|
height: calc(100vh - 140px); /* Full height minus header and padding */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar Component */
|
||||||
|
.calendar {
|
||||||
background: white;
|
background: white;
|
||||||
padding: 2rem;
|
border-radius: 12px;
|
||||||
border-radius: 8px;
|
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-view h2 {
|
.calendar-header {
|
||||||
color: #333;
|
display: flex;
|
||||||
margin-bottom: 1rem;
|
align-items: center;
|
||||||
}
|
justify-content: space-between;
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
.demo-section {
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
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;
|
|
||||||
color: white;
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-year {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0.5rem 1rem;
|
color: white;
|
||||||
border-radius: 4px;
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-right: 1rem;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-section button:hover {
|
.nav-button:hover {
|
||||||
background-color: #0056b3;
|
background: rgba(255,255,255,0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-placeholder {
|
.calendar-grid {
|
||||||
margin-top: 2rem;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
flex: 1;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-header {
|
||||||
|
background: #f8f9fa;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background: #e9ecef;
|
text-align: center;
|
||||||
border-radius: 4px;
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-placeholder ul {
|
.calendar-day {
|
||||||
margin: 1rem 0;
|
border: 1px solid #f0f0f0;
|
||||||
padding-left: 2rem;
|
padding: 0.75rem;
|
||||||
|
min-height: 100px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-placeholder li {
|
.calendar-day:hover {
|
||||||
margin: 0.5rem 0;
|
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) {
|
@media (max-width: 768px) {
|
||||||
|
|||||||
36
src/app.rs
36
src/app.rs
@@ -1,7 +1,8 @@
|
|||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_router::prelude::*;
|
use yew_router::prelude::*;
|
||||||
use gloo_storage::{LocalStorage, Storage};
|
use gloo_storage::{LocalStorage, Storage};
|
||||||
use crate::components::{Login, Register};
|
use crate::components::{Login, Register, Calendar};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(Clone, Routable, PartialEq)]
|
#[derive(Clone, Routable, PartialEq)]
|
||||||
enum Route {
|
enum Route {
|
||||||
@@ -104,37 +105,12 @@ pub fn App() -> Html {
|
|||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
fn CalendarView() -> Html {
|
fn CalendarView() -> Html {
|
||||||
let counter = use_state(|| 0);
|
// Sample events for demonstration
|
||||||
let onclick = {
|
let events = HashMap::new();
|
||||||
let counter = counter.clone();
|
|
||||||
move |_| {
|
|
||||||
let value = *counter + 1;
|
|
||||||
counter.set(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div class="calendar-view">
|
<div class="calendar-view">
|
||||||
<h2>{"Welcome to your Calendar!"}</h2>
|
<Calendar events={events} />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
186
src/components/calendar.rs
Normal file
186
src/components/calendar.rs
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
pub mod login;
|
pub mod login;
|
||||||
pub mod register;
|
pub mod register;
|
||||||
|
pub mod calendar;
|
||||||
|
|
||||||
pub use login::Login;
|
pub use login::Login;
|
||||||
pub use register::Register;
|
pub use register::Register;
|
||||||
|
pub use calendar::Calendar;
|
||||||
Reference in New Issue
Block a user