WIP Chakra site wrapper
This commit is contained in:
parent
8a6a355097
commit
eff9c45ea8
22 changed files with 262 additions and 876 deletions
|
@ -1,2 +1,4 @@
|
|||
PORT=9000
|
||||
IMAGE_INLINE_SIZE_LIMIT=20000
|
||||
REACT_APP_VERSION=development
|
||||
REACT_APP_COMMIT=abcdef1
|
|
@ -1,130 +0,0 @@
|
|||
import React, { ReactNode, useState } from "react";
|
||||
|
||||
import cn from "classnames";
|
||||
|
||||
import { AlertLink } from "./AlertLink";
|
||||
|
||||
export interface AlertProps {
|
||||
/**
|
||||
* Child elements within
|
||||
*/
|
||||
children?: ReactNode;
|
||||
/**
|
||||
* Additional Class
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* The type of this Alert, changes it's color
|
||||
*/
|
||||
type?: "info" | "success" | "warning" | "danger";
|
||||
/**
|
||||
* Alert Title
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* An Icon to be displayed on the right hand side of the Alert
|
||||
*/
|
||||
icon?: ReactNode;
|
||||
/**
|
||||
* Display an Avatar on the left hand side of this Alert
|
||||
*/
|
||||
avatar?: ReactNode;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
important?: boolean;
|
||||
/**
|
||||
* Adds an 'X' to the right side of the Alert that dismisses the Alert
|
||||
*/
|
||||
dismissable?: boolean;
|
||||
/**
|
||||
* Event to call after dissmissing
|
||||
*/
|
||||
onDismissClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
export const Alert: React.FC<AlertProps> = ({
|
||||
children,
|
||||
className,
|
||||
type = "info",
|
||||
title,
|
||||
icon,
|
||||
avatar,
|
||||
important = false,
|
||||
dismissable = false,
|
||||
onDismissClick,
|
||||
}) => {
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
|
||||
const classes = {
|
||||
"alert-dismissible": dismissable,
|
||||
"alert-important": important,
|
||||
};
|
||||
|
||||
const handleDismissed = (
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
||||
) => {
|
||||
setDismissed(true);
|
||||
onDismissClick && onDismissClick(e);
|
||||
};
|
||||
|
||||
const wrappedTitle = title ? <h4 className="alert-title">{title}</h4> : null;
|
||||
const wrappedChildren =
|
||||
children && !important ? (
|
||||
<div className="text-muted">{children}</div>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
|
||||
const wrapIfIcon = (): ReactNode => {
|
||||
if (avatar) {
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<div>
|
||||
<span className="float-start me-3">{avatar}</span>
|
||||
</div>
|
||||
<div>{wrappedChildren}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (icon) {
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<div>
|
||||
<span className="alert-icon">{icon}</span>
|
||||
</div>
|
||||
<div>
|
||||
{wrappedTitle}
|
||||
{wrappedChildren}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{wrappedTitle}
|
||||
{wrappedChildren}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
if (!dismissed) {
|
||||
return (
|
||||
<div
|
||||
className={cn("alert", `alert-${type}`, classes, className)}
|
||||
role="alert">
|
||||
{wrapIfIcon()}
|
||||
{dismissable ? (
|
||||
<button
|
||||
className="btn-close"
|
||||
data-bs-dismiss="alert"
|
||||
aria-label="close"
|
||||
onClick={handleDismissed}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
Alert.Link = AlertLink;
|
|
@ -1,34 +0,0 @@
|
|||
import React, { ReactNode } from "react";
|
||||
|
||||
import cn from "classnames";
|
||||
|
||||
export interface AlertLinkProps {
|
||||
/**
|
||||
* Child elements within
|
||||
*/
|
||||
children?: ReactNode;
|
||||
/**
|
||||
* Additional Class
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* Href
|
||||
*/
|
||||
href?: string;
|
||||
/**
|
||||
* onClick handler
|
||||
*/
|
||||
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
|
||||
}
|
||||
export const AlertLink: React.FC<AlertLinkProps> = ({
|
||||
children,
|
||||
className,
|
||||
href,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<a className={cn("alert-link", className)} href={href} onClick={onClick}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export * from "./Alert";
|
|
@ -1,80 +1,77 @@
|
|||
import React from "react";
|
||||
|
||||
import { useHealthState } from "context";
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Stack,
|
||||
Text,
|
||||
useColorModeValue,
|
||||
} from "@chakra-ui/react";
|
||||
import { intl } from "locale";
|
||||
|
||||
function Footer() {
|
||||
const { health } = useHealthState();
|
||||
|
||||
return (
|
||||
<footer className="footer footer-transparent d-print-none">
|
||||
<div className="container">
|
||||
<div className="row text-center align-items-center flex-row-reverse">
|
||||
<div className="col-lg-auto ms-lg-auto">
|
||||
<ul className="list-inline list-inline-dots mb-0">
|
||||
<li className="list-inline-item">
|
||||
<a
|
||||
href="https://nginxproxymanager.com?utm_source=npm"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="link-secondary">
|
||||
{intl.formatMessage({
|
||||
id: "footer.userguide",
|
||||
defaultMessage: "User Guide",
|
||||
})}
|
||||
</a>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<a
|
||||
href="https://github.com/jc21/nginx-proxy-manager/releases?utm_source=npm"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="link-secondary">
|
||||
{intl.formatMessage({
|
||||
id: "footer.changelog",
|
||||
defaultMessage: "Change Log",
|
||||
})}
|
||||
</a>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<a
|
||||
href="https://github.com/jc21/nginx-proxy-manager?utm_source=npm"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="link-secondary">
|
||||
{intl.formatMessage({
|
||||
id: "footer.github",
|
||||
defaultMessage: "Github",
|
||||
})}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="col-12 col-lg-auto mt-3 mt-lg-0">
|
||||
<ul className="list-inline list-inline-dots mb-0">
|
||||
<li className="list-inline-item">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "footer.copyright",
|
||||
defaultMessage: "Copyright © {year} jc21.com",
|
||||
},
|
||||
{ year: new Date().getFullYear() },
|
||||
)}
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<a
|
||||
href="https://github.com/jc21/nginx-proxy-manager/releases?utm_source=npm"
|
||||
target="_blank"
|
||||
className="link-secondary"
|
||||
rel="noopener noreferrer">
|
||||
v{health.version} {String.fromCharCode(183)} {health.commit}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<Box
|
||||
bg={useColorModeValue("gray.50", "gray.900")}
|
||||
color={useColorModeValue("gray.700", "gray.200")}>
|
||||
<Container
|
||||
as={Stack}
|
||||
maxW={"6xl"}
|
||||
py={4}
|
||||
direction={{ base: "column", md: "row" }}
|
||||
spacing={4}
|
||||
justify={{ base: "center", md: "space-between" }}
|
||||
align={{ base: "center", md: "center" }}>
|
||||
<Text>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "footer.copyright",
|
||||
defaultMessage: "Copyright © {year} jc21.com",
|
||||
},
|
||||
{ year: new Date().getFullYear() },
|
||||
)}
|
||||
</Text>
|
||||
<Stack direction={"row"} spacing={6}>
|
||||
<a
|
||||
href="https://nginxproxymanager.com?utm_source=npm"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="link-secondary">
|
||||
{intl.formatMessage({
|
||||
id: "footer.userguide",
|
||||
defaultMessage: "User Guide",
|
||||
})}
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/jc21/nginx-proxy-manager/releases?utm_source=npm"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="link-secondary">
|
||||
{intl.formatMessage({
|
||||
id: "footer.changelog",
|
||||
defaultMessage: "Change Log",
|
||||
})}
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/jc21/nginx-proxy-manager?utm_source=npm"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="link-secondary">
|
||||
{intl.formatMessage({
|
||||
id: "footer.github",
|
||||
defaultMessage: "Github",
|
||||
})}
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/jc21/nginx-proxy-manager/releases?utm_source=npm"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
v{process.env.REACT_APP_VERSION} {String.fromCharCode(183)}{" "}
|
||||
{process.env.REACT_APP_COMMIT}
|
||||
</a>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
99
frontend/src/components/Nav/Nav.tsx
Normal file
99
frontend/src/components/Nav/Nav.tsx
Normal file
|
@ -0,0 +1,99 @@
|
|||
import React from "react";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Avatar,
|
||||
HStack,
|
||||
IconButton,
|
||||
Button,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
MenuDivider,
|
||||
useDisclosure,
|
||||
useColorModeValue,
|
||||
Stack,
|
||||
} from "@chakra-ui/react";
|
||||
import { LocalePicker, ThemeSwitcher } from "components";
|
||||
import { FaBars, FaTimes } from "react-icons/fa";
|
||||
|
||||
import logo from "../../img/logo-256.png";
|
||||
import { NavLink } from "./NavLink";
|
||||
|
||||
const Links = ["Dashboard", "Projects", "Team"];
|
||||
|
||||
export const Nav = () => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box bg={useColorModeValue("gray.100", "gray.900")} px={4}>
|
||||
<Flex h={16} alignItems={"center"} justifyContent={"space-between"}>
|
||||
<IconButton
|
||||
size={"md"}
|
||||
icon={isOpen ? <FaTimes /> : <FaBars />}
|
||||
aria-label={"Open Menu"}
|
||||
display={{ md: "none" }}
|
||||
onClick={isOpen ? onClose : onOpen}
|
||||
/>
|
||||
<HStack spacing={8} alignItems={"center"}>
|
||||
<Box>
|
||||
<img src={logo} width={32} alt="Logo" />
|
||||
</Box>
|
||||
<HStack
|
||||
as={"nav"}
|
||||
spacing={4}
|
||||
display={{ base: "none", md: "flex" }}>
|
||||
{Links.map((link) => (
|
||||
<NavLink key={link}>{link}</NavLink>
|
||||
))}
|
||||
</HStack>
|
||||
</HStack>
|
||||
<Flex alignItems={"center"}>
|
||||
<Stack h={10} m={4} justify={"end"} direction={"row"}>
|
||||
<ThemeSwitcher />
|
||||
<LocalePicker className="text-right" />
|
||||
</Stack>
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
rounded={"full"}
|
||||
variant={"link"}
|
||||
cursor={"pointer"}
|
||||
minW={0}>
|
||||
<Avatar
|
||||
size={"sm"}
|
||||
src={
|
||||
"https://images.unsplash.com/photo-1493666438817-866a91353ca9?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&fit=crop&h=200&w=200&s=b616b2c5b373a80ffc9636ba24f7a4a9"
|
||||
}
|
||||
/>
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem>Link 1</MenuItem>
|
||||
<MenuItem>Link 2</MenuItem>
|
||||
<MenuDivider />
|
||||
<MenuItem>Link 3</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{isOpen ? (
|
||||
<Box pb={4} display={{ md: "none" }}>
|
||||
<Stack as={"nav"} spacing={4}>
|
||||
{Links.map((link) => (
|
||||
<NavLink key={link}>{link}</NavLink>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
|
||||
<Box p={4}>Main Content Here</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Nav.Link = NavLink;
|
25
frontend/src/components/Nav/NavLink.tsx
Normal file
25
frontend/src/components/Nav/NavLink.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React, { ReactNode } from "react";
|
||||
|
||||
import { Link, useColorModeValue } from "@chakra-ui/react";
|
||||
|
||||
export interface NavLinkProps {
|
||||
/**
|
||||
* Child elements within
|
||||
*/
|
||||
children?: ReactNode;
|
||||
}
|
||||
export const NavLink: React.FC<NavLinkProps> = ({ children }) => {
|
||||
return (
|
||||
<Link
|
||||
px={2}
|
||||
py={1}
|
||||
rounded={"md"}
|
||||
_hover={{
|
||||
textDecoration: "none",
|
||||
bg: useColorModeValue("gray.200", "gray.700"),
|
||||
}}
|
||||
href={"#"}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
1
frontend/src/components/Nav/index.ts
Normal file
1
frontend/src/components/Nav/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./Nav";
|
|
@ -1,104 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
import { Dropdown, Navigation } from "components";
|
||||
import { intl } from "locale";
|
||||
import {
|
||||
Book,
|
||||
DeviceDesktop,
|
||||
Home,
|
||||
Lock,
|
||||
Settings,
|
||||
Shield,
|
||||
Users,
|
||||
} from "tabler-icons-react";
|
||||
|
||||
const NavMenu: React.FC<{ openOnMobile: boolean }> = ({ openOnMobile }) => {
|
||||
return (
|
||||
<Navigation.Menu
|
||||
theme="light"
|
||||
className="mb-3"
|
||||
withinHeader={true}
|
||||
openOnMobile={openOnMobile}
|
||||
items={[
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
id: "dashboard.title",
|
||||
defaultMessage: "Dashboard",
|
||||
}),
|
||||
icon: <Home />,
|
||||
to: "/",
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
id: "hosts.title",
|
||||
defaultMessage: "Hosts",
|
||||
}),
|
||||
icon: <DeviceDesktop />,
|
||||
to: "/hosts",
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
id: "accesslists.title",
|
||||
defaultMessage: "Access Lists",
|
||||
}),
|
||||
icon: <Lock />,
|
||||
to: "/access-lists",
|
||||
},
|
||||
{
|
||||
title: "SSL",
|
||||
icon: <Shield />,
|
||||
dropdownItems: [
|
||||
<Dropdown.Item
|
||||
key="ssl-certificates"
|
||||
to="/ssl/certificates"
|
||||
role="button">
|
||||
<span className="nav-link-title">
|
||||
{intl.formatMessage({
|
||||
id: "certificates.title",
|
||||
defaultMessage: "Certificates",
|
||||
})}
|
||||
</span>
|
||||
</Dropdown.Item>,
|
||||
<Dropdown.Item
|
||||
key="ssl-authorities"
|
||||
to="/ssl/authorities"
|
||||
role="button">
|
||||
<span className="nav-link-title">
|
||||
{intl.formatMessage({
|
||||
id: "cert_authorities.title",
|
||||
defaultMessage: "Certificate Authorities",
|
||||
})}
|
||||
</span>
|
||||
</Dropdown.Item>,
|
||||
],
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
id: "auditlog.title",
|
||||
defaultMessage: "Audit Log",
|
||||
}),
|
||||
icon: <Book />,
|
||||
to: "/audit-log",
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
id: "users.title",
|
||||
defaultMessage: "Users",
|
||||
}),
|
||||
icon: <Users />,
|
||||
to: "/users",
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
id: "settings.title",
|
||||
defaultMessage: "Settings",
|
||||
}),
|
||||
icon: <Settings />,
|
||||
to: "/settings",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { NavMenu };
|
|
@ -1,19 +0,0 @@
|
|||
import { ReactNode } from "react";
|
||||
|
||||
import { NavigationHeader } from "./NavigationHeader";
|
||||
import { NavigationMenu } from "./NavigationMenu";
|
||||
import { NavigationMenuItem } from "./NavigationMenuItem";
|
||||
|
||||
export interface NavigationProps {
|
||||
/**
|
||||
* Child elements within
|
||||
*/
|
||||
children?: ReactNode;
|
||||
}
|
||||
export const Navigation = ({ children }: NavigationProps) => {
|
||||
return children;
|
||||
};
|
||||
|
||||
Navigation.Header = NavigationHeader;
|
||||
Navigation.Menu = NavigationMenu;
|
||||
Navigation.MenuItem = NavigationMenuItem;
|
|
@ -1,211 +0,0 @@
|
|||
import React, { ReactNode, useState, useRef, useEffect } from "react";
|
||||
|
||||
import cn from "classnames";
|
||||
import { Bell } from "tabler-icons-react";
|
||||
|
||||
import { NavMenu } from "..";
|
||||
import { Badge } from "../Badge";
|
||||
import { ButtonList } from "../ButtonList";
|
||||
import { Dropdown } from "../Dropdown";
|
||||
import { NavigationMenu } from "./NavigationMenu";
|
||||
import { NavigationMenuItemProps } from "./NavigationMenuItem";
|
||||
export interface NavigationHeaderProps {
|
||||
/**
|
||||
* Additional Class
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* Logo and/or Text elements to show on the left brand side of the header
|
||||
*/
|
||||
brandContent?: ReactNode;
|
||||
/**
|
||||
* Color theme for the nav bar
|
||||
*/
|
||||
theme?: "transparent" | "light" | "dark";
|
||||
/**
|
||||
* Buttons to show in the header
|
||||
*/
|
||||
buttons?: ReactNode[];
|
||||
/**
|
||||
* Notifications Content
|
||||
*/
|
||||
notifications?: ReactNode;
|
||||
/**
|
||||
* Has unread notifications, shows red dot
|
||||
*/
|
||||
hasUnreadNotifications?: boolean;
|
||||
/**
|
||||
* Avatar Object
|
||||
*/
|
||||
avatar?: ReactNode;
|
||||
/**
|
||||
* Profile name to show next to avatar
|
||||
*/
|
||||
profileName?: string;
|
||||
/**
|
||||
* Profile text to show beneath profileName
|
||||
*/
|
||||
profileSubName?: string;
|
||||
/**
|
||||
* Profile dropdown menu items
|
||||
*/
|
||||
profileItems?: ReactNode[];
|
||||
/**
|
||||
* Applies dark theme to Notifications and Profile dropdowns
|
||||
*/
|
||||
darkDropdowns?: boolean;
|
||||
/**
|
||||
* Navigation Menu within this Header
|
||||
*/
|
||||
menuItems?: NavigationMenuItemProps[];
|
||||
}
|
||||
export const NavigationHeader: React.FC<NavigationHeaderProps> = ({
|
||||
className,
|
||||
theme = "transparent",
|
||||
brandContent,
|
||||
buttons,
|
||||
notifications,
|
||||
hasUnreadNotifications,
|
||||
avatar,
|
||||
profileName,
|
||||
profileSubName,
|
||||
profileItems,
|
||||
darkDropdowns,
|
||||
menuItems,
|
||||
}) => {
|
||||
const [notificationsShown, setNotificationsShown] = useState(false);
|
||||
const [profileShown, setProfileShown] = useState(false);
|
||||
const [mobileNavShown, setMobileNavShown] = useState(false);
|
||||
const profileRef = useRef(null);
|
||||
const notificationsRef = useRef(null);
|
||||
|
||||
const toggleMobileNavShown = () =>
|
||||
setMobileNavShown((prevState) => {
|
||||
return !prevState;
|
||||
});
|
||||
|
||||
const handleClickOutside = (event: any) => {
|
||||
if (
|
||||
profileRef.current &&
|
||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
||||
!profileRef.current.contains(event.target)
|
||||
) {
|
||||
setProfileShown(false);
|
||||
}
|
||||
if (
|
||||
notificationsRef.current &&
|
||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
||||
!notificationsRef.current.contains(event.target)
|
||||
) {
|
||||
setNotificationsShown(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
className={cn(
|
||||
`navbar navbar-expand-md navbar-${theme} d-print-none`,
|
||||
className,
|
||||
)}>
|
||||
<div className="container-xl">
|
||||
<button
|
||||
className="navbar-toggler"
|
||||
type="button"
|
||||
onClick={toggleMobileNavShown}>
|
||||
<span className="navbar-toggler-icon" />
|
||||
</button>
|
||||
<h1 className="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
|
||||
{brandContent}
|
||||
</h1>
|
||||
<div className="navbar-nav flex-row order-md-last">
|
||||
{buttons ? (
|
||||
<div className="nav-item d-none d-md-flex me-3">
|
||||
<ButtonList>{buttons}</ButtonList>
|
||||
</div>
|
||||
) : null}
|
||||
{notifications ? (
|
||||
<div
|
||||
className="nav-item dropdown d-none d-md-flex me-3"
|
||||
ref={notificationsRef}>
|
||||
<button
|
||||
style={{
|
||||
border: 0,
|
||||
backgroundColor: "transparent",
|
||||
}}
|
||||
className="nav-link px-0"
|
||||
aria-label="Show notifications"
|
||||
onClick={() => {
|
||||
setNotificationsShown(!notificationsShown);
|
||||
}}>
|
||||
<Bell className="icon" />
|
||||
{hasUnreadNotifications ? <Badge color="red" /> : null}
|
||||
</button>
|
||||
<Dropdown
|
||||
className="dropdown-menu-end dropdown-menu-card"
|
||||
show={notificationsShown}
|
||||
dark={darkDropdowns}>
|
||||
<div className="card">
|
||||
<div className="card-body">{notifications}</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
ref={profileRef}
|
||||
className={cn("nav-item", {
|
||||
dropdown: !!profileItems,
|
||||
})}>
|
||||
<button
|
||||
style={{
|
||||
border: 0,
|
||||
backgroundColor: "transparent",
|
||||
}}
|
||||
className="nav-link d-flex lh-1 text-reset p-0"
|
||||
aria-label={profileItems && "Open user menu"}
|
||||
onClick={() => {
|
||||
setProfileShown(!profileShown);
|
||||
}}>
|
||||
{avatar}
|
||||
{profileName ? (
|
||||
<div className="d-none d-xl-block ps-2">
|
||||
<div style={{ textAlign: "left" }}>{profileName}</div>
|
||||
{profileSubName ? (
|
||||
<div
|
||||
className="mt-1 small text-muted"
|
||||
style={{ textAlign: "left" }}>
|
||||
{profileSubName}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</button>
|
||||
{profileItems ? (
|
||||
<Dropdown
|
||||
className="dropdown-menu-end dropdown-menu-card"
|
||||
show={profileShown}
|
||||
dark={darkDropdowns}
|
||||
arrow>
|
||||
{profileItems}
|
||||
</Dropdown>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{menuItems ? (
|
||||
<NavigationMenu
|
||||
items={menuItems}
|
||||
withinHeader
|
||||
openOnMobile={false}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</header>
|
||||
<NavMenu openOnMobile={mobileNavShown} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,130 +0,0 @@
|
|||
import React, { ReactNode, useState, useRef, useEffect } from "react";
|
||||
|
||||
import cn from "classnames";
|
||||
import styled from "styled-components";
|
||||
|
||||
import {
|
||||
NavigationMenuItem,
|
||||
NavigationMenuItemProps,
|
||||
} from "./NavigationMenuItem";
|
||||
|
||||
/**
|
||||
* This menu handles the state of the dropdowns being shown, instead of state
|
||||
* being handled within the NavigationItem object, because we want the behaviour
|
||||
* of clicking one menu item with a dropdown to close the already open dropdown
|
||||
* of another menu item. This can only be done if we handle state one level above
|
||||
* the items.
|
||||
*/
|
||||
|
||||
const StyledNavWrapper = styled.div<{ shown: boolean }>`
|
||||
@media (max-width: 767.98px) {
|
||||
transition: max-height 300ms ease-in-out;
|
||||
max-height: ${(p) => (p.shown ? `80vh` : `0`)};
|
||||
min-height: ${(p) => (p.shown ? `inherit` : `0`)};
|
||||
overflow: hidden;
|
||||
padding: ${(p) => (p.shown ? `inherit` : `0`)};
|
||||
}
|
||||
`;
|
||||
|
||||
export interface NavigationMenuProps {
|
||||
/** Additional Class */
|
||||
className?: string;
|
||||
/** Navigation Items */
|
||||
items: NavigationMenuItemProps[];
|
||||
/** If this menu sits within a Navigation.Header */
|
||||
withinHeader?: boolean;
|
||||
/** Color theme for the nav bar */
|
||||
theme?: "transparent" | "light" | "dark";
|
||||
/** Search content */
|
||||
searchContent?: ReactNode;
|
||||
/** Navigation is currently hidden on mobile */
|
||||
openOnMobile?: boolean;
|
||||
}
|
||||
export const NavigationMenu: React.FC<NavigationMenuProps> = ({
|
||||
className,
|
||||
items,
|
||||
withinHeader,
|
||||
theme = "transparent",
|
||||
searchContent,
|
||||
openOnMobile = true,
|
||||
}) => {
|
||||
const [dropdownShown, setDropdownShown] = useState(0);
|
||||
const navRef = useRef(null);
|
||||
|
||||
const handleClickOutside = (event: any) => {
|
||||
if (
|
||||
navRef.current &&
|
||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
||||
!navRef.current.contains(event.target)
|
||||
) {
|
||||
setDropdownShown(0);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const itemClicked = (
|
||||
e: React.MouseEvent<HTMLAnchorElement, MouseEvent>,
|
||||
item: NavigationMenuItemProps,
|
||||
idx: number,
|
||||
) => {
|
||||
setDropdownShown(dropdownShown === idx ? 0 : idx);
|
||||
item.onClick && item.onClick(e);
|
||||
};
|
||||
|
||||
const wrapMenu = (el: ReactNode) => {
|
||||
if (withinHeader) {
|
||||
return (
|
||||
<div className="navbar-expand-md">
|
||||
<StyledNavWrapper
|
||||
shown={openOnMobile}
|
||||
className={cn(`navbar navbar-${theme} navbar-collapse`, className)}>
|
||||
<div className="container-xl">{el}</div>
|
||||
</StyledNavWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={"navbar-expand-md"}>
|
||||
<div
|
||||
className={cn(`navbar navbar-${theme}`, className)}
|
||||
id="navbar-menu">
|
||||
<div className="container-xl">
|
||||
{el}
|
||||
{searchContent ? (
|
||||
<div className="my-2 my-md-0 flex-grow-1 flex-md-grow-0 order-first order-md-last">
|
||||
{searchContent}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return wrapMenu(
|
||||
<ul className="navbar-nav" ref={navRef}>
|
||||
{items.map((item: any, idx: number) => {
|
||||
const onClickItem = (
|
||||
e: React.MouseEvent<HTMLAnchorElement, MouseEvent>,
|
||||
) => {
|
||||
itemClicked(e, item, idx);
|
||||
};
|
||||
return (
|
||||
<NavigationMenuItem
|
||||
key={`navmenu-${idx}`}
|
||||
onClick={onClickItem}
|
||||
dropdownShow={dropdownShown === idx}
|
||||
activeOnlyWhenExact
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>,
|
||||
);
|
||||
};
|
||||
|
||||
NavigationMenu.Item = NavigationMenuItem;
|
|
@ -1,122 +0,0 @@
|
|||
import React, { ReactNode } from "react";
|
||||
|
||||
import cn from "classnames";
|
||||
import { Link, useRouteMatch } from "react-router-dom";
|
||||
|
||||
import { Dropdown } from "../Dropdown";
|
||||
|
||||
export interface NavigationMenuItemProps {
|
||||
/**
|
||||
* Additional Class
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* An Icon to be displayed on the right hand side of the Alert
|
||||
*/
|
||||
icon?: ReactNode;
|
||||
/**
|
||||
* Title of the Item
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Href if this is navigating somewhere
|
||||
*/
|
||||
href?: string;
|
||||
/**
|
||||
* target property, only used when href is set
|
||||
*/
|
||||
target?: string;
|
||||
/**
|
||||
* Router Link to if using react-router-dom
|
||||
*/
|
||||
to?: string;
|
||||
/**
|
||||
* Router Link property if using react-router-dom
|
||||
*/
|
||||
activeOnlyWhenExact?: boolean;
|
||||
/**
|
||||
* On click handler
|
||||
*/
|
||||
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
|
||||
/**
|
||||
* Provide dropdown items if this is to be a dropdown menu
|
||||
*/
|
||||
dropdownItems?: ReactNode[];
|
||||
/**
|
||||
* State of the dropdown being shown
|
||||
*/
|
||||
dropdownShow?: boolean;
|
||||
/**
|
||||
* Applies dark theme to dropdown
|
||||
*/
|
||||
darkDropdown?: boolean;
|
||||
/**
|
||||
* Shows this item as being active
|
||||
*/
|
||||
active?: boolean;
|
||||
/**
|
||||
* Disables the menu item
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* Badge if you want to show one
|
||||
*/
|
||||
badge?: ReactNode;
|
||||
}
|
||||
export const NavigationMenuItem: React.FC<NavigationMenuItemProps> = ({
|
||||
className,
|
||||
icon,
|
||||
title,
|
||||
href,
|
||||
target,
|
||||
to,
|
||||
activeOnlyWhenExact,
|
||||
onClick,
|
||||
dropdownItems,
|
||||
dropdownShow,
|
||||
darkDropdown,
|
||||
active,
|
||||
disabled,
|
||||
badge,
|
||||
}) => {
|
||||
const match = useRouteMatch({
|
||||
path: to,
|
||||
exact: activeOnlyWhenExact,
|
||||
});
|
||||
|
||||
return (
|
||||
<li
|
||||
className={cn(
|
||||
"nav-item",
|
||||
dropdownItems && "dropdown",
|
||||
{ active: match || active },
|
||||
className,
|
||||
)}>
|
||||
<Link
|
||||
to={to ?? ""}
|
||||
className={cn(
|
||||
"nav-link",
|
||||
dropdownItems && "dropdown-toggle",
|
||||
disabled && "disabled",
|
||||
)}
|
||||
href={href}
|
||||
target={target}
|
||||
role="button"
|
||||
aria-expanded="false"
|
||||
onClick={onClick}>
|
||||
{icon && (
|
||||
<span className="nav-link-icon d-md-none d-lg-inline-block">
|
||||
{icon}
|
||||
</span>
|
||||
)}
|
||||
<span className="nav-link-title">{title}</span>
|
||||
{badge}
|
||||
</Link>
|
||||
{dropdownItems ? (
|
||||
<Dropdown show={dropdownShow} dark={darkDropdown} arrow>
|
||||
{dropdownItems}
|
||||
</Dropdown>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export * from "./Navigation";
|
|
@ -1,10 +1,10 @@
|
|||
import React, { ReactNode } from "react";
|
||||
|
||||
import { Footer } from "components";
|
||||
import { Avatar, Dropdown, Navigation } from "components";
|
||||
import { LocalePicker } from "components";
|
||||
import { useAuthState, useUserState } from "context";
|
||||
import { intl } from "locale";
|
||||
import { Nav } from "components";
|
||||
// import { LocalePicker } from "components";
|
||||
// import { useAuthState, useUserState } from "context";
|
||||
// import { intl } from "locale";
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledSiteContainer = styled.div`
|
||||
|
@ -29,13 +29,14 @@ interface Props {
|
|||
children?: ReactNode;
|
||||
}
|
||||
function SiteWrapper({ children }: Props) {
|
||||
const user = useUserState();
|
||||
const { logout } = useAuthState();
|
||||
// const user = useUserState();
|
||||
// const { logout } = useAuthState();
|
||||
|
||||
return (
|
||||
<StyledSiteContainer className="wrapper">
|
||||
<StyledScrollContainer>
|
||||
<Navigation.Header
|
||||
<Nav />
|
||||
{/*
|
||||
theme="light"
|
||||
brandContent={
|
||||
<img
|
||||
|
@ -75,6 +76,7 @@ function SiteWrapper({ children }: Props) {
|
|||
</Dropdown.Item>,
|
||||
]}
|
||||
/>
|
||||
*/}
|
||||
<div className="content">
|
||||
<div className="container-xl">
|
||||
<StyledContentContainer>{children}</StyledContentContainer>
|
||||
|
|
|
@ -3,11 +3,13 @@ import React from "react";
|
|||
import { Button, Icon, useColorMode } from "@chakra-ui/react";
|
||||
import { FiSun, FiMoon } from "react-icons/fi";
|
||||
|
||||
export const ThemeSwitcher: React.FC = () => {
|
||||
function ThemeSwitcher() {
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
return (
|
||||
<Button onClick={toggleColorMode}>
|
||||
{colorMode === "light" ? <Icon as={FiMoon} /> : <Icon as={FiSun} />}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export { ThemeSwitcher };
|
||||
|
|
|
@ -1,44 +1,45 @@
|
|||
import React from "react";
|
||||
|
||||
import { Alert } from "components";
|
||||
import styled from "styled-components";
|
||||
|
||||
const Root = styled.div`
|
||||
padding: 20vh 10vw 0 10vw;
|
||||
|
||||
&& .ant-alert-warning {
|
||||
background-color: #2a2a2a;
|
||||
border: 2px solid #2ab1a4;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
&& .ant-alert-message {
|
||||
color: #fff;
|
||||
font-size: 6vh;
|
||||
}
|
||||
|
||||
&& .ant-alert-description {
|
||||
font-size: 4vh;
|
||||
line-height: 5vh;
|
||||
}
|
||||
|
||||
&& .ant-alert-with-description {
|
||||
padding-left: 23vh;
|
||||
}
|
||||
|
||||
&& .ant-alert-with-description .ant-alert-icon {
|
||||
font-size: 15vh;
|
||||
}
|
||||
`;
|
||||
import { Box, Flex, Heading, Text, Stack } from "@chakra-ui/react";
|
||||
import { LocalePicker } from "components";
|
||||
import { intl } from "locale";
|
||||
import { FaTimes } from "react-icons/fa";
|
||||
|
||||
function Unhealthy() {
|
||||
return (
|
||||
<Root>
|
||||
<Alert type="warning" icon="alert-triangle">
|
||||
Nginx Proxy Manager is <strong>unhealthy</strong>. We'll continue to
|
||||
check the health and hope to be back up and running soon!
|
||||
</Alert>
|
||||
</Root>
|
||||
<>
|
||||
<Stack h={10} m={4} justify={"end"} direction={"row"}>
|
||||
<LocalePicker className="text-right" />
|
||||
</Stack>
|
||||
<Box textAlign="center" py={10} px={6}>
|
||||
<Box display="inline-block">
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
bg={"red.500"}
|
||||
rounded={"50px"}
|
||||
w={"55px"}
|
||||
h={"55px"}
|
||||
textAlign="center">
|
||||
<FaTimes size={"30px"} color={"white"} />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Heading as="h2" size="xl" mt={6} mb={2}>
|
||||
{intl.formatMessage({
|
||||
id: "unhealthy.title",
|
||||
defaultMessage: "Nginx Proxy Manager is unhealthy",
|
||||
})}
|
||||
</Heading>
|
||||
<Text color={"gray.500"}>
|
||||
{intl.formatMessage({
|
||||
id: "unhealthy.body",
|
||||
defaultMessage:
|
||||
"We'll continue to check the health and hope to be back up and running soon!",
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
export * from "./Alert";
|
||||
export * from "./Avatar";
|
||||
export * from "./AvatarList";
|
||||
export * from "./Badge";
|
||||
|
@ -10,11 +9,11 @@ export * from "./Footer";
|
|||
export * from "./Loader";
|
||||
export * from "./Loading";
|
||||
export * from "./LocalePicker";
|
||||
export * from "./Navigation";
|
||||
export * from "./NavMenu";
|
||||
export * from "./Nav";
|
||||
export * from "./Router";
|
||||
export * from "./SinglePage";
|
||||
export * from "./SiteWrapper";
|
||||
export * from "./SuspenseLoader";
|
||||
export * from "./Table";
|
||||
export * from "./ThemeSwitcher";
|
||||
export * from "./Unhealthy";
|
||||
|
|
|
@ -80,6 +80,12 @@
|
|||
"setup-required": {
|
||||
"defaultMessage": "Setup Required"
|
||||
},
|
||||
"unhealthy.title": {
|
||||
"defaultMessage": "Nginx Proxy Manager is unhealthy"
|
||||
},
|
||||
"unhealthy.body": {
|
||||
"defaultMessage": "We'll continue to check the health and hope to be back up and running soon!"
|
||||
},
|
||||
"user.email": {
|
||||
"defaultMessage": "Email"
|
||||
},
|
||||
|
|
|
@ -12,11 +12,10 @@ import {
|
|||
useToast,
|
||||
Link,
|
||||
} from "@chakra-ui/react";
|
||||
import { LocalePicker } from "components";
|
||||
import { LocalePicker, ThemeSwitcher } from "components";
|
||||
import { useAuthState } from "context";
|
||||
import { intl } from "locale";
|
||||
|
||||
import { ThemeSwitcher } from "../../components/ThemeSwitcher";
|
||||
import logo from "../../img/logo-256.png";
|
||||
|
||||
function Login() {
|
||||
|
|
|
@ -11,11 +11,10 @@ import {
|
|||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { createUser } from "api/npm";
|
||||
import { LocalePicker } from "components";
|
||||
import { LocalePicker, ThemeSwitcher } from "components";
|
||||
import { useAuthState, useHealthState } from "context";
|
||||
import { intl } from "locale";
|
||||
|
||||
import { ThemeSwitcher } from "../../components/ThemeSwitcher";
|
||||
import logo from "../../img/logo-256.png";
|
||||
|
||||
function Setup() {
|
||||
|
|
|
@ -7,7 +7,13 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||
. "$DIR/../.common.sh"
|
||||
|
||||
docker_cmd() {
|
||||
docker run --rm -e CI=true -v "$(pwd)/frontend:/app/frontend" -w /app/frontend node:14 ${*}
|
||||
docker run --rm \
|
||||
-e CI=true \
|
||||
-e REACT_APP_VERSION=${BUILD_VERSION:-0.0.0} \
|
||||
-e REACT_APP_COMMIT=${BUILD_COMMIT:-0000000} \
|
||||
-v "$(pwd)/frontend:/app/frontend" \
|
||||
-w /app/frontend node:14 \
|
||||
${*}
|
||||
}
|
||||
|
||||
cd "${DIR}/../.." || exit 1
|
||||
|
|
Loading…
Reference in a new issue