Moved tabler compoments into this project for more control

This commit is contained in:
Jamie Curnow
2021-07-14 20:48:05 +10:00
parent 5ad5ad41c7
commit 52f3801b4e
37 changed files with 1247 additions and 12883 deletions

View File

@@ -16,6 +16,7 @@
"@typescript-eslint/eslint-plugin": "^4.28.1",
"@typescript-eslint/parser": "^4.28.1",
"babel-eslint": "^10.1.0",
"classnames": "^2.3.1",
"date-fns": "2.22.1",
"eslint": "^7.30.0",
"eslint-config-prettier": "^8.3.0",
@@ -46,8 +47,6 @@
"rooks": "5.0.2",
"styled-components": "5.3.0",
"tabler-icons-react": "^1.35.0",
"tabler-react": "^2.0.0-alpha.1",
"tabler-react-typescript": "0.0.5",
"typescript": "^4.3.5"
},
"scripts": {

View File

@@ -0,0 +1,130 @@
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;

View File

@@ -0,0 +1,34 @@
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>
);
};

View File

@@ -0,0 +1 @@
export * from "./Alert";

View File

@@ -0,0 +1,73 @@
import React, { ReactNode } from "react";
import cn from "classnames";
import { Badge } from "../Badge";
export interface AvatarProps {
/**
* Child elements within
*/
children?: ReactNode;
/**
* Additional Class
*/
className?: string;
/**
* Color only when using Initials
*/
color?: string;
/**
* Full or data url of an avatar image
*/
url?: string;
/**
* Initials to use instead of an image
*/
initials?: string;
/**
* Size of the avatar
*/
size?: string;
/**
* Display a status color
*/
status?: "info" | "success" | "warning" | "danger";
/**
* Shape of the avatar
*/
shape?: "rounded" | "rounded-circle" | "rounded-0" | "rounded-lg";
/**
* Icon instead of Image or Initials
*/
icon?: ReactNode;
}
export const Avatar: React.FC<AvatarProps> = ({
children,
className,
color,
url,
initials,
size,
shape,
status,
icon,
}) => {
const styles = {
backgroundImage: "url('" + url + "')",
};
const classes = [];
color && classes.push("bg-" + color);
size && classes.push("avatar-" + size);
shape && classes.push(shape);
return (
<span style={styles} className={cn("avatar", classes, className)}>
{initials && !url ? initials.toUpperCase() : null}
{!initials && !url ? children : null}
{icon && <span className="avatar-icon">{icon}</span>}
{status ? <Badge color={status} /> : null}
</span>
);
};

View File

@@ -0,0 +1 @@
export * from "./Avatar";

View File

@@ -0,0 +1,34 @@
import React, { ReactNode } from "react";
import cn from "classnames";
export interface AvatarListProps {
/**
* Child elements within
*/
children?: ReactNode;
/**
* Additional Class
*/
className?: string;
/**
* Displays stacked avatars
*/
stacked?: boolean;
}
export const AvatarList: React.FC<AvatarListProps> = ({
children,
className,
stacked,
}) => {
return (
<div
className={cn(
"avatar-list",
stacked && "avatar-list-stacked",
className,
)}>
{children}
</div>
);
};

View File

@@ -0,0 +1 @@
export * from "./AvatarList";

View File

@@ -0,0 +1,36 @@
import React, { ReactNode } from "react";
import cn from "classnames";
export interface BadgeProps {
/**
* Child elements within
*/
children?: ReactNode;
/**
* Additional Class
*/
className?: string;
/**
* Color of the Badge
*/
color?: string;
/**
* Type of Badge
*/
type?: "pill" | "soft";
}
export const Badge: React.FC<BadgeProps> = ({
children,
className,
color,
type,
}) => {
let modifier = "";
type === "soft" && (modifier = "-lt");
const classes = ["badge", "bg-" + (color || "blue") + modifier];
type === "pill" && classes.push("badge-pill");
return <span className={cn(classes, className)}>{children}</span>;
};

View File

@@ -0,0 +1 @@
export * from "./Badge";

View File

@@ -0,0 +1,106 @@
import React, { ReactNode } from "react";
import cn from "classnames";
export interface ButtonProps {
/**
* Child elements within
*/
children?: ReactNode;
/**
* Additional Class
*/
className?: string;
/**
* Color of the Button
*/
color?: string;
/**
* Disables the Button
*/
disabled?: boolean;
/**
* Show a spinner instead of content
*/
loading?: boolean;
/**
* Button shape
*/
shape?: "ghost" | "square" | "pill" | "outline" | "icon";
/**
* Button size
*/
size?: "sm" | "lg";
/**
* Is this button only showing an icon?
*/
iconOnly?: boolean;
/**
* Link to url
*/
href?: string;
/**
* target property, only used when href is set
*/
target?: string;
/**
* On click handler
*/
onClick?: any;
}
export const Button: React.FC<ButtonProps> = ({
children,
className,
color,
disabled,
loading,
shape,
size,
iconOnly,
href,
target,
onClick,
}) => {
const classes = [
"btn",
{
disabled: disabled,
"btn-icon": iconOnly,
"btn-loading": loading,
[`btn-${size}`]: !!size,
},
];
let modifier = "";
shape === "ghost" && (modifier = "-ghost");
shape === "outline" && (modifier = "-outline");
shape &&
["ghost", "outline"].indexOf(shape) === -1 &&
classes.push(`btn-${shape}`);
color && classes.push(`btn${modifier}-${color}`);
modifier && classes.push(`btn${modifier}`);
if (href) {
// Render a A tag
return (
<a
className={cn(classes, className)}
aria-label="Button"
href={href}
onClick={onClick}
target={target}>
{children}
</a>
);
}
return (
<button
className={cn(classes, className)}
aria-label="Button"
onClick={onClick}>
{children}
</button>
);
};

View File

@@ -0,0 +1 @@
export * from "./Button";

View File

@@ -0,0 +1,30 @@
import React, { ReactNode } from "react";
import cn from "classnames";
export interface ButtonListProps {
/**
* Child elements within
*/
children?: ReactNode;
/**
* Additional Class
*/
className?: string;
/**
* Alignment
*/
align?: "center" | "right";
}
export const ButtonList: React.FC<ButtonListProps> = ({
children,
className,
align,
}) => {
const classes = {
"justify-content-center": align === "center",
"justify-content-end": align === "right",
};
return <div className={cn("btn-list", classes, className)}>{children}</div>;
};

View File

@@ -0,0 +1 @@
export * from "./ButtonList";

View File

@@ -0,0 +1,56 @@
import React, { ReactNode } from "react";
import cn from "classnames";
import { DropdownItem } from "./DropdownItem";
export interface DropdownProps {
/**
* Child elements within
*/
children: ReactNode;
/**
* Additional Class
*/
className?: string;
/**
* Header text
*/
header?: string;
/**
* Shows arrow notch
*/
arrow?: boolean;
/**
* Dark mode for dropdown
*/
dark?: boolean;
/**
* Force this to show
*/
show?: boolean;
}
export const Dropdown: React.FC<DropdownProps> = ({
children,
className,
header,
arrow,
dark,
show,
}) => {
return (
<div
className={cn(
"dropdown-menu",
arrow && "dropdown-menu-arrow",
dark && ["bg-dark", "text-white"],
show && "show",
className,
)}>
{header && <span className="dropdown-header">{header}</span>}
{children}
</div>
);
};
Dropdown.Item = DropdownItem;

View File

@@ -0,0 +1,65 @@
import React, { ReactNode } from "react";
import cn from "classnames";
export interface DropdownItemProps {
/**
* Child elements within
*/
children?: ReactNode;
/**
* Additional Class
*/
className?: string;
/**
* Set if this is just a divider
*/
divider?: boolean;
/**
* Set if this is active
*/
active?: boolean;
/**
* Set if this is disabled
*/
disabled?: boolean;
/**
* Icon to use as well
*/
icon?: ReactNode;
/**
* Href
*/
href?: string;
/**
* onClick handler
*/
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
}
export const DropdownItem: React.FC<DropdownItemProps> = ({
children,
className,
divider,
active,
disabled,
icon,
href,
onClick,
}) => {
return divider ? (
<div className={cn("dropdown-divider", className)} />
) : (
<a
className={cn(
"dropdown-item",
active && "active",
disabled && "disabled",
className,
)}
href={href}
onClick={onClick}>
{icon && <span className="dropdown-item-icon">{icon}</span>}
{children}
</a>
);
};

View File

@@ -0,0 +1 @@
export * from "./Dropdown";

View File

@@ -2,7 +2,6 @@ import React from "react";
import { useHealthState } from "context";
import styled from "styled-components";
import { Site } from "tabler-react";
const FixedFooterWrapper = styled.div`
position: fixed;
@@ -16,40 +15,75 @@ interface Props {
function Footer({ fixed }: Props) {
const { health } = useHealthState();
const footerNav = (
<div>
const wrapped = () => {
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">
rel="noreferrer"
className="link-secondary">
User Guide
</a>{" "}
{String.fromCharCode(183)}{" "}
</a>
</li>
<li className="list-inline-item">
<a
href="https://github.com/jc21/nginx-proxy-manager/releases?utm_source=npm"
target="_blank"
rel="noreferrer">
rel="noreferrer"
className="link-secondary">
Changelog
</a>{" "}
{String.fromCharCode(183)}{" "}
</a>
</li>
<li className="list-inline-item">
<a
href="https://github.com/jc21/nginx-proxy-manager?utm_source=npm"
target="_blank"
rel="noreferrer">
rel="noreferrer"
className="link-secondary">
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">
Copyright © {new Date().getFullYear()} jc21.com. Theme by{" "}
<a
className="link-secondary"
href="https://preview.tabler.io/"
target="_blank"
rel="noreferrer">
Tabler
</a>
</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>
);
const note =
"v" + health.version + " " + String.fromCharCode(183) + " " + health.commit;
};
return fixed ? (
<FixedFooterWrapper>
<Site.Footer copyright={note} nav={footerNav} />
</FixedFooterWrapper>
<FixedFooterWrapper>{wrapped()}</FixedFooterWrapper>
) : (
<Site.Footer copyright={note} nav={footerNav} />
wrapped()
);
}

View File

@@ -0,0 +1,17 @@
import React, { ReactNode } from "react";
import cn from "classnames";
export interface LoaderProps {
/**
* Child elements within
*/
children?: ReactNode;
/**
* Additional Class
*/
className?: string;
}
export const Loader: React.FC<LoaderProps> = ({ children, className }) => {
return <div className={cn({ loader: true }, className)}>{children}</div>;
};

View File

@@ -0,0 +1 @@
export * from "./Loader";

View File

@@ -1,7 +1,7 @@
import React from "react";
import { Loader } from "components";
import styled from "styled-components";
import { Loader } from "tabler-react";
const Root = styled.div`
text-align: center;

View File

@@ -1,5 +1,6 @@
import React from "react";
import { Navigation } from "components";
import {
Book,
DeviceDesktop,
@@ -9,7 +10,6 @@ import {
Shield,
Users,
} from "tabler-icons-react";
import { Navigation } from "tabler-react-typescript";
function NavMenu() {
return (

View File

@@ -0,0 +1,19 @@
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;

View File

@@ -0,0 +1,170 @@
import React, { ReactNode, useState } from "react";
import cn from "classnames";
import { Bell } from "tabler-icons-react";
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);
return (
<header
className={cn(
`navbar navbar-expand-md navbar-${theme} d-print-none`,
className,
)}>
<div className="container-xl">
<button
className="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbar-menu">
<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">
<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
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 /> : null}
</div>
</header>
);
};

View File

@@ -0,0 +1,106 @@
import React, { ReactNode, useState } from "react";
import cn from "classnames";
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.
*/
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;
}
export const NavigationMenu: React.FC<NavigationMenuProps> = ({
className,
items,
withinHeader,
theme = "transparent",
searchContent,
}) => {
const [dropdownShown, setDropdownShown] = useState(0);
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={cn("collapse navbar-collapse", className)}>
<div className="d-flex flex-column flex-md-row flex-fill align-items-stretch align-items-md-center">
{el}
</div>
</div>
);
}
return (
<div className={"navbar-expand-md"}>
<div className={cn(`navbar navbar-${theme}`, className)}>
<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">
{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;

View File

@@ -0,0 +1,122 @@
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?: any;
/**
* 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>
);
};

View File

@@ -0,0 +1 @@
export * from "./Navigation";

View File

@@ -1,9 +1,9 @@
import React, { ReactNode } from "react";
import { Footer } from "components";
import { Avatar, Dropdown, Navigation } from "components";
import { useAuthState, useUserState } from "context";
import styled from "styled-components";
import { Avatar, Dropdown, Navigation } from "tabler-react-typescript";
import { NavMenu } from "./NavMenu";
@@ -23,14 +23,12 @@ function SiteWrapper({ children }: Props) {
<Navigation.Header
theme="light"
brandContent={
<a href=".">
<img
src="/images/logo-bold-horizontal-grey.svg"
alt="Nginx Proxy Manager"
className="navbar-brand-image"
height="32"
/>
</a>
}
avatar={<Avatar size="sm" url={user.gravatarUrl} />}
profileName={user.nickname}

View File

@@ -1,7 +1,7 @@
import React from "react";
import { Alert } from "components";
import styled from "styled-components";
import { Alert } from "tabler-react";
const Root = styled.div`
padding: 20vh 10vw 0 10vw;

View File

@@ -1,5 +1,14 @@
export * from "./Alert";
export * from "./Avatar";
export * from "./AvatarList";
export * from "./Badge";
export * from "./Button";
export * from "./ButtonList";
export * from "./Dropdown";
export * from "./Footer";
export * from "./Loader";
export * from "./Loading";
export * from "./Navigation";
export * from "./Router";
export * from "./SinglePage";
export * from "./SiteWrapper";

View File

@@ -1,56 +0,0 @@
import React from "react";
import { useHealthState } from "context";
import styled from "styled-components";
import { Site } from "tabler-react";
const FixedFooterWrapper = styled.div`
position: fixed;
bottom: 0;
width: 100%;
`;
interface Props {
fixed?: boolean;
}
function Footer({ fixed }: Props) {
const { health } = useHealthState();
const footerNav = (
<div>
<a
href="https://nginxproxymanager.com?utm_source=npm"
target="_blank"
rel="noreferrer">
User Guide
</a>{" "}
{String.fromCharCode(183)}{" "}
<a
href="https://github.com/jc21/nginx-proxy-manager/releases?utm_source=npm"
target="_blank"
rel="noreferrer">
Changelog
</a>{" "}
{String.fromCharCode(183)}{" "}
<a
href="https://github.com/jc21/nginx-proxy-manager?utm_source=npm"
target="_blank"
rel="noreferrer">
Github
</a>
</div>
);
const note =
"v" + health.version + " " + String.fromCharCode(183) + " " + health.commit;
return fixed ? (
<FixedFooterWrapper>
<Site.Footer copyright={note} nav={footerNav} />
</FixedFooterWrapper>
) : (
<Site.Footer copyright={note} nav={footerNav} />
);
}
export { Footer };

View File

@@ -1,130 +0,0 @@
import React from "react";
import { useAuthState, useUserState } from "context";
import { Site } from "tabler-react";
function Header() {
const user = useUserState();
const { logout } = useAuthState();
const accountDropdownProps = {
avatarURL: user.gravatarUrl,
name: user.nickname,
description: user.roles.includes("admin")
? "Administrator"
: "Standard User",
options: [
{ icon: "user", value: "Profile" },
{ icon: "settings", value: "Settings" },
{ isDivider: true },
{
icon: "help-circle",
value: "Need help?",
href: "https://nginxproxymanager.com",
target: "_blank",
},
{ icon: "log-out", value: "Log out", onClick: logout },
],
};
const navBarItems = [
{
value: "Home",
to: "/",
icon: "home",
//LinkComponent: withRouter(NavLink),
useExact: true,
},
{
value: "Interface",
icon: "box",
subItems: [
{
value: "Cards Design",
to: "/cards",
//LinkComponent: withRouter(NavLink),
},
//{ value: "Charts", to: "/charts", LinkComponent: withRouter(NavLink) },
{
value: "Pricing Cards",
to: "/pricing-cards",
//LinkComponent: withRouter(NavLink),
},
],
},
{
value: "Components",
icon: "calendar",
/*
subItems: [
{ value: "Maps", to: "/maps", LinkComponent: withRouter(NavLink) },
{ value: "Icons", to: "/icons", LinkComponent: withRouter(NavLink) },
{ value: "Store", to: "/store", LinkComponent: withRouter(NavLink) },
{ value: "Blog", to: "/blog", LinkComponent: withRouter(NavLink) },
],
*/
},
{
value: "Pages",
icon: "file",
subItems: [
{
value: "Profile",
to: "/profile",
//LinkComponent: withRouter(NavLink),
},
//{ value: "Login", to: "/login", LinkComponent: withRouter(NavLink) },
{
value: "Register",
to: "/register",
//LinkComponent: withRouter(NavLink),
},
{
value: "Forgot password",
to: "/forgot-password",
//LinkComponent: withRouter(NavLink),
},
{
value: "Empty page",
to: "/empty-page",
//LinkComponent: withRouter(NavLink),
},
//{ value: "RTL", to: "/rtl", LinkComponent: withRouter(NavLink) },
],
},
{
value: "Forms",
to: "/form-elements",
icon: "check-square",
//LinkComponent: withRouter(NavLink),
},
{
value: "Gallery",
to: "/gallery",
icon: "image",
//LinkComponent: withRouter(NavLink),
},
{
icon: "file-text",
value: "Documentation",
to:
process.env.NODE_ENV === "production"
? "https://tabler.github.io/tabler-react/documentation"
: "/documentation",
},
];
return (
<>
<Site.Header
href="/"
alt="Nginx Proxy Manager"
imageURL="/images/logo-bold-horizontal-grey.svg"
accountDropdown={accountDropdownProps}
/>
<Site.Nav itemsObjects={navBarItems} />
</>
);
}
export { Header };

View File

@@ -1,2 +0,0 @@
export * from "./Footer";
export * from "./Header";

View File

@@ -6,4 +6,16 @@ import App from "./App";
import "./index.scss";
declare global {
interface Function {
Item: React.FC<any>;
Link: React.FC<any>;
Header: React.FC<any>;
Main: React.FC<any>;
Options: React.FC<any>;
SubTitle: React.FC<any>;
Title: React.FC<any>;
}
}
ReactDOM.render(<App />, document.getElementById("root"));

View File

@@ -1,23 +1,10 @@
import React, { useState, ChangeEvent } from "react";
import { SinglePage } from "components";
import { Alert, Button } from "components";
import { useAuthState } from "context";
import styled from "styled-components";
import { Alert, Button, Container, Form, Card } from "tabler-react";
import logo from "../../img/logo-text-vertical-grey.png";
const Wrapper = styled(Container)`
margin: 15px auto;
max-width: 400px;
display: block;
`;
const LogoWrapper = styled.div`
text-align: center;
padding-bottom: 15px;
`;
function Login() {
const { login } = useAuthState();
const [loading, setLoading] = useState(false);
@@ -34,8 +21,8 @@ function Login() {
try {
await login(formData.email, formData.password);
} catch ({ message }) {
setErrorMessage(message);
} catch (err: any) {
setErrorMessage(err.message);
setLoading(false);
}
};
@@ -44,50 +31,58 @@ function Login() {
setFormData({ ...formData, [target.name]: target.value });
};
const formBody = (
<>
<Card.Title>Login</Card.Title>
<Form method="post" type="card" onSubmit={onSubmit}>
return (
<div className="container-tight py-4">
<div className="text-center mb-4">
<img src={logo} alt="Logo" />
</div>
<form
className="card card-md"
method="post"
autoComplete="off"
onSubmit={onSubmit}>
<div className="card-body">
{errorMessage ? <Alert type="danger">{errorMessage}</Alert> : null}
<Form.Group label="Email Address">
<Form.Input
onChange={onChange}
name="email"
<div className="mb-3">
<label className="form-label">Email address</label>
<input
type="email"
onChange={onChange}
className="form-control"
name="email"
value={formData.email}
maxLength={150}
disabled={loading}
placeholder="Email"
maxLength={150}
required
/>
</Form.Group>
<Form.Group label="Password">
<Form.Input
onChange={onChange}
name="password"
</div>
<div className="mb-2">
<label className="form-label">Password</label>
<div className="input-group input-group-flat">
<input
type="password"
onChange={onChange}
className="form-control"
name="password"
value={formData.password}
disabled={loading}
placeholder="Password"
minLength={8}
maxLength={100}
disabled={loading}
autoComplete="off"
required
/>
</Form.Group>
<Button color="cyan" loading={loading} block>
Login
</div>
</div>
<div className="form-footer">
<Button color="cyan" loading={loading} className="w-100">
Sign in
</Button>
</Form>
</>
);
return (
<SinglePage>
<Wrapper>
<LogoWrapper>
<img src={logo} alt="Logo" />
</LogoWrapper>
<Card body={formBody} />
</Wrapper>
</SinglePage>
</div>
</div>
</form>
</div>
);
}

View File

@@ -1,29 +1,17 @@
import React, { useState, ChangeEvent } from "react";
import { createUser } from "api/npm";
import { SinglePage } from "components";
import { Alert, Button } from "components";
import { useAuthState, useHealthState } from "context";
import styled from "styled-components";
import { Alert, Button, Container, Form, Card } from "tabler-react";
import logo from "../../img/logo-text-vertical-grey.png";
const Wrapper = styled(Container)`
margin: 15px auto;
max-width: 400px;
display: block;
`;
const LogoWrapper = styled.div`
text-align: center;
padding-bottom: 15px;
`;
function Setup() {
const { refreshHealth } = useHealthState();
const { login } = useAuthState();
const [loading, setLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const [formData, setFormData] = useState({
name: "",
nickname: "",
@@ -56,17 +44,16 @@ function Setup() {
await login(response.email, password);
// Trigger a Health change
refreshHealth();
// window.location.reload();
} catch ({ message }) {
setErrorMessage(message);
} catch (err: any) {
setErrorMessage(err.message);
setLoading(false);
}
} else {
setErrorMessage("Unable to create user!");
}
} catch ({ message }) {
setErrorMessage(message);
} catch (err: any) {
setErrorMessage(err.message);
}
setLoading(false);
};
@@ -75,68 +62,90 @@ function Setup() {
setFormData({ ...formData, [target.name]: target.value });
};
const formBody = (
<>
<Card.Title>Initial Setup</Card.Title>
<Form method="post" type="card" onSubmit={onSubmit}>
{errorMessage ? <Alert type="danger">{errorMessage}</Alert> : null}
<Form.Group label="Full Name">
<Form.Input
return (
<div className="page page-center">
<div className="container-tight py-4">
<div className="text-center mb-4">
<img src={logo} alt="Logo" />
</div>
<form
className="card card-md"
method="post"
autoComplete="off"
onSubmit={onSubmit}>
<div className="card-body">
<h2 className="card-title text-center mb-4">
Create your first Account
</h2>
{errorMessage ? (
<Alert type="danger" className="text-center">
{errorMessage}
</Alert>
) : null}
<div className="mb-3">
<label className="form-label">Name</label>
<input
onChange={onChange}
className="form-control"
name="name"
value={formData.name}
disabled={loading}
placeholder="Name"
required
/>
</Form.Group>
<Form.Group label="Nickname">
<Form.Input
</div>
<div className="mb-3">
<label className="form-label">Nickname</label>
<input
onChange={onChange}
className="form-control"
name="nickname"
value={formData.nickname}
disabled={loading}
placeholder="Nickname"
required
/>
</Form.Group>
<Form.Group label="Email Address">
<Form.Input
onChange={onChange}
name="email"
</div>
<div className="mb-3">
<label className="form-label">Email</label>
<input
type="email"
onChange={onChange}
className="form-control"
name="email"
value={formData.email}
maxLength={150}
disabled={loading}
placeholder="Email"
maxLength={150}
required
/>
</Form.Group>
<Form.Group label="Password">
<Form.Input
onChange={onChange}
name="password"
</div>
<div className="mb-3">
<label className="form-label">Password</label>
<input
type="password"
onChange={onChange}
className="form-control"
name="password"
value={formData.password}
disabled={loading}
placeholder="Password"
minLength={8}
maxLength={100}
disabled={loading}
autoComplete="off"
required
/>
</Form.Group>
<Button color="cyan" loading={loading} block>
</div>
<div className="form-footer">
<Button color="cyan" loading={loading} className="w-100">
Create Account
</Button>
</Form>
</>
);
return (
<SinglePage>
<Wrapper>
<LogoWrapper>
<img src={logo} alt="Logo" />
</LogoWrapper>
<Card body={formBody} />
</Wrapper>
</SinglePage>
</div>
</div>
</form>
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff