Full Printing Now Available (with some bugs) #20
@@ -30,6 +30,8 @@ web-sys = { version = "0.3", features = [
|
||||
"RequestMode",
|
||||
"Response",
|
||||
"CssStyleDeclaration",
|
||||
"MediaQueryList",
|
||||
"MediaQueryListEvent",
|
||||
] }
|
||||
wasm-bindgen = "0.2"
|
||||
js-sys = "0.3"
|
||||
|
||||
@@ -6,7 +6,7 @@ dist = "dist"
|
||||
BACKEND_API_URL = "http://localhost:3000/api"
|
||||
|
||||
[watch]
|
||||
watch = ["src", "Cargo.toml", "../calendar-models/src", "styles.css", "index.html"]
|
||||
watch = ["src", "Cargo.toml", "../calendar-models/src", "styles.css", "print-preview.css", "index.html"]
|
||||
ignore = ["../backend/", "../target/"]
|
||||
|
||||
[serve]
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<base data-trunk-public-url />
|
||||
<link data-trunk rel="css" href="styles.css">
|
||||
<link data-trunk rel="css" href="print-preview.css">
|
||||
<link data-trunk rel="copy-file" href="styles/google.css">
|
||||
<link data-trunk rel="icon" href="favicon.ico">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
|
||||
1215
frontend/print-preview.css
Normal file
1215
frontend/print-preview.css
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1702,6 +1702,9 @@ pub fn App() -> Html {
|
||||
on_close={on_mobile_warning_close}
|
||||
/>
|
||||
</div>
|
||||
|
||||
// Hidden print copy that gets shown only during printing
|
||||
<div id="print-preview-copy" class="print-preview-paper" style="display: none;"></div>
|
||||
</BrowserRouter>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::components::{
|
||||
CalendarHeader, CreateEventModal, EventCreationData, EventModal, MonthView, ViewMode, WeekView,
|
||||
CalendarHeader, CreateEventModal, EventCreationData, EventModal, MonthView, PrintPreviewModal, ViewMode, WeekView,
|
||||
};
|
||||
use crate::models::ical::VEvent;
|
||||
use crate::services::{calendar_service::{UserInfo, ExternalCalendar}, CalendarService};
|
||||
@@ -389,6 +389,15 @@ pub fn Calendar(props: &CalendarProps) -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
// Handle print calendar preview
|
||||
let show_print_preview = use_state(|| false);
|
||||
let on_print = {
|
||||
let show_print_preview = show_print_preview.clone();
|
||||
Callback::from(move |_: MouseEvent| {
|
||||
show_print_preview.set(true);
|
||||
})
|
||||
};
|
||||
|
||||
// Handle drag-to-create event
|
||||
let on_create_event = {
|
||||
let show_create_modal = show_create_modal.clone();
|
||||
@@ -457,6 +466,7 @@ pub fn Calendar(props: &CalendarProps) -> Html {
|
||||
on_today={on_today}
|
||||
time_increment={Some(*time_increment)}
|
||||
on_time_increment_toggle={Some(on_time_increment_toggle)}
|
||||
on_print={Some(on_print)}
|
||||
/>
|
||||
|
||||
{
|
||||
@@ -563,6 +573,32 @@ pub fn Calendar(props: &CalendarProps) -> Html {
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
// Print preview modal
|
||||
{
|
||||
if *show_print_preview {
|
||||
html! {
|
||||
<PrintPreviewModal
|
||||
on_close={{
|
||||
let show_print_preview = show_print_preview.clone();
|
||||
Callback::from(move |_| {
|
||||
show_print_preview.set(false);
|
||||
})
|
||||
}}
|
||||
view_mode={props.view.clone()}
|
||||
current_date={*current_date}
|
||||
selected_date={*selected_date}
|
||||
events={(*events).clone()}
|
||||
user_info={props.user_info.clone()}
|
||||
external_calendars={props.external_calendars.clone()}
|
||||
time_increment={*time_increment}
|
||||
today={today}
|
||||
/>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ pub struct CalendarHeaderProps {
|
||||
pub time_increment: Option<u32>,
|
||||
#[prop_or_default]
|
||||
pub on_time_increment_toggle: Option<Callback<MouseEvent>>,
|
||||
#[prop_or_default]
|
||||
pub on_print: Option<Callback<MouseEvent>>,
|
||||
}
|
||||
|
||||
#[function_component(CalendarHeader)]
|
||||
@@ -39,6 +41,17 @@ pub fn calendar_header(props: &CalendarHeaderProps) -> Html {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
{
|
||||
if let Some(print_callback) = &props.on_print {
|
||||
html! {
|
||||
<button class="print-button" onclick={print_callback.clone()} title="Print Calendar">
|
||||
<i class="fas fa-print"></i>
|
||||
</button>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<h2 class="month-year">{title}</h2>
|
||||
<div class="header-right">
|
||||
|
||||
@@ -13,6 +13,7 @@ pub mod external_calendar_modal;
|
||||
pub mod login;
|
||||
pub mod mobile_warning_modal;
|
||||
pub mod month_view;
|
||||
pub mod print_preview_modal;
|
||||
pub mod recurring_edit_modal;
|
||||
pub mod route_handler;
|
||||
pub mod sidebar;
|
||||
@@ -32,6 +33,7 @@ pub use event_modal::EventModal;
|
||||
pub use login::Login;
|
||||
pub use mobile_warning_modal::MobileWarningModal;
|
||||
pub use month_view::MonthView;
|
||||
pub use print_preview_modal::PrintPreviewModal;
|
||||
pub use recurring_edit_modal::{RecurringEditAction, RecurringEditModal};
|
||||
pub use route_handler::RouteHandler;
|
||||
pub use sidebar::{Sidebar, Theme, ViewMode};
|
||||
|
||||
362
frontend/src/components/print_preview_modal.rs
Normal file
362
frontend/src/components/print_preview_modal.rs
Normal file
@@ -0,0 +1,362 @@
|
||||
use crate::components::{ViewMode, WeekView, MonthView};
|
||||
use crate::models::ical::VEvent;
|
||||
use crate::services::calendar_service::{UserInfo, ExternalCalendar};
|
||||
use chrono::NaiveDate;
|
||||
use std::collections::HashMap;
|
||||
use wasm_bindgen::{closure::Closure, JsCast};
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct PrintPreviewModalProps {
|
||||
pub on_close: Callback<()>,
|
||||
pub view_mode: ViewMode,
|
||||
pub current_date: NaiveDate,
|
||||
pub selected_date: NaiveDate,
|
||||
pub events: HashMap<NaiveDate, Vec<VEvent>>,
|
||||
pub user_info: Option<UserInfo>,
|
||||
pub external_calendars: Vec<ExternalCalendar>,
|
||||
pub time_increment: u32,
|
||||
pub today: NaiveDate,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn PrintPreviewModal(props: &PrintPreviewModalProps) -> Html {
|
||||
let start_hour = use_state(|| 6u32);
|
||||
let end_hour = use_state(|| 22u32);
|
||||
let zoom_level = use_state(|| 0.4f64); // Default 40% zoom
|
||||
|
||||
let close_modal = {
|
||||
let on_close = props.on_close.clone();
|
||||
Callback::from(move |_| {
|
||||
on_close.emit(());
|
||||
})
|
||||
};
|
||||
|
||||
let backdrop_click = {
|
||||
let on_close = props.on_close.clone();
|
||||
Callback::from(move |e: MouseEvent| {
|
||||
if e.target() == e.current_target() {
|
||||
on_close.emit(());
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let on_start_hour_change = {
|
||||
let start_hour = start_hour.clone();
|
||||
let end_hour = end_hour.clone();
|
||||
Callback::from(move |e: Event| {
|
||||
let target = e.target_dyn_into::<web_sys::HtmlSelectElement>();
|
||||
if let Some(select) = target {
|
||||
if let Ok(hour) = select.value().parse::<u32>() {
|
||||
if hour < *end_hour {
|
||||
start_hour.set(hour);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let on_end_hour_change = {
|
||||
let start_hour = start_hour.clone();
|
||||
let end_hour = end_hour.clone();
|
||||
Callback::from(move |e: Event| {
|
||||
let target = e.target_dyn_into::<web_sys::HtmlSelectElement>();
|
||||
if let Some(select) = target {
|
||||
if let Ok(hour) = select.value().parse::<u32>() {
|
||||
if hour > *start_hour && hour <= 24 {
|
||||
end_hour.set(hour);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
let format_hour = |hour: u32| -> String {
|
||||
if hour == 0 {
|
||||
"12 AM".to_string()
|
||||
} else if hour < 12 {
|
||||
format!("{} AM", hour)
|
||||
} else if hour == 12 {
|
||||
"12 PM".to_string()
|
||||
} else {
|
||||
format!("{} PM", hour - 12)
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate dynamic base unit for print preview
|
||||
let calculate_print_dimensions = |start_hour: u32, end_hour: u32, time_increment: u32| -> (f64, f64, f64) {
|
||||
let visible_hours = (end_hour - start_hour) as f64;
|
||||
let slots_per_hour = if time_increment == 15 { 4.0 } else { 2.0 };
|
||||
let header_height = 50.0; // Fixed week header height in print preview
|
||||
let header_border = 2.0; // Week header bottom border (2px solid)
|
||||
let container_spacing = 8.0; // Additional container spacing/margins
|
||||
let total_overhead = header_height + header_border + container_spacing;
|
||||
let available_height = 720.0 - total_overhead; // Available for time content
|
||||
let base_unit = available_height / (visible_hours * slots_per_hour);
|
||||
let pixels_per_hour = base_unit * slots_per_hour;
|
||||
|
||||
(base_unit, pixels_per_hour, available_height)
|
||||
};
|
||||
|
||||
// Calculate print dimensions for the current hour range
|
||||
let (base_unit, pixels_per_hour, _available_height) = calculate_print_dimensions(*start_hour, *end_hour, props.time_increment);
|
||||
|
||||
// Effect to update print copy whenever modal renders or content changes
|
||||
{
|
||||
let start_hour = *start_hour;
|
||||
let end_hour = *end_hour;
|
||||
let time_increment = props.time_increment;
|
||||
let original_base_unit = base_unit;
|
||||
use_effect(move || {
|
||||
if let Some(window) = web_sys::window() {
|
||||
if let Some(document) = window.document() {
|
||||
// Set CSS variables on document root
|
||||
if let Some(document_element) = document.document_element() {
|
||||
if let Some(html_element) = document_element.dyn_ref::<web_sys::HtmlElement>() {
|
||||
let style = html_element.style();
|
||||
let _ = style.set_property("--print-start-hour", &start_hour.to_string());
|
||||
let _ = style.set_property("--print-end-hour", &end_hour.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Copy content from print-preview-content to the hidden print-preview-copy div
|
||||
let copy_content = move || {
|
||||
if let Some(preview_content) = document.query_selector(".print-preview-content").ok().flatten() {
|
||||
if let Some(print_copy) = document.get_element_by_id("print-preview-copy") {
|
||||
// Clone the preview content
|
||||
if let Some(content_clone) = preview_content.clone_node_with_deep(true).ok() {
|
||||
// Clear the print copy div and add the cloned content
|
||||
print_copy.set_inner_html("");
|
||||
let _ = print_copy.append_child(&content_clone);
|
||||
|
||||
// Get the actual rendered height of the print copy div and recalculate base-unit
|
||||
if let Some(print_copy_html) = print_copy.dyn_ref::<web_sys::HtmlElement>() {
|
||||
// Temporarily make visible to measure height, then hide again
|
||||
let original_display = print_copy_html.style().get_property_value("display").unwrap_or_default();
|
||||
let _ = print_copy_html.style().set_property("display", "block");
|
||||
let _ = print_copy_html.style().set_property("visibility", "hidden");
|
||||
let _ = print_copy_html.style().set_property("position", "absolute");
|
||||
let _ = print_copy_html.style().set_property("top", "-9999px");
|
||||
|
||||
// Now measure the height
|
||||
let actual_height = print_copy_html.client_height() as f64;
|
||||
|
||||
// Restore original display
|
||||
let _ = print_copy_html.style().set_property("display", &original_display);
|
||||
let _ = print_copy_html.style().remove_property("visibility");
|
||||
let _ = print_copy_html.style().remove_property("position");
|
||||
let _ = print_copy_html.style().remove_property("top");
|
||||
|
||||
// Recalculate base-unit and pixels-per-hour based on actual height
|
||||
let visible_hours = (end_hour - start_hour) as f64;
|
||||
let slots_per_hour = if time_increment == 15 { 4.0 } else { 2.0 };
|
||||
let header_height = 50.0;
|
||||
let header_border = 2.0;
|
||||
let container_spacing = 8.0;
|
||||
let total_overhead = header_height + header_border + container_spacing;
|
||||
let available_height = actual_height - total_overhead;
|
||||
let actual_base_unit = available_height / (visible_hours * slots_per_hour);
|
||||
let actual_pixels_per_hour = actual_base_unit * slots_per_hour;
|
||||
|
||||
|
||||
// Set CSS variables with recalculated values
|
||||
let style = print_copy_html.style();
|
||||
let _ = style.set_property("--print-base-unit", &format!("{:.2}", actual_base_unit));
|
||||
let _ = style.set_property("--print-pixels-per-hour", &format!("{:.2}", actual_pixels_per_hour));
|
||||
let _ = style.set_property("--print-start-hour", &start_hour.to_string());
|
||||
let _ = style.set_property("--print-end-hour", &end_hour.to_string());
|
||||
|
||||
// Copy data attributes
|
||||
let _ = print_copy.set_attribute("data-start-hour", &start_hour.to_string());
|
||||
let _ = print_copy.set_attribute("data-end-hour", &end_hour.to_string());
|
||||
|
||||
// Recalculate event positions using the new base-unit
|
||||
let events = print_copy.query_selector_all(".week-event").unwrap();
|
||||
let scale_factor = actual_base_unit / original_base_unit;
|
||||
|
||||
for i in 0..events.length() {
|
||||
if let Some(event_element) = events.get(i) {
|
||||
if let Some(event_html) = event_element.dyn_ref::<web_sys::HtmlElement>() {
|
||||
let event_style = event_html.style();
|
||||
|
||||
// Get current positioning values and recalculate
|
||||
if let Ok(current_top) = event_style.get_property_value("top") {
|
||||
if current_top.ends_with("px") {
|
||||
if let Ok(top_px) = current_top[..current_top.len()-2].parse::<f64>() {
|
||||
let new_top = top_px * scale_factor;
|
||||
let _ = event_style.set_property("top", &format!("{:.2}px", new_top));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(current_height) = event_style.get_property_value("height") {
|
||||
if current_height.ends_with("px") {
|
||||
if let Ok(height_px) = current_height[..current_height.len()-2].parse::<f64>() {
|
||||
let new_height = height_px * scale_factor;
|
||||
let _ = event_style.set_property("height", &format!("{:.2}px", new_height));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
web_sys::console::log_1(&format!("Height: {:.2}, Original base-unit: {:.2}, New base-unit: {:.2}, Scale factor: {:.2}",
|
||||
actual_height, original_base_unit, actual_base_unit, scale_factor).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Copy content immediately
|
||||
copy_content();
|
||||
|
||||
// Also set up a small delay to catch any async rendering
|
||||
let copy_callback = Closure::wrap(Box::new(copy_content) as Box<dyn FnMut()>);
|
||||
let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
copy_callback.as_ref().unchecked_ref(),
|
||||
100
|
||||
);
|
||||
copy_callback.forget();
|
||||
}
|
||||
}
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
let on_print = {
|
||||
Callback::from(move |_: MouseEvent| {
|
||||
if let Some(window) = web_sys::window() {
|
||||
// Print copy is already updated by the use_effect, just trigger print
|
||||
let _ = window.print();
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="modal-backdrop print-preview-modal-backdrop" onclick={backdrop_click}>
|
||||
<div class="modal-content print-preview-modal">
|
||||
<div class="modal-header">
|
||||
<h3>{"Print Preview"}</h3>
|
||||
<button class="modal-close" onclick={close_modal.clone()}>{"×"}</button>
|
||||
</div>
|
||||
<div class="modal-body print-preview-body">
|
||||
<div class="print-preview-controls">
|
||||
{
|
||||
if props.view_mode == ViewMode::Week {
|
||||
html! {
|
||||
<>
|
||||
<div class="control-group">
|
||||
<label for="start-hour">{"Start Hour:"}</label>
|
||||
<select id="start-hour" onchange={on_start_hour_change}>
|
||||
{
|
||||
(0..24).map(|hour| {
|
||||
html! {
|
||||
<option value={hour.to_string()} selected={hour == *start_hour}>
|
||||
{format_hour(hour)}
|
||||
</option>
|
||||
}
|
||||
}).collect::<Html>()
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label for="end-hour">{"End Hour:"}</label>
|
||||
<select id="end-hour" onchange={on_end_hour_change}>
|
||||
{
|
||||
(1..=24).map(|hour| {
|
||||
html! {
|
||||
<option value={hour.to_string()} selected={hour == *end_hour}>
|
||||
{if hour == 24 { "12 AM".to_string() } else { format_hour(hour) }}
|
||||
</option>
|
||||
}
|
||||
}).collect::<Html>()
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="hour-range-info">
|
||||
{format!("Will print from {} to {}",
|
||||
format_hour(*start_hour),
|
||||
if *end_hour == 24 { "12 AM".to_string() } else { format_hour(*end_hour) }
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<div class="month-info">
|
||||
{"Will print entire month view"}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
<div class="zoom-display-info">
|
||||
<label>{"Zoom: "}</label>
|
||||
<span>{format!("{}%", (*zoom_level * 100.0) as i32)}</span>
|
||||
<span class="zoom-hint">{"(scroll to zoom)"}</span>
|
||||
</div>
|
||||
<div class="preview-actions">
|
||||
<button class="btn-primary" onclick={on_print}>{"Print"}</button>
|
||||
<button class="btn-secondary" onclick={close_modal}>{"Cancel"}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="print-preview-display" onwheel={{
|
||||
let zoom_level = zoom_level.clone();
|
||||
Callback::from(move |e: WheelEvent| {
|
||||
e.prevent_default(); // Prevent page scroll
|
||||
let delta_y = e.delta_y();
|
||||
let zoom_change = if delta_y < 0.0 { 1.1 } else { 1.0 / 1.1 };
|
||||
let new_zoom = (*zoom_level * zoom_change).clamp(0.2, 1.5);
|
||||
zoom_level.set(new_zoom);
|
||||
})
|
||||
}}>
|
||||
<div class="print-preview-paper"
|
||||
data-start-hour={start_hour.to_string()}
|
||||
data-end-hour={end_hour.to_string()}
|
||||
style={format!(
|
||||
"--print-start-hour: {}; --print-end-hour: {}; --print-base-unit: {:.2}; --print-pixels-per-hour: {:.2}; transform: scale({}); transform-origin: top center;",
|
||||
*start_hour, *end_hour, base_unit, pixels_per_hour, *zoom_level
|
||||
)}>
|
||||
<div class="print-preview-content">
|
||||
{
|
||||
match props.view_mode {
|
||||
ViewMode::Week => html! {
|
||||
<WeekView
|
||||
key={format!("week-preview-{}-{}", *start_hour, *end_hour)}
|
||||
current_date={props.current_date}
|
||||
today={props.today}
|
||||
events={props.events.clone()}
|
||||
on_event_click={Callback::noop()}
|
||||
user_info={props.user_info.clone()}
|
||||
external_calendars={props.external_calendars.clone()}
|
||||
time_increment={props.time_increment}
|
||||
print_mode={true}
|
||||
print_pixels_per_hour={Some(pixels_per_hour)}
|
||||
print_start_hour={Some(*start_hour)}
|
||||
/>
|
||||
},
|
||||
ViewMode::Month => html! {
|
||||
<MonthView
|
||||
key={format!("month-preview-{}-{}", *start_hour, *end_hour)}
|
||||
current_month={props.current_date}
|
||||
selected_date={Some(props.selected_date)}
|
||||
today={props.today}
|
||||
events={props.events.clone()}
|
||||
on_day_select={None::<Callback<NaiveDate>>}
|
||||
on_event_click={Callback::noop()}
|
||||
user_info={props.user_info.clone()}
|
||||
external_calendars={props.external_calendars.clone()}
|
||||
/>
|
||||
},
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -350,9 +350,9 @@ pub fn sidebar(props: &SidebarProps) -> Html {
|
||||
>
|
||||
{
|
||||
if props.refreshing_calendar_id == Some(cal.id) {
|
||||
"⏳" // Loading spinner
|
||||
html! { <i class="fas fa-spinner fa-spin"></i> }
|
||||
} else {
|
||||
"🔄" // Normal refresh icon
|
||||
html! { <i class="fas fa-sync-alt"></i> }
|
||||
}
|
||||
}
|
||||
</button>
|
||||
|
||||
@@ -42,6 +42,12 @@ pub struct WeekViewProps {
|
||||
pub context_menus_open: bool,
|
||||
#[prop_or_default]
|
||||
pub time_increment: u32,
|
||||
#[prop_or_default]
|
||||
pub print_mode: bool,
|
||||
#[prop_or_default]
|
||||
pub print_pixels_per_hour: Option<f64>,
|
||||
#[prop_or_default]
|
||||
pub print_start_hour: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
@@ -438,13 +444,13 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
// Time labels
|
||||
<div class={classes!("time-labels", if props.time_increment == 15 { Some("quarter-mode") } else { None })}>
|
||||
{
|
||||
time_labels.iter().map(|time| {
|
||||
time_labels.iter().enumerate().map(|(hour, time)| {
|
||||
let is_quarter_mode = props.time_increment == 15;
|
||||
html! {
|
||||
<div class={classes!(
|
||||
"time-label",
|
||||
if is_quarter_mode { Some("quarter-mode") } else { None }
|
||||
)}>
|
||||
)} data-hour={hour.to_string()}>
|
||||
{time}
|
||||
</div>
|
||||
}
|
||||
@@ -701,10 +707,10 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
>
|
||||
// Time slot backgrounds - 24 hour slots to represent full day
|
||||
{
|
||||
(0..24).map(|_hour| {
|
||||
(0..24).map(|hour| {
|
||||
let slots_per_hour = 60 / props.time_increment;
|
||||
html! {
|
||||
<div class={classes!("time-slot", if props.time_increment == 15 { Some("quarter-mode") } else { None })}>
|
||||
<div class={classes!("time-slot", if props.time_increment == 15 { Some("quarter-mode") } else { None })} data-hour={hour.to_string()}>
|
||||
{
|
||||
(0..slots_per_hour).map(|_slot| {
|
||||
let slot_class = if props.time_increment == 15 {
|
||||
@@ -726,7 +732,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
<div class="events-container">
|
||||
{
|
||||
day_events.iter().enumerate().filter_map(|(event_idx, event)| {
|
||||
let (start_pixels, duration_pixels, is_all_day) = calculate_event_position(event, *date, props.time_increment);
|
||||
let (start_pixels, duration_pixels, is_all_day) = calculate_event_position(event, *date, props.time_increment, props.print_pixels_per_hour, props.print_start_hour);
|
||||
|
||||
// Skip all-day events (they're rendered in the header)
|
||||
if is_all_day {
|
||||
@@ -755,6 +761,8 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
let event_for_drag = event.clone();
|
||||
let date_for_drag = *date;
|
||||
let time_increment = props.time_increment;
|
||||
let print_pixels_per_hour = props.print_pixels_per_hour;
|
||||
let print_start_hour = props.print_start_hour;
|
||||
Callback::from(move |e: MouseEvent| {
|
||||
e.stop_propagation(); // Prevent drag-to-create from starting on event clicks
|
||||
|
||||
@@ -768,7 +776,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
let click_y_relative = if click_y_relative > 0.0 { click_y_relative } else { e.offset_y() as f64 };
|
||||
|
||||
// Get event's current position in day column coordinates
|
||||
let (event_start_pixels, _, _) = calculate_event_position(&event_for_drag, date_for_drag, time_increment);
|
||||
let (event_start_pixels, _, _) = calculate_event_position(&event_for_drag, date_for_drag, time_increment, print_pixels_per_hour, print_start_hour);
|
||||
let event_start_pixels = event_start_pixels as f64;
|
||||
|
||||
// Convert click position to day column coordinates
|
||||
@@ -1054,7 +1062,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
};
|
||||
|
||||
// Calculate positions for the preview
|
||||
let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date, props.time_increment);
|
||||
let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date, props.time_increment, props.print_pixels_per_hour, props.print_start_hour);
|
||||
let original_duration = original_end.signed_duration_since(event.dtstart.with_timezone(&chrono::Local).naive_local());
|
||||
let original_end_pixels = original_start_pixels + (original_duration.num_minutes() as f32);
|
||||
|
||||
@@ -1084,7 +1092,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
let original_start = event.dtstart.with_timezone(&chrono::Local).naive_local();
|
||||
|
||||
// Calculate positions for the preview
|
||||
let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date, props.time_increment);
|
||||
let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date, props.time_increment, props.print_pixels_per_hour, props.print_start_hour);
|
||||
|
||||
let new_end_pixels = drag.current_y;
|
||||
let new_height = (new_end_pixels - original_start_pixels as f64).max(20.0);
|
||||
@@ -1218,7 +1226,7 @@ fn pixels_to_time(pixels: f64, time_increment: u32) -> NaiveTime {
|
||||
NaiveTime::from_hms_opt(hours, minutes, 0).unwrap_or(NaiveTime::from_hms_opt(0, 0, 0).unwrap())
|
||||
}
|
||||
|
||||
fn calculate_event_position(event: &VEvent, date: NaiveDate, time_increment: u32) -> (f32, f32, bool) {
|
||||
fn calculate_event_position(event: &VEvent, date: NaiveDate, time_increment: u32, print_pixels_per_hour: Option<f64>, print_start_hour: Option<u32>) -> (f32, f32, bool) {
|
||||
// Convert UTC times to local time for display
|
||||
let local_start = event.dtstart.with_timezone(&Local);
|
||||
let event_date = local_start.date_naive();
|
||||
@@ -1238,11 +1246,23 @@ fn calculate_event_position(event: &VEvent, date: NaiveDate, time_increment: u32
|
||||
return (0.0, 30.0, true); // Position at top, 30px height, is_all_day = true
|
||||
}
|
||||
|
||||
// Calculate start position in pixels from midnight
|
||||
// Calculate start position in pixels
|
||||
let start_hour = local_start.hour() as f32;
|
||||
let start_minute = local_start.minute() as f32;
|
||||
let pixels_per_hour = if time_increment == 15 { 120.0 } else { 60.0 };
|
||||
let start_pixels = (start_hour + start_minute / 60.0) * pixels_per_hour;
|
||||
let pixels_per_hour = if let Some(print_pph) = print_pixels_per_hour {
|
||||
print_pph as f32 // Use the dynamic print mode calculation
|
||||
} else {
|
||||
if time_increment == 15 { 120.0 } else { 60.0 } // Default values
|
||||
};
|
||||
|
||||
// In print mode, offset by the start hour to show relative position within visible range
|
||||
let hour_offset = if let Some(print_start) = print_start_hour {
|
||||
print_start as f32
|
||||
} else {
|
||||
0.0 // No offset for normal view (starts at midnight)
|
||||
};
|
||||
|
||||
let start_pixels = ((start_hour + start_minute / 60.0) - hour_offset) * pixels_per_hour;
|
||||
|
||||
// Calculate duration and height
|
||||
let duration_pixels = if let Some(end) = event.dtend {
|
||||
@@ -1251,19 +1271,19 @@ fn calculate_event_position(event: &VEvent, date: NaiveDate, time_increment: u32
|
||||
|
||||
// Handle events that span multiple days by capping at midnight
|
||||
if end_date > date {
|
||||
// Event continues past midnight, cap at 24:00
|
||||
let max_pixels = 24.0 * pixels_per_hour;
|
||||
max_pixels - start_pixels
|
||||
// Event continues past midnight, cap at end of visible range
|
||||
let max_hour = if let Some(_print_start) = print_start_hour { 24.0 } else { 24.0 };
|
||||
let max_pixels = (max_hour - hour_offset) * pixels_per_hour;
|
||||
(max_pixels - start_pixels).max(20.0)
|
||||
} else {
|
||||
let end_hour = local_end.hour() as f32;
|
||||
let end_minute = local_end.minute() as f32;
|
||||
let end_pixels = (end_hour + end_minute / 60.0) * pixels_per_hour;
|
||||
let end_pixels = ((end_hour + end_minute / 60.0) - hour_offset) * pixels_per_hour;
|
||||
(end_pixels - start_pixels).max(20.0) // Minimum 20px height
|
||||
}
|
||||
} else {
|
||||
pixels_per_hour // Default 1 hour if no end time
|
||||
};
|
||||
|
||||
(start_pixels, duration_pixels, false) // is_all_day = false
|
||||
}
|
||||
|
||||
@@ -1304,7 +1324,7 @@ fn calculate_event_layout(events: &[VEvent], date: NaiveDate, time_increment: u3
|
||||
return None;
|
||||
}
|
||||
|
||||
let (_, _, _) = calculate_event_position(event, date, time_increment);
|
||||
let (_, _, _) = calculate_event_position(event, date, time_increment, None, None);
|
||||
let local_start = event.dtstart.with_timezone(&Local);
|
||||
let event_date = local_start.date_naive();
|
||||
if event_date == date ||
|
||||
|
||||
@@ -18,19 +18,10 @@
|
||||
--shadow-sm: 0 1px 3px rgba(0,0,0,0.1);
|
||||
--shadow-md: 0 4px 6px rgba(0,0,0,0.1);
|
||||
--shadow-lg: 0 8px 25px rgba(0,0,0,0.15);
|
||||
--border-light: 1px solid #e9ecef;
|
||||
--border-medium: 1px solid #dee2e6;
|
||||
--transition-fast: 0.15s ease;
|
||||
--transition-normal: 0.2s ease;
|
||||
--transition-slow: 0.3s ease;
|
||||
|
||||
/* Common Glass/Glassmorphism Effects */
|
||||
--glass-bg: var(--glass-bg);
|
||||
--glass-bg-light: var(--glass-bg-light);
|
||||
--glass-bg-lighter: var(--glass-bg-lighter);
|
||||
--glass-border: 1px solid var(--glass-bg-light);
|
||||
--glass-border-light: 1px solid var(--glass-bg-lighter);
|
||||
|
||||
/* Standard Control Dimensions */
|
||||
--control-height: 40px;
|
||||
--control-padding: 0.875rem;
|
||||
@@ -39,12 +30,54 @@
|
||||
|
||||
/* Common Transition */
|
||||
--standard-transition: all 0.2s ease;
|
||||
|
||||
/* Default Light Theme Colors */
|
||||
--background-primary: #f8f9fa;
|
||||
--background-secondary: #ffffff;
|
||||
--background-tertiary: #f1f3f4;
|
||||
--text-primary: #333333;
|
||||
--text-secondary: #6c757d;
|
||||
--text-inverse: #ffffff;
|
||||
--border-primary: #e9ecef;
|
||||
--border-secondary: #dee2e6;
|
||||
--border-light: #f8f9fa;
|
||||
--error-color: #dc3545;
|
||||
--success-color: #28a745;
|
||||
--warning-color: #ffc107;
|
||||
--info-color: #17a2b8;
|
||||
|
||||
/* Modal Colors */
|
||||
--modal-background: #ffffff;
|
||||
--modal-text: #333333;
|
||||
--modal-header-background: #ffffff;
|
||||
--modal-header-border: #e5e7eb;
|
||||
|
||||
/* Button Colors */
|
||||
--button-primary-bg: #667eea;
|
||||
--button-primary-text: #ffffff;
|
||||
--button-secondary-bg: #6c757d;
|
||||
--button-secondary-text: #ffffff;
|
||||
--button-danger-bg: #dc3545;
|
||||
--button-danger-text: #ffffff;
|
||||
|
||||
/* Input Colors */
|
||||
--input-background: #ffffff;
|
||||
--input-border: #ced4da;
|
||||
--input-border-focus: #80bdff;
|
||||
--input-text: #495057;
|
||||
|
||||
/* Glass/Glassmorphism Effects */
|
||||
--glass-bg: rgba(255, 255, 255, 0.1);
|
||||
--glass-bg-light: rgba(255, 255, 255, 0.2);
|
||||
--glass-bg-lighter: rgba(255, 255, 255, 0.3);
|
||||
--glass-border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
--glass-border-light: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
color: #333;
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@@ -115,6 +148,44 @@ input, select, textarea, button {
|
||||
[data-theme="dark"] {
|
||||
--primary-color: #374151;
|
||||
--primary-gradient: linear-gradient(135deg, #374151 0%, #1f2937 100%);
|
||||
|
||||
/* Dark Theme Overrides */
|
||||
--background-primary: #111827;
|
||||
--background-secondary: #1f2937;
|
||||
--background-tertiary: #374151;
|
||||
--text-primary: #f9fafb;
|
||||
--text-secondary: #d1d5db;
|
||||
--text-inverse: #111827;
|
||||
--border-primary: #374151;
|
||||
--border-secondary: #4b5563;
|
||||
--border-light: #6b7280;
|
||||
|
||||
/* Modal Colors - Dark */
|
||||
--modal-background: #1f2937;
|
||||
--modal-text: #f3f4f6;
|
||||
--modal-header-background: #1f2937;
|
||||
--modal-header-border: #374151;
|
||||
|
||||
/* Button Colors - Dark */
|
||||
--button-primary-bg: #4f46e5;
|
||||
--button-primary-text: #ffffff;
|
||||
--button-secondary-bg: #4b5563;
|
||||
--button-secondary-text: #ffffff;
|
||||
--button-danger-bg: #dc2626;
|
||||
--button-danger-text: #ffffff;
|
||||
|
||||
/* Input Colors - Dark */
|
||||
--input-background: #374151;
|
||||
--input-border: #4b5563;
|
||||
--input-border-focus: #6366f1;
|
||||
--input-text: #f9fafb;
|
||||
|
||||
/* Glass Effects - Dark */
|
||||
--glass-bg: rgba(0, 0, 0, 0.2);
|
||||
--glass-bg-light: rgba(0, 0, 0, 0.3);
|
||||
--glass-bg-lighter: rgba(0, 0, 0, 0.4);
|
||||
--glass-border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
--glass-border-light: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
[data-theme="rose"] {
|
||||
@@ -133,8 +204,8 @@ input, select, textarea, button {
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
color: #333;
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@@ -637,6 +708,25 @@ body {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.print-button {
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.print-button:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
@@ -3684,10 +3774,37 @@ body {
|
||||
}
|
||||
|
||||
.external-calendar-info input[type="checkbox"] {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: rgba(255, 255, 255, 0.8);
|
||||
background: transparent;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.external-calendar-info input[type="checkbox"]:hover {
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.external-calendar-info input[type="checkbox"]:checked {
|
||||
border-color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.external-calendar-info input[type="checkbox"]:checked::after {
|
||||
content: "✓";
|
||||
position: absolute;
|
||||
top: 45%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.external-calendar-color {
|
||||
@@ -3762,10 +3879,37 @@ body {
|
||||
}
|
||||
|
||||
.calendar-info input[type="checkbox"] {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: rgba(255, 255, 255, 0.8);
|
||||
background: transparent;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.calendar-info input[type="checkbox"]:hover {
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.calendar-info input[type="checkbox"]:checked {
|
||||
border-color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.calendar-info input[type="checkbox"]:checked::after {
|
||||
content: "✓";
|
||||
position: absolute;
|
||||
top: 45%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Create External Calendar Button */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,51 +0,0 @@
|
||||
/* Base Styles - Always Loaded */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.app {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.login-layout {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Base Layout */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
margin-left: 280px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Basic Form Elements */
|
||||
input, select, textarea, button {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.loading {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #28a745;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user