Compare commits
2 Commits
749ffaff58
...
811cceae52
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
811cceae52 | ||
|
|
34461640af |
19
Cargo.toml
19
Cargo.toml
@@ -7,7 +7,24 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
yew = { version = "0.21", features = ["csr"] }
|
yew = { version = "0.21", features = ["csr"] }
|
||||||
web-sys = "0.3"
|
web-sys = { version = "0.3", features = [
|
||||||
|
"console",
|
||||||
|
"HtmlSelectElement",
|
||||||
|
"HtmlInputElement",
|
||||||
|
"HtmlTextAreaElement",
|
||||||
|
"Event",
|
||||||
|
"MouseEvent",
|
||||||
|
"InputEvent",
|
||||||
|
"Element",
|
||||||
|
"Document",
|
||||||
|
"Window",
|
||||||
|
"Location",
|
||||||
|
"Headers",
|
||||||
|
"Request",
|
||||||
|
"RequestInit",
|
||||||
|
"RequestMode",
|
||||||
|
"Response",
|
||||||
|
] }
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
|
|
||||||
# HTTP client for CalDAV requests
|
# HTTP client for CalDAV requests
|
||||||
|
|||||||
@@ -866,6 +866,11 @@ impl CalDAVClient {
|
|||||||
ical.push_str("END:VALARM\r\n");
|
ical.push_str("END:VALARM\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recurrence rule
|
||||||
|
if let Some(rrule) = &event.recurrence_rule {
|
||||||
|
ical.push_str(&format!("RRULE:{}\r\n", rrule));
|
||||||
|
}
|
||||||
|
|
||||||
ical.push_str("END:VEVENT\r\n");
|
ical.push_str("END:VEVENT\r\n");
|
||||||
ical.push_str("END:VCALENDAR\r\n");
|
ical.push_str("END:VCALENDAR\r\n");
|
||||||
|
|
||||||
|
|||||||
@@ -415,6 +415,124 @@ pub async fn create_event(
|
|||||||
// Generate a unique UID for the event
|
// Generate a unique UID for the event
|
||||||
let uid = format!("{}-{}", uuid::Uuid::new_v4(), chrono::Utc::now().timestamp());
|
let uid = format!("{}-{}", uuid::Uuid::new_v4(), chrono::Utc::now().timestamp());
|
||||||
|
|
||||||
|
// Parse status
|
||||||
|
let status = match request.status.to_lowercase().as_str() {
|
||||||
|
"tentative" => crate::calendar::EventStatus::Tentative,
|
||||||
|
"cancelled" => crate::calendar::EventStatus::Cancelled,
|
||||||
|
_ => crate::calendar::EventStatus::Confirmed,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse class
|
||||||
|
let class = match request.class.to_lowercase().as_str() {
|
||||||
|
"private" => crate::calendar::EventClass::Private,
|
||||||
|
"confidential" => crate::calendar::EventClass::Confidential,
|
||||||
|
_ => crate::calendar::EventClass::Public,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse attendees (comma-separated email list)
|
||||||
|
let attendees: Vec<String> = if request.attendees.trim().is_empty() {
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
request.attendees
|
||||||
|
.split(',')
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse categories (comma-separated list)
|
||||||
|
let categories: Vec<String> = if request.categories.trim().is_empty() {
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
request.categories
|
||||||
|
.split(',')
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse reminders and convert to EventReminder structs
|
||||||
|
let reminders: Vec<crate::calendar::EventReminder> = match request.reminder.to_lowercase().as_str() {
|
||||||
|
"15min" => vec![crate::calendar::EventReminder {
|
||||||
|
minutes_before: 15,
|
||||||
|
action: crate::calendar::ReminderAction::Display,
|
||||||
|
description: None,
|
||||||
|
}],
|
||||||
|
"30min" => vec![crate::calendar::EventReminder {
|
||||||
|
minutes_before: 30,
|
||||||
|
action: crate::calendar::ReminderAction::Display,
|
||||||
|
description: None,
|
||||||
|
}],
|
||||||
|
"1hour" => vec![crate::calendar::EventReminder {
|
||||||
|
minutes_before: 60,
|
||||||
|
action: crate::calendar::ReminderAction::Display,
|
||||||
|
description: None,
|
||||||
|
}],
|
||||||
|
"2hours" => vec![crate::calendar::EventReminder {
|
||||||
|
minutes_before: 120,
|
||||||
|
action: crate::calendar::ReminderAction::Display,
|
||||||
|
description: None,
|
||||||
|
}],
|
||||||
|
"1day" => vec![crate::calendar::EventReminder {
|
||||||
|
minutes_before: 1440, // 24 * 60
|
||||||
|
action: crate::calendar::ReminderAction::Display,
|
||||||
|
description: None,
|
||||||
|
}],
|
||||||
|
"2days" => vec![crate::calendar::EventReminder {
|
||||||
|
minutes_before: 2880, // 48 * 60
|
||||||
|
action: crate::calendar::ReminderAction::Display,
|
||||||
|
description: None,
|
||||||
|
}],
|
||||||
|
"1week" => vec![crate::calendar::EventReminder {
|
||||||
|
minutes_before: 10080, // 7 * 24 * 60
|
||||||
|
action: crate::calendar::ReminderAction::Display,
|
||||||
|
description: None,
|
||||||
|
}],
|
||||||
|
_ => Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse recurrence with BYDAY support for weekly recurrence
|
||||||
|
let recurrence_rule = match request.recurrence.to_lowercase().as_str() {
|
||||||
|
"daily" => Some("FREQ=DAILY".to_string()),
|
||||||
|
"weekly" => {
|
||||||
|
// Handle weekly recurrence with optional BYDAY parameter
|
||||||
|
let mut rrule = "FREQ=WEEKLY".to_string();
|
||||||
|
|
||||||
|
// Check if specific days are selected (recurrence_days has 7 elements: [Sun, Mon, Tue, Wed, Thu, Fri, Sat])
|
||||||
|
if request.recurrence_days.len() == 7 {
|
||||||
|
let selected_days: Vec<&str> = request.recurrence_days
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, &selected)| {
|
||||||
|
if selected {
|
||||||
|
Some(match i {
|
||||||
|
0 => "SU", // Sunday
|
||||||
|
1 => "MO", // Monday
|
||||||
|
2 => "TU", // Tuesday
|
||||||
|
3 => "WE", // Wednesday
|
||||||
|
4 => "TH", // Thursday
|
||||||
|
5 => "FR", // Friday
|
||||||
|
6 => "SA", // Saturday
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !selected_days.is_empty() {
|
||||||
|
rrule.push_str(&format!(";BYDAY={}", selected_days.join(",")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(rrule)
|
||||||
|
},
|
||||||
|
"monthly" => Some("FREQ=MONTHLY".to_string()),
|
||||||
|
"yearly" => Some("FREQ=YEARLY".to_string()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
// Create the CalendarEvent struct
|
// Create the CalendarEvent struct
|
||||||
let event = crate::calendar::CalendarEvent {
|
let event = crate::calendar::CalendarEvent {
|
||||||
uid,
|
uid,
|
||||||
@@ -431,17 +549,21 @@ pub async fn create_event(
|
|||||||
} else {
|
} else {
|
||||||
Some(request.location.clone())
|
Some(request.location.clone())
|
||||||
},
|
},
|
||||||
status: crate::calendar::EventStatus::Confirmed,
|
status,
|
||||||
class: crate::calendar::EventClass::Public,
|
class,
|
||||||
priority: None,
|
priority: request.priority,
|
||||||
organizer: None,
|
organizer: if request.organizer.trim().is_empty() {
|
||||||
attendees: Vec::new(),
|
None
|
||||||
categories: Vec::new(),
|
} else {
|
||||||
|
Some(request.organizer.clone())
|
||||||
|
},
|
||||||
|
attendees,
|
||||||
|
categories,
|
||||||
created: Some(chrono::Utc::now()),
|
created: Some(chrono::Utc::now()),
|
||||||
last_modified: Some(chrono::Utc::now()),
|
last_modified: Some(chrono::Utc::now()),
|
||||||
recurrence_rule: None,
|
recurrence_rule,
|
||||||
all_day: request.all_day,
|
all_day: request.all_day,
|
||||||
reminders: Vec::new(),
|
reminders,
|
||||||
etag: None,
|
etag: None,
|
||||||
href: None,
|
href: None,
|
||||||
calendar_path: Some(calendar_path.clone()),
|
calendar_path: Some(calendar_path.clone()),
|
||||||
|
|||||||
@@ -80,6 +80,15 @@ pub struct CreateEventRequest {
|
|||||||
pub end_time: String, // HH:MM format
|
pub end_time: String, // HH:MM format
|
||||||
pub location: String,
|
pub location: String,
|
||||||
pub all_day: bool,
|
pub all_day: bool,
|
||||||
|
pub status: String, // confirmed, tentative, cancelled
|
||||||
|
pub class: String, // public, private, confidential
|
||||||
|
pub priority: Option<u8>, // 0-9 priority level
|
||||||
|
pub organizer: String, // organizer email
|
||||||
|
pub attendees: String, // comma-separated attendee emails
|
||||||
|
pub categories: String, // comma-separated categories
|
||||||
|
pub reminder: String, // reminder type
|
||||||
|
pub recurrence: String, // recurrence type
|
||||||
|
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
|
||||||
pub calendar_path: Option<String>, // Optional - use first calendar if not specified
|
pub calendar_path: Option<String>, // Optional - use first calendar if not specified
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
43
src/app.rs
43
src/app.rs
@@ -2,7 +2,7 @@ use yew::prelude::*;
|
|||||||
use yew_router::prelude::*;
|
use yew_router::prelude::*;
|
||||||
use gloo_storage::{LocalStorage, Storage};
|
use gloo_storage::{LocalStorage, Storage};
|
||||||
use web_sys::MouseEvent;
|
use web_sys::MouseEvent;
|
||||||
use crate::components::{Sidebar, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler};
|
use crate::components::{Sidebar, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler, EventStatus, EventClass, ReminderType, RecurrenceType};
|
||||||
use crate::services::{CalendarService, calendar_service::{UserInfo, CalendarEvent}};
|
use crate::services::{CalendarService, calendar_service::{UserInfo, CalendarEvent}};
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
@@ -221,6 +221,38 @@ pub fn App() -> Html {
|
|||||||
let end_date = event_data.end_date.format("%Y-%m-%d").to_string();
|
let end_date = event_data.end_date.format("%Y-%m-%d").to_string();
|
||||||
let end_time = event_data.end_time.format("%H:%M").to_string();
|
let end_time = event_data.end_time.format("%H:%M").to_string();
|
||||||
|
|
||||||
|
// Convert enums to strings for backend
|
||||||
|
let status_str = match event_data.status {
|
||||||
|
EventStatus::Tentative => "tentative",
|
||||||
|
EventStatus::Cancelled => "cancelled",
|
||||||
|
_ => "confirmed",
|
||||||
|
}.to_string();
|
||||||
|
|
||||||
|
let class_str = match event_data.class {
|
||||||
|
EventClass::Private => "private",
|
||||||
|
EventClass::Confidential => "confidential",
|
||||||
|
_ => "public",
|
||||||
|
}.to_string();
|
||||||
|
|
||||||
|
let reminder_str = match event_data.reminder {
|
||||||
|
ReminderType::Minutes15 => "15min",
|
||||||
|
ReminderType::Minutes30 => "30min",
|
||||||
|
ReminderType::Hour1 => "1hour",
|
||||||
|
ReminderType::Hours2 => "2hours",
|
||||||
|
ReminderType::Day1 => "1day",
|
||||||
|
ReminderType::Days2 => "2days",
|
||||||
|
ReminderType::Week1 => "1week",
|
||||||
|
_ => "none",
|
||||||
|
}.to_string();
|
||||||
|
|
||||||
|
let recurrence_str = match event_data.recurrence {
|
||||||
|
RecurrenceType::Daily => "daily",
|
||||||
|
RecurrenceType::Weekly => "weekly",
|
||||||
|
RecurrenceType::Monthly => "monthly",
|
||||||
|
RecurrenceType::Yearly => "yearly",
|
||||||
|
_ => "none",
|
||||||
|
}.to_string();
|
||||||
|
|
||||||
match calendar_service.create_event(
|
match calendar_service.create_event(
|
||||||
&token,
|
&token,
|
||||||
&password,
|
&password,
|
||||||
@@ -232,6 +264,15 @@ pub fn App() -> Html {
|
|||||||
end_time,
|
end_time,
|
||||||
event_data.location,
|
event_data.location,
|
||||||
event_data.all_day,
|
event_data.all_day,
|
||||||
|
status_str,
|
||||||
|
class_str,
|
||||||
|
event_data.priority,
|
||||||
|
event_data.organizer,
|
||||||
|
event_data.attendees,
|
||||||
|
event_data.categories,
|
||||||
|
reminder_str,
|
||||||
|
recurrence_str,
|
||||||
|
event_data.recurrence_days,
|
||||||
None // Let backend use first available calendar
|
None // Let backend use first available calendar
|
||||||
).await {
|
).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use web_sys::{HtmlInputElement, HtmlTextAreaElement};
|
use web_sys::{HtmlInputElement, HtmlTextAreaElement, HtmlSelectElement};
|
||||||
use chrono::{NaiveDate, NaiveTime};
|
use chrono::{NaiveDate, NaiveTime};
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
@@ -10,6 +10,65 @@ pub struct CreateEventModalProps {
|
|||||||
pub on_create: Callback<EventCreationData>,
|
pub on_create: Callback<EventCreationData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
|
pub enum EventStatus {
|
||||||
|
Tentative,
|
||||||
|
Confirmed,
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EventStatus {
|
||||||
|
fn default() -> Self {
|
||||||
|
EventStatus::Confirmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
|
pub enum EventClass {
|
||||||
|
Public,
|
||||||
|
Private,
|
||||||
|
Confidential,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EventClass {
|
||||||
|
fn default() -> Self {
|
||||||
|
EventClass::Public
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
|
pub enum ReminderType {
|
||||||
|
None,
|
||||||
|
Minutes15,
|
||||||
|
Minutes30,
|
||||||
|
Hour1,
|
||||||
|
Hours2,
|
||||||
|
Day1,
|
||||||
|
Days2,
|
||||||
|
Week1,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ReminderType {
|
||||||
|
fn default() -> Self {
|
||||||
|
ReminderType::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
|
pub enum RecurrenceType {
|
||||||
|
None,
|
||||||
|
Daily,
|
||||||
|
Weekly,
|
||||||
|
Monthly,
|
||||||
|
Yearly,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RecurrenceType {
|
||||||
|
fn default() -> Self {
|
||||||
|
RecurrenceType::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
pub struct EventCreationData {
|
pub struct EventCreationData {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@@ -20,6 +79,15 @@ pub struct EventCreationData {
|
|||||||
pub end_time: NaiveTime,
|
pub end_time: NaiveTime,
|
||||||
pub location: String,
|
pub location: String,
|
||||||
pub all_day: bool,
|
pub all_day: bool,
|
||||||
|
pub status: EventStatus,
|
||||||
|
pub class: EventClass,
|
||||||
|
pub priority: Option<u8>,
|
||||||
|
pub organizer: String,
|
||||||
|
pub attendees: String, // Comma-separated list
|
||||||
|
pub categories: String, // Comma-separated list
|
||||||
|
pub reminder: ReminderType,
|
||||||
|
pub recurrence: RecurrenceType,
|
||||||
|
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for EventCreationData {
|
impl Default for EventCreationData {
|
||||||
@@ -37,6 +105,15 @@ impl Default for EventCreationData {
|
|||||||
end_time,
|
end_time,
|
||||||
location: String::new(),
|
location: String::new(),
|
||||||
all_day: false,
|
all_day: false,
|
||||||
|
status: EventStatus::default(),
|
||||||
|
class: EventClass::default(),
|
||||||
|
priority: None,
|
||||||
|
organizer: String::new(),
|
||||||
|
attendees: String::new(),
|
||||||
|
categories: String::new(),
|
||||||
|
reminder: ReminderType::default(),
|
||||||
|
recurrence: RecurrenceType::default(),
|
||||||
|
recurrence_days: vec![false; 7], // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] - all false by default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,6 +186,135 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let on_organizer_input = {
|
||||||
|
let event_data = event_data.clone();
|
||||||
|
Callback::from(move |e: InputEvent| {
|
||||||
|
if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
|
||||||
|
let mut data = (*event_data).clone();
|
||||||
|
data.organizer = input.value();
|
||||||
|
event_data.set(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_attendees_input = {
|
||||||
|
let event_data = event_data.clone();
|
||||||
|
Callback::from(move |e: InputEvent| {
|
||||||
|
if let Some(textarea) = e.target_dyn_into::<HtmlTextAreaElement>() {
|
||||||
|
let mut data = (*event_data).clone();
|
||||||
|
data.attendees = textarea.value();
|
||||||
|
event_data.set(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_categories_input = {
|
||||||
|
let event_data = event_data.clone();
|
||||||
|
Callback::from(move |e: InputEvent| {
|
||||||
|
if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
|
||||||
|
let mut data = (*event_data).clone();
|
||||||
|
data.categories = input.value();
|
||||||
|
event_data.set(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_status_change = {
|
||||||
|
let event_data = event_data.clone();
|
||||||
|
Callback::from(move |e: Event| {
|
||||||
|
if let Some(select) = e.target_dyn_into::<HtmlSelectElement>() {
|
||||||
|
let mut data = (*event_data).clone();
|
||||||
|
data.status = match select.value().as_str() {
|
||||||
|
"tentative" => EventStatus::Tentative,
|
||||||
|
"cancelled" => EventStatus::Cancelled,
|
||||||
|
_ => EventStatus::Confirmed,
|
||||||
|
};
|
||||||
|
event_data.set(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_class_change = {
|
||||||
|
let event_data = event_data.clone();
|
||||||
|
Callback::from(move |e: Event| {
|
||||||
|
if let Some(select) = e.target_dyn_into::<HtmlSelectElement>() {
|
||||||
|
let mut data = (*event_data).clone();
|
||||||
|
data.class = match select.value().as_str() {
|
||||||
|
"private" => EventClass::Private,
|
||||||
|
"confidential" => EventClass::Confidential,
|
||||||
|
_ => EventClass::Public,
|
||||||
|
};
|
||||||
|
event_data.set(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_priority_input = {
|
||||||
|
let event_data = event_data.clone();
|
||||||
|
Callback::from(move |e: InputEvent| {
|
||||||
|
if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
|
||||||
|
let mut data = (*event_data).clone();
|
||||||
|
data.priority = input.value().parse::<u8>().ok().filter(|&p| p <= 9);
|
||||||
|
event_data.set(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_reminder_change = {
|
||||||
|
let event_data = event_data.clone();
|
||||||
|
Callback::from(move |e: Event| {
|
||||||
|
if let Some(select) = e.target_dyn_into::<HtmlSelectElement>() {
|
||||||
|
let mut data = (*event_data).clone();
|
||||||
|
data.reminder = match select.value().as_str() {
|
||||||
|
"15min" => ReminderType::Minutes15,
|
||||||
|
"30min" => ReminderType::Minutes30,
|
||||||
|
"1hour" => ReminderType::Hour1,
|
||||||
|
"2hours" => ReminderType::Hours2,
|
||||||
|
"1day" => ReminderType::Day1,
|
||||||
|
"2days" => ReminderType::Days2,
|
||||||
|
"1week" => ReminderType::Week1,
|
||||||
|
_ => ReminderType::None,
|
||||||
|
};
|
||||||
|
event_data.set(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_recurrence_change = {
|
||||||
|
let event_data = event_data.clone();
|
||||||
|
Callback::from(move |e: Event| {
|
||||||
|
if let Some(select) = e.target_dyn_into::<HtmlSelectElement>() {
|
||||||
|
let mut data = (*event_data).clone();
|
||||||
|
data.recurrence = match select.value().as_str() {
|
||||||
|
"daily" => RecurrenceType::Daily,
|
||||||
|
"weekly" => RecurrenceType::Weekly,
|
||||||
|
"monthly" => RecurrenceType::Monthly,
|
||||||
|
"yearly" => RecurrenceType::Yearly,
|
||||||
|
_ => RecurrenceType::None,
|
||||||
|
};
|
||||||
|
// Reset recurrence days when changing recurrence type
|
||||||
|
data.recurrence_days = vec![false; 7];
|
||||||
|
event_data.set(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_weekday_change = {
|
||||||
|
let event_data = event_data.clone();
|
||||||
|
move |day_index: usize| {
|
||||||
|
let event_data = event_data.clone();
|
||||||
|
Callback::from(move |e: Event| {
|
||||||
|
if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
|
||||||
|
let mut data = (*event_data).clone();
|
||||||
|
if day_index < data.recurrence_days.len() {
|
||||||
|
data.recurrence_days[day_index] = input.checked();
|
||||||
|
event_data.set(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let on_start_date_change = {
|
let on_start_date_change = {
|
||||||
let event_data = event_data.clone();
|
let event_data = event_data.clone();
|
||||||
Callback::from(move |e: Event| {
|
Callback::from(move |e: Event| {
|
||||||
@@ -302,6 +508,148 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html {
|
|||||||
placeholder="Enter event location"
|
placeholder="Enter event location"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="event-status">{"Status"}</label>
|
||||||
|
<select
|
||||||
|
id="event-status"
|
||||||
|
class="form-input"
|
||||||
|
onchange={on_status_change}
|
||||||
|
>
|
||||||
|
<option value="confirmed" selected={matches!(data.status, EventStatus::Confirmed)}>{"Confirmed"}</option>
|
||||||
|
<option value="tentative" selected={matches!(data.status, EventStatus::Tentative)}>{"Tentative"}</option>
|
||||||
|
<option value="cancelled" selected={matches!(data.status, EventStatus::Cancelled)}>{"Cancelled"}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="event-class">{"Privacy"}</label>
|
||||||
|
<select
|
||||||
|
id="event-class"
|
||||||
|
class="form-input"
|
||||||
|
onchange={on_class_change}
|
||||||
|
>
|
||||||
|
<option value="public" selected={matches!(data.class, EventClass::Public)}>{"Public"}</option>
|
||||||
|
<option value="private" selected={matches!(data.class, EventClass::Private)}>{"Private"}</option>
|
||||||
|
<option value="confidential" selected={matches!(data.class, EventClass::Confidential)}>{"Confidential"}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="event-priority">{"Priority (0-9, optional)"}</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="event-priority"
|
||||||
|
class="form-input"
|
||||||
|
value={data.priority.map(|p| p.to_string()).unwrap_or_default()}
|
||||||
|
oninput={on_priority_input}
|
||||||
|
placeholder="0-9 priority level"
|
||||||
|
min="0"
|
||||||
|
max="9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="event-organizer">{"Organizer Email"}</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="event-organizer"
|
||||||
|
class="form-input"
|
||||||
|
value={data.organizer.clone()}
|
||||||
|
oninput={on_organizer_input}
|
||||||
|
placeholder="organizer@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="event-attendees">{"Attendees (comma-separated emails)"}</label>
|
||||||
|
<textarea
|
||||||
|
id="event-attendees"
|
||||||
|
class="form-input"
|
||||||
|
value={data.attendees.clone()}
|
||||||
|
oninput={on_attendees_input}
|
||||||
|
placeholder="attendee1@example.com, attendee2@example.com"
|
||||||
|
rows="2"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="event-categories">{"Categories (comma-separated)"}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="event-categories"
|
||||||
|
class="form-input"
|
||||||
|
value={data.categories.clone()}
|
||||||
|
oninput={on_categories_input}
|
||||||
|
placeholder="work, meeting, personal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="event-reminder">{"Reminder"}</label>
|
||||||
|
<select
|
||||||
|
id="event-reminder"
|
||||||
|
class="form-input"
|
||||||
|
onchange={on_reminder_change}
|
||||||
|
>
|
||||||
|
<option value="none" selected={matches!(data.reminder, ReminderType::None)}>{"None"}</option>
|
||||||
|
<option value="15min" selected={matches!(data.reminder, ReminderType::Minutes15)}>{"15 minutes"}</option>
|
||||||
|
<option value="30min" selected={matches!(data.reminder, ReminderType::Minutes30)}>{"30 minutes"}</option>
|
||||||
|
<option value="1hour" selected={matches!(data.reminder, ReminderType::Hour1)}>{"1 hour"}</option>
|
||||||
|
<option value="2hours" selected={matches!(data.reminder, ReminderType::Hours2)}>{"2 hours"}</option>
|
||||||
|
<option value="1day" selected={matches!(data.reminder, ReminderType::Day1)}>{"1 day"}</option>
|
||||||
|
<option value="2days" selected={matches!(data.reminder, ReminderType::Days2)}>{"2 days"}</option>
|
||||||
|
<option value="1week" selected={matches!(data.reminder, ReminderType::Week1)}>{"1 week"}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="event-recurrence">{"Recurrence"}</label>
|
||||||
|
<select
|
||||||
|
id="event-recurrence"
|
||||||
|
class="form-input"
|
||||||
|
onchange={on_recurrence_change}
|
||||||
|
>
|
||||||
|
<option value="none" selected={matches!(data.recurrence, RecurrenceType::None)}>{"None"}</option>
|
||||||
|
<option value="daily" selected={matches!(data.recurrence, RecurrenceType::Daily)}>{"Daily"}</option>
|
||||||
|
<option value="weekly" selected={matches!(data.recurrence, RecurrenceType::Weekly)}>{"Weekly"}</option>
|
||||||
|
<option value="monthly" selected={matches!(data.recurrence, RecurrenceType::Monthly)}>{"Monthly"}</option>
|
||||||
|
<option value="yearly" selected={matches!(data.recurrence, RecurrenceType::Yearly)}>{"Yearly"}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Show weekday selection only when weekly recurrence is selected
|
||||||
|
if matches!(data.recurrence, RecurrenceType::Weekly) {
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{"Repeat on"}</label>
|
||||||
|
<div class="weekday-selection">
|
||||||
|
{
|
||||||
|
["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, day)| {
|
||||||
|
let day_checked = data.recurrence_days.get(i).cloned().unwrap_or(false);
|
||||||
|
let on_change = on_weekday_change(i);
|
||||||
|
html! {
|
||||||
|
<label key={i} class="weekday-checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={day_checked}
|
||||||
|
onchange={on_change}
|
||||||
|
/>
|
||||||
|
<span class="weekday-label">{day}</span>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Html>()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ pub use create_calendar_modal::CreateCalendarModal;
|
|||||||
pub use context_menu::ContextMenu;
|
pub use context_menu::ContextMenu;
|
||||||
pub use event_context_menu::EventContextMenu;
|
pub use event_context_menu::EventContextMenu;
|
||||||
pub use calendar_context_menu::CalendarContextMenu;
|
pub use calendar_context_menu::CalendarContextMenu;
|
||||||
pub use create_event_modal::{CreateEventModal, EventCreationData};
|
pub use create_event_modal::{CreateEventModal, EventCreationData, EventStatus, EventClass, ReminderType, RecurrenceType};
|
||||||
pub use sidebar::Sidebar;
|
pub use sidebar::Sidebar;
|
||||||
pub use calendar_list_item::CalendarListItem;
|
pub use calendar_list_item::CalendarListItem;
|
||||||
pub use route_handler::RouteHandler;
|
pub use route_handler::RouteHandler;
|
||||||
@@ -599,6 +599,15 @@ impl CalendarService {
|
|||||||
end_time: String,
|
end_time: String,
|
||||||
location: String,
|
location: String,
|
||||||
all_day: bool,
|
all_day: bool,
|
||||||
|
status: String,
|
||||||
|
class: String,
|
||||||
|
priority: Option<u8>,
|
||||||
|
organizer: String,
|
||||||
|
attendees: String,
|
||||||
|
categories: String,
|
||||||
|
reminder: String,
|
||||||
|
recurrence: String,
|
||||||
|
recurrence_days: Vec<bool>,
|
||||||
calendar_path: Option<String>
|
calendar_path: Option<String>
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let window = web_sys::window().ok_or("No global window exists")?;
|
let window = web_sys::window().ok_or("No global window exists")?;
|
||||||
@@ -616,6 +625,15 @@ impl CalendarService {
|
|||||||
"end_time": end_time,
|
"end_time": end_time,
|
||||||
"location": location,
|
"location": location,
|
||||||
"all_day": all_day,
|
"all_day": all_day,
|
||||||
|
"status": status,
|
||||||
|
"class": class,
|
||||||
|
"priority": priority,
|
||||||
|
"organizer": organizer,
|
||||||
|
"attendees": attendees,
|
||||||
|
"categories": categories,
|
||||||
|
"reminder": reminder,
|
||||||
|
"recurrence": recurrence,
|
||||||
|
"recurrence_days": recurrence_days,
|
||||||
"calendar_path": calendar_path
|
"calendar_path": calendar_path
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
57
styles.css
57
styles.css
@@ -1271,6 +1271,54 @@ body {
|
|||||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Weekday selection styles */
|
||||||
|
.weekday-selection {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: white;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
min-width: 3rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-checkbox:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: #f8f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-checkbox input[type="checkbox"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-checkbox input[type="checkbox"]:checked + .weekday-label {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-checkbox:has(input[type="checkbox"]:checked) {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-color: #667eea;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #495057;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
/* Mobile adjustments for event creation modal */
|
/* Mobile adjustments for event creation modal */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.create-event-modal .form-row {
|
.create-event-modal .form-row {
|
||||||
@@ -1287,4 +1335,13 @@ body {
|
|||||||
.create-event-modal .btn {
|
.create-event-modal .btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.weekday-selection {
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-checkbox {
|
||||||
|
min-width: 2.5rem;
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user