5 Commits

Author SHA1 Message Date
Connor Johnstone
890940fe31 Update README with project status and usage notes
All checks were successful
Build and Push Docker Image / docker (push) Successful in 32s
2025-09-13 18:28:14 -04:00
Connor Johnstone
fdea5cd646 Fix timezone conversion bug for events displaying on wrong day
- Preserve original local dates when converting times to UTC for storage
- Prevent events created late in day from appearing on next day due to timezone offset
- Remove hacky bandaid logic in week view that tried to handle timezone shifts
- Use stored event date directly instead of calculating from UTC conversion
- Ensure events always display on intended day regardless of timezone

Fixes issue where events created within last 4 hours of day would show on wrong day
after UTC conversion caused date component to shift to next day.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-13 18:27:09 -04:00
Connor Johnstone
b307be7eb1 Add password visibility toggle to login form
- Implement show/hide password functionality with eye icon toggle button
- Add dynamic input type switching between password and text
- Position toggle button inside password input field with proper styling
- Include hover, focus states and accessibility features (tabindex, title)
- Use FontAwesome eye/eye-slash icons for visual feedback
- Maintain secure default (password hidden) with optional visibility
- Integrate proper tab order with existing form elements

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-13 18:21:56 -04:00
Connor Johnstone
9d84c380d1 Fix server and username pre-population on login page
- Save credentials to LocalStorage on successful login when remember checkboxes are checked
- Save credentials immediately when input values change and remember is enabled
- Fix closure ownership issues with checkbox state in submit handler
- Ensure remembered values persist and pre-populate correctly on subsequent visits
- Address issue where values weren't saved if checkboxes defaulted to checked state

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-13 18:19:16 -04:00
Connor Johnstone
fad03f94f9 Improve login form layout and accessibility
- Move remember checkboxes inline with inputs for better space utilization
- Implement proper tab order: server → username → password → checkboxes
- Increase form width from 400px to 500px to accommodate horizontal layout
- Make checkbox labels more concise ("Remember" instead of full text)
- Enhance checkbox styling with vertical label placement and larger size
- Reduce form label bottom margin for tighter spacing
- Ensure consistent input widths across all form fields

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-13 18:15:47 -04:00
5 changed files with 169 additions and 60 deletions

View File

@@ -4,7 +4,7 @@
![Runway Screenshot](sample.png)
>[!WARNING]
>This project was entirely vibe coded. It's my first attempt vibe coding anything, but I just sat down one day and realized this was something I've wanted to build for many years, but would just take way too long. With AI, I've been able to lay out the majority of the app in one long weekend. So proceed at your own risk, but I actually think the app is pretty solid.
>This project was entirely vibe coded. It's my first attempt vibe coding anything, but I just sat down one day and realized this was something I've wanted to build for many years, but would just take way too long. With AI, I've been able to lay out the majority of the app in one long weekend. So proceed at your own risk, but I actually think the app is pretty decent. There are still a lot of places where the AI has implemented some really poor solutions to the problems that I didn't catch, but I've begun using it for my own general use.
A modern CalDAV web client built with Rust WebAssembly.

View File

@@ -164,17 +164,20 @@ impl EventCreationData {
self.end_time.format("%H:%M").to_string(),
)
} else {
// Convert local date/time to UTC
// Convert local date/time to UTC, but preserve original local dates
let start_local = Local.from_local_datetime(&self.start_date.and_time(self.start_time)).single();
let end_local = Local.from_local_datetime(&self.end_date.and_time(self.end_time)).single();
if let (Some(start_dt), Some(end_dt)) = (start_local, end_local) {
let start_utc = start_dt.with_timezone(&chrono::Utc);
let end_utc = end_dt.with_timezone(&chrono::Utc);
// IMPORTANT: Use original local dates, not UTC dates!
// This ensures events display on the correct day regardless of timezone conversion
(
start_utc.format("%Y-%m-%d").to_string(),
self.start_date.format("%Y-%m-%d").to_string(),
start_utc.format("%H:%M").to_string(),
end_utc.format("%Y-%m-%d").to_string(),
self.end_date.format("%Y-%m-%d").to_string(),
end_utc.format("%H:%M").to_string(),
)
} else {

View File

@@ -24,23 +24,40 @@ pub fn Login(props: &LoginProps) -> Html {
let remember_server = use_state(|| true);
let remember_username = use_state(|| true);
// Password visibility toggle
let show_password = use_state(|| false);
let server_url_ref = use_node_ref();
let username_ref = use_node_ref();
let password_ref = use_node_ref();
let on_server_url_change = {
let server_url = server_url.clone();
let remember_server = remember_server.clone();
Callback::from(move |e: Event| {
let target = e.target_unchecked_into::<HtmlInputElement>();
server_url.set(target.value());
let new_value = target.value();
server_url.set(new_value.clone());
// Save to localStorage immediately if remember is checked
if *remember_server {
let _ = LocalStorage::set("remembered_server_url", new_value);
}
})
};
let on_username_change = {
let username = username.clone();
let remember_username = remember_username.clone();
Callback::from(move |e: Event| {
let target = e.target_unchecked_into::<HtmlInputElement>();
username.set(target.value());
let new_value = target.value();
username.set(new_value.clone());
// Save to localStorage immediately if remember is checked
if *remember_username {
let _ = LocalStorage::set("remembered_username", new_value);
}
})
};
@@ -84,12 +101,21 @@ pub fn Login(props: &LoginProps) -> Html {
})
};
let on_toggle_password_visibility = {
let show_password = show_password.clone();
Callback::from(move |_| {
show_password.set(!*show_password);
})
};
let on_submit = {
let server_url = server_url.clone();
let username = username.clone();
let password = password.clone();
let error_message = error_message.clone();
let is_loading = is_loading.clone();
let remember_server = remember_server.clone();
let remember_username = remember_username.clone();
let on_login = props.on_login.clone();
Callback::from(move |e: SubmitEvent| {
@@ -100,6 +126,8 @@ pub fn Login(props: &LoginProps) -> Html {
let password = (*password).clone();
let error_message = error_message.clone();
let is_loading = is_loading.clone();
let remember_server_value = *remember_server;
let remember_username_value = *remember_username;
let on_login = on_login.clone();
// Basic client-side validation
@@ -140,6 +168,14 @@ pub fn Login(props: &LoginProps) -> Html {
let _ = LocalStorage::set("user_preferences", &prefs_json);
}
// Save server URL and username to LocalStorage if remember checkboxes are checked
if remember_server_value {
let _ = LocalStorage::set("remembered_server_url", server_url.clone());
}
if remember_username_value {
let _ = LocalStorage::set("remembered_username", username.clone());
}
is_loading.set(false);
on_login.emit(token);
}
@@ -164,59 +200,79 @@ pub fn Login(props: &LoginProps) -> Html {
<form onsubmit={on_submit}>
<div class="form-group">
<label for="server_url">{"CalDAV Server URL"}</label>
<input
ref={server_url_ref}
type="text"
id="server_url"
placeholder="https://your-caldav-server.com/dav/"
value={(*server_url).clone()}
onchange={on_server_url_change}
disabled={*is_loading}
/>
<div class="remember-checkbox">
<div class="input-with-checkbox">
<input
type="checkbox"
id="remember_server"
checked={*remember_server}
onchange={on_remember_server_change}
ref={server_url_ref}
type="text"
id="server_url"
placeholder="https://your-caldav-server.com/dav/"
value={(*server_url).clone()}
onchange={on_server_url_change}
disabled={*is_loading}
tabindex="1"
/>
<label for="remember_server">{"Remember server"}</label>
<div class="remember-checkbox">
<label for="remember_server">{"Remember"}</label>
<input
type="checkbox"
id="remember_server"
checked={*remember_server}
onchange={on_remember_server_change}
tabindex="4"
/>
</div>
</div>
</div>
<div class="form-group">
<label for="username">{"Username"}</label>
<input
ref={username_ref}
type="text"
id="username"
placeholder="Enter your username"
value={(*username).clone()}
onchange={on_username_change}
disabled={*is_loading}
/>
<div class="remember-checkbox">
<div class="input-with-checkbox">
<input
type="checkbox"
id="remember_username"
checked={*remember_username}
onchange={on_remember_username_change}
ref={username_ref}
type="text"
id="username"
placeholder="Enter your username"
value={(*username).clone()}
onchange={on_username_change}
disabled={*is_loading}
tabindex="2"
/>
<label for="remember_username">{"Remember username"}</label>
<div class="remember-checkbox">
<label for="remember_username">{"Remember"}</label>
<input
type="checkbox"
id="remember_username"
checked={*remember_username}
onchange={on_remember_username_change}
tabindex="5"
/>
</div>
</div>
</div>
<div class="form-group">
<label for="password">{"Password"}</label>
<input
ref={password_ref}
type="password"
id="password"
placeholder="Enter your password"
value={(*password).clone()}
onchange={on_password_change}
disabled={*is_loading}
/>
<div class="password-input-container">
<input
ref={password_ref}
type={if *show_password { "text" } else { "password" }}
id="password"
placeholder="Enter your password"
value={(*password).clone()}
onchange={on_password_change}
disabled={*is_loading}
tabindex="3"
/>
<button
type="button"
class="password-toggle-btn"
onclick={on_toggle_password_visibility}
tabindex="6"
title={if *show_password { "Hide password" } else { "Show password" }}
>
<i class={if *show_password { "fas fa-eye-slash" } else { "fas fa-eye" }}></i>
</button>
</div>
</div>
{

View File

@@ -1229,15 +1229,12 @@ fn pixels_to_time(pixels: f64, time_increment: u32) -> NaiveTime {
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();
// Position events based on when they appear in local time, not their original date
// For timezone issues: an event created at 10 PM Sunday might be stored as Monday UTC
// but should still display on Sunday's column since that's when the user sees it
let should_display_here = event_date == date ||
(event_date == date - chrono::Duration::days(1) && local_start.hour() >= 20);
// Events should display based on their stored date (which now preserves the original local date)
// not the calculated local date from UTC conversion, since we fixed the creation logic
let event_date = event.dtstart.date_naive(); // Use the stored date, not the converted local date
if !should_display_here {
if event_date != date {
return (0.0, 0.0, false); // Event not on this date
}

View File

@@ -452,7 +452,7 @@ body {
border-radius: var(--border-radius-medium);
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
width: 100%;
max-width: 400px;
max-width: 500px;
}
.login-form h2, .register-form h2 {
@@ -492,30 +492,83 @@ body {
cursor: not-allowed;
}
.remember-checkbox {
.input-with-checkbox {
display: flex;
align-items: center;
gap: 0.375rem;
margin-top: 0.375rem;
gap: 1rem;
}
.input-with-checkbox input[type="text"],
.input-with-checkbox input[type="password"] {
flex: 1;
min-width: 0;
}
.form-group input {
width: 100%;
}
.remember-checkbox {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.1rem;
opacity: 0.7;
white-space: nowrap;
min-width: 80px;
}
.remember-checkbox input[type="checkbox"] {
width: auto;
margin: 0;
cursor: pointer;
transform: scale(0.85);
transform: scale(1.1);
}
.remember-checkbox label {
margin: 0;
font-size: 0.75rem;
font-size: 0.55rem;
color: #888;
cursor: pointer;
user-select: none;
font-weight: 400;
}
.password-input-container {
position: relative;
display: flex;
align-items: center;
}
.password-input-container input {
padding-right: 3rem !important;
}
.password-toggle-btn {
position: absolute;
right: 0.75rem;
background: transparent;
border: none;
color: #888;
cursor: pointer;
padding: 0.25rem;
font-size: 1rem;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s ease;
z-index: 1;
}
.password-toggle-btn:hover {
color: #555;
}
.password-toggle-btn:focus {
outline: none;
color: #667eea;
}
.login-button, .register-button {
width: 100%;
padding: var(--control-padding);
@@ -1826,7 +1879,7 @@ body {
.form-group label {
display: block;
margin-bottom: 0.75rem;
margin-bottom: 0.4rem;
color: #374151;
font-weight: 600;
font-size: 0.95rem;