From 843970e229d4e71439f407053a2f37028952fcdb Mon Sep 17 00:00:00 2001 From: Mattrixwv Date: Sat, 1 Mar 2025 23:32:41 -0500 Subject: [PATCH] Modals and API calls working for admin tab --- package-lock.json | 10 ++ package.json | 1 + src/App.tsx | 5 - .../account/AccountStatusSelector.tsx | 41 +++++ src/components/button/Button.tsx | 11 +- src/components/modal/Modal.tsx | 2 +- src/components/modal/ModalBackground.tsx | 6 +- src/components/modal/ModalHeader.tsx | 13 +- src/components/modal/RaidBuilderModal.tsx | 59 +++++++ src/components/nav/PublicNavLinks.tsx | 4 - src/components/tab/TabGroup.tsx | 13 +- src/components/table/Table.tsx | 39 +++++ src/components/table/TableBody.tsx | 46 +++++ src/components/table/TableHead.tsx | 30 ++++ src/hooks/AccountHooks.ts | 164 ++++++++++++++++++ src/hooks/AuthHooks.ts | 3 - src/index.css | 6 +- src/interface/Account.ts | 20 +++ src/interface/ModalInterfaces.ts | 2 +- src/main.tsx | 26 +-- src/pages/protected/AdminPage.tsx | 31 +++- src/pages/protected/LogoutPage.tsx | 3 +- src/pages/public/LoginPage.tsx | 53 +++--- src/pages/public/TestPage.tsx | 66 ------- src/providers/AuthProvider.tsx | 2 + src/ui/account/AccountAdminButtons.tsx | 70 ++++++++ src/ui/account/AccountsList.tsx | 149 ++++++++++++++++ src/ui/account/AccountsListSkeleton.tsx | 7 + src/ui/account/AccountsLoader.tsx | 45 +++++ src/ui/account/modals/AccountModal.tsx | 113 ++++++++++++ .../modals/AccountPasswordResetModal.tsx | 74 ++++++++ src/ui/account/modals/DeleteAccountModal.tsx | 55 ++++++ .../modals/ForcePasswordResetModal.tsx | 56 ++++++ .../modals/RevokeRefreshTokenModal.tsx | 56 ++++++ 34 files changed, 1150 insertions(+), 131 deletions(-) create mode 100644 src/components/account/AccountStatusSelector.tsx create mode 100644 src/components/modal/RaidBuilderModal.tsx create mode 100644 src/components/table/Table.tsx create mode 100644 src/components/table/TableBody.tsx create mode 100644 src/components/table/TableHead.tsx create mode 100644 src/hooks/AccountHooks.ts delete mode 100644 src/hooks/AuthHooks.ts create mode 100644 src/interface/Account.ts delete mode 100644 src/pages/public/TestPage.tsx create mode 100644 src/ui/account/AccountAdminButtons.tsx create mode 100644 src/ui/account/AccountsList.tsx create mode 100644 src/ui/account/AccountsListSkeleton.tsx create mode 100644 src/ui/account/AccountsLoader.tsx create mode 100644 src/ui/account/modals/AccountModal.tsx create mode 100644 src/ui/account/modals/AccountPasswordResetModal.tsx create mode 100644 src/ui/account/modals/DeleteAccountModal.tsx create mode 100644 src/ui/account/modals/ForcePasswordResetModal.tsx create mode 100644 src/ui/account/modals/RevokeRefreshTokenModal.tsx diff --git a/package-lock.json b/package-lock.json index d69a68c..fb84e2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 1e6d3c8..87a19fb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index 974627c..1ca9de4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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: }, - { - path: "/test", - element: - }, { path: "/login", element: diff --git a/src/components/account/AccountStatusSelector.tsx b/src/components/account/AccountStatusSelector.tsx new file mode 100644 index 0000000..a80bd34 --- /dev/null +++ b/src/components/account/AccountStatusSelector.tsx @@ -0,0 +1,41 @@ +import { AccountStatus } from "@/interface/Account"; + + +export default function AccountStatusSelector({ + value, + onChange +}:{ + value: AccountStatus; + onChange: (e: React.ChangeEvent) => void; +}){ + const modalId = crypto.randomUUID().replace("-", ""); + + + return ( +
+ { + Object.keys(AccountStatus).map((status: string) => ( + + )) + } +
+ ); +} diff --git a/src/components/button/Button.tsx b/src/components/button/Button.tsx index 13a925c..2771f58 100644 --- a/src/components/button/Button.tsx +++ b/src/components/button/Button.tsx @@ -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, HTMLButtonElement>{ @@ -17,7 +17,7 @@ export interface ButtonProps extends React.DetailedHTMLProps { close && -
-
+ } ); diff --git a/src/components/modal/RaidBuilderModal.tsx b/src/components/modal/RaidBuilderModal.tsx new file mode 100644 index 0000000..bfd221a --- /dev/null +++ b/src/components/modal/RaidBuilderModal.tsx @@ -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 ( + + +

+ {modalHeader} +

+
+ +
+ {modalBody} +
+
+ +
+ {modalFooter} +
+
+
+ ); +} diff --git a/src/components/nav/PublicNavLinks.tsx b/src/components/nav/PublicNavLinks.tsx index 8133c60..2201f8a 100644 --- a/src/components/nav/PublicNavLinks.tsx +++ b/src/components/nav/PublicNavLinks.tsx @@ -6,10 +6,6 @@ const publicLinks = [ { name: "Home", path: "/" - }, - { - name: "Test", - path: "/test" } ]; diff --git a/src/components/tab/TabGroup.tsx b/src/components/tab/TabGroup.tsx index 5b628f1..b5c2e61 100644 --- a/src/components/tab/TabGroup.tsx +++ b/src/components/tab/TabGroup.tsx @@ -12,19 +12,22 @@ export interface Tab { } export interface TabGroupProps extends HTMLProps{ - 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(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() on their first render + const divProps = {...props}; + delete divProps.tabs; return (
{ tabs.map((tab, index) => ( @@ -108,8 +111,8 @@ function TabContent({ return (
{tab.tabContent} diff --git a/src/components/table/Table.tsx b/src/components/table/Table.tsx new file mode 100644 index 0000000..c33caf0 --- /dev/null +++ b/src/components/table/Table.tsx @@ -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{ + 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 ( + + + +
+ ); +} diff --git a/src/components/table/TableBody.tsx b/src/components/table/TableBody.tsx new file mode 100644 index 0000000..8b8b79e --- /dev/null +++ b/src/components/table/TableBody.tsx @@ -0,0 +1,46 @@ +import clsx from "clsx"; + + +export default function TableBody({ + bodyElements +}:{ + bodyElements: React.ReactNode[][]; +}){ + return ( + + { + bodyElements.map((row, rowIndex) => ( + + { + row.map((element, elementIndex) => ( + +
+ {element} +
+ + )) + } + + )) + } + + ); +} diff --git a/src/components/table/TableHead.tsx b/src/components/table/TableHead.tsx new file mode 100644 index 0000000..a3882b0 --- /dev/null +++ b/src/components/table/TableHead.tsx @@ -0,0 +1,30 @@ +import clsx from "clsx"; + + +export default function TableHead({ + headElements +}:{ + headElements: React.ReactNode[]; +}){ + return ( + + + { + headElements.map((element, index) => ( + + {element} + + )) + } + + + ); +} diff --git a/src/hooks/AccountHooks.ts b/src/hooks/AccountHooks.ts new file mode 100644 index 0000000..198c7eb --- /dev/null +++ b/src/hooks/AccountHooks.ts @@ -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"] }); + } + }); + +} diff --git a/src/hooks/AuthHooks.ts b/src/hooks/AuthHooks.ts deleted file mode 100644 index b28b04f..0000000 --- a/src/hooks/AuthHooks.ts +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/index.css b/src/index.css index fa28255..93fd150 100644 --- a/src/index.css +++ b/src/index.css @@ -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; diff --git a/src/interface/Account.ts b/src/interface/Account.ts new file mode 100644 index 0000000..d081a4b --- /dev/null +++ b/src/interface/Account.ts @@ -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; +} diff --git a/src/interface/ModalInterfaces.ts b/src/interface/ModalInterfaces.ts index 3258e2b..ff04aa7 100644 --- a/src/interface/ModalInterfaces.ts +++ b/src/interface/ModalInterfaces.ts @@ -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"; diff --git a/src/main.tsx b/src/main.tsx index e27411c..c43e879 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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,18 +7,23 @@ import { AuthProvider } from './providers/AuthProvider.tsx' import { ThemeProvider } from './providers/ThemeProvider.tsx' +const queryClient = new QueryClient(); + + createRoot(document.getElementById('root')!).render( - - + - - - + + + + + ); diff --git a/src/pages/protected/AdminPage.tsx b/src/pages/protected/AdminPage.tsx index b8ea3dc..c9b70cc 100644 --- a/src/pages/protected/AdminPage.tsx +++ b/src/pages/protected/AdminPage.tsx @@ -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: + } + ]; return ( -
- Admin Page -
+
+

+ Admin Functions +

+
+ + +
+
); } diff --git a/src/pages/protected/LogoutPage.tsx b/src/pages/protected/LogoutPage.tsx index ac3ade0..e323313 100644 --- a/src/pages/protected/LogoutPage.tsx +++ b/src/pages/protected/LogoutPage.tsx @@ -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{ diff --git a/src/pages/public/LoginPage.tsx b/src/pages/public/LoginPage.tsx index d73e444..cb4d98a 100644 --- a/src/pages/public/LoginPage.tsx +++ b/src/pages/public/LoginPage.tsx @@ -40,26 +40,39 @@ export default function LoginPage(){ return ( -
- - - + - Login - - +
+ +
+
+ +
+
+ + Login + +
+ + ); } diff --git a/src/pages/public/TestPage.tsx b/src/pages/public/TestPage.tsx deleted file mode 100644 index 0416d30..0000000 --- a/src/pages/public/TestPage.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import TabGroup, { Tab } from "@/components/tab/TabGroup"; - - -export default function TestPage(){ - const tabs: Tab[] = [ - { - tabHeader: "Tab 1", - tabContent: - }, - { - tabHeader: "Tab 2", - tabContent: - }, - { - tabHeader: "Tab 3", - tabContent: - } - ]; - - - - - return ( -
- -
- ); -} - - -function Tab1(){ - console.log("Tab 1"); - - - return ( -
- Tab 1 Content -
- ); -} - -function Tab2(){ - console.log("Tab 2"); - - - return ( -
- Tab 2 Content -
- ); -} - -function Tab3(){ - console.log("Tab 3"); - - - return ( -
- Tab 3 Content -
- ); -} diff --git a/src/providers/AuthProvider.tsx b/src/providers/AuthProvider.tsx index a6ace84..8d2d9ca 100644 --- a/src/providers/AuthProvider.tsx +++ b/src/providers/AuthProvider.tsx @@ -82,6 +82,8 @@ export function AuthProvider({ setExpiration }), [ jwt, setJwt, expiration, setExpiration ]); + //TODO: Return a spinner while the first token is being fetched + return ( diff --git a/src/ui/account/AccountAdminButtons.tsx b/src/ui/account/AccountAdminButtons.tsx new file mode 100644 index 0000000..ca9c5d7 --- /dev/null +++ b/src/ui/account/AccountAdminButtons.tsx @@ -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 ( +
+ + + + + + + + + + + + + + + +
+ ); +} diff --git a/src/ui/account/AccountsList.tsx b/src/ui/account/AccountsList.tsx new file mode 100644 index 0000000..fd4b021 --- /dev/null +++ b/src/ui/account/AccountsList.tsx @@ -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(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[] = [ +
+ ID +
, +
+ Username +
, +
+ Email +
, +
+ Login Date +
, +
+ Status +
, +
+ Actions +
+ ]; + + const bodyElements: React.ReactNode[][] = accounts.map((account) => [ +
+ {account.accountId} +
, +
+ {account.username} +
, +
+ {account.email} +
, +
+ {moment(account.loginDate).format("MM-DD-YYYY HH:mm")} +
, +
+ {account.accountStatus} +
, +
+
+   +
+ { + setSelectedAccount(account); + setDisplayForcePasswordResetModal(true); + }} + showAccountPasswordSetModal={() => { + setSelectedAccount(account); + setDisplayAccountPasswordSetModal(true); + }} + showRevokeRefreshTokenModal={() => { + setSelectedAccount(account); + setDisplayRevokeRefreshTokenModal(true); + }} + showUpdateAccountModal={() => { + setSelectedAccount(account); + setDisplayAccountModal(true); + }} + showDeleteAccountModal={() => { + setSelectedAccount(account); + setDisplayDeleteAccountModal(true); + }} + /> +
+ ]); + + + return ( + <> + + {setDisplayForcePasswordResetModal(false); setSelectedAccount(undefined);}} + account={selectedAccount} + /> + {setDisplayAccountPasswordSetModal(false); setSelectedAccount(undefined);}} + account={selectedAccount} + /> + {setDisplayRevokeRefreshTokenModal(false); setSelectedAccount(undefined);}} + account={selectedAccount} + /> + {setDisplayAccountModal(false); setSelectedAccount(undefined);}} + account={selectedAccount} + /> + {setDisplayDeleteAccountModal(false); setSelectedAccount(undefined);}} + account={selectedAccount} + /> + + ); +} diff --git a/src/ui/account/AccountsListSkeleton.tsx b/src/ui/account/AccountsListSkeleton.tsx new file mode 100644 index 0000000..1c1f548 --- /dev/null +++ b/src/ui/account/AccountsListSkeleton.tsx @@ -0,0 +1,7 @@ +export default function AccountsListSkeleton(){ + return ( +
+ Accounts List Skeleton +
+ ); +} diff --git a/src/ui/account/AccountsLoader.tsx b/src/ui/account/AccountsLoader.tsx new file mode 100644 index 0000000..24acc2c --- /dev/null +++ b/src/ui/account/AccountsLoader.tsx @@ -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 + } + else if(accountsQuery.isError){ + //TODO: + return
Error: {accountsQuery.error.message}
+ } + else{ + return ( + <> + {/* TODO: Add Account Button */} + setDisplayCreateAccountModal(true)} + > + Create Account + + setDisplayCreateAccountModal(false)} + account={undefined} + /> + {/* Account Search Bar */} + + {/* TODO: Add Pagination */} + + ); + } +} diff --git a/src/ui/account/modals/AccountModal.tsx b/src/ui/account/modals/AccountModal.tsx new file mode 100644 index 0000000..b410a89 --- /dev/null +++ b/src/ui/account/modals/AccountModal.tsx @@ -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(account?.username ?? ""); + const [ email, setEmail ] = useState(account?.email ?? ""); + const [ password, setPassword ] = useState(""); + const [ accountStatus, setAccountStatus ] = useState(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 ( + + setUsername(e.target.value)} + /> + setEmail(e.target.value)} + /> + { + !account && ( + setPassword(e.target.value)} + /> + ) + } + setAccountStatus(e.currentTarget.value as AccountStatus)} + /> + + } + modalFooter={ + <> + + {account ? "Update" : "Create"} + + + Cancel + + + } + /> + ); +} diff --git a/src/ui/account/modals/AccountPasswordResetModal.tsx b/src/ui/account/modals/AccountPasswordResetModal.tsx new file mode 100644 index 0000000..61352bb --- /dev/null +++ b/src/ui/account/modals/AccountPasswordResetModal.tsx @@ -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(""); + + + 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 ( + +
Enter new password for {account?.username}.
+ setNewPassword(e.target.value)} + placeholder="Password" + /> + + } + modalFooter={ + <> + + Reset + + + Cancel + + + } + /> + ); +} diff --git a/src/ui/account/modals/DeleteAccountModal.tsx b/src/ui/account/modals/DeleteAccountModal.tsx new file mode 100644 index 0000000..58d461c --- /dev/null +++ b/src/ui/account/modals/DeleteAccountModal.tsx @@ -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 ( + + + Delete + + + Cancel + + + } + /> + ); +} diff --git a/src/ui/account/modals/ForcePasswordResetModal.tsx b/src/ui/account/modals/ForcePasswordResetModal.tsx new file mode 100644 index 0000000..6541756 --- /dev/null +++ b/src/ui/account/modals/ForcePasswordResetModal.tsx @@ -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 ( + + + Reset + + + Cancel + + + } + /> + ); +} diff --git a/src/ui/account/modals/RevokeRefreshTokenModal.tsx b/src/ui/account/modals/RevokeRefreshTokenModal.tsx new file mode 100644 index 0000000..7670aac --- /dev/null +++ b/src/ui/account/modals/RevokeRefreshTokenModal.tsx @@ -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 ( + + + Revoke + + + Cancel + + + } + /> + ); +}