From f181943279d8741cb2b9ab3a0d9b1a9a3b42cdc3 Mon Sep 17 00:00:00 2001 From: horacioskrp Date: Wed, 2 Sep 2026 15:46:51 +0000 Subject: [PATCH 1/4] fix(nav): garde le menu actif sur les sous-pages, avec correspondance la plus specifique --- resources/js/components/app-header.tsx | 13 ++- .../js/components/app-sidebar-header.tsx | 16 ++- resources/js/components/nav-main.tsx | 28 ++++-- resources/js/hooks/use-current-url.ts | 97 +++++++++++++------ resources/js/layouts/settings/layout.tsx | 7 +- 5 files changed, 111 insertions(+), 50 deletions(-) diff --git a/resources/js/components/app-header.tsx b/resources/js/components/app-header.tsx index ff8d128..5f6a8ef 100644 --- a/resources/js/components/app-header.tsx +++ b/resources/js/components/app-header.tsx @@ -28,7 +28,7 @@ import { TooltipTrigger, } from '@/components/ui/tooltip'; import { UserMenuContent } from '@/components/user-menu-content'; -import { useCurrentUrl } from '@/hooks/use-current-url'; +import { toPath, useCurrentUrl } from '@/hooks/use-current-url'; import { useInitials } from '@/hooks/use-initials'; import { cn, toUrl } from '@/lib/utils'; import { dashboard } from '@/routes'; @@ -68,7 +68,9 @@ export function AppHeader({ breadcrumbs = [] }: Props) { const page = usePage(); const { auth } = page.props; const getInitials = useInitials(); - const { isCurrentUrl, whenCurrentUrl } = useCurrentUrl(); + // Même règle que la sidebar : une sous-page garde son entrée allumée. + const { findActiveHref } = useCurrentUrl(); + const activeHref = findActiveHref(mainNavItems.map((i) => i.href)); return ( <>
@@ -155,10 +157,7 @@ export function AppHeader({ breadcrumbs = [] }: Props) { href={item.href} className={cn( navigationMenuTriggerStyle(), - whenCurrentUrl( - item.href, - activeItemStyles, - ), + toPath(item.href) === activeHref ? activeItemStyles : '', 'h-9 cursor-pointer px-3', )} > @@ -167,7 +166,7 @@ export function AppHeader({ breadcrumbs = [] }: Props) { )} {item.title} - {isCurrentUrl(item.href) && ( + {toPath(item.href) === activeHref && (
)} diff --git a/resources/js/components/app-sidebar-header.tsx b/resources/js/components/app-sidebar-header.tsx index 3f9d495..a624dc3 100644 --- a/resources/js/components/app-sidebar-header.tsx +++ b/resources/js/components/app-sidebar-header.tsx @@ -16,24 +16,32 @@ import { } from '@/components/ui/dropdown-menu'; import { SidebarTrigger } from '@/components/ui/sidebar'; import { UserMenuContent } from '@/components/user-menu-content'; -import { useCurrentUrl } from '@/hooks/use-current-url'; +import { toPath, useCurrentUrl } from '@/hooks/use-current-url'; import { getFullName } from '@/hooks/use-initials'; import { mainNavItems } from '@/types'; export function AppSidebarHeader() { const { auth } = usePage().props; - const { currentUrl, isCurrentUrl } = useCurrentUrl(); + const { currentUrl, findActiveHref } = useCurrentUrl(); const fullName = getFullName(auth.user.firstname, auth.user.lastname); const initials = `${auth.user.firstname?.[0] ?? ''}${auth.user.lastname?.[0] ?? ''}`.toUpperCase(); // Fil d'Ariane dérivé du menu selon l'URL courante : [Section, Page] ou [Page]. const trail = useMemo(() => { + // Même règle que la navigation : le lien le plus spécifique gagne, et une + // sous-page (/students/12/edit) reste rattachée à son entrée de menu. + const activeHref = findActiveHref( + mainNavItems.flatMap((item) => (item.items ? item.items.map((s) => s.href) : [item.href])), + ); + + if (!activeHref) return []; + for (const item of mainNavItems) { if (item.items) { - const sub = item.items.find((s) => isCurrentUrl(s.href)); + const sub = item.items.find((s) => toPath(s.href) === activeHref); if (sub) return [item.title, sub.title]; - } else if (isCurrentUrl(item.href)) { + } else if (toPath(item.href) === activeHref) { return [item.title]; } } diff --git a/resources/js/components/nav-main.tsx b/resources/js/components/nav-main.tsx index 0741f42..2ade041 100644 --- a/resources/js/components/nav-main.tsx +++ b/resources/js/components/nav-main.tsx @@ -15,7 +15,7 @@ import { SidebarMenuSubButton, SidebarMenuSubItem, } from '@/components/ui/sidebar'; -import { useCurrentUrl } from '@/hooks/use-current-url'; +import { toPath, useCurrentUrl } from '@/hooks/use-current-url'; import type { NavItem } from '@/types'; // Style de l'élément actif (parent ou sous-item) — factorisé pour éviter la triplication. @@ -23,7 +23,7 @@ const ACTIVE_CLASS = 'data-[active=true]:bg-sidebar-primary data-[active=true]:text-sidebar-primary-foreground data-[active=true]:font-semibold data-[active=true]:[&>svg]:text-sidebar-primary-foreground'; export function NavMain({ items = [] }: Readonly<{ items: NavItem[] }>) { - const { currentUrl, isCurrentUrl } = useCurrentUrl(); + const { currentUrl, findActiveHref } = useCurrentUrl(); const auth = (usePage().props as { auth?: { permissions?: string[] } }).auth; const permissions = auth?.permissions ?? []; @@ -35,9 +35,17 @@ export function NavMain({ items = [] }: Readonly<{ items: NavItem[] }>) { .map((item) => (item.items ? { ...item, items: item.items.filter(allowed) } : item)) .filter((item) => (item.items ? item.items.length > 0 : allowed(item))); - // Un groupe est actif si l'un de ses sous-items correspond à l'URL courante. - const isGroupActive = (item: NavItem) => - item.items?.some((sub) => isCurrentUrl(sub.href)) ?? false; + // Lien actif = le plus spécifique parmi TOUTES les entrées du menu. Ainsi une + // sous-page (/students/12/edit) garde son entrée allumée, et /accounting/transactions + // n'allume pas aussi /accounting. + const activeHref = findActiveHref( + visibleItems.flatMap((item) => (item.items ? item.items.map((s) => s.href) : [item.href])), + ); + + const isLinkActive = (href: NavItem["href"]) => activeHref !== null && toPath(href) === activeHref; + + // Un groupe est actif si l'un de ses sous-items est le lien actif. + const isGroupActive = (item: NavItem) => item.items?.some((sub) => isLinkActive(sub.href)) ?? false; // État d'ouverture contrôlé : le groupe courant s'ouvre automatiquement // (et à chaque navigation), tout en laissant l'utilisateur replier/déplier. @@ -56,7 +64,11 @@ export function NavMain({ items = [] }: Readonly<{ items: NavItem[] }>) { const setOpen = (title: string, open: boolean) => setOpenTitles((prev) => { const next = new Set(prev); - open ? next.add(title) : next.delete(title); + if (open) { + next.add(title); + } else { + next.delete(title); + } return next; }); @@ -91,7 +103,7 @@ export function NavMain({ items = [] }: Readonly<{ items: NavItem[] }>) { @@ -107,7 +119,7 @@ export function NavMain({ items = [] }: Readonly<{ items: NavItem[] }>) { ) : ( diff --git a/resources/js/hooks/use-current-url.ts b/resources/js/hooks/use-current-url.ts index 495f249..af3f0ac 100644 --- a/resources/js/hooks/use-current-url.ts +++ b/resources/js/hooks/use-current-url.ts @@ -2,57 +2,98 @@ import type { InertiaLinkProps } from '@inertiajs/react'; import { usePage } from '@inertiajs/react'; import { toUrl } from '@/lib/utils'; -export type IsCurrentUrlFn = ( - urlToCheck: NonNullable, - currentUrl?: string, -) => boolean; +type Href = NonNullable; + +export type IsCurrentUrlFn = (urlToCheck: Href, currentUrl?: string) => boolean; export type WhenCurrentUrlFn = ( - urlToCheck: NonNullable, + urlToCheck: Href, ifTrue: TIfTrue, ifFalse?: TIfFalse, ) => TIfTrue | TIfFalse; export type UseCurrentUrlReturn = { currentUrl: string; + /** Correspondance exacte avec l'URL courante. */ isCurrentUrl: IsCurrentUrlFn; + /** L'URL courante est-elle cette page **ou l'une de ses sous-pages** ? */ + isUnderUrl: IsCurrentUrlFn; + /** + * Parmi plusieurs liens, celui qui correspond le mieux à l'URL courante + * (le plus spécifique). Retourne son chemin normalisé, ou null. + */ + findActiveHref: (hrefs: Href[]) => string | null; whenCurrentUrl: WhenCurrentUrlFn; }; +/** Chemin normalisé d'un lien : sans origine, sans query ni fragment, sans / final. */ +export function toPath(href: Href): string | null { + const raw = toUrl(href); + + if (!raw || raw === '#' || raw.startsWith('#')) { + return null; + } + + let path: string; + try { + path = new URL(raw, window?.location.origin).pathname; + } catch { + return null; + } + + return path.length > 1 ? path.replace(/\/+$/, '') : path; +} + +/** + * L'URL courante est-elle `path` ou l'un de ses descendants ? + * + * La comparaison se fait **par segment** : `/students` ne doit pas capter + * `/students-import` ni `/students-stats`, qui sont des entrées de menu + * distinctes. + */ +function isUnder(current: string, path: string): boolean { + if (path === '/') { + return current === '/'; + } + + return current === path || current.startsWith(path + '/'); +} + export function useCurrentUrl(): UseCurrentUrlReturn { const page = usePage(); - const currentUrlPath = new URL(page.url, window?.location.origin).pathname; - - const isCurrentUrl: IsCurrentUrlFn = ( - urlToCheck: NonNullable, - currentUrl?: string, - ) => { - const urlToCompare = currentUrl ?? currentUrlPath; - const urlString = toUrl(urlToCheck); - - if (!urlString.startsWith('http')) { - return urlString === urlToCompare; - } - - try { - const absoluteUrl = new URL(urlString); - return absoluteUrl.pathname === urlToCompare; - } catch { - return false; - } + const currentUrlPath = toPath(page.url) ?? '/'; + + const isCurrentUrl: IsCurrentUrlFn = (urlToCheck, currentUrl) => { + const path = toPath(urlToCheck); + + return path !== null && path === (currentUrl ?? currentUrlPath); + }; + + const isUnderUrl: IsCurrentUrlFn = (urlToCheck, currentUrl) => { + const path = toPath(urlToCheck); + + return path !== null && isUnder(currentUrl ?? currentUrlPath, path); }; + // Le lien le plus spécifique l'emporte : sur /accounting/transactions, seul + // « Journal des transactions » s'allume, pas « Vue d'ensemble » (/accounting). + const findActiveHref = (hrefs: Href[]): string | null => + hrefs + .map(toPath) + .filter((path): path is string => path !== null && isUnder(currentUrlPath, path)) + .sort((a, b) => b.length - a.length)[0] ?? null; + const whenCurrentUrl: WhenCurrentUrlFn = ( - urlToCheck: NonNullable, + urlToCheck: Href, ifTrue: TIfTrue, ifFalse: TIfFalse = null as TIfFalse, - ): TIfTrue | TIfFalse => { - return isCurrentUrl(urlToCheck) ? ifTrue : ifFalse; - }; + ): TIfTrue | TIfFalse => (isCurrentUrl(urlToCheck) ? ifTrue : ifFalse); return { currentUrl: currentUrlPath, isCurrentUrl, + isUnderUrl, + findActiveHref, whenCurrentUrl, }; } diff --git a/resources/js/layouts/settings/layout.tsx b/resources/js/layouts/settings/layout.tsx index bf00197..9efa2be 100644 --- a/resources/js/layouts/settings/layout.tsx +++ b/resources/js/layouts/settings/layout.tsx @@ -3,7 +3,7 @@ import { User, Lock, Shield, Palette } from 'lucide-react'; import type { PropsWithChildren } from 'react'; import Heading from '@/components/heading'; import { Separator } from '@/components/ui/separator'; -import { useCurrentUrl } from '@/hooks/use-current-url'; +import { toPath, useCurrentUrl } from '@/hooks/use-current-url'; import { cn, toUrl } from '@/lib/utils'; import { edit as editAppearance } from '@/routes/appearance'; import { edit } from '@/routes/profile'; @@ -35,7 +35,8 @@ const sidebarNavItems: NavItem[] = [ ]; export default function SettingsLayout({ children, bare = false }: Readonly>) { - const { isCurrentUrl } = useCurrentUrl(); + // Le lien le plus spécifique gagne, et une sous-page reste rattachée à son onglet. + const activeHref = useCurrentUrl().findActiveHref(sidebarNavItems.map((i) => i.href)); // When server-side rendering, we only render the layout on the client... if (globalThis.window === undefined) { @@ -63,7 +64,7 @@ export default function SettingsLayout({ children, bare = false }: Readonly Date: Wed, 2 Sep 2026 15:57:42 +0000 Subject: [PATCH 2/4] fix(nav): garde Mon compte actif sur ses sous-sections (mot de passe, 2FA, apparence) --- .../js/components/app-sidebar-header.tsx | 17 +++++++------- resources/js/components/nav-main.tsx | 18 ++++++++------- resources/js/hooks/use-current-url.ts | 23 +++++++++++++++++++ resources/js/types/menu.ts | 3 +++ resources/js/types/navigation.ts | 6 +++++ 5 files changed, 51 insertions(+), 16 deletions(-) diff --git a/resources/js/components/app-sidebar-header.tsx b/resources/js/components/app-sidebar-header.tsx index a624dc3..324062c 100644 --- a/resources/js/components/app-sidebar-header.tsx +++ b/resources/js/components/app-sidebar-header.tsx @@ -16,13 +16,13 @@ import { } from '@/components/ui/dropdown-menu'; import { SidebarTrigger } from '@/components/ui/sidebar'; import { UserMenuContent } from '@/components/user-menu-content'; -import { toPath, useCurrentUrl } from '@/hooks/use-current-url'; +import { useCurrentUrl } from '@/hooks/use-current-url'; import { getFullName } from '@/hooks/use-initials'; import { mainNavItems } from '@/types'; export function AppSidebarHeader() { const { auth } = usePage().props; - const { currentUrl, findActiveHref } = useCurrentUrl(); + const { currentUrl, findActiveKey } = useCurrentUrl(); const fullName = getFullName(auth.user.firstname, auth.user.lastname); const initials = `${auth.user.firstname?.[0] ?? ''}${auth.user.lastname?.[0] ?? ''}`.toUpperCase(); @@ -31,17 +31,18 @@ export function AppSidebarHeader() { const trail = useMemo(() => { // Même règle que la navigation : le lien le plus spécifique gagne, et une // sous-page (/students/12/edit) reste rattachée à son entrée de menu. - const activeHref = findActiveHref( - mainNavItems.flatMap((item) => (item.items ? item.items.map((s) => s.href) : [item.href])), + const active = findActiveKey( + mainNavItems + .flatMap((item) => (item.items ? item.items : [item])) + .map((entry) => ({ key: entry, hrefs: [entry.href, ...(entry.match ?? [])] })), ); - if (!activeHref) return []; + if (!active) return []; for (const item of mainNavItems) { if (item.items) { - const sub = item.items.find((s) => toPath(s.href) === activeHref); - if (sub) return [item.title, sub.title]; - } else if (toPath(item.href) === activeHref) { + if (item.items.includes(active)) return [item.title, active.title]; + } else if (item === active) { return [item.title]; } } diff --git a/resources/js/components/nav-main.tsx b/resources/js/components/nav-main.tsx index 2ade041..4b2cb56 100644 --- a/resources/js/components/nav-main.tsx +++ b/resources/js/components/nav-main.tsx @@ -15,7 +15,7 @@ import { SidebarMenuSubButton, SidebarMenuSubItem, } from '@/components/ui/sidebar'; -import { toPath, useCurrentUrl } from '@/hooks/use-current-url'; +import { useCurrentUrl } from '@/hooks/use-current-url'; import type { NavItem } from '@/types'; // Style de l'élément actif (parent ou sous-item) — factorisé pour éviter la triplication. @@ -23,7 +23,7 @@ const ACTIVE_CLASS = 'data-[active=true]:bg-sidebar-primary data-[active=true]:text-sidebar-primary-foreground data-[active=true]:font-semibold data-[active=true]:[&>svg]:text-sidebar-primary-foreground'; export function NavMain({ items = [] }: Readonly<{ items: NavItem[] }>) { - const { currentUrl, findActiveHref } = useCurrentUrl(); + const { currentUrl, findActiveKey } = useCurrentUrl(); const auth = (usePage().props as { auth?: { permissions?: string[] } }).auth; const permissions = auth?.permissions ?? []; @@ -38,14 +38,16 @@ export function NavMain({ items = [] }: Readonly<{ items: NavItem[] }>) { // Lien actif = le plus spécifique parmi TOUTES les entrées du menu. Ainsi une // sous-page (/students/12/edit) garde son entrée allumée, et /accounting/transactions // n'allume pas aussi /accounting. - const activeHref = findActiveHref( - visibleItems.flatMap((item) => (item.items ? item.items.map((s) => s.href) : [item.href])), + const activeItem = findActiveKey( + visibleItems + .flatMap((item) => (item.items ? item.items : [item])) + .map((entry) => ({ key: entry, hrefs: [entry.href, ...(entry.match ?? [])] })), ); - const isLinkActive = (href: NavItem["href"]) => activeHref !== null && toPath(href) === activeHref; + const isLinkActive = (item: NavItem) => activeItem === item; // Un groupe est actif si l'un de ses sous-items est le lien actif. - const isGroupActive = (item: NavItem) => item.items?.some((sub) => isLinkActive(sub.href)) ?? false; + const isGroupActive = (item: NavItem) => item.items?.some((sub) => isLinkActive(sub)) ?? false; // État d'ouverture contrôlé : le groupe courant s'ouvre automatiquement // (et à chaque navigation), tout en laissant l'utilisateur replier/déplier. @@ -103,7 +105,7 @@ export function NavMain({ items = [] }: Readonly<{ items: NavItem[] }>) { @@ -119,7 +121,7 @@ export function NavMain({ items = [] }: Readonly<{ items: NavItem[] }>) { ) : ( diff --git a/resources/js/hooks/use-current-url.ts b/resources/js/hooks/use-current-url.ts index af3f0ac..38be12b 100644 --- a/resources/js/hooks/use-current-url.ts +++ b/resources/js/hooks/use-current-url.ts @@ -23,6 +23,12 @@ export type UseCurrentUrlReturn = { * (le plus spécifique). Retourne son chemin normalisé, ou null. */ findActiveHref: (hrefs: Href[]) => string | null; + /** + * Parmi plusieurs entrées, celle dont l'un des liens correspond le mieux à + * l'URL courante. Chaque entrée peut porter plusieurs chemins (une section + * regroupant des URL sans préfixe commun). + */ + findActiveKey: (entries: { key: T; hrefs: Href[] }[]) => T | null; whenCurrentUrl: WhenCurrentUrlFn; }; @@ -83,6 +89,22 @@ export function useCurrentUrl(): UseCurrentUrlReturn { .filter((path): path is string => path !== null && isUnder(currentUrlPath, path)) .sort((a, b) => b.length - a.length)[0] ?? null; + const findActiveKey = (entries: { key: T; hrefs: Href[] }[]): T | null => { + let best: { key: T; length: number } | null = null; + + for (const entry of entries) { + for (const href of entry.hrefs) { + const path = toPath(href); + if (path === null || !isUnder(currentUrlPath, path)) continue; + if (!best || path.length > best.length) { + best = { key: entry.key, length: path.length }; + } + } + } + + return best?.key ?? null; + }; + const whenCurrentUrl: WhenCurrentUrlFn = ( urlToCheck: Href, ifTrue: TIfTrue, @@ -94,6 +116,7 @@ export function useCurrentUrl(): UseCurrentUrlReturn { isCurrentUrl, isUnderUrl, findActiveHref, + findActiveKey, whenCurrentUrl, }; } diff --git a/resources/js/types/menu.ts b/resources/js/types/menu.ts index 81b6546..2fb1af5 100644 --- a/resources/js/types/menu.ts +++ b/resources/js/types/menu.ts @@ -140,5 +140,8 @@ export const mainNavItems: NavItem[] = [ title: 'Mon compte', href: '/settings/profile', icon: UserCircle, + // Les réglages du compte n'ont pas de préfixe commun exclusif : sans ces + // chemins, l'entrée s'éteindrait sur Mot de passe, 2FA ou Apparence. + match: ['/settings/password', '/settings/two-factor', '/settings/appearance'], }, ]; diff --git a/resources/js/types/navigation.ts b/resources/js/types/navigation.ts index e6dcd2f..cf20f08 100644 --- a/resources/js/types/navigation.ts +++ b/resources/js/types/navigation.ts @@ -14,4 +14,10 @@ export type NavItem = { items?: NavItem[]; /** Permission requise pour afficher l'entrée (absente = toujours visible). */ permission?: string; + /** + * Chemins supplémentaires qui gardent l'entrée active (avec leurs + * sous-pages). Utile quand une section regroupe plusieurs URL sans préfixe + * commun exclusif — ex. « Mon compte » : profil, mot de passe, 2FA, apparence. + */ + match?: string[]; }; From bebd5b95459bf89cab09a17fe47c49618e6b80a9 Mon Sep 17 00:00:00 2001 From: horacioskrp Date: Wed, 2 Sep 2026 16:30:14 +0000 Subject: [PATCH 3/4] chore(front): supprime les composants morts et les dependances devenues inutiles --- package-lock.json | 153 ----------- package.json | 2 - .../js/components/Schools/school-drawer.tsx | 247 ----------------- resources/js/components/app-header.tsx | 250 ------------------ resources/js/components/delete-user.tsx | 121 --------- resources/js/components/form-drawer.tsx | 59 ----- resources/js/components/nav-footer.tsx | 49 ---- resources/js/components/nav-user.tsx | 54 ---- .../js/components/ui/placeholder-pattern.tsx | 20 -- resources/js/components/ui/toggle-group.tsx | 71 ----- resources/js/components/ui/toggle.tsx | 45 ---- .../js/layouts/app/app-header-layout.tsx | 16 -- .../js/layouts/auth/auth-card-layout.tsx | 43 --- .../js/layouts/auth/auth-split-layout.tsx | 37 --- 14 files changed, 1167 deletions(-) delete mode 100644 resources/js/components/Schools/school-drawer.tsx delete mode 100644 resources/js/components/app-header.tsx delete mode 100644 resources/js/components/delete-user.tsx delete mode 100644 resources/js/components/form-drawer.tsx delete mode 100644 resources/js/components/nav-footer.tsx delete mode 100644 resources/js/components/nav-user.tsx delete mode 100644 resources/js/components/ui/placeholder-pattern.tsx delete mode 100644 resources/js/components/ui/toggle-group.tsx delete mode 100644 resources/js/components/ui/toggle.tsx delete mode 100644 resources/js/layouts/app/app-header-layout.tsx delete mode 100644 resources/js/layouts/auth/auth-card-layout.tsx delete mode 100644 resources/js/layouts/auth/auth-split-layout.tsx diff --git a/package-lock.json b/package-lock.json index c984a96..1a03897 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,8 +18,6 @@ "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.2.3", - "@radix-ui/react-toggle": "^1.1.2", - "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", "@tailwindcss/vite": "^4.1.11", "@tiptap/extension-text-align": "^3.26.1", @@ -2625,157 +2623,6 @@ } } }, - "node_modules/@radix-ui/react-toggle": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", - "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", - "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-toggle": "1.1.10", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-tooltip": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", diff --git a/package.json b/package.json index 2552b2a..c5d5ef1 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,6 @@ "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.2.3", - "@radix-ui/react-toggle": "^1.1.2", - "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", "@tailwindcss/vite": "^4.1.11", "@tiptap/extension-text-align": "^3.26.1", diff --git a/resources/js/components/Schools/school-drawer.tsx b/resources/js/components/Schools/school-drawer.tsx deleted file mode 100644 index 681d3f3..0000000 --- a/resources/js/components/Schools/school-drawer.tsx +++ /dev/null @@ -1,247 +0,0 @@ -import { useForm } from '@inertiajs/react'; -import { useEffect } from 'react'; -import { CreateDrawer } from '@/components/create-drawer'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Textarea } from '@/components/ui/textarea'; -import { route } from '@/helpers/route'; - -interface SchoolData { - id?: string; - name: string; - code: string; - email: string | null; - phone: string | null; - address: string | null; -} - -interface SchoolDrawerProps { - isOpen: boolean; - onOpenChange: (open: boolean) => void; - school?: SchoolData | null; - onSuccess?: () => void; -} - -export function SchoolDrawer({ - isOpen, - onOpenChange, - school, - onSuccess, -}: Readonly) { - const isEditing = !!school?.id; - - const { data, setData, post, put, processing, reset, errors } = useForm({ - name: school?.name || '', - code: school?.code || '', - email: school?.email || '', - phone: school?.phone || '', - address: school?.address || '', - }); - - // Réinitialiser le formulaire quand school change ou le drawer s'ouvre - useEffect(() => { - if (isOpen && school?.id) { - setData({ - name: school.name, - code: school.code, - email: school.email || '', - phone: school.phone || '', - address: school.address || '', - }); - } else if (isOpen && !school?.id) { - reset(); - } - }, [school?.id, isOpen]); - - const handleSubmit = (e: React.SubmitEvent) => { - e.preventDefault(); - - if (isEditing && school?.id) { - put(route('schools.update', school.id), { - onSuccess: () => { - reset(); - onOpenChange(false); - onSuccess?.(); - }, - }); - } else { - post(route('schools.store'), { - onSuccess: () => { - reset(); - onOpenChange(false); - onSuccess?.(); - }, - }); - } - }; - - const handleOpenChange = (open: boolean) => { - onOpenChange(open); - if (!open) { - reset(); - } - }; - - return ( - -
- {/* Name */} -
- - setData('name', e.target.value)} - disabled={processing} - className="h-10 border-gray-300 bg-gray-50 focus:bg-white" - /> - {errors.name && ( -

- ❌ {errors.name} -

- )} -
- - {/* Code */} -
- - setData('code', e.target.value)} - disabled={processing} - className="h-10 border-gray-300 bg-gray-50 focus:bg-white" - /> - {errors.code && ( -

- ❌ {errors.code} -

- )} -
- - {/* Email */} -
- - setData('email', e.target.value)} - disabled={processing} - className="h-10 border-gray-300 bg-gray-50 focus:bg-white" - /> - {errors.email && ( -

- ❌ {errors.email} -

- )} -
- - {/* Phone */} -
- - setData('phone', e.target.value)} - disabled={processing} - className="h-10 border-gray-300 bg-gray-50 focus:bg-white" - /> - {errors.phone && ( -

- ❌ {errors.phone} -

- )} -
- - {/* Address */} -
- -