From 40938cba024d711484c0624139e775c8cae4f77b Mon Sep 17 00:00:00 2001
From: Lennart K <18233294+lennart-k@users.noreply.github.com>
Date: Fri, 4 Jul 2025 19:44:17 +0200
Subject: [PATCH] Some work on the frontend
---
crates/frontend/public/assets/style.css | 224 +++++++++++-------
.../components/addressbooks_section.html | 56 -----
.../components/calendars_section.html | 72 ------
.../templates/components/profile_section.html | 58 -----
.../sections/addressbooks_section.html | 54 +++++
.../sections/calendars_section.html | 70 ++++++
.../components/sections/profile_section.html | 56 +++++
.../public/templates/layouts/default.html | 3 +-
.../frontend/public/templates/pages/user.html | 20 +-
crates/frontend/src/lib.rs | 4 +
crates/frontend/src/pages/mod.rs | 1 +
crates/frontend/src/pages/user.rs | 14 ++
crates/frontend/src/routes/addressbooks.rs | 53 +++++
crates/frontend/src/routes/calendars.rs | 52 ++++
crates/frontend/src/routes/mod.rs | 2 +
crates/frontend/src/routes/user.rs | 31 +--
16 files changed, 470 insertions(+), 300 deletions(-)
delete mode 100644 crates/frontend/public/templates/components/addressbooks_section.html
delete mode 100644 crates/frontend/public/templates/components/calendars_section.html
delete mode 100644 crates/frontend/public/templates/components/profile_section.html
create mode 100644 crates/frontend/public/templates/components/sections/addressbooks_section.html
create mode 100644 crates/frontend/public/templates/components/sections/calendars_section.html
create mode 100644 crates/frontend/public/templates/components/sections/profile_section.html
create mode 100644 crates/frontend/src/pages/mod.rs
create mode 100644 crates/frontend/src/pages/user.rs
create mode 100644 crates/frontend/src/routes/addressbooks.rs
create mode 100644 crates/frontend/src/routes/calendars.rs
diff --git a/crates/frontend/public/assets/style.css b/crates/frontend/public/assets/style.css
index 0c218f4..2cd0e3d 100644
--- a/crates/frontend/public/assets/style.css
+++ b/crates/frontend/public/assets/style.css
@@ -1,21 +1,40 @@
:root {
--background-color: #FFF;
--background-darker: #EEE;
+ --text-on-background-color: #111;
--primary-color: #2F2FE1;
--primary-color-dark: color-mix(in srgb, var(--primary-color), #000000 80%);
--text-on-primary-color: #FFF;
/* --color-red: #FE2060; */
/* --color-red: #EE1D59; */
--color-red: #E31B39;
+ --dilute-color: black;
+ --border-color: black;
}
-html {
+@media (prefers-color-scheme: dark) {
+ :root {
+ --background-color: #222;
+ --background-darker: #292929;
+ --text-on-background-color: #CACACA;
+ --primary-color: color-mix(in srgb, #2F2FE1, white 15%);
+ --primary-color-dark: color-mix(in srgb, var(--primary-color), #000000 80%);
+ --text-on-primary-color: #FFF;
+ /* --color-red: #FE2060; */
+ --color-red: #EE1D59;
+ --dilute-color: white;
+ --border-color: color-mix(in srgb, var(--background-color), var(--dilute-color) 15%);
+ }
+}
+
+html, dialog {
background-color: var(--background-color);
+ color: var(--text-on-background-color);
}
body {
/* position: relative; */
- font-family: sans-serif;
+ font-family: 'Noto Sans', Helvetica, Arial, sans-serif;
margin: 0 auto;
max-width: 1200px;
min-height: 100%;
@@ -29,6 +48,10 @@ body {
padding: 12px;
}
+a {
+ color: var(--text-on-background-color);
+}
+
header {
background: var(--background-darker);
height: 60px;
@@ -37,24 +60,45 @@ header {
align-items: center;
padding: 12px;
- border: 2px solid black;
+ border: 2px solid var(--border-color);
border-radius: 12px;
margin: 12px;
box-shadow: 4px 2px 12px -5px black;
- a {
+ display: flex;
+ justify-content: space-between;
+
+ a.logo {
font-size: 2em;
text-decoration: none;
- color: black;
+ }
+
+ nav {
+ display: flex;
+
+ border-radius: 12px;
+ background: color-mix(in srgb, var(--background-darker), var(--dilute-color) 5%);
+
+ a {
+ text-decoration: none;
+ margin: 4px 8px;
+ padding: 8px 12px;
+ border-radius: 12px;
+ background: color-mix(in srgb, var(--background-darker), var(--dilute-color) 2%);
+
+ &:hover {
+ background: color-mix(in srgb, var(--background-darker), var(--dilute-color) 20%);
+ }
+
+ &.active {
+ background: color-mix(in srgb, var(--background-darker), var(--dilute-color) 15%);
+ }
+ }
}
.logout_form {
display: contents;
-
- button {
- margin-left: auto;
- }
}
}
@@ -69,7 +113,7 @@ button,
.button {
border: none;
background: var(--primary-color);
- padding: 10px 16px;
+ padding: 8px 16px;
border-radius: 8px;
color: var(--text-on-primary-color);
font-size: 0.9em;
@@ -97,7 +141,7 @@ input[type="password"] {
}
section {
- border: 1px solid black;
+ border: 1px solid var(--border-color);
border-radius: 12px;
box-shadow: 4px 2px 12px -8px black;
border-collapse: collapse;
@@ -108,7 +152,7 @@ section {
}
table {
- border: 1px solid black;
+ border: 1px solid var(--border-color);
border-radius: 12px;
box-shadow: 4px 2px 12px -6px black;
border-collapse: collapse;
@@ -118,7 +162,7 @@ table {
td,
th {
padding: 8px;
- border: 1px solid black;
+ border: 1px solid var(--border-color);
width: max-content;
}
@@ -126,12 +170,8 @@ table {
height: 40px;
}
- /* tr:nth-of-type(2n+1) { */
- /* background: var(--background-darker); */
- /* } */
-
tr:hover {
- background: #DDD;
+ background: color-mix(in srgb, var(--background-color), var(--dilute-color) 10%);
}
tr:first-child th {
@@ -151,87 +191,85 @@ table {
}
}
-#page-user {
- ul {
- padding-left: 0;
+ul.collection-list {
+ padding-left: 0;
- li.collection-list-item {
- list-style: none;
- display: contents;
+ li.collection-list-item {
+ list-style: none;
+ display: contents;
- a {
- background: #EEE;
- display: grid;
- min-height: 80px;
- grid-template-areas:
- ". . color-chip"
- "title comps color-chip"
- "description description color-chip"
- "subscription-url subscription-url color-chip"
- "actions actions color-chip"
- ". . color-chip";
- grid-template-rows: 12px auto auto auto auto 12px;
- grid-template-columns: min-content auto 80px;
- row-gap: 4px;
- color: inherit;
- text-decoration: none;
- padding-left: 12px;
+ a {
+ background: color-mix(in srgb, var(--background-color), var(--dilute-color) 5%);
+ display: grid;
+ min-height: 80px;
+ grid-template-areas:
+ ". . color-chip"
+ "title comps color-chip"
+ "description description color-chip"
+ "subscription-url subscription-url color-chip"
+ "actions actions color-chip"
+ ". . color-chip";
+ grid-template-rows: 12px auto auto auto auto 12px;
+ grid-template-columns: min-content auto 80px;
+ row-gap: 4px;
+ color: inherit;
+ text-decoration: none;
+ padding-left: 12px;
- border: 2px solid black;
- border-radius: 12px;
- margin: 12px;
- box-shadow: 4px 2px 12px -6px black;
- overflow: hidden;
+ border: 2px solid var(--border-color);
+ border-radius: 12px;
+ margin: 12px;
+ box-shadow: 4px 2px 12px -6px black;
+ overflow: hidden;
- .title {
- font-weight: bold;
- grid-area: title;
- margin-right: 12px;
- white-space: nowrap;
- }
+ .title {
+ font-weight: bold;
+ grid-area: title;
+ margin-right: 12px;
+ white-space: nowrap;
+ }
+
+ span {
+ margin: 8px initial;
+ }
+
+ .comps {
+ grid-area: comps;
span {
- margin: 8px initial;
+ margin: 0 2px;
+ background: var(--primary-color);
+ color: var(--text-on-primary-color);
+ font-size: .8em;
+ padding: 3px 8px;
+ border-radius: 12px;
}
+ }
- .comps {
- grid-area: comps;
+ .description {
+ grid-area: description;
+ white-space: nowrap;
+ }
- span {
- margin: 0 2px;
- background: var(--primary-color);
- color: var(--text-on-primary-color);
- font-size: .8em;
- padding: 3px 8px;
- border-radius: 12px;
- }
- }
+ .subscription-url {
+ grid-area: subscription-url;
+ white-space: nowrap;
+ }
- .description {
- grid-area: description;
- white-space: nowrap;
- }
+ .color-chip {
+ background: var(--color);
+ grid-area: color-chip;
+ }
- .subscription-url {
- grid-area: subscription-url;
- white-space: nowrap;
- }
+ .actions {
+ grid-area: actions;
+ width: fit-content;
+ display: flex;
+ gap: 12px;
+ }
- .color-chip {
- background: var(--color);
- grid-area: color-chip;
- }
-
- .actions {
- grid-area: actions;
- width: fit-content;
- display: flex;
- gap: 12px;
- }
-
- &:hover {
- background: #DDD;
- }
+ &:hover {
+ background: color-mix(in srgb, var(--background-color), var(--dilute-color) 10%);
}
}
}
@@ -242,7 +280,7 @@ textarea {
}
dialog {
- border: 1px solid black;
+ border: 1px solid var(--border-color);
border-radius: 16px;
padding: 32px;
}
@@ -252,6 +290,14 @@ footer {
justify-content: center;
margin-top: 32px;
gap: 24px;
- /* position: absolute; */
bottom: 20px;
}
+
+input[type="text"],
+input[type="password"] {
+ background: color-mix(in srgb, var(--background-color), var(--dilute-color) 10%);
+ border: 2px solid var(--border-color);
+ padding: 6px 6px;
+ color: var(--text-on-background-color);
+ margin: 2px;
+}
diff --git a/crates/frontend/public/templates/components/addressbooks_section.html b/crates/frontend/public/templates/components/addressbooks_section.html
deleted file mode 100644
index 409fd2f..0000000
--- a/crates/frontend/public/templates/components/addressbooks_section.html
+++ /dev/null
@@ -1,56 +0,0 @@
-
- Addressbooks
-
- {%if !deleted_addressbooks.is_empty() %}
- Deleted Addressbooks
-
- {% endif %}
-
-
-
-
diff --git a/crates/frontend/public/templates/components/calendars_section.html b/crates/frontend/public/templates/components/calendars_section.html
deleted file mode 100644
index 8499d2b..0000000
--- a/crates/frontend/public/templates/components/calendars_section.html
+++ /dev/null
@@ -1,72 +0,0 @@
-
- Calendars
-
- {%if !deleted_calendars.is_empty() %}
- Deleted Calendars
-
- {% endif %}
-
-
-
diff --git a/crates/frontend/public/templates/components/profile_section.html b/crates/frontend/public/templates/components/profile_section.html
deleted file mode 100644
index 5b94c20..0000000
--- a/crates/frontend/public/templates/components/profile_section.html
+++ /dev/null
@@ -1,58 +0,0 @@
-
- Profile
-
- {% let groups = user.memberships_without_self() %}
- {% if groups.len() > 0 %}
- Groups
-
- {% for group in groups %}
- - {{ group }}
- {% endfor %}
-
- {% endif %}
-
- App tokens
-
-
- | Name |
- Created at |
- |
-
- {% for app_token in app_tokens %}
-
- | {{ app_token.name }} |
-
- {% if let Some(created_at) = app_token.created_at %}
- {{ chrono_humanize::HumanTime::from(created_at.to_owned()) }}
- {% endif %}
- |
-
-
- |
-
- {% endfor %}
-
- |
-
- |
- |
-
-
- {% if is_apple %}
-
- {% endif %}
- |
-
-
- {% if let Some(hostname) = davx5_hostname %}
- Configure
- in DAVx5
- {% endif %}
-
diff --git a/crates/frontend/public/templates/components/sections/addressbooks_section.html b/crates/frontend/public/templates/components/sections/addressbooks_section.html
new file mode 100644
index 0000000..94245c7
--- /dev/null
+++ b/crates/frontend/public/templates/components/sections/addressbooks_section.html
@@ -0,0 +1,54 @@
+
{{user.id }}'s Addressbooks
+
+{%if !deleted_addressbooks.is_empty() %}
+Deleted Addressbooks
+
+{% endif %}
+
+
+
diff --git a/crates/frontend/public/templates/components/sections/calendars_section.html b/crates/frontend/public/templates/components/sections/calendars_section.html
new file mode 100644
index 0000000..50a1c04
--- /dev/null
+++ b/crates/frontend/public/templates/components/sections/calendars_section.html
@@ -0,0 +1,70 @@
+{{ user.id }}'s Calendars
+
+{%if !deleted_calendars.is_empty() %}
+Deleted Calendars
+
+{% endif %}
+
+
diff --git a/crates/frontend/public/templates/components/sections/profile_section.html b/crates/frontend/public/templates/components/sections/profile_section.html
new file mode 100644
index 0000000..96faef3
--- /dev/null
+++ b/crates/frontend/public/templates/components/sections/profile_section.html
@@ -0,0 +1,56 @@
+{{ user.id }}'s Profile
+
+{% let groups = user.memberships_without_self() %}
+{% if groups.len() > 0 %}
+Groups
+
+ {% for group in groups %}
+ - {{ group }}
+ {% endfor %}
+
+{% endif %}
+
+App tokens
+
+
+ | Name |
+ Created at |
+ |
+
+ {% for app_token in app_tokens %}
+
+ | {{ app_token.name }} |
+
+ {% if let Some(created_at) = app_token.created_at %}
+ {{ chrono_humanize::HumanTime::from(created_at.to_owned()) }}
+ {% endif %}
+ |
+
+
+ |
+
+ {% endfor %}
+
+ |
+
+ |
+ |
+
+
+ {% if is_apple %}
+
+ {% endif %}
+ |
+
+
+{% if let Some(hostname) = davx5_hostname %}
+Configure
+ in DAVx5
+{% endif %}
diff --git a/crates/frontend/public/templates/layouts/default.html b/crates/frontend/public/templates/layouts/default.html
index f17efbf..17774ad 100644
--- a/crates/frontend/public/templates/layouts/default.html
+++ b/crates/frontend/public/templates/layouts/default.html
@@ -12,7 +12,8 @@
{% block header %}
- RustiCal
+ RustiCal
+ {% block header_center %}{% endblock %}
diff --git a/crates/frontend/public/templates/pages/user.html b/crates/frontend/public/templates/pages/user.html
index cb009ef..926e571 100644
--- a/crates/frontend/public/templates/pages/user.html
+++ b/crates/frontend/public/templates/pages/user.html
@@ -5,15 +5,15 @@
{% endblock %}
-
-{% block content %}
-
-
-
Welcome {{ user.id }}!
-
-{% include "components/profile_section.html" %}
-{% include "components/calendars_section.html" %}
-{% include "components/addressbooks_section.html" %}
-
+{% block header_center %}
+
+{% endblock %}
+
+{% block content %}
+{{ section|safe }}
{% endblock %}
diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs
index 8929bb4..5f94fca 100644
--- a/crates/frontend/src/lib.rs
+++ b/crates/frontend/src/lib.rs
@@ -8,6 +8,7 @@ use axum::{
};
use headers::{ContentType, HeaderMapExt};
use http::{Method, StatusCode};
+use routes::{addressbooks::route_addressbooks, calendars::route_calendars};
use rustical_oidc::{OidcConfig, OidcServiceConfig, route_get_oidc_callback, route_post_oidc};
use rustical_store::{
AddressbookStore, CalendarStore,
@@ -20,6 +21,7 @@ mod assets;
mod config;
pub mod nextcloud_login;
mod oidc_user_store;
+pub(crate) mod pages;
mod routes;
pub use config::FrontendConfig;
@@ -56,6 +58,7 @@ pub fn frontend_router
),
)
// Calendar
+ .route("/user/{user}/calendar", get(route_calendars::))
.route(
"/user/{user}/calendar/{calendar}",
get(route_calendar::),
@@ -65,6 +68,7 @@ pub fn frontend_router),
)
// Addressbook
+ .route("/user/{user}/addressbook", get(route_addressbooks::))
.route(
"/user/{user}/addressbook/{addressbook}",
get(route_addressbook::),
diff --git a/crates/frontend/src/pages/mod.rs b/crates/frontend/src/pages/mod.rs
new file mode 100644
index 0000000..22d12a3
--- /dev/null
+++ b/crates/frontend/src/pages/mod.rs
@@ -0,0 +1 @@
+pub mod user;
diff --git a/crates/frontend/src/pages/user.rs b/crates/frontend/src/pages/user.rs
new file mode 100644
index 0000000..e395ea0
--- /dev/null
+++ b/crates/frontend/src/pages/user.rs
@@ -0,0 +1,14 @@
+use askama::Template;
+use askama_web::WebTemplate;
+use rustical_store::auth::Principal;
+
+pub trait Section: Template {
+ fn name() -> &'static str;
+}
+
+#[derive(Template, WebTemplate)]
+#[template(path = "pages/user.html")]
+pub struct UserPage {
+ pub user: Principal,
+ pub section: S,
+}
diff --git a/crates/frontend/src/routes/addressbooks.rs b/crates/frontend/src/routes/addressbooks.rs
new file mode 100644
index 0000000..2e41a6f
--- /dev/null
+++ b/crates/frontend/src/routes/addressbooks.rs
@@ -0,0 +1,53 @@
+use std::sync::Arc;
+
+use askama::Template;
+use askama_web::WebTemplate;
+use axum::{Extension, extract::Path, response::IntoResponse};
+use http::StatusCode;
+use rustical_store::{Addressbook, AddressbookStore, auth::Principal};
+
+use crate::pages::user::{Section, UserPage};
+
+impl Section for AddressbooksSection {
+ fn name() -> &'static str {
+ "addressbooks"
+ }
+}
+
+#[derive(Template, WebTemplate)]
+#[template(path = "components/sections/addressbooks_section.html")]
+pub struct AddressbooksSection {
+ pub user: Principal,
+ pub addressbooks: Vec,
+ pub deleted_addressbooks: Vec,
+}
+
+pub async fn route_addressbooks(
+ Path(user_id): Path,
+ Extension(addr_store): Extension>,
+ user: Principal,
+) -> impl IntoResponse {
+ if user_id != user.id {
+ return StatusCode::UNAUTHORIZED.into_response();
+ }
+
+ let mut addressbooks = vec![];
+ for group in user.memberships() {
+ addressbooks.extend(addr_store.get_addressbooks(group).await.unwrap());
+ }
+
+ let mut deleted_addressbooks = vec![];
+ for group in user.memberships() {
+ deleted_addressbooks.extend(addr_store.get_deleted_addressbooks(group).await.unwrap());
+ }
+
+ UserPage {
+ section: AddressbooksSection {
+ user: user.clone(),
+ addressbooks,
+ deleted_addressbooks,
+ },
+ user,
+ }
+ .into_response()
+}
diff --git a/crates/frontend/src/routes/calendars.rs b/crates/frontend/src/routes/calendars.rs
new file mode 100644
index 0000000..7cebc29
--- /dev/null
+++ b/crates/frontend/src/routes/calendars.rs
@@ -0,0 +1,52 @@
+use std::sync::Arc;
+
+use crate::pages::user::{Section, UserPage};
+use askama::Template;
+use askama_web::WebTemplate;
+use axum::{Extension, extract::Path, response::IntoResponse};
+use http::StatusCode;
+use rustical_store::{Calendar, CalendarStore, auth::Principal};
+
+impl Section for CalendarsSection {
+ fn name() -> &'static str {
+ "calendars"
+ }
+}
+
+#[derive(Template, WebTemplate)]
+#[template(path = "components/sections/calendars_section.html")]
+pub struct CalendarsSection {
+ pub user: Principal,
+ pub calendars: Vec,
+ pub deleted_calendars: Vec,
+}
+
+pub async fn route_calendars(
+ Path(user_id): Path,
+ Extension(cal_store): Extension>,
+ user: Principal,
+) -> impl IntoResponse {
+ if user_id != user.id {
+ return StatusCode::UNAUTHORIZED.into_response();
+ }
+
+ let mut calendars = vec![];
+ for group in user.memberships() {
+ calendars.extend(cal_store.get_calendars(group).await.unwrap());
+ }
+
+ let mut deleted_calendars = vec![];
+ for group in user.memberships() {
+ deleted_calendars.extend(cal_store.get_deleted_calendars(group).await.unwrap());
+ }
+
+ UserPage {
+ section: CalendarsSection {
+ user: user.clone(),
+ calendars,
+ deleted_calendars,
+ },
+ user,
+ }
+ .into_response()
+}
diff --git a/crates/frontend/src/routes/mod.rs b/crates/frontend/src/routes/mod.rs
index e007c7c..f3ecc12 100644
--- a/crates/frontend/src/routes/mod.rs
+++ b/crates/frontend/src/routes/mod.rs
@@ -1,5 +1,7 @@
pub mod addressbook;
+pub mod addressbooks;
pub mod app_token;
pub mod calendar;
+pub mod calendars;
pub mod login;
pub mod user;
diff --git a/crates/frontend/src/routes/user.rs b/crates/frontend/src/routes/user.rs
index fb45cc8..c2bc7cb 100644
--- a/crates/frontend/src/routes/user.rs
+++ b/crates/frontend/src/routes/user.rs
@@ -11,19 +11,23 @@ use axum_extra::{TypedHeader, extract::Host};
use headers::UserAgent;
use http::StatusCode;
use rustical_store::{
- Addressbook, AddressbookStore, Calendar, CalendarStore,
+ AddressbookStore, CalendarStore,
auth::{AppToken, AuthenticationProvider, Principal},
};
+use crate::pages::user::{Section, UserPage};
+
+impl Section for ProfileSection {
+ fn name() -> &'static str {
+ "profile"
+ }
+}
+
#[derive(Template, WebTemplate)]
-#[template(path = "pages/user.html")]
-pub struct UserPage {
+#[template(path = "components/sections/profile_section.html")]
+pub struct ProfileSection {
pub user: Principal,
pub app_tokens: Vec,
- pub calendars: Vec,
- pub deleted_calendars: Vec,
- pub addressbooks: Vec,
- pub deleted_addressbooks: Vec,
pub is_apple: bool,
pub davx5_hostname: Option,
}
@@ -69,14 +73,13 @@ pub async fn route_user_named<
let davx5_hostname = user_agent.as_str().contains("Android").then_some(host);
UserPage {
- app_tokens: auth_provider.get_app_tokens(&user.id).await.unwrap(),
- calendars,
- deleted_calendars,
- addressbooks,
- deleted_addressbooks,
+ section: ProfileSection {
+ user: user.clone(),
+ app_tokens: auth_provider.get_app_tokens(&user.id).await.unwrap(),
+ is_apple,
+ davx5_hostname,
+ },
user,
- is_apple,
- davx5_hostname,
}
.into_response()
}