646 lines
30 KiB
TypeScript
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>
|
|
);
|
|
}
|