Minor frontend improvements, feature to create calendar

This commit is contained in:
Lennart
2025-06-08 21:46:20 +02:00
parent bbe9113f5c
commit 573781310a
8 changed files with 165 additions and 10 deletions

View File

@@ -17,6 +17,7 @@ serde.workspace = true
thiserror.workspace = true
tokio.workspace = true
rustical_store.workspace = true
rustical_ical.workspace = true
rust-embed.workspace = true
futures-core.workspace = true
hex.workspace = true

View File

@@ -159,14 +159,14 @@ table {
display: grid;
min-height: 80px;
grid-template-areas:
". color-chip"
"title color-chip"
"description color-chip"
"subscription-url color-chip"
"restore color-chip"
". color-chip";
". . color-chip"
"title comps color-chip"
"description . color-chip"
"subscription-url . color-chip"
"restore . color-chip"
". . color-chip";
grid-template-rows: 12px auto auto auto 12px;
grid-template-columns: auto 80px;
grid-template-columns: min-content auto 80px;
color: inherit;
text-decoration: none;
padding-left: 12px;
@@ -180,14 +180,31 @@ table {
.title {
font-weight: bold;
grid-area: title;
margin-right: 12px;
white-space: nowrap;
}
.comps {
grid-area: comps;
span {
margin: 0 2px;
background: var(--primary-color);
color: var(--text-on-primary-color);
font-size: .8em;
padding: 3px 8px;
border-radius: 12px;
}
}
.description {
grid-area: description;
white-space: nowrap;
}
.subscription-url {
grid-area: subscription-url;
white-space: nowrap;
}
.color-chip {

View File

@@ -64,6 +64,11 @@
<li class="collection-list-item" style="--color: {{ color }}">
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}">
<span class="title">{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}</span>
<div class="comps">
{% for comp in calendar.components %}
<span>{{ comp }}</span>
{% endfor %}
</div>
<span class="description">
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
</span>
@@ -85,6 +90,11 @@
<li class="collection-list-item" style="--color: {{ color }}">
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}">
<span class="title">{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}</span>
<div class="comps">
{% for comp in calendar.components %}
<span>{{ comp }}</span>
{% endfor %}
</div>
<span class="description">
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
</span>
@@ -97,6 +107,49 @@
{% endfor %}
</ul>
{% endif %}
<h3>Create calendar</h3>
<form action="/frontend/user/{{ user.id }}/calendar" method="POST">
<label>
href
<input type="text" name="id" />
</label>
<br>
<label>
Displayname
<input type="text" name="displayname" />
</label>
<br>
<label>
Description
<input type="text" name="description" />
</label>
<br>
<label>
Color
<input type="color" name="color" />
</label>
<br>
<label>
Subscription URL
<input type="text" name="subscription_url" />
</label>
<br>
<label>
Support VEVENT
<input type="checkbox" name="comp_event" checked />
</label>
<label>
Support VTODO
<input type="checkbox" name="comp_todo" checked />
</label>
<label>
Support VJOURNAL
<input type="checkbox" name="comp_journal" />
</label>
<br>
<button type="submit">Create</button>
</form>
</section>
<section>

View File

@@ -34,7 +34,7 @@ use crate::{
routes::{
addressbook::{route_addressbook, route_addressbook_restore},
app_token::{route_delete_app_token, route_post_app_token},
calendar::{route_calendar, route_calendar_restore},
calendar::{route_calendar, route_calendar_restore, route_create_calendar},
login::{route_get_login, route_post_login, route_post_logout},
user::{route_get_home, route_root, route_user_named},
},
@@ -66,6 +66,7 @@ pub fn frontend_router<
post(route_delete_app_token::<AP>),
)
// Calendar
.route("/user/{user}/calendar", post(route_create_calendar::<CS>))
.route(
"/user/{user}/calendar/{calendar}",
get(route_calendar::<CS>),

View File

@@ -3,14 +3,16 @@ use std::sync::Arc;
use askama::Template;
use askama_web::WebTemplate;
use axum::{
Extension,
Extension, Form,
extract::Path,
response::{IntoResponse, Redirect, Response},
};
use axum_extra::TypedHeader;
use headers::Referer;
use http::StatusCode;
use rustical_ical::CalendarObjectType;
use rustical_store::{Calendar, CalendarStore, auth::User};
use serde::{Deserialize, Deserializer};
#[derive(Template, WebTemplate)]
#[template(path = "pages/calendar.html")]
@@ -32,6 +34,82 @@ pub async fn route_calendar<C: CalendarStore>(
.into_response())
}
fn empty_to_none<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
let val: Option<String> = Deserialize::deserialize(deserializer)?;
Ok(val.filter(|val| !val.is_empty()))
}
#[derive(Deserialize, Clone)]
pub struct PutCalendarForm {
id: String,
#[serde(deserialize_with = "empty_to_none")]
displayname: Option<String>,
#[serde(deserialize_with = "empty_to_none")]
description: Option<String>,
#[serde(deserialize_with = "empty_to_none")]
color: Option<String>,
#[serde(deserialize_with = "empty_to_none")]
subscription_url: Option<String>,
comp_event: Option<String>,
comp_todo: Option<String>,
comp_journal: Option<String>,
}
pub async fn route_create_calendar<C: CalendarStore>(
Path(owner): Path<String>,
Extension(store): Extension<Arc<C>>,
user: User,
Form(PutCalendarForm {
id,
displayname,
description,
color,
subscription_url,
comp_event,
comp_todo,
comp_journal,
}): Form<PutCalendarForm>,
) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) {
return Ok(StatusCode::UNAUTHORIZED.into_response());
}
assert!(!id.is_empty());
let mut comps = vec![];
if comp_event.is_some() {
comps.push(CalendarObjectType::Event);
}
if comp_todo.is_some() {
comps.push(CalendarObjectType::Todo);
}
if comp_journal.is_some() {
comps.push(CalendarObjectType::Journal);
}
let cal = Calendar {
id: id.to_owned(),
displayname,
description,
color,
subscription_url,
principal: user.id,
components: comps,
order: 0,
timezone_id: None,
timezone: None,
synctoken: 0,
deleted_at: None,
push_topic: uuid::Uuid::new_v4().to_string(),
};
store.insert_calendar(cal).await?;
Ok(Redirect::to(&id).into_response())
}
pub async fn route_calendar_restore<CS: CalendarStore>(
Path((owner, cal_id)): Path<(String, String)>,
Extension(store): Extension<Arc<CS>>,