Compare commits
5 Commits
a4476dcfae
...
890940fe31
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
890940fe31 | ||
|
|
fdea5cd646 | ||
|
|
b307be7eb1 | ||
|
|
9d84c380d1 | ||
|
|
fad03f94f9 |
@@ -4,7 +4,7 @@
|
||||

|
||||
|
||||
>[!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.
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,6 +200,7 @@ pub fn Login(props: &LoginProps) -> Html {
|
||||
<form onsubmit={on_submit}>
|
||||
<div class="form-group">
|
||||
<label for="server_url">{"CalDAV Server URL"}</label>
|
||||
<div class="input-with-checkbox">
|
||||
<input
|
||||
ref={server_url_ref}
|
||||
type="text"
|
||||
@@ -172,20 +209,24 @@ pub fn Login(props: &LoginProps) -> Html {
|
||||
value={(*server_url).clone()}
|
||||
onchange={on_server_url_change}
|
||||
disabled={*is_loading}
|
||||
tabindex="1"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
<label for="remember_server">{"Remember server"}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">{"Username"}</label>
|
||||
<div class="input-with-checkbox">
|
||||
<input
|
||||
ref={username_ref}
|
||||
type="text"
|
||||
@@ -194,29 +235,44 @@ pub fn Login(props: &LoginProps) -> Html {
|
||||
value={(*username).clone()}
|
||||
onchange={on_username_change}
|
||||
disabled={*is_loading}
|
||||
tabindex="2"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
<label for="remember_username">{"Remember username"}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">{"Password"}</label>
|
||||
<div class="password-input-container">
|
||||
<input
|
||||
ref={password_ref}
|
||||
type="password"
|
||||
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>
|
||||
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user