Compare commits
10 Commits
4cbc495c48
...
print-prev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca1ca0c3b1 | ||
|
|
64dbf65beb | ||
|
|
96585440d1 | ||
|
|
a297d38276 | ||
|
|
4fdaa9931d | ||
|
|
c6c7b38bef | ||
|
|
78db2cc00f | ||
|
|
73d191c5ca | ||
| d930468748 | |||
|
|
91be4436a9 |
@@ -78,17 +78,75 @@ pub async fn fetch_external_calendar_events(
|
||||
|
||||
// If not fetched from cache, get from external URL
|
||||
if !fetched_from_cache {
|
||||
let client = Client::new();
|
||||
let response = client
|
||||
.get(&calendar.url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ApiError::Internal(format!("Failed to fetch calendar: {}", e)))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(ApiError::Internal(format!("Calendar server returned: {}", response.status())));
|
||||
// Log the URL being fetched for debugging
|
||||
println!("🌍 Fetching calendar URL: {}", calendar.url);
|
||||
|
||||
let user_agents = vec![
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||
"Mozilla/5.0 (compatible; Runway Calendar/1.0)",
|
||||
"Outlook-iOS/709.2226530.prod.iphone (3.24.1)"
|
||||
];
|
||||
|
||||
let mut response = None;
|
||||
let mut last_error = None;
|
||||
|
||||
// Try different user agents
|
||||
for (i, ua) in user_agents.iter().enumerate() {
|
||||
println!("🔄 Attempt {} with User-Agent: {}", i + 1, ua);
|
||||
|
||||
let client = Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::limited(10))
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.user_agent(*ua)
|
||||
.build()
|
||||
.map_err(|e| ApiError::Internal(format!("Failed to create HTTP client: {}", e)))?;
|
||||
|
||||
let result = client
|
||||
.get(&calendar.url)
|
||||
.header("Accept", "text/calendar,application/calendar+xml,text/plain,*/*")
|
||||
.header("Accept-Charset", "utf-8")
|
||||
.header("Cache-Control", "no-cache")
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(resp) => {
|
||||
let status = resp.status();
|
||||
println!("📡 Response status: {}", status);
|
||||
if status.is_success() {
|
||||
response = Some(resp);
|
||||
break;
|
||||
} else if status == 400 {
|
||||
// Check if this is an Outlook auth error
|
||||
let error_body = resp.text().await.unwrap_or_default();
|
||||
if error_body.contains("OwaPage") || error_body.contains("Outlook") {
|
||||
println!("🚫 Outlook authentication error detected, trying next approach...");
|
||||
last_error = Some(format!("Outlook auth error: {}", error_body.chars().take(100).collect::<String>()));
|
||||
continue;
|
||||
}
|
||||
last_error = Some(format!("Bad Request: {}", error_body.chars().take(100).collect::<String>()));
|
||||
} else {
|
||||
last_error = Some(format!("HTTP {}", status));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ Request failed: {}", e);
|
||||
last_error = Some(format!("Request error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let response = response.ok_or_else(|| {
|
||||
ApiError::Internal(format!(
|
||||
"Failed to fetch calendar after trying {} different approaches. Last error: {}",
|
||||
user_agents.len(),
|
||||
last_error.unwrap_or("Unknown error".to_string())
|
||||
))
|
||||
})?;
|
||||
|
||||
// Response is guaranteed to be successful here since we checked in the loop
|
||||
println!("✅ Successfully fetched calendar data");
|
||||
|
||||
ics_content = response
|
||||
.text()
|
||||
.await
|
||||
|
||||
@@ -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", "print.css", "print-preview.css", "index.html"]
|
||||
watch = ["src", "Cargo.toml", "../calendar-models/src", "styles.css", "print-preview.css", "index.html"]
|
||||
ignore = ["../backend/", "../target/"]
|
||||
|
||||
[serve]
|
||||
|
||||
@@ -6,10 +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.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>
|
||||
|
||||
@@ -5,12 +5,51 @@
|
||||
|
||||
.print-preview-modal {
|
||||
width: 90vw;
|
||||
max-width: 1200px;
|
||||
height: 80vh;
|
||||
max-height: none;
|
||||
max-width: 1600px;
|
||||
height: auto; /* Let modal size itself based on content */
|
||||
max-height: 90vh; /* But don't exceed viewport */
|
||||
margin: 5vh auto;
|
||||
}
|
||||
|
||||
.print-preview-modal .modal-content {
|
||||
background: var(--modal-background) !important;
|
||||
color: var(--modal-text) !important;
|
||||
}
|
||||
|
||||
.print-preview-modal .modal-header {
|
||||
background: var(--modal-header-background) !important;
|
||||
border-bottom-color: var(--modal-header-border) !important;
|
||||
}
|
||||
|
||||
.print-preview-modal .modal-header h3 {
|
||||
color: var(--modal-text) !important;
|
||||
}
|
||||
|
||||
.print-preview-modal .modal-body {
|
||||
background: var(--modal-background) !important;
|
||||
}
|
||||
|
||||
.print-preview-modal .modal-close {
|
||||
color: var(--modal-text) !important;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
font-size: 1.5rem !important;
|
||||
cursor: pointer !important;
|
||||
padding: 0.25rem !important;
|
||||
}
|
||||
|
||||
.print-preview-modal .modal-close:hover {
|
||||
color: #666 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .print-preview-modal .modal-close {
|
||||
color: #f3f4f6 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .print-preview-modal .modal-close:hover {
|
||||
color: #d1d5db !important;
|
||||
}
|
||||
|
||||
.print-preview-body {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
@@ -108,12 +147,12 @@
|
||||
}
|
||||
|
||||
.preview-actions .btn-primary {
|
||||
background: #3B82F6;
|
||||
background: var(--primary-color, #3B82F6);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.preview-actions .btn-primary:hover {
|
||||
background: #2563EB;
|
||||
background: var(--primary-color-hover, #2563EB);
|
||||
}
|
||||
|
||||
.preview-actions .btn-secondary {
|
||||
@@ -131,8 +170,8 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
overflow: hidden;
|
||||
background: #f5f5f5;
|
||||
overflow: auto; /* Allow scrolling if paper is larger than container */
|
||||
background: var(--background-tertiary, #f5f5f5);
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
cursor: grab;
|
||||
@@ -147,15 +186,17 @@
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 2px;
|
||||
/* transform: scale() is now applied dynamically via inline style */
|
||||
width: 11in; /* Actual letter width */
|
||||
min-height: 8.5in; /* Letter height */
|
||||
width: 1056px; /* 11in at 96 DPI - LANDSCAPE width */
|
||||
height: 816px; /* 8.5in at 96 DPI - LANDSCAPE height */
|
||||
padding: 48px; /* 0.5 inches at 96 DPI */
|
||||
box-sizing: border-box; /* Include padding in dimensions */
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
flex-shrink: 0; /* Don't shrink in flex container */
|
||||
}
|
||||
|
||||
.print-preview-content {
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -176,8 +217,8 @@
|
||||
.print-preview-paper .week-day-header.today,
|
||||
.print-preview-paper .week-day-column.today {
|
||||
background-color: transparent !important;
|
||||
border-color: #ddd !important;
|
||||
color: #333 !important;
|
||||
border-color: var(--border-color, #ddd) !important;
|
||||
color: var(--text-color, #333) !important;
|
||||
font-weight: normal !important;
|
||||
}
|
||||
|
||||
@@ -313,28 +354,52 @@
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Scale time slots appropriately in preview */
|
||||
/* Dynamic time slot heights based on calculated base unit */
|
||||
.print-preview-paper .time-slot {
|
||||
height: 60px !important; /* Default for 30-minute mode: 2 × 30px = 60px */
|
||||
min-height: 60px !important;
|
||||
max-height: 60px !important;
|
||||
border-bottom: 1px solid #ddd !important;
|
||||
height: calc(var(--print-pixels-per-hour) * 1px) !important;
|
||||
min-height: calc(var(--print-pixels-per-hour) * 1px) !important;
|
||||
max-height: calc(var(--print-pixels-per-hour) * 1px) !important;
|
||||
border-bottom: 1px solid var(--border-color, #ddd) !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* 15-minute mode (quarter-mode) gets more height */
|
||||
.print-preview-paper .time-slot.quarter-mode {
|
||||
height: 120px !important; /* 15-minute mode: 4 slots × 30px = 120px */
|
||||
min-height: 120px !important;
|
||||
max-height: 120px !important;
|
||||
/* Dynamic time label heights - should match time slots */
|
||||
.print-preview-paper .time-label {
|
||||
height: calc(var(--print-pixels-per-hour) * 1px) !important;
|
||||
min-height: calc(var(--print-pixels-per-hour) * 1px) !important;
|
||||
max-height: calc(var(--print-pixels-per-hour) * 1px) !important;
|
||||
}
|
||||
|
||||
/* Sub-hour time slot styling */
|
||||
/* Fixed week header height for print - smaller to maximize content */
|
||||
.print-preview-paper .week-day-header {
|
||||
height: 50px !important;
|
||||
min-height: 50px !important;
|
||||
max-height: 50px !important;
|
||||
padding: 0.25rem !important; /* Reduce padding to fit content */
|
||||
}
|
||||
|
||||
.print-preview-paper .weekday-name {
|
||||
font-size: 0.75rem !important; /* Slightly smaller font */
|
||||
margin-bottom: 0.125rem !important;
|
||||
}
|
||||
|
||||
.print-preview-paper .week-day-header .day-number {
|
||||
font-size: 1.25rem !important; /* Slightly smaller day number */
|
||||
}
|
||||
|
||||
/* 15-minute and 30-minute modes use the same calculated height */
|
||||
.print-preview-paper .time-slot.quarter-mode {
|
||||
height: calc(var(--print-pixels-per-hour) * 1px) !important;
|
||||
min-height: calc(var(--print-pixels-per-hour) * 1px) !important;
|
||||
max-height: calc(var(--print-pixels-per-hour) * 1px) !important;
|
||||
}
|
||||
|
||||
/* Sub-hour time slot styling - use dynamic base unit */
|
||||
.print-preview-paper .time-slot-half {
|
||||
height: 30px !important;
|
||||
min-height: 30px !important;
|
||||
max-height: 30px !important;
|
||||
border-bottom: 1px dotted #ddd !important;
|
||||
height: calc(var(--print-base-unit) * 1px) !important;
|
||||
min-height: calc(var(--print-base-unit) * 1px) !important;
|
||||
max-height: calc(var(--print-base-unit) * 1px) !important;
|
||||
border-bottom: 1px dotted var(--border-color-light, #ddd) !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
@@ -343,10 +408,10 @@
|
||||
}
|
||||
|
||||
.print-preview-paper .time-slot-quarter {
|
||||
height: 30px !important;
|
||||
min-height: 30px !important;
|
||||
max-height: 30px !important;
|
||||
border-bottom: 1px dotted #eee !important;
|
||||
height: calc(var(--print-base-unit) * 1px) !important;
|
||||
min-height: calc(var(--print-base-unit) * 1px) !important;
|
||||
max-height: calc(var(--print-base-unit) * 1px) !important;
|
||||
border-bottom: 1px dotted var(--border-color-lighter, #eee) !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
@@ -354,23 +419,7 @@
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
/* Debug: Test if data attributes are working at all */
|
||||
.print-preview-paper .time-slot[data-hour] {
|
||||
border-left: 3px solid blue !important;
|
||||
}
|
||||
|
||||
.print-preview-paper .time-label[data-hour] {
|
||||
background-color: yellow !important;
|
||||
}
|
||||
|
||||
/* Test specific hour targeting */
|
||||
.print-preview-paper .time-slot[data-hour="0"] {
|
||||
border-left: 3px solid red !important;
|
||||
}
|
||||
|
||||
.print-preview-paper .time-label[data-hour="0"] {
|
||||
background-color: red !important;
|
||||
}
|
||||
/* Debug styles removed */
|
||||
|
||||
/* Use data attributes for precise hour targeting */
|
||||
/* Hide hours before start hour - both time slots AND time labels */
|
||||
@@ -1011,60 +1060,6 @@
|
||||
/* End hour 24 (midnight next day) hides no hours since it includes the full day */
|
||||
.print-preview-paper[data-end-hour="24"] { /* Shows all hours - no additional hiding needed */ }
|
||||
|
||||
/* Event positioning adjustments - shift events up when start hours are hidden */
|
||||
/* Each hidden hour = 60px in 30-min mode, 120px in 15-min mode */
|
||||
|
||||
/* Reposition events to align with visible time labels */
|
||||
/* 30-minute mode: Each hidden hour = 60px to shift up */
|
||||
.print-preview-paper[data-start-hour="1"] .week-event { transform: translateY(-60px); }
|
||||
.print-preview-paper[data-start-hour="2"] .week-event { transform: translateY(-120px); }
|
||||
.print-preview-paper[data-start-hour="3"] .week-event { transform: translateY(-180px); }
|
||||
.print-preview-paper[data-start-hour="4"] .week-event { transform: translateY(-240px); }
|
||||
.print-preview-paper[data-start-hour="5"] .week-event { transform: translateY(-300px); }
|
||||
.print-preview-paper[data-start-hour="6"] .week-event { transform: translateY(-360px); }
|
||||
.print-preview-paper[data-start-hour="7"] .week-event { transform: translateY(-420px); }
|
||||
.print-preview-paper[data-start-hour="8"] .week-event { transform: translateY(-480px); }
|
||||
.print-preview-paper[data-start-hour="9"] .week-event { transform: translateY(-540px); }
|
||||
.print-preview-paper[data-start-hour="10"] .week-event { transform: translateY(-600px); }
|
||||
.print-preview-paper[data-start-hour="11"] .week-event { transform: translateY(-660px); }
|
||||
.print-preview-paper[data-start-hour="12"] .week-event { transform: translateY(-720px); }
|
||||
.print-preview-paper[data-start-hour="13"] .week-event { transform: translateY(-780px); }
|
||||
.print-preview-paper[data-start-hour="14"] .week-event { transform: translateY(-840px); }
|
||||
.print-preview-paper[data-start-hour="15"] .week-event { transform: translateY(-900px); }
|
||||
.print-preview-paper[data-start-hour="16"] .week-event { transform: translateY(-960px); }
|
||||
.print-preview-paper[data-start-hour="17"] .week-event { transform: translateY(-1020px); }
|
||||
.print-preview-paper[data-start-hour="18"] .week-event { transform: translateY(-1080px); }
|
||||
.print-preview-paper[data-start-hour="19"] .week-event { transform: translateY(-1140px); }
|
||||
.print-preview-paper[data-start-hour="20"] .week-event { transform: translateY(-1200px); }
|
||||
.print-preview-paper[data-start-hour="21"] .week-event { transform: translateY(-1260px); }
|
||||
.print-preview-paper[data-start-hour="22"] .week-event { transform: translateY(-1320px); }
|
||||
.print-preview-paper[data-start-hour="23"] .week-event { transform: translateY(-1380px); }
|
||||
|
||||
/* 15-minute mode: Each hidden hour = 120px to shift up */
|
||||
.print-preview-paper[data-start-hour="1"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-120px); }
|
||||
.print-preview-paper[data-start-hour="2"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-240px); }
|
||||
.print-preview-paper[data-start-hour="3"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-360px); }
|
||||
.print-preview-paper[data-start-hour="4"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-480px); }
|
||||
.print-preview-paper[data-start-hour="5"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-600px); }
|
||||
.print-preview-paper[data-start-hour="6"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-720px); }
|
||||
.print-preview-paper[data-start-hour="7"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-840px); }
|
||||
.print-preview-paper[data-start-hour="8"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-960px); }
|
||||
.print-preview-paper[data-start-hour="9"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-1080px); }
|
||||
.print-preview-paper[data-start-hour="10"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-1200px); }
|
||||
.print-preview-paper[data-start-hour="11"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-1320px); }
|
||||
.print-preview-paper[data-start-hour="12"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-1440px); }
|
||||
.print-preview-paper[data-start-hour="13"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-1560px); }
|
||||
.print-preview-paper[data-start-hour="14"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-1680px); }
|
||||
.print-preview-paper[data-start-hour="15"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-1800px); }
|
||||
.print-preview-paper[data-start-hour="16"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-1920px); }
|
||||
.print-preview-paper[data-start-hour="17"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-2040px); }
|
||||
.print-preview-paper[data-start-hour="18"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-2160px); }
|
||||
.print-preview-paper[data-start-hour="19"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-2280px); }
|
||||
.print-preview-paper[data-start-hour="20"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-2400px); }
|
||||
.print-preview-paper[data-start-hour="21"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-2520px); }
|
||||
.print-preview-paper[data-start-hour="22"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-2640px); }
|
||||
.print-preview-paper[data-start-hour="23"]:has(.time-slot.quarter-mode) .week-event { transform: translateY(-2760px); }
|
||||
|
||||
/* Hide events that are completely outside the visible time range */
|
||||
/* Clip events outside the visible hour range using overflow: hidden */
|
||||
|
||||
@@ -1087,7 +1082,6 @@
|
||||
.print-preview-paper[data-start-hour][data-end-hour] .time-grid,
|
||||
.print-preview-paper[data-start-hour][data-end-hour] .week-day-column {
|
||||
min-height: 400px !important;
|
||||
border: 2px solid red !important; /* Debug border to see if this rule applies */
|
||||
}
|
||||
|
||||
/* Height adjustments based on visible hours = end_hour - start_hour */
|
||||
@@ -1171,4 +1165,51 @@
|
||||
.print-preview-paper[data-start-hour="8"][data-end-hour="20"]:has(.time-slot.quarter-mode) .time-labels,
|
||||
.print-preview-paper[data-start-hour="8"][data-end-hour="20"]:has(.time-slot.quarter-mode) .week-days-grid,
|
||||
.print-preview-paper[data-start-hour="7"][data-end-hour="19"]:has(.time-slot.quarter-mode) .time-labels,
|
||||
.print-preview-paper[data-start-hour="7"][data-end-hour="19"]:has(.time-slot.quarter-mode) .week-days-grid { min-height: 1530px !important; }
|
||||
.print-preview-paper[data-start-hour="7"][data-end-hour="19"]:has(.time-slot.quarter-mode) .week-days-grid { min-height: 1530px !important; }
|
||||
|
||||
/* Print Page Setup - Force landscape orientation and fit to page */
|
||||
@page {
|
||||
size: letter landscape;
|
||||
margin: 0.25in;
|
||||
}
|
||||
|
||||
/* Print Media Rules - Show only the print-preview-copy when printing */
|
||||
@media print {
|
||||
html {
|
||||
print-color-adjust: exact; /* Preserve colors when printing */
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
|
||||
/* Hide all top-level app content */
|
||||
.app {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Show only the print copy div */
|
||||
#print-preview-copy {
|
||||
display: block !important;
|
||||
position: absolute !important;
|
||||
left: 0 !important;
|
||||
top: 0 !important;
|
||||
/* width: 960px !important; */
|
||||
/* height: 720px !important; */
|
||||
aspect-ratio: 1.3125 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
box-sizing: border-box !important;
|
||||
/* Properties to help browsers scale to fit page */
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
object-fit: contain;
|
||||
page-break-inside: avoid;
|
||||
orphans: 1;
|
||||
widows: 1;
|
||||
}
|
||||
|
||||
/* Ensure print content uses the full page */
|
||||
body {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
background: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,302 +0,0 @@
|
||||
/* Print-specific styles for calendar printing */
|
||||
@media print {
|
||||
/* Hide UI elements that shouldn't be printed */
|
||||
.app-sidebar,
|
||||
.current-time-indicator-container,
|
||||
.current-time-indicator,
|
||||
.print-button,
|
||||
.nav-button,
|
||||
.today-button,
|
||||
.time-increment-button,
|
||||
.modal-backdrop,
|
||||
.create-event-modal,
|
||||
.event-modal,
|
||||
.calendar-management-modal,
|
||||
.context-menu {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Remove today highlighting from calendar elements */
|
||||
.calendar-day.today,
|
||||
.week-day-header.today,
|
||||
.week-day-column.today {
|
||||
background-color: transparent !important;
|
||||
border-color: var(--border-color) !important;
|
||||
color: var(--text-color) !important;
|
||||
}
|
||||
|
||||
/* Remove today-specific styling from day numbers */
|
||||
.calendar-day.today .day-number {
|
||||
background-color: transparent !important;
|
||||
color: var(--text-color) !important;
|
||||
font-weight: normal !important;
|
||||
}
|
||||
|
||||
/* Remove today indicator from week day headers */
|
||||
.week-day-header.today .weekday-name {
|
||||
color: var(--text-color) !important;
|
||||
font-weight: normal !important;
|
||||
}
|
||||
|
||||
/* Page setup */
|
||||
@page {
|
||||
size: letter landscape;
|
||||
margin: 0.5in;
|
||||
}
|
||||
|
||||
/* Make app and main container fill full page width */
|
||||
.app,
|
||||
.app-main,
|
||||
.calendar-view {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Remove any flexbox constraints that might limit width */
|
||||
.app {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
margin-left: 0 !important; /* Remove sidebar margin */
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
/* Ensure calendar uses full available width */
|
||||
.calendar-container,
|
||||
.calendar {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Adjust calendar header for printing */
|
||||
.calendar-header {
|
||||
margin-bottom: 0.5rem !important;
|
||||
padding-bottom: 0.5rem !important;
|
||||
border-bottom: 1px solid var(--border-color) !important;
|
||||
}
|
||||
|
||||
/* Ensure text is readable in print */
|
||||
body, html {
|
||||
color: black !important;
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
/* Make sure event text is readable */
|
||||
.week-event,
|
||||
.calendar-event,
|
||||
.month-event {
|
||||
border: 1px solid #333 !important;
|
||||
color: black !important;
|
||||
font-size: 0.8rem !important;
|
||||
line-height: 1.2 !important;
|
||||
}
|
||||
|
||||
/* Ensure month view events are visible */
|
||||
.calendar-day .event-list .event-item {
|
||||
background-color: #f5f5f5 !important;
|
||||
border: 1px solid #333 !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
/* Week view specific adjustments - force full day view */
|
||||
.week-view-container {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.week-day-column {
|
||||
border-right: 1px solid #333 !important;
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.time-column {
|
||||
border-right: 2px solid #333 !important;
|
||||
width: 60px !important;
|
||||
min-width: 60px !important;
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Default time slot sizes - will be adjusted by hour range */
|
||||
.time-slot {
|
||||
height: 20px !important;
|
||||
min-height: 20px !important;
|
||||
max-height: 20px !important;
|
||||
border-bottom: 1px solid #ddd !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Time slot quarters for 15-minute mode */
|
||||
.time-slot.quarter-mode .time-slot-quarter {
|
||||
height: 5px !important;
|
||||
min-height: 5px !important;
|
||||
max-height: 5px !important;
|
||||
border-bottom: 1px solid #eee !important;
|
||||
}
|
||||
|
||||
/* Time slot halves for 30-minute mode */
|
||||
.time-slot .time-slot-half {
|
||||
height: 10px !important;
|
||||
min-height: 10px !important;
|
||||
max-height: 10px !important;
|
||||
border-bottom: 1px solid #eee !important;
|
||||
}
|
||||
|
||||
/* Make hour boundaries more visible */
|
||||
.time-slot:nth-child(4n) {
|
||||
border-bottom: 1px solid #333 !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
|
||||
/* Dynamic hour range hiding for print mode */
|
||||
body[data-print-mode="true"][data-print-start-hour="1"] .week-view .time-slot:nth-child(1) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="2"] .week-view .time-slot:nth-child(-n+2) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="3"] .week-view .time-slot:nth-child(-n+3) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="4"] .week-view .time-slot:nth-child(-n+4) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="5"] .week-view .time-slot:nth-child(-n+5) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="6"] .week-view .time-slot:nth-child(-n+6) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="7"] .week-view .time-slot:nth-child(-n+7) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="8"] .week-view .time-slot:nth-child(-n+8) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="9"] .week-view .time-slot:nth-child(-n+9) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="10"] .week-view .time-slot:nth-child(-n+10) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="11"] .week-view .time-slot:nth-child(-n+11) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="12"] .week-view .time-slot:nth-child(-n+12) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="13"] .week-view .time-slot:nth-child(-n+13) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="14"] .week-view .time-slot:nth-child(-n+14) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="15"] .week-view .time-slot:nth-child(-n+15) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="16"] .week-view .time-slot:nth-child(-n+16) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="17"] .week-view .time-slot:nth-child(-n+17) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="18"] .week-view .time-slot:nth-child(-n+18) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="19"] .week-view .time-slot:nth-child(-n+19) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="20"] .week-view .time-slot:nth-child(-n+20) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="21"] .week-view .time-slot:nth-child(-n+21) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="22"] .week-view .time-slot:nth-child(-n+22) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-start-hour="23"] .week-view .time-slot:nth-child(-n+23) { display: none !important; }
|
||||
|
||||
/* Hide hours after end-hour */
|
||||
body[data-print-mode="true"][data-print-end-hour="1"] .week-view .time-slot:nth-child(n+2) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="2"] .week-view .time-slot:nth-child(n+3) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="3"] .week-view .time-slot:nth-child(n+4) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="4"] .week-view .time-slot:nth-child(n+5) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="5"] .week-view .time-slot:nth-child(n+6) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="6"] .week-view .time-slot:nth-child(n+7) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="7"] .week-view .time-slot:nth-child(n+8) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="8"] .week-view .time-slot:nth-child(n+9) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="9"] .week-view .time-slot:nth-child(n+10) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="10"] .week-view .time-slot:nth-child(n+11) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="11"] .week-view .time-slot:nth-child(n+12) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="12"] .week-view .time-slot:nth-child(n+13) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="13"] .week-view .time-slot:nth-child(n+14) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="14"] .week-view .time-slot:nth-child(n+15) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="15"] .week-view .time-slot:nth-child(n+16) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="16"] .week-view .time-slot:nth-child(n+17) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="17"] .week-view .time-slot:nth-child(n+18) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="18"] .week-view .time-slot:nth-child(n+19) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="19"] .week-view .time-slot:nth-child(n+20) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="20"] .week-view .time-slot:nth-child(n+21) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="21"] .week-view .time-slot:nth-child(n+22) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="22"] .week-view .time-slot:nth-child(n+23) { display: none !important; }
|
||||
body[data-print-mode="true"][data-print-end-hour="23"] .week-view .time-slot:nth-child(n+24) { display: none !important; }
|
||||
|
||||
/* Force the week grid to show full height */
|
||||
.week-grid,
|
||||
.week-content,
|
||||
.time-grid {
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Ensure all hours are visible by removing any height constraints */
|
||||
.week-view-container,
|
||||
.week-view-container > div {
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Month view specific adjustments */
|
||||
.calendar-grid {
|
||||
border: 1px solid #333 !important;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
border: 1px solid #333 !important;
|
||||
min-height: 80px !important;
|
||||
}
|
||||
|
||||
/* Ensure grid lines are visible - handled above in main time-slot rules */
|
||||
|
||||
/* Make sure header text is visible */
|
||||
.calendar-header h2,
|
||||
.calendar-header .month-year {
|
||||
color: black !important;
|
||||
font-size: 1.5rem !important;
|
||||
}
|
||||
|
||||
.week-day-header .weekday-name,
|
||||
.week-day-header .date-number {
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
/* Time labels in week view */
|
||||
.time-label {
|
||||
color: #666 !important;
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
|
||||
/* Remove any shadows or fancy effects */
|
||||
* {
|
||||
box-shadow: none !important;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Ensure proper spacing */
|
||||
.calendar-day .day-number {
|
||||
margin-bottom: 0.25rem !important;
|
||||
}
|
||||
|
||||
/* Make sure events don't overlap text in month view */
|
||||
.calendar-day .event-list {
|
||||
margin-top: 0.25rem !important;
|
||||
}
|
||||
|
||||
/* Force page break before calendar if needed */
|
||||
.calendar {
|
||||
page-break-inside: avoid !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Additional print button styling for screen display */
|
||||
.print-button {
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.print-button:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.print-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ pub fn calendar_header(props: &CalendarHeaderProps) -> 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 {
|
||||
|
||||
@@ -30,8 +30,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
|
||||
let is_creating = use_state(|| false);
|
||||
|
||||
// External Calendar state
|
||||
let external_name_ref = use_node_ref();
|
||||
let external_url_ref = use_node_ref();
|
||||
let external_name = use_state(|| String::new());
|
||||
let external_url = use_state(|| String::new());
|
||||
let external_selected_color = use_state(|| Some("#4285f4".to_string()));
|
||||
let external_is_loading = use_state(|| false);
|
||||
let external_error_message = use_state(|| None::<String>);
|
||||
@@ -43,6 +43,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
|
||||
let selected_color = selected_color.clone();
|
||||
let create_error_message = create_error_message.clone();
|
||||
let is_creating = is_creating.clone();
|
||||
let external_name = external_name.clone();
|
||||
let external_url = external_url.clone();
|
||||
let external_is_loading = external_is_loading.clone();
|
||||
let external_error_message = external_error_message.clone();
|
||||
let external_selected_color = external_selected_color.clone();
|
||||
@@ -56,6 +58,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
|
||||
selected_color.set(None);
|
||||
create_error_message.set(None);
|
||||
is_creating.set(false);
|
||||
external_name.set(String::new());
|
||||
external_url.set(String::new());
|
||||
external_is_loading.set(false);
|
||||
external_error_message.set(None);
|
||||
external_selected_color.set(Some("#4285f4".to_string()));
|
||||
@@ -146,8 +150,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
|
||||
|
||||
// External Calendar handlers
|
||||
let on_external_submit = {
|
||||
let external_name_ref = external_name_ref.clone();
|
||||
let external_url_ref = external_url_ref.clone();
|
||||
let external_name = external_name.clone();
|
||||
let external_url = external_url.clone();
|
||||
let external_selected_color = external_selected_color.clone();
|
||||
let external_is_loading = external_is_loading.clone();
|
||||
let external_error_message = external_error_message.clone();
|
||||
@@ -157,24 +161,28 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
|
||||
Callback::from(move |e: SubmitEvent| {
|
||||
e.prevent_default();
|
||||
|
||||
let name = external_name_ref
|
||||
.cast::<HtmlInputElement>()
|
||||
.map(|input| input.value())
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let url = external_url_ref
|
||||
.cast::<HtmlInputElement>()
|
||||
.map(|input| input.value())
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let name = (*external_name).trim().to_string();
|
||||
let url = (*external_url).trim().to_string();
|
||||
let color = (*external_selected_color).clone().unwrap_or_else(|| "#4285f4".to_string());
|
||||
|
||||
if name.is_empty() || url.is_empty() {
|
||||
external_error_message.set(Some("Name and URL are required".to_string()));
|
||||
// Debug logging to understand the issue
|
||||
web_sys::console::log_1(&format!("External calendar form submission - Name: '{}', URL: '{}'", name, url).into());
|
||||
|
||||
if name.is_empty() {
|
||||
external_error_message.set(Some("Calendar name is required".to_string()));
|
||||
web_sys::console::log_1(&"Validation failed: empty name".into());
|
||||
return;
|
||||
}
|
||||
|
||||
if url.is_empty() {
|
||||
external_error_message.set(Some("Calendar URL is required".to_string()));
|
||||
web_sys::console::log_1(&"Validation failed: empty URL".into());
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic URL validation
|
||||
if !url.starts_with("http://") && !url.starts_with("https://") {
|
||||
external_error_message.set(Some("Please enter a valid HTTP or HTTPS URL".to_string()));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -204,6 +212,25 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
// External input change handlers
|
||||
let on_external_name_change = {
|
||||
let external_name = external_name.clone();
|
||||
Callback::from(move |e: Event| {
|
||||
if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
|
||||
external_name.set(input.value());
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let on_external_url_change = {
|
||||
let external_url = external_url.clone();
|
||||
Callback::from(move |e: Event| {
|
||||
if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
|
||||
external_url.set(input.value());
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
if !props.is_open {
|
||||
return html! {};
|
||||
}
|
||||
@@ -333,7 +360,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
|
||||
<input
|
||||
type="text"
|
||||
id="external-name"
|
||||
ref={external_name_ref.clone()}
|
||||
value={(*external_name).clone()}
|
||||
onchange={on_external_name_change}
|
||||
placeholder="Enter calendar name"
|
||||
disabled={*external_is_loading}
|
||||
/>
|
||||
@@ -344,7 +372,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {
|
||||
<input
|
||||
type="url"
|
||||
id="external-url"
|
||||
ref={external_url_ref.clone()}
|
||||
value={(*external_url).clone()}
|
||||
onchange={on_external_url_change}
|
||||
placeholder="https://example.com/calendar.ics"
|
||||
disabled={*external_is_loading}
|
||||
/>
|
||||
|
||||
@@ -71,44 +71,6 @@ pub fn PrintPreviewModal(props: &PrintPreviewModalProps) -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
let on_print = {
|
||||
let start_hour = *start_hour;
|
||||
let end_hour = *end_hour;
|
||||
let view_mode = props.view_mode.clone();
|
||||
Callback::from(move |_: MouseEvent| {
|
||||
if let Some(window) = web_sys::window() {
|
||||
if let Some(document) = window.document() {
|
||||
if let Some(body) = document.body() {
|
||||
// Add print attributes to body for CSS targeting
|
||||
if view_mode == ViewMode::Week {
|
||||
let _ = body.set_attribute("data-print-start-hour", &start_hour.to_string());
|
||||
let _ = body.set_attribute("data-print-end-hour", &end_hour.to_string());
|
||||
}
|
||||
let _ = body.set_attribute("data-print-mode", "true");
|
||||
|
||||
// Trigger print
|
||||
if let Err(e) = window.print() {
|
||||
web_sys::console::log_1(&format!("Print failed: {:?}", e).into());
|
||||
}
|
||||
|
||||
// Clean up attributes after a short delay
|
||||
let cleanup_body = body.clone();
|
||||
let cleanup_callback = Closure::wrap(Box::new(move || {
|
||||
let _ = cleanup_body.remove_attribute("data-print-start-hour");
|
||||
let _ = cleanup_body.remove_attribute("data-print-end-hour");
|
||||
let _ = cleanup_body.remove_attribute("data-print-mode");
|
||||
}) as Box<dyn FnMut()>);
|
||||
|
||||
let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
cleanup_callback.as_ref().unchecked_ref(),
|
||||
1000
|
||||
);
|
||||
cleanup_callback.forget();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let format_hour = |hour: u32| -> String {
|
||||
if hour == 0 {
|
||||
@@ -122,6 +84,157 @@ pub fn PrintPreviewModal(props: &PrintPreviewModalProps) -> Html {
|
||||
}
|
||||
};
|
||||
|
||||
// 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">
|
||||
@@ -200,9 +313,12 @@ pub fn PrintPreviewModal(props: &PrintPreviewModalProps) -> Html {
|
||||
})
|
||||
}}>
|
||||
<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: {}; transform: scale({}); transform-origin: top center;", *start_hour, *end_hour, *zoom_level)}>
|
||||
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 {
|
||||
@@ -216,6 +332,9 @@ pub fn PrintPreviewModal(props: &PrintPreviewModalProps) -> Html {
|
||||
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! {
|
||||
|
||||
@@ -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)]
|
||||
@@ -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