mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-08-28 03:30:05 +00:00
Moved tabler compoments into this project for more control
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^4.28.1",
|
"@typescript-eslint/eslint-plugin": "^4.28.1",
|
||||||
"@typescript-eslint/parser": "^4.28.1",
|
"@typescript-eslint/parser": "^4.28.1",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
|
"classnames": "^2.3.1",
|
||||||
"date-fns": "2.22.1",
|
"date-fns": "2.22.1",
|
||||||
"eslint": "^7.30.0",
|
"eslint": "^7.30.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
@@ -46,8 +47,6 @@
|
|||||||
"rooks": "5.0.2",
|
"rooks": "5.0.2",
|
||||||
"styled-components": "5.3.0",
|
"styled-components": "5.3.0",
|
||||||
"tabler-icons-react": "^1.35.0",
|
"tabler-icons-react": "^1.35.0",
|
||||||
"tabler-react": "^2.0.0-alpha.1",
|
|
||||||
"tabler-react-typescript": "0.0.5",
|
|
||||||
"typescript": "^4.3.5"
|
"typescript": "^4.3.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
130
frontend/src/components/Alert/Alert.tsx
Normal file
130
frontend/src/components/Alert/Alert.tsx
Normal 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;
|
34
frontend/src/components/Alert/AlertLink.tsx
Normal file
34
frontend/src/components/Alert/AlertLink.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
1
frontend/src/components/Alert/index.ts
Normal file
1
frontend/src/components/Alert/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./Alert";
|
73
frontend/src/components/Avatar/Avatar.tsx
Normal file
73
frontend/src/components/Avatar/Avatar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
1
frontend/src/components/Avatar/index.ts
Normal file
1
frontend/src/components/Avatar/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./Avatar";
|
34
frontend/src/components/AvatarList/AvatarList.tsx
Normal file
34
frontend/src/components/AvatarList/AvatarList.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
1
frontend/src/components/AvatarList/index.ts
Normal file
1
frontend/src/components/AvatarList/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./AvatarList";
|
36
frontend/src/components/Badge/Badge.tsx
Normal file
36
frontend/src/components/Badge/Badge.tsx
Normal 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>;
|
||||||
|
};
|
1
frontend/src/components/Badge/index.ts
Normal file
1
frontend/src/components/Badge/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./Badge";
|
106
frontend/src/components/Button/Button.tsx
Normal file
106
frontend/src/components/Button/Button.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
1
frontend/src/components/Button/index.ts
Normal file
1
frontend/src/components/Button/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./Button";
|
30
frontend/src/components/ButtonList/ButtonList.tsx
Normal file
30
frontend/src/components/ButtonList/ButtonList.tsx
Normal 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>;
|
||||||
|
};
|
1
frontend/src/components/ButtonList/index.ts
Normal file
1
frontend/src/components/ButtonList/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./ButtonList";
|
56
frontend/src/components/Dropdown/Dropdown.tsx
Normal file
56
frontend/src/components/Dropdown/Dropdown.tsx
Normal 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;
|
65
frontend/src/components/Dropdown/DropdownItem.tsx
Normal file
65
frontend/src/components/Dropdown/DropdownItem.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
1
frontend/src/components/Dropdown/index.ts
Normal file
1
frontend/src/components/Dropdown/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./Dropdown";
|
@@ -2,7 +2,6 @@ import React from "react";
|
|||||||
|
|
||||||
import { useHealthState } from "context";
|
import { useHealthState } from "context";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { Site } from "tabler-react";
|
|
||||||
|
|
||||||
const FixedFooterWrapper = styled.div`
|
const FixedFooterWrapper = styled.div`
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -16,40 +15,75 @@ interface Props {
|
|||||||
function Footer({ fixed }: Props) {
|
function Footer({ fixed }: Props) {
|
||||||
const { health } = useHealthState();
|
const { health } = useHealthState();
|
||||||
|
|
||||||
const footerNav = (
|
const wrapped = () => {
|
||||||
<div>
|
return (
|
||||||
<a
|
<footer className="footer footer-transparent d-print-none">
|
||||||
href="https://nginxproxymanager.com?utm_source=npm"
|
<div className="container">
|
||||||
target="_blank"
|
<div className="row text-center align-items-center flex-row-reverse">
|
||||||
rel="noreferrer">
|
<div className="col-lg-auto ms-lg-auto">
|
||||||
User Guide
|
<ul className="list-inline list-inline-dots mb-0">
|
||||||
</a>{" "}
|
<li className="list-inline-item">
|
||||||
{String.fromCharCode(183)}{" "}
|
<a
|
||||||
<a
|
href="https://nginxproxymanager.com?utm_source=npm"
|
||||||
href="https://github.com/jc21/nginx-proxy-manager/releases?utm_source=npm"
|
target="_blank"
|
||||||
target="_blank"
|
rel="noreferrer"
|
||||||
rel="noreferrer">
|
className="link-secondary">
|
||||||
Changelog
|
User Guide
|
||||||
</a>{" "}
|
</a>
|
||||||
{String.fromCharCode(183)}{" "}
|
</li>
|
||||||
<a
|
<li className="list-inline-item">
|
||||||
href="https://github.com/jc21/nginx-proxy-manager?utm_source=npm"
|
<a
|
||||||
target="_blank"
|
href="https://github.com/jc21/nginx-proxy-manager/releases?utm_source=npm"
|
||||||
rel="noreferrer">
|
target="_blank"
|
||||||
Github
|
rel="noreferrer"
|
||||||
</a>
|
className="link-secondary">
|
||||||
</div>
|
Changelog
|
||||||
);
|
</a>
|
||||||
|
</li>
|
||||||
const note =
|
<li className="list-inline-item">
|
||||||
"v" + health.version + " " + String.fromCharCode(183) + " " + health.commit;
|
<a
|
||||||
|
href="https://github.com/jc21/nginx-proxy-manager?utm_source=npm"
|
||||||
|
target="_blank"
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return fixed ? (
|
return fixed ? (
|
||||||
<FixedFooterWrapper>
|
<FixedFooterWrapper>{wrapped()}</FixedFooterWrapper>
|
||||||
<Site.Footer copyright={note} nav={footerNav} />
|
|
||||||
</FixedFooterWrapper>
|
|
||||||
) : (
|
) : (
|
||||||
<Site.Footer copyright={note} nav={footerNav} />
|
wrapped()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
17
frontend/src/components/Loader/Loader.tsx
Normal file
17
frontend/src/components/Loader/Loader.tsx
Normal 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>;
|
||||||
|
};
|
1
frontend/src/components/Loader/index.ts
Normal file
1
frontend/src/components/Loader/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./Loader";
|
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { Loader } from "components";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { Loader } from "tabler-react";
|
|
||||||
|
|
||||||
const Root = styled.div`
|
const Root = styled.div`
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { Navigation } from "components";
|
||||||
import {
|
import {
|
||||||
Book,
|
Book,
|
||||||
DeviceDesktop,
|
DeviceDesktop,
|
||||||
@@ -9,7 +10,6 @@ import {
|
|||||||
Shield,
|
Shield,
|
||||||
Users,
|
Users,
|
||||||
} from "tabler-icons-react";
|
} from "tabler-icons-react";
|
||||||
import { Navigation } from "tabler-react-typescript";
|
|
||||||
|
|
||||||
function NavMenu() {
|
function NavMenu() {
|
||||||
return (
|
return (
|
||||||
|
19
frontend/src/components/Navigation/Navigation.tsx
Normal file
19
frontend/src/components/Navigation/Navigation.tsx
Normal 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;
|
170
frontend/src/components/Navigation/NavigationHeader.tsx
Normal file
170
frontend/src/components/Navigation/NavigationHeader.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
106
frontend/src/components/Navigation/NavigationMenu.tsx
Normal file
106
frontend/src/components/Navigation/NavigationMenu.tsx
Normal 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;
|
122
frontend/src/components/Navigation/NavigationMenuItem.tsx
Normal file
122
frontend/src/components/Navigation/NavigationMenuItem.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
1
frontend/src/components/Navigation/index.ts
Normal file
1
frontend/src/components/Navigation/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./Navigation";
|
@@ -1,9 +1,9 @@
|
|||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
|
|
||||||
import { Footer } from "components";
|
import { Footer } from "components";
|
||||||
|
import { Avatar, Dropdown, Navigation } from "components";
|
||||||
import { useAuthState, useUserState } from "context";
|
import { useAuthState, useUserState } from "context";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { Avatar, Dropdown, Navigation } from "tabler-react-typescript";
|
|
||||||
|
|
||||||
import { NavMenu } from "./NavMenu";
|
import { NavMenu } from "./NavMenu";
|
||||||
|
|
||||||
@@ -23,14 +23,12 @@ function SiteWrapper({ children }: Props) {
|
|||||||
<Navigation.Header
|
<Navigation.Header
|
||||||
theme="light"
|
theme="light"
|
||||||
brandContent={
|
brandContent={
|
||||||
<a href=".">
|
<img
|
||||||
<img
|
src="/images/logo-bold-horizontal-grey.svg"
|
||||||
src="/images/logo-bold-horizontal-grey.svg"
|
alt="Nginx Proxy Manager"
|
||||||
alt="Nginx Proxy Manager"
|
className="navbar-brand-image"
|
||||||
className="navbar-brand-image"
|
height="32"
|
||||||
height="32"
|
/>
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
}
|
}
|
||||||
avatar={<Avatar size="sm" url={user.gravatarUrl} />}
|
avatar={<Avatar size="sm" url={user.gravatarUrl} />}
|
||||||
profileName={user.nickname}
|
profileName={user.nickname}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { Alert } from "components";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { Alert } from "tabler-react";
|
|
||||||
|
|
||||||
const Root = styled.div`
|
const Root = styled.div`
|
||||||
padding: 20vh 10vw 0 10vw;
|
padding: 20vh 10vw 0 10vw;
|
||||||
|
@@ -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 "./Footer";
|
||||||
|
export * from "./Loader";
|
||||||
export * from "./Loading";
|
export * from "./Loading";
|
||||||
|
export * from "./Navigation";
|
||||||
export * from "./Router";
|
export * from "./Router";
|
||||||
export * from "./SinglePage";
|
export * from "./SinglePage";
|
||||||
export * from "./SiteWrapper";
|
export * from "./SiteWrapper";
|
||||||
|
@@ -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 };
|
|
@@ -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 };
|
|
@@ -1,2 +0,0 @@
|
|||||||
export * from "./Footer";
|
|
||||||
export * from "./Header";
|
|
@@ -6,4 +6,16 @@ import App from "./App";
|
|||||||
|
|
||||||
import "./index.scss";
|
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"));
|
ReactDOM.render(<App />, document.getElementById("root"));
|
||||||
|
@@ -1,23 +1,10 @@
|
|||||||
import React, { useState, ChangeEvent } from "react";
|
import React, { useState, ChangeEvent } from "react";
|
||||||
|
|
||||||
import { SinglePage } from "components";
|
import { Alert, Button } from "components";
|
||||||
import { useAuthState } from "context";
|
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";
|
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() {
|
function Login() {
|
||||||
const { login } = useAuthState();
|
const { login } = useAuthState();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -34,8 +21,8 @@ function Login() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await login(formData.email, formData.password);
|
await login(formData.email, formData.password);
|
||||||
} catch ({ message }) {
|
} catch (err: any) {
|
||||||
setErrorMessage(message);
|
setErrorMessage(err.message);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -44,50 +31,58 @@ function Login() {
|
|||||||
setFormData({ ...formData, [target.name]: target.value });
|
setFormData({ ...formData, [target.name]: target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
const formBody = (
|
|
||||||
<>
|
|
||||||
<Card.Title>Login</Card.Title>
|
|
||||||
<Form method="post" type="card" onSubmit={onSubmit}>
|
|
||||||
{errorMessage ? <Alert type="danger">{errorMessage}</Alert> : null}
|
|
||||||
<Form.Group label="Email Address">
|
|
||||||
<Form.Input
|
|
||||||
onChange={onChange}
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
value={formData.email}
|
|
||||||
maxLength={150}
|
|
||||||
disabled={loading}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group label="Password">
|
|
||||||
<Form.Input
|
|
||||||
onChange={onChange}
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
value={formData.password}
|
|
||||||
minLength={8}
|
|
||||||
maxLength={100}
|
|
||||||
disabled={loading}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Button color="cyan" loading={loading} block>
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SinglePage>
|
<div className="container-tight py-4">
|
||||||
<Wrapper>
|
<div className="text-center mb-4">
|
||||||
<LogoWrapper>
|
<img src={logo} alt="Logo" />
|
||||||
<img src={logo} alt="Logo" />
|
</div>
|
||||||
</LogoWrapper>
|
<form
|
||||||
<Card body={formBody} />
|
className="card card-md"
|
||||||
</Wrapper>
|
method="post"
|
||||||
</SinglePage>
|
autoComplete="off"
|
||||||
|
onSubmit={onSubmit}>
|
||||||
|
<div className="card-body">
|
||||||
|
{errorMessage ? <Alert type="danger">{errorMessage}</Alert> : null}
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="form-label">Email address</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
onChange={onChange}
|
||||||
|
className="form-control"
|
||||||
|
name="email"
|
||||||
|
value={formData.email}
|
||||||
|
disabled={loading}
|
||||||
|
placeholder="Email"
|
||||||
|
maxLength={150}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</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}
|
||||||
|
autoComplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-footer">
|
||||||
|
<Button color="cyan" loading={loading} className="w-100">
|
||||||
|
Sign in
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,29 +1,17 @@
|
|||||||
import React, { useState, ChangeEvent } from "react";
|
import React, { useState, ChangeEvent } from "react";
|
||||||
|
|
||||||
import { createUser } from "api/npm";
|
import { createUser } from "api/npm";
|
||||||
import { SinglePage } from "components";
|
import { Alert, Button } from "components";
|
||||||
import { useAuthState, useHealthState } from "context";
|
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";
|
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() {
|
function Setup() {
|
||||||
const { refreshHealth } = useHealthState();
|
const { refreshHealth } = useHealthState();
|
||||||
const { login } = useAuthState();
|
const { login } = useAuthState();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
nickname: "",
|
nickname: "",
|
||||||
@@ -56,17 +44,16 @@ function Setup() {
|
|||||||
await login(response.email, password);
|
await login(response.email, password);
|
||||||
// Trigger a Health change
|
// Trigger a Health change
|
||||||
refreshHealth();
|
refreshHealth();
|
||||||
|
|
||||||
// window.location.reload();
|
// window.location.reload();
|
||||||
} catch ({ message }) {
|
} catch (err: any) {
|
||||||
setErrorMessage(message);
|
setErrorMessage(err.message);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setErrorMessage("Unable to create user!");
|
setErrorMessage("Unable to create user!");
|
||||||
}
|
}
|
||||||
} catch ({ message }) {
|
} catch (err: any) {
|
||||||
setErrorMessage(message);
|
setErrorMessage(err.message);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
@@ -75,68 +62,90 @@ function Setup() {
|
|||||||
setFormData({ ...formData, [target.name]: target.value });
|
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
|
|
||||||
onChange={onChange}
|
|
||||||
name="name"
|
|
||||||
value={formData.name}
|
|
||||||
disabled={loading}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group label="Nickname">
|
|
||||||
<Form.Input
|
|
||||||
onChange={onChange}
|
|
||||||
name="nickname"
|
|
||||||
value={formData.nickname}
|
|
||||||
disabled={loading}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group label="Email Address">
|
|
||||||
<Form.Input
|
|
||||||
onChange={onChange}
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
value={formData.email}
|
|
||||||
maxLength={150}
|
|
||||||
disabled={loading}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group label="Password">
|
|
||||||
<Form.Input
|
|
||||||
onChange={onChange}
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
value={formData.password}
|
|
||||||
minLength={8}
|
|
||||||
maxLength={100}
|
|
||||||
disabled={loading}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Button color="cyan" loading={loading} block>
|
|
||||||
Create Account
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SinglePage>
|
<div className="page page-center">
|
||||||
<Wrapper>
|
<div className="container-tight py-4">
|
||||||
<LogoWrapper>
|
<div className="text-center mb-4">
|
||||||
<img src={logo} alt="Logo" />
|
<img src={logo} alt="Logo" />
|
||||||
</LogoWrapper>
|
</div>
|
||||||
<Card body={formBody} />
|
<form
|
||||||
</Wrapper>
|
className="card card-md"
|
||||||
</SinglePage>
|
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
|
||||||
|
/>
|
||||||
|
</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
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="form-label">Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
onChange={onChange}
|
||||||
|
className="form-control"
|
||||||
|
name="email"
|
||||||
|
value={formData.email}
|
||||||
|
disabled={loading}
|
||||||
|
placeholder="Email"
|
||||||
|
maxLength={150}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</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}
|
||||||
|
autoComplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-footer">
|
||||||
|
<Button color="cyan" loading={loading} className="w-100">
|
||||||
|
Create Account
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
12511
frontend/yarn.lock
12511
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user