Modals and API calls working for admin tab

This commit is contained in:
2025-03-01 23:32:41 -05:00
parent d68e8864a0
commit 843970e229
34 changed files with 1150 additions and 131 deletions

10
package-lock.json generated
View File

@@ -17,6 +17,7 @@
"@types/node": "^22.13.4",
"axios": "^1.7.9",
"clsx": "^2.1.1",
"moment": "^2.30.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.5.0",
@@ -3471,6 +3472,15 @@
"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": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

View File

@@ -19,6 +19,7 @@
"@types/node": "^22.13.4",
"axios": "^1.7.9",
"clsx": "^2.1.1",
"moment": "^2.30.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.5.0",

View File

@@ -12,7 +12,6 @@ import RaidLayoutPage from "./pages/protected/RaidLayoutPage";
import HomePage from "./pages/public/HomePage";
import LoginPage from "./pages/public/LoginPage";
import SignupPage from "./pages/public/SignupPage";
import TestPage from "./pages/public/TestPage";
import { ProtectedRoute } from "./providers/AuthProvider";
import ErrorBoundary from "./providers/ErrorBoundary";
@@ -28,10 +27,6 @@ const routes = createBrowserRouter([
path: "/",
element: <HomePage/>
},
{
path: "/test",
element: <TestPage/>
},
{
path: "/login",
element: <LoginPage/>

View 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>
);
}

View File

@@ -3,7 +3,7 @@ import clsx from "clsx";
export type ButtonRounding = "none" | "sm" | "md" | "lg" | "full";
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 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){
const {
rounding = "lg",
shape = "vertical",
shape = "horizontal",
size = "md"
} = props;
@@ -33,6 +33,7 @@ export default function Button(props: ButtonProps){
{...props}
className={clsx(
props.className,
"transition-colors duration-300",
//Rounding
{
"rounded-none": rounding === "none",
@@ -44,19 +45,19 @@ export default function Button(props: ButtonProps){
//Shape & Size
{
//Square
"p-0": size === "xsm" && shape === "square",
"p-0": size === "xs" && shape === "square",
"p-1": size === "sm" && shape === "square",
"p-2": size === "md" && shape === "square",
"p-3": size === "lg" && shape === "square",
"p-4": size === "xl" && shape === "square",
//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-4 py-2": size === "md" && shape === "horizontal",
"px-6 py-3": size === "lg" && shape === "horizontal",
"px-8 py-4": size === "xl" && shape === "horizontal",
//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-2 py-4": size === "md" && shape === "vertical",
"px-3 py-6": size === "lg" && shape === "vertical",

View File

@@ -34,7 +34,7 @@ export default function Modal(props: ModalProps){
<div
{...divProps}
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]",
className
)}

View File

@@ -29,8 +29,10 @@ export default function ModalBackground(props: ModalBackgroundProps){
{
"bg-[#00000044]": backgroundType === "darken",
"bg-[#FFFFFF44]": backgroundType === "lighten",
"backdrop-blur-sm bg-radial-[circle] from-transparent from-25% to-[#00000066]": backgroundType === "darken-blur",
"backdrop-blur-sm bg-radial-[circle] from-transparent from-25% to-[#FFFFFF66]": backgroundType === "lighten-blur",
"backdrop-blur-sm bg-black/15": backgroundType === "darken-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",
"backdrop-blur-sm": backgroundType === "blur"
}

View File

@@ -1,6 +1,7 @@
import { ModalHeaderProps } from "@/interface/ModalInterfaces";
import clsx from "clsx";
import { BsXLg } from "react-icons/bs";
import Button from "../button/Button";
export default function ModalHeader(props: ModalHeaderProps){
@@ -30,14 +31,20 @@ export default function ModalHeader(props: ModalHeaderProps){
</div>
{
close &&
<div
className="absolute top-1 right-1 cursor-pointer"
<Button
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}
>
<BsXLg
size={20}
/>
</div>
</Button>
}
</div>
);

View 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>
);
}

View File

@@ -6,10 +6,6 @@ const publicLinks = [
{
name: "Home",
path: "/"
},
{
name: "Test",
path: "/test"
}
];

View File

@@ -12,19 +12,22 @@ export interface Tab {
}
export interface TabGroupProps extends HTMLProps<HTMLDivElement>{
tabs: Tab[];
tabs?: Tab[];
}
export default function TabGroup(props: TabGroupProps){
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);
//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 (
<div
{...props}
{...divProps}
className={clsx(
className,
"flex flex-col w-full"
@@ -50,7 +53,7 @@ export default function TabGroup(props: TabGroupProps){
</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) => (
@@ -108,8 +111,8 @@ function TabContent({
return (
<div
className={clsx(
tab.headerClasses,
""
"w-full",
tab.contentClasses
)}
>
{tab.tabContent}

View 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>
);
}

View 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>
);
}

View 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
View 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"] });
}
});
}

View File

@@ -1,3 +0,0 @@

View File

@@ -29,6 +29,10 @@
color: var(--text-color);
}
a:hover{
color: var(--color-blue-300);
}
a.active {
color: var(--color-blue-400);
}
@@ -40,7 +44,7 @@ body {
max-width: var(--breakpoint-2xl);
margin-inline: auto;
padding-top: 82px;
padding-top: 90px;
padding-inline: 1rem;
text-align: center;

20
src/interface/Account.ts Normal file
View 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;
}

View File

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

View File

@@ -1,3 +1,4 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
@@ -6,8 +7,12 @@ import { AuthProvider } from './providers/AuthProvider.tsx'
import { ThemeProvider } from './providers/ThemeProvider.tsx'
const queryClient = new QueryClient();
createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider
defaultTheme="dark"
storageKey="vite-ui-theme"
@@ -19,5 +24,6 @@ createRoot(document.getElementById('root')!).render(
<App />
</AuthProvider>
</ThemeProvider>
</QueryClientProvider>
</StrictMode>
);

View File

@@ -1,10 +1,33 @@
import TabGroup, { Tab } from "@/components/tab/TabGroup";
import AccountsLoader from "@/ui/account/AccountsLoader";
export default function AdminPage(){
//TODO:
const tabs: Tab[] = [
{
tabHeader: "Accounts",
tabContent: <AccountsLoader/>
}
];
return (
<div>
Admin Page
<main
className="flex flex-col items-center justify-center"
>
<h1
className="text-4xl"
>
Admin Functions
</h1>
<div
className="w-full"
>
<TabGroup
tabs={tabs}
>
</TabGroup>
</div>
</main>
);
}

View File

@@ -4,7 +4,7 @@ import { useNavigate } from "react-router";
export default function LogoutPage(){
const { setJwt } = useAuth();
const { setJwt, setExpiration } = useAuth();
const navigate = useNavigate();
@@ -12,6 +12,7 @@ export default function LogoutPage(){
const response = await api.get("/auth/logout");
if(response.status === 200){
setJwt(null);
setExpiration(null);
navigate("/");
}
else{

View File

@@ -40,26 +40,39 @@ export default function LoginPage(){
return (
<main>
<form
action={login}
className="flex flex-col justify-center space-y-8"
className="flex flex-col items-center justify-center space-y-8"
>
<div
className="mx-auto"
>
<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
className="mx-auto"
type="submit"
>
Login
</PrimaryButton>
</div>
</form>
</main>
);
}

View File

@@ -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>
);
}

View File

@@ -82,6 +82,8 @@ export function AuthProvider({
setExpiration
}), [ jwt, setJwt, expiration, setExpiration ]);
//TODO: Return a spinner while the first token is being fetched
return (
<AuthContext.Provider value={currentTokens}>

View 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>
);
}

View 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"
>
&nbsp;
</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}
/>
</>
);
}

View File

@@ -0,0 +1,7 @@
export default function AccountsListSkeleton(){
return (
<div>
Accounts List Skeleton
</div>
);
}

View 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 */}
</>
);
}
}

View 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>
</>
}
/>
);
}

View 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>
</>
}
/>
);
}

View 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>
</>
}
/>
);
}

View 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>
</>
}
/>
);
}

View 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>
</>
}
/>
);
}