Modals and API calls working for admin tab
This commit is contained in:
10
package-lock.json
generated
10
package-lock.json
generated
@@ -17,6 +17,7 @@
|
|||||||
"@types/node": "^22.13.4",
|
"@types/node": "^22.13.4",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
@@ -3471,6 +3472,15 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/moment": {
|
||||||
|
"version": "2.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||||
|
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"@types/node": "^22.13.4",
|
"@types/node": "^22.13.4",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import RaidLayoutPage from "./pages/protected/RaidLayoutPage";
|
|||||||
import HomePage from "./pages/public/HomePage";
|
import HomePage from "./pages/public/HomePage";
|
||||||
import LoginPage from "./pages/public/LoginPage";
|
import LoginPage from "./pages/public/LoginPage";
|
||||||
import SignupPage from "./pages/public/SignupPage";
|
import SignupPage from "./pages/public/SignupPage";
|
||||||
import TestPage from "./pages/public/TestPage";
|
|
||||||
import { ProtectedRoute } from "./providers/AuthProvider";
|
import { ProtectedRoute } from "./providers/AuthProvider";
|
||||||
import ErrorBoundary from "./providers/ErrorBoundary";
|
import ErrorBoundary from "./providers/ErrorBoundary";
|
||||||
|
|
||||||
@@ -28,10 +27,6 @@ const routes = createBrowserRouter([
|
|||||||
path: "/",
|
path: "/",
|
||||||
element: <HomePage/>
|
element: <HomePage/>
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/test",
|
|
||||||
element: <TestPage/>
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/login",
|
path: "/login",
|
||||||
element: <LoginPage/>
|
element: <LoginPage/>
|
||||||
|
|||||||
41
src/components/account/AccountStatusSelector.tsx
Normal file
41
src/components/account/AccountStatusSelector.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { AccountStatus } from "@/interface/Account";
|
||||||
|
|
||||||
|
|
||||||
|
export default function AccountStatusSelector({
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
}:{
|
||||||
|
value: AccountStatus;
|
||||||
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
}){
|
||||||
|
const modalId = crypto.randomUUID().replace("-", "");
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex flex-row flex-wrap justify-start gap-x-4"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
Object.keys(AccountStatus).map((status: string) => (
|
||||||
|
<label
|
||||||
|
key={status}
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name={`accountStatusSelector${modalId}`}
|
||||||
|
value={status}
|
||||||
|
onChange={onChange}
|
||||||
|
checked={value === status}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="ml-1"
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import clsx from "clsx";
|
|||||||
|
|
||||||
export type ButtonRounding = "none" | "sm" | "md" | "lg" | "full";
|
export type ButtonRounding = "none" | "sm" | "md" | "lg" | "full";
|
||||||
export type ButtonShape = "vertical" | "horizontal" | "square";
|
export type ButtonShape = "vertical" | "horizontal" | "square";
|
||||||
export type ButtonSizeType = "xsm" | "sm" | "md" | "lg" | "xl";
|
export type ButtonSizeType = "xs" | "sm" | "md" | "lg" | "xl";
|
||||||
export type ButtonVariant = "solid" | "outline" | "ghost" | "outline-ghost" | "icon";
|
export type ButtonVariant = "solid" | "outline" | "ghost" | "outline-ghost" | "icon";
|
||||||
|
|
||||||
export interface ButtonProps extends React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>{
|
export interface ButtonProps extends React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>{
|
||||||
@@ -17,7 +17,7 @@ export interface ButtonProps extends React.DetailedHTMLProps<React.ButtonHTMLAtt
|
|||||||
export default function Button(props: ButtonProps){
|
export default function Button(props: ButtonProps){
|
||||||
const {
|
const {
|
||||||
rounding = "lg",
|
rounding = "lg",
|
||||||
shape = "vertical",
|
shape = "horizontal",
|
||||||
size = "md"
|
size = "md"
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -33,6 +33,7 @@ export default function Button(props: ButtonProps){
|
|||||||
{...props}
|
{...props}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
props.className,
|
props.className,
|
||||||
|
"transition-colors duration-300",
|
||||||
//Rounding
|
//Rounding
|
||||||
{
|
{
|
||||||
"rounded-none": rounding === "none",
|
"rounded-none": rounding === "none",
|
||||||
@@ -44,19 +45,19 @@ export default function Button(props: ButtonProps){
|
|||||||
//Shape & Size
|
//Shape & Size
|
||||||
{
|
{
|
||||||
//Square
|
//Square
|
||||||
"p-0": size === "xsm" && shape === "square",
|
"p-0": size === "xs" && shape === "square",
|
||||||
"p-1": size === "sm" && shape === "square",
|
"p-1": size === "sm" && shape === "square",
|
||||||
"p-2": size === "md" && shape === "square",
|
"p-2": size === "md" && shape === "square",
|
||||||
"p-3": size === "lg" && shape === "square",
|
"p-3": size === "lg" && shape === "square",
|
||||||
"p-4": size === "xl" && shape === "square",
|
"p-4": size === "xl" && shape === "square",
|
||||||
//Horizontal
|
//Horizontal
|
||||||
"px-1 py-0": size === "xsm" && shape === "horizontal",
|
"px-1 py-0": size === "xs" && shape === "horizontal",
|
||||||
"px-2 py-1": size === "sm" && shape === "horizontal",
|
"px-2 py-1": size === "sm" && shape === "horizontal",
|
||||||
"px-4 py-2": size === "md" && shape === "horizontal",
|
"px-4 py-2": size === "md" && shape === "horizontal",
|
||||||
"px-6 py-3": size === "lg" && shape === "horizontal",
|
"px-6 py-3": size === "lg" && shape === "horizontal",
|
||||||
"px-8 py-4": size === "xl" && shape === "horizontal",
|
"px-8 py-4": size === "xl" && shape === "horizontal",
|
||||||
//Vertical
|
//Vertical
|
||||||
"px-0 py-1": size === "xsm" && shape === "vertical",
|
"px-0 py-1": size === "xs" && shape === "vertical",
|
||||||
"px-1 py-2": size === "sm" && shape === "vertical",
|
"px-1 py-2": size === "sm" && shape === "vertical",
|
||||||
"px-2 py-4": size === "md" && shape === "vertical",
|
"px-2 py-4": size === "md" && shape === "vertical",
|
||||||
"px-3 py-6": size === "lg" && shape === "vertical",
|
"px-3 py-6": size === "lg" && shape === "vertical",
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default function Modal(props: ModalProps){
|
|||||||
<div
|
<div
|
||||||
{...divProps}
|
{...divProps}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50",
|
"fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 max-w-(--breakpoint-sm) z-50",
|
||||||
"flex flex-col rounded-lg max-h-full shadow-lg shadow-[#00000066]",
|
"flex flex-col rounded-lg max-h-full shadow-lg shadow-[#00000066]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ export default function ModalBackground(props: ModalBackgroundProps){
|
|||||||
{
|
{
|
||||||
"bg-[#00000044]": backgroundType === "darken",
|
"bg-[#00000044]": backgroundType === "darken",
|
||||||
"bg-[#FFFFFF44]": backgroundType === "lighten",
|
"bg-[#FFFFFF44]": backgroundType === "lighten",
|
||||||
"backdrop-blur-sm bg-radial-[circle] from-transparent from-25% to-[#00000066]": backgroundType === "darken-blur",
|
"backdrop-blur-sm bg-black/15": backgroundType === "darken-blur",
|
||||||
"backdrop-blur-sm bg-radial-[circle] from-transparent from-25% to-[#FFFFFF66]": backgroundType === "lighten-blur",
|
"backdrop-blur-sm bg-white/5": backgroundType === "lighten-blur",
|
||||||
|
"backdrop-blur-sm bg-radial-[circle] from-transparent from-25% to-[#00000066]": backgroundType === "darken-blur-radial",
|
||||||
|
"backdrop-blur-sm bg-radial-[circle] from-transparent from-25% to-[#FFFFFF66]": backgroundType === "lighten-blur-radial",
|
||||||
"bg-[#00000000]": backgroundType === "transparent",
|
"bg-[#00000000]": backgroundType === "transparent",
|
||||||
"backdrop-blur-sm": backgroundType === "blur"
|
"backdrop-blur-sm": backgroundType === "blur"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ModalHeaderProps } from "@/interface/ModalInterfaces";
|
import { ModalHeaderProps } from "@/interface/ModalInterfaces";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { BsXLg } from "react-icons/bs";
|
import { BsXLg } from "react-icons/bs";
|
||||||
|
import Button from "../button/Button";
|
||||||
|
|
||||||
|
|
||||||
export default function ModalHeader(props: ModalHeaderProps){
|
export default function ModalHeader(props: ModalHeaderProps){
|
||||||
@@ -30,14 +31,20 @@ export default function ModalHeader(props: ModalHeaderProps){
|
|||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
close &&
|
close &&
|
||||||
<div
|
<Button
|
||||||
className="absolute top-1 right-1 cursor-pointer"
|
variant="ghost"
|
||||||
|
shape="square"
|
||||||
|
size="sm"
|
||||||
|
className={clsx(
|
||||||
|
"absolute top-1 right-1 cursor-pointer",
|
||||||
|
"hover:bg-red-500 hover:text-white active:bg-red-600 active:text-white"
|
||||||
|
)}
|
||||||
onClick={close}
|
onClick={close}
|
||||||
>
|
>
|
||||||
<BsXLg
|
<BsXLg
|
||||||
size={20}
|
size={20}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
59
src/components/modal/RaidBuilderModal.tsx
Normal file
59
src/components/modal/RaidBuilderModal.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import Modal from "./Modal";
|
||||||
|
import ModalBody from "./ModalBody";
|
||||||
|
import ModalFooter from "./ModalFooter";
|
||||||
|
import ModalHeader from "./ModalHeader";
|
||||||
|
|
||||||
|
|
||||||
|
export default function RaidBuilderModal({
|
||||||
|
display,
|
||||||
|
modalHeader,
|
||||||
|
modalBody,
|
||||||
|
modalFooter,
|
||||||
|
close
|
||||||
|
}:{
|
||||||
|
display: boolean;
|
||||||
|
modalHeader: React.ReactNode;
|
||||||
|
modalBody: React.ReactNode;
|
||||||
|
modalFooter: React.ReactNode;
|
||||||
|
close: () => void;
|
||||||
|
}){
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
display={display}
|
||||||
|
close={close}
|
||||||
|
className="bg-(--bg-color) text-(--text-color)"
|
||||||
|
backgroundType={theme === "dark" ? "lighten-blur" : "darken-blur"}
|
||||||
|
>
|
||||||
|
<ModalHeader
|
||||||
|
className="bg-[#00000022] dark:bg-[#FFFFFF16]"
|
||||||
|
close={close}
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
className="text-2xl"
|
||||||
|
>
|
||||||
|
{modalHeader}
|
||||||
|
</h3>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<div
|
||||||
|
className="my-8"
|
||||||
|
>
|
||||||
|
{modalBody}
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter
|
||||||
|
className="bg-[#00000022] dark:bg-[#FFFFFF16]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex flex-row items-center justify-center gap-4"
|
||||||
|
>
|
||||||
|
{modalFooter}
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,10 +6,6 @@ const publicLinks = [
|
|||||||
{
|
{
|
||||||
name: "Home",
|
name: "Home",
|
||||||
path: "/"
|
path: "/"
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test",
|
|
||||||
path: "/test"
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -12,19 +12,22 @@ export interface Tab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TabGroupProps extends HTMLProps<HTMLDivElement>{
|
export interface TabGroupProps extends HTMLProps<HTMLDivElement>{
|
||||||
tabs: Tab[];
|
tabs?: Tab[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function TabGroup(props: TabGroupProps){
|
export default function TabGroup(props: TabGroupProps){
|
||||||
const { tabs, className } = props;
|
const { tabs, className } = props;
|
||||||
|
if(!tabs){ throw new Error("Tabs must be present"); }
|
||||||
const [ activeTab, setActiveTab ] = useState<number>(tabs.map((tab, index) => tab.active ? index : undefined)[0] ?? 0);
|
const [ activeTab, setActiveTab ] = useState<number>(tabs.map((tab, index) => tab.active ? index : undefined)[0] ?? 0);
|
||||||
//TODO: Possible to maintain state of past tabs if we "cache" them in a useState<JSX.Element>() on their first render
|
//TODO: Possible to maintain state of past tabs if we "cache" them in a useState<JSX.Element>() on their first render
|
||||||
|
const divProps = {...props};
|
||||||
|
delete divProps.tabs;
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...divProps}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
className,
|
className,
|
||||||
"flex flex-col w-full"
|
"flex flex-col w-full"
|
||||||
@@ -50,7 +53,7 @@ export default function TabGroup(props: TabGroupProps){
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="flex flex-col items-center justify-center"
|
className="flex flex-col items-center justify-center mt-8"
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
tabs.map((tab, index) => (
|
tabs.map((tab, index) => (
|
||||||
@@ -108,8 +111,8 @@ function TabContent({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
tab.headerClasses,
|
"w-full",
|
||||||
""
|
tab.contentClasses
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{tab.tabContent}
|
{tab.tabContent}
|
||||||
|
|||||||
39
src/components/table/Table.tsx
Normal file
39
src/components/table/Table.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { HTMLProps } from "react";
|
||||||
|
import TableBody from "./TableBody";
|
||||||
|
import TableHead from "./TableHead";
|
||||||
|
|
||||||
|
|
||||||
|
export interface TableProps extends HTMLProps<HTMLTableElement>{
|
||||||
|
tableHeadElements?: React.ReactNode[];
|
||||||
|
tableBodyElements?: React.ReactNode[][];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function Table(props: TableProps){
|
||||||
|
const {
|
||||||
|
tableHeadElements,
|
||||||
|
tableBodyElements
|
||||||
|
} = props;
|
||||||
|
const tableProps = {...props};
|
||||||
|
delete tableProps.tableHeadElements;
|
||||||
|
delete tableProps.tableBodyElements;
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table
|
||||||
|
{...tableProps}
|
||||||
|
className={clsx(
|
||||||
|
"w-full",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TableHead
|
||||||
|
headElements={tableHeadElements ?? []}
|
||||||
|
/>
|
||||||
|
<TableBody
|
||||||
|
bodyElements={tableBodyElements ?? []}
|
||||||
|
/>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
src/components/table/TableBody.tsx
Normal file
46
src/components/table/TableBody.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
|
||||||
|
export default function TableBody({
|
||||||
|
bodyElements
|
||||||
|
}:{
|
||||||
|
bodyElements: React.ReactNode[][];
|
||||||
|
}){
|
||||||
|
return (
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
bodyElements.map((row, rowIndex) => (
|
||||||
|
<tr
|
||||||
|
key={rowIndex}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
row.map((element, elementIndex) => (
|
||||||
|
<td
|
||||||
|
key={elementIndex}
|
||||||
|
className={clsx(
|
||||||
|
{
|
||||||
|
"w-0": elementIndex === 0 || elementIndex === row.length - 1
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"bg-neutral-200 dark:bg-neutral-700",
|
||||||
|
{
|
||||||
|
"py-4 my-2": elementIndex < row.length - 1,
|
||||||
|
"rounded-l pl-2": elementIndex === 0,
|
||||||
|
"rounded-r pr-2": elementIndex === row.length - 1
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{element}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
src/components/table/TableHead.tsx
Normal file
30
src/components/table/TableHead.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
|
||||||
|
export default function TableHead({
|
||||||
|
headElements
|
||||||
|
}:{
|
||||||
|
headElements: React.ReactNode[];
|
||||||
|
}){
|
||||||
|
return (
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{
|
||||||
|
headElements.map((element, index) => (
|
||||||
|
<th
|
||||||
|
key={index}
|
||||||
|
className={clsx(
|
||||||
|
{
|
||||||
|
"pl-2": index === 0,
|
||||||
|
"pr-2": index === headElements.length - 1
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{element}
|
||||||
|
</th>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
);
|
||||||
|
}
|
||||||
164
src/hooks/AccountHooks.ts
Normal file
164
src/hooks/AccountHooks.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { Account } from "@/interface/Account";
|
||||||
|
import { api } from "@/util/AxiosUtil";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
|
||||||
|
export function useGetAccounts(page: number, pageSize: number, searchTerm?: string){
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["accounts", {page, pageSize, searchTerm}],
|
||||||
|
queryFn: async () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("page", page.toString());
|
||||||
|
params.append("pageSize", pageSize.toString());
|
||||||
|
if(searchTerm){
|
||||||
|
params.append("search", searchTerm ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.get(`/account?${params}`);
|
||||||
|
|
||||||
|
if(response.status !== 200){
|
||||||
|
throw new Error("Failed to get accounts");
|
||||||
|
}
|
||||||
|
else if(response.data.errors){
|
||||||
|
throw new Error(response.data.errors.join(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data as Account[];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useForcePasswordReset(accountId: string){
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["forcePasswordReset", accountId],
|
||||||
|
mutationFn: async () => {
|
||||||
|
const response = await api.put(`/account/${accountId}/forcePasswordReset`);
|
||||||
|
|
||||||
|
if(response.status !== 200){
|
||||||
|
throw new Error("Failed to force password reset");
|
||||||
|
}
|
||||||
|
else if(response.data.errors){
|
||||||
|
throw new Error(response.data.errors.join(", "));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["accounts"] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useResetPassword(accountId: string){
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["resetPassword", accountId],
|
||||||
|
mutationFn: async (password: string) => {
|
||||||
|
const response = await api.put(`/account/${accountId}/resetPassword`, {
|
||||||
|
password
|
||||||
|
});
|
||||||
|
|
||||||
|
if(response.status !== 200){
|
||||||
|
throw new Error("Failed to reset password");
|
||||||
|
}
|
||||||
|
else if(response.data.errors){
|
||||||
|
throw new Error(response.data.errors.join(", "));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["accounts"] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRevokeRefreshToken(accountId: string){
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["revokeRefreshToken", accountId],
|
||||||
|
mutationFn: async () => {
|
||||||
|
const response = await api.put(`/account/${accountId}/revokeRefreshToken`);
|
||||||
|
|
||||||
|
if(response.status !== 200){
|
||||||
|
throw new Error("Failed to revoke refresh token");
|
||||||
|
}
|
||||||
|
else if(response.data.errors){
|
||||||
|
throw new Error(response.data.errors.join(", "));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["accounts"] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateAccount(){
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["createAccount"],
|
||||||
|
mutationFn: async (account: Account) => {
|
||||||
|
const response = await api.post("/account", account);
|
||||||
|
|
||||||
|
if(response.status !== 200){
|
||||||
|
throw new Error("Failed to create account");
|
||||||
|
}
|
||||||
|
else if(response.data.errors){
|
||||||
|
throw new Error(response.data.errors.join(", "));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["accounts"] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateAccount(){
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["updateAccount"],
|
||||||
|
mutationFn: async (account: Account) => {
|
||||||
|
const response = await api.put(`/account/${account.accountId}`, account);
|
||||||
|
|
||||||
|
if(response.status !== 200){
|
||||||
|
throw new Error("Failed to update account");
|
||||||
|
}
|
||||||
|
else if(response.data.errors){
|
||||||
|
throw new Error(response.data.errors.join(", "));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["accounts"] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteAccount(accountId: string){
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["deleteAccount", accountId],
|
||||||
|
mutationFn: async () => {
|
||||||
|
const response = await api.delete(`/account/${accountId}`);
|
||||||
|
|
||||||
|
if(response.status !== 200){
|
||||||
|
throw new Error("Failed to delete account");
|
||||||
|
}
|
||||||
|
else if(response.data.errors){
|
||||||
|
throw new Error(response.data.errors.join(", "));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["accounts"] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -29,6 +29,10 @@
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a:hover{
|
||||||
|
color: var(--color-blue-300);
|
||||||
|
}
|
||||||
|
|
||||||
a.active {
|
a.active {
|
||||||
color: var(--color-blue-400);
|
color: var(--color-blue-400);
|
||||||
}
|
}
|
||||||
@@ -40,7 +44,7 @@ body {
|
|||||||
max-width: var(--breakpoint-2xl);
|
max-width: var(--breakpoint-2xl);
|
||||||
|
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
padding-top: 82px;
|
padding-top: 90px;
|
||||||
padding-inline: 1rem;
|
padding-inline: 1rem;
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
20
src/interface/Account.ts
Normal file
20
src/interface/Account.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export enum AccountStatus {
|
||||||
|
ACTIVE = "ACTIVE",
|
||||||
|
LOCKED = "LOCKED",
|
||||||
|
INACTIVE = "INACTIVE",
|
||||||
|
DELETED = "DELETED",
|
||||||
|
UNCONFIRMED = "UNCONFIRMED"
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export interface Account {
|
||||||
|
accountId: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
loginDate: Date;
|
||||||
|
email: string;
|
||||||
|
forceReset: boolean;
|
||||||
|
refreshToken?: string;
|
||||||
|
refreshTokenExpiration?: Date;
|
||||||
|
accountStatus: AccountStatus;
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { HTMLProps } from "react";
|
import { HTMLProps } from "react";
|
||||||
|
|
||||||
|
|
||||||
export type ModalBackgroundType = "darken" | "lighten" | "blur" | "darken-blur" | "lighten-blur" | "transparent" | "none";
|
export type ModalBackgroundType = "darken" | "lighten" | "blur" | "darken-blur" | "lighten-blur" | "darken-blur-radial" | "lighten-blur-radial" | "transparent" | "none";
|
||||||
export type ModalHeaderFooterBackgroundType = "darken" | "lighten" | "none";
|
export type ModalHeaderFooterBackgroundType = "darken" | "lighten" | "none";
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
26
src/main.tsx
26
src/main.tsx
@@ -1,3 +1,4 @@
|
|||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
@@ -6,18 +7,23 @@ import { AuthProvider } from './providers/AuthProvider.tsx'
|
|||||||
import { ThemeProvider } from './providers/ThemeProvider.tsx'
|
import { ThemeProvider } from './providers/ThemeProvider.tsx'
|
||||||
|
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ThemeProvider
|
<QueryClientProvider client={queryClient}>
|
||||||
defaultTheme="dark"
|
<ThemeProvider
|
||||||
storageKey="vite-ui-theme"
|
defaultTheme="dark"
|
||||||
>
|
storageKey="vite-ui-theme"
|
||||||
<AuthProvider
|
|
||||||
jwtStorageKey="jwt"
|
|
||||||
refreshTokenStorageKey="refreshToken"
|
|
||||||
>
|
>
|
||||||
<App />
|
<AuthProvider
|
||||||
</AuthProvider>
|
jwtStorageKey="jwt"
|
||||||
</ThemeProvider>
|
refreshTokenStorageKey="refreshToken"
|
||||||
|
>
|
||||||
|
<App />
|
||||||
|
</AuthProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,33 @@
|
|||||||
|
import TabGroup, { Tab } from "@/components/tab/TabGroup";
|
||||||
|
import AccountsLoader from "@/ui/account/AccountsLoader";
|
||||||
|
|
||||||
|
|
||||||
export default function AdminPage(){
|
export default function AdminPage(){
|
||||||
//TODO:
|
const tabs: Tab[] = [
|
||||||
|
{
|
||||||
|
tabHeader: "Accounts",
|
||||||
|
tabContent: <AccountsLoader/>
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<main
|
||||||
Admin Page
|
className="flex flex-col items-center justify-center"
|
||||||
</div>
|
>
|
||||||
|
<h1
|
||||||
|
className="text-4xl"
|
||||||
|
>
|
||||||
|
Admin Functions
|
||||||
|
</h1>
|
||||||
|
<div
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<TabGroup
|
||||||
|
tabs={tabs}
|
||||||
|
>
|
||||||
|
</TabGroup>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useNavigate } from "react-router";
|
|||||||
|
|
||||||
|
|
||||||
export default function LogoutPage(){
|
export default function LogoutPage(){
|
||||||
const { setJwt } = useAuth();
|
const { setJwt, setExpiration } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ export default function LogoutPage(){
|
|||||||
const response = await api.get("/auth/logout");
|
const response = await api.get("/auth/logout");
|
||||||
if(response.status === 200){
|
if(response.status === 200){
|
||||||
setJwt(null);
|
setJwt(null);
|
||||||
|
setExpiration(null);
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
|
|||||||
@@ -40,26 +40,39 @@ export default function LoginPage(){
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<main>
|
||||||
action={login}
|
<form
|
||||||
className="flex flex-col justify-center space-y-8"
|
action={login}
|
||||||
>
|
className="flex flex-col items-center justify-center space-y-8"
|
||||||
<TextInput
|
|
||||||
id="username"
|
|
||||||
name="username"
|
|
||||||
placeholder="Username"
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
placeholder="Password"
|
|
||||||
/>
|
|
||||||
<PrimaryButton
|
|
||||||
className="mx-auto"
|
|
||||||
type="submit"
|
|
||||||
>
|
>
|
||||||
Login
|
<div
|
||||||
</PrimaryButton>
|
className="mx-auto"
|
||||||
</form>
|
>
|
||||||
|
<TextInput
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
placeholder="Username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="mx-auto"
|
||||||
|
>
|
||||||
|
<PasswordInput
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
placeholder="Password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex flex-row justify-center items-center"
|
||||||
|
>
|
||||||
|
<PrimaryButton
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
import TabGroup, { Tab } from "@/components/tab/TabGroup";
|
|
||||||
|
|
||||||
|
|
||||||
export default function TestPage(){
|
|
||||||
const tabs: Tab[] = [
|
|
||||||
{
|
|
||||||
tabHeader: "Tab 1",
|
|
||||||
tabContent: <Tab1/>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tabHeader: "Tab 2",
|
|
||||||
tabContent: <Tab2/>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tabHeader: "Tab 3",
|
|
||||||
tabContent: <Tab3/>
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main
|
|
||||||
className="flex flex-col items-center justify-center gap-4 mt-8"
|
|
||||||
>
|
|
||||||
<TabGroup
|
|
||||||
tabs={tabs}
|
|
||||||
/>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function Tab1(){
|
|
||||||
console.log("Tab 1");
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
Tab 1 Content
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Tab2(){
|
|
||||||
console.log("Tab 2");
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
Tab 2 Content
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Tab3(){
|
|
||||||
console.log("Tab 3");
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
Tab 3 Content
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -82,6 +82,8 @@ export function AuthProvider({
|
|||||||
setExpiration
|
setExpiration
|
||||||
}), [ jwt, setJwt, expiration, setExpiration ]);
|
}), [ jwt, setJwt, expiration, setExpiration ]);
|
||||||
|
|
||||||
|
//TODO: Return a spinner while the first token is being fetched
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={currentTokens}>
|
<AuthContext.Provider value={currentTokens}>
|
||||||
|
|||||||
70
src/ui/account/AccountAdminButtons.tsx
Normal file
70
src/ui/account/AccountAdminButtons.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { ButtonProps } from "@/components/button/Button";
|
||||||
|
import DangerButton from "@/components/button/DangerButton";
|
||||||
|
import PrimaryButton from "@/components/button/PrimaryButton";
|
||||||
|
import TertiaryButton from "@/components/button/TertiaryButton";
|
||||||
|
import WarningButton from "@/components/button/WarningButton";
|
||||||
|
import { BsKeyFill, BsLockFill, BsPencilFill, BsTrash3, BsXCircle } from "react-icons/bs";
|
||||||
|
|
||||||
|
|
||||||
|
export default function AccountAdminButtons({
|
||||||
|
buttonProps,
|
||||||
|
showForcePasswordResetModal,
|
||||||
|
showAccountPasswordSetModal,
|
||||||
|
showRevokeRefreshTokenModal,
|
||||||
|
showUpdateAccountModal,
|
||||||
|
showDeleteAccountModal
|
||||||
|
}:{
|
||||||
|
buttonProps: ButtonProps;
|
||||||
|
showForcePasswordResetModal: () => void;
|
||||||
|
showAccountPasswordSetModal: () => void;
|
||||||
|
showRevokeRefreshTokenModal: () => void;
|
||||||
|
showUpdateAccountModal: () => void;
|
||||||
|
showDeleteAccountModal: () => void;
|
||||||
|
}){
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex flex-row gap-2"
|
||||||
|
>
|
||||||
|
<WarningButton
|
||||||
|
{...buttonProps}
|
||||||
|
onClick={showForcePasswordResetModal}
|
||||||
|
>
|
||||||
|
<BsLockFill
|
||||||
|
size={22}
|
||||||
|
/>
|
||||||
|
</WarningButton>
|
||||||
|
<DangerButton
|
||||||
|
{...buttonProps}
|
||||||
|
onClick={showAccountPasswordSetModal}
|
||||||
|
>
|
||||||
|
<BsKeyFill
|
||||||
|
size={22}
|
||||||
|
/>
|
||||||
|
</DangerButton>
|
||||||
|
<TertiaryButton
|
||||||
|
{...buttonProps}
|
||||||
|
onClick={showRevokeRefreshTokenModal}
|
||||||
|
>
|
||||||
|
<BsXCircle
|
||||||
|
size={22}
|
||||||
|
/>
|
||||||
|
</TertiaryButton>
|
||||||
|
<PrimaryButton
|
||||||
|
{...buttonProps}
|
||||||
|
onClick={showUpdateAccountModal}
|
||||||
|
>
|
||||||
|
<BsPencilFill
|
||||||
|
size={22}
|
||||||
|
/>
|
||||||
|
</PrimaryButton>
|
||||||
|
<DangerButton
|
||||||
|
{...buttonProps}
|
||||||
|
onClick={showDeleteAccountModal}
|
||||||
|
>
|
||||||
|
<BsTrash3
|
||||||
|
size={22}
|
||||||
|
/>
|
||||||
|
</DangerButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
149
src/ui/account/AccountsList.tsx
Normal file
149
src/ui/account/AccountsList.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { ButtonProps } from "@/components/button/Button";
|
||||||
|
import Table from "@/components/table/Table";
|
||||||
|
import { Account } from "@/interface/Account";
|
||||||
|
import moment from "moment";
|
||||||
|
import { useState } from "react";
|
||||||
|
import AccountAdminButtons from "./AccountAdminButtons";
|
||||||
|
import AccountModal from "./modals/AccountModal";
|
||||||
|
import AccountPasswordRestModal from "./modals/AccountPasswordResetModal";
|
||||||
|
import DeleteAccountModal from "./modals/DeleteAccountModal";
|
||||||
|
import ForcePasswordResetModal from "./modals/ForcePasswordResetModal";
|
||||||
|
import RevokeRefreshTokenModal from "./modals/RevokeRefreshTokenModal";
|
||||||
|
|
||||||
|
|
||||||
|
export interface AccountsListProps {
|
||||||
|
accounts: Account[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function AccountsList(props: AccountsListProps){
|
||||||
|
const { accounts } = props;
|
||||||
|
|
||||||
|
|
||||||
|
const [ selectedAccount, setSelectedAccount ] = useState<Account | undefined>(undefined);
|
||||||
|
const [ displayForcePasswordResetModal, setDisplayForcePasswordResetModal ] = useState(false);
|
||||||
|
const [ displayAccountPasswordSetModal, setDisplayAccountPasswordSetModal ] = useState(false);
|
||||||
|
const [ displayRevokeRefreshTokenModal, setDisplayRevokeRefreshTokenModal ] = useState(false);
|
||||||
|
const [ displayAccountModal, setDisplayAccountModal ] = useState(false);
|
||||||
|
const [ displayDeleteAccountModal, setDisplayDeleteAccountModal ] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
const buttonProps: ButtonProps = {
|
||||||
|
variant: "ghost",
|
||||||
|
size: "md",
|
||||||
|
shape: "square"
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const headElements: React.ReactNode[] = [
|
||||||
|
<div>
|
||||||
|
ID
|
||||||
|
</div>,
|
||||||
|
<div>
|
||||||
|
Username
|
||||||
|
</div>,
|
||||||
|
<div>
|
||||||
|
Email
|
||||||
|
</div>,
|
||||||
|
<div>
|
||||||
|
Login Date
|
||||||
|
</div>,
|
||||||
|
<div>
|
||||||
|
Status
|
||||||
|
</div>,
|
||||||
|
<div
|
||||||
|
className="pl-16"
|
||||||
|
>
|
||||||
|
Actions
|
||||||
|
</div>
|
||||||
|
];
|
||||||
|
|
||||||
|
const bodyElements: React.ReactNode[][] = accounts.map((account) => [
|
||||||
|
<div
|
||||||
|
className="text-nowrap"
|
||||||
|
>
|
||||||
|
{account.accountId}
|
||||||
|
</div>,
|
||||||
|
<div>
|
||||||
|
{account.username}
|
||||||
|
</div>,
|
||||||
|
<div>
|
||||||
|
{account.email}
|
||||||
|
</div>,
|
||||||
|
<div
|
||||||
|
className="text-nowrap"
|
||||||
|
>
|
||||||
|
{moment(account.loginDate).format("MM-DD-YYYY HH:mm")}
|
||||||
|
</div>,
|
||||||
|
<div>
|
||||||
|
{account.accountStatus}
|
||||||
|
</div>,
|
||||||
|
<div
|
||||||
|
className="flex flex-row items-center justify-center gap-2 pl-16"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="py-4 border-l border-neutral-500"
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<AccountAdminButtons
|
||||||
|
buttonProps={buttonProps}
|
||||||
|
showForcePasswordResetModal={() => {
|
||||||
|
setSelectedAccount(account);
|
||||||
|
setDisplayForcePasswordResetModal(true);
|
||||||
|
}}
|
||||||
|
showAccountPasswordSetModal={() => {
|
||||||
|
setSelectedAccount(account);
|
||||||
|
setDisplayAccountPasswordSetModal(true);
|
||||||
|
}}
|
||||||
|
showRevokeRefreshTokenModal={() => {
|
||||||
|
setSelectedAccount(account);
|
||||||
|
setDisplayRevokeRefreshTokenModal(true);
|
||||||
|
}}
|
||||||
|
showUpdateAccountModal={() => {
|
||||||
|
setSelectedAccount(account);
|
||||||
|
setDisplayAccountModal(true);
|
||||||
|
}}
|
||||||
|
showDeleteAccountModal={() => {
|
||||||
|
setSelectedAccount(account);
|
||||||
|
setDisplayDeleteAccountModal(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Table
|
||||||
|
tableHeadElements={headElements}
|
||||||
|
tableBodyElements={bodyElements}
|
||||||
|
/>
|
||||||
|
<ForcePasswordResetModal
|
||||||
|
display={displayForcePasswordResetModal}
|
||||||
|
close={() => {setDisplayForcePasswordResetModal(false); setSelectedAccount(undefined);}}
|
||||||
|
account={selectedAccount}
|
||||||
|
/>
|
||||||
|
<AccountPasswordRestModal
|
||||||
|
display={displayAccountPasswordSetModal}
|
||||||
|
close={() => {setDisplayAccountPasswordSetModal(false); setSelectedAccount(undefined);}}
|
||||||
|
account={selectedAccount}
|
||||||
|
/>
|
||||||
|
<RevokeRefreshTokenModal
|
||||||
|
display={displayRevokeRefreshTokenModal}
|
||||||
|
close={() => {setDisplayRevokeRefreshTokenModal(false); setSelectedAccount(undefined);}}
|
||||||
|
account={selectedAccount}
|
||||||
|
/>
|
||||||
|
<AccountModal
|
||||||
|
display={displayAccountModal}
|
||||||
|
close={() => {setDisplayAccountModal(false); setSelectedAccount(undefined);}}
|
||||||
|
account={selectedAccount}
|
||||||
|
/>
|
||||||
|
<DeleteAccountModal
|
||||||
|
display={displayDeleteAccountModal}
|
||||||
|
close={() => {setDisplayDeleteAccountModal(false); setSelectedAccount(undefined);}}
|
||||||
|
account={selectedAccount}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
src/ui/account/AccountsListSkeleton.tsx
Normal file
7
src/ui/account/AccountsListSkeleton.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default function AccountsListSkeleton(){
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Accounts List Skeleton
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
src/ui/account/AccountsLoader.tsx
Normal file
45
src/ui/account/AccountsLoader.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import PrimaryButton from "@/components/button/PrimaryButton";
|
||||||
|
import { useGetAccounts } from "@/hooks/AccountHooks";
|
||||||
|
import { useState } from "react";
|
||||||
|
import AccountsList from "./AccountsList";
|
||||||
|
import AccountsListSkeleton from "./AccountsListSkeleton";
|
||||||
|
import AccountModal from "./modals/AccountModal";
|
||||||
|
|
||||||
|
|
||||||
|
export default function AccountsLoader(){
|
||||||
|
const [ displayCreateAccountModal, setDisplayCreateAccountModal ] = useState(false);
|
||||||
|
|
||||||
|
const accountsQuery = useGetAccounts(0, 20);
|
||||||
|
|
||||||
|
|
||||||
|
if(accountsQuery.isLoading){
|
||||||
|
return <AccountsListSkeleton/>
|
||||||
|
}
|
||||||
|
else if(accountsQuery.isError){
|
||||||
|
//TODO:
|
||||||
|
return <div>Error: {accountsQuery.error.message}</div>
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* TODO: Add Account Button */}
|
||||||
|
<PrimaryButton
|
||||||
|
className="mb-8"
|
||||||
|
onClick={() => setDisplayCreateAccountModal(true)}
|
||||||
|
>
|
||||||
|
Create Account
|
||||||
|
</PrimaryButton>
|
||||||
|
<AccountModal
|
||||||
|
display={displayCreateAccountModal}
|
||||||
|
close={() => setDisplayCreateAccountModal(false)}
|
||||||
|
account={undefined}
|
||||||
|
/>
|
||||||
|
{/* Account Search Bar */}
|
||||||
|
<AccountsList
|
||||||
|
accounts={accountsQuery.data ?? []}
|
||||||
|
/>
|
||||||
|
{/* TODO: Add Pagination */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
113
src/ui/account/modals/AccountModal.tsx
Normal file
113
src/ui/account/modals/AccountModal.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import AccountStatusSelector from "@/components/account/AccountStatusSelector";
|
||||||
|
import PrimaryButton from "@/components/button/PrimaryButton";
|
||||||
|
import SecondaryButton from "@/components/button/SecondaryButton";
|
||||||
|
import PasswordInput from "@/components/input/PasswordInput";
|
||||||
|
import TextInput from "@/components/input/TextInput";
|
||||||
|
import RaidBuilderModal from "@/components/modal/RaidBuilderModal";
|
||||||
|
import { useCreateAccount, useUpdateAccount } from "@/hooks/AccountHooks";
|
||||||
|
import { Account, AccountStatus } from "@/interface/Account";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
export default function AccountModal({
|
||||||
|
display,
|
||||||
|
close,
|
||||||
|
account
|
||||||
|
}:{
|
||||||
|
display: boolean;
|
||||||
|
close: () => void;
|
||||||
|
account: Account | undefined;
|
||||||
|
}){
|
||||||
|
const [ username, setUsername ] = useState<string>(account?.username ?? "");
|
||||||
|
const [ email, setEmail ] = useState<string>(account?.email ?? "");
|
||||||
|
const [ password, setPassword ] = useState<string>("");
|
||||||
|
const [ accountStatus, setAccountStatus ] = useState<AccountStatus>(account?.accountStatus ?? AccountStatus.ACTIVE);
|
||||||
|
const modalId = crypto.randomUUID().replace("-", "");
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setUsername(account?.username ?? "");
|
||||||
|
setEmail(account?.email ?? "");
|
||||||
|
setPassword(account?.password ?? "");
|
||||||
|
setAccountStatus(account?.accountStatus ?? AccountStatus.ACTIVE);
|
||||||
|
}, [ account, setUsername, setEmail, setPassword, setAccountStatus ]);
|
||||||
|
|
||||||
|
|
||||||
|
const updateAccountMutate = useUpdateAccount();
|
||||||
|
const createAccountMutate = useCreateAccount();
|
||||||
|
|
||||||
|
if((updateAccountMutate.isSuccess) || (createAccountMutate.isSuccess)){
|
||||||
|
updateAccountMutate.reset();
|
||||||
|
createAccountMutate.reset();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
else if((updateAccountMutate.isError) || (updateAccountMutate.isError)){
|
||||||
|
//TODO: Add message modal here
|
||||||
|
console.log(updateAccountMutate.error);
|
||||||
|
console.log(createAccountMutate.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const updateAccount = () => {
|
||||||
|
updateAccountMutate.mutate({accountId: account?.accountId, username, email, password, accountStatus} as Account);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createAccount = () => {
|
||||||
|
createAccountMutate.mutate({username, email, password, accountStatus} as Account);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RaidBuilderModal
|
||||||
|
display={display}
|
||||||
|
close={close}
|
||||||
|
modalHeader={account ? "Update Account" : "Create Account"}
|
||||||
|
modalBody={
|
||||||
|
<div
|
||||||
|
className="flex flex-col items-center justify-center gap-4"
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id={`accountModalUsername${modalId}`}
|
||||||
|
placeholder="Username"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
id={`accountModalEmail${modalId}`}
|
||||||
|
placeholder="Email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
!account && (
|
||||||
|
<PasswordInput
|
||||||
|
id={`accountModalPassword${modalId}`}
|
||||||
|
placeholder="Password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<AccountStatusSelector
|
||||||
|
value={accountStatus}
|
||||||
|
onChange={(e) => setAccountStatus(e.currentTarget.value as AccountStatus)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
modalFooter={
|
||||||
|
<>
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={account ? updateAccount : createAccount}
|
||||||
|
>
|
||||||
|
{account ? "Update" : "Create"}
|
||||||
|
</PrimaryButton>
|
||||||
|
<SecondaryButton
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</SecondaryButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
74
src/ui/account/modals/AccountPasswordResetModal.tsx
Normal file
74
src/ui/account/modals/AccountPasswordResetModal.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import PrimaryButton from "@/components/button/PrimaryButton";
|
||||||
|
import SecondaryButton from "@/components/button/SecondaryButton";
|
||||||
|
import PasswordInput from "@/components/input/PasswordInput";
|
||||||
|
import RaidBuilderModal from "@/components/modal/RaidBuilderModal";
|
||||||
|
import { useResetPassword } from "@/hooks/AccountHooks";
|
||||||
|
import { Account } from "@/interface/Account";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
export default function AccountPasswordRestModal({
|
||||||
|
display,
|
||||||
|
close,
|
||||||
|
account
|
||||||
|
}:{
|
||||||
|
display: boolean;
|
||||||
|
close: () => void;
|
||||||
|
account: Account | undefined;
|
||||||
|
}){
|
||||||
|
const [ newPassword, setNewPassword ] = useState<string>("");
|
||||||
|
|
||||||
|
|
||||||
|
const passwordResetMutate = useResetPassword(account?.accountId ?? "");
|
||||||
|
const modalId = crypto.randomUUID().replace("-", "");
|
||||||
|
|
||||||
|
|
||||||
|
const resetPassword = () => {
|
||||||
|
passwordResetMutate.mutate(newPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(passwordResetMutate.isSuccess){
|
||||||
|
passwordResetMutate.reset();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
else if(passwordResetMutate.isError){
|
||||||
|
//TODO: Add message modal here
|
||||||
|
console.log(passwordResetMutate.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RaidBuilderModal
|
||||||
|
display={display}
|
||||||
|
close={close}
|
||||||
|
modalHeader={"Reset Password"}
|
||||||
|
modalBody={
|
||||||
|
<div
|
||||||
|
className="flex flex-col gap-4"
|
||||||
|
>
|
||||||
|
<div>Enter new password for {account?.username}.</div>
|
||||||
|
<PasswordInput
|
||||||
|
id={`passwordResetModal${modalId}`}
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
placeholder="Password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
modalFooter={
|
||||||
|
<>
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={resetPassword}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</PrimaryButton>
|
||||||
|
<SecondaryButton
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</SecondaryButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
src/ui/account/modals/DeleteAccountModal.tsx
Normal file
55
src/ui/account/modals/DeleteAccountModal.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import DangerButton from "@/components/button/DangerButton";
|
||||||
|
import SecondaryButton from "@/components/button/SecondaryButton";
|
||||||
|
import RaidBuilderModal from "@/components/modal/RaidBuilderModal";
|
||||||
|
import { useDeleteAccount } from "@/hooks/AccountHooks";
|
||||||
|
import { Account } from "@/interface/Account";
|
||||||
|
|
||||||
|
|
||||||
|
export default function DeleteAccountModal({
|
||||||
|
display,
|
||||||
|
close,
|
||||||
|
account
|
||||||
|
}:{
|
||||||
|
display: boolean;
|
||||||
|
close: () => void;
|
||||||
|
account: Account | undefined;
|
||||||
|
}){
|
||||||
|
const deleteAccountMutate = useDeleteAccount(account?.accountId ?? "");
|
||||||
|
|
||||||
|
|
||||||
|
const deleteAccount = () => {
|
||||||
|
deleteAccountMutate.mutate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(deleteAccountMutate.isSuccess){
|
||||||
|
deleteAccountMutate.reset();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
else if(deleteAccountMutate.isError){
|
||||||
|
//TODO: Add message modal here
|
||||||
|
console.log(deleteAccountMutate.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RaidBuilderModal
|
||||||
|
display={display}
|
||||||
|
close={close}
|
||||||
|
modalHeader={"Delete Account"}
|
||||||
|
modalBody={`Are you sure you want to delete ${account?.username}?`}
|
||||||
|
modalFooter={
|
||||||
|
<>
|
||||||
|
<DangerButton
|
||||||
|
onClick={deleteAccount}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</DangerButton>
|
||||||
|
<SecondaryButton
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</SecondaryButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
56
src/ui/account/modals/ForcePasswordResetModal.tsx
Normal file
56
src/ui/account/modals/ForcePasswordResetModal.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import DangerButton from "@/components/button/DangerButton";
|
||||||
|
import SecondaryButton from "@/components/button/SecondaryButton";
|
||||||
|
import RaidBuilderModal from "@/components/modal/RaidBuilderModal";
|
||||||
|
import { useForcePasswordReset } from "@/hooks/AccountHooks";
|
||||||
|
import { Account } from "@/interface/Account";
|
||||||
|
|
||||||
|
|
||||||
|
export default function ForcePasswordResetModal({
|
||||||
|
display,
|
||||||
|
close,
|
||||||
|
account
|
||||||
|
}:{
|
||||||
|
display: boolean;
|
||||||
|
close: () => void;
|
||||||
|
account: Account | undefined;
|
||||||
|
}){
|
||||||
|
const accountMutate = useForcePasswordReset(account?.accountId ?? "");
|
||||||
|
|
||||||
|
|
||||||
|
const forcePasswordReset = () => {
|
||||||
|
accountMutate.mutate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(accountMutate.isSuccess){
|
||||||
|
accountMutate.reset();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
else if(accountMutate.isError){
|
||||||
|
//TODO: Add message modal here
|
||||||
|
console.log(accountMutate.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RaidBuilderModal
|
||||||
|
display={display}
|
||||||
|
close={close}
|
||||||
|
modalHeader={"Force Password Reset"}
|
||||||
|
modalBody={`Are you sure you want to force reset the password for ${account?.username}?`}
|
||||||
|
modalFooter={
|
||||||
|
<>
|
||||||
|
<DangerButton
|
||||||
|
onClick={forcePasswordReset}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</DangerButton>
|
||||||
|
<SecondaryButton
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</SecondaryButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
56
src/ui/account/modals/RevokeRefreshTokenModal.tsx
Normal file
56
src/ui/account/modals/RevokeRefreshTokenModal.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import DangerButton from "@/components/button/DangerButton";
|
||||||
|
import SecondaryButton from "@/components/button/SecondaryButton";
|
||||||
|
import RaidBuilderModal from "@/components/modal/RaidBuilderModal";
|
||||||
|
import { useRevokeRefreshToken } from "@/hooks/AccountHooks";
|
||||||
|
import { Account } from "@/interface/Account";
|
||||||
|
|
||||||
|
|
||||||
|
export default function RevokeRefreshTokenModal({
|
||||||
|
display,
|
||||||
|
close,
|
||||||
|
account
|
||||||
|
}:{
|
||||||
|
display: boolean;
|
||||||
|
close: () => void;
|
||||||
|
account: Account | undefined;
|
||||||
|
}){
|
||||||
|
const revokeRefreshTokenMutate = useRevokeRefreshToken(account?.accountId ?? "");
|
||||||
|
|
||||||
|
|
||||||
|
const revokeRefreshToken = () => {
|
||||||
|
revokeRefreshTokenMutate.mutate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(revokeRefreshTokenMutate.isSuccess){
|
||||||
|
revokeRefreshTokenMutate.reset();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
else if(revokeRefreshTokenMutate.isError){
|
||||||
|
//TODO: Add message modal here
|
||||||
|
console.log(revokeRefreshTokenMutate.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RaidBuilderModal
|
||||||
|
display={display}
|
||||||
|
close={close}
|
||||||
|
modalHeader={"Revoke Refresh Token"}
|
||||||
|
modalBody={`Are you sure you want to revoke the refresh token for ${account?.username}?`}
|
||||||
|
modalFooter={
|
||||||
|
<>
|
||||||
|
<DangerButton
|
||||||
|
onClick={revokeRefreshToken}
|
||||||
|
>
|
||||||
|
Revoke
|
||||||
|
</DangerButton>
|
||||||
|
<SecondaryButton
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</SecondaryButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user