10 Commits

Author SHA1 Message Date
Connor Johnstone
ca1ca0c3b1 Implement comprehensive theme system with FontAwesome icons
- Add comprehensive CSS custom properties for all theme colors
- Include modal, button, input, text, and background color variables
- Enhance dark theme with complete variable overrides for proper contrast
- Replace hardcoded colors in print-preview.css with theme variables
- Add FontAwesome CDN integration and replace emoji icons
- Create minimalistic glass-effect checkbox styling with transparency
- Fix white-on-white text issue in dark theme across all modals

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-12 14:55:07 -04:00
Connor Johnstone
64dbf65beb Fix event positioning in print copy with dynamic base-unit recalculation
- Measure actual print copy div height after aspect-ratio scaling
- Recalculate base-unit based on measured height vs original 720px assumption
- Apply position scaling to .week-event elements in print copy only
- Parse and recalculate top/height pixel values using scale factor
- Add landscape orientation and fit-to-page CSS hints for better printing
- Preserve hour filtering with proper data attributes and CSS variables

This ensures events display correctly when print copy is scaled to fit page
while maintaining proper aspect ratio and hour range filtering.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-12 14:10:43 -04:00
Connor Johnstone
96585440d1 Implement hybrid print preview with CSS-based approach
- Add hidden print-preview-copy div at app level for clean print isolation
- Use @media print rules to show only print copy (960x720) in landscape
- Auto-sync print copy with preview content on modal render via use_effect
- Copy CSS variables and data attributes for proper hour filtering
- Set explicit dimensions matching print-preview-paper content area
- Force landscape orientation with @page rule for better calendar printing

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-12 13:17:21 -04:00
Connor Johnstone
a297d38276 Implement selective print preview content printing with dynamic restoration
- Replace page body with only .print-preview-content div during printing
- Use visibilitychange and focus events to restore original content when print dialog closes
- Add 100ms delay before print dialog to show content replacement briefly
- Remove debug logging for clean production code
- Ensure print output matches preview exactly by sending only preview content to system print dialog

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-12 10:59:42 -04:00
Connor Johnstone
4fdaa9931d Fix print preview event positioning to respond to hour range changes
- Add print_start_hour parameter to calculate_event_position function
- Implement proper hour offset calculation for events in print mode
- Remove CSS transform hacks for event positioning
- Use dynamic pixels_per_hour for proper scaling with hour ranges
- Increase modal max-width to 1600px for better visibility
- Events now correctly reposition when start/end hours change

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 20:38:56 -04:00
Connor Johnstone
c6c7b38bef Implement dynamic base unit calculation for print preview scaling
- Add dynamic height calculation system based on selected hour range and time increment
- Replace hardcoded CSS heights with CSS variables (--print-base-unit, --print-pixels-per-hour)
- Update WeekView component with print mode support and dynamic event positioning
- Optimize week header for print: reduced to 50px height with smaller fonts
- Account for all borders and spacing in calculation (660px available content height)
- Remove debug styling (blue borders, yellow backgrounds)
- Ensure time slots, time labels, and events scale together proportionally
- Perfect fit within 720px content area regardless of hour selection

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 19:26:53 -04:00
Connor Johnstone
78db2cc00f Fix print preview paper dimensions and cleanup redundant CSS files
- Change print preview to landscape orientation (11" x 8.5")
- Fix paper div to render at exact 1056x816 pixels
- Add 48px padding (0.5 inches) directly to paper div
- Remove CSS file redundancy: deleted styles.css.backup, styles/base.css, styles/default.css
- Improve modal sizing to accommodate paper dimensions

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 18:53:40 -04:00
Connor Johnstone
73d191c5ca Merge branch 'main' of git.rcjohnstone.com:connor/calendar into print-preview-feature 2025-09-11 18:01:56 -04:00
d930468748 Merge pull request 'Small bugfixes on the external calendar handling' (#19) from bugfix/external_cal_misc into main
All checks were successful
Build and Push Docker Image / docker (push) Successful in 4m3s
Reviewed-on: #19
2025-09-11 18:01:00 -04:00
Connor Johnstone
91be4436a9 Fix external calendar creation and Outlook compatibility issues
- Fix external calendar form validation by replacing node refs with controlled state inputs
- Add multiple user-agent fallback approach for better external calendar compatibility
- Enhance HTTP client configuration with proper redirect handling and timeouts
- Add detailed error logging and Outlook-specific error detection
- Improve request headers for better calendar server compatibility

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 17:58:53 -04:00
16 changed files with 626 additions and 7575 deletions

View File

@@ -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)))?;
// Log the URL being fetched for debugging
println!("🌍 Fetching calendar URL: {}", calendar.url);
if !response.status().is_success() {
return Err(ApiError::Internal(format!("Calendar server returned: {}", response.status())));
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

View File

@@ -30,6 +30,8 @@ web-sys = { version = "0.3", features = [
"RequestMode",
"Response",
"CssStyleDeclaration",
"MediaQueryList",
"MediaQueryListEvent",
] }
wasm-bindgen = "0.2"
js-sys = "0.3"

View File

@@ -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]

View File

@@ -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>

View File

@@ -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 */
@@ -1172,3 +1166,50 @@
.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 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;
}
}

View File

@@ -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);
}

View File

@@ -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>
}
}

View File

@@ -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 {

View File

@@ -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}
/>

View File

@@ -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! {

View File

@@ -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>

View File

@@ -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 ||

View File

@@ -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

View File

@@ -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