Implement comprehensive theme system with calendar view support
- Add 8 attractive themes (Default, Ocean, Forest, Sunset, Purple, Dark, Rose, Mint) - Extend theme system to calendar header, month view, and week view components - Add dynamic theme-aware event color palettes that change with selected theme - Implement CSS custom properties for consistent theming across all components - Add localStorage persistence for user theme preferences - Create theme-aware calendar styling including day states, headers, and grid lines - Optimize dark theme with proper contrast and readability improvements - Add reactive event color system that updates when themes change 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ web-sys = { version = "0.3", features = [
|
||||
"RequestInit",
|
||||
"RequestMode",
|
||||
"Response",
|
||||
"CssStyleDeclaration",
|
||||
] }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
|
||||
@@ -809,8 +809,10 @@ pub async fn update_event(
|
||||
let is_series_update = request.update_action.as_deref() == Some("update_series");
|
||||
|
||||
// Search for the event by UID across the specified calendars
|
||||
// For recurring events, we might need to find by base UID pattern if exact match fails
|
||||
let mut found_event: Option<(CalendarEvent, String, String)> = None; // (event, calendar_path, href)
|
||||
for calendar_path in &calendar_paths {
|
||||
// First try exact match
|
||||
match client.fetch_event_by_uid(calendar_path, &search_uid).await {
|
||||
Ok(Some(event)) => {
|
||||
if let Some(href) = event.href.clone() {
|
||||
@@ -818,7 +820,32 @@ pub async fn update_event(
|
||||
break;
|
||||
}
|
||||
},
|
||||
Ok(None) => continue, // Event not found in this calendar
|
||||
Ok(None) => {
|
||||
// If exact match fails, try to find by base UID pattern for recurring events
|
||||
println!("🔍 Exact match failed for '{}', searching by base UID pattern", search_uid);
|
||||
match client.fetch_events(calendar_path).await {
|
||||
Ok(events) => {
|
||||
// Look for any event whose UID starts with the search_uid
|
||||
for event in events {
|
||||
if let Some(href) = &event.href {
|
||||
// Check if this event's UID starts with our search UID (base pattern)
|
||||
if event.uid.starts_with(&search_uid) && event.uid != search_uid {
|
||||
println!("🎯 Found recurring event by pattern: '{}' matches '{}'", event.uid, search_uid);
|
||||
found_event = Some((event.clone(), calendar_path.clone(), href.clone()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if found_event.is_some() {
|
||||
break;
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Error fetching events from {}: {:?}", calendar_path, e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to fetch event from calendar {}: {}", calendar_path, e);
|
||||
continue;
|
||||
@@ -1021,6 +1048,56 @@ pub async fn update_event(
|
||||
|
||||
// Keep existing recurrence rule (don't overwrite with recurrence_rule variable)
|
||||
// event.recurrence_rule stays as-is from the original event
|
||||
|
||||
// However, allow exception_dates to be updated - this is needed for "This and Future" events
|
||||
if let Some(exception_dates_str) = &request.exception_dates {
|
||||
// Parse the ISO datetime strings into DateTime<Utc>
|
||||
let mut new_exception_dates = Vec::new();
|
||||
for date_str in exception_dates_str {
|
||||
if let Ok(parsed_date) = chrono::DateTime::parse_from_rfc3339(date_str) {
|
||||
new_exception_dates.push(parsed_date.with_timezone(&chrono::Utc));
|
||||
} else if let Ok(parsed_date) = chrono::DateTime::parse_from_str(date_str, "%Y-%m-%d %H:%M:%S UTC") {
|
||||
new_exception_dates.push(parsed_date.with_timezone(&chrono::Utc));
|
||||
} else {
|
||||
eprintln!("Failed to parse exception date: {}", date_str);
|
||||
}
|
||||
}
|
||||
|
||||
// Merge with existing exception dates (avoid duplicates)
|
||||
for new_date in new_exception_dates {
|
||||
if !event.exception_dates.contains(&new_date) {
|
||||
event.exception_dates.push(new_date);
|
||||
}
|
||||
}
|
||||
|
||||
println!("🔄 Updated exception dates: {} total", event.exception_dates.len());
|
||||
}
|
||||
|
||||
// Handle UNTIL date modification for "This and Future Events"
|
||||
if let Some(until_date_str) = &request.until_date {
|
||||
println!("🔄 Adding UNTIL clause to RRULE: {}", until_date_str);
|
||||
|
||||
if let Some(ref rrule) = event.recurrence_rule {
|
||||
// Remove existing UNTIL if present and add new one
|
||||
let rrule_without_until = rrule.split(';')
|
||||
.filter(|part| !part.starts_with("UNTIL="))
|
||||
.collect::<Vec<&str>>()
|
||||
.join(";");
|
||||
|
||||
// Parse the until_date and format for RRULE
|
||||
if let Ok(until_datetime) = chrono::DateTime::parse_from_rfc3339(until_date_str) {
|
||||
let until_utc = until_datetime.with_timezone(&chrono::Utc);
|
||||
let until_formatted = until_utc.format("%Y%m%dT%H%M%SZ").to_string();
|
||||
|
||||
event.recurrence_rule = Some(format!("{};UNTIL={}", rrule_without_until, until_formatted));
|
||||
println!("🔄 Modified RRULE: {}", event.recurrence_rule.as_ref().unwrap());
|
||||
|
||||
// Clear exception dates since we're using UNTIL instead
|
||||
event.exception_dates.clear();
|
||||
println!("🔄 Cleared exception dates for UNTIL approach");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For regular updates, use the new recurrence rule
|
||||
event.recurrence_rule = recurrence_rule;
|
||||
|
||||
@@ -124,6 +124,9 @@ pub struct UpdateEventRequest {
|
||||
pub calendar_path: Option<String>, // Optional - search all calendars if not specified
|
||||
pub update_action: Option<String>, // "update_series" for recurring events
|
||||
pub occurrence_date: Option<String>, // ISO date string for specific occurrence
|
||||
pub exception_dates: Option<Vec<String>>, // ISO datetime strings for EXDATE
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub until_date: Option<String>, // ISO datetime string for RRULE UNTIL clause
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
||||
96
src/app.rs
96
src/app.rs
@@ -2,10 +2,36 @@ use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
use gloo_storage::{LocalStorage, Storage};
|
||||
use web_sys::MouseEvent;
|
||||
use crate::components::{Sidebar, ViewMode, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler, EventStatus, EventClass, ReminderType, RecurrenceType, DeleteAction};
|
||||
use crate::components::{Sidebar, ViewMode, Theme, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler, EventStatus, EventClass, ReminderType, RecurrenceType, DeleteAction};
|
||||
use crate::services::{CalendarService, calendar_service::{UserInfo, CalendarEvent}};
|
||||
use chrono::NaiveDate;
|
||||
|
||||
fn get_theme_event_colors() -> Vec<String> {
|
||||
if let Some(window) = web_sys::window() {
|
||||
if let Some(document) = window.document() {
|
||||
if let Some(root) = document.document_element() {
|
||||
if let Ok(Some(computed_style)) = window.get_computed_style(&root) {
|
||||
if let Ok(colors_string) = computed_style.get_property_value("--event-colors") {
|
||||
if !colors_string.is_empty() {
|
||||
return colors_string
|
||||
.split(',')
|
||||
.map(|color| color.trim().to_string())
|
||||
.filter(|color| !color.is_empty())
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vec![
|
||||
"#3B82F6".to_string(), "#10B981".to_string(), "#F59E0B".to_string(), "#EF4444".to_string(),
|
||||
"#8B5CF6".to_string(), "#06B6D4".to_string(), "#84CC16".to_string(), "#F97316".to_string(),
|
||||
"#EC4899".to_string(), "#6366F1".to_string(), "#14B8A6".to_string(), "#F3B806".to_string(),
|
||||
"#8B5A2B".to_string(), "#6B7280".to_string(), "#DC2626".to_string(), "#7C3AED".to_string()
|
||||
]
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn App() -> Html {
|
||||
@@ -41,12 +67,17 @@ pub fn App() -> Html {
|
||||
}
|
||||
});
|
||||
|
||||
let available_colors = [
|
||||
"#3B82F6", "#10B981", "#F59E0B", "#EF4444",
|
||||
"#8B5CF6", "#06B6D4", "#84CC16", "#F97316",
|
||||
"#EC4899", "#6366F1", "#14B8A6", "#F3B806",
|
||||
"#8B5A2B", "#6B7280", "#DC2626", "#7C3AED"
|
||||
];
|
||||
// Theme state - load from localStorage if available
|
||||
let current_theme = use_state(|| {
|
||||
// Try to load saved theme from localStorage
|
||||
if let Ok(saved_theme) = LocalStorage::get::<String>("calendar_theme") {
|
||||
Theme::from_value(&saved_theme)
|
||||
} else {
|
||||
Theme::Default // Default theme
|
||||
}
|
||||
});
|
||||
|
||||
let available_colors = use_state(|| get_theme_event_colors());
|
||||
|
||||
let on_login = {
|
||||
let auth_token = auth_token.clone();
|
||||
@@ -80,6 +111,41 @@ pub fn App() -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
let on_theme_change = {
|
||||
let current_theme = current_theme.clone();
|
||||
let available_colors = available_colors.clone();
|
||||
Callback::from(move |new_theme: Theme| {
|
||||
// Save theme to localStorage
|
||||
let _ = LocalStorage::set("calendar_theme", new_theme.value());
|
||||
|
||||
// Apply theme to document root
|
||||
if let Some(document) = web_sys::window().and_then(|w| w.document()) {
|
||||
if let Some(root) = document.document_element() {
|
||||
let _ = root.set_attribute("data-theme", new_theme.value());
|
||||
}
|
||||
}
|
||||
|
||||
// Update state
|
||||
current_theme.set(new_theme);
|
||||
|
||||
// Update available colors after theme change
|
||||
available_colors.set(get_theme_event_colors());
|
||||
})
|
||||
};
|
||||
|
||||
// Apply initial theme on mount
|
||||
{
|
||||
let current_theme = current_theme.clone();
|
||||
use_effect_with((), move |_| {
|
||||
let theme = (*current_theme).clone();
|
||||
if let Some(document) = web_sys::window().and_then(|w| w.document()) {
|
||||
if let Some(root) = document.document_element() {
|
||||
let _ = root.set_attribute("data-theme", theme.value());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch user info when token is available
|
||||
{
|
||||
let user_info = user_info.clone();
|
||||
@@ -347,7 +413,7 @@ pub fn App() -> Html {
|
||||
|
||||
let on_event_update = {
|
||||
let auth_token = auth_token.clone();
|
||||
Callback::from(move |(original_event, new_start, new_end): (CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime)| {
|
||||
Callback::from(move |(original_event, new_start, new_end, preserve_rrule, until_date): (CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime, bool, Option<chrono::DateTime<chrono::Utc>>)| {
|
||||
web_sys::console::log_1(&format!("Updating event: {} to new times: {} - {}",
|
||||
original_event.uid,
|
||||
new_start.format("%Y-%m-%d %H:%M"),
|
||||
@@ -428,7 +494,10 @@ pub fn App() -> Html {
|
||||
reminder_str,
|
||||
recurrence_str,
|
||||
recurrence_days,
|
||||
original_event.calendar_path
|
||||
original_event.calendar_path,
|
||||
original_event.exception_dates.clone(),
|
||||
if preserve_rrule { Some("update_series".to_string()) } else { None },
|
||||
until_date
|
||||
).await {
|
||||
Ok(_) => {
|
||||
web_sys::console::log_1(&"Event updated successfully".into());
|
||||
@@ -506,10 +575,12 @@ pub fn App() -> Html {
|
||||
color_picker_open={(*color_picker_open).clone()}
|
||||
on_color_change={on_color_change}
|
||||
on_color_picker_toggle={on_color_picker_toggle}
|
||||
available_colors={available_colors.iter().map(|c| c.to_string()).collect::<Vec<_>>()}
|
||||
available_colors={(*available_colors).clone()}
|
||||
on_calendar_context_menu={on_calendar_context_menu}
|
||||
current_view={(*current_view).clone()}
|
||||
on_view_change={on_view_change}
|
||||
current_theme={(*current_theme).clone()}
|
||||
on_theme_change={on_theme_change}
|
||||
/>
|
||||
<main class="app-main">
|
||||
<RouteHandler
|
||||
@@ -919,7 +990,10 @@ pub fn App() -> Html {
|
||||
reminder_str,
|
||||
recurrence_str,
|
||||
updated_data.recurrence_days,
|
||||
updated_data.selected_calendar
|
||||
updated_data.selected_calendar,
|
||||
original_event.exception_dates.clone(),
|
||||
Some("update_series".to_string()), // This is for event edit modal, preserve original RRULE
|
||||
None // No until_date for edit modal
|
||||
).await {
|
||||
Ok(_) => {
|
||||
web_sys::console::log_1(&"Event updated successfully".into());
|
||||
|
||||
@@ -24,7 +24,7 @@ pub struct CalendarProps {
|
||||
#[prop_or_default]
|
||||
pub on_create_event_request: Option<Callback<EventCreationData>>,
|
||||
#[prop_or_default]
|
||||
pub on_event_update_request: Option<Callback<(CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime)>>,
|
||||
pub on_event_update_request: Option<Callback<(CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime, bool, Option<chrono::DateTime<chrono::Utc>>)>>,
|
||||
#[prop_or_default]
|
||||
pub context_menus_open: bool,
|
||||
}
|
||||
@@ -194,9 +194,9 @@ pub fn Calendar(props: &CalendarProps) -> Html {
|
||||
// Handle drag-to-move event
|
||||
let on_event_update = {
|
||||
let on_event_update_request = props.on_event_update_request.clone();
|
||||
Callback::from(move |(event, new_start, new_end): (CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime)| {
|
||||
Callback::from(move |(event, new_start, new_end, preserve_rrule, until_date): (CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime, bool, Option<chrono::DateTime<chrono::Utc>>)| {
|
||||
if let Some(callback) = &on_event_update_request {
|
||||
callback.emit((event, new_start, new_end));
|
||||
callback.emit((event, new_start, new_end, preserve_rrule, until_date));
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ pub use context_menu::ContextMenu;
|
||||
pub use event_context_menu::{EventContextMenu, DeleteAction};
|
||||
pub use calendar_context_menu::CalendarContextMenu;
|
||||
pub use create_event_modal::{CreateEventModal, EventCreationData, EventStatus, EventClass, ReminderType, RecurrenceType};
|
||||
pub use sidebar::{Sidebar, ViewMode};
|
||||
pub use sidebar::{Sidebar, ViewMode, Theme};
|
||||
pub use calendar_list_item::CalendarListItem;
|
||||
pub use route_handler::RouteHandler;
|
||||
pub use recurring_edit_modal::{RecurringEditModal, RecurringEditAction};
|
||||
@@ -27,7 +27,7 @@ pub struct RouteHandlerProps {
|
||||
#[prop_or_default]
|
||||
pub on_create_event_request: Option<Callback<crate::components::EventCreationData>>,
|
||||
#[prop_or_default]
|
||||
pub on_event_update_request: Option<Callback<(CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime)>>,
|
||||
pub on_event_update_request: Option<Callback<(CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime, bool, Option<chrono::DateTime<chrono::Utc>>)>>,
|
||||
#[prop_or_default]
|
||||
pub context_menus_open: bool,
|
||||
}
|
||||
@@ -105,7 +105,7 @@ pub struct CalendarViewProps {
|
||||
#[prop_or_default]
|
||||
pub on_create_event_request: Option<Callback<crate::components::EventCreationData>>,
|
||||
#[prop_or_default]
|
||||
pub on_event_update_request: Option<Callback<(CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime)>>,
|
||||
pub on_event_update_request: Option<Callback<(CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime, bool, Option<chrono::DateTime<chrono::Utc>>)>>,
|
||||
#[prop_or_default]
|
||||
pub context_menus_open: bool,
|
||||
}
|
||||
|
||||
@@ -20,6 +20,59 @@ pub enum ViewMode {
|
||||
Week,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum Theme {
|
||||
Default,
|
||||
Ocean,
|
||||
Forest,
|
||||
Sunset,
|
||||
Purple,
|
||||
Dark,
|
||||
Rose,
|
||||
Mint,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Theme::Default => "Default",
|
||||
Theme::Ocean => "Ocean",
|
||||
Theme::Forest => "Forest",
|
||||
Theme::Sunset => "Sunset",
|
||||
Theme::Purple => "Purple",
|
||||
Theme::Dark => "Dark",
|
||||
Theme::Rose => "Rose",
|
||||
Theme::Mint => "Mint",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value(&self) -> &'static str {
|
||||
match self {
|
||||
Theme::Default => "default",
|
||||
Theme::Ocean => "ocean",
|
||||
Theme::Forest => "forest",
|
||||
Theme::Sunset => "sunset",
|
||||
Theme::Purple => "purple",
|
||||
Theme::Dark => "dark",
|
||||
Theme::Rose => "rose",
|
||||
Theme::Mint => "mint",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_value(value: &str) -> Self {
|
||||
match value {
|
||||
"ocean" => Theme::Ocean,
|
||||
"forest" => Theme::Forest,
|
||||
"sunset" => Theme::Sunset,
|
||||
"purple" => Theme::Purple,
|
||||
"dark" => Theme::Dark,
|
||||
"rose" => Theme::Rose,
|
||||
"mint" => Theme::Mint,
|
||||
_ => Theme::Default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ViewMode {
|
||||
fn default() -> Self {
|
||||
ViewMode::Month
|
||||
@@ -38,6 +91,8 @@ pub struct SidebarProps {
|
||||
pub on_calendar_context_menu: Callback<(MouseEvent, String)>,
|
||||
pub current_view: ViewMode,
|
||||
pub on_view_change: Callback<ViewMode>,
|
||||
pub current_theme: Theme,
|
||||
pub on_theme_change: Callback<Theme>,
|
||||
}
|
||||
|
||||
#[function_component(Sidebar)]
|
||||
@@ -57,6 +112,18 @@ pub fn sidebar(props: &SidebarProps) -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
let on_theme_change = {
|
||||
let on_theme_change = props.on_theme_change.clone();
|
||||
Callback::from(move |e: Event| {
|
||||
let target = e.target_dyn_into::<HtmlSelectElement>();
|
||||
if let Some(select) = target {
|
||||
let value = select.value();
|
||||
let new_theme = Theme::from_value(&value);
|
||||
on_theme_change.emit(new_theme);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<aside class="app-sidebar">
|
||||
<div class="sidebar-header">
|
||||
@@ -120,6 +187,19 @@ pub fn sidebar(props: &SidebarProps) -> Html {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="theme-selector">
|
||||
<select class="theme-selector-dropdown" onchange={on_theme_change}>
|
||||
<option value="default" selected={matches!(props.current_theme, Theme::Default)}>{"🎨 Default"}</option>
|
||||
<option value="ocean" selected={matches!(props.current_theme, Theme::Ocean)}>{"🌊 Ocean"}</option>
|
||||
<option value="forest" selected={matches!(props.current_theme, Theme::Forest)}>{"🌲 Forest"}</option>
|
||||
<option value="sunset" selected={matches!(props.current_theme, Theme::Sunset)}>{"🌅 Sunset"}</option>
|
||||
<option value="purple" selected={matches!(props.current_theme, Theme::Purple)}>{"💜 Purple"}</option>
|
||||
<option value="dark" selected={matches!(props.current_theme, Theme::Dark)}>{"🌙 Dark"}</option>
|
||||
<option value="rose" selected={matches!(props.current_theme, Theme::Rose)}>{"🌹 Rose"}</option>
|
||||
<option value="mint" selected={matches!(props.current_theme, Theme::Mint)}>{"🍃 Mint"}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button onclick={props.on_logout.reform(|_| ())} class="logout-button">{"Logout"}</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -24,7 +24,7 @@ pub struct WeekViewProps {
|
||||
#[prop_or_default]
|
||||
pub on_create_event_request: Option<Callback<EventCreationData>>,
|
||||
#[prop_or_default]
|
||||
pub on_event_update: Option<Callback<(CalendarEvent, NaiveDateTime, NaiveDateTime)>>,
|
||||
pub on_event_update: Option<Callback<(CalendarEvent, NaiveDateTime, NaiveDateTime, bool, Option<chrono::DateTime<chrono::Utc>>)>>,
|
||||
#[prop_or_default]
|
||||
pub context_menus_open: bool,
|
||||
#[prop_or_default]
|
||||
@@ -106,6 +106,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
let on_event_update = props.on_event_update.clone();
|
||||
let on_create_event = props.on_create_event.clone();
|
||||
let on_create_event_request = props.on_create_event_request.clone();
|
||||
let events = props.events.clone();
|
||||
Callback::from(move |action: RecurringEditAction| {
|
||||
if let Some(edit) = (*pending_recurring_edit).clone() {
|
||||
match action {
|
||||
@@ -127,60 +128,142 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
).into());
|
||||
|
||||
// Update the original series with the exception (times unchanged)
|
||||
update_callback.emit((updated_series, original_start, original_end));
|
||||
update_callback.emit((updated_series, original_start, original_end, true, None)); // preserve_rrule = true for EXDATE, no until_date
|
||||
}
|
||||
|
||||
// 2. Then create the new single event using the update callback (to avoid refresh)
|
||||
if let Some(update_callback) = &on_event_update {
|
||||
let mut new_event = edit.event.clone();
|
||||
new_event.uid = format!("{}-exception-{}", edit.event.uid, edit.event.start.timestamp());
|
||||
new_event.start = chrono::DateTime::from_naive_utc_and_offset(edit.new_start, chrono::Utc);
|
||||
new_event.end = Some(chrono::DateTime::from_naive_utc_and_offset(edit.new_end, chrono::Utc));
|
||||
new_event.recurrence_rule = None; // This is now a single event
|
||||
new_event.exception_dates.clear(); // No exception dates for single event
|
||||
// 2. Then create the new single event using the create callback
|
||||
if let Some(create_callback) = &on_create_event_request {
|
||||
// Convert to EventCreationData for single event
|
||||
let event_data = EventCreationData {
|
||||
title: edit.event.summary.clone().unwrap_or_default(),
|
||||
description: edit.event.description.clone().unwrap_or_default(),
|
||||
start_date: edit.new_start.date(),
|
||||
start_time: edit.new_start.time(),
|
||||
end_date: edit.new_end.date(),
|
||||
end_time: edit.new_end.time(),
|
||||
location: edit.event.location.clone().unwrap_or_default(),
|
||||
all_day: edit.event.all_day,
|
||||
status: EventStatus::Confirmed,
|
||||
class: EventClass::Public,
|
||||
priority: edit.event.priority,
|
||||
organizer: edit.event.organizer.clone().unwrap_or_default(),
|
||||
attendees: edit.event.attendees.join(","),
|
||||
categories: edit.event.categories.join(","),
|
||||
reminder: ReminderType::None,
|
||||
recurrence: RecurrenceType::None, // Single event, no recurrence
|
||||
recurrence_days: vec![false; 7],
|
||||
selected_calendar: edit.event.calendar_path.clone(),
|
||||
};
|
||||
|
||||
// Use update callback to create the new event (should work without refresh)
|
||||
update_callback.emit((new_event, edit.new_start, edit.new_end));
|
||||
// Create the single event
|
||||
create_callback.emit(event_data);
|
||||
}
|
||||
},
|
||||
RecurringEditAction::FutureEvents => {
|
||||
// Split series and modify future events
|
||||
web_sys::console::log_1(&format!("🔄 Splitting series and modifying future: {}",
|
||||
edit.event.summary.as_deref().unwrap_or("Untitled")).into());
|
||||
// 1. Update original series to end before this occurrence
|
||||
// 1. Update original series to set UNTIL to end before this occurrence
|
||||
if let Some(update_callback) = &on_event_update {
|
||||
let mut original_series = edit.event.clone();
|
||||
|
||||
// Add UNTIL clause to end the series before this occurrence
|
||||
let until_date = edit.event.start - chrono::Duration::days(1);
|
||||
|
||||
// Parse existing RRULE and add UNTIL
|
||||
if let Some(rrule) = &original_series.recurrence_rule {
|
||||
let updated_rrule = if rrule.contains("UNTIL=") {
|
||||
// Replace existing UNTIL
|
||||
let re = regex::Regex::new(r"UNTIL=[^;]*").unwrap();
|
||||
re.replace(rrule, &format!("UNTIL={}", until_date.format("%Y%m%dT%H%M%SZ"))).to_string()
|
||||
// Find the original series event (not the occurrence)
|
||||
// UIDs like "uuid-timestamp" need to split on the last hyphen, not the first
|
||||
let base_uid = if let Some(last_hyphen_pos) = edit.event.uid.rfind('-') {
|
||||
let suffix = &edit.event.uid[last_hyphen_pos + 1..];
|
||||
// Check if suffix is numeric (timestamp), if so remove it
|
||||
if suffix.chars().all(|c| c.is_numeric()) {
|
||||
edit.event.uid[..last_hyphen_pos].to_string()
|
||||
} else {
|
||||
// Add UNTIL to existing rule
|
||||
format!("{};UNTIL={}", rrule, until_date.format("%Y%m%dT%H%M%SZ"))
|
||||
};
|
||||
original_series.recurrence_rule = Some(updated_rrule);
|
||||
edit.event.uid.clone()
|
||||
}
|
||||
} else {
|
||||
edit.event.uid.clone()
|
||||
};
|
||||
|
||||
web_sys::console::log_1(&format!("🔍 Looking for original series: '{}' from occurrence: '{}'", base_uid, edit.event.uid).into());
|
||||
|
||||
// Find the original series event by searching for the base UID
|
||||
let mut original_series = None;
|
||||
for events_list in events.values() {
|
||||
for event in events_list {
|
||||
if event.uid == base_uid {
|
||||
original_series = Some(event.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if original_series.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update original series
|
||||
update_callback.emit((original_series, edit.event.start.with_timezone(&chrono::Local).naive_local(), edit.event.end.unwrap_or(edit.event.start).with_timezone(&chrono::Local).naive_local()));
|
||||
let mut original_series = match original_series {
|
||||
Some(series) => {
|
||||
web_sys::console::log_1(&format!("✅ Found original series: '{}'", series.uid).into());
|
||||
series
|
||||
},
|
||||
None => {
|
||||
web_sys::console::log_1(&format!("⚠️ Could not find original series '{}', using occurrence but fixing UID", base_uid).into());
|
||||
let mut fallback_event = edit.event.clone();
|
||||
// Ensure the UID is the base UID, not the occurrence UID
|
||||
fallback_event.uid = base_uid.clone();
|
||||
fallback_event
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate the day before this occurrence for UNTIL clause
|
||||
let until_date = edit.event.start.date_naive() - chrono::Duration::days(1);
|
||||
let until_datetime = until_date.and_time(chrono::NaiveTime::from_hms_opt(23, 59, 59).unwrap());
|
||||
let until_utc = chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(until_datetime, chrono::Utc);
|
||||
|
||||
web_sys::console::log_1(&format!("🔄 Will set UNTIL {} for original series to end before occurrence {}",
|
||||
until_utc.format("%Y-%m-%d %H:%M:%S UTC"),
|
||||
edit.event.start.format("%Y-%m-%d %H:%M:%S UTC")).into());
|
||||
|
||||
// Use the original series start time (not the dragged occurrence time)
|
||||
let original_start = original_series.start.with_timezone(&chrono::Local).naive_local();
|
||||
let original_end = original_series.end.unwrap_or(original_series.start).with_timezone(&chrono::Local).naive_local();
|
||||
|
||||
// Send until_date to backend instead of modifying RRULE on frontend
|
||||
update_callback.emit((original_series, original_start, original_end, true, Some(until_utc))); // preserve_rrule = true, backend will add UNTIL
|
||||
}
|
||||
|
||||
// 2. Create new series starting from this occurrence with modified times
|
||||
if let Some(update_callback) = &on_event_update {
|
||||
let mut new_series = edit.event.clone();
|
||||
new_series.uid = format!("{}-future-{}", edit.event.uid, edit.event.start.timestamp());
|
||||
new_series.start = chrono::DateTime::from_naive_utc_and_offset(edit.new_start, chrono::Utc);
|
||||
new_series.end = Some(chrono::DateTime::from_naive_utc_and_offset(edit.new_end, chrono::Utc));
|
||||
new_series.exception_dates.clear(); // New series has no exceptions
|
||||
if let Some(create_callback) = &on_create_event_request {
|
||||
// Convert the recurring event to EventCreationData for the create callback
|
||||
let event_data = EventCreationData {
|
||||
title: edit.event.summary.clone().unwrap_or_default(),
|
||||
description: edit.event.description.clone().unwrap_or_default(),
|
||||
start_date: edit.new_start.date(),
|
||||
start_time: edit.new_start.time(),
|
||||
end_date: edit.new_end.date(),
|
||||
end_time: edit.new_end.time(),
|
||||
location: edit.event.location.clone().unwrap_or_default(),
|
||||
all_day: edit.event.all_day,
|
||||
status: EventStatus::Confirmed, // Default status
|
||||
class: EventClass::Public, // Default class
|
||||
priority: edit.event.priority,
|
||||
organizer: edit.event.organizer.clone().unwrap_or_default(),
|
||||
attendees: edit.event.attendees.join(","),
|
||||
categories: edit.event.categories.join(","),
|
||||
reminder: ReminderType::None, // Default reminder
|
||||
recurrence: if let Some(rrule) = &edit.event.recurrence_rule {
|
||||
if rrule.contains("FREQ=DAILY") {
|
||||
RecurrenceType::Daily
|
||||
} else if rrule.contains("FREQ=WEEKLY") {
|
||||
RecurrenceType::Weekly
|
||||
} else if rrule.contains("FREQ=MONTHLY") {
|
||||
RecurrenceType::Monthly
|
||||
} else if rrule.contains("FREQ=YEARLY") {
|
||||
RecurrenceType::Yearly
|
||||
} else {
|
||||
RecurrenceType::None
|
||||
}
|
||||
} else {
|
||||
RecurrenceType::None
|
||||
},
|
||||
recurrence_days: vec![false; 7], // Default days
|
||||
selected_calendar: edit.event.calendar_path.clone(),
|
||||
};
|
||||
|
||||
// Use update callback to create the new series (should work without refresh)
|
||||
update_callback.emit((new_series, edit.new_start, edit.new_end));
|
||||
// Create the new series
|
||||
create_callback.emit(event_data);
|
||||
}
|
||||
},
|
||||
RecurringEditAction::AllEvents => {
|
||||
@@ -188,7 +271,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
let series_event = edit.event.clone();
|
||||
|
||||
if let Some(callback) = &on_event_update {
|
||||
callback.emit((series_event, edit.new_start, edit.new_end));
|
||||
callback.emit((series_event, edit.new_start, edit.new_end, true, None)); // Regular drag operation - preserve RRULE, no until_date
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -382,7 +465,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
} else {
|
||||
// Regular event - proceed with update
|
||||
if let Some(callback) = &on_event_update {
|
||||
callback.emit((event.clone(), new_start_datetime, new_end_datetime));
|
||||
callback.emit((event.clone(), new_start_datetime, new_end_datetime, true, None)); // Regular drag operation - preserve RRULE, no until_date
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -418,7 +501,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
} else {
|
||||
// Regular event - proceed with update
|
||||
if let Some(callback) = &on_event_update {
|
||||
callback.emit((event.clone(), new_start_datetime, new_end_datetime));
|
||||
callback.emit((event.clone(), new_start_datetime, new_end_datetime, true, None)); // Regular drag operation - preserve RRULE, no until_date
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -449,7 +532,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
} else {
|
||||
// Regular event - proceed with update
|
||||
if let Some(callback) = &on_event_update {
|
||||
callback.emit((event.clone(), new_start_datetime, new_end_datetime));
|
||||
callback.emit((event.clone(), new_start_datetime, new_end_datetime, true, None)); // Regular drag operation - preserve RRULE, no until_date
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,7 +718,7 @@ impl CalendarService {
|
||||
|
||||
let body_string = serde_json::to_string(&body)
|
||||
.map_err(|e| format!("JSON serialization failed: {}", e))?;
|
||||
|
||||
|
||||
let url = format!("{}/calendar/events/create", self.base_url);
|
||||
opts.set_body(&body_string.into());
|
||||
let request = Request::new_with_str_and_init(&url, &opts)
|
||||
@@ -777,7 +777,10 @@ impl CalendarService {
|
||||
reminder: String,
|
||||
recurrence: String,
|
||||
recurrence_days: Vec<bool>,
|
||||
calendar_path: Option<String>
|
||||
calendar_path: Option<String>,
|
||||
exception_dates: Vec<DateTime<Utc>>,
|
||||
update_action: Option<String>,
|
||||
until_date: Option<DateTime<Utc>>
|
||||
) -> Result<(), String> {
|
||||
let window = web_sys::window().ok_or("No global window exists")?;
|
||||
|
||||
@@ -805,13 +808,15 @@ impl CalendarService {
|
||||
"recurrence": recurrence,
|
||||
"recurrence_days": recurrence_days,
|
||||
"calendar_path": calendar_path,
|
||||
"update_action": "update_series",
|
||||
"occurrence_date": null
|
||||
"update_action": update_action,
|
||||
"occurrence_date": null,
|
||||
"exception_dates": exception_dates.iter().map(|dt| dt.to_rfc3339()).collect::<Vec<String>>(),
|
||||
"until_date": until_date.as_ref().map(|dt| dt.to_rfc3339())
|
||||
});
|
||||
|
||||
let body_string = serde_json::to_string(&body)
|
||||
.map_err(|e| format!("JSON serialization failed: {}", e))?;
|
||||
|
||||
|
||||
let url = format!("{}/calendar/events/update", self.base_url);
|
||||
opts.set_body(&body_string.into());
|
||||
let request = Request::new_with_str_and_init(&url, &opts)
|
||||
|
||||
454
styles.css
454
styles.css
@@ -383,7 +383,7 @@ body {
|
||||
|
||||
/* Calendar Component */
|
||||
.calendar {
|
||||
background: white;
|
||||
background: var(--calendar-bg, white);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
@@ -397,8 +397,8 @@ body {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1.5rem 2rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
background: var(--header-bg, linear-gradient(135deg, #667eea 0%, #764ba2 100%));
|
||||
color: var(--header-text, white);
|
||||
}
|
||||
|
||||
.month-year {
|
||||
@@ -486,7 +486,7 @@ body {
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
grid-template-rows: auto repeat(6, 1fr);
|
||||
flex: 1;
|
||||
background: white;
|
||||
background: var(--calendar-bg, white);
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
@@ -495,41 +495,42 @@ body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: white;
|
||||
background: var(--calendar-bg, white);
|
||||
}
|
||||
|
||||
/* Week Header */
|
||||
.week-header {
|
||||
display: grid;
|
||||
grid-template-columns: 80px repeat(7, 1fr);
|
||||
background: #f8f9fa;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
background: var(--weekday-header-bg, #f8f9fa);
|
||||
border-bottom: 2px solid var(--time-label-border, #e9ecef);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.time-gutter {
|
||||
background: #f8f9fa;
|
||||
border-right: 1px solid #e9ecef;
|
||||
background: var(--time-label-bg, #f8f9fa);
|
||||
border-right: 1px solid var(--time-label-border, #e9ecef);
|
||||
}
|
||||
|
||||
.week-day-header {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
border-right: 1px solid #e9ecef;
|
||||
background: #f8f9fa;
|
||||
border-right: 1px solid var(--time-label-border, #e9ecef);
|
||||
background: var(--weekday-header-bg, #f8f9fa);
|
||||
color: var(--weekday-header-text, inherit);
|
||||
}
|
||||
|
||||
.week-day-header.today {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
background: var(--calendar-today-bg, #e3f2fd);
|
||||
color: var(--calendar-today-text, #1976d2);
|
||||
}
|
||||
|
||||
.weekday-name {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
color: var(--weekday-header-text, #666);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 0.25rem;
|
||||
@@ -541,7 +542,7 @@ body {
|
||||
}
|
||||
|
||||
.week-day-header.today .weekday-name {
|
||||
color: #1976d2;
|
||||
color: var(--calendar-today-text, #1976d2);
|
||||
}
|
||||
|
||||
/* Week Content */
|
||||
@@ -559,8 +560,8 @@ body {
|
||||
|
||||
/* Time Labels */
|
||||
.time-labels {
|
||||
background: #f8f9fa;
|
||||
border-right: 1px solid #e9ecef;
|
||||
background: var(--time-label-bg, #f8f9fa);
|
||||
border-right: 1px solid var(--time-label-border, #e9ecef);
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 5;
|
||||
@@ -573,8 +574,8 @@ body {
|
||||
justify-content: center;
|
||||
padding-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: #666;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
color: var(--time-label-text, #666);
|
||||
border-bottom: 1px solid var(--calendar-border, #f0f0f0);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -593,7 +594,7 @@ body {
|
||||
|
||||
.week-day-column {
|
||||
position: relative;
|
||||
border-right: 1px solid #e9ecef;
|
||||
border-right: 1px solid var(--time-label-border, #e9ecef);
|
||||
min-height: 1500px; /* 25 time labels × 60px = 1500px total */
|
||||
}
|
||||
|
||||
@@ -602,20 +603,20 @@ body {
|
||||
}
|
||||
|
||||
.week-day-column.today {
|
||||
background: #fafffe;
|
||||
background: var(--calendar-day-hover, #fafffe);
|
||||
}
|
||||
|
||||
/* Time Slots */
|
||||
.time-slot {
|
||||
height: 60px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
border-bottom: 1px solid var(--calendar-border, #f0f0f0);
|
||||
position: relative;
|
||||
pointer-events: none; /* Don't capture mouse events */
|
||||
}
|
||||
|
||||
.time-slot-half {
|
||||
height: 30px;
|
||||
border-bottom: 1px dotted #f5f5f5;
|
||||
border-bottom: 1px dotted var(--calendar-border, #f5f5f5);
|
||||
pointer-events: none; /* Don't capture mouse events */
|
||||
}
|
||||
|
||||
@@ -839,7 +840,7 @@ body {
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
grid-template-rows: auto 1fr;
|
||||
flex: 1;
|
||||
background: white;
|
||||
background: var(--calendar-bg, white);
|
||||
}
|
||||
|
||||
.week-view .calendar-day {
|
||||
@@ -859,7 +860,7 @@ body {
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
border: 1px solid #f0f0f0;
|
||||
border: 1px solid var(--calendar-border, #f0f0f0);
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -867,52 +868,53 @@ body {
|
||||
transition: background-color 0.2s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: var(--calendar-day-bg, white);
|
||||
}
|
||||
|
||||
.calendar-day:hover {
|
||||
background-color: #f8f9ff;
|
||||
background-color: var(--calendar-day-hover, #f8f9ff);
|
||||
}
|
||||
|
||||
.calendar-day.current-month {
|
||||
background: white;
|
||||
background: var(--calendar-day-bg, white);
|
||||
}
|
||||
|
||||
.calendar-day.prev-month,
|
||||
.calendar-day.next-month {
|
||||
background: #fafafa;
|
||||
color: #ccc;
|
||||
background: var(--calendar-day-prev-next, #fafafa);
|
||||
color: var(--calendar-day-prev-next-text, #ccc);
|
||||
}
|
||||
|
||||
.calendar-day.today {
|
||||
background: #e3f2fd;
|
||||
border: 2px solid #2196f3;
|
||||
background: var(--calendar-today-bg, #e3f2fd);
|
||||
border: 2px solid var(--calendar-today-border, #2196f3);
|
||||
}
|
||||
|
||||
.calendar-day.has-events {
|
||||
background: #fff3e0;
|
||||
background: var(--calendar-has-events-bg, #fff3e0);
|
||||
}
|
||||
|
||||
.calendar-day.today.has-events {
|
||||
background: #e1f5fe;
|
||||
background: var(--calendar-today-bg, #e1f5fe);
|
||||
}
|
||||
|
||||
.calendar-day.selected {
|
||||
background: #e8f5e8;
|
||||
border: 2px solid #4caf50;
|
||||
background: var(--calendar-selected-bg, #e8f5e8);
|
||||
border: 2px solid var(--calendar-selected-border, #4caf50);
|
||||
box-shadow: 0 0 8px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.calendar-day.selected.has-events {
|
||||
background: #f1f8e9;
|
||||
background: var(--calendar-selected-bg, #f1f8e9);
|
||||
}
|
||||
|
||||
.calendar-day.selected.today {
|
||||
background: #e0f2f1;
|
||||
border: 2px solid #4caf50;
|
||||
background: var(--calendar-selected-bg, #e0f2f1);
|
||||
border: 2px solid var(--calendar-selected-border, #4caf50);
|
||||
}
|
||||
|
||||
.calendar-day.selected .day-number {
|
||||
color: #2e7d32;
|
||||
color: var(--calendar-selected-text, #2e7d32);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -923,7 +925,7 @@ body {
|
||||
}
|
||||
|
||||
.calendar-day.today .day-number {
|
||||
color: #1976d2;
|
||||
color: var(--calendar-today-text, #1976d2);
|
||||
}
|
||||
|
||||
.day-events {
|
||||
@@ -1896,4 +1898,376 @@ body {
|
||||
.recurring-option .option-description {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Theme Selector Styles */
|
||||
.theme-selector {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.theme-selector-dropdown {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.theme-selector-dropdown:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.theme-selector-dropdown:focus {
|
||||
outline: none;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.theme-selector-dropdown option {
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
/* Theme Definitions */
|
||||
:root {
|
||||
/* Default Theme */
|
||||
--primary-gradient: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
|
||||
--primary-bg: #f8f9fa;
|
||||
--primary-text: #333;
|
||||
--sidebar-bg: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
|
||||
--sidebar-text: white;
|
||||
--header-bg: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--header-text: white;
|
||||
--card-bg: white;
|
||||
--border-color: #e9ecef;
|
||||
--accent-color: #667eea;
|
||||
--calendar-bg: white;
|
||||
--calendar-border: #f0f0f0;
|
||||
--calendar-day-bg: white;
|
||||
--calendar-day-hover: #f8f9ff;
|
||||
--calendar-day-prev-next: #fafafa;
|
||||
--calendar-day-prev-next-text: #ccc;
|
||||
--calendar-today-bg: #e3f2fd;
|
||||
--calendar-today-border: #2196f3;
|
||||
--calendar-today-text: #1976d2;
|
||||
--calendar-selected-bg: #e8f5e8;
|
||||
--calendar-selected-border: #4caf50;
|
||||
--calendar-selected-text: #2e7d32;
|
||||
--calendar-has-events-bg: #fff3e0;
|
||||
--weekday-header-bg: #f8f9fa;
|
||||
--weekday-header-text: #666;
|
||||
--time-label-bg: #f8f9fa;
|
||||
--time-label-text: #666;
|
||||
--time-label-border: #e9ecef;
|
||||
--event-colors: #3B82F6, #10B981, #F59E0B, #EF4444, #8B5CF6, #06B6D4, #84CC16, #F97316, #EC4899, #6366F1, #14B8A6, #F3B806, #8B5A2B, #6B7280, #DC2626, #7C3AED;
|
||||
}
|
||||
|
||||
/* Ocean Theme */
|
||||
[data-theme="ocean"] {
|
||||
--primary-gradient: linear-gradient(180deg, #2196F3 0%, #0277BD 100%);
|
||||
--primary-bg: #e3f2fd;
|
||||
--primary-text: #0d47a1;
|
||||
--sidebar-bg: linear-gradient(180deg, #2196F3 0%, #0277BD 100%);
|
||||
--sidebar-text: white;
|
||||
--header-bg: linear-gradient(135deg, #2196F3 0%, #0277BD 100%);
|
||||
--header-text: white;
|
||||
--card-bg: #ffffff;
|
||||
--border-color: #bbdefb;
|
||||
--accent-color: #2196F3;
|
||||
--calendar-bg: #ffffff;
|
||||
--calendar-border: #bbdefb;
|
||||
--calendar-day-bg: #ffffff;
|
||||
--calendar-day-hover: #e1f5fe;
|
||||
--calendar-day-prev-next: #f3f8ff;
|
||||
--calendar-day-prev-next-text: #90caf9;
|
||||
--calendar-today-bg: #b3e5fc;
|
||||
--calendar-today-border: #0277BD;
|
||||
--calendar-today-text: #01579b;
|
||||
--calendar-selected-bg: #e0f7fa;
|
||||
--calendar-selected-border: #00acc1;
|
||||
--calendar-selected-text: #00695c;
|
||||
--calendar-has-events-bg: #fff8e1;
|
||||
--weekday-header-bg: #e3f2fd;
|
||||
--weekday-header-text: #0d47a1;
|
||||
--time-label-bg: #e3f2fd;
|
||||
--time-label-text: #0d47a1;
|
||||
--time-label-border: #bbdefb;
|
||||
--event-colors: #2196F3, #03DAC6, #FF9800, #F44336, #9C27B0, #00BCD4, #8BC34A, #FF5722, #E91E63, #3F51B5, #009688, #FFC107, #607D8B, #795548, #E53935, #673AB7;
|
||||
}
|
||||
|
||||
[data-theme="ocean"] body {
|
||||
background-color: var(--primary-bg);
|
||||
color: var(--primary-text);
|
||||
}
|
||||
|
||||
[data-theme="ocean"] .app-sidebar {
|
||||
background: var(--sidebar-bg);
|
||||
}
|
||||
|
||||
/* Forest Theme */
|
||||
[data-theme="forest"] {
|
||||
--primary-gradient: linear-gradient(180deg, #4CAF50 0%, #2E7D32 100%);
|
||||
--primary-bg: #e8f5e8;
|
||||
--primary-text: #1b5e20;
|
||||
--sidebar-bg: linear-gradient(180deg, #4CAF50 0%, #2E7D32 100%);
|
||||
--sidebar-text: white;
|
||||
--header-bg: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
|
||||
--header-text: white;
|
||||
--card-bg: #ffffff;
|
||||
--border-color: #c8e6c9;
|
||||
--accent-color: #4CAF50;
|
||||
--calendar-bg: #ffffff;
|
||||
--calendar-border: #c8e6c9;
|
||||
--calendar-day-bg: #ffffff;
|
||||
--calendar-day-hover: #f1f8e9;
|
||||
--calendar-day-prev-next: #f9fbe7;
|
||||
--calendar-day-prev-next-text: #a5d6a7;
|
||||
--calendar-today-bg: #c8e6c9;
|
||||
--calendar-today-border: #2E7D32;
|
||||
--calendar-today-text: #1b5e20;
|
||||
--calendar-selected-bg: #e8f5e8;
|
||||
--calendar-selected-border: #388e3c;
|
||||
--calendar-selected-text: #2e7d32;
|
||||
--calendar-has-events-bg: #fff3e0;
|
||||
--weekday-header-bg: #e8f5e8;
|
||||
--weekday-header-text: #1b5e20;
|
||||
--time-label-bg: #e8f5e8;
|
||||
--time-label-text: #1b5e20;
|
||||
--time-label-border: #c8e6c9;
|
||||
--event-colors: #4CAF50, #8BC34A, #FF9800, #FF5722, #9C27B0, #03DAC6, #CDDC39, #FF6F00, #E91E63, #3F51B5, #009688, #FFC107, #795548, #607D8B, #F44336, #673AB7;
|
||||
}
|
||||
|
||||
[data-theme="forest"] body {
|
||||
background-color: var(--primary-bg);
|
||||
color: var(--primary-text);
|
||||
}
|
||||
|
||||
[data-theme="forest"] .app-sidebar {
|
||||
background: var(--sidebar-bg);
|
||||
}
|
||||
|
||||
/* Sunset Theme */
|
||||
[data-theme="sunset"] {
|
||||
--primary-gradient: linear-gradient(180deg, #FF9800 0%, #F57C00 100%);
|
||||
--primary-bg: #fff3e0;
|
||||
--primary-text: #e65100;
|
||||
--sidebar-bg: linear-gradient(180deg, #FF9800 0%, #F57C00 100%);
|
||||
--sidebar-text: white;
|
||||
--header-bg: linear-gradient(135deg, #FF9800 0%, #F57C00 100%);
|
||||
--header-text: white;
|
||||
--card-bg: #ffffff;
|
||||
--border-color: #ffcc02;
|
||||
--accent-color: #FF9800;
|
||||
--calendar-bg: #ffffff;
|
||||
--calendar-border: #ffe0b2;
|
||||
--calendar-day-bg: #ffffff;
|
||||
--calendar-day-hover: #fff8e1;
|
||||
--calendar-day-prev-next: #fffde7;
|
||||
--calendar-day-prev-next-text: #ffcc02;
|
||||
--calendar-today-bg: #ffe0b2;
|
||||
--calendar-today-border: #F57C00;
|
||||
--calendar-today-text: #e65100;
|
||||
--calendar-selected-bg: #fff3e0;
|
||||
--calendar-selected-border: #ff8f00;
|
||||
--calendar-selected-text: #ff6f00;
|
||||
--calendar-has-events-bg: #f3e5f5;
|
||||
--weekday-header-bg: #fff3e0;
|
||||
--weekday-header-text: #e65100;
|
||||
--time-label-bg: #fff3e0;
|
||||
--time-label-text: #e65100;
|
||||
--time-label-border: #ffe0b2;
|
||||
--event-colors: #FF9800, #FF5722, #F44336, #E91E63, #9C27B0, #673AB7, #3F51B5, #2196F3, #03DAC6, #009688, #4CAF50, #8BC34A, #CDDC39, #FFC107, #FF6F00, #795548;
|
||||
}
|
||||
|
||||
[data-theme="sunset"] body {
|
||||
background-color: var(--primary-bg);
|
||||
color: var(--primary-text);
|
||||
}
|
||||
|
||||
[data-theme="sunset"] .app-sidebar {
|
||||
background: var(--sidebar-bg);
|
||||
}
|
||||
|
||||
/* Purple Theme */
|
||||
[data-theme="purple"] {
|
||||
--primary-gradient: linear-gradient(180deg, #9C27B0 0%, #6A1B9A 100%);
|
||||
--primary-bg: #f3e5f5;
|
||||
--primary-text: #4a148c;
|
||||
--sidebar-bg: linear-gradient(180deg, #9C27B0 0%, #6A1B9A 100%);
|
||||
--sidebar-text: white;
|
||||
--header-bg: linear-gradient(135deg, #9C27B0 0%, #6A1B9A 100%);
|
||||
--header-text: white;
|
||||
--card-bg: #ffffff;
|
||||
--border-color: #ce93d8;
|
||||
--accent-color: #9C27B0;
|
||||
--calendar-bg: #ffffff;
|
||||
--calendar-border: #ce93d8;
|
||||
--calendar-day-bg: #ffffff;
|
||||
--calendar-day-hover: #f8e9fc;
|
||||
--calendar-day-prev-next: #fce4ec;
|
||||
--calendar-day-prev-next-text: #ce93d8;
|
||||
--calendar-today-bg: #e1bee7;
|
||||
--calendar-today-border: #6A1B9A;
|
||||
--calendar-today-text: #4a148c;
|
||||
--calendar-selected-bg: #f3e5f5;
|
||||
--calendar-selected-border: #8e24aa;
|
||||
--calendar-selected-text: #6a1b9a;
|
||||
--calendar-has-events-bg: #fff3e0;
|
||||
--weekday-header-bg: #f3e5f5;
|
||||
--weekday-header-text: #4a148c;
|
||||
--time-label-bg: #f3e5f5;
|
||||
--time-label-text: #4a148c;
|
||||
--time-label-border: #ce93d8;
|
||||
--event-colors: #9C27B0, #673AB7, #3F51B5, #2196F3, #03DAC6, #009688, #4CAF50, #8BC34A, #CDDC39, #FFC107, #FF9800, #FF5722, #F44336, #E91E63, #795548, #607D8B;
|
||||
}
|
||||
|
||||
[data-theme="purple"] body {
|
||||
background-color: var(--primary-bg);
|
||||
color: var(--primary-text);
|
||||
}
|
||||
|
||||
[data-theme="purple"] .app-sidebar {
|
||||
background: var(--sidebar-bg);
|
||||
}
|
||||
|
||||
/* Dark Theme */
|
||||
[data-theme="dark"] {
|
||||
--primary-gradient: linear-gradient(180deg, #424242 0%, #212121 100%);
|
||||
--primary-bg: #121212;
|
||||
--primary-text: #ffffff;
|
||||
--sidebar-bg: linear-gradient(180deg, #424242 0%, #212121 100%);
|
||||
--sidebar-text: white;
|
||||
--header-bg: linear-gradient(135deg, #424242 0%, #212121 100%);
|
||||
--header-text: white;
|
||||
--card-bg: #1e1e1e;
|
||||
--border-color: #333333;
|
||||
--accent-color: #666666;
|
||||
--calendar-bg: #1f1f1f;
|
||||
--calendar-border: #333333;
|
||||
--calendar-day-bg: #1f1f1f;
|
||||
--calendar-day-hover: #2a2a2a;
|
||||
--calendar-day-prev-next: #1a1a1a;
|
||||
--calendar-day-prev-next-text: #555;
|
||||
--calendar-today-bg: #2d2d2d;
|
||||
--calendar-today-border: #bb86fc;
|
||||
--calendar-today-text: #bb86fc;
|
||||
--calendar-selected-bg: #2a2a2a;
|
||||
--calendar-selected-border: #bb86fc;
|
||||
--calendar-selected-text: #bb86fc;
|
||||
--calendar-has-events-bg: #272727;
|
||||
--weekday-header-bg: #1a1a1a;
|
||||
--weekday-header-text: #e0e0e0;
|
||||
--time-label-bg: #1a1a1a;
|
||||
--time-label-text: #e0e0e0;
|
||||
--time-label-border: #333333;
|
||||
--event-colors: #bb86fc, #03dac6, #cf6679, #ff9800, #4caf50, #2196f3, #9c27b0, #f44336, #795548, #607d8b, #e91e63, #3f51b5, #009688, #8bc34a, #ffc107, #ff5722;
|
||||
}
|
||||
|
||||
[data-theme="dark"] body {
|
||||
background-color: var(--primary-bg);
|
||||
color: var(--primary-text);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .app-sidebar {
|
||||
background: var(--sidebar-bg);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .app-main {
|
||||
background-color: var(--primary-bg);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .calendar-day {
|
||||
background: var(--card-bg);
|
||||
border-color: var(--border-color);
|
||||
color: var(--primary-text);
|
||||
}
|
||||
|
||||
/* Rose Theme */
|
||||
[data-theme="rose"] {
|
||||
--primary-gradient: linear-gradient(180deg, #E91E63 0%, #AD1457 100%);
|
||||
--primary-bg: #fce4ec;
|
||||
--primary-text: #880e4f;
|
||||
--sidebar-bg: linear-gradient(180deg, #E91E63 0%, #AD1457 100%);
|
||||
--sidebar-text: white;
|
||||
--header-bg: linear-gradient(135deg, #E91E63 0%, #AD1457 100%);
|
||||
--header-text: white;
|
||||
--card-bg: #ffffff;
|
||||
--border-color: #f8bbd9;
|
||||
--accent-color: #E91E63;
|
||||
--calendar-bg: #ffffff;
|
||||
--calendar-border: #f8bbd9;
|
||||
--calendar-day-bg: #ffffff;
|
||||
--calendar-day-hover: #fdf2f8;
|
||||
--calendar-day-prev-next: #fef7ff;
|
||||
--calendar-day-prev-next-text: #f8bbd9;
|
||||
--calendar-today-bg: #f48fb1;
|
||||
--calendar-today-border: #AD1457;
|
||||
--calendar-today-text: #880e4f;
|
||||
--calendar-selected-bg: #fce4ec;
|
||||
--calendar-selected-border: #c2185b;
|
||||
--calendar-selected-text: #ad1457;
|
||||
--calendar-has-events-bg: #fff3e0;
|
||||
--weekday-header-bg: #fce4ec;
|
||||
--weekday-header-text: #880e4f;
|
||||
--time-label-bg: #fce4ec;
|
||||
--time-label-text: #880e4f;
|
||||
--time-label-border: #f8bbd9;
|
||||
--event-colors: #E91E63, #9C27B0, #673AB7, #3F51B5, #2196F3, #03DAC6, #009688, #4CAF50, #8BC34A, #CDDC39, #FFC107, #FF9800, #FF5722, #F44336, #795548, #607D8B;
|
||||
}
|
||||
|
||||
[data-theme="rose"] body {
|
||||
background-color: var(--primary-bg);
|
||||
color: var(--primary-text);
|
||||
}
|
||||
|
||||
[data-theme="rose"] .app-sidebar {
|
||||
background: var(--sidebar-bg);
|
||||
}
|
||||
|
||||
/* Mint Theme */
|
||||
[data-theme="mint"] {
|
||||
--primary-gradient: linear-gradient(180deg, #26A69A 0%, #00695C 100%);
|
||||
--primary-bg: #e0f2f1;
|
||||
--primary-text: #004d40;
|
||||
--sidebar-bg: linear-gradient(180deg, #26A69A 0%, #00695C 100%);
|
||||
--sidebar-text: white;
|
||||
--header-bg: linear-gradient(135deg, #26A69A 0%, #00695C 100%);
|
||||
--header-text: white;
|
||||
--card-bg: #ffffff;
|
||||
--border-color: #b2dfdb;
|
||||
--accent-color: #26A69A;
|
||||
--calendar-bg: #ffffff;
|
||||
--calendar-border: #b2dfdb;
|
||||
--calendar-day-bg: #ffffff;
|
||||
--calendar-day-hover: #f0fdfc;
|
||||
--calendar-day-prev-next: #f7ffff;
|
||||
--calendar-day-prev-next-text: #b2dfdb;
|
||||
--calendar-today-bg: #b2dfdb;
|
||||
--calendar-today-border: #00695C;
|
||||
--calendar-today-text: #004d40;
|
||||
--calendar-selected-bg: #e0f2f1;
|
||||
--calendar-selected-border: #00897b;
|
||||
--calendar-selected-text: #00695c;
|
||||
--calendar-has-events-bg: #fff3e0;
|
||||
--weekday-header-bg: #e0f2f1;
|
||||
--weekday-header-text: #004d40;
|
||||
--time-label-bg: #e0f2f1;
|
||||
--time-label-text: #004d40;
|
||||
--time-label-border: #b2dfdb;
|
||||
--event-colors: #26A69A, #009688, #4CAF50, #8BC34A, #CDDC39, #FFC107, #FF9800, #FF5722, #F44336, #E91E63, #9C27B0, #673AB7, #3F51B5, #2196F3, #795548, #607D8B;
|
||||
}
|
||||
|
||||
[data-theme="mint"] body {
|
||||
background-color: var(--primary-bg);
|
||||
color: var(--primary-text);
|
||||
}
|
||||
|
||||
[data-theme="mint"] .app-sidebar {
|
||||
background: var(--sidebar-bg);
|
||||
}
|
||||
Reference in New Issue
Block a user