mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 19:22:26 +00:00
frontend: Add dropdown for timezone selection
This commit is contained in:
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -3152,6 +3152,7 @@ dependencies = [
|
|||||||
"headers",
|
"headers",
|
||||||
"hex",
|
"hex",
|
||||||
"http",
|
"http",
|
||||||
|
"itertools 0.14.0",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"rand 0.9.2",
|
"rand 0.9.2",
|
||||||
@@ -3160,6 +3161,7 @@ dependencies = [
|
|||||||
"rustical_oidc",
|
"rustical_oidc",
|
||||||
"rustical_store",
|
"rustical_store",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
@@ -3168,6 +3170,7 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"vtimezones-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -39,3 +39,6 @@ headers.workspace = true
|
|||||||
tower-sessions.workspace = true
|
tower-sessions.workspace = true
|
||||||
percent-encoding.workspace = true
|
percent-encoding.workspace = true
|
||||||
tower-http = { workspace = true, optional = true }
|
tower-http = { workspace = true, optional = true }
|
||||||
|
vtimezones-rs.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
itertools.workspace = true
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import { escapeXml } from ".";
|
|||||||
export class CreateCalendarForm extends LitElement {
|
export class CreateCalendarForm extends LitElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
|
this.fetchTimezones()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchTimezones() {
|
||||||
|
this.timezones = await getTimezones()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override createRenderRoot() {
|
protected override createRenderRoot() {
|
||||||
@@ -36,6 +41,8 @@ export class CreateCalendarForm extends LitElement {
|
|||||||
|
|
||||||
dialog: Ref<HTMLDialogElement> = createRef()
|
dialog: Ref<HTMLDialogElement> = createRef()
|
||||||
form: Ref<HTMLFormElement> = createRef()
|
form: Ref<HTMLFormElement> = createRef()
|
||||||
|
@property()
|
||||||
|
timezones: Array<String> = []
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
return html`
|
return html`
|
||||||
@@ -65,7 +72,12 @@ export class CreateCalendarForm extends LitElement {
|
|||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Timezone (optional)
|
Timezone (optional)
|
||||||
<input type="text" name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value} />
|
<select name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value}>
|
||||||
|
<option value="">No timezone</option>
|
||||||
|
${this.timezones.map(timezone => html`
|
||||||
|
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
||||||
|
`)}
|
||||||
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
|
|||||||
@@ -2,11 +2,17 @@ import { html, LitElement } from "lit";
|
|||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
||||||
import { escapeXml } from ".";
|
import { escapeXml } from ".";
|
||||||
|
import { getTimezones } from "./timezones.ts";
|
||||||
|
|
||||||
@customElement("edit-calendar-form")
|
@customElement("edit-calendar-form")
|
||||||
export class EditCalendarForm extends LitElement {
|
export class EditCalendarForm extends LitElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
|
this.fetchTimezones()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchTimezones() {
|
||||||
|
this.timezones = await getTimezones()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override createRenderRoot() {
|
protected override createRenderRoot() {
|
||||||
@@ -36,7 +42,8 @@ export class EditCalendarForm extends LitElement {
|
|||||||
|
|
||||||
dialog: Ref<HTMLDialogElement> = createRef()
|
dialog: Ref<HTMLDialogElement> = createRef()
|
||||||
form: Ref<HTMLFormElement> = createRef()
|
form: Ref<HTMLFormElement> = createRef()
|
||||||
|
@property()
|
||||||
|
timezones: Array<String> = []
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
return html`
|
return html`
|
||||||
@@ -51,7 +58,12 @@ export class EditCalendarForm extends LitElement {
|
|||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Timezone (optional)
|
Timezone (optional)
|
||||||
<input type="text" name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value} />
|
<select name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value}>
|
||||||
|
<option value="">No timezone</option>
|
||||||
|
${this.timezones.map(timezone => html`
|
||||||
|
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
||||||
|
`)}
|
||||||
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
|
|||||||
12
crates/frontend/js-components/lib/timezones.ts
Normal file
12
crates/frontend/js-components/lib/timezones.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
let timezonesPromise = null
|
||||||
|
export async function getTimezones() {
|
||||||
|
timezonesPromise ||= new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
let response = await fetch('/frontend/_timezones.json')
|
||||||
|
resolve(await response.json())
|
||||||
|
} catch (e) {
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return await timezonesPromise
|
||||||
|
}
|
||||||
@@ -27,6 +27,11 @@ let CreateCalendarForm = class extends i {
|
|||||||
this.components = /* @__PURE__ */ new Set();
|
this.components = /* @__PURE__ */ new Set();
|
||||||
this.dialog = e();
|
this.dialog = e();
|
||||||
this.form = e();
|
this.form = e();
|
||||||
|
this.timezones = [];
|
||||||
|
this.fetchTimezones();
|
||||||
|
}
|
||||||
|
async fetchTimezones() {
|
||||||
|
this.timezones = await getTimezones();
|
||||||
}
|
}
|
||||||
createRenderRoot() {
|
createRenderRoot() {
|
||||||
return this;
|
return this;
|
||||||
@@ -59,7 +64,12 @@ let CreateCalendarForm = class extends i {
|
|||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Timezone (optional)
|
Timezone (optional)
|
||||||
<input type="text" name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value} />
|
<select name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value}>
|
||||||
|
<option value="">No timezone</option>
|
||||||
|
${this.timezones.map((timezone) => x`
|
||||||
|
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
||||||
|
`)}
|
||||||
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
@@ -179,6 +189,9 @@ __decorateClass([
|
|||||||
__decorateClass([
|
__decorateClass([
|
||||||
n$1()
|
n$1()
|
||||||
], CreateCalendarForm.prototype, "components", 2);
|
], CreateCalendarForm.prototype, "components", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], CreateCalendarForm.prototype, "timezones", 2);
|
||||||
CreateCalendarForm = __decorateClass([
|
CreateCalendarForm = __decorateClass([
|
||||||
t("create-calendar-form")
|
t("create-calendar-form")
|
||||||
], CreateCalendarForm);
|
], CreateCalendarForm);
|
||||||
|
|||||||
@@ -2,6 +2,18 @@ import { i, x } from "./lit-DkXrt_Iv.mjs";
|
|||||||
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
|
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
|
||||||
import { e, n } from "./ref-BwbQvJBB.mjs";
|
import { e, n } from "./ref-BwbQvJBB.mjs";
|
||||||
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
|
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
|
||||||
|
let timezonesPromise = null;
|
||||||
|
async function getTimezones() {
|
||||||
|
timezonesPromise ||= new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
let response = await fetch("/frontend/_timezones.json");
|
||||||
|
resolve(await response.json());
|
||||||
|
} catch (e2) {
|
||||||
|
reject(e2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return await timezonesPromise;
|
||||||
|
}
|
||||||
var __defProp = Object.defineProperty;
|
var __defProp = Object.defineProperty;
|
||||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
var __decorateClass = (decorators, target, key, kind) => {
|
var __decorateClass = (decorators, target, key, kind) => {
|
||||||
@@ -22,6 +34,11 @@ let EditCalendarForm = class extends i {
|
|||||||
this.components = /* @__PURE__ */ new Set();
|
this.components = /* @__PURE__ */ new Set();
|
||||||
this.dialog = e();
|
this.dialog = e();
|
||||||
this.form = e();
|
this.form = e();
|
||||||
|
this.timezones = [];
|
||||||
|
this.fetchTimezones();
|
||||||
|
}
|
||||||
|
async fetchTimezones() {
|
||||||
|
this.timezones = await getTimezones();
|
||||||
}
|
}
|
||||||
createRenderRoot() {
|
createRenderRoot() {
|
||||||
return this;
|
return this;
|
||||||
@@ -39,7 +56,12 @@ let EditCalendarForm = class extends i {
|
|||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Timezone (optional)
|
Timezone (optional)
|
||||||
<input type="text" name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value} />
|
<select name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value}>
|
||||||
|
<option value="">No timezone</option>
|
||||||
|
${this.timezones.map((timezone) => x`
|
||||||
|
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
||||||
|
`)}
|
||||||
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
@@ -150,6 +172,9 @@ __decorateClass([
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
], EditCalendarForm.prototype, "components", 2);
|
], EditCalendarForm.prototype, "components", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], EditCalendarForm.prototype, "timezones", 2);
|
||||||
EditCalendarForm = __decorateClass([
|
EditCalendarForm = __decorateClass([
|
||||||
t("edit-calendar-form")
|
t("edit-calendar-form")
|
||||||
], EditCalendarForm);
|
], EditCalendarForm);
|
||||||
|
|||||||
@@ -85,4 +85,3 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<create-calendar-form user="{{ user.id }}"></create-calendar-form>
|
<create-calendar-form user="{{ user.id }}"></create-calendar-form>
|
||||||
<import-calendar-form user="{{ user.id }}"></import-calendar-form>
|
<import-calendar-form user="{{ user.id }}"></import-calendar-form>
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ use crate::routes::{
|
|||||||
app_token::{route_delete_app_token, route_post_app_token},
|
app_token::{route_delete_app_token, route_post_app_token},
|
||||||
calendar::{route_calendar, route_calendar_restore},
|
calendar::{route_calendar, route_calendar_restore},
|
||||||
login::{route_get_login, route_post_login, route_post_logout},
|
login::{route_get_login, route_post_login, route_post_logout},
|
||||||
|
timezones::route_timezones,
|
||||||
user::{route_get_home, route_root, route_user_named},
|
user::{route_get_home, route_root, route_user_named},
|
||||||
};
|
};
|
||||||
#[cfg(not(feature = "dev"))]
|
#[cfg(not(feature = "dev"))]
|
||||||
@@ -79,7 +80,11 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
|
|||||||
.route("/", get(route_root))
|
.route("/", get(route_root))
|
||||||
.nest("/user", user_router)
|
.nest("/user", user_router)
|
||||||
.route("/login", get(route_get_login).post(route_post_login::<AP>))
|
.route("/login", get(route_get_login).post(route_post_login::<AP>))
|
||||||
.route("/logout", post(route_post_logout));
|
.route("/logout", post(route_post_logout))
|
||||||
|
.route(
|
||||||
|
"/_timezones.json",
|
||||||
|
get(route_timezones).head(route_timezones),
|
||||||
|
);
|
||||||
|
|
||||||
#[cfg(not(feature = "dev"))]
|
#[cfg(not(feature = "dev"))]
|
||||||
let mut router = router.route_service("/assets/{*file}", EmbedService::<Assets>::default());
|
let mut router = router.route_service("/assets/{*file}", EmbedService::<Assets>::default());
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ pub mod app_token;
|
|||||||
pub mod calendar;
|
pub mod calendar;
|
||||||
pub mod calendars;
|
pub mod calendars;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
|
pub mod timezones;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|||||||
30
crates/frontend/src/routes/timezones.rs
Normal file
30
crates/frontend/src/routes/timezones.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use headers::{CacheControl, ContentType, HeaderMapExt};
|
||||||
|
use http::{HeaderMap, HeaderValue, Method};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use std::{sync::LazyLock, time::Duration};
|
||||||
|
|
||||||
|
static VTIMEZONES_JSON: LazyLock<String> = LazyLock::new(|| {
|
||||||
|
serde_json::to_string(
|
||||||
|
&vtimezones_rs::VTIMEZONES
|
||||||
|
.keys()
|
||||||
|
.sorted()
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
pub async fn route_timezones(method: Method) -> Response {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.typed_insert(ContentType::json());
|
||||||
|
headers.insert(
|
||||||
|
"ETag",
|
||||||
|
HeaderValue::from_static(vtimezones_rs::IANA_TZDB_VERSION),
|
||||||
|
);
|
||||||
|
headers.typed_insert(CacheControl::new().with_max_age(Duration::from_hours(2)));
|
||||||
|
|
||||||
|
if method == Method::HEAD {
|
||||||
|
return headers.into_response();
|
||||||
|
}
|
||||||
|
(headers, VTIMEZONES_JSON.as_str()).into_response()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user