main layout ok
This commit is contained in:
parent
d812eabdbd
commit
360e8e02a7
10 changed files with 565 additions and 128 deletions
BIN
assets/favicon.png
Normal file
BIN
assets/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
58
assets/favicon.svg
Normal file
58
assets/favicon.svg
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<clipPath id="rnd">
|
||||
<rect width="512" height="512" rx="108"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<!-- Fond noir -->
|
||||
<rect width="512" height="512" rx="108" fill="#1A1A1A"/>
|
||||
|
||||
<!-- Bandeau rouge droite -->
|
||||
<rect x="346" y="0" width="166" height="512" fill="#CC1111" clip-path="url(#rnd)"/>
|
||||
|
||||
<!-- Toit diagonal style EPTM -->
|
||||
<polygon points="300,0 346,0 346,96 300,60" fill="#1A1A1A" clip-path="url(#rnd)"/>
|
||||
|
||||
<!-- Calendrier — corps -->
|
||||
<rect x="64" y="112" width="262" height="214" rx="20" fill="white" fill-opacity="0.07" stroke="white" stroke-opacity="0.25" stroke-width="2.5"/>
|
||||
|
||||
<!-- Calendrier — en-tête -->
|
||||
<rect x="64" y="112" width="262" height="62" rx="20" fill="white" fill-opacity="0.12"/>
|
||||
<rect x="64" y="150" width="262" height="24" fill="white" fill-opacity="0.07"/>
|
||||
|
||||
<!-- Anneaux -->
|
||||
<rect x="138" y="82" width="18" height="52" rx="9" fill="white" fill-opacity="0.7"/>
|
||||
<rect x="238" y="82" width="18" height="52" rx="9" fill="white" fill-opacity="0.7"/>
|
||||
|
||||
<!-- Titre dans l'en-tête -->
|
||||
<rect x="104" y="128" width="76" height="9" rx="4" fill="white" fill-opacity="0.45"/>
|
||||
|
||||
<!-- Grille ligne 1 -->
|
||||
<circle cx="136" cy="234" r="18" fill="#22C55E"/>
|
||||
<line x1="126" y1="234" x2="133" y2="242" stroke="white" stroke-width="4" stroke-linecap="round"/>
|
||||
<line x1="133" y1="242" x2="148" y2="225" stroke="white" stroke-width="4" stroke-linecap="round"/>
|
||||
|
||||
<circle cx="196" cy="234" r="18" fill="#22C55E"/>
|
||||
<line x1="186" y1="234" x2="193" y2="242" stroke="white" stroke-width="4" stroke-linecap="round"/>
|
||||
<line x1="193" y1="242" x2="208" y2="225" stroke="white" stroke-width="4" stroke-linecap="round"/>
|
||||
|
||||
<circle cx="256" cy="234" r="18" fill="#CC1111"/>
|
||||
<line x1="246" y1="224" x2="266" y2="244" stroke="white" stroke-width="4" stroke-linecap="round"/>
|
||||
<line x1="266" y1="224" x2="246" y2="244" stroke="white" stroke-width="4" stroke-linecap="round"/>
|
||||
|
||||
<!-- Grille ligne 2 -->
|
||||
<circle cx="136" cy="294" r="18" fill="#F59E0B"/>
|
||||
<text x="136" y="301" text-anchor="middle" font-family="system-ui,sans-serif" font-weight="800" font-size="20" fill="white">E</text>
|
||||
|
||||
<circle cx="196" cy="294" r="18" fill="#22C55E"/>
|
||||
<line x1="186" y1="294" x2="193" y2="302" stroke="white" stroke-width="4" stroke-linecap="round"/>
|
||||
<line x1="193" y1="302" x2="208" y2="285" stroke="white" stroke-width="4" stroke-linecap="round"/>
|
||||
|
||||
<circle cx="256" cy="294" r="18" fill="#CC1111"/>
|
||||
<line x1="246" y1="284" x2="266" y2="304" stroke="white" stroke-width="4" stroke-linecap="round"/>
|
||||
<line x1="266" y1="284" x2="246" y2="304" stroke="white" stroke-width="4" stroke-linecap="round"/>
|
||||
|
||||
<!-- Texte EPTM — un seul bloc pour espacement uniforme, M tombe sur le rouge -->
|
||||
<text x="118" y="432" text-anchor="start" font-family="Arial,system-ui,sans-serif" font-weight="900" font-size="108" letter-spacing="-2" fill="white">EPTM</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
64
assets/responsive.css
Normal file
64
assets/responsive.css
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/* Reset default margins and padding */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body, html {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#root, #__next {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* App shell: viewport-bounded with internal scroll on content */
|
||||
.content-area {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Mobile: hide desktop sidebar, account for fixed topbar (56px) */
|
||||
@media (max-width: 767px) {
|
||||
.sidebar-desktop { display: none !important; }
|
||||
.content-area {
|
||||
margin-left: 0 !important;
|
||||
width: 100% !important;
|
||||
padding-top: calc(56px + 0.75rem) !important;
|
||||
padding-left: 0.75rem !important;
|
||||
padding-right: 0.75rem !important;
|
||||
padding-bottom: 0.75rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet */
|
||||
@media (min-width: 768px) and (max-width: 1024px) {
|
||||
.content-area {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Desktop: hide mobile topbar */
|
||||
@media (min-width: 768px) {
|
||||
.topbar-mobile { display: none !important; }
|
||||
}
|
||||
|
||||
/* Ensure responsive images and content */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Ensure flex containers wrap properly */
|
||||
.content-area > * {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
|
@ -9,8 +9,10 @@ services:
|
|||
- "8001:8001"
|
||||
volumes:
|
||||
- ./eptm_dashboard:/app/eptm_dashboard
|
||||
- ./rxconfig.py:/app/rxconfig.py
|
||||
- ./data:/data
|
||||
- ./logs:/logs
|
||||
- ./assets:/app/assets
|
||||
env_file:
|
||||
- .env.prod
|
||||
environment:
|
||||
|
|
|
|||
|
|
@ -12,16 +12,23 @@ from .pages.logs import logs_page
|
|||
from .pages.users import users_page
|
||||
from .pages.params import params_page
|
||||
|
||||
app = rx.App()
|
||||
TITLE = "EPTM Dashboard"
|
||||
|
||||
app.add_page(login_page, route="/login")
|
||||
app.add_page(accueil_page, route="/accueil", on_load=AccueilState.load_data)
|
||||
app.add_page(traiter_page, route="/traiter", on_load=AuthState.check_auth)
|
||||
app.add_page(fiche_page, route="/fiche", on_load=FicheState.load_data)
|
||||
app.add_page(classe_page, route="/classe", on_load=AuthState.check_auth)
|
||||
app.add_page(import_page_page, route="/import", on_load=AuthState.check_auth)
|
||||
app.add_page(escada_page, route="/escada", on_load=AuthState.check_auth)
|
||||
app.add_page(export_page, route="/export", on_load=AuthState.check_auth)
|
||||
app.add_page(logs_page, route="/logs", on_load=AuthState.check_auth)
|
||||
app.add_page(users_page, route="/users", on_load=AuthState.check_auth)
|
||||
app.add_page(params_page, route="/params", on_load=AuthState.check_auth)
|
||||
app = rx.App(
|
||||
stylesheets=["/responsive.css"],
|
||||
head_components=[
|
||||
rx.el.link(rel="icon", type="image/png", href="/favicon.png"),
|
||||
],
|
||||
)
|
||||
|
||||
app.add_page(login_page, route="/login", title=TITLE)
|
||||
app.add_page(accueil_page, route="/accueil", on_load=AccueilState.load_data, title=TITLE)
|
||||
app.add_page(traiter_page, route="/traiter", on_load=AuthState.check_auth, title=TITLE)
|
||||
app.add_page(fiche_page, route="/fiche", on_load=FicheState.load_data, title=TITLE)
|
||||
app.add_page(classe_page, route="/classe", on_load=AuthState.check_auth, title=TITLE)
|
||||
app.add_page(import_page_page, route="/import", on_load=AuthState.check_auth, title=TITLE)
|
||||
app.add_page(escada_page, route="/escada", on_load=AuthState.check_auth, title=TITLE)
|
||||
app.add_page(export_page, route="/export", on_load=AuthState.check_auth, title=TITLE)
|
||||
app.add_page(logs_page, route="/logs", on_load=AuthState.check_auth, title=TITLE)
|
||||
app.add_page(users_page, route="/users", on_load=AuthState.check_auth, title=TITLE)
|
||||
app.add_page(params_page, route="/params", on_load=AuthState.check_auth, title=TITLE)
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@ def _kpi_card(label: str, value: rx.Var) -> rx.Component:
|
|||
border_radius="8px",
|
||||
padding="0.75rem 1rem",
|
||||
flex="1",
|
||||
min_width="120px",
|
||||
min_width="80px",
|
||||
width="100%",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -105,11 +106,13 @@ def accueil_page() -> rx.Component:
|
|||
spacing="3",
|
||||
width="100%",
|
||||
wrap="wrap",
|
||||
align_items="stretch",
|
||||
),
|
||||
|
||||
rx.divider(),
|
||||
|
||||
rx.heading("🚨 Avis de sanction — quota atteint", size="5"),
|
||||
rx.box(
|
||||
rx.cond(
|
||||
AccueilState.sanctions.length() == 0,
|
||||
rx.box(
|
||||
|
|
@ -119,6 +122,7 @@ def accueil_page() -> rx.Component:
|
|||
border="1px solid #c8e6c9",
|
||||
border_radius="6px",
|
||||
padding="0.75rem 1rem",
|
||||
width="100%",
|
||||
),
|
||||
rx.vstack(
|
||||
rx.box(
|
||||
|
|
@ -136,6 +140,8 @@ def accueil_page() -> rx.Component:
|
|||
spacing="1",
|
||||
),
|
||||
),
|
||||
width="100%",
|
||||
),
|
||||
|
||||
rx.divider(),
|
||||
|
||||
|
|
@ -147,10 +153,12 @@ def accueil_page() -> rx.Component:
|
|||
border="1px solid #90caf9",
|
||||
border_radius="6px",
|
||||
padding="0.75rem 1rem",
|
||||
width="100%",
|
||||
),
|
||||
|
||||
spacing="5",
|
||||
width="100%",
|
||||
max_width="100%",
|
||||
align="start",
|
||||
padding_bottom="2rem",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ from ..state import AuthState
|
|||
|
||||
def login_page() -> rx.Component:
|
||||
return rx.center(
|
||||
rx.form(
|
||||
rx.vstack(
|
||||
rx.image(src="/logo.png", width="160px"),
|
||||
rx.heading("EPTM Dashboard", size="5", color="#37474f"),
|
||||
rx.center(
|
||||
rx.image(src="/logo.png", width="320px", height="auto"),
|
||||
width="100%",
|
||||
),
|
||||
rx.cond(
|
||||
AuthState.login_error != "",
|
||||
rx.box(
|
||||
|
|
@ -19,12 +22,14 @@ def login_page() -> rx.Component:
|
|||
),
|
||||
),
|
||||
rx.input(
|
||||
name="username",
|
||||
placeholder="Identifiant",
|
||||
value=AuthState.login_user,
|
||||
on_change=AuthState.set_login_user,
|
||||
width="100%",
|
||||
),
|
||||
rx.input(
|
||||
name="password",
|
||||
placeholder="Mot de passe",
|
||||
type="password",
|
||||
value=AuthState.login_pass,
|
||||
|
|
@ -33,17 +38,22 @@ def login_page() -> rx.Component:
|
|||
),
|
||||
rx.button(
|
||||
"Se connecter",
|
||||
on_click=AuthState.handle_login,
|
||||
type="submit",
|
||||
width="100%",
|
||||
color_scheme="indigo",
|
||||
),
|
||||
spacing="3",
|
||||
width="350px",
|
||||
width="100%",
|
||||
align="center",
|
||||
),
|
||||
on_submit=AuthState.handle_login,
|
||||
width="420px",
|
||||
padding="2rem",
|
||||
background_color="white",
|
||||
border_radius="8px",
|
||||
box_shadow="0 2px 16px rgba(0,0,0,0.08)",
|
||||
),
|
||||
width="100%",
|
||||
height="100vh",
|
||||
background_color="#f8f9fa",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,23 @@
|
|||
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", "house"),
|
||||
("À traiter", "/traiter", "triangle-alert"),
|
||||
("Tableau de bord", "/accueil", "layout-dashboard"),
|
||||
("A traiter", "/traiter", "triangle-alert"),
|
||||
("Fiche apprenti", "/fiche", "user"),
|
||||
("Vue classe", "/classe", "users"),
|
||||
("Import", "/import", "upload"),
|
||||
|
|
@ -14,109 +28,348 @@ _PAGES = [
|
|||
_ADMIN_PAGES = [
|
||||
("Logs", "/logs", "file-text"),
|
||||
("Utilisateurs", "/users", "user-cog"),
|
||||
("Paramètres", "/params", "settings"),
|
||||
("Parametres", "/params", "settings"),
|
||||
]
|
||||
|
||||
|
||||
def _nav_item(label: str, href: str, icon: str) -> rx.Component:
|
||||
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.icon(icon, size=16, color="#9e9e9e"),
|
||||
rx.text(label, size="2", color="#555555"),
|
||||
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",
|
||||
padding_x="1rem",
|
||||
padding_y="0.55rem",
|
||||
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,
|
||||
width="100%",
|
||||
on_click=click_handler,
|
||||
text_decoration="none",
|
||||
_hover={"background_color": "#f8f9fa"},
|
||||
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 _user_widget(collapsed: bool = False) -> rx.Component:
|
||||
if collapsed:
|
||||
return rx.tooltip(
|
||||
rx.vstack(
|
||||
rx.avatar(fallback=AuthState.name_initials, size="2",
|
||||
color_scheme="ruby", radius="full"),
|
||||
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(
|
||||
rx.avatar(fallback=AuthState.name_initials, size="2",
|
||||
color_scheme="ruby", radius="full"),
|
||||
rx.vstack(
|
||||
rx.text(AuthState.name, size="2", font_weight="600",
|
||||
color="#f3f4f6", 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", width="140px"),
|
||||
text_align="center",
|
||||
padding="1rem",
|
||||
border_bottom="1px solid #dee2e6",
|
||||
width="100%",
|
||||
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="0",
|
||||
width="100%",
|
||||
spacing="1", width="100%",
|
||||
padding_x=rx.cond(AuthState.sidebar_collapsed, "0.5rem", "0"),
|
||||
padding_y="0.5rem",
|
||||
),
|
||||
rx.cond(
|
||||
AuthState.role == "admin",
|
||||
rx.vstack(
|
||||
|
||||
_admin_section(),
|
||||
rx.spacer(),
|
||||
|
||||
# User
|
||||
rx.box(height="1px", width="100%", background_color=_BORDER),
|
||||
rx.box(
|
||||
rx.text(
|
||||
"ADMIN", size="1", color="#9e9e9e",
|
||||
font_weight="700", letter_spacing="0.12em",
|
||||
rx.cond(
|
||||
AuthState.sidebar_collapsed,
|
||||
_user_widget(collapsed=True),
|
||||
_user_widget(collapsed=False),
|
||||
),
|
||||
padding_x="1rem",
|
||||
padding_top="0.75rem",
|
||||
padding_bottom="0.25rem",
|
||||
),
|
||||
*[_nav_item(l, h, i) for l, h, i in _ADMIN_PAGES],
|
||||
spacing="0",
|
||||
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.text(AuthState.name, size="2", font_weight="600", color="#37474f"),
|
||||
rx.text(AuthState.role, size="1", color="#9e9e9e"),
|
||||
rx.button(
|
||||
"↩ Déconnexion",
|
||||
on_click=AuthState.logout,
|
||||
size="1",
|
||||
variant="outline",
|
||||
color_scheme="gray",
|
||||
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",
|
||||
),
|
||||
spacing="2",
|
||||
width="100%",
|
||||
),
|
||||
padding="1rem",
|
||||
border_top="1px solid #dee2e6",
|
||||
width="100%",
|
||||
),
|
||||
height="100vh",
|
||||
width="240px",
|
||||
spacing="0",
|
||||
align="start",
|
||||
),
|
||||
background_color="white",
|
||||
border_right="1px solid #dee2e6",
|
||||
|
||||
class_name="topbar-mobile",
|
||||
background_color=_BG,
|
||||
border_bottom=f"1px solid {_BORDER}",
|
||||
position="fixed",
|
||||
left="0",
|
||||
top="0",
|
||||
height="100vh",
|
||||
z_index="100",
|
||||
top="0", left="0", right="0",
|
||||
width="100%",
|
||||
z_index="200",
|
||||
)
|
||||
|
||||
|
||||
# ── Layout wrapper ───────────────────────────────────────────────────────────
|
||||
|
||||
def layout(content: rx.Component) -> rx.Component:
|
||||
return rx.hstack(
|
||||
return rx.box(
|
||||
sidebar(),
|
||||
_mobile_topbar(),
|
||||
rx.box(
|
||||
content,
|
||||
margin_left="240px",
|
||||
padding="2rem",
|
||||
width="100%",
|
||||
min_height="100vh",
|
||||
background_color="#f8f9fa",
|
||||
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})",
|
||||
),
|
||||
transition="margin-left 0.22s ease, width 0.22s ease",
|
||||
box_sizing="border-box",
|
||||
),
|
||||
spacing="0",
|
||||
align="start",
|
||||
width="100%",
|
||||
height="100vh",
|
||||
overflow="hidden",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,31 @@ class AuthState(rx.State):
|
|||
login_pass: str = ""
|
||||
login_error: str = ""
|
||||
|
||||
sidebar_collapsed: bool = False
|
||||
mobile_menu_open: bool = False
|
||||
admin_expanded: bool = True
|
||||
|
||||
@rx.var
|
||||
def name_initials(self) -> str:
|
||||
if not self.name:
|
||||
return "?"
|
||||
parts = self.name.split()
|
||||
if len(parts) >= 2:
|
||||
return (parts[0][0] + parts[1][0]).upper()
|
||||
return self.name[:2].upper()
|
||||
|
||||
def toggle_sidebar(self):
|
||||
self.sidebar_collapsed = not self.sidebar_collapsed
|
||||
|
||||
def toggle_mobile_menu(self):
|
||||
self.mobile_menu_open = not self.mobile_menu_open
|
||||
|
||||
def close_mobile_menu(self):
|
||||
self.mobile_menu_open = False
|
||||
|
||||
def toggle_admin(self):
|
||||
self.admin_expanded = not self.admin_expanded
|
||||
|
||||
def set_login_user(self, value: str):
|
||||
self.login_user = value
|
||||
|
||||
|
|
@ -27,7 +52,7 @@ class AuthState(rx.State):
|
|||
if not self.authenticated:
|
||||
return rx.redirect("/login")
|
||||
|
||||
def handle_login(self):
|
||||
def handle_login(self, form_data: dict | None = None):
|
||||
self.login_error = ""
|
||||
users = self._load_users()
|
||||
user = users.get(self.login_user)
|
||||
|
|
|
|||
10
rxconfig.py
10
rxconfig.py
|
|
@ -8,5 +8,15 @@ config = rx.Config(
|
|||
frontend_port=int(os.getenv("FRONTEND_PORT", "3000")),
|
||||
backend_port=int(os.getenv("BACKEND_PORT", "8000")),
|
||||
vite_allowed_hosts=True,
|
||||
plugins=[
|
||||
rx.plugins.RadixThemesPlugin(
|
||||
theme=rx.theme(
|
||||
appearance="inherit",
|
||||
accent_color="red",
|
||||
radius="medium",
|
||||
scaling="95%",
|
||||
)
|
||||
),
|
||||
],
|
||||
disable_plugins=[SitemapPlugin],
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue