mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 20:32:48 +00:00
Some work on the frontend
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
<section>
|
||||
<h2>Addressbooks</h2>
|
||||
<ul>
|
||||
{% for addressbook in addressbooks %}
|
||||
<li class="collection-list-item">
|
||||
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}">
|
||||
<span class="title">
|
||||
{%- if addressbook.principal != user.id -%}{{ addressbook.principal }}/{%- endif -%}
|
||||
{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}
|
||||
</span>
|
||||
<span class="description">
|
||||
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
|
||||
</span>
|
||||
<div class="actions">
|
||||
<form action="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}" target="_blank"
|
||||
method="GET">
|
||||
<button type="submit">Download</button>
|
||||
</form>
|
||||
<delete-button trash
|
||||
href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
You do not have any addressbooks yet
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{%if !deleted_addressbooks.is_empty() %}
|
||||
<h3>Deleted Addressbooks</h3>
|
||||
<ul>
|
||||
{% for addressbook in deleted_addressbooks %}
|
||||
<li class="collection-list-item">
|
||||
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}">
|
||||
<span class="title">
|
||||
{%- if addressbook.principal != user.id -%}{{ addressbook.principal }}/{%- endif -%}
|
||||
{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}
|
||||
</span>
|
||||
<span class="description">
|
||||
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
|
||||
</span>
|
||||
<div class="actions">
|
||||
<form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore"
|
||||
method="POST" class="restore-form">
|
||||
<button type="submit">Restore</button>
|
||||
</form>
|
||||
<delete-button href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<create-addressbook-form user="{{ user.id }}"></create-addressbook-form>
|
||||
|
||||
</section>
|
||||
@@ -1,72 +0,0 @@
|
||||
<section>
|
||||
<h2>Calendars</h2>
|
||||
<ul>
|
||||
{% for calendar in calendars %}
|
||||
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
|
||||
<li class="collection-list-item" style="--color: {{ color }}">
|
||||
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}">
|
||||
<span class="title">
|
||||
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
|
||||
{{ 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>
|
||||
{% if let Some(subscription_url) = calendar.subscription_url %}
|
||||
<span class="subscription-url">{{ subscription_url }}</span>
|
||||
{% endif %}
|
||||
<div class="actions">
|
||||
<form action="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}" target="_blank" method="GET">
|
||||
<button type="submit">Download</button>
|
||||
</form>
|
||||
{% if !calendar.id.starts_with("_birthdays_") %}
|
||||
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="color-chip"></div>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
You do not have any calendars yet
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{%if !deleted_calendars.is_empty() %}
|
||||
<h3>Deleted Calendars</h3>
|
||||
<ul>
|
||||
{% for calendar in deleted_calendars %}
|
||||
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
|
||||
<li class="collection-list-item" style="--color: {{ color }}">
|
||||
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}">
|
||||
<span class="title">
|
||||
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
|
||||
{{ 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>
|
||||
<div class="actions">
|
||||
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST"
|
||||
class="restore-form">
|
||||
<button type="submit">Restore</button>
|
||||
</form>
|
||||
<delete-button href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
||||
</div>
|
||||
<div class="color-chip"></div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<create-calendar-form user="{{ user.id }}"></create-calendar-form>
|
||||
|
||||
</section>
|
||||
@@ -1,58 +0,0 @@
|
||||
<section>
|
||||
<h2>Profile</h2>
|
||||
|
||||
{% let groups = user.memberships_without_self() %}
|
||||
{% if groups.len() > 0 %}
|
||||
<h3>Groups</h3>
|
||||
<ul>
|
||||
{% for group in groups %}
|
||||
<li>{{ group }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<h3>App tokens</h3>
|
||||
<table id="app-tokens">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Created at</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for app_token in app_tokens %}
|
||||
<tr>
|
||||
<td>{{ app_token.name }}</td>
|
||||
<td>
|
||||
{% if let Some(created_at) = app_token.created_at %}
|
||||
{{ chrono_humanize::HumanTime::from(created_at.to_owned()) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<form action="/frontend/user/{{ user.id }}/app_token/{{ app_token.id }}/delete" method="POST">
|
||||
<button type="submit" class="delete">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="generate">
|
||||
<td>
|
||||
<form action="/frontend/user/{{ user.id }}/app_token" method="POST" id="form_generate_app_token">
|
||||
<label class="font_bold" for="generate_app_token_name">App name</label>
|
||||
<input type="text" name="name" id="generate_app_token_name" />
|
||||
</form>
|
||||
</td>
|
||||
<td></td>
|
||||
<td>
|
||||
<button type="submit" form="form_generate_app_token">Generate</button>
|
||||
{% if is_apple %}
|
||||
<button type="submit" form="form_generate_app_token" name="apple" value="true">Apple Configuration Profile
|
||||
(contains token)</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% if let Some(hostname) = davx5_hostname %}
|
||||
<a
|
||||
href="intent://{{ hostname | urlencode }}#Intent;action=android.intent.action.VIEW;component=at.bitfire.davdroid.ui.setup.LoginActivity;scheme=davx5;package=at.bitfire.davdroid;S.loginFlow=1;end">Configure
|
||||
in DAVx5</a>
|
||||
{% endif %}
|
||||
</section>
|
||||
@@ -0,0 +1,54 @@
|
||||
<h2>{{user.id }}'s Addressbooks</h2>
|
||||
<ul class="collection-list">
|
||||
{% for addressbook in addressbooks %}
|
||||
<li class="collection-list-item">
|
||||
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}">
|
||||
<span class="title">
|
||||
{%- if addressbook.principal != user.id -%}{{ addressbook.principal }}/{%- endif -%}
|
||||
{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}
|
||||
</span>
|
||||
<span class="description">
|
||||
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
|
||||
</span>
|
||||
<div class="actions">
|
||||
<form action="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}" target="_blank"
|
||||
method="GET">
|
||||
<button type="submit">Download</button>
|
||||
</form>
|
||||
<delete-button trash
|
||||
href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
You do not have any addressbooks yet
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{%if !deleted_addressbooks.is_empty() %}
|
||||
<h3>Deleted Addressbooks</h3>
|
||||
<ul class="collection-list">
|
||||
{% for addressbook in deleted_addressbooks %}
|
||||
<li class="collection-list-item">
|
||||
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}">
|
||||
<span class="title">
|
||||
{%- if addressbook.principal != user.id -%}{{ addressbook.principal }}/{%- endif -%}
|
||||
{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}
|
||||
</span>
|
||||
<span class="description">
|
||||
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
|
||||
</span>
|
||||
<div class="actions">
|
||||
<form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore"
|
||||
method="POST" class="restore-form">
|
||||
<button type="submit">Restore</button>
|
||||
</form>
|
||||
<delete-button href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<create-addressbook-form user="{{ user.id }}"></create-addressbook-form>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<h2>{{ user.id }}'s Calendars</h2>
|
||||
<ul class="collection-list">
|
||||
{% for calendar in calendars %}
|
||||
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
|
||||
<li class="collection-list-item" style="--color: {{ color }}">
|
||||
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}">
|
||||
<span class="title">
|
||||
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
|
||||
{{ 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>
|
||||
{% if let Some(subscription_url) = calendar.subscription_url %}
|
||||
<span class="subscription-url">{{ subscription_url }}</span>
|
||||
{% endif %}
|
||||
<div class="actions">
|
||||
<form action="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}" target="_blank" method="GET">
|
||||
<button type="submit">Download</button>
|
||||
</form>
|
||||
{% if !calendar.id.starts_with("_birthdays_") %}
|
||||
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="color-chip"></div>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
You do not have any calendars yet
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{%if !deleted_calendars.is_empty() %}
|
||||
<h3>Deleted Calendars</h3>
|
||||
<ul class="collection-list">
|
||||
{% for calendar in deleted_calendars %}
|
||||
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
|
||||
<li class="collection-list-item" style="--color: {{ color }}">
|
||||
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}">
|
||||
<span class="title">
|
||||
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
|
||||
{{ 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>
|
||||
<div class="actions">
|
||||
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST"
|
||||
class="restore-form">
|
||||
<button type="submit">Restore</button>
|
||||
</form>
|
||||
<delete-button href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
||||
</div>
|
||||
<div class="color-chip"></div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<create-calendar-form user="{{ user.id }}"></create-calendar-form>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<h2>{{ user.id }}'s Profile</h2>
|
||||
|
||||
{% let groups = user.memberships_without_self() %}
|
||||
{% if groups.len() > 0 %}
|
||||
<h3>Groups</h3>
|
||||
<ul>
|
||||
{% for group in groups %}
|
||||
<li>{{ group }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<h3>App tokens</h3>
|
||||
<table id="app-tokens">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Created at</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for app_token in app_tokens %}
|
||||
<tr>
|
||||
<td>{{ app_token.name }}</td>
|
||||
<td>
|
||||
{% if let Some(created_at) = app_token.created_at %}
|
||||
{{ chrono_humanize::HumanTime::from(created_at.to_owned()) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<form action="/frontend/user/{{ user.id }}/app_token/{{ app_token.id }}/delete" method="POST">
|
||||
<button type="submit" class="delete">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="generate">
|
||||
<td>
|
||||
<form action="/frontend/user/{{ user.id }}/app_token" method="POST" id="form_generate_app_token">
|
||||
<label class="font_bold" for="generate_app_token_name">App name</label>
|
||||
<input type="text" name="name" id="generate_app_token_name" />
|
||||
</form>
|
||||
</td>
|
||||
<td></td>
|
||||
<td>
|
||||
<button type="submit" form="form_generate_app_token">Generate</button>
|
||||
{% if is_apple %}
|
||||
<button type="submit" form="form_generate_app_token" name="apple" value="true">Apple Configuration Profile
|
||||
(contains token)</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% if let Some(hostname) = davx5_hostname %}
|
||||
<a
|
||||
href="intent://{{ hostname | urlencode }}#Intent;action=android.intent.action.VIEW;component=at.bitfire.davdroid.ui.setup.LoginActivity;scheme=davx5;package=at.bitfire.davdroid;S.loginFlow=1;end">Configure
|
||||
in DAVx5</a>
|
||||
{% endif %}
|
||||
@@ -12,7 +12,8 @@
|
||||
<body>
|
||||
{% block header %}
|
||||
<header>
|
||||
<a href="/frontend/user">RustiCal</a>
|
||||
<a class="logo" href="/frontend/user">RustiCal</a>
|
||||
{% block header_center %}{% endblock %}
|
||||
<form method="POST" action="/frontend/logout" class="logout_form">
|
||||
<button type="submit">Log out</button>
|
||||
</form>
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
<script type="module" src="/frontend/assets/js/create-addressbook-form.mjs" async></script>
|
||||
<script type="module" src="/frontend/assets/js/delete-button.mjs" async></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="page-user">
|
||||
|
||||
<h1>Welcome {{ user.id }}!</h1>
|
||||
|
||||
{% include "components/profile_section.html" %}
|
||||
{% include "components/calendars_section.html" %}
|
||||
{% include "components/addressbooks_section.html" %}
|
||||
|
||||
{% block header_center %}
|
||||
<nav class="header-center">
|
||||
<a href="/frontend/user/{{ user.id }}" {% if S::name() == "profile" %}class="active"{% endif %}>Profile</a>
|
||||
<a href="/frontend/user/{{ user.id }}/calendar" {% if S::name() == "calendars" %}class="active"{% endif %}>Calendars</a>
|
||||
<a href="/frontend/user/{{ user.id }}/addressbook" {% if S::name() == "addressbooks" %}class="active"{% endif %}>Addressbooks</a>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ section|safe }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -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<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
|
||||
post(route_delete_app_token::<AP>),
|
||||
)
|
||||
// Calendar
|
||||
.route("/user/{user}/calendar", get(route_calendars::<CS>))
|
||||
.route(
|
||||
"/user/{user}/calendar/{calendar}",
|
||||
get(route_calendar::<CS>),
|
||||
@@ -65,6 +68,7 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
|
||||
post(route_calendar_restore::<CS>),
|
||||
)
|
||||
// Addressbook
|
||||
.route("/user/{user}/addressbook", get(route_addressbooks::<AS>))
|
||||
.route(
|
||||
"/user/{user}/addressbook/{addressbook}",
|
||||
get(route_addressbook::<AS>),
|
||||
|
||||
1
crates/frontend/src/pages/mod.rs
Normal file
1
crates/frontend/src/pages/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod user;
|
||||
14
crates/frontend/src/pages/user.rs
Normal file
14
crates/frontend/src/pages/user.rs
Normal file
@@ -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<S: Section> {
|
||||
pub user: Principal,
|
||||
pub section: S,
|
||||
}
|
||||
53
crates/frontend/src/routes/addressbooks.rs
Normal file
53
crates/frontend/src/routes/addressbooks.rs
Normal file
@@ -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<Addressbook>,
|
||||
pub deleted_addressbooks: Vec<Addressbook>,
|
||||
}
|
||||
|
||||
pub async fn route_addressbooks<AS: AddressbookStore>(
|
||||
Path(user_id): Path<String>,
|
||||
Extension(addr_store): Extension<Arc<AS>>,
|
||||
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()
|
||||
}
|
||||
52
crates/frontend/src/routes/calendars.rs
Normal file
52
crates/frontend/src/routes/calendars.rs
Normal file
@@ -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<Calendar>,
|
||||
pub deleted_calendars: Vec<Calendar>,
|
||||
}
|
||||
|
||||
pub async fn route_calendars<CS: CalendarStore>(
|
||||
Path(user_id): Path<String>,
|
||||
Extension(cal_store): Extension<Arc<CS>>,
|
||||
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()
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<AppToken>,
|
||||
pub calendars: Vec<Calendar>,
|
||||
pub deleted_calendars: Vec<Calendar>,
|
||||
pub addressbooks: Vec<Addressbook>,
|
||||
pub deleted_addressbooks: Vec<Addressbook>,
|
||||
pub is_apple: bool,
|
||||
pub davx5_hostname: Option<String>,
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user