2024-04-24 12:12:47 +02:00
|
|
|
/*
|
|
|
|
GoToSocial
|
|
|
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
import React, { PropsWithChildren } from "react";
|
|
|
|
import { Link, useRoute } from "wouter";
|
|
|
|
import {
|
|
|
|
BaseUrlContext,
|
|
|
|
MenuLevelContext,
|
|
|
|
useBaseUrl,
|
|
|
|
useHasPermission,
|
|
|
|
useMenuLevel,
|
|
|
|
} from "./util";
|
|
|
|
import UserLogoutCard from "../../components/user-logout-card";
|
|
|
|
import { nanoid } from "nanoid";
|
|
|
|
|
|
|
|
export interface MenuItemProps {
|
|
|
|
/**
|
|
|
|
* Name / title of this menu item.
|
|
|
|
*/
|
|
|
|
name?: string;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Url path component for this menu item.
|
|
|
|
*/
|
|
|
|
itemUrl: string;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If this menu item is a category containing
|
|
|
|
* children, which child should be selected by
|
|
|
|
* default when category title is clicked.
|
|
|
|
*
|
|
|
|
* Optional, use for categories only.
|
|
|
|
*/
|
|
|
|
defaultChild?: string;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Permissions required to access this
|
|
|
|
* menu item (none, "moderator", "admin").
|
|
|
|
*/
|
|
|
|
permissions?: string[];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fork-awesome string to render
|
|
|
|
* icon for this menu item.
|
|
|
|
*/
|
|
|
|
icon?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function MenuItem(props: PropsWithChildren<MenuItemProps>) {
|
|
|
|
const {
|
|
|
|
name,
|
|
|
|
itemUrl,
|
|
|
|
defaultChild,
|
|
|
|
permissions,
|
|
|
|
icon,
|
|
|
|
children,
|
|
|
|
} = props;
|
|
|
|
|
|
|
|
// Derive where this item is
|
|
|
|
// in terms of URL routing.
|
|
|
|
const baseUrl = useBaseUrl();
|
|
|
|
const thisUrl = [ baseUrl, itemUrl ].join('/');
|
|
|
|
|
|
|
|
// Derive where this item is in
|
|
|
|
// terms of nesting within the menu.
|
|
|
|
const thisLevel = useMenuLevel();
|
|
|
|
const nextLevel = thisLevel+1;
|
|
|
|
const topLevel = thisLevel === 0;
|
|
|
|
|
|
|
|
// Check whether this item is currently active
|
|
|
|
// (ie., user has selected it in the menu).
|
|
|
|
//
|
|
|
|
// This uses a wildcard to mark both parent
|
|
|
|
// and relevant child as active.
|
|
|
|
//
|
|
|
|
// See:
|
|
|
|
// https://github.com/molefrog/wouter?tab=readme-ov-file#useroute-route-matching-and-parameters
|
|
|
|
const [isActive] = useRoute([ thisUrl, "*?" ].join("/"));
|
|
|
|
|
|
|
|
// Don't render item if logged-in user
|
|
|
|
// doesn't have permissions to use it.
|
|
|
|
if (!useHasPermission(permissions)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check whether this item has children.
|
|
|
|
const hasChildren = children !== undefined;
|
|
|
|
const childrenArray = hasChildren && Array.isArray(children);
|
|
|
|
|
|
|
|
// Class name of the item varies depending
|
|
|
|
// on where it is in the menu, and whether
|
|
|
|
// it has children beneath it or not.
|
|
|
|
const classNames: string[] = [];
|
|
|
|
if (topLevel) {
|
|
|
|
classNames.push("category", "top-level");
|
|
|
|
} else {
|
2024-11-21 14:09:58 +01:00
|
|
|
switch (true) {
|
|
|
|
case thisLevel === 1 && hasChildren:
|
|
|
|
classNames.push("category", "expanding");
|
|
|
|
break;
|
|
|
|
case thisLevel === 1 && !hasChildren:
|
|
|
|
classNames.push("view", "expanding");
|
|
|
|
break;
|
|
|
|
case thisLevel >= 2 && hasChildren:
|
|
|
|
classNames.push("nested", "category");
|
|
|
|
break;
|
|
|
|
case thisLevel >= 2 && !hasChildren:
|
|
|
|
classNames.push("nested", "view");
|
|
|
|
break;
|
2024-04-24 12:12:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isActive) {
|
|
|
|
classNames.push("active");
|
|
|
|
}
|
|
|
|
|
|
|
|
let content: React.JSX.Element | null;
|
|
|
|
if ((isActive || topLevel) && childrenArray) {
|
|
|
|
// Render children as a nested list.
|
|
|
|
content = <ul>{children}</ul>;
|
|
|
|
} else if (isActive && hasChildren) {
|
|
|
|
// Render child as solo element.
|
|
|
|
content = <>{children}</>;
|
|
|
|
} else {
|
|
|
|
// Not active: hide children.
|
|
|
|
content = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If a default child is defined, this item should point to that.
|
|
|
|
const href = defaultChild ? [ thisUrl, defaultChild ].join("/") : thisUrl;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<li key={nanoid()} className={classNames.join(" ")}>
|
|
|
|
<Link href={href} className="title">
|
|
|
|
<span>
|
|
|
|
{icon && <i className={`icon fa fa-fw ${icon}`} aria-hidden="true" />}
|
|
|
|
{name}
|
|
|
|
</span>
|
|
|
|
</Link>
|
|
|
|
{ content &&
|
|
|
|
<BaseUrlContext.Provider value={thisUrl}>
|
|
|
|
<MenuLevelContext.Provider value={nextLevel}>
|
|
|
|
{content}
|
|
|
|
</MenuLevelContext.Provider>
|
|
|
|
</BaseUrlContext.Provider>
|
|
|
|
}
|
|
|
|
</li>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface SidebarMenuProps{}
|
|
|
|
|
|
|
|
export function SidebarMenu({ children }: PropsWithChildren<SidebarMenuProps>) {
|
|
|
|
return (
|
|
|
|
<div className="sidebar">
|
|
|
|
<UserLogoutCard />
|
|
|
|
<nav className="menu-tree">
|
|
|
|
<MenuLevelContext.Provider value={0}>
|
|
|
|
<ul className="top-level">
|
|
|
|
{children}
|
|
|
|
</ul>
|
|
|
|
</MenuLevelContext.Provider>
|
|
|
|
</nav>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|