first commit

This commit is contained in:
OnlyPapy98
2025-12-16 14:20:02 +01:00
commit dde2e8aebf
320 changed files with 30462 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
:host {
display: contents;
}

View File

@@ -0,0 +1,182 @@
<z-layout class="border overflow-hidden bg-pmu-vert min-h-screen h-full">
<z-sidebar
[zWidth]="250"
[zCollapsible]="true"
[zCollapsed]="sidebarCollapsed()"
[zCollapsedWidth]="70"
(zCollapsedChange)="onCollapsedChange($event)"
class="!p-0 dark:!bg-pmu-vert/10 !bg-surface"
>
<nav
[class]="
'flex flex-col h-full overflow-hidden ' +
(sidebarCollapsed() ? 'gap-1 p-1 pt-4' : 'gap-4 p-4')
"
>
<div class="flex items-center justify-center">
<app-pmu-logo></app-pmu-logo>
</div>
<z-sidebar-group>
@if (!sidebarCollapsed()) {
<z-sidebar-group-label>Menu principal</z-sidebar-group-label>
} @for (item of mainMenuItems; track item.label) {
<button
z-button
zType="ghost"
[class]="
(sidebarCollapsed() ? 'justify-center' : 'justify-start') +
(isActive(item.link || '', item.exact || false)
? ' !bg-primary/10 !text-primary'
: ' hover:bg-accent')
"
[zTooltip]="sidebarCollapsed() ? item.label : ''"
zPosition="right"
(click)="navigate(item?.link)"
>
@if (isEmoji(item.icon)) {
<span class="text-lg">{{ item.icon }}</span>
} @else {
<i [class]="item.icon + (sidebarCollapsed() ? '' : ' mr-2')" aria-hidden="true"></i>
} @if (!sidebarCollapsed()) {
<span>{{ item.label }}</span>
}
</button>
}
</z-sidebar-group>
<z-sidebar-group>
@if (!sidebarCollapsed()) {
<z-sidebar-group-label>Agents & Utilisateurs</z-sidebar-group-label>
} @for (item of workspaceMenuItems; track item.label) { @if (item.submenu) {
<button
z-button
zType="ghost"
z-menu
[zMenuTriggerFor]="submenu"
zPlacement="rightTop"
[class]="
(sidebarCollapsed() ? 'justify-center' : 'justify-start') +
(isAnyActive(item) ? ' !bg-primary/10 !text-primary' : ' hover:bg-accent')
"
[zTooltip]="sidebarCollapsed() ? item.label : null"
zPosition="right"
>
<i [class]="sidebarCollapsed() ? item.icon : item.icon + ' mr-2'"></i>
@if (!sidebarCollapsed()) {
<span class="flex-1 text-left">{{ item.label }}</span>
<i class="icon-chevron-right"></i>
}
</button>
<ng-template #submenu>
<div z-menu-content class="w-48">
@for (subitem of item.submenu; track subitem.label) {
<button
z-menu-item
[class]="isActive(subitem.link || '', subitem.exact || false) ? '!text-primary' : ''"
(click)="navigate(subitem.link)"
>
{{ subitem.label }}
</button>
}
</div>
</ng-template>
} @else {
<button
z-button
zType="ghost"
[class]="
(sidebarCollapsed() ? 'justify-center' : 'justify-start') +
(isActive(item.link || '', item.exact || false)
? ' !bg-primary/10 !text-primary'
: ' hover:bg-accent')
"
[zTooltip]="sidebarCollapsed() ? item.label : ''"
zPosition="right"
(click)="navigate(item?.link)"
>
<i [class]="sidebarCollapsed() ? item.icon : item.icon + ' mr-2'"></i>
@if (!sidebarCollapsed()) {
<span>{{ item.label }}</span>
}
</button>
} }
</z-sidebar-group>
<div class="mt-auto">
<div
z-menu
[zMenuTriggerFor]="userMenu"
zPlacement="rightBottom"
[class]="
'flex items-center justify-center gap-2 cursor-pointer rounded-md hover:bg-accent ' +
(sidebarCollapsed() ? 'p-0 m-2' : 'p-2')
"
>
<z-avatar zSize="sm" [zImage]="avatar" />
@if (!sidebarCollapsed()) {
<div>
<span class="font-medium text-sm truncate">{{ user()?.nom }} {{ user()?.prenom }}</span>
<div class="text-xs">{{ user()?.identifiant }}</div>
</div>
<i class="icon-chevrons-up-down ml-auto"></i>
}
</div>
<ng-template #userMenu>
<div z-menu-content class="w-48">
<button z-menu-item (click)="navigate('/profile')">
<i class="icon-user mr-2"></i>
Profile
</button>
<z-divider zSpacing="sm" />
<button z-menu-item (click)="logout()">
<i class="icon-log-out mr-2"></i>
Déconnexion
</button>
</div>
</ng-template>
</div>
</nav>
</z-sidebar>
<!-- min-h-[200px] is just for the demo purpose to have a minimum height -->
<z-content class="min-h-screen">
<div class="flex items-center">
<button
z-button
zType="ghost"
zSize="sm"
class="-ml-2 dark:text-white text-black"
(click)="toggleSidebar()"
>
<i class="icon-panel-left"></i>
</button>
<z-divider zOrientation="vertical" class="h-4 ml-2" />
<z-breadcrumb>
<z-breadcrumb-list zWrap="wrap" zAlign="start">
<z-breadcrumb-item>
<z-breadcrumb-link zLink="/docs/components/layout">Home</z-breadcrumb-link>
</z-breadcrumb-item>
<z-breadcrumb-separator />
<z-breadcrumb-item>
<z-breadcrumb-link zLink="/docs/components/layout">Components</z-breadcrumb-link>
</z-breadcrumb-item>
</z-breadcrumb-list>
</z-breadcrumb>
<div class="ml-auto flex justify-end items-center">
<app-mode-toggle></app-mode-toggle>
</div>
</div>
<div class="space-y-4 py-4 text-text">
<router-outlet></router-outlet>
</div>
</z-content>
</z-layout>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Layout } from './layout';
describe('Layout', () => {
let component: Layout;
let fixture: ComponentFixture<Layout>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Layout]
})
.compileComponents();
fixture = TestBed.createComponent(Layout);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,119 @@
import { CommonModule } from '@angular/common';
import { Component, signal } from '@angular/core';
import { Router, RouterModule } from '@angular/router';
import { ZardAvatarComponent } from '@shared/components/avatar/avatar.component';
import { ZardBreadcrumbModule } from '@shared/components/breadcrumb/breadcrumb.module';
import { ZardButtonComponent } from '@shared/components/button/button.component';
import { ZardDividerComponent } from '@shared/components/divider/divider.component';
import { LayoutModule } from '@shared/components/layout/layout.module';
import { ZardMenuModule } from '@shared/components/menu/menu.module';
import { ZardTooltipModule } from '@shared/components/tooltip/tooltip';
import { MenuItem } from 'src/app/core/interfaces/menu-item';
import { Theme } from 'src/app/core/services/theme';
import { ModeToggle } from '@shared/components/mode-toggle/mode-toggle';
import { PmuLogo } from '@shared/components/pmu-logo/pmu-logo';
import { User } from 'src/app/core/interfaces/user';
import { Auth } from 'src/app/core/services/auth';
@Component({
selector: 'app-layout',
imports: [
CommonModule,
RouterModule,
LayoutModule,
ZardButtonComponent,
ZardBreadcrumbModule,
ZardMenuModule,
ZardTooltipModule,
ZardDividerComponent,
ZardAvatarComponent,
ModeToggle,
PmuLogo,
],
templateUrl: './layout.html',
styleUrl: './layout.css',
})
export class Layout {
sidebarCollapsed = signal(false);
user = signal<User | null>(null);
constructor(public theme: Theme, public auth: Auth, public router: Router) {
this.user.set(auth.getUser());
}
mainMenuItems: MenuItem[] = [
{ icon: '🏠', label: 'Tableau de bord', link: '/', exact: true },
{ icon: '🏟️', label: 'Hippodromes', link: '/hippodromes' },
{ icon: '📅', label: 'Reunions', link: '/reunions' },
{ icon: '🏇', label: 'Courses', link: '/courses' },
{ icon: 'icon-chart-bar', label: 'Rapport des courses', link: '/rapport-courses' },
];
workspaceMenuItems: MenuItem[] = [
{
icon: 'icon-folder',
label: 'Gestion Agents',
submenu: [
{ icon: 'icon-user-plus', label: 'Gestion Agents', link: '/agents' },
{ icon: 'icon-sliders', label: 'Gestion limites Agents', link: '/limits' },
],
},
{ icon: 'icon-monitor', label: 'Gestion des TPE', link: '/tpes' },
{
icon: 'icon-users',
label: 'Utilisateurs',
submenu: [
{ icon: 'icon-users', label: 'Liste des utilisateurs', link: '/users' },
{ icon: 'icon-shield', label: 'Rôles & Permissions', link: '/roles' },
],
},
];
avatar = {
fallback: 'ZA',
url: '/assets/images/avatar.svg',
alt: 'ZadUI',
};
toggleSidebar() {
this.sidebarCollapsed.update((collapsed) => !collapsed);
}
onCollapsedChange(collapsed: boolean) {
this.sidebarCollapsed.set(collapsed);
}
toggleTheme() {
this.theme.toggle();
}
async logout() {
this.auth.logout();
await this.router.navigateByUrl('/auth/login');
}
navigate(link: string | undefined) {
if (link) {
this.router.navigateByUrl(link);
}
}
isActive(link: string, exact = false): boolean {
const current = this.router.url;
return exact ? current === link : current.startsWith(link);
}
isAnyActive(item: MenuItem): boolean {
if (item.link && this.isActive(item.link, !!item.exact)) return true;
if (item.submenu && item.submenu.length) {
return item.submenu.some((s) => !!s.link && this.isActive(s.link!, !!s.exact));
}
return false;
}
isEmoji(icon: string): boolean {
// simple: emojis are not alphanumeric or typical class names
// Detect if it contains non-ASCII characters
return /[^\u0000-\u00ff]/.test(icon);
}
}