ezidam: new menu system, with main and submenus

This commit is contained in:
Philippe Loctaux 2023-03-24 22:15:07 +01:00
parent ddf6f25dd2
commit efebe2fa80
9 changed files with 133 additions and 120 deletions

View file

@ -2,38 +2,40 @@ mod icons;
mod items; mod items;
mod template; mod template;
pub use items::{Admin, User}; pub use self::items::{AdminMenu, UserMenu};
pub use template::Template; pub use self::template::{MainItem, SubItem};
pub enum Menu { pub enum MenuWithActiveItem {
User(User), User(UserMenu),
Admin(Admin), Admin(AdminMenu),
} }
impl Menu { impl MenuWithActiveItem {
/// ID of selected menu item
pub fn selected(&self) -> &'static str { pub fn selected(&self) -> &'static str {
match self { match self {
Menu::User(user) => user.id(), MenuWithActiveItem::User(selected) => selected.id(),
Menu::Admin(admin) => admin.id(), MenuWithActiveItem::Admin(selected) => selected.id(),
} }
} }
pub fn list(&self) -> Vec<Template> { /// List of all items of menu
pub fn list(&self) -> Vec<MainItem> {
match self { match self {
Menu::User(user) => user.list(), MenuWithActiveItem::User(_) => UserMenu::list(),
Menu::Admin(admin) => admin.list(), MenuWithActiveItem::Admin(_) => AdminMenu::list(),
} }
} }
} }
impl From<User> for Menu { impl From<UserMenu> for MenuWithActiveItem {
fn from(value: User) -> Self { fn from(value: UserMenu) -> Self {
Self::User(value) Self::User(value)
} }
} }
impl From<Admin> for Menu { impl From<AdminMenu> for MenuWithActiveItem {
fn from(value: Admin) -> Self { fn from(value: AdminMenu) -> Self {
Self::Admin(value) Self::Admin(value)
} }
} }

View file

@ -1,3 +1,4 @@
/// Find icons on https://tabler-icons.io /// Find icons on https://tabler-icons.io
pub const HOME: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-home" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l-2 0l9 -9l9 9l-2 0"></path><path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7"></path><path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6"></path></svg>"#; pub const HOME: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-home" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l-2 0l9 -9l9 9l-2 0"></path><path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7"></path><path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6"></path></svg>"#;
pub const LOGOUT: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-logout" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2"></path><path d="M7 12h14l-3 -3m0 6l3 -3"></path></svg>"#; pub const LOGOUT: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-logout" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2"></path><path d="M7 12h14l-3 -3m0 6l3 -3"></path></svg>"#;
pub const SETTINGS: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-settings" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"></path><path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0"></path></svg>"#;

View file

@ -1,5 +1,5 @@
mod admin; mod admin;
mod user; mod user;
pub use admin::Admin; pub use admin::AdminMenu;
pub use user::User; pub use user::UserMenu;

View file

@ -1,43 +1,54 @@
use super::super::icons; use crate::menu::{icons, MainItem, SubItem};
use crate::menu::Template; use crate::routes;
use rocket::uri;
const LIST: &[Admin] = &[Admin::ExitAdmin, Admin::AdminDashboard]; pub enum AdminMenu {
Exit,
pub enum Admin { Dashboard,
ExitAdmin, Settings,
AdminDashboard,
} }
impl Admin { impl AdminMenu {
pub fn list(&self) -> Vec<Template> {
LIST.iter().map(Template::from).collect()
}
pub fn id(&self) -> &'static str { pub fn id(&self) -> &'static str {
match self { match self {
Self::ExitAdmin => "exit_admin", AdminMenu::Exit => "exit",
Self::AdminDashboard => "admin_dashboard", AdminMenu::Dashboard => "dashboard",
AdminMenu::Settings => "settings",
} }
} }
pub fn list() -> Vec<MainItem> {
pub fn label(&self) -> &'static str { vec![
match self { MainItem {
Self::ExitAdmin => "Exit admin panel", id: AdminMenu::Exit.id(),
Self::AdminDashboard => "Admin dashboard", label: "Exit admin panel",
} link: uri!(routes::root::homepage).to_string(),
} icon: icons::LOGOUT,
sub: None,
pub fn link(&self) -> &'static str { },
match self { MainItem {
Self::ExitAdmin => "/", id: AdminMenu::Dashboard.id(),
Self::AdminDashboard => "/admin", label: "Admin dashboard",
} link: uri!(routes::admin::dashboard::admin_dashboard).to_string(),
} icon: icons::HOME,
sub: None,
pub fn icon(&self) -> &'static str { },
match self { MainItem {
Self::ExitAdmin => icons::LOGOUT, id: AdminMenu::Settings.id(),
Self::AdminDashboard => icons::HOME, label: "Server settings",
} link: uri!(routes::admin::settings::admin_settings).to_string(),
icon: icons::SETTINGS,
// sub: None,
sub: Some(vec![
SubItem {
label: "Branding",
link: uri!(routes::admin::settings::admin_settings).to_string(),
},
SubItem {
label: "Security",
link: uri!(routes::setup::setup).to_string(),
},
]),
},
]
} }
} }

View file

@ -1,38 +1,24 @@
use super::super::icons; use crate::menu::{icons, MainItem};
use crate::menu::Template; use crate::routes;
use rocket::uri;
const LIST: &[User] = &[User::Home]; pub enum UserMenu {
pub enum User {
Home, Home,
} }
impl User { impl UserMenu {
pub fn list(&self) -> Vec<Template> {
LIST.iter().map(Template::from).collect()
}
pub fn id(&self) -> &'static str { pub fn id(&self) -> &'static str {
match self { match self {
User::Home => "home", UserMenu::Home => "home",
} }
} }
pub fn list() -> Vec<MainItem> {
pub fn label(&self) -> &'static str { vec![MainItem {
match self { id: UserMenu::Home.id(),
User::Home => "Home", label: "Home",
} link: uri!(routes::root::homepage).to_string(),
} icon: icons::HOME,
sub: None,
pub fn link(&self) -> &'static str { }]
match self {
User::Home => "/",
}
}
pub fn icon(&self) -> &'static str {
match self {
User::Home => icons::HOME,
}
} }
} }

View file

@ -1,33 +1,18 @@
use super::{Admin, User};
use rocket::serde::Serialize; use rocket::serde::Serialize;
#[derive(Serialize)] #[derive(Serialize)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
pub struct Template { pub struct MainItem {
id: &'static str, pub id: &'static str,
label: &'static str, pub label: &'static str,
link: &'static str, pub link: String,
icon: &'static str, pub icon: &'static str,
pub sub: Option<Vec<SubItem>>,
} }
impl From<&User> for Template { #[derive(Serialize)]
fn from(value: &User) -> Self { #[serde(crate = "rocket::serde")]
Self { pub struct SubItem {
id: value.id(), pub label: &'static str,
label: value.label(), pub link: String,
link: value.link(),
icon: value.icon(),
}
}
}
impl From<&Admin> for Template {
fn from(value: &Admin) -> Self {
Self {
id: value.id(),
label: value.label(),
link: value.link(),
icon: value.icon(),
}
}
} }

View file

@ -4,9 +4,8 @@ mod responder;
mod template; mod template;
use self::content::*; use self::content::*;
use crate::menu::Menu; use crate::menu::MenuWithActiveItem;
use erased_serde::Serialize; use erased_serde::Serialize;
pub use flash::FlashKind; pub use flash::FlashKind;
pub enum Page { pub enum Page {
@ -44,9 +43,8 @@ impl Page {
} }
/// Show menu with active item /// Show menu with active item
fn menu(&self) -> Option<Menu> { fn menu_with_active_item(&self) -> Option<MenuWithActiveItem> {
use crate::menu::Admin as AdminMenu; use crate::menu::{AdminMenu, UserMenu};
use crate::menu::User as UserMenu;
match self { match self {
Page::Error(_) => None, Page::Error(_) => None,
@ -54,7 +52,8 @@ impl Page {
Page::Homepage(_) => Some(UserMenu::Home.into()), Page::Homepage(_) => Some(UserMenu::Home.into()),
Page::Authorize(_) => None, Page::Authorize(_) => None,
Page::Redirect(_) => None, Page::Redirect(_) => None,
Page::AdminDashboard(_) => Some(AdminMenu::AdminDashboard.into()), Page::AdminDashboard(_) => Some(AdminMenu::Dashboard.into()),
Page::AdminSettings(_) => Some(AdminMenu::Settings.into()),
} }
} }

View file

@ -1,5 +1,5 @@
use super::Page; use super::Page;
use crate::menu::Template as MenuTemplate; use crate::menu::MainItem;
use rocket::request::FlashMessage; use rocket::request::FlashMessage;
use rocket::serde::Serialize; use rocket::serde::Serialize;
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
@ -9,7 +9,7 @@ use rocket_dyn_templates::Template;
struct TemplateContent<S: Serialize> { struct TemplateContent<S: Serialize> {
title: &'static str, title: &'static str,
version: &'static str, version: &'static str,
menu: Option<(&'static str, Vec<MenuTemplate>)>, menu: Option<(&'static str, Vec<MainItem>)>,
flash: Option<(String, String)>, flash: Option<(String, String)>,
#[serde(flatten)] #[serde(flatten)]
@ -22,7 +22,9 @@ fn render(p: Page, flash: Option<(String, String)>) -> Template {
TemplateContent { TemplateContent {
title: p.page_title(), title: p.page_title(),
version: env!("CARGO_PKG_VERSION"), version: env!("CARGO_PKG_VERSION"),
menu: p.menu().map(|menu| (menu.selected(), menu.list())), menu: p
.menu_with_active_item()
.map(|menu| (menu.selected(), menu.list())),
flash, flash,
content: p.content(), content: p.content(),
}, },

View file

@ -3,17 +3,44 @@
<ul class="navbar-nav"> <ul class="navbar-nav">
{% for item in menu.1 %} {% for item in menu.1 %}
{# Define css classes #}
{% set nav_class = "nav-item" %}
{# Active #}
{% if item.id == menu.0 %} {% if item.id == menu.0 %}
<li class="nav-item active"> {% set nav_class = nav_class ~ " active" %}
{% else %}
<li class="nav-item">
{% endif %} {% endif %}
{# Submenu #}
{% if item.sub %}
{% set nav_class = nav_class ~ " dropdown" %}
{% endif %}
{# Display css classes #}
<li class="{{ nav_class }}">
{# Display primary menu item #}
{% if item.sub %}
{# Contains a submenu #}
<a class="nav-link dropdown-toggle" href="#navbar-{{ item.id }}" data-bs-toggle="dropdown" data-bs-auto-close="outside" role="button" aria-expanded="false">
{% else %}
{# Primary menu item only #}
<a class="nav-link" href="{{ item.link }}"> <a class="nav-link" href="{{ item.link }}">
<span class="nav-link-icon d-md-none d-lg-inline-block">{{item.icon | safe }}</span> {% endif %}
<span class="nav-link-icon d-md-none d-lg-inline-block">{{ item.icon | safe }}</span>
<span class="nav-link-title">{{ item.label }}</span> <span class="nav-link-title">{{ item.label }}</span>
</a> </a>
{# Display submenu items #}
{% if item.sub %}
<div class="dropdown-menu">
{% for sub_item in item.sub %}
<a class="dropdown-item" href="{{ sub_item.link }}">{{ sub_item.label }}</a>
{% endfor %}
</div>
{% endif %}
</li> </li>
{% endfor %} {% endfor %}