Merge pull request 'notification-badge' (#401) from notification-badge into main
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions

Reviewed-on: #401
This commit is contained in:
philipp 2024-04-19 16:36:16 +02:00
commit 31d45d6ab4
20 changed files with 132 additions and 59 deletions

View File

@ -72,5 +72,6 @@ export default defineConfig({
webServer: { webServer: {
timeout: 15 * 60 * 1000, timeout: 15 * 60 * 1000,
command: 'cd .. && ./test_db.sh && cargo r', command: 'cd .. && ./test_db.sh && cargo r',
url: 'http://127.0.0.1:8000'
}, },
}); });

View File

@ -13,3 +13,4 @@
@import 'components/search'; @import 'components/search';
@import 'components/important'; @import 'components/important';
@import 'components/searchable-table'; @import 'components/searchable-table';
@import 'components/notification';

View File

@ -0,0 +1,5 @@
.notification {
right: -.2rem;
top: -.1rem;
font-size: .5rem;
}

View File

@ -8,7 +8,7 @@ test("cox can create and delete trip", async ({ page }) => {
await page.getByPlaceholder("Passwort").fill("cox"); await page.getByPlaceholder("Passwort").fill("cox");
await page.getByPlaceholder("Passwort").press("Enter"); await page.getByPlaceholder("Passwort").press("Enter");
await page.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').click(); await page.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').click();
await page.locator(".relative").first().click(); await page.locator('a[href="#"]:has-text("Ausfahrt")').first().click();
await page.locator("#sidebar #planned_starting_time").click(); await page.locator("#sidebar #planned_starting_time").click();
await page.locator("#sidebar #planned_starting_time").fill("18:00"); await page.locator("#sidebar #planned_starting_time").fill("18:00");
await page.locator("#sidebar #planned_starting_time").press("Tab"); await page.locator("#sidebar #planned_starting_time").press("Tab");
@ -39,7 +39,7 @@ test.describe("cox can edit trips", () => {
await page.getByPlaceholder("Passwort").fill("cox"); await page.getByPlaceholder("Passwort").fill("cox");
await page.getByPlaceholder("Passwort").press("Enter"); await page.getByPlaceholder("Passwort").press("Enter");
await page.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').click(); await page.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').click();
await page.locator(".relative").first().click(); await page.locator('a[href="#"]:has-text("Ausfahrt")').first().click();
await page.locator("#sidebar #planned_starting_time").click(); await page.locator("#sidebar #planned_starting_time").click();
await page.locator("#sidebar #planned_starting_time").fill("18:00"); await page.locator("#sidebar #planned_starting_time").fill("18:00");
await page.locator("#sidebar #planned_starting_time").press("Tab"); await page.locator("#sidebar #planned_starting_time").press("Tab");

View File

@ -47,16 +47,18 @@ pub struct User {
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct UserWithRoles { pub struct UserWithRolesAndNotificationCount {
#[serde(flatten)] #[serde(flatten)]
pub user: User, pub user: User,
pub amount_unread_notifications: i32,
pub roles: Vec<String>, pub roles: Vec<String>,
} }
impl UserWithRoles { impl UserWithRolesAndNotificationCount {
pub async fn from_user(user: User, db: &SqlitePool) -> Self { pub async fn from_user(user: User, db: &SqlitePool) -> Self {
Self { Self {
roles: user.roles(db).await, roles: user.roles(db).await,
amount_unread_notifications: user.amount_unread_notifications(db).await,
user, user,
} }
} }
@ -237,6 +239,17 @@ impl User {
.count .count
} }
pub async fn amount_unread_notifications(&self, db: &SqlitePool) -> i32 {
sqlx::query!(
"SELECT COUNT(*) as count FROM notification WHERE user_id = ? AND read_at IS NULL",
self.id
)
.fetch_one(db)
.await
.unwrap()
.count
}
pub async fn has_role(&self, db: &SqlitePool, role: &str) -> bool { pub async fn has_role(&self, db: &SqlitePool, role: &str) -> bool {
if sqlx::query!( if sqlx::query!(
"SELECT * FROM user_role WHERE user_id=? AND role_id = (SELECT id FROM role WHERE name = ?)", "SELECT * FROM user_role WHERE user_id=? AND role_id = (SELECT id FROM role WHERE name = ?)",

View File

@ -2,7 +2,7 @@ use crate::model::{
boat::{Boat, BoatToAdd, BoatToUpdate}, boat::{Boat, BoatToAdd, BoatToUpdate},
location::Location, location::Location,
log::Log, log::Log,
user::{AdminUser, User, UserWithRoles}, user::{AdminUser, User, UserWithRolesAndNotificationCount},
}; };
use rocket::{ use rocket::{
form::Form, form::Form,
@ -33,7 +33,7 @@ async fn index(
context.insert("users", &users); context.insert("users", &users);
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRoles::from_user(admin.user, db).await, &UserWithRolesAndNotificationCount::from_user(admin.user, db).await,
); );
Template::render("admin/boat/index", context.into_json()) Template::render("admin/boat/index", context.into_json())

View File

@ -10,7 +10,7 @@ use crate::model::log::Log;
use crate::model::mail::Mail; use crate::model::mail::Mail;
use crate::model::role::Role; use crate::model::role::Role;
use crate::model::user::AdminUser; use crate::model::user::AdminUser;
use crate::model::user::UserWithRoles; use crate::model::user::UserWithRolesAndNotificationCount;
use crate::tera::Config; use crate::tera::Config;
#[get("/mail")] #[get("/mail")]
@ -27,7 +27,7 @@ async fn index(
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRoles::from_user(admin.user, db).await, &UserWithRolesAndNotificationCount::from_user(admin.user, db).await,
); );
context.insert("roles", &roles); context.insert("roles", &roles);

View File

@ -2,7 +2,7 @@ use crate::model::{
log::Log, log::Log,
notification::Notification, notification::Notification,
role::Role, role::Role,
user::{AdminUser, User, UserWithRoles}, user::{AdminUser, User, UserWithRolesAndNotificationCount},
}; };
use rocket::{ use rocket::{
form::Form, form::Form,
@ -26,7 +26,7 @@ async fn index(
} }
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRoles::from_user(user.user, db).await, &UserWithRolesAndNotificationCount::from_user(user.user, db).await,
); );
context.insert("roles", &Role::all(db).await); context.insert("roles", &Role::all(db).await);

View File

@ -1,6 +1,6 @@
use crate::model::{ use crate::model::{
role::Role, role::Role,
user::{SchnupperBetreuerUser, User, UserWithRoles}, user::{SchnupperBetreuerUser, User, UserWithRolesAndNotificationCount},
}; };
use futures::future::join_all; use futures::future::join_all;
use rocket::{ use rocket::{
@ -38,9 +38,9 @@ async fn index(
let user_futures: Vec<_> = User::all_with_role(db, &schnupperant) let user_futures: Vec<_> = User::all_with_role(db, &schnupperant)
.await .await
.into_iter() .into_iter()
.map(|u| async move { UserWithRoles::from_user(u, db).await }) .map(|u| async move { UserWithRolesAndNotificationCount::from_user(u, db).await })
.collect(); .collect();
let users: Vec<UserWithRoles> = join_all(user_futures).await; let users: Vec<UserWithRolesAndNotificationCount> = join_all(user_futures).await;
let mut context = Context::new(); let mut context = Context::new();
if let Some(msg) = flash { if let Some(msg) = flash {
@ -49,7 +49,7 @@ async fn index(
context.insert("schnupperanten", &users); context.insert("schnupperanten", &users);
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRoles::from_user(user.into(), db).await, &UserWithRolesAndNotificationCount::from_user(user.into(), db).await,
); );
Template::render("admin/schnupper/index", context.into_json()) Template::render("admin/schnupper/index", context.into_json())

View File

@ -6,8 +6,8 @@ use crate::model::{
logbook::Logbook, logbook::Logbook,
role::Role, role::Role,
user::{ user::{
AdminUser, User, UserWithMembershipPdf, UserWithRoles, UserWithRolesAndMembershipPdf, AdminUser, User, UserWithMembershipPdf, UserWithRolesAndMembershipPdf,
VorstandUser, UserWithRolesAndNotificationCount, VorstandUser,
}, },
}; };
use futures::future::join_all; use futures::future::join_all;
@ -67,7 +67,10 @@ async fn index(
context.insert("users", &users); context.insert("users", &users);
context.insert("roles", &roles); context.insert("roles", &roles);
context.insert("families", &families); context.insert("families", &families);
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await); context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user, db).await,
);
Template::render("admin/user/index", context.into_json()) Template::render("admin/user/index", context.into_json())
} }
@ -99,7 +102,10 @@ async fn index_admin(
context.insert("users", &users); context.insert("users", &users);
context.insert("roles", &roles); context.insert("roles", &roles);
context.insert("families", &families); context.insert("families", &families);
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await); context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user, db).await,
);
Template::render("admin/user/index", context.into_json()) Template::render("admin/user/index", context.into_json())
} }
@ -127,7 +133,7 @@ async fn fees(
} }
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRoles::from_user(admin.into(), db).await, &UserWithRolesAndNotificationCount::from_user(admin.into(), db).await,
); );
Template::render("admin/user/fees", context.into_json()) Template::render("admin/user/fees", context.into_json())
@ -147,7 +153,7 @@ async fn scheckbuch(
for s in scheckbooks { for s in scheckbooks {
scheckbooks_with_roles.push(( scheckbooks_with_roles.push((
Logbook::completed_with_user(db, &s).await, Logbook::completed_with_user(db, &s).await,
UserWithRoles::from_user(s, db).await, UserWithRolesAndNotificationCount::from_user(s, db).await,
)) ))
} }
@ -158,7 +164,7 @@ async fn scheckbuch(
} }
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRoles::from_user(user.into(), db).await, &UserWithRolesAndNotificationCount::from_user(user.into(), db).await,
); );
Template::render("admin/user/scheckbuch", context.into_json()) Template::render("admin/user/scheckbuch", context.into_json())

View File

@ -1,7 +1,7 @@
use crate::model::{ use crate::model::{
boat::Boat, boat::Boat,
boathouse::Boathouse, boathouse::Boathouse,
user::{AdminUser, UserWithRoles, VorstandUser}, user::{AdminUser, UserWithRolesAndNotificationCount, VorstandUser},
}; };
use rocket::{ use rocket::{
form::Form, form::Form,
@ -39,7 +39,7 @@ async fn index(
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRoles::from_user(admin.into(), db).await, &UserWithRolesAndNotificationCount::from_user(admin.into(), db).await,
); );
Template::render("board/boathouse", context.into_json()) Template::render("board/boathouse", context.into_json())

View File

@ -13,7 +13,7 @@ use crate::{
model::{ model::{
boat::Boat, boat::Boat,
boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified}, boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified},
user::{CoxUser, DonauLinzUser, TechUser, User, UserWithRoles}, user::{CoxUser, DonauLinzUser, TechUser, User, UserWithRolesAndNotificationCount},
}, },
tera::log::KioskCookie, tera::log::KioskCookie,
}; };
@ -59,7 +59,7 @@ async fn index(
context.insert("boats", &boats); context.insert("boats", &boats);
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRoles::from_user(user.into(), db).await, &UserWithRolesAndNotificationCount::from_user(user.into(), db).await,
); );
Template::render("boatdamages", context.into_json()) Template::render("boatdamages", context.into_json())

View File

@ -14,7 +14,7 @@ use crate::{
model::{ model::{
boat::Boat, boat::Boat,
boatreservation::{BoatReservation, BoatReservationToAdd}, boatreservation::{BoatReservation, BoatReservationToAdd},
user::{DonauLinzUser, User, UserWithRoles}, user::{DonauLinzUser, User, UserWithRolesAndNotificationCount},
}, },
tera::log::KioskCookie, tera::log::KioskCookie,
}; };
@ -74,7 +74,7 @@ async fn index(
context.insert("user", &User::all(db).await); context.insert("user", &User::all(db).await);
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRoles::from_user(user.into(), db).await, &UserWithRolesAndNotificationCount::from_user(user.into(), db).await,
); );
Template::render("boatreservations", context.into_json()) Template::render("boatreservations", context.into_json())

View File

@ -18,7 +18,7 @@ use tera::Context;
use crate::model::{ use crate::model::{
log::Log, log::Log,
user::{AdminUser, User, UserWithRoles}, user::{AdminUser, User, UserWithRolesAndNotificationCount},
}; };
#[derive(Serialize)] #[derive(Serialize)]
@ -51,7 +51,7 @@ async fn send(db: &State<SqlitePool>, _user: AdminUser) -> Template {
Template::render( Template::render(
"ergo.final", "ergo.final",
context!(loggedin_user: &UserWithRoles::from_user(_user.user, db).await, thirty, dozen), context!(loggedin_user: &UserWithRolesAndNotificationCount::from_user(_user.user, db).await, thirty, dozen),
) )
} }
@ -120,7 +120,10 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
if let Some(msg) = flash { if let Some(msg) = flash {
context.insert("flash", &msg.into_inner()); context.insert("flash", &msg.into_inner());
} }
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await); context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user, db).await,
);
context.insert("users", &users); context.insert("users", &users);
context.insert("thirty", &thirty); context.insert("thirty", &thirty);
context.insert("dozen", &dozen); context.insert("dozen", &dozen);

View File

@ -24,7 +24,9 @@ use crate::model::{
LogbookUpdateError, LogbookUpdateError,
}, },
logtype::LogType, logtype::LogType,
user::{AdminUser, DonauLinzUser, User, UserWithRoles, UserWithWaterStatus}, user::{
AdminUser, DonauLinzUser, User, UserWithRolesAndNotificationCount, UserWithWaterStatus,
},
}; };
pub struct KioskCookie(String); pub struct KioskCookie(String);
@ -84,7 +86,7 @@ async fn index(
context.insert("logtypes", &logtypes); context.insert("logtypes", &logtypes);
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRoles::from_user(user.into(), db).await, &UserWithRolesAndNotificationCount::from_user(user.into(), db).await,
); );
context.insert("on_water", &on_water); context.insert("on_water", &on_water);
context.insert("distances", &distances); context.insert("distances", &distances);
@ -98,7 +100,7 @@ async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
Template::render( Template::render(
"log.completed", "log.completed",
context!(logs, loggedin_user: &UserWithRoles::from_user(user.into(), db).await), context!(logs, loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.into(), db).await),
) )
} }
@ -108,7 +110,7 @@ async fn show_for_year(db: &State<SqlitePool>, user: AdminUser, year: i32) -> Te
Template::render( Template::render(
"log.completed", "log.completed",
context!(logs, loggedin_user: &UserWithRoles::from_user(user.user, db).await), context!(logs, loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.user, db).await),
) )
} }

View File

@ -23,7 +23,7 @@ use tera::Context;
use crate::model::{ use crate::model::{
notification::Notification, notification::Notification,
role::Role, role::Role,
user::{User, UserWithRoles}, user::{User, UserWithRolesAndNotificationCount},
}; };
pub(crate) mod admin; pub(crate) mod admin;
@ -53,7 +53,10 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
} }
context.insert("notifications", &Notification::for_user(db, &user).await); context.insert("notifications", &Notification::for_user(db, &user).await);
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await); context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user, db).await,
);
Template::render("index", context.into_json()) Template::render("index", context.into_json())
} }
@ -75,7 +78,10 @@ async fn steering(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage
context.insert("coxes", &coxes); context.insert("coxes", &coxes);
context.insert("bootskundige", &bootskundige); context.insert("bootskundige", &bootskundige);
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await); context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user, db).await,
);
Template::render("steering", context.into_json()) Template::render("steering", context.into_json())
} }

View File

@ -13,7 +13,7 @@ use crate::model::{
logbook::Logbook, logbook::Logbook,
tripdetails::TripDetails, tripdetails::TripDetails,
triptype::TripType, triptype::TripType,
user::{AllowedForPlannedTripsUser, User, UserWithRoles}, user::{AllowedForPlannedTripsUser, User, UserWithRolesAndNotificationCount},
usertrip::{UserTrip, UserTripDeleteError, UserTripError}, usertrip::{UserTrip, UserTripDeleteError, UserTripError},
}; };
@ -44,7 +44,10 @@ async fn index(
} }
context.insert("fee", &user.fee(db).await); context.insert("fee", &user.fee(db).await);
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await); context.insert(
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user, db).await,
);
context.insert("days", &days); context.insert("days", &days);
Template::render("planned", context.into_json()) Template::render("planned", context.into_json())
} }

View File

@ -4,7 +4,7 @@ use sqlx::SqlitePool;
use crate::model::{ use crate::model::{
stat::{self, BoatStat, Stat}, stat::{self, BoatStat, Stat},
user::{DonauLinzUser, UserWithRoles}, user::{DonauLinzUser, UserWithRolesAndNotificationCount},
}; };
use super::log::KioskCookie; use super::log::KioskCookie;
@ -16,7 +16,7 @@ async fn index_boat(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
Template::render( Template::render(
"stat.boats", "stat.boats",
context!(loggedin_user: &UserWithRoles::from_user(user.into(), db).await, stat, kiosk), context!(loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, stat, kiosk),
) )
} }
@ -38,7 +38,7 @@ async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -
Template::render( Template::render(
"stat.people", "stat.people",
context!(loggedin_user: &UserWithRoles::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km), context!(loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km),
) )
} }

View File

@ -33,7 +33,24 @@
{{ loggedin_user.name }} {{ loggedin_user.name }}
</a> </a>
</div> </div>
<div> <div class="flex items-center">
{% if loggedin_user.amount_unread_notifications > 0 %}
<a href="/#notification"
class="relative inline-flex items-end ms-2 me-3">
<svg height="20"
width="24"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 20 24">
<path d="M1.5 8.67v8.58a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V8.67l-8.928 5.493a3 3 0 0 1-3.144 0L1.5 8.67Z" />
<path d="M22.5 6.908V6.75a3 3 0 0 0-3-3h-15a3 3 0 0 0-3 3v.158l9.714 5.978a1.5 1.5 0 0 0 1.572 0L22.5 6.908Z" />
</svg>
<small class="bg-red-500 rounded-full w-3 h-3 inline-flex justify-center items-center absolute p-1 notification">
{{ loggedin_user.amount_unread_notifications }}
</small>
</a>
{% endif %}
<a href="#" <a href="#"
class="inline-flex justify-center rounded-md bg-primary-600 mx-1 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer" class="inline-flex justify-center rounded-md bg-primary-600 mx-1 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer"
data-sidebar="true" data-sidebar="true"
@ -163,18 +180,24 @@
<div class="p-2 border border-t-0 border-{{ bg }} mb-4 rounded-b-md"> <div class="p-2 border border-t-0 border-{{ bg }} mb-4 rounded-b-md">
{% if participants | length > 0 %} {% if participants | length > 0 %}
{% for rower in participants %} {% for rower in participants %}
<div class="relative">
{{ rower.name }} {{ rower.name }}
{% if rower.is_guest %}<small class="text-gray-600 dark:text-gray-100">(Scheckbuch)</small>{% endif %} {% if rower.is_guest %}<small class="text-gray-600 dark:text-gray-100">(Scheckbuch)</small>{% endif %}
{% if rower.is_real_guest %} {% if rower.is_real_guest %}
<small class="text-gray-600 dark:text-gray-100">(Gast)</small> <small class="text-gray-600 dark:text-gray-100">(Gast)</small>
{% if allow_removing %} {% if allow_removing %}
<a href="/planned/remove/{{ trip_details_id }}/{{ rower.name }}" <a href="/planned/remove/{{ trip_details_id }}/{{ rower.name }}"
class="btn btn-attention btn-fw">Abmelden</a> class="absolute r-0 bg-red-500 w-5 h-5 text-white rounded-full flex items-center justify-center transform rotate-45 top-0 right-0">
<svg class="inline h-5 w-5" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
</svg>
<span class="sr-only">Abmelden</span>
</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
<span class="hidden">(angemeldet seit <span class="hidden">(angemeldet seit
{{ rower.registered_at }})</span> {{ rower.registered_at }})</span>
<br /> </div>
{% endfor %} {% endfor %}
{% else %} {% else %}
{{ text }} {{ text }}

View File

@ -2,7 +2,11 @@
{% extends "base" %} {% extends "base" %}
{% block content %} {% block content %}
<div class="max-w-screen-lg w-full"> <div class="max-w-screen-lg w-full">
<h1 class="h1">Ruderassistent</h1> <h1 class="h1">
Ruder
<wbr />
assistent
</h1>
<div class="grid gap-3 my-5"> <div class="grid gap-3 my-5">
<div class="m-auto"> <div class="m-auto">
<a href="/planned" <a href="/planned"
@ -11,9 +15,15 @@
<span class="text-xl px-3">Geplante Ausfahrten</span> <span class="text-xl px-3">Geplante Ausfahrten</span>
</a> </a>
</div> </div>
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" <div id="notification"
class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert"> role="alert">
<h2 class="h2">Nachrichten</h2> <h2 class="h2">Nachrichten</h2>
{% if loggedin_user.amount_unread_notifications > 10 %}
<div class="text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 text-center pb-3 px-3">
Du hast viele ungelesene Benachrichtigungen. Um deine Oberfläche übersichtlich zu halten und wichtige Updates nicht zu verpassen, nimm dir bitte einen Moment Zeit sie zu überprüfen und als gelesen zu markieren (&#10003;).
</div>
{% endif %}
<div class="divide-y"> <div class="divide-y">
{% for notification in notifications %} {% for notification in notifications %}
{% if not notification.read_at %} {% if not notification.read_at %}
@ -38,8 +48,8 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>
<details class="py-3 bg-gray-200 dark:bg-primary-950 rounded-b-md"> <details class="py-3 border-t rounded-b-md">
<summary class="px-3">Vergangene Nachrichten (14 Tage)</summary> <summary class="px-3 cursor-pointer">Vergangene Nachrichten (14 Tage)</summary>
<div class="divide-y text-sm"> <div class="divide-y text-sm">
{% for notification in notifications %} {% for notification in notifications %}
{% if notification.read_at %} {% if notification.read_at %}