ldc-groupe/resources/ts/back/Components/Layout/AppSidebar.tsx

646 lines
30 KiB
TypeScript

// * Packages
import { Link, usePage } from "@inertiajs/react";
import { ElementType, ReactNode, useEffect, useRef } from "react";
import {
ChevronDownIcon,
MoreHorizontalIcon,
HomeIcon,
UsersIcon,
SquareArrowOutUpRightIcon,
TelescopeIcon,
StickyNoteIcon,
CogIcon,
HistoryIcon,
ContactIcon,
NewspaperIcon,
Building2Icon,
RadarIcon,
RollerCoasterIcon,
FileInputIcon,
TagIcon,
FolderIcon,
} from "lucide-react";
import { route } from "ziggy-js";
// * Components shadcn
import {
Sidebar,
SidebarHeader,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuAction,
useSidebar,
} from "@/components/ui/sidebar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Collapsible,
CollapsibleTrigger,
CollapsibleContent,
} from "@/components/ui/collapsible";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
// * Other
import ApplicationLogo from "@/back/Components/ApplicationLogo";
import { Views } from "@/lib/utils";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import { AppEnv, AuthProp, LaravelEnum } from "@/back/types/global";
type NavListProps = Array<{
/** Title of the group. */
title: string;
/** Condition to set the visibility of the group. */
show: boolean;
/** Links presents in the group. */
links: Array<{
/** Title of the button. */
title: string;
/** Main model of the button (used for route checking). */
model?: string;
/** External link of the button (if it's a not CRUD, just a simple link). */
link?: string;
/** Define some conditions for active or not the button (in addition to the route model checking if defined). */
activeCondition?: boolean[];
/** Condition to set the visibility of the button. */
show: boolean;
/** Lucide icon of the button. */
icon: ElementType;
/** Define some actions in side popover (like create/trash). */
// eslint-disable-next-line no-unused-vars
actions?: (model: string) => ReactNode;
}>;
}>;
type CustomDropdownMenuItemProps = {
/** Link of the action. */
targetRoute: string;
/** Lucide icon of the action. */
icon: ElementType;
/** Translation of the action. */
trans: string;
};
/**
* Sidenav action button.
*
* @param string targetRoute
* @param ElementType icon
* @param string trans
* @return ReactNode
*/
function CustomDropdownMenuItem({
targetRoute,
icon: Icon,
trans,
}: CustomDropdownMenuItemProps): ReactNode {
return (
<DropdownMenuItem className="p-0">
<Button variant="ghost" asChild>
<Link href={targetRoute} className="w-full">
<Icon />
{trans}
</Link>
</Button>
</DropdownMenuItem>
);
}
export default function AppSidebar() {
const { appEnv, auth, enumUserRole } = usePage().props as unknown as {
appEnv: AppEnv;
auth: AuthProp;
enumUserRole: LaravelEnum;
};
const authPolicies = auth.policies;
const { state } = useSidebar();
const prevStateRef = useRef(state);
const isCollapsed = state === "collapsed";
// Translations.
const transDefaultSideAction = str(
useTrans(`crud.actions.${Views.CREATION.label}`),
)
.ucFirst()
.value();
const transPages = useTrans("back.pages");
const transDashboard = useTrans("back.dashboard");
const transWebsite = useTrans("back.your_website");
const transDynamicPages = useTrans("models.classes.dynamic-page", {
choice: 2,
});
const transModules = useTrans("back.modules");
const transNews = useTrans("models.classes.news", { choice: 2 });
const transNewsCategories = useTrans("models.classes.news-category", {
choice: 2,
});
const transNewsTags = useTrans("models.classes.news-tag", { choice: 2 });
const transContactCount = useTrans("models.classes.contact-count");
const transDocuments = useTrans("models.classes.document", { choice: 2 });
const transDocumentCategories = useTrans(
"models.classes.document-category",
{ choice: 2 },
);
const transDocumentTags = useTrans("models.classes.document-tag", {
choice: 2,
});
const transCompanies = useTrans("models.classes.company", { choice: 2 });
const transCompanyActivities = useTrans("models.classes.company-activity", {
choice: 2,
});
const transCompanySectors = useTrans("models.classes.company-sector", {
choice: 2,
});
const transAdmin = useTrans("enums.user-role.admin");
const transUsers = useTrans("models.classes.user", { choice: 2 });
const transActivityLogs = useTrans("models.classes.activity-log");
const transPageConfigs = useTrans("models.classes.page-config", {
choice: 2,
});
const transArchives = str(useTrans("back.archives")).ucFirst().value();
const transTelescope = useTrans("back.telescope");
/**
* Get sidenav actions.
*
* @param ReactNode items
* @param string model
* @return
*/
const filterActions = ({
items,
model,
}: {
items?: ReactNode[];
model?: string;
}): ReactNode => {
let computedItems = items;
// Set create action by default.
if (!computedItems && model) {
computedItems = [
authPolicies[model].create && (
<CustomDropdownMenuItem
key="create"
targetRoute={route(`back.${model}.create`)}
icon={Views.CREATION.icon}
trans={transDefaultSideAction}
/>
),
];
}
const visibleItems = computedItems
? computedItems.filter(Boolean)
: null;
return visibleItems && visibleItems.length > 0 ? visibleItems : null;
};
const navList: NavListProps = [
{
title: transPages,
show: true,
links: [
{
title: transDashboard,
model: "dashboard",
show: true,
icon: HomeIcon,
},
{
title: transWebsite,
link: "/",
show: true,
icon: SquareArrowOutUpRightIcon,
},
{
title: str(transDynamicPages).ucFirst().value(),
model: "dynamic-pages",
show: authPolicies.dynamicPages.viewAny,
icon: StickyNoteIcon,
actions: (model: string) =>
filterActions({
items: [
authPolicies.dynamicPages.create && (
<CustomDropdownMenuItem
targetRoute={route(
`back.${model}.create`,
)}
icon={Views.CREATION.icon}
trans={transDefaultSideAction}
/>
),
authPolicies.dynamicPages.viewTrash && (
<CustomDropdownMenuItem
targetRoute={route(
`back.${model}.trash`,
)}
icon={Views.ARCHIVATION.icon}
trans={str(transArchives)
.ucFirst()
.value()}
/>
),
],
}),
},
],
},
{
title: transModules,
show:
authPolicies.news.viewAny ||
auth.policies.contact_counts.viewAny,
links: [
{
title: str(transNews).ucFirst().value(),
model: "news",
activeCondition: [
route().current("back.news-categories.*"),
route().current("back.news-tags.*"),
],
icon: NewspaperIcon,
show: auth.policies.news.viewAny,
actions: (model: string) =>
filterActions({
items: [
authPolicies.news.create && (
<CustomDropdownMenuItem
targetRoute={route(
`back.${model}.create`,
)}
icon={Views.CREATION.icon}
trans={transDefaultSideAction}
/>
),
authPolicies.news.viewTrash && (
<CustomDropdownMenuItem
targetRoute={route(
`back.${model}.trash`,
)}
icon={Views.ARCHIVATION.icon}
trans={str(transArchives)
.ucFirst()
.value()}
/>
),
authPolicies.newsCategories.viewAny && (
<CustomDropdownMenuItem
targetRoute={route(
`back.news-categories.index`,
)}
icon={FolderIcon}
trans={str(transNewsCategories)
.ucFirst()
.value()}
/>
),
authPolicies.newsTags.viewAny && (
<CustomDropdownMenuItem
targetRoute={route(
`back.news-tags.index`,
)}
icon={TagIcon}
trans={str(transNewsTags)
.ucFirst()
.value()}
/>
),
],
}),
},
{
title: str(transContactCount).ucFirst().value(),
model: "contact-counts",
icon: ContactIcon,
show: auth.policies.contact_counts.viewAny,
},
{
title: str(transDocuments).ucFirst().value(),
model: "documents",
activeCondition: [
route().current("back.document-categories.*"),
route().current("back.document-tags.*"),
],
icon: FileInputIcon,
show: auth.policies.documents.viewAny,
actions: (model: string) =>
filterActions({
items: [
authPolicies.documents.create && (
<CustomDropdownMenuItem
targetRoute={route(
`back.${model}.create`,
)}
icon={Views.CREATION.icon}
trans={transDefaultSideAction}
/>
),
authPolicies.documentCategories.create && (
<CustomDropdownMenuItem
icon={FolderIcon}
targetRoute={route(
`back.document-categories.index`,
)}
trans={str(transDocumentCategories)
.ucFirst()
.value()}
/>
),
authPolicies.documentTags.create && (
<CustomDropdownMenuItem
icon={TagIcon}
targetRoute={route(
`back.document-tags.index`,
)}
trans={str(transDocumentTags)
.ucFirst()
.value()}
/>
),
],
}),
},
{
title: str(transCompanies).ucFirst().plural().value(),
model: "companies",
activeCondition: [
route().current("back.company-activities.*"),
route().current("back.company-sectors.*"),
],
icon: Building2Icon,
show: auth.policies.companies.viewAny,
actions: (model: string) =>
filterActions({
items: [
authPolicies.companies.create && (
<CustomDropdownMenuItem
targetRoute={route(
`back.${model}.create`,
)}
icon={Views.CREATION.icon}
trans={transDefaultSideAction}
/>
),
authPolicies.companies.viewTrash && (
<CustomDropdownMenuItem
targetRoute={route(
`back.${model}.trash`,
)}
icon={Views.ARCHIVATION.icon}
trans={str(transArchives)
.ucFirst()
.value()}
/>
),
authPolicies.companyActivities.viewAny && (
<CustomDropdownMenuItem
icon={RollerCoasterIcon}
trans={str(transCompanyActivities)
.ucFirst()
.value()}
targetRoute={route(
`back.company-activities.index`,
)}
/>
),
authPolicies.companySectors.viewAny && (
<CustomDropdownMenuItem
icon={RadarIcon}
trans={str(transCompanySectors)
.ucFirst()
.value()}
targetRoute={route(
`back.company-sectors.index`,
)}
/>
),
],
}),
},
],
},
{
title: str(transAdmin).plural().value(),
show: auth.user.role === enumUserRole.conceptor.value,
links: [
{
title: str(transUsers).ucFirst().plural().value(),
model: "users",
icon: UsersIcon,
show: authPolicies.users.viewAny,
actions: (model: string) => filterActions({ model }),
},
{
title: str(transActivityLogs).ucFirst().plural().value(),
model: "activity-logs",
icon: HistoryIcon,
show: authPolicies.activityLogs.viewAny,
},
{
title: str(transPageConfigs).ucFirst().value(),
model: "page-configs",
show: authPolicies.pageConfigs.viewAny,
icon: CogIcon,
actions: (model: string) =>
filterActions({
items: [
authPolicies.pageConfigs.create && (
<CustomDropdownMenuItem
targetRoute={route(
`back.${model}.create`,
)}
icon={Views.CREATION.icon}
trans={transDefaultSideAction}
/>
),
authPolicies.pageConfigs.viewTrash && (
<CustomDropdownMenuItem
targetRoute={route(
`back.${model}.trash`,
)}
icon={Views.ARCHIVATION.icon}
trans={transArchives}
/>
),
],
}),
},
{
title: transTelescope,
link: route().has("telescope")
? route("telescope")
: undefined,
icon: TelescopeIcon,
show: appEnv.mode === "local" && route().has("telescope"),
},
],
},
];
useEffect(() => {
if (prevStateRef.current !== state) {
const elmts = document.querySelectorAll(".sidebar-transition");
elmts.forEach((element) => {
element.classList.add("animation-transition");
setTimeout(() => {
element.classList.remove("animation-transition");
}, 400);
});
prevStateRef.current = state;
}
}, [state]);
return (
<Sidebar collapsible="icon" className="z-30">
<SidebarHeader className="sidebar-transition">
<ApplicationLogo forceLight isCollapsed={isCollapsed} />
</SidebarHeader>
<SidebarContent
className={`sidebar-transition ${isCollapsed ? "pt-2" : ""}`}
>
{navList.map(
(navGroup) =>
navGroup.show && (
<Collapsible
key={navGroup.title}
className="group/collapsible"
defaultOpen
>
<SidebarGroup>
{!isCollapsed && (
<SidebarGroupLabel asChild>
<CollapsibleTrigger>
{navGroup.title}
<ChevronDownIcon className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
</CollapsibleTrigger>
</SidebarGroupLabel>
)}
<CollapsibleContent>
<SidebarGroupContent>
<SidebarMenu>
{navGroup.links
.filter(
(navLink) =>
navLink.show,
)
.map((navLink) => {
const isActive =
route().current(
`back.${navLink.model}.*`,
) ||
navLink.activeCondition?.some(
Boolean,
);
const sidebarButton = (
<SidebarMenuButton
className={
isActive
? "text-secondary-foreground"
: "text-primary-foreground"
}
asChild
>
{navLink.link ? (
<a
href={
navLink.link
}
target="_blank"
rel="noreferrer"
>
<navLink.icon />
{
navLink.title
}
</a>
) : (
<Link
href={route(
`back.${navLink.model}.index`,
)}
className="bg-transparent"
>
<navLink.icon />
{
navLink.title
}
</Link>
)}
</SidebarMenuButton>
);
return (
<SidebarMenuItem
key={
navLink.title
}
className={`transition-colors rounded-md ${isActive ? "bg-secondary text-secondary-foreground" : "bg-opacity-0"}`}
>
{isCollapsed ? (
<Tooltip>
<TooltipTrigger
asChild
>
{
sidebarButton
}
</TooltipTrigger>
<TooltipContent side="right">
<p>
{
navLink.title
}
</p>
</TooltipContent>
</Tooltip>
) : (
sidebarButton
)}
{navLink.model &&
navLink.actions?.(
navLink.model,
) && (
<DropdownMenu>
<DropdownMenuTrigger
asChild
>
<SidebarMenuAction
className={`${isActive ? "text-secondary-foreground" : ""} !top-0 !right-0 px-4 h-full`}
>
<MoreHorizontalIcon />
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
side="right"
align="start"
>
{navLink.actions(
navLink.model,
)}
</DropdownMenuContent>
</DropdownMenu>
)}
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroupContent>
</CollapsibleContent>
</SidebarGroup>
</Collapsible>
),
)}
</SidebarContent>
</Sidebar>
);
}