399 lines
14 KiB
Python
399 lines
14 KiB
Python
import reflex as rx
|
|
from .state import AuthState
|
|
|
|
FULL_W = "240px"
|
|
RAIL_W = "68px"
|
|
TOPBAR_H = "56px"
|
|
|
|
# EPTM brand palette (logo: noir #000 + rouge #e00010)
|
|
_BG = "#f8f9fa" # sidebar background (light)
|
|
_BORDER = "#e5e7eb" # subtle separator
|
|
_TEXT = "#4b5563" # inactive text
|
|
_TEXT_MUTED = "#9ca3af" # muted labels
|
|
_ACTIVE_BG = "rgba(220, 0, 14, 0.18)" # EPTM red tint
|
|
_ACTIVE_CLR = "#ff4a54" # bright red on dark bg
|
|
_HOVER_BG = "#f3f4f6"
|
|
_USER_BG = "#f3f4f6" # slightly darker user section
|
|
|
|
_PAGES = [
|
|
("Tableau de bord", "/accueil", "layout-dashboard"),
|
|
("Apprentis", "/fiche", "user"),
|
|
("Classes", "/classe", "users"),
|
|
]
|
|
|
|
_ADMIN_PAGES = [
|
|
("Escada", "/escada", "globe"),
|
|
("Logs", "/logs", "file-text"),
|
|
("Utilisateurs", "/users", "user-cog"),
|
|
("Parametres", "/params", "settings"),
|
|
]
|
|
|
|
|
|
def _nav_full(label: str, href: str, icon_name: str, close_menu: bool = False) -> rx.Component:
|
|
is_active = AuthState.router.page.path == href
|
|
click_handler = AuthState.close_mobile_menu if close_menu else None
|
|
return rx.link(
|
|
rx.hstack(
|
|
rx.box(
|
|
width="3px",
|
|
height="100%",
|
|
min_height="20px",
|
|
border_radius="0 2px 2px 0",
|
|
background_color=rx.cond(is_active, _ACTIVE_CLR, "transparent"),
|
|
position="absolute",
|
|
left="0",
|
|
top="0",
|
|
),
|
|
rx.icon(
|
|
icon_name, size=17,
|
|
color=rx.cond(is_active, _ACTIVE_CLR, _TEXT),
|
|
flex_shrink="0",
|
|
),
|
|
rx.text(
|
|
label, size="2",
|
|
color=rx.cond(is_active, "#ffffff", _TEXT),
|
|
font_weight=rx.cond(is_active, "600", "400"),
|
|
white_space="nowrap",
|
|
overflow="hidden",
|
|
),
|
|
spacing="3",
|
|
align="center",
|
|
width="100%",
|
|
padding_x="0.75rem",
|
|
padding_y="0.5rem",
|
|
border_radius="0 6px 6px 0",
|
|
background_color=rx.cond(is_active, _ACTIVE_BG, "transparent"),
|
|
_hover={"background_color": _HOVER_BG},
|
|
position="relative",
|
|
),
|
|
href=href,
|
|
on_click=click_handler,
|
|
text_decoration="none",
|
|
width="100%",
|
|
display="block",
|
|
)
|
|
|
|
|
|
def _nav_rail(label: str, href: str, icon_name: str) -> rx.Component:
|
|
is_active = AuthState.router.page.path == href
|
|
return rx.tooltip(
|
|
rx.link(
|
|
rx.box(
|
|
rx.icon(icon_name, size=20,
|
|
color=rx.cond(is_active, _ACTIVE_CLR, _TEXT)),
|
|
width="100%",
|
|
display="flex",
|
|
align_items="center",
|
|
justify_content="center",
|
|
padding_y="0.6rem",
|
|
border_radius="6px",
|
|
background_color=rx.cond(is_active, _ACTIVE_BG, "transparent"),
|
|
border_left=rx.cond(is_active, f"3px solid {_ACTIVE_CLR}", "3px solid transparent"),
|
|
_hover={"background_color": _HOVER_BG},
|
|
),
|
|
href=href,
|
|
text_decoration="none",
|
|
width="100%",
|
|
display="block",
|
|
),
|
|
content=label,
|
|
side="right",
|
|
)
|
|
|
|
|
|
def _nav_item(label: str, href: str, icon_name: str) -> rx.Component:
|
|
return rx.cond(
|
|
AuthState.sidebar_collapsed,
|
|
_nav_rail(label, href, icon_name),
|
|
_nav_full(label, href, icon_name),
|
|
)
|
|
|
|
|
|
def _admin_section(mobile: bool = False) -> rx.Component:
|
|
return rx.cond(
|
|
AuthState.role == "admin",
|
|
rx.vstack(
|
|
rx.box(height="1px", width="100%", background_color=_BORDER),
|
|
rx.cond(
|
|
AuthState.sidebar_collapsed if not mobile else rx.Var.create(False),
|
|
# Rail mode
|
|
rx.vstack(
|
|
*[_nav_rail(l, h, i) for l, h, i in _ADMIN_PAGES],
|
|
spacing="1", width="100%",
|
|
padding_x="0.5rem", padding_y="0.5rem",
|
|
),
|
|
# Full mode with collapsible
|
|
rx.vstack(
|
|
rx.button(
|
|
rx.hstack(
|
|
rx.icon("shield", size=13, color=_TEXT_MUTED),
|
|
rx.text("Admin", size="1", color=_TEXT_MUTED,
|
|
font_weight="600", letter_spacing="0.1em"),
|
|
rx.spacer(),
|
|
rx.icon(
|
|
rx.cond(AuthState.admin_expanded, "chevron-up", "chevron-down"),
|
|
size=13, color=_TEXT_MUTED,
|
|
),
|
|
spacing="2", align="center", width="100%",
|
|
),
|
|
on_click=AuthState.toggle_admin,
|
|
variant="ghost",
|
|
width="100%",
|
|
size="1",
|
|
padding_x="0.75rem",
|
|
padding_y="0.4rem",
|
|
color=_TEXT_MUTED,
|
|
_hover={"background_color": _HOVER_BG},
|
|
cursor="pointer",
|
|
),
|
|
rx.cond(
|
|
AuthState.admin_expanded,
|
|
rx.vstack(
|
|
*[
|
|
_nav_full(l, h, i, close_menu=mobile)
|
|
for l, h, i in _ADMIN_PAGES
|
|
],
|
|
spacing="1", width="100%",
|
|
),
|
|
),
|
|
spacing="0", width="100%",
|
|
padding_x="0.75rem", padding_y="0.25rem",
|
|
),
|
|
),
|
|
spacing="0", width="100%",
|
|
),
|
|
)
|
|
|
|
|
|
def _avatar_or_photo(size: str = "2") -> rx.Component:
|
|
img_size = "32px" if size == "2" else "28px"
|
|
return rx.cond(
|
|
AuthState.photo_url != "",
|
|
rx.image(
|
|
src=AuthState.photo_url,
|
|
width=img_size,
|
|
height=img_size,
|
|
border_radius="50%",
|
|
object_fit="cover",
|
|
border="1.5px solid var(--gray-5)",
|
|
flex_shrink="0",
|
|
),
|
|
rx.image(
|
|
src="/default_avatar.svg",
|
|
width=img_size,
|
|
height=img_size,
|
|
border_radius="50%",
|
|
flex_shrink="0",
|
|
),
|
|
)
|
|
|
|
|
|
def _user_widget(collapsed: bool = False) -> rx.Component:
|
|
if collapsed:
|
|
return rx.tooltip(
|
|
rx.vstack(
|
|
_avatar_or_photo(size="2"),
|
|
rx.icon_button(
|
|
rx.icon("log-out", size=14),
|
|
on_click=AuthState.logout,
|
|
variant="ghost", size="1", cursor="pointer",
|
|
),
|
|
spacing="2", align="center", width="100%",
|
|
),
|
|
content=AuthState.name,
|
|
side="right",
|
|
)
|
|
return rx.hstack(
|
|
_avatar_or_photo(size="2"),
|
|
rx.vstack(
|
|
rx.text(AuthState.name, size="2", font_weight="600",
|
|
color=_TEXT, white_space="nowrap", overflow="hidden"),
|
|
rx.text(AuthState.role, size="1", color=_TEXT_MUTED),
|
|
spacing="0", align="start", overflow="hidden", flex="1",
|
|
),
|
|
rx.icon_button(
|
|
rx.icon("log-out", size=14),
|
|
on_click=AuthState.logout,
|
|
variant="ghost", size="1", cursor="pointer",
|
|
),
|
|
spacing="2", align="center", width="100%", overflow="hidden",
|
|
)
|
|
|
|
|
|
# ── Desktop sidebar ──────────────────────────────────────────────────────────
|
|
|
|
def sidebar() -> rx.Component:
|
|
return rx.box(
|
|
rx.vstack(
|
|
# Header: logo + toggle
|
|
rx.hstack(
|
|
rx.cond(
|
|
AuthState.sidebar_collapsed,
|
|
rx.box(flex="1"),
|
|
rx.box(
|
|
rx.image(src="/logo.png", height="112px",
|
|
object_fit="contain", max_width="160px", width="100%"),
|
|
flex="1",
|
|
display="flex",
|
|
align_items="center",
|
|
justify_content="center",
|
|
min_width="0",
|
|
),
|
|
),
|
|
rx.icon_button(
|
|
rx.cond(
|
|
AuthState.sidebar_collapsed,
|
|
rx.icon("panel-left-open", size=16),
|
|
rx.icon("panel-left-close", size=16),
|
|
),
|
|
on_click=AuthState.toggle_sidebar,
|
|
variant="ghost", size="2",
|
|
color=_TEXT, cursor="pointer", flex_shrink="0",
|
|
),
|
|
width="100%", align="center",
|
|
padding_y="0.75rem",
|
|
padding_x=rx.cond(AuthState.sidebar_collapsed, "0.5rem", "0.75rem"),
|
|
),
|
|
|
|
rx.box(height="1px", width="100%", background_color=_BORDER),
|
|
|
|
# Nav
|
|
rx.vstack(
|
|
*[_nav_item(l, h, i) for l, h, i in _PAGES],
|
|
spacing="1", width="100%",
|
|
padding_x=rx.cond(AuthState.sidebar_collapsed, "0.5rem", "0"),
|
|
padding_y="0.5rem",
|
|
),
|
|
|
|
_admin_section(),
|
|
rx.spacer(),
|
|
|
|
# User
|
|
rx.box(height="1px", width="100%", background_color=_BORDER),
|
|
rx.box(
|
|
rx.cond(
|
|
AuthState.sidebar_collapsed,
|
|
_user_widget(collapsed=True),
|
|
_user_widget(collapsed=False),
|
|
),
|
|
padding_y="0.75rem",
|
|
padding_x=rx.cond(AuthState.sidebar_collapsed, "0.5rem", "0.75rem"),
|
|
width="100%",
|
|
background_color=_USER_BG,
|
|
),
|
|
|
|
height="100vh", width="100%",
|
|
spacing="0", align="start",
|
|
overflow_y="auto", overflow_x="hidden",
|
|
),
|
|
class_name="sidebar-desktop",
|
|
background_color=_BG,
|
|
border_right=f"1px solid {_BORDER}",
|
|
position="fixed",
|
|
left="0", top="0",
|
|
height="100vh",
|
|
width=rx.cond(AuthState.sidebar_collapsed, RAIL_W, FULL_W),
|
|
transition="width 0.22s ease",
|
|
z_index="100",
|
|
overflow="hidden",
|
|
)
|
|
|
|
|
|
# ── Mobile top bar ───────────────────────────────────────────────────────────
|
|
|
|
def _mobile_topbar() -> rx.Component:
|
|
return rx.box(
|
|
# Bar row
|
|
rx.hstack(
|
|
rx.box(
|
|
rx.image(src="/logo.png", height="40px", object_fit="contain"),
|
|
background_color="white",
|
|
border_radius="5px",
|
|
padding="4px 8px",
|
|
display="flex",
|
|
align_items="center",
|
|
justify_content="center",
|
|
),
|
|
rx.spacer(),
|
|
rx.icon_button(
|
|
rx.cond(
|
|
AuthState.mobile_menu_open,
|
|
rx.icon("x", size=20),
|
|
rx.icon("menu", size=20),
|
|
),
|
|
on_click=AuthState.toggle_mobile_menu,
|
|
variant="ghost", size="2",
|
|
color=_TEXT, cursor="pointer",
|
|
),
|
|
width="100%", align="center",
|
|
padding_x="1rem",
|
|
height=TOPBAR_H,
|
|
),
|
|
|
|
# Dropdown
|
|
rx.cond(
|
|
AuthState.mobile_menu_open,
|
|
rx.box(
|
|
rx.vstack(
|
|
rx.box(height="1px", width="100%", background_color=_BORDER),
|
|
rx.vstack(
|
|
*[_nav_full(l, h, i, close_menu=True) for l, h, i in _PAGES],
|
|
spacing="1", width="100%",
|
|
padding_x="0", padding_y="0.5rem",
|
|
),
|
|
_admin_section(mobile=True),
|
|
rx.box(height="1px", width="100%", background_color=_BORDER),
|
|
rx.box(
|
|
_user_widget(collapsed=False),
|
|
padding_x="0.75rem", padding_y="0.65rem",
|
|
background_color=_USER_BG, width="100%",
|
|
),
|
|
spacing="0", width="100%",
|
|
),
|
|
background_color=_BG,
|
|
width="100%",
|
|
max_height=f"calc(100vh - {TOPBAR_H})",
|
|
overflow_y="auto",
|
|
),
|
|
),
|
|
|
|
class_name="topbar-mobile",
|
|
background_color=_BG,
|
|
border_bottom=f"1px solid {_BORDER}",
|
|
position="fixed",
|
|
top="0", left="0", right="0",
|
|
width="100%",
|
|
z_index="200",
|
|
)
|
|
|
|
|
|
# ── Layout wrapper ───────────────────────────────────────────────────────────
|
|
|
|
def layout(content: rx.Component) -> rx.Component:
|
|
return rx.box(
|
|
sidebar(),
|
|
_mobile_topbar(),
|
|
rx.box(
|
|
content,
|
|
class_name="content-area",
|
|
padding=rx.cond(AuthState.sidebar_collapsed, "1rem", "1.5rem"),
|
|
background_color="var(--gray-2)",
|
|
margin_left=rx.cond(AuthState.sidebar_collapsed, RAIL_W, FULL_W),
|
|
width=rx.cond(
|
|
AuthState.sidebar_collapsed,
|
|
f"calc(100% - {RAIL_W})",
|
|
f"calc(100% - {FULL_W})",
|
|
),
|
|
max_width=rx.cond(
|
|
AuthState.sidebar_collapsed,
|
|
f"calc(100% - {RAIL_W})",
|
|
f"calc(100% - {FULL_W})",
|
|
),
|
|
overflow_x="hidden",
|
|
transition="margin-left 0.22s ease, width 0.22s ease",
|
|
box_sizing="border-box",
|
|
),
|
|
width="100%",
|
|
height="100vh",
|
|
overflow="hidden",
|
|
)
|