Working on the Organization side.

This commit is contained in:
charlesgauthereau
2025-07-26 14:43:59 +02:00
parent f7cd169090
commit f8f599e4dd
27 changed files with 685 additions and 119 deletions
@@ -0,0 +1,41 @@
import {
Sidebar,
SidebarContent,
SidebarHeader,
SidebarMenu,
SidebarMenuItem
} from "@/components/ui/sidebar";
import {SidebarLogo} from "@/components/wrappers/dashboard/common/sidebar/logo-sidebar";
import {SidebarMenuCustomMain} from "@/components/wrappers/dashboard/common/sidebar/menu-sidebar-main";
import {SideBarFooterCredit} from "@/components/wrappers/dashboard/common/sidebar/side-bar-footer-credit";
import {LoggedInButton} from "@/components/wrappers/dashboard/loggedInButton/LoggedInButton";
import {OrganizationCombobox} from "@/components/wrappers/dashboard/organization/organization-combobox";
export function AppSidebar() {
return (
<Sidebar collapsible="icon">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarLogo/>
</SidebarMenuItem>
<SidebarMenuItem>
<OrganizationCombobox/>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarMenuCustomMain/>
</SidebarContent>
<SidebarMenu>
<SidebarMenuItem>
<LoggedInButton />
</SidebarMenuItem>
</SidebarMenu>
<SideBarFooterCredit />
</Sidebar>
);
}
@@ -0,0 +1,51 @@
"use client"
import { useSidebar } from "@/components/ui/sidebar";
import Link from "next/link";
import { env } from "@/env.mjs";
import { useTheme } from "next-themes";
import Image from "next/image";
import { useEffect, useState } from "react";
export const SidebarLogo = () => {
const { state, isMobile } = useSidebar();
const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
const imageTheme = resolvedTheme === "dark" ? "/images/logo-white.png" : "/images/logo-black.png";
return (
<div className="ml-1 flex items-center justify-center">
<Link href={"/"}>
{state === 'collapsed' && !isMobile ? (
<Image
loading="eager"
src={"/images/logo.png"}
alt={`Logo ${env.NEXT_PUBLIC_PROJECT_NAME}`}
className="h-10 w-10 object-contain"
height={10}
width={10}
/>
) : (
<div className="m-4">
<Image
loading="eager"
src={imageTheme}
alt={`Logo ${env.NEXT_PUBLIC_PROJECT_NAME}`}
className="object-contain"
width={190}
height={90}
/>
</div>
)}
</Link>
</div>
);
};
@@ -0,0 +1,56 @@
"use client"
import {Home, Settings, Target, Users, Pickaxe, Euro, BanknoteX, Layers, ChartArea, ShieldHalf} from "lucide-react";
import {SidebarGroupItem, SidebarMenuCustomBase} from "@/components/wrappers/dashboard/common/sidebar/menu-sidebar";
import {authClient, useSession} from "@/lib/auth/auth-client";
export const SidebarMenuCustomMain = () => {
const BASE_URL = `/dashboard`;
const { data: activeOrganization } = authClient.useActiveOrganization();
const { data: organizations } = authClient.useListOrganizations();
const {data: session, isPending, error} = authClient.useSession();
const member = authClient.useActiveMember();
if (isPending) return null;
if (error || !session) {
return null;
}
const groupContent: SidebarGroupItem["group_content"] = [
{ title: "Dashboard", url: "/home", icon: Home },
{ title: "Projects", url: "/projects", icon: Layers, details:true },
{ title: "Statistics", url: "/statistics", icon: ChartArea },
];
if (activeOrganization && (member?.data?.role === "admin" || member?.data?.role === "owner")) {
groupContent.push({ title: "Settings", url: "/settings", icon: Settings, details:true });
}
const items: SidebarGroupItem[] = [
{
label: "Application",
type: "list",
group_content: groupContent,
},
];
if (session?.user.role == "admin" || session?.user.role == "superadmin") {
items.push({
label: "Administration",
type: "list",
group_content: [
{title: "Agents", url: "/agents", icon: ShieldHalf, details: true},
{title: "Administration panel", url: "/admin", icon: Settings, details: true},
]
})
}
return <SidebarMenuCustomBase baseUrl={BASE_URL} items={items}/>;
};
@@ -0,0 +1,208 @@
"use client";
import {
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem
} from "@/components/ui/sidebar";
import {
ChevronDown,
MoreHorizontal
} from "lucide-react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
} from "@radix-ui/react-collapsible";
import React, { useEffect, useState } from "react";
import Link from "next/link";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import { usePathname } from "next/navigation";
export type SidebarItem = {
title: string;
url: string;
redirect?: boolean;
not_from_base_url?: boolean;
icon: React.ElementType;
dropdown?: SidebarItem[];
submenu?: SidebarItem[];
details?: boolean;
};
export type SidebarGroupItem = {
label: string;
type: "list" | "collapse";
group_content: SidebarItem[];
};
type SidebarMenuCustomBaseProps = {
baseUrl: string;
items: SidebarGroupItem[];
};
export const SidebarMenuCustomBase = ({ baseUrl, items }: SidebarMenuCustomBaseProps) => {
const pathname = usePathname();
const [activeItem, setActiveItem] = useState("");
useEffect(() => {
const normalize = (url: string) => url.split("?")[0].replace(/\/$/, "");
const findActiveUrl = () => {
const normalizedPathname = normalize(pathname);
console.log(normalizedPathname);
for (const group of items) {
for (const item of group.group_content) {
const itemUrl = normalize(item.url);
const fullUrl = item.not_from_base_url
? itemUrl
: normalize(`${baseUrl}${itemUrl}`);
if (
normalizedPathname === fullUrl ||
(item.details && normalizedPathname.startsWith(`${fullUrl}/`))
) {
return itemUrl;
}
if (item.submenu) {
for (const subItem of item.submenu) {
const subItemUrl = normalize(subItem.url);
const subFullUrl = normalize(`${baseUrl}${subItemUrl}`);
if (
normalizedPathname === subFullUrl ||
(subItem.details && normalizedPathname.startsWith(`${subFullUrl}/`))
) {
return subItemUrl;
}
}
}
}
}
return "";
};
const activeUrl = findActiveUrl();
setActiveItem(activeUrl);
}, [pathname, baseUrl, items]);
return (
<SidebarMenu>
{items.map((group, index) => (
<Collapsible key={index} defaultOpen disabled={group.type === "list"}>
<SidebarGroup>
<SidebarGroupLabel asChild>
<CollapsibleTrigger>
{group.label ?? "Missing label"}
{group.type === "collapse" && (
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
)}
</CollapsibleTrigger>
</SidebarGroupLabel>
<CollapsibleContent>
<SidebarGroupContent>
{group.group_content.map((item, index) => (
<SidebarMenuItem key={index} className="mb-1">
<SidebarMenuButton asChild>
<Link
href={
item.redirect || item.not_from_base_url
? item.url
: `${baseUrl}${item.url}`
}
target={item.redirect ? "_blank" : ""}
className={cn(
buttonVariants({
size: "lg",
variant: activeItem === item.url ? "secondary" : "ghost"
}),
"justify-start p-0",
"hover:bg-gray-700"
)}
onClick={() => setActiveItem(item.url)}
>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
{item.submenu && (
<SidebarMenuSub>
{item.submenu.map((sub, idx) => (
<SidebarMenuSubItem key={idx}>
<SidebarMenuSubButton asChild>
<Link
href={`${baseUrl}${sub.url}`}
className={cn(
buttonVariants({
size: "lg",
variant: activeItem === sub.url ? "secondary" : "ghost"
}),
"justify-start p-0",
"hover:bg-gray-700"
)}
onClick={() => setActiveItem(sub.url)}
>
<sub.icon />
{sub.title}
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
)}
{item.dropdown && (
<SidebarMenuAction>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<MoreHorizontal />
</DropdownMenuTrigger>
<DropdownMenuContent side="right" align="start">
{item.dropdown.map((dropdown, idx) => (
<DropdownMenuItem key={idx} asChild>
<Link
href={
dropdown.redirect || dropdown.not_from_base_url
? dropdown.url
: `${baseUrl}${dropdown.url}`
}
className="justify-start p-0"
target={dropdown.redirect ? "_blank" : ""}
onClick={() => setActiveItem(dropdown.url)}
>
<span>{dropdown.title}</span>
</Link>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuAction>
)}
</SidebarMenuItem>
))}
</SidebarGroupContent>
</CollapsibleContent>
</SidebarGroup>
</Collapsible>
))}
</SidebarMenu>
);
};
@@ -0,0 +1,19 @@
"use client";
import { useSidebar } from "@/components/ui/sidebar";
import { env } from "@/env.mjs";
export type SideBarFooterCreditProps = {};
export const SideBarFooterCredit = (props: SideBarFooterCreditProps) => {
const { state } = useSidebar();
return (
<>
{state === "expanded" && (
<div className="text-center">
<h1 className="text-[10px]">Portabase Community Edition v{env.NEXT_PUBLIC_PROJECT_VERSION}</h1>
</div>
)}
</>
);
};
@@ -0,0 +1,41 @@
"use client";
import {useSidebar} from "@/components/ui/sidebar";
import {useTheme} from "next-themes";
import Image from "next/image";
import Link from "next/link";
export type SideBarLogoProps = {};
export const SideBarLogo = (props: SideBarLogoProps) => {
const {state, isMobile} = useSidebar();
const {resolvedTheme} = useTheme();
return state === "collapsed" && !isMobile ? (
<Link href={"/dashboard"}>
<div className="flex items-center justify-center">
<Image
src={"/images/logo/picto.png"}
alt="Logo"
width={32}
height={32}
loading="eager"
className="object-contain"
/>
</div>
</Link>
) : (
<Link href={"/dashboard"}>
<div className="w-full h-auto p-4 flex items-center justify-center">
<Image
loading="eager"
src={"/images/logo/logo.png"}
alt="Logo"
width={190}
height={190}
className="object-contain"
/>
</div>
</Link>
);
};