Compare commits

...

75 Commits

Author SHA1 Message Date
bba79a9e70 Update for sonarqube scans 2026-02-05 21:04:13 -05:00
0031e1c9e7 Update dependencies 2026-01-07 14:06:46 -05:00
8b5efb0879 Update eslint to type check 2025-05-25 13:34:23 -04:00
c4265bbcad Update dependencies 2025-05-24 13:02:54 -04:00
567ac0aa86 Add Raid Builder demo video to home page 2025-03-26 21:59:59 -04:00
1670cd5b7a Fix Raid Instance action button permissions 2025-03-25 20:49:27 -04:00
f3f50dcd14 Fix raid layout selector label bug 2025-03-25 20:41:24 -04:00
6bb0f18ea9 Fix roster label bug 2025-03-25 20:35:59 -04:00
7c2a322fa4 Fix raid group request confirmation error 2025-03-25 20:27:02 -04:00
e3dd099785 Fix Raid Group Request submit issue 2025-03-25 20:22:27 -04:00
f4a0ca4320 Fix success loop on account confirmation 2025-03-25 20:13:59 -04:00
e8e576a200 Update raid instance creator table style 2025-03-25 19:04:52 -04:00
c5fbb1d83f Show better error messages 2025-03-23 11:47:10 -04:00
3b738c337b Confirm and password emails sending 2025-03-22 23:46:27 -04:00
4d7585c770 Add extra time for jwt to expire 2025-03-21 20:17:07 -04:00
81507afbcc Updated input validation 2025-03-21 20:10:15 -04:00
031184b666 Fix raid groups list search flickering 2025-03-16 23:34:28 -04:00
d07bd8a97d Fix character selection layout 2025-03-16 22:17:11 -04:00
1d50d452bc Fix person selector layout 2025-03-16 21:53:11 -04:00
3f6b6a9e63 Fix game class name alignment 2025-03-16 21:12:54 -04:00
6155b4784d Fix person name wrapping 2025-03-16 20:52:34 -04:00
538a96d909 Fix raid layout not saving on new instance 2025-03-16 20:38:32 -04:00
624f3bf369 Fix raid layout not saving 2025-03-16 20:25:01 -04:00
73628fded6 Fix raid size not adjusting bug 2025-03-16 20:17:38 -04:00
5f238ea185 Update accept request button 2025-03-16 20:00:58 -04:00
f10e54ddfd Fix adhoc raid instance saving 2025-03-16 15:06:36 -04:00
7fcabf69f1 Update for Adhoc instances 2025-03-16 13:42:02 -04:00
abf8a08f83 Fix missing search input placeholder 2025-03-16 13:39:37 -04:00
ea5794aa22 Add AM/PM marker to date display 2025-03-16 13:35:26 -04:00
220c9969e2 Update date display to 12-hour format 2025-03-16 13:31:58 -04:00
490385788a Fix modal resets 2025-03-16 12:30:27 -04:00
6bc38ec748 Fix login issues 2025-03-16 11:14:42 -04:00
fddfeaf9ca Fix title 2025-03-16 10:43:34 -04:00
c623680ea8 Fix icons 2025-03-16 10:18:38 -04:00
add7b97056 Fixed build warnings 2025-03-15 22:46:10 -04:00
59a57dbdc3 Raid Instance Creator works for raiders 2025-03-15 22:41:24 -04:00
10d8159353 Forgot password page created 2025-03-15 21:52:26 -04:00
d42f625540 Password reset working 2025-03-15 21:27:15 -04:00
49243a71a1 Signup page (mostly) working 2025-03-15 20:20:14 -04:00
0eeedf1e3b Raid Instances showing up on calendar 2025-03-15 18:37:57 -04:00
ea0018bae2 Tutorial Working 2025-03-15 18:23:04 -04:00
a842c24d0d Buttons hidden by permissions 2025-03-15 16:51:13 -04:00
56236fd2ac Raid Instance Creator working 2025-03-15 12:20:05 -04:00
cff5e098b8 Raid Instance tab working 2025-03-11 22:58:18 -04:00
c9ceeea3b4 Person page working 2025-03-10 22:55:16 -04:00
5a2c8a8936 Raid Request button working 2025-03-10 20:30:39 -04:00
439e82b821 Requests tab working 2025-03-10 19:19:48 -04:00
65fb3479fd User tab working 2025-03-09 19:49:40 -04:00
8b8538a968 Raid Layout page working 2025-03-09 12:15:29 -04:00
c2f13d9900 Class Groups tab working 2025-03-08 18:45:52 -05:00
90337f92ab Fix person list skeleton 2025-03-08 14:50:02 -05:00
b763a1c7bd People tab working 2025-03-08 13:26:39 -05:00
0dfb971bc2 Raid Group calendar working 2025-03-07 21:54:17 -05:00
61789d7ca2 Raid groups page working 2025-03-06 23:54:26 -05:00
6c80becf71 Update skeletons 2025-03-06 22:53:06 -05:00
a463bb734f Game Classes tab working 2025-03-06 22:31:31 -05:00
28462776ac Game calendar working 2025-03-06 19:49:03 -05:00
ef6da3ea64 Games page working 2025-03-05 20:33:52 -05:00
b78b6109b3 Admin page raid groups tab working 2025-03-05 20:12:10 -05:00
58d1e83a2f Fixed search box functionality 2025-03-04 22:17:42 -05:00
ffe51d6fbb Games tab on admin page working 2025-03-04 21:14:24 -05:00
91800574e4 Fix token refresh issues 2025-03-02 22:41:43 -05:00
7c3b462651 Added paging to accounts table 2025-03-02 20:45:44 -05:00
d06d421d03 Add AccountList skeleton 2025-03-02 14:39:39 -05:00
3d06d8189d Add timed message modal component 2025-03-02 12:48:11 -05:00
6f3fe1798b Added message components 2025-03-02 11:42:37 -05:00
843970e229 Modals and API calls working for admin tab 2025-03-01 23:32:41 -05:00
d68e8864a0 Add tab components 2025-02-26 23:19:35 -05:00
1499f4055f Added buttons 2025-02-26 21:10:29 -05:00
d1b0a499a8 Update modal 2025-02-25 23:19:45 -05:00
29724da42f Update routing and add error boundary 2025-02-25 22:50:05 -05:00
bc7ffcd5bf Update css files 2025-02-25 22:31:40 -05:00
f6982951eb Create modal component 2025-02-25 22:03:38 -05:00
eba1676c15 Modal initial comit 2025-02-24 23:29:46 -05:00
5bb6e0a37f Authorization working 2025-02-24 21:53:20 -05:00
243 changed files with 22339 additions and 49 deletions

5
.gitignore vendored
View File

@@ -22,3 +22,8 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# Sonarqube
sonarBuild.sh
sonarBuild.ps1
.scannerwork/

View File

@@ -1,50 +1,3 @@
# React + TypeScript + Vite
# Raid Builder Web
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
```js
// eslint.config.js
import react from 'eslint-plugin-react'
export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```
[![Quality Gate Status](https://sonarqube.mattrixwv.com/api/project_badges/measure?project=RaidBuilderWeb&metric=alert_status&token=sqb_b0e5f21c5093cde890647ef9346274717bf3d59f)](https://sonarqube.mattrixwv.com/dashboard?id=RaidBuilderWeb)

3
TODO.txt Normal file
View File

@@ -0,0 +1,3 @@
Change page layout to tanstack router
Make raid layout page
Move providers to context and components to providers

42
eslint.config.js Normal file
View File

@@ -0,0 +1,42 @@
import js from "@eslint/js";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import globals from "globals";
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommendedTypeChecked],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname
}
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
react: react
},
rules: {
...react.configs.recommended.rules,
...react.configs["jsx-runtime"].rules,
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true }
]
},
settings: {
react: {
version: "detect"
}
}
}
);

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/favicon.ico"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Raid Builder</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

5918
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

47
package.json Normal file
View File

@@ -0,0 +1,47 @@
{
"name": "raid-builder-web-react",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && eslint && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@fullcalendar/core": "^6.1.19",
"@fullcalendar/daygrid": "^6.1.19",
"@fullcalendar/interaction": "^6.1.19",
"@fullcalendar/react": "^6.1.19",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-query": "^5.90.12",
"@types/node": "^25.0.3",
"axios": "^1.13.2",
"clsx": "^2.1.1",
"eslint-plugin-react": "^7.37.5",
"moment": "^2.30.1",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-icons": "^5.5.0",
"react-joyride": "^3.0.0-7",
"react-router": "^7.11.0",
"react-tooltip": "^5.30.0",
"tailwindcss": "^4.1.18",
"use-debounce": "^10.0.6",
"zod": "^4.2.1"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"eslint": "^9.39.2",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
"globals": "^16.5.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.50.0",
"vite": "^7.3.0"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

114
src/App.tsx Normal file
View File

@@ -0,0 +1,114 @@
import { createBrowserRouter, RouterProvider } from "react-router";
import NavBar from "./components/nav/NavBar";
import AccountPage from "./pages/protected/AccountPage";
import AdminPage from "./pages/protected/AdminPage";
import GamePage from "./pages/protected/GamePage";
import GamesPage from "./pages/protected/GamesPage";
import LogoutPage from "./pages/protected/LogoutPage";
import PersonPage from "./pages/protected/PersonPage";
import RaidGroupPage from "./pages/protected/RaidGroupPage";
import RaidGroupsPage from "./pages/protected/RaidGroupsPage";
import RaidInstancePage from "./pages/protected/RaidInstancePage";
import RaidLayoutPage from "./pages/protected/RaidLayoutPage";
import ConfirmPage from "./pages/public/ConfirmPage";
import ForgotPasswordPage from "./pages/public/ForgotPassword";
import ForgotTokenPage from "./pages/public/ForgotTokenPage";
import HomePage from "./pages/public/HomePage";
import LoginPage from "./pages/public/LoginPage";
import SignupPage from "./pages/public/SignupPage";
import { ProtectedRoute } from "./providers/components/AuthProviderComponent";
import ErrorBoundary from "./providers/components/ErrorBoundary";
const routes = createBrowserRouter([
{
element: <NavBar/>,
children: [
{
errorElement: <ErrorBoundary/>,
children: [
{
path: "/",
element: <HomePage/>
},
{
path: "/login",
element: <LoginPage/>
},
{
path: "/signup",
element: <SignupPage/>
},
{
path: "/forgotPassword",
element: <ForgotPasswordPage/>
},
{
path: "/forgotPassword/:forgotToken",
element: <ForgotTokenPage/>
},
{
path: "/confirm/:confirmToken",
element: <ConfirmPage/>
},
{
element: <ProtectedRoute/>,
children: [
{
path: "/account",
element: <AccountPage/>
},
{
path: "/logout",
element: <LogoutPage/>
},
{
path: "/admin",
element: <AdminPage/>
},
{
path: "/game",
element: <GamesPage/>
},
{
path: "/game/:gameId",
element: <GamePage/>
},
{
path: "/raidGroup",
element: <RaidGroupsPage/>
},
{
path: "/raidGroup/:raidGroupId",
element: <RaidGroupPage/>
},
{
path: "/raidGroup/:raidGroupId/person/:personId",
element: <PersonPage/>
},
{
path: "/raidGroup/:raidGroupId/raidLayout/:raidLayoutId",
element: <RaidLayoutPage/>
},
{
path: "/raidGroup/:raidGroupId/raidInstance",
element: <RaidInstancePage/>
},
{
path: "/raidGroup/:raidGroupId/raidInstance/:raidInstanceId",
element: <RaidInstancePage/>
}
]
}
]
}
]
}
]);
export default function App(){
return (
<RouterProvider router={routes}/>
);
}

View File

@@ -0,0 +1,309 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="135.44383mm"
height="213.09181mm"
viewBox="0 0 135.44383 213.09181"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
sodipodi:docname="RaidBuilderIcon.svg"
inkscape:export-filename="..\..\..\..\Programs\Web\raidBuilderWeb\public\raidBuilderIcon.svg"
inkscape:export-xdpi="59.635048"
inkscape:export-ydpi="59.635048"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
inkscape:zoom="1"
inkscape:cx="-5.9999998"
inkscape:cy="413.49999"
inkscape:window-width="2560"
inkscape:window-height="1369"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer2" />
<defs
id="defs1">
<linearGradient
id="linearGradient225"
inkscape:label="Shield Outer">
<stop
style="stop-color:#666600;stop-opacity:1;"
offset="0"
id="stop224" />
<stop
style="stop-color:#cccc00;stop-opacity:1;"
offset="1"
id="stop225" />
</linearGradient>
<linearGradient
id="linearGradient217"
inkscape:label="Shield Fill">
<stop
style="stop-color:#4c1500;stop-opacity:1;"
offset="0"
id="stop216" />
<stop
style="stop-color:#7a2500;stop-opacity:1;"
offset="1"
id="stop217" />
</linearGradient>
<linearGradient
id="swatch15"
inkscape:swatch="solid">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop15" />
</linearGradient>
<linearGradient
id="swatch14"
inkscape:swatch="solid">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop14" />
</linearGradient>
<linearGradient
id="swatch13"
inkscape:swatch="solid"
inkscape:label="Letter">
<stop
style="stop-color:#6372ff;stop-opacity:1;"
offset="0"
id="stop13" />
</linearGradient>
<linearGradient
id="swatch12"
inkscape:swatch="solid"
inkscape:label="Letter Outline">
<stop
style="stop-color:#5a001b;stop-opacity:1;"
offset="0"
id="stop12" />
</linearGradient>
<linearGradient
id="swatch11"
inkscape:swatch="solid">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop11" />
</linearGradient>
<linearGradient
id="swatch10"
inkscape:swatch="solid">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop10" />
</linearGradient>
<linearGradient
id="swatch9"
inkscape:swatch="solid">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop9" />
</linearGradient>
<linearGradient
id="swatch3"
inkscape:label="Swords"
inkscape:swatch="solid">
<stop
style="stop-color:#671500;stop-opacity:1;"
offset="0"
id="stop3" />
</linearGradient>
<inkscape:path-effect
effect="mirror_symmetry"
start_point="109.2765,26.728528"
end_point="109.2765,143.17813"
center_point="109.2765,84.953329"
id="path-effect10"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="free"
discard_orig_path="false"
fuse_paths="false"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" />
<linearGradient
id="swatch1"
inkscape:label="Shield Outer">
<stop
style="stop-color:#646400;stop-opacity:1;"
offset="0"
id="stop1" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient4"
x1="37.570831"
y1="148.51927"
x2="172.80254"
y2="148.51927"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#swatch9"
id="linearGradient9"
x1="164.49226"
y1="563.35052"
x2="630.75256"
y2="563.35052"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#swatch10"
id="linearGradient10"
x1="45.840488"
y1="84.953331"
x2="172.71251"
y2="84.953331"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#swatch13"
id="linearGradient212"
gradientUnits="userSpaceOnUse"
x1="25.538338"
y1="191.0405"
x2="49.956448"
y2="191.0405" />
<linearGradient
inkscape:collect="always"
xlink:href="#swatch12"
id="linearGradient213"
gradientUnits="userSpaceOnUse"
x1="25.538338"
y1="191.0405"
x2="49.956448"
y2="191.0405" />
<linearGradient
inkscape:collect="always"
xlink:href="#swatch13"
id="linearGradient214"
gradientUnits="userSpaceOnUse"
x1="-7.043222"
y1="170.3968"
x2="20.277262"
y2="170.3968" />
<linearGradient
inkscape:collect="always"
xlink:href="#swatch12"
id="linearGradient215"
gradientUnits="userSpaceOnUse"
x1="-7.043222"
y1="170.3968"
x2="20.277262"
y2="170.3968" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient217"
id="linearGradient216"
gradientUnits="userSpaceOnUse"
x1="166.86644"
y1="346.35645"
x2="543.37854"
y2="784.35645"
spreadMethod="pad" />
<linearGradient
inkscape:collect="always"
xlink:href="#swatch3"
id="linearGradient223"
gradientUnits="userSpaceOnUse"
x1="45.74139"
y1="85.041733"
x2="172.81161"
y2="85.041733" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient225"
id="linearGradient224"
gradientUnits="userSpaceOnUse"
x1="40.481247"
y1="63.588024"
x2="149.78378"
y2="214.13594" />
</defs>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Swords"
style="display:inline"
transform="translate(-37.465061,-23.621729)"
sodipodi:insensitive="true">
<path
d="m 48.133501,27.1261 c -0.875794,0.461739 -1.679385,1.506522 -2.059419,2.732917 -0.257264,0.830207 -0.291602,1.692177 -0.15403,2.440154 l 4.88442,-0.10376 2.847281,-3.969745 c -0.539125,-0.53641 -1.269351,-0.995251 -2.099749,-1.252574 -1.226398,-0.380035 -2.54245,-0.308219 -3.418373,0.153254 z m 0.864049,8.67163 5.691829,10.178023 c 0.818709,-0.6755 1.614517,-1.223595 2.487958,-1.683773 0.873387,-0.459994 1.775488,-0.806623 2.795568,-1.099891 l -5.175707,-10.44963 -2.136009,2.977396 z m 9.84112,11.647241 c -0.759743,0.400274 -1.496586,0.938015 -2.369671,1.712745 L 66.79801,67.627649 c 0.694006,-0.470403 1.350315,-0.869995 2.027566,-1.226797 0.6775,-0.356951 1.378187,-0.672317 2.158583,-0.978711 L 61.591265,46.459012 c -1.132678,0.28202 -1.992851,0.585671 -2.752595,0.985959 z m 11.607841,22.03188 c -2.257736,1.189661 -4.5455,3.244091 -9.71083,7.478415 l 1.447631,2.747663 c 9.485159,-7.760186 9.605743,-7.823559 21.36968,-11.2588 l -1.44763,-2.747658 c -6.412968,1.865959 -9.401233,2.591094 -11.658908,3.78056 z m 3.137683,5.955757 c -0.990293,0.521596 -1.933369,1.171019 -3.101667,2.077697 l 32.006043,59.162725 6.78793,6.5051 -1.52696,-9.27693 -30.697841,-59.851933 c -1.408251,0.450961 -2.477331,0.861509 -3.467505,1.383341 z M 170.4195,27.1261 c 0.87579,0.461739 1.67938,1.506522 2.05942,2.732917 0.25726,0.830207 0.2916,1.692177 0.15403,2.440154 l -4.88442,-0.10376 -2.84728,-3.969745 c 0.53912,-0.53641 1.26935,-0.995251 2.09975,-1.252574 1.22639,-0.380035 2.54245,-0.308219 3.41837,0.153254 z m -0.86405,8.67163 -5.69183,10.178023 c -0.81871,-0.6755 -1.61452,-1.223595 -2.48796,-1.683773 -0.87338,-0.459994 -1.77549,-0.806623 -2.79557,-1.099891 l 5.17571,-10.44963 2.13601,2.977396 z m -9.84112,11.647241 c 0.75974,0.400274 1.49659,0.938015 2.36967,1.712745 l -10.32901,18.469933 c -0.69401,-0.470403 -1.35032,-0.869995 -2.02757,-1.226797 -0.6775,-0.356951 -1.37818,-0.672317 -2.15858,-0.978711 l 9.39289,-18.963129 c 1.13268,0.28202 1.99286,0.585671 2.7526,0.985959 z m -11.60784,22.03188 c 2.25773,1.189661 4.5455,3.244091 9.71083,7.478415 l -1.44763,2.747663 c -9.48516,-7.760186 -9.60575,-7.823559 -21.36968,-11.2588 l 1.44763,-2.747658 c 6.41297,1.865959 9.40123,2.591094 11.65891,3.78056 z m -3.13768,5.955757 c 0.99029,0.521596 1.93336,1.171019 3.10166,2.077697 l -32.00604,59.162725 -6.78793,6.5051 1.52696,-9.27693 30.69784,-59.851933 c 1.40825,0.450961 2.47733,0.861509 3.46751,1.383341 z"
id="path1-0"
style="display:inline;fill:url(#linearGradient223);stroke:url(#linearGradient10);stroke-width:0.197985"
inkscape:label="Outline"
inkscape:path-effect="#path-effect10"
inkscape:original-d="m 48.133501,27.1261 c -0.875794,0.461739 -1.679385,1.506522 -2.059419,2.732917 -0.257264,0.830207 -0.291602,1.692177 -0.15403,2.440154 l 4.88442,-0.10376 2.847281,-3.969745 c -0.539125,-0.53641 -1.269351,-0.995251 -2.099749,-1.252574 -1.226398,-0.380035 -2.54245,-0.308219 -3.418373,0.153254 z m 0.864049,8.67163 5.691829,10.178023 c 0.818709,-0.6755 1.614517,-1.223595 2.487958,-1.683773 0.873387,-0.459994 1.775488,-0.806623 2.795568,-1.099891 l -5.175707,-10.44963 -2.136009,2.977396 z m 9.84112,11.647241 c -0.759743,0.400274 -1.496586,0.938015 -2.369671,1.712745 L 66.79801,67.627649 c 0.694006,-0.470403 1.350315,-0.869995 2.027566,-1.226797 0.6775,-0.356951 1.378187,-0.672317 2.158583,-0.978711 L 61.591265,46.459012 c -1.132678,0.28202 -1.992851,0.585671 -2.752595,0.985959 z m 11.607841,22.03188 c -2.257736,1.189661 -4.5455,3.244091 -9.71083,7.478415 l 1.447631,2.747663 c 9.485159,-7.760186 9.605743,-7.823559 21.36968,-11.2588 l -1.44763,-2.747658 c -6.412968,1.865959 -9.401233,2.591094 -11.658908,3.78056 z m 3.137683,5.955757 c -0.990293,0.521596 -1.933369,1.171019 -3.101667,2.077697 l 32.006043,59.162725 6.78793,6.5051 -1.52696,-9.27693 -30.697841,-59.851933 c -1.408251,0.450961 -2.477331,0.861509 -3.467505,1.383341 z"
transform="matrix(1.0658976,0,0,1.0647436,-11.290577,-4.7306918)"
sodipodi:insensitive="true" />
</g>
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="Shield"
style="display:inline;fill:url(#linearGradient4)"
transform="translate(-37.465061,-23.621729)"
sodipodi:insensitive="true">
<path
style="fill:url(#linearGradient216);fill-rule:nonzero;stroke:url(#linearGradient9);stroke-width:3.77953;stroke-dasharray:none;stroke-opacity:1"
d="M 384.22136,866.17854 C 346.2122,852.01472 314.71308,833.25777 283.18273,806.01253 225.33369,756.02543 184.27358,682.43474 170.38271,603.84478 c -5.2229,-29.54949 -5.11093,-26.51519 -5.5145,-149.43544 l -0.37594,-114.50623 8.42628,-0.5587 c 4.63445,-0.30728 15.9397,-1.23054 25.12278,-2.05169 61.95284,-5.53977 120.20353,-24.92782 165.60529,-55.11975 12.12441,-8.06267 31.38578,-23.06982 33.03537,-25.73892 0.31293,-0.50634 4.31215,2.31655 8.88713,6.27308 40.26094,34.81846 97.69777,60.08028 160.7126,70.68449 13.80381,2.32293 36.3077,4.74423 55.08405,5.92677 l 9.3868,0.59118 -0.40303,113.54603 c -0.27563,77.65784 -0.75271,116.41945 -1.50941,122.6372 -5.85838,48.13787 -16.71599,85.33197 -36.5955,125.36238 -31.02772,62.479 -77.90359,111.42228 -139.1663,145.30408 -16.17168,8.94387 -44.87489,21.43604 -54.02402,23.51225 -1.57112,0.35654 -6.66308,-1.04852 -14.83295,-4.09297 z"
id="path9"
inkscape:label="Fill"
transform="scale(0.26458333)"
sodipodi:insensitive="true" />
<path
d="m 169.8625,83.843633 c -42.92141,0 -62.02998,-22.04861 -62.32419,-22.342477 -0.58773,-0.882289 -1.46968,-1.176156 -2.35162,-1.176156 -0.88195,0 -1.76389,0.293867 -2.35197,1.175812 0,0 -19.402777,22.342477 -62.324187,22.342477 -1.763889,0 -2.9397,1.175811 -2.9397,2.9397 v 56.150581 c 0,30.57418 13.817244,59.67835 36.747799,77.90497 9.407522,7.34942 19.402777,12.64109 29.986108,15.875 0.29387,0 0.58808,0 0.88195,0 0.29386,0 0.58807,0 0.88194,0 10.58333,-3.23391 20.57859,-8.52558 29.98611,-15.875 22.93056,-18.22697 36.7478,-47.33113 36.7478,-77.90497 V 86.783333 c 0,-1.763889 -1.17615,-2.9397 -2.94004,-2.9397 z m -2.9397,59.090277 c 0,28.8103 -12.9353,56.15058 -34.6897,73.20139 -8.52558,6.46747 -17.63889,11.46527 -27.04641,14.40497 C 95.779165,227.60057 86.665854,222.89664 78.140276,216.1353 56.385877,199.08414 43.450577,171.74386 43.450577,142.93391 V 89.723033 c 35.865855,-0.881944 55.562499,-16.168866 61.736113,-22.342477 6.17361,6.173611 25.87025,21.460533 61.73611,22.342477 z"
id="path1"
style="fill:url(#linearGradient224);stroke-width:0.345;stroke-dasharray:none"
inkscape:label="Outline"
sodipodi:insensitive="true" />
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Runes"
transform="matrix(1.2396599,0,0,1.3573618,35.341625,-64.747823)"
sodipodi:insensitive="true">
<path
style="font-size:72.5698px;font-family:'Elder Futhark';-inkscape-font-specification:'Elder Futhark, Normal';fill:url(#linearGradient214);stroke:url(#linearGradient215);stroke-width:0.354849"
d="m -2.7908332,176.61555 v 25.12305 l -2.019765,5.06713 -2.0197649,-5.06713 -0.035435,-67.75072 21.4024221,21.11895 -16.6187682,16.72507 19.4535262,19.59526 2.728455,5.138 -5.421475,-2.40954 z M 9.0797154,155.10683 -2.7908332,143.16541 v 23.8474 z"
id="text3"
inkscape:label="R"
transform="scale(1.242653,0.80472988)"
aria-label="r"
sodipodi:insensitive="true" />
<path
d="M 25.778794,231.8298 25.738765,150.25119 49.75602,173.86816 32.983971,191.0405 49.75602,208.21284 Z M 43.511534,208.21284 30.42213,194.68312 v 27.05944 z m 0,-34.34468 -13.089404,-13.52972 v 27.01941 z"
id="text2"
style="font-size:81.9789px;font-family:'Elder Futhark';-inkscape-font-specification:'Elder Futhark, Normal';fill:url(#linearGradient212);stroke:url(#linearGradient213);stroke-width:0.400855"
inkscape:label="B"
transform="scale(1.393201,0.71777152)"
aria-label="b"
sodipodi:insensitive="true" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,17 @@
import { CallBackProps, Joyride, Props } from "react-joyride";
interface TutorialComponentProps extends Props {
updateFunction: () => void;
}
export default function TutorialComponent(props: TutorialComponentProps){
return Joyride({
...props,
callback: (callbackProps: CallBackProps) => {
if(callbackProps.type === "tour:end"){
props.updateFunction();
}
}
});
}

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().replaceAll("-", "");
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 as AccountStatus}
/>
<span
className="ml-1"
>
{status}
</span>
</label>
))
}
</div>
);
}

View File

@@ -0,0 +1,69 @@
import clsx from "clsx";
export type ButtonRounding = "none" | "sm" | "md" | "lg" | "full";
export type ButtonShape = "vertical" | "horizontal" | "square";
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>{
rounding?: ButtonRounding;
shape?: ButtonShape;
size?: ButtonSizeType;
variant?: ButtonVariant;
}
export default function Button(props: ButtonProps){
const {
rounding = "lg",
shape = "horizontal",
size = "md"
} = props;
const buttonProps = {...props};
delete buttonProps.rounding;
delete buttonProps.shape;
delete buttonProps.size;
delete buttonProps.variant;
return (
<button
{...props}
className={clsx(
props.className,
"transition-colors duration-300",
//Rounding
{
"rounded-none": rounding === "none",
"rounded-sm": rounding === "sm",
"rounded-md": rounding === "md",
"rounded-lg": rounding === "lg",
"rounded-full": rounding === "full"
},
//Shape & Size
{
//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 === "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 === "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",
"px-4 py-8": size === "xl" && shape === "vertical",
}
)}
/>
);
}

View File

@@ -0,0 +1,46 @@
import clsx from "clsx";
import Button, { ButtonProps } from "./Button";
export default function DangerButton(props: ButtonProps){
const {
variant = "solid",
disabled
} = props;
return (
<Button
{...props}
className={clsx(
props.className,
//Background
{
"bg-transparent": variant === "outline" || variant === "icon",
"bg-red-600 hover:bg-red-700 active:bg-red-800": (variant === "solid") && (!disabled),
"bg-red-400/80": (variant === "solid") && (disabled),
"bg-transparent hover:bg-red-600 active:bg-red-700": (variant === "ghost" || variant === "outline-ghost") && (!disabled),
"bg-transparent ": (variant === "ghost" || variant === "outline-ghost") && (disabled)
},
//Text
{
"text-white": variant === "solid",
"text-red-600 hover:text-red-700 active:text-red-800": (variant === "outline" || variant === "icon") && (!disabled),
"text-red-400/80": (variant === "outline" || variant === "icon") && (disabled),
"text-red-600 hover:text-white active:text-white": (variant === "ghost" || variant === "outline-ghost") && (!disabled),
"text-red-400/80 ": (variant === "ghost" || variant === "outline-ghost") && (disabled)
},
//Outline
{
"outline-none": variant === "ghost" || variant === "icon",
"outline outline-red-600 hover:outline-red-700 active:outline-red-800": (variant === "solid" || variant === "outline") && (!disabled),
"outline outline-red-400/80": (variant === "solid" || variant === "outline") && (disabled),
"outline hover:outline-red-600 active:outline-red-700": (variant === "outline-ghost") && (!disabled),
"outline outline-red-400/80 ": (variant === "outline-ghost") && (disabled)
}
)}
>
{props.children}
</Button>
);
}

View File

@@ -0,0 +1,46 @@
import clsx from "clsx";
import Button, { ButtonProps } from "./Button";
export default function DarkButton(props: ButtonProps){
const {
variant = "solid",
disabled
} = props;
return (
<Button
{...props}
className={clsx(
props.className,
//Background
{
"bg-transparent": variant === "outline" || variant === "icon",
"bg-black hover:bg-neutral-700 active:bg-neutral-500": (variant === "solid") && (!disabled),
"bg-neutral-700": (variant === "solid") && (disabled),
"bg-transparent hover:bg-black active:bg-neutral-700": (variant === "ghost" || variant === "outline-ghost") && (!disabled),
"bg-transparent ": (variant === "ghost" || variant === "outline-ghost") && (disabled)
},
//Text
{
"text-white": variant === "solid",
"text-black hover:text-neutral-700 active:text-neutral-500": (variant === "outline" || variant === "icon") && (!disabled),
"text-neutral-700": (variant === "outline" || variant === "icon") && (disabled),
"text-black hover:text-white active:text-white": (variant === "ghost" || variant === "outline-ghost") && (!disabled),
"text-neutral-700 ": (variant === "ghost" || variant === "outline-ghost") && (disabled)
},
//Outline
{
"outline-none": variant === "ghost" || variant === "icon",
"outline outline-black hover:outline-neutral-700 active:outline-neutral-500": (variant === "solid" || variant === "outline") && (!disabled),
"outline outline-neutral-700": (variant === "solid" || variant === "outline") && (disabled),
"outline hover:outline-black active:outline-neutral-700": (variant === "outline-ghost") && (!disabled),
"outline outline-neutral-700 ": (variant === "outline-ghost") && (disabled)
}
)}
>
{props.children}
</Button>
);
}

View File

@@ -0,0 +1,39 @@
import clsx from "clsx";
import Button, { ButtonProps } from "./Button";
export default function InfoButton(props: ButtonProps){
const {
variant = "solid"
} = props;
return (
<Button
{...props}
className={clsx(
props.className,
//Background
{
"bg-transparent": variant === "outline" || variant === "icon",
"bg-cyan-300 hover:bg-cyan-400 active:bg-cyan-500": variant === "solid",
"bg-transparent hover:bg-cyan-300 active:bg-cyan-400": variant === "ghost" || variant === "outline-ghost"
},
//Text
{
"text-black": variant === "solid",
"text-cyan-300 hover:text-cyan-400 active:text-cyan-500": variant === "outline" || variant === "icon",
"text-cyan-300 hover:text-black active:text-black": variant === "ghost" || variant === "outline-ghost"
},
//Outline
{
"outline-none": variant === "ghost" || variant === "icon",
"outline outline-cyan-300 hover:outline-cyan-400 active:outline-cyan-500": variant === "solid" || variant === "outline",
"outline hover:outline-cyan-300 active:outline-cyan-400": variant === "outline-ghost"
}
)}
>
{props.children}
</Button>
);
}

View File

@@ -0,0 +1,46 @@
import clsx from "clsx";
import Button, { ButtonProps } from "./Button";
export default function LightButton(props: ButtonProps){
const {
variant = "solid",
disabled
} = props;
return (
<Button
{...props}
className={clsx(
props.className,
//Background
{
"bg-transparent": variant === "outline" || variant === "icon",
"bg-white hover:bg-neutral-300 active:bg-neutral-400": (variant === "solid") && (!disabled),
"bg-neutral-400/80": (variant === "solid") && (disabled),
"bg-transparent hover:bg-white active:bg-neutral-300": (variant === "ghost" || variant === "outline-ghost") && (!disabled),
"bg-transparent ": (variant === "ghost" || variant === "outline-ghost") && (disabled)
},
//Text
{
"text-black": variant === "solid",
"text-white hover:text-neutral-300 active:text-neutral-400": (variant === "outline" || variant === "icon") && (!disabled),
"text-neutral-400/80": (variant === "outline" || variant === "icon") && (disabled),
"text-white hover:text-black active:text-black": (variant === "ghost" || variant === "outline-ghost") && (!disabled),
"text-neutral-400/80 ": (variant === "ghost" || variant === "outline-ghost") && (disabled)
},
//Outline
{
"outline-none": variant === "ghost" || variant === "icon",
"outline outline-white hover:outline-neutral-300 active:outline-neutral-400": (variant === "solid" || variant === "outline") && (!disabled),
"outline outline-neutral-400/80": (variant === "solid" || variant === "outline") && (disabled),
"outline hover:outline-neutral-300 active:outline-neutral-400": (variant === "outline-ghost") && (!disabled),
"outline outline-neutral-400/80 ": (variant === "outline-ghost") && (disabled)
}
)}
>
{props.children}
</Button>
);
}

View File

@@ -0,0 +1,46 @@
import clsx from "clsx";
import Button, { ButtonProps } from "./Button";
export default function MoltenButton(props: ButtonProps){
const {
variant = "solid",
disabled
} = props;
return (
<Button
{...props}
className={clsx(
props.className,
//Background
{
"bg-transparent": variant === "outline" || variant === "icon",
"bg-orange-600 hover:bg-orange-700 active:bg-orange-800": (variant === "solid") && (!disabled),
"bg-orange-400/80": (variant === "solid") && (disabled),
"bg-transparent hover:bg-orange-600 active:bg-orange-700": (variant === "ghost" || variant === "outline-ghost") && (!disabled),
"bg-transparent ": (variant === "ghost" || variant === "outline-ghost") && (disabled)
},
//Text
{
"text-white": variant === "solid",
"text-orange-600 hover:text-orange-700 active:text-orange-800": (variant === "outline" || variant === "icon") && (!disabled),
"text-orange-400/80": (variant === "outline" || variant === "icon") && (disabled),
"text-orange-600 hover:text-white active:text-white": (variant === "ghost" || variant === "outline-ghost") && (!disabled),
"text-orange-400/80 ": (variant === "ghost" || variant === "outline-ghost") && (disabled)
},
//Outline
{
"outline-none": variant === "ghost" || variant === "icon",
"outline outline-orange-600 hover:outline-orange-700 active:outline-orange-800": (variant === "solid" || variant === "outline") && (!disabled),
"outline outline-orange-400/80": (variant === "solid" || variant === "outline") && (disabled),
"outline hover:outline-orange-600 active:outline-orange-700": (variant === "outline-ghost") && (!disabled),
"outline outline-orange-400/80 ": (variant === "outline-ghost") && (disabled)
}
)}
>
{props.children}
</Button>
);
}

View File

@@ -0,0 +1,46 @@
import clsx from "clsx";
import Button, { ButtonProps } from "./Button";
export default function PrimaryButton(props: ButtonProps){
const {
variant = "solid",
disabled
} = props;
return (
<Button
{...props}
className={clsx(
props.className,
//Background
{
"bg-transparent": variant === "outline" || variant === "icon",
"bg-blue-600 hover:bg-blue-700 active:bg-blue-800": (variant === "solid") && (!disabled),
"bg-blue-400/80": (variant === "solid") && (disabled),
"bg-transparent hover:bg-blue-600 active:bg-blue-700": (variant === "ghost" || variant === "outline-ghost") && (!disabled),
"bg-transparent ": (variant === "ghost" || variant === "outline-ghost") && (disabled)
},
//Text
{
"text-white": variant === "solid",
"text-blue-600 hover:text-blue-700 active:text-blue-800": (variant === "outline" || variant === "icon") && (!disabled),
"text-blue-400/80": (variant === "outline" || variant === "icon") && (disabled),
"text-blue-600 hover:text-white active:text-white": (variant === "ghost" || variant === "outline-ghost") && (!disabled),
"text-blue-400/80 ": (variant === "ghost" || variant === "outline-ghost") && (disabled)
},
//Outline
{
"outline-none": variant === "ghost" || variant === "icon",
"outline outline-blue-600 hover:outline-blue-700 active:outline-blue-800": (variant === "solid" || variant === "outline") && (!disabled),
"outline outline-blue-400/80": (variant === "solid" || variant === "outline") && (disabled),
"outline hover:outline-blue-600 active:outline-blue-700": (variant === "outline-ghost") && (!disabled),
"outline outline-blue-400/80 ": (variant === "outline-ghost") && (disabled)
}
)}
>
{props.children}
</Button>
);
}

View File

@@ -0,0 +1,46 @@
import clsx from "clsx";
import Button, { ButtonProps } from "./Button";
export default function SecondaryButton(props: ButtonProps){
const {
variant = "solid",
disabled
} = props;
return (
<Button
{...props}
className={clsx(
props.className,
//Background
{
"bg-transparent": variant === "outline" || variant === "icon",
"bg-neutral-500 hover:bg-neutral-600 active:bg-neutral-700": (variant === "solid") && (!disabled),
"bg-neutral-700": (variant === "solid") && (disabled),
"bg-transparent hover:bg-neutral-500 active:bg-neutral-600": (variant === "ghost" || variant === "outline-ghost") && (!disabled),
"bg-transparent ": (variant === "ghost" || variant === "outline-ghost") && (disabled)
},
//Text
{
"text-white": variant === "solid",
"text-neutral-500 hover:text-neutral-600 active:text-neutral-700": (variant === "outline" || variant === "icon") && (!disabled),
"text-neutral-700": (variant === "outline" || variant === "icon") && (disabled),
"text-neutral-500 hover:text-white active:text-white": (variant === "ghost" || variant === "outline-ghost") && (!disabled),
"text-neutral-700 ": (variant === "ghost" || variant === "outline-ghost") && (disabled)
},
//Outline
{
"outline-none": variant === "ghost" || variant === "icon",
"outline outline-neutral-500 hover:outline-neutral-600 active:outline-neutral-700": (variant === "solid" || variant === "outline") && (!disabled),
"outline outline-neutral-700": (variant === "solid" || variant === "outline") && (disabled),
"outline hover:outline-neutral-500 active:outline-neutral-600": (variant === "outline-ghost") && (!disabled),
"outline outline-neutral-700 ": (variant === "outline-ghost") && (disabled)
}
)}
>
{props.children}
</Button>
);
}

View File

@@ -0,0 +1,46 @@
import clsx from "clsx";
import Button, { ButtonProps } from "./Button";
export default function SuccessButton(props: ButtonProps){
const {
variant = "solid",
disabled
} = props;
return (
<Button
{...props}
className={clsx(
props.className,
//Background
{
"bg-transparent": variant === "outline" || variant === "icon",
"bg-green-600 hover:bg-green-700 active:bg-green-800": (variant === "solid") && (!disabled),
"bg-green-300/80": (variant === "solid") && (disabled),
"bg-transparent hover:bg-green-600 active:bg-green-700": (variant === "ghost" || variant === "outline-ghost") && (!disabled),
"bg-transparent ": (variant === "ghost" || variant === "outline-ghost") && (disabled)
},
//Text
{
"text-white": variant === "solid",
"text-green-600 hover:text-green-700 active:text-green-800": (variant === "outline" || variant === "icon") && (!disabled),
"text-green-300/80": (variant === "outline" || variant === "icon") && (disabled),
"text-green-600 hover:text-white active:text-white": (variant === "ghost" || variant === "outline-ghost") && (!disabled),
"text-green-300/80 ": (variant === "ghost" || variant === "outline-ghost") && (disabled)
},
//Outline
{
"outline-none": variant === "ghost" || variant === "icon",
"outline outline-green-600 hover:outline-green-700 active:outline-green-800": (variant === "solid" || variant === "outline") && (!disabled),
"outline outline-green-300/80": (variant === "solid" || variant === "outline") && (disabled),
"outline hover:outline-green-600 active:outline-green-700": (variant === "outline-ghost") && (!disabled),
"outline outline-green-300/80 ": (variant === "outline-ghost") && (disabled)
}
)}
>
{props.children}
</Button>
);
}

View File

@@ -0,0 +1,46 @@
import clsx from "clsx";
import Button, { ButtonProps } from "./Button";
export default function TertiaryButton(props: ButtonProps){
const {
variant = "solid",
disabled
} = props;
return (
<Button
{...props}
className={clsx(
props.className,
//Background
{
"bg-transparent": variant === "outline" || variant === "icon",
"bg-purple-600 hover:bg-purple-700 active:bg-purple-800": (variant === "solid") && (!disabled),
"bg-purple-400/80": (variant === "solid") && (disabled),
"bg-transparent hover:bg-purple-600 active:bg-purple-700": (variant === "ghost" || variant === "outline-ghost") && (!disabled),
"bg-transparent ": (variant === "ghost" || variant === "outline-ghost") && (disabled)
},
//Text
{
"text-white": variant === "solid",
"text-purple-600 hover:text-purple-700 active:text-purple-800": (variant === "outline" || variant === "icon") && (!disabled),
"text-purple-400/80": (variant === "outline" || variant === "icon") && (disabled),
"text-purple-600 hover:text-white active:text-white": (variant === "ghost" || variant === "outline-ghost") && (!disabled),
"text-purple-400/80 ": (variant === "ghost" || variant === "outline-ghost") && (disabled)
},
//Outline
{
"outline-none": variant === "ghost" || variant === "icon",
"outline outline-purple-600 hover:outline-purple-700 active:outline-purple-800": (variant === "solid" || variant === "outline") && (!disabled),
"outline outline-purple-400/80": (variant === "solid" || variant === "outline") && (disabled),
"outline hover:outline-purple-600 active:outline-purple-700": (variant === "outline-ghost") && (!disabled),
"outline outline-purple-400/80 ": (variant === "outline-ghost") && (disabled)
}
)}
>
{props.children}
</Button>
);
}

View File

@@ -0,0 +1,46 @@
import clsx from "clsx";
import Button, { ButtonProps } from "./Button";
export default function WarningButton(props: ButtonProps){
const {
variant = "solid",
disabled
} = props;
return (
<Button
{...props}
className={clsx(
props.className,
//Background
{
"bg-transparent": variant === "outline" || variant === "icon",
"bg-yellow-400 hover:bg-yellow-500 active:bg-yellow-600": (variant === "solid") && (!disabled),
"bg-yellow-600/80": (variant === "solid") && (disabled),
"bg-transparent hover:bg-yellow-400 active:bg-yellow-500": (variant === "ghost" || variant === "outline-ghost") && (!disabled),
"bg-transparent ": (variant === "ghost" || variant === "outline-ghost") && (disabled)
},
//Text
{
"text-black": variant === "solid",
"text-yellow-400 hover:text-yellow-500 active:text-yellow-600": (variant === "outline" || variant === "icon") && (!disabled),
"text-yellow-600/80": (variant === "outline" || variant === "icon") && (disabled),
"text-yellow-400 hover:text-black active:text-black": (variant === "ghost" || variant === "outline-ghost") && (!disabled),
"text-yellow-600/80 ": (variant === "ghost" || variant === "outline-ghost") && (disabled)
},
//Outline
{
"outline-none": variant === "ghost" || variant === "icon",
"outline outline-yellow-400 hover:outline-yellow-500 active:outline-yellow-600": (variant === "solid" || variant === "outline") && (!disabled),
"outline outline-yellow-600/80": (variant === "solid" || variant === "outline") && (disabled),
"outline hover:outline-yellow-400 active:outline-yellow-500": (variant === "outline-ghost") && (!disabled),
"outline outline-yellow-600/80 ": (variant === "outline-ghost") && (disabled)
}
)}
>
{props.children}
</Button>
);
}

View File

@@ -0,0 +1,68 @@
import { ClassGroup } from "@/interface/ClassGroup";
import SelectClassGroupModal from "@/ui/classGroup/modal/SelectClassGroupModal";
import { useEffect, useState } from "react";
export default function ClassGroupSelector({
raidGroupId,
selectedClassGroups,
onChange
}:{
raidGroupId: string;
selectedClassGroups: (ClassGroup | null)[];
onChange: (classGroups: (ClassGroup | null)[]) => void;
}){
const [ classGroups, setClassGroups ] = useState(selectedClassGroups);
const [ displaySelectClassGroupModal, setDisplaySelectClassGroupModal ] = useState(false);
const [ selectedCell, setSelectedCell ] = useState(0);
useEffect(() => {
setClassGroups(selectedClassGroups);
}, [selectedClassGroups]);
const updateClassGroups = (classGroup?: ClassGroup | null) => {
const newClassGroups = [...classGroups];
if(classGroup){
newClassGroups[selectedCell] = classGroup;
}
else{
newClassGroups[selectedCell] = null;
}
setClassGroups(newClassGroups);
onChange(newClassGroups);
}
return (
<>
<div
className="grid grid-cols-3 gap-4"
style={{flex: "0 0 33.333333333%"}}
>
{
classGroups.map((classGroup, index) => (
<div
key={`${index}`}
className="cursor-pointer border px-2 py-1"
onClick={() => {
setDisplaySelectClassGroupModal(true);
setSelectedCell(index);
}}
>
{classGroup?.classGroupName ?? "Any"}
</div>
))
}
</div>
<SelectClassGroupModal
display={displaySelectClassGroupModal}
close={() => setDisplaySelectClassGroupModal(false)}
selectedClassGroup={classGroups[selectedCell]}
onChange={updateClassGroups}
raidGroupId={raidGroupId}
/>
</>
);
}

View File

@@ -0,0 +1,42 @@
import { useGetClassGroupsByRaidLayout } from "@/hooks/ClassGroupHooks";
import DangerMessage from "../message/DangerMessage";
export default function ClassGroupsByRaidLayoutDisplay({
raidGroupId,
raidLayoutId
}:{
raidGroupId: string;
raidLayoutId: string;
}){
const classGroupsQuery = useGetClassGroupsByRaidLayout(raidGroupId, raidLayoutId);
if(classGroupsQuery.status === "pending"){
return (<div>Loading...</div>);
}
else if(classGroupsQuery.status === "error"){
return (<DangerMessage>Error: {classGroupsQuery.error.message}</DangerMessage>);
}
else{
return (
<div
className="flex flex-row items-center justify-center gap-x-4"
>
{
classGroupsQuery.data.map((classGroup, index) => (
<div
key={classGroup?.classGroupId ?? index}
>
{classGroup?.classGroupName ?? "Any"}
</div>
))
}
{
classGroupsQuery.data.length === 0 &&
<>&nbsp;</>
}
</div>
);
}
}

View File

@@ -0,0 +1,100 @@
import { useGetGames } from "@/hooks/GameHooks";
import { Game } from "@/interface/Game";
import clsx from "clsx";
import { useEffect, useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import TextInput from "../input/TextInput";
export default function GameSelector({
disabled,
game,
onChange
}:{
disabled: boolean;
game?: Game;
onChange: (game: Game | undefined) => void;
}){
const [ gameSearch, setGameSearch ] = useState(game?.gameName ?? "");
const [ searchTerm, setSearchTerm ] = useState(game?.gameName ?? "");
const [ searching, setSearching ] = useState(false);
const modalId = crypto.randomUUID().replaceAll("-", "");
const gameSearchQuery = useGetGames(0, 5, gameSearch);
const games = gameSearchQuery.data;
const setGame = (selectedGame?: Game) => {
setSearchTerm(selectedGame?.gameName ?? "");
setGameSearch(selectedGame?.gameName ?? "");
setSearching(false);
onChange?.(selectedGame);
}
const updateGameSearch = useDebouncedCallback((searchTerm: string) => {
setGameSearch(searchTerm);
}, 500);
useEffect(() => {
updateGameSearch(searchTerm);
}, [ searchTerm, updateGameSearch ]);
return (
<div
className="relative flex flex-col"
>
<TextInput
id={`gameSearchTextInput${modalId}`}
placeholder="Game Search"
onChange={(e) => { setSearchTerm(e.target.value); onChange?.(undefined); setSearching(true); }}
value={searchTerm}
disabled={disabled}
/>
<div
className="relative mx-4 z-10"
>
<div
className={clsx(
"absolute flex flex-col justify-center items-center w-full min-h-6 rounded-lg bg-neutral-700",
{
"hidden": !searching
}
)}
>
{
games && games.map((searchGame, index) => (
<div
key={searchGame.gameId}
className={clsx("w-full border-x-2 border-black dark:border-gray-400 cursor-pointer",
{
"rounded-t-lg border-t-2 border-b": index === 0,
"rounded-b-lg border-b-2 border-t": index === games.length - 1,
"border-y": index > 0 && index < games.length - 1,
}
)}
onClick={() => setGame(searchGame)}
>
<div
className="mx-4 py-2"
>
{searchGame.gameName}
</div>
</div>
))
}
{
games?.length == 0 &&
<div
className="mx-4 py-2"
>
No games found
</div>
}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,45 @@
import { useGetGameClassesByClassGroup } from "@/hooks/GameClassHooks";
import DangerMessage from "../message/DangerMessage";
export default function GameClassByClassGroupDisplay({
classGroupId
}:{
classGroupId: string;
}){
const gameClassesQuery = useGetGameClassesByClassGroup(classGroupId);
const displayId = crypto.randomUUID().replaceAll("-", "");
if(gameClassesQuery.status === "pending"){
return (<div>Loading...</div>);
}
else if(gameClassesQuery.status === "error"){
return (<DangerMessage>Error: {gameClassesQuery.error.message}</DangerMessage>);
}
else{
return (
<div
className="flex flex-row flex-wrap items-center justify-center gap-x-4"
>
{
gameClassesQuery.data.map((gameClass) => (
<div
key={`gameClassByClassGroupDisplay${classGroupId}${gameClass.gameClassId}${displayId}`}
className="flex flex-row items-center justify-center"
>
{
gameClass.gameClassIcon &&
<img
className="max-h-6 max-w-6 mr-2"
src={`${import.meta.env.VITE_ICON_URL}/gameClass/${gameClass.gameClassIcon}`}
/>
}
{gameClass.gameClassName}
</div>
))
}
</div>
);
}
}

View File

@@ -0,0 +1,32 @@
import { useGetGameClass } from "@/hooks/GameClassHooks";
import DangerMessage from "../message/DangerMessage";
export default function GameClassCellDisplay({
gameClassId
}:{
gameClassId: string;
}){
const gameClassQuery = useGetGameClass(gameClassId);
if(gameClassQuery.status === "pending"){
return <div>Loading...</div>;
}
else if(gameClassQuery.status === "error"){
return <DangerMessage>Error: {gameClassQuery.error.message}</DangerMessage>
}
else{
return (
<div
className="flex flex-row items-center justify-center"
>
<img
className="max-h-14 max-w-14 mr-2"
src={`${import.meta.env.VITE_ICON_URL}/gameClass/${gameClassQuery.data.gameClassIcon}`}
/>
<span>{gameClassQuery.data.gameClassName}</span>
</div>
);
}
}

View File

@@ -0,0 +1,73 @@
import { useGetGameClasses } from "@/hooks/GameClassHooks";
import { useEffect, useState } from "react";
import DangerMessage from "../message/DangerMessage";
export function GameClassSelector({
gameId,
gameClassId,
onChange
}:{
gameId: string;
gameClassId?: string;
onChange?: (gameClassId?: string) => void;
}){
const [ selectedGameClassId, setSelectedGameClassId ] = useState(gameClassId);
const selectorId = crypto.randomUUID().replaceAll("-", "");
const gameClassesQuery = useGetGameClasses(gameId, 0, 100, undefined);
useEffect(() => {
onChange?.(selectedGameClassId);
}, [ selectedGameClassId, onChange ]);
if(gameClassesQuery.status === "pending"){
return <div>Loading...</div>
}
else if(gameClassesQuery.status === "error"){
return <DangerMessage>Error loading Game Classes: {gameClassesQuery.error.message}</DangerMessage>
}
else{
return (
<div
className="grid grid-cols-3 gap-4"
style={{flex: "0 0 33.333333333%"}}
>
{
gameClassesQuery.data.map((gameClass) => (
<div
key={gameClass.gameClassId}
className="flex flex-row"
>
<input
id={`gameClassSelector${gameClass.gameClassId}${selectorId}`}
className="cursor-pointer"
type="radio"
name="gameClassId"
value={gameClass.gameClassId}
checked={selectedGameClassId === gameClass.gameClassId}
onChange={(e) => setSelectedGameClassId(e.target.value)}
/>
<label
className="ml-2 flex flex-row flex-nowrap justify-center items-center text-nowrap cursor-pointer"
htmlFor={`gameClassSelector${gameClass.gameClassId}${selectorId}`}
>
{
gameClass.gameClassIcon &&
<img
className="m-auto max-h-6 max-w-6 mr-2"
src={`${import.meta.env.VITE_ICON_URL}/gameClass/${gameClass.gameClassIcon}`}
/>
}
{gameClass.gameClassName}
</label>
</div>
))
}
</div>
);
}
}

View File

@@ -0,0 +1,83 @@
import { useGetGameClasses } from "@/hooks/GameClassHooks";
import { useEffect, useState } from "react";
import DangerMessage from "../message/DangerMessage";
export function GameClassesSelector({
gameId,
gameClassIds,
onChange
}:{
gameId: string;
gameClassIds?: string[];
onChange?: (gameClassIds: string[]) => void;
}){
const [ selectedGameClassIds, setSelectedGameClassIds ] = useState<string[]>(gameClassIds ?? []);
const selectorId = crypto.randomUUID().replaceAll("-", "");
const gameClassesQuery = useGetGameClasses(gameId, 0, 100, undefined);
const updateSelectedGameClassIds = (selectedGameClassId: string) => {
if(selectedGameClassIds.includes(selectedGameClassId)){
setSelectedGameClassIds(selectedGameClassIds.filter((id) => id !== selectedGameClassId));
}
else{
setSelectedGameClassIds([...selectedGameClassIds, selectedGameClassId]);
}
}
useEffect(() => {
onChange?.(selectedGameClassIds);
}, [ selectedGameClassIds, onChange ]);
if(gameClassesQuery.status === "pending"){
return <div>Loading...</div>
}
else if(gameClassesQuery.status === "error"){
return <DangerMessage>Error loading Game Classes: {gameClassesQuery.error.message}</DangerMessage>
}
else{
return (
<div
className="grid grid-cols-3 gap-4"
style={{flex: "0 0 33.333333333%"}}
>
{
gameClassesQuery.data.map((gameClass) => (
<div
key={gameClass.gameClassId}
className="flex flex-row"
>
<input
id={`gameClassSelector${gameClass.gameClassId}${selectorId}`}
className="cursor-pointer"
type="checkbox"
name="gameClassId"
value={gameClass.gameClassId}
checked={selectedGameClassIds.includes(gameClass.gameClassId ?? "")}
onChange={(e) => updateSelectedGameClassIds(e.target.value)}
/>
<label
className="ml-2 flex flex-row flex-nowrap justify-center items-center text-nowrap cursor-pointer"
htmlFor={`gameClassSelector${gameClass.gameClassId}${selectorId}`}
>
{
gameClass.gameClassIcon &&
<img
className="m-auto max-h-6 max-w-6 mr-2"
src={`${import.meta.env.VITE_ICON_URL}/gameClass/${gameClass.gameClassIcon}`}
/>
}
{gameClass.gameClassName}
</label>
</div>
))
}
</div>
);
}
}

View File

@@ -0,0 +1,47 @@
import clsx from "clsx";
import { ComponentProps } from "react";
interface DateInputProps extends ComponentProps<"input">{
id: string;
inputClasses?: string;
labelClasses?: string;
accepted?: boolean;
}
export default function DateInput(props: DateInputProps){
const { id, placeholder, inputClasses, labelClasses } = props;
return (
<div
className="bg-inherit px-4 pb-4 rounded-sm w-full md:flex md:justify-center"
>
<div
className="relative bg-inherit w-full"
>
<input
{...props}
id={id}
className={clsx(
"peer px-2 py-1 rounded-lg ring-2 ring-gray-500 focus:ring-sky-600 outline-hidden w-full",
inputClasses
)}
type="datetime-local"
/>
<label
htmlFor={id}
id={`${id}Label`}
className={clsx(
"absolute cursor-pointer left-0 -top-3 mx-1 px-1",
"bg-white dark:bg-neutral-825 text-sm peer-focus:text-sky-600",
labelClasses
)}
>
{placeholder}
</label>
</div>
</div>
);
}

View File

@@ -0,0 +1,49 @@
import { BsCloudUpload } from "react-icons/bs";
export default function FileInput({
file,
setFile
}:{
file: File | null | undefined;
setFile: (input: File | null) => void;
}){
return (
<div
className="relative border-2 rounded-lg border-gray-500 h-24 mx-4 w-md z-0"
>
<div
className="absolute cursor-text left-0 -top-3 bg-white dark:bg-neutral-800 text-gray-500 mx-1 px-1"
>
Icon File
</div>
<input
type="file"
name="iconFile"
className="relative opacity-0 w-full h-full z-50 cursor-pointer"
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
/>
<div
className="absolute top-0 left-0 flex flex-col justify-center items-center w-full h-full"
>
<div
className="flex flex-row gap-2"
>
<BsCloudUpload
size={24}
/>
Drop files anywhere or click to select file
</div>
{
file && (
<p
className="text-green-600"
>
Name: {file.name}
</p>
)
}
</div>
</div>
);
}

View File

@@ -0,0 +1,37 @@
import FileInput from "./FileInput";
export default function IconInput({
file,
setFile,
addErrorMessage
}:{
file: File | null | undefined;
setFile: (input: File | null) => void;
addErrorMessage: (message: string) => void;
}){
const setIconFile = (inputFile: File | null) => {
if((inputFile) && (!inputFile.type.startsWith("image"))){
addErrorMessage("File is invalid image format: " + inputFile.type);
}
//Prevent files larger than 10MB form being uploaded
else if((inputFile) && (inputFile.size > 10485760)){
addErrorMessage("File is too large: " + inputFile.size + " bytes");
}
//Prevent empty files
else if((inputFile) && (inputFile.size <= 0)){
addErrorMessage("File is empty");
}
else{
setFile(inputFile);
}
}
return (
<FileInput
file={file}
setFile={setIconFile}
/>
);
}

View File

@@ -0,0 +1,88 @@
import clsx from "clsx";
import { useEffect, useState } from "react";
export default function NumberInput({
id,
name,
label,
defaultValue,
value,
min,
max,
onChange,
disabled
}:{
id: string;
name?: string;
accepted?: boolean;
label?: string;
defaultValue?: number;
value?: number;
min?: number;
max?: number;
onChange?: (value: number) => void;
disabled?: boolean;
}){
const [ inputValue, setInputValue ] = useState(value ?? 1);
const [ minValue, setMinValue ] = useState(min ?? Number.MIN_VALUE);
const [ maxValue, setMaxValue ] = useState(max ?? Number.MAX_VALUE);
const inputId = crypto.randomUUID().replaceAll("-", "");
useEffect(() => {
//TODO: Fix this warning. Use component library
// eslint-disable-next-line react-hooks/set-state-in-effect
setMinValue(min ?? Number.MIN_VALUE);
setMaxValue(max ?? Number.MAX_VALUE);
setInputValue(value ?? defaultValue ?? 0);
}, [ id, min, max, defaultValue, value ]);
const changeInput = (value: number) => {
if(value < minValue){
value = maxValue;
}
else if(value > maxValue){
value = minValue;
}
setInputValue(value);
onChange?.(value);
}
return (
<div
className="bg-inherit px-4 rounded-sm w-full md:flex md:justify-center"
>
<div
className="relative bg-inherit w-18"
>
<input
id={`${id}NumberInput${inputId}`}
type="number"
name={name}
className={clsx(
"peer px-2 py-1 w-16 rounded-lg ring-2 ring-gray-500 focus:ring-sky-600 outline-hidden",
)}
onChange={(e) => changeInput(parseInt(e.target.value || "1"))}
value={inputValue}
disabled={disabled}
/>
<label
htmlFor={`${id}NumberInput${inputId}`}
id={`${id}NumberInput${inputId}Label`}
className={clsx(
"absolute cursor-pointer left-0 -top-3 mx-1 px-1",
"bg-white dark:bg-neutral-800 text-sm",
"text-gray-500 peer-focus:text-sky-600"
)}
style={{transitionProperty: "top, font-size, line-height", transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)", transitionDuration: "150ms"}}
>
{label}
</label>
</div>
</div>
);
}

View File

@@ -0,0 +1,63 @@
import clsx from "clsx";
import { ComponentProps } from "react";
interface PasswordInputProps extends ComponentProps<"input">{
id: string;
inputClasses?: string;
labelClasses?: string;
accepted?: boolean;
}
export default function PasswordInput(props: PasswordInputProps){
const { id, placeholder, inputClasses, labelClasses, accepted } = props;
return (
<div
className="flex flex-row justify-center w-full rounded-sm bg-inherit"
>
<div
className="relative bg-inherit w-full"
>
<input
{...props}
type="password"
className={clsx(
"peer bg-transparent w-full min-w-72 px-2 py-1 rounded-lg",
"ring-2 focus:ring-sky-600 placeholder-transparent outline-hidden",
inputClasses,
{
"ring-gray-500": accepted === undefined,
"ring-red-600": accepted === false,
"ring-green-600": accepted === true
}
)}
/>
<label
htmlFor={id}
id={`${id}Label`}
className={clsx(
"absolute cursor-text left-0 -top-3 mx-1 px-1",
"bg-white dark:bg-neutral-825 text-sm",
"peer-placeholder-shown:text-base peer-placeholder-shown:text-gray-500 peer-placeholder-shown:top-1 peer-focus:-top-3 peer-focus:text-sky-600 peer-focus:text-sm",
labelClasses,
{
"text-gray-500": accepted === undefined,
"text-red-600": accepted === false,
"text-green-600": accepted === true
}
)}
style={{transitionProperty: "top, font-size, line-height",
transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
transitionDuration: "150ms"
}}
>
{placeholder}
</label>
</div>
</div>
);
}

View File

@@ -0,0 +1,64 @@
import clsx from "clsx";
import { ComponentProps } from "react";
export interface TextAreaProps extends ComponentProps<"textarea">{
id: string;
inputClasses?: string;
labelClasses?: string;
accepted?: boolean;
}
export default function TextArea(props: TextAreaProps){
const { id, placeholder, name, inputClasses, labelClasses, accepted } = props;
return (
<div
className="bg-inherit p-4 rounded-sm w-full md:flex md:justify-center"
>
<div
className="relative bg-inherit w-full"
>
<textarea
{...props}
id={id}
name={name}
className={clsx(
"peer bg-transparent w-full md:min-w-72 h-24 px-2 py-1 rounded-lg",
"ring-2 ring-gray-500 focus:ring-sky-600 placeholder-transparent outline-hidden",
inputClasses,
{
"ring-gray-500": accepted === undefined,
"ring-red-600": accepted === false,
"ring-green-600": accepted === true
}
)}
style={{resize: "none"}}
/>
<label
htmlFor={id}
id={`${id}Label`}
className={clsx(
"absolute cursor-text left-0 -top-3 mx-1 px-1",
"bg-white dark:bg-neutral-825 text-sm",
"peer-placeholder-shown:text-base peer-placeholder-shown:text-gray-500 peer-placeholder-shown:top-1 peer-focus:-top-3 peer-focus:text-sky-600 peer-focus:text-sm",
labelClasses,
{
"text-gray-500": accepted === undefined,
"text-red-600": accepted === false,
"text-green-600": accepted === true
}
)}
style={{
transitionProperty: "top, font-size, line-height",
transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
transitionDuration: "150ms"
}}
>
{placeholder}
</label>
</div>
</div>
);
}

View File

@@ -0,0 +1,65 @@
import clsx from "clsx";
import { ComponentProps } from "react";
interface TextInputProps extends ComponentProps<"input">{
id: string;
inputClasses?: string;
labelClasses?: string;
accepted?: boolean;
}
export default function TextInput(props: TextInputProps){
const { id, placeholder, name, inputClasses, labelClasses, accepted } = props;
return (
<div
className="flex flex-row justify-center w-full rounded-sm bg-inherit"
>
<div
className="relative bg-inherit w-full"
>
<input
{...props}
type="text"
className={clsx(
"peer bg-transparent w-full min-w-72 px-2 py-1 rounded-lg",
"ring-2 focus:ring-sky-600 placeholder-transparent outline-hidden",
inputClasses,
{
"ring-gray-500": accepted === undefined,
"ring-red-600": accepted === false,
"ring-green-600": accepted === true
}
)}
name={name}
/>
<label
htmlFor={id}
id={`${id}Label`}
className={clsx(
"absolute cursor-text left-0 -top-3 mx-1 px-1",
"bg-white dark:bg-neutral-825 text-sm",
"peer-placeholder-shown:text-base peer-placeholder-shown:text-gray-500 peer-placeholder-shown:top-1 peer-focus:-top-3 peer-focus:text-sky-600 peer-focus:text-sm",
labelClasses,
{
"text-gray-500": accepted === undefined,
"text-red-600": accepted === false,
"text-green-600": accepted === true
}
)}
style={{
transitionProperty: "top, font-size, line-height",
transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
transitionDuration: "150ms"
}}
>
{placeholder}
</label>
</div>
</div>
);
}

View File

@@ -0,0 +1,16 @@
import clsx from "clsx";
import { HTMLProps } from "react";
import Message from "./Message";
export default function DangerMessage(props: HTMLProps<HTMLDivElement>){
return (
<Message
{...props}
className={clsx(
"bg-red-100 text-red-500",
props.className
)}
/>
);
}

View File

@@ -0,0 +1,16 @@
import clsx from "clsx";
import { HTMLProps } from "react";
import Message from "./Message";
export default function DarkMessage(props: HTMLProps<HTMLDivElement>){
return (
<Message
{...props}
className={clsx(
"bg-black text-white",
props.className
)}
/>
);
}

View File

@@ -0,0 +1,16 @@
import clsx from "clsx";
import { HTMLProps } from "react";
import Message from "./Message";
export default function InfoMessage(props: HTMLProps<HTMLDivElement>){
return (
<Message
{...props}
className={clsx(
"bg-cyan-100 text-sky-500",
props.className
)}
/>
);
}

View File

@@ -0,0 +1,16 @@
import clsx from "clsx";
import { HTMLProps } from "react";
import Message from "./Message";
export default function LightMessage(props: HTMLProps<HTMLDivElement>){
return (
<Message
{...props}
className={clsx(
"bg-white text-black",
props.className
)}
/>
);
}

View File

@@ -0,0 +1,15 @@
import clsx from "clsx";
import { HTMLProps } from "react";
export default function Message(props: HTMLProps<HTMLDivElement>){
return (
<div
{...props}
className={clsx(
"px-2 py-1 outline rounded-lg",
props.className
)}
/>
);
}

View File

@@ -0,0 +1,16 @@
import clsx from "clsx";
import { HTMLProps } from "react";
import Message from "./Message";
export default function MoltenMessage(props: HTMLProps<HTMLDivElement>){
return (
<Message
{...props}
className={clsx(
"bg-orange-100 text-orange-500",
props.className
)}
/>
);
}

View File

@@ -0,0 +1,16 @@
import clsx from "clsx";
import { HTMLProps } from "react";
import Message from "./Message";
export default function PrimaryMessage(props: HTMLProps<HTMLDivElement>){
return (
<Message
{...props}
className={clsx(
"bg-blue-200 text-blue-500",
props.className
)}
/>
);
}

View File

@@ -0,0 +1,16 @@
import clsx from "clsx";
import { HTMLProps } from "react";
import Message from "./Message";
export default function SecondaryMessage(props: HTMLProps<HTMLDivElement>){
return (
<Message
{...props}
className={clsx(
"bg-neutral-200 text-neutral-600",
props.className
)}
/>
);
}

View File

@@ -0,0 +1,16 @@
import clsx from "clsx";
import { HTMLProps } from "react";
import Message from "./Message";
export default function SuccessMessage(props: HTMLProps<HTMLDivElement>){
return (
<Message
{...props}
className={clsx(
"bg-green-200 text-green-600",
props.className
)}
/>
);
}

View File

@@ -0,0 +1,16 @@
import clsx from "clsx";
import { HTMLProps } from "react";
import Message from "./Message";
export default function TertiaryMessage(props: HTMLProps<HTMLDivElement>){
return (
<Message
{...props}
className={clsx(
"bg-purple-200 text-purple-500",
props.className
)}
/>
);
}

View File

@@ -0,0 +1,16 @@
import clsx from "clsx";
import { HTMLProps } from "react";
import Message from "./Message";
export default function WarningMessage(props: HTMLProps<HTMLDivElement>){
return (
<Message
{...props}
className={clsx(
"bg-yellow-100 text-yellow-600",
props.className
)}
/>
);
}

View File

@@ -0,0 +1,54 @@
import { ModalProps } from "@/interface/ModalInterfaces";
import clsx from "clsx";
import ModalBackground from "./ModalBackground";
export default function Modal(props: ModalProps){
const {
display,
backgroundType = "blur",
backgroundClassName,
top = false,
close,
className,
children
} = props;
const divProps = {...props};
delete divProps["children"];
delete divProps["display"];
delete divProps["backgroundType"];
delete divProps["backgroundClassName"];
delete divProps["close"];
delete divProps["className"];
delete divProps["top"];
return (
<>
{
display &&
<>
<ModalBackground
backgroundType={backgroundType}
className={backgroundClassName}
close={close}
/>
<div
{...divProps}
className={clsx(
"fixed left-1/2 -translate-x-1/2 max-w-(--breakpoint-sm) z-50",
{
"top-1/2 -translate-y-1/2": !top,
"top-0": top
},
"flex flex-col rounded-lg max-h-full shadow-lg shadow-[#00000066]",
className
)}
>
{children}
</div>
</>
}
</>
);
}

View File

@@ -0,0 +1,43 @@
import { ModalBackgroundProps } from "@/interface/ModalInterfaces";
import clsx from "clsx";
export default function ModalBackground(props: ModalBackgroundProps){
const {
backgroundType = "blur",
close,
className
} = props;
const divProps = { ...props };
delete divProps["backgroundType"];
delete divProps["close"];
delete divProps["className"];
if(backgroundType === "none"){
return (<></>);
}
return (
<div
{...divProps}
className={clsx(
"fixed left-0 top-0 w-full h-full z-40",
"flex flex-row justify-center items-center",
className,
{
"bg-[#00000044]": backgroundType === "darken",
"bg-[#FFFFFF44]": backgroundType === "lighten",
"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"
}
)}
onClick={close}
/>
);
}

View File

@@ -0,0 +1,23 @@
import clsx from "clsx";
import { HTMLProps } from "react";
export default function ModalBody(props: HTMLProps<HTMLDivElement>){
const {
className,
children
} = props;
return (
<div
{...props}
className={clsx(
"flex flex-col items-center px-8 py-4 overflow-auto",
className
)}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,27 @@
import clsx from "clsx";
import { HTMLProps } from "react";
export default function ModalFooter(props: HTMLProps<HTMLDivElement>){
const {
className,
children
} = props;
return (
<div
{...props}
className={clsx(
"flex flex-row justify-center w-full rounded-b-lg",
className
)}
>
<div
className="flex flex-row justify-center items-center w-full mx-8 my-3"
>
{children}
</div>
</div>
);
}

View File

@@ -0,0 +1,51 @@
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){
const {
close,
className,
children
} = props;
const divProps = {...props};
delete divProps["close"];
delete divProps["className"];
delete divProps["children"];
return (
<div
{...divProps}
className={clsx(
"flex flex-row justify-center w-full rounded-t-lg",
className
)}
>
<div
className="flex flex-row justify-center mx-8 my-3"
>
{children}
</div>
{
close &&
<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}
/>
</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 w-full"
>
{modalFooter}
</div>
</ModalFooter>
</Modal>
);
}

View File

@@ -0,0 +1,32 @@
import { useTheme } from "@/providers/ThemeProvider";
import { BsLightbulb, BsLightbulbFill } from "react-icons/bs";
export default function DarkModeToggle(){
const { theme, setTheme } = useTheme();
return (
<div>
<input
id="darkModeCheckbox"
type="checkbox"
className="peer hidden"
onChange={() => setTheme(theme === "dark" ? "light" : "dark")}
defaultChecked={theme === "dark"}
/>
<label
htmlFor="darkModeCheckbox"
className="block peer-checked:hidden"
>
<BsLightbulbFill/>
</label>
<label
htmlFor="darkModeCheckbox"
className="hidden peer-checked:block"
>
<BsLightbulb/>
</label>
</div>
);
}

View File

@@ -0,0 +1,59 @@
import raidBuilderIcon from "@/assets/raidBuilderIcon.svg";
import clsx from "clsx";
import { BsList } from "react-icons/bs";
import { Link, Outlet } from "react-router";
import DarkModeToggle from "./DarkModeToggle";
import ProtectedNavLinks from "./ProtectedNavLinks";
import PublicNavLinks from "./PublicNavLinks";
export default function NavBar(){
return (
<>
<nav
className={clsx(
"border-b-2 z-40",
"bg-gray-700 border-gray-600 dark:bg-zinc-900 dark:border-neutral-850 text-white"
)}
>
<div
className="navContents"
>
<Link
to="/"
className="flex items-center space-x-3 rtl:space-x-reverse"
>
<img
src={raidBuilderIcon}
alt="Raid Builder Logo"
width={30}
height={30}
fetchPriority="high"
/>
<span
className="self-center text-2xl font-semibold whitespace-nowrap"
>
Raid Builder
</span>
</Link>
<div
className="peer md:hidden text-3xl"
>
<BsList/>
</div>
<div
className={clsx(
"relative top-0 left-0 flex flex-row items-center rounded-lg space-x-4",
"bg-gray-700 dark:bg-zinc-900"
)}
>
<PublicNavLinks/>
<ProtectedNavLinks/>
<DarkModeToggle/>
</div>
</div>
</nav>
<Outlet/>
</>
);
}

View File

@@ -0,0 +1,62 @@
import { useAuth } from "@/providers/AuthProvider";
import { isSiteAdmin } from "@/util/PermissionUtil";
import { BsFillPersonFill } from "react-icons/bs";
import { NavLink } from "react-router";
export default function ProtectedNavLinks(){
const { jwt, accountPermissions } = useAuth();
if(!jwt){
return <></>;
}
const protectedLinks = [
{
name: "Game",
path: "/game"
},
{
name: "Raid Group",
path: "/raidGroup"
}
];
if(isSiteAdmin(accountPermissions)){
protectedLinks.push({
name: "Admin",
path: "/admin"
});
}
protectedLinks.push({
name: "Logout",
path: "/logout"
});
return (
<>
{
protectedLinks.map((link) => (
<NavLink
key={link.name}
to={link.path}
>
{link.name}
</NavLink>
))
}
{
jwt &&
<NavLink
to="/account"
>
<BsFillPersonFill
size={22}
/>
</NavLink>
}
</>
);
}

View File

@@ -0,0 +1,46 @@
import { useAuth } from "@/providers/AuthProvider";
import { NavLink } from "react-router";
const publicLinks = [
{
name: "Home",
path: "/"
}
];
export default function PublicNavLinks(){
const { jwt } = useAuth();
return (
<>
{
publicLinks.map((link) => (
<NavLink
key={link.name}
to={link.path}
>
{link.name}
</NavLink>
))
}
{
!jwt &&
<>
<NavLink
to="/login"
>
Login
</NavLink>
<NavLink
to="/signup"
>
Signup
</NavLink>
</>
}
</>
);
}

View File

@@ -0,0 +1,65 @@
import { generatePagination } from "@/util/PaginationUtil";
import { BsChevronLeft, BsChevronRight } from "react-icons/bs";
import PrimaryButton from "../button/PrimaryButton";
export default function Pagination({
currentPage,
totalPages,
onChange
}:{
currentPage: number;
totalPages: number;
onChange: (page: number) => void;
}){
const pages = generatePagination(currentPage, totalPages);
return(
<div
className="flex flex-row items-center justify-center w-full text-xl text-white"
>
<div
className="mr-8"
>
<PrimaryButton
shape="square"
className="w-9 h-9"
disabled={currentPage <= 1}
onClick={() => onChange(currentPage - 1)}
>
<BsChevronLeft/>
</PrimaryButton>
</div>
<div
className="flex flex-row items-center justify-center gap-4"
>
{
pages.map((page, index) => (
<PrimaryButton
key={index}
size="sm"
shape="square"
className="w-9 h-9"
disabled={(page === "...") || (page === currentPage)}
onClick={() => onChange(page as number)}
>
{page}
</PrimaryButton>
))
}
</div>
<div
className="ml-8"
>
<PrimaryButton
shape="square"
className="w-9 h-9"
disabled={currentPage >= totalPages}
onClick={() => onChange(currentPage + 1)}
>
<BsChevronRight/>
</PrimaryButton>
</div>
</div>
);
}

View File

@@ -0,0 +1,49 @@
import { useGetPersonCharactersByPersonId } from "@/hooks/PersonCharacterHooks";
import DangerMessage from "../message/DangerMessage";
export default function PersonCharacterDisplay({
personId,
raidGroupId
}:{
personId: string;
raidGroupId: string;
}){
const personCharacterQuery = useGetPersonCharactersByPersonId(personId, raidGroupId);
if(personCharacterQuery.status === "pending"){
return (<div>Loading...</div>);
}
else if(personCharacterQuery.status === "error"){
return (<DangerMessage>Error loading characters: {personCharacterQuery.error.message}</DangerMessage>);
}
else{
return (
<div
className="flex flex-row flex-wrap items-center justify-center"
>
{
personCharacterQuery.data.map((personCharacter) => {
return (
<div
key={personCharacter.personCharacterId}
className="flex flex-row flex-nowrap items-center justify-center mr-8"
>
<img
className="m-auto max-h-6 max-w-6 mr-2"
src={`${import.meta.env.VITE_ICON_URL}/gameClass/id/${personCharacter.gameClassId}`}
/>
{personCharacter.characterName}
</div>
);
})
}
{
personCharacterQuery.data.length === 0 &&
<div>No characters</div>
}
</div>
);
}
}

View File

@@ -0,0 +1,76 @@
import { PersonCharacter } from "@/interface/PersonCharacter";
import { useEffect, useState } from "react";
export default function PersonCharacterSelector({
personCharacters,
selectedCharacterId,
onChange
}:{
personCharacters: PersonCharacter[];
selectedCharacterId?: string;
onChange?: (characterId: string | undefined) => void;
}){
const [ currentlySelectedCharacterId, setCurrentlySelectedCharacterId ] = useState(selectedCharacterId);
const selectorId = crypto.randomUUID().replaceAll("-", "");
useEffect(() => {
setCurrentlySelectedCharacterId(selectedCharacterId);
}, [ selectedCharacterId ]);
const updateInput = (newCharacterId?: string) => {
if(newCharacterId === currentlySelectedCharacterId){
setCurrentlySelectedCharacterId(undefined);
onChange?.(undefined);
}
else{
setCurrentlySelectedCharacterId(newCharacterId);
onChange?.(newCharacterId);
}
}
return (
<div
className="grid grid-cols-3 grid-rows-3 gap-x-8 gap-y-4"
style={{flex: "0 0 33.333333333%"}}
>
{
personCharacters.map((ch) => (
<div
key={ch.personCharacterId}
className="flex flex-row flex-nowrap"
>
<input
type="radio"
id={`personCharacter${ch.personCharacterId}Selector${selectorId}`}
name="character"
value={ch.personCharacterId}
checked={ch.personCharacterId === currentlySelectedCharacterId}
onChange={() => {}}
onClick={() => updateInput(ch.personCharacterId)}
/>
<label
className="ml-2"
htmlFor={`personCharacter${ch.personCharacterId}Selector${selectorId}`}
>
<div
className="flex flex-row flex-nowrap text-nowrap"
>
{
<img
className="mr-2 max-h-8 max-w-8"
src={`${import.meta.env.VITE_ICON_URL}/gameClass/id/${ch.gameClassId}`}
/>
}
{ch.characterName}
</div>
</label>
</div>
))
}
</div>
);
}

View File

@@ -0,0 +1,61 @@
import { useEffect, useState } from "react";
import NumberInput from "../input/NumberInput";
export default function RatingSelector({
rating,
onChange
}:{
rating?: number;
onChange?: (rating?: number) => void;
}){
const ratings = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const selectorId = crypto.randomUUID().replaceAll("-", "");
const [ currentRating, setCurrentRating ] = useState(rating);
useEffect(() => {
setCurrentRating(rating);
}, [ rating, setCurrentRating ]);
useEffect(() => {
onChange?.(currentRating);
}, [ currentRating, onChange ]);
return (
<div>
<NumberInput
id={`characterRatingSelector${selectorId}`}
label="Rating"
value={currentRating}
onChange={(value) => setCurrentRating(value)}
min={0}
max={10}
/>
</div>
);
return (
<div>
<label>
<select
onChange={(e) => setCurrentRating(parseInt(e.target.value))}
value={currentRating}
>
<option value={undefined}></option>
{
ratings.map((rating) => (
<option
key={rating}
value={rating}
>
{rating}
</option>
))
}
</select>
<span>Character Rating</span>
</label>
</div>
);
}

View File

@@ -0,0 +1,41 @@
import { RaidGroupPermissionType } from "@/interface/RaidGroup";
export default function RaidGroupPermissionSelector({
value,
onChange
}:{
value?: RaidGroupPermissionType;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}){
const modalId = crypto.randomUUID().replaceAll("-", "");
return (
<div
className="flex flex-row flex-wrap justify-start gap-x-4"
>
{
Object.keys(RaidGroupPermissionType).map((permissionType) => (
<label
key={permissionType}
className="whitespace-nowrap"
>
<input
type="radio"
name={`raidGroupPermissionTypeSelector${modalId}`}
value={permissionType}
onChange={onChange}
checked={value === permissionType as RaidGroupPermissionType}
/>
<span
className="ml-1"
>
{permissionType}
</span>
</label>
))
}
</div>
);
}

View File

@@ -0,0 +1,126 @@
import clsx from "clsx";
import { HTMLProps, useState } from "react";
export interface Tab {
tabId?: string;
tabHeader: React.ReactNode;
headerClasses?: string;
tabContent: React.ReactNode;
contentClasses?: string;
active?: boolean;
onTabClick?: () => void;
}
export interface TabGroupProps extends HTMLProps<HTMLDivElement>{
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
{...divProps}
className={clsx(
className,
"flex flex-col w-full"
)}
>
<div
className="flex flex-row items-center justify-start"
>
{
tabs.map((tab, index) => (
<TabHeader
id={tab.tabId}
key={index}
tab={tab}
active={activeTab === index}
onClick={() => { setActiveTab(index); tab.onTabClick?.(); }}
/>
))
}
<div
className="w-full h-full py-2 border-b border-(--text-color)"
>
&nbsp;
</div>
</div>
<div
className="flex flex-col items-center justify-center mt-8"
>
{
tabs.map((tab, index) => (
<TabContent
key={index}
tab={tab}
active={activeTab === index}
/>
))
}
</div>
</div>
);
}
function TabHeader({
id,
tab,
onClick,
active
}:{
id?: string;
tab: Tab;
onClick: () => void;
active: boolean;
}){
return (
<div
id={id}
className={clsx(
tab.headerClasses,
"px-4 py-2 rounded-t-lg cursor-pointer whitespace-nowrap",
"border-x border-t border-(--text-color)",
{
"border-b border-(--text-color)": !active
}
)}
onClick={onClick}
>
{tab.tabHeader}
</div>
);
}
function TabContent({
tab,
active
}:{
tab: Tab;
active: boolean;
}){
if(!active){
return null;
}
return (
<div
className={clsx(
"w-full",
tab.contentClasses
)}
>
{tab.tabContent}
</div>
);
}

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 h-14",
{
"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>
);
}

250
src/hooks/AccountHooks.ts Normal file
View File

@@ -0,0 +1,250 @@
import { Account } from "@/interface/Account";
import { AccountTutorialStatus } from "@/interface/AccountTutorialStatus";
import { Counter } from "@/interface/Counters";
import { RaidGroupPermissionType } from "@/interface/RaidGroup";
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("searchTerm", searchTerm ?? "");
}
const response = await api.get(`/account?${params}`);
return response.data as Account[];
}
});
}
export function useGetAccountsByRaidGroup(raidGroupId: string, page: number, pageSize: number, searchTerm?: string){
return useQuery({
queryKey: ["accounts", "raidGroup", raidGroupId, {page, pageSize, searchTerm}],
queryFn: async () => {
const params = new URLSearchParams();
params.append("page", page.toString());
params.append("pageSize", pageSize.toString());
if(searchTerm){
params.append("searchTerm", searchTerm ?? "");
}
const response = await api.get(`/account/raidGroup/${raidGroupId}?${params}`);
return response.data as Account[];
}
});
}
export function useGetRaidGroupPermissionsForAccount(raidGroupId?: string, accountId?: string){
return useQuery({
queryKey: ["accounts", "raidGroup", raidGroupId, "account", accountId],
queryFn: async () => {
const response = await api.get(`/account/${accountId}/raidGroup/${raidGroupId}/permission`);
return (response.data as {permission: RaidGroupPermissionType;}).permission;
},
enabled: !!raidGroupId && !!accountId
});
}
export function useGetAccountsCount(searchTerm?: string){
const searchParams = new URLSearchParams();
if(searchTerm){
searchParams.append("searchTerm", searchTerm);
}
return useQuery({
queryKey: [ "accounts", "count", searchTerm],
queryFn: async () => {
const response = await api.get(`/account/count?${searchParams}`);
return (response.data as Counter).count;
}
});
}
export function useGetAccountsByRaidGroupCount(raidGroupId: string, searchTerm?: string){
const searchParams = new URLSearchParams();
if(searchTerm){
searchParams.append("searchTerm", searchTerm);
}
return useQuery({
queryKey: [ "accounts", "raidGroup", raidGroupId, "count", searchTerm],
queryFn: async () => {
const response = await api.get(`/account/raidGroup/${raidGroupId}/count?${searchParams}`);
return (response.data as Counter).count;
}
});
}
export function useGetTutorialsStatus(accountId: string | null){
return useQuery({
queryKey: ["tutorials", "account", accountId],
queryFn: async () => {
const response = await api.get(`/account/tutorial`);
return response.data as AccountTutorialStatus;
},
enabled: !!accountId
});
}
export function useUpdateTutorialsStatus(){
return useMutation({
mutationKey: ["tutorials", "accounts"],
mutationFn: async (tutorials: AccountTutorialStatus) => {
await api.put(`/account/tutorial`, tutorials);
}
});
}
export function useUpdatePassword(){
return useMutation({
mutationKey: ["updatePassword"],
mutationFn: async ({currentPassword, newPassword}:{currentPassword: string; newPassword: string;}) => {
await api.post("/auth/resetPassword", {
currentPassword,
newPassword
});
}
});
}
export function useForcePasswordReset(accountId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["forcePasswordReset", accountId],
mutationFn: async () => {
await api.put(`/account/${accountId}/forcePasswordReset`);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["accounts"] });
}
});
}
export function useResetPassword(accountId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["resetPassword", accountId],
mutationFn: async (password: string) => {
await api.put(`/account/${accountId}/resetPassword`, {
password
});
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["accounts"] });
}
});
}
export function useRevokeRefreshToken(accountId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["revokeRefreshToken", accountId],
mutationFn: async () => {
await api.put(`/account/${accountId}/revokeRefreshToken`);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["accounts"] });
}
});
}
export function useCreateAccount(){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["createAccount"],
mutationFn: async (account: Account) => {
await api.post("/account", account);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["accounts"] });
}
});
}
export function useUpdateAccount(){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["updateAccount"],
mutationFn: async (account: Account) => {
await api.put(`/account/${account.accountId}`, account);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["accounts"] });
}
});
}
export function useUpdateRaidGroupPermissionsForAccount(raidGroupId?: string, accountId?: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["updateRaidGroupPermissionsForAccount", raidGroupId, accountId],
mutationFn: async (permission: RaidGroupPermissionType) => {
await api.put(`/account/${accountId}/raidGroup/${raidGroupId}/permission`, {
permission
});
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["accounts"] });
}
});
}
export function useDeleteAccount(accountId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["deleteAccount", accountId],
mutationFn: async () => {
await api.delete(`/account/${accountId}`);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["accounts"] });
}
});
}
export function useRemoveAccountFromRaidGroup(raidGroupId?: string, accountId?: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["removeAccountFromRaidGroup", raidGroupId, accountId],
mutationFn: async () => {
await api.delete(`/account/${accountId}/raidGroup/${raidGroupId}/permission`);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["accounts"] });
}
});
}

45
src/hooks/AuthHooks.ts Normal file
View File

@@ -0,0 +1,45 @@
import { Account } from "@/interface/Account";
import api from "@/util/AxiosUtil";
import { useMutation } from "@tanstack/react-query";
export function useSignup(){
return useMutation({
mutationKey: ["signup"],
mutationFn: async (account: Account) => {
await api.post("/auth/signup", account);
}
});
}
export function useConfirm(){
return useMutation({
mutationKey: ["confirm"],
mutationFn: async (confirmToken: string) => {
await api.post(`/auth/confirm/${confirmToken}`);
}
});
}
export function useForgotPassword(){
return useMutation({
mutationKey: ["forgotPassword"],
mutationFn: async (username: string) => {
const params = new URLSearchParams();
params.append("username", username);
await api.post(`/auth/forgot?${params}`);
}
});
}
export function useForgotResetPassword(forgotToken: string){
return useMutation({
mutationKey: ["forgotResetPassword"],
mutationFn: async (password: string) => {
await api.post(`/auth/forgot/${forgotToken}`, {password});
}
});
}

133
src/hooks/CalendarHooks.ts Normal file
View File

@@ -0,0 +1,133 @@
import { CalendarEvent } from "@/interface/Calendar";
import api from "@/util/AxiosUtil";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
export function useGetGameCalendar(gameId: string){
return useQuery({
queryKey: ["gameCalendar", gameId],
queryFn: async () => {
const response = await api.get(`/calendar/game/${gameId}`);
return response.data as CalendarEvent[];
}
});
}
export function useGetRaidGroupCalendar(raidGroupId: string){
return useQuery({
queryKey: ["raidGroupCalendar", raidGroupId],
queryFn: async () => {
const response = await api.get(`/calendar/raidGroup/${raidGroupId}`);
return response.data as CalendarEvent[];
}
});
}
export function useCreateGameCalendarEvent(gameId: string){
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (calendarEvent: CalendarEvent) => {
await api.post(`/calendar/game/${gameId}`, {...calendarEvent, gameCalendarEventId: calendarEvent.calendarEventId, calendarEventId: undefined});
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["gameCalendar"]})
}
});
}
export function useUpdateGameCalendarEvent(gameId: string){
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (calendarEvent: CalendarEvent) => {
await api.put(`/calendar/game/${gameId}`,
{
...calendarEvent,
gameCalendarEventId: calendarEvent.calendarEventId,
calendarEventId: undefined
});
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["gameCalendar"]})
}
});
}
export function useDeleteGameCalendarEvent(gameId: string){
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (calendarEvent: CalendarEvent) => {
await api.delete(`/calendar/game/${gameId}/${calendarEvent.calendarEventId}`);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["gameCalendar"]})
}
});
}
export function useCreateRaidGroupCalendarEvent(raidGroupId: string){
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (calendarEvent: CalendarEvent) => {
await api.post(`/calendar/raidGroup/${raidGroupId}`,
{
...calendarEvent,
raidGroupCalendarEventId: calendarEvent.calendarEventId,
calendarEventId: undefined
});
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["raidGroupCalendar"]})
}
});
}
export function useUpdateRaidGroupCalendarEvent(raidGroupId: string){
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (calendarEvent: CalendarEvent) => {
await api.put(`/calendar/raidGroup/${raidGroupId}`, {...calendarEvent, raidGroupCalendarEventId: calendarEvent.calendarEventId, calendarEventId: undefined});
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["raidGroupCalendar"]})
}
});
}
export function useDeleteRaidGroupCalendarEvent(raidGroupId: string){
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (calendarEvent: CalendarEvent) => {
await api.delete(`/calendar/raidGroup/${raidGroupId}/${calendarEvent.calendarEventId}`);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["raidGroupCalendar"]})
}
});
}
export function useGetRaidInstanceCalendarEvents(raidGroupId?: string){
return useQuery({
queryKey: ["raidInstanceCalendarEvents", raidGroupId],
queryFn: async () => {
const response = await api.get(`/calendar/raidGroup/${raidGroupId}/raidInstance`);
return response.data as CalendarEvent[];
},
enabled: !!raidGroupId && raidGroupId !== ""
});
}

View File

@@ -0,0 +1,112 @@
import { ClassGroup } from "@/interface/ClassGroup";
import { Counter } from "@/interface/Counters";
import api from "@/util/AxiosUtil";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
export function useGetClassGroups(raidGroupId: string, page: number, pageSize: number, searchTerm?: string){
return useQuery({
queryKey: ["classGroups", raidGroupId, { page, pageSize, searchTerm }],
queryFn: async () => {
const params = new URLSearchParams();
params.append("page", page.toString());
params.append("pageSize", pageSize.toString());
if(searchTerm){
params.append("searchTerm", searchTerm);
}
const response = await api.get(`/raidGroup/${raidGroupId}/classGroup?${params}`);
return response.data as ClassGroup[];
},
enabled: !!raidGroupId
});
}
export function useGetClassGroupsCount(raidGroupId: string, searchTerm?: string){
const searchParams = new URLSearchParams();
if(searchTerm){
searchParams.append("searchTerm", searchTerm);
}
return useQuery({
queryKey: ["classGroups", "count", searchTerm],
queryFn: async () => {
const response = await api.get(`/raidGroup/${raidGroupId}/classGroup/count?${searchParams}`);
return (response.data as Counter).count;
}
});
}
export function useGetClassGroupsByRaidLayout(raidGroupId: string, raidLayoutId: string | undefined){
return useQuery({
queryKey: ["classGroups", "raidLayout", raidLayoutId],
queryFn: async () => {
const response = await api.get(`/raidGroup/${raidGroupId}/classGroup/raidLayout/${raidLayoutId}`);
return response.data as (ClassGroup | null)[];
},
enabled: !!raidGroupId && !!raidLayoutId
})
}
export function useCreateClassGroup(raidGroupId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["createClassGroup"],
mutationFn: async ({classGroupName, gameClassIds}:{classGroupName: string; gameClassIds: string[];}) => {
await api.post(`/raidGroup/${raidGroupId}/classGroup`,
{
classGroup: {
classGroupName: classGroupName,
raidGroupId: raidGroupId
},
gameClassIds
});
},
onSuccess: () => {
void queryClient.invalidateQueries({queryKey: ["classGroups"]});
}
});
}
export function useUpdateClassGroup(raidGroupId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["updateClassGroup"],
mutationFn: async ({classGroup, gameClassIds}:{classGroup: ClassGroup; gameClassIds: string[];}) => {
await api.put(`/raidGroup/${raidGroupId}/classGroup/${classGroup.classGroupId}`,
{
classGroup,
gameClassIds
}
);
},
onSuccess: () => {
void queryClient.invalidateQueries({queryKey: ["gameClasses", "classGroups"]});
void queryClient.invalidateQueries({queryKey: ["classGroups"]});
}
});
}
export function useDeleteClassGroup(raidGroupId: string, classGroupId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["deleteClassGroup", classGroupId, raidGroupId],
mutationFn: async () => {
await api.delete(`/raidGroup/${raidGroupId}/classGroup/${classGroupId}`);
},
onSuccess: () => {
void queryClient.invalidateQueries({queryKey: ["classGroups"]});
}
});
}

131
src/hooks/GameClassHooks.ts Normal file
View File

@@ -0,0 +1,131 @@
import { Counter } from "@/interface/Counters";
import { GameClass } from "@/interface/GameClass";
import api from "@/util/AxiosUtil";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
export function useGetGameClass(gameClassId: string){
return useQuery({
queryKey: ["gameClasses", gameClassId],
queryFn: async () => {
const response = await api.get(`/gameClass/${gameClassId}`);
return response.data as GameClass;
},
enabled: !!gameClassId
})
}
export function useGetGameClasses(gameId: string, page: number, pageSize: number, searchTerm?: string){
return useQuery({
queryKey: ["gameClasses", gameId, { page, pageSize, searchTerm }],
queryFn: async () => {
const params = new URLSearchParams();
params.append("page", page.toString());
params.append("pageSize", pageSize.toString());
if(searchTerm){
params.append("searchTerm", searchTerm);
}
const response = await api.get(`/gameClass/game/${gameId}?${params}`);
return response.data as GameClass[];
}
});
}
export function useGetGameClassesByClassGroup(classGroupId?: string){
return useQuery({
queryKey: ["gameClasses", "classGroups", classGroupId],
queryFn: async () => {
const response = await api.get(`/gameClass/classGroup/${classGroupId}`);
return response.data as GameClass[];
},
enabled: !!classGroupId
});
}
export function useGetGameClassesCount(gameId: string, searchTerm?: string){
const searchParams = new URLSearchParams();
if(searchTerm){
searchParams.append("searchTerm", searchTerm);
}
return useQuery({
queryKey: ["gameClasses", gameId, "count", searchTerm],
queryFn: async () => {
const response = await api.get(`/gameClass/game/${gameId}/count?${searchParams}`);
return (response.data as Counter).count;
}
});
}
export function useCreateGameClass(){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["createGameClass"],
mutationFn: async ({gameId, gameClassName, iconFile}:{gameId: string; gameClassName: string; iconFile: File | null}) => {
const formData = new FormData();
if(iconFile){
formData.append("iconFile", iconFile);
}
formData.append("gameClassName", gameClassName);
formData.append("gameId", gameId);
await api.post(
`/gameClass/game/${gameId}`,
formData
);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["gameClasses"] });
}
});
}
export function useUpdateGameClass(){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["updateGameClass"],
mutationFn: async ({gameClass, iconFile}:{gameClass: GameClass; iconFile: File | null}) => {
const formData = new FormData();
if(iconFile){
formData.append("iconFile", iconFile);
}
formData.append("gameClassName", gameClass.gameClassName);
formData.append("gameId", gameClass.gameId);
if(gameClass.gameClassIcon){
formData.append("gameClassIcon", gameClass.gameClassIcon);
}
await api.put(`/gameClass/${gameClass.gameClassId}/game/${gameClass.gameId}`, formData);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["gameClasses"] });
}
});
}
export function useDeleteGameClass(){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["deleteGameClass"],
mutationFn: async (gameClass: GameClass) => {
await api.delete(`/gameClass/${gameClass.gameClassId}/game/${gameClass.gameId}`);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["gameClasses"] });
}
});
}

115
src/hooks/GameHooks.ts Normal file
View File

@@ -0,0 +1,115 @@
import { Counter } from "@/interface/Counters";
import { Game } from "@/interface/Game";
import api from "@/util/AxiosUtil";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
export function useGetGame(gameId: string, disabled: boolean){
return useQuery({
queryKey: ["games", gameId],
queryFn: async () => {
const response = await api.get(`/game/${gameId}`);
return (response.data as Game)?.gameId ? response.data as Game : undefined;
},
enabled: !disabled
});
}
export function useGetGames(page: number, pageSize: number, searchTerm?: string){
return useQuery({
queryKey: ["games", { page, pageSize, searchTerm }],
queryFn: async () => {
const params = new URLSearchParams();
params.append("page", page.toString());
params.append("pageSize", pageSize.toString());
if(searchTerm){
params.append("searchTerm", searchTerm);
}
const response = await api.get(`/game?${params}`);
return response.data as Game[];
}
});
}
export function useGetGamesCount(searchTerm?: string){
const searchParams = new URLSearchParams();
if(searchTerm){
searchParams.append("searchTerm", searchTerm);
}
return useQuery({
queryKey: ["games", "count", searchTerm],
queryFn: async () => {
const response = await api.get(`/game/count?${searchParams}`);
return (response.data as Counter).count;
}
});
}
export function useCreateGame(){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["createGame"],
mutationFn: async ({gameName, iconFile}:{gameName: string; iconFile: File | null;}) => {
const formData = new FormData();
if(iconFile){
formData.append("iconFile", iconFile);
}
formData.append("gameName", gameName);
await api.post(
"/game",
formData
);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["games"] });
}
});
}
export function useUpdateGame(){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["updateGame"],
mutationFn: async ({game, iconFile}:{game: Game, iconFile: File | null}) => {
const formData = new FormData();
if(iconFile){
formData.append("iconFile", iconFile);
}
formData.append("gameName", game.gameName);
if(game.gameIcon){
formData.append("gameIcon", game.gameIcon);
}
await api.put(`/game/${game.gameId}`, formData);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["games"] });
}
});
}
export function useDeleteGame(){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["deleteGame"],
mutationFn: async (gameId: string) => {
await api.delete(`/game/${gameId}`);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["games"] });
}
});
}

View File

@@ -0,0 +1,108 @@
import { Counter } from "@/interface/Counters";
import { PersonCharacter } from "@/interface/PersonCharacter";
import api from "@/util/AxiosUtil";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
export function useGetPersonCharactersByPersonId(personId: string, raidGroupId: string){
return useQuery({
queryKey: ["personCharacters", personId],
queryFn: async () => {
const response = await api.get(`/raidGroup/${raidGroupId}/person/${personId}/character`);
return response.data as PersonCharacter[];
}
});
}
export function useGetPersonCharactersByPersonIdSearch(personId: string, raidGroupId: string, page: number, pageSize: number, searchTerm?: string){
const params = new URLSearchParams();
params.append("page", page.toString());
params.append("pageSize", pageSize.toString());
if(searchTerm){
params.append("searchTerm", searchTerm);
}
return useQuery({
queryKey: ["personCharacters", personId, { page, pageSize, searchTerm }],
queryFn: async () => {
const response = await api.get(`/raidGroup/${raidGroupId}/person/${personId}/character/page?${params}`);
return response.data as PersonCharacter[];
}
});
}
export function useGetPersonCharactersCountByPersonIdSearch(personId: string, raidGroupId: string, searchTerm?: string){
const params = new URLSearchParams();
if(searchTerm){
params.append("searchTerm", searchTerm);
}
return useQuery({
queryKey: ["personCharactersCount", personId, { searchTerm }],
queryFn: async () => {
const response = await api.get(`/raidGroup/${raidGroupId}/person/${personId}/character/count?${params}`);
return (response.data as Counter).count;
}
});
}
export function useGetPersonCharactersByRaidGroup(raidGroupId: string){
return useQuery({
queryKey: ["personCharacters", raidGroupId],
queryFn: async () => {
const response = await api.get(`/raidGroup/${raidGroupId}/person/character`);
return response.data as PersonCharacter[];
},
enabled: !!raidGroupId
});
}
export function useCreatePersonCharacter(raidGroupId: string, personId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["createPersonCharacter"],
mutationFn: async (personCharacter: PersonCharacter) => {
await api.post(`/raidGroup/${raidGroupId}/person/${personId}/character`, personCharacter);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["personCharacters"] });
}
});
}
export function useUpdatePersonCharacter(raidGroupId: string, personId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["updatePersonCharacter"],
mutationFn: async (personCharacter: PersonCharacter) => {
await api.put(`/raidGroup/${raidGroupId}/person/${personId}/character/${personCharacter.personCharacterId}`, personCharacter);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["personCharacters"] });
}
});
}
export function useDeletePersonCharacter(raidGroupId: string, personId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["deletePersonCharacter"],
mutationFn: async (personCharacterId: string) => {
await api.delete(`/raidGroup/${raidGroupId}/person/${personId}/character/${personCharacterId}`);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["personCharacters"] });
}
});
}

97
src/hooks/PersonHooks.ts Normal file
View File

@@ -0,0 +1,97 @@
import { Counter } from "@/interface/Counters";
import { Person } from "@/interface/Person";
import api from "@/util/AxiosUtil";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
export function useGetPerson(raidGroupId: string, personId: string, disabled: boolean){
return useQuery({
queryKey: ["people", raidGroupId, personId],
queryFn: async () => {
const response = await api.get(`/raidGroup/${raidGroupId}/person/${personId}`);
return response.data as Person;
},
enabled: !disabled
});
}
export function useGetPeopleByRaidGroup(raidGroupId: string, page: number, pageSize: number, searchTerm?: string){
return useQuery({
queryKey: ["people", raidGroupId, { page, pageSize, searchTerm }],
queryFn: async () => {
const searchParams = new URLSearchParams();
searchParams.append("page", page.toString());
searchParams.append("pageSize", pageSize.toString());
if(searchTerm){
searchParams.append("searchTerm", searchTerm);
}
const response = await api.get(`/raidGroup/${raidGroupId}/person?${searchParams}`);
return response.data as Person[];
}
});
}
export function useGetPeopleByRaidGroupCount(raidGroupId: string, searchTerm?: string){
const searchParams = new URLSearchParams();
if(searchTerm){
searchParams.append("searchTerm", searchTerm);
}
return useQuery({
queryKey: ["people", raidGroupId, "count", searchTerm],
queryFn: async () => {
const response = await api.get(`/raidGroup/${raidGroupId}/person/count?${searchParams}`);
return (response.data as Counter).count;
}
});
}
export function useCreatePerson(){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["createPerson"],
mutationFn: async ({raidGroupId, personName, discordId}:{raidGroupId: string; personName: string; discordId?: string;}) => {
await api.post(`/raidGroup/${raidGroupId}/person`, {raidGroupId, personName, discordId});
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["people"] });
}
});
}
export function useUpdatePerson(){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["updatePerson"],
mutationFn: async (person: Person) => {
await api.put(`/raidGroup/${person.raidGroupId}/person/${person.personId}`, person);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["people"] });
}
});
}
export function useDeletePerson(){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["deletePerson"],
mutationFn: async ({raidGroupId, personId}:{raidGroupId: string; personId: string;}) => {
await api.delete(`/raidGroup/${raidGroupId}/person/${personId}`);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["people"] });
}
});
}

189
src/hooks/RaidGroupHooks.ts Normal file
View File

@@ -0,0 +1,189 @@
import { Counter } from "@/interface/Counters";
import { RaidGroup } from "@/interface/RaidGroup";
import api from "@/util/AxiosUtil";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
export function useGetRaidGroup(raidGroupId: string, disabled: boolean){
return useQuery({
queryKey: ["raidGroups", raidGroupId],
queryFn: async () => {
const response = await api.get(`/raidGroup/${raidGroupId}`);
return (response.data as RaidGroup)?.raidGroupId ? response.data as RaidGroup : undefined;
},
enabled: !disabled
});
}
export function useGetRaidGroups(page: number, pageSize: number, searchTerm?: string){
return useQuery({
queryKey: ["raidGroups", { page, pageSize, searchTerm }],
queryFn: async () => {
const params = new URLSearchParams();
params.append("page", page.toString());
params.append("pageSize", pageSize.toString());
if(searchTerm){
params.append("searchTerm", searchTerm);
}
const response = await api.get(`/raidGroup?${params}`);
return response.data as RaidGroup[];
}
});
}
export function useGetRaidGroupsCount(searchTerm?: string){
const searchParams = new URLSearchParams();
if(searchTerm){
searchParams.append("searchTerm", searchTerm);
}
return useQuery({
queryKey: ["raidGroups", "count", searchTerm],
queryFn: async () => {
const response = await api.get(`/raidGroup/count?${searchParams}`);
return (response.data as Counter).count;
}
});
}
export function useGetRaidGroupsByGame(gameId: string, page: number, pageSize: number, searchTerm?: string){
return useQuery({
queryKey: ["raidGroups", gameId, { page, pageSize, searchTerm }],
queryFn: async () => {
const params = new URLSearchParams();
params.append("page", page.toString());
params.append("pageSize", pageSize.toString());
if(searchTerm){
params.append("searchTerm", searchTerm);
}
const response = await api.get(`/raidGroup/game/${gameId}?${params}`);
return response.data as RaidGroup[];
}
});
}
export function useGetRaidGroupsByGameCount(gameId: string, searchTerm?: string){
const searchParams = new URLSearchParams();
if(searchTerm){
searchParams.append("searchTerm", searchTerm);
}
return useQuery({
queryKey: ["raidGroups", gameId, "count", searchTerm],
queryFn: async () => {
const response = await api.get(`/raidGroup/game/${gameId}/count?${searchParams}`);
return (response.data as Counter).count;
}
});
}
export function useGetRaidGroupsByAccount(accountId: string, page: number, pageSize: number, searchTerm?: string){
return useQuery({
queryKey: ["raidGroups", accountId, { page, pageSize, searchTerm }],
queryFn: async () => {
const params = new URLSearchParams();
params.append("page", page.toString());
params.append("pageSize", pageSize.toString());
if(searchTerm){
params.append("searchTerm", searchTerm);
}
const response = await api.get(`/raidGroup/account/${accountId}?${params}`);
return response.data as RaidGroup[];
}
});
}
export function useGetRaidGroupsCountByAccount(accountId: string, searchTerm?: string){
const searchParams = new URLSearchParams();
if(searchTerm){
searchParams.append("searchTerm", searchTerm);
}
return useQuery({
queryKey: ["raidGroups", accountId, "count", searchTerm],
queryFn: async () => {
const response = await api.get(`/raidGroup/account/${accountId}/count?${searchParams}`);
return (response.data as Counter).count;
}
});
}
export function useCreateRaidGroup(){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["createRaidGroup"],
mutationFn: async ({raidGroupName, gameId, iconFile}:{raidGroupName: string; gameId: string; iconFile: File | null;}) => {
const formData = new FormData();
if(iconFile){
formData.append("iconFile", iconFile);
}
formData.append("raidGroupName", raidGroupName);
formData.append("gameId", gameId);
await api.post(
"/raidGroup",
formData
);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["raidGroups"] });
}
});
}
export function useUpdateRaidGroup(){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["updateRaidGroup"],
mutationFn: async ({raidGroup, iconFile}:{raidGroup: RaidGroup; iconFile: File | null;}) => {
const formData = new FormData();
if(iconFile){
formData.append("iconFile", iconFile);
}
formData.append("raidGroupName", raidGroup.raidGroupName);
formData.append("gameId", raidGroup.gameId);
if(raidGroup.raidGroupIcon){
formData.append("raidGroupIcon", raidGroup.raidGroupIcon);
}
await api.put(`/raidGroup/${raidGroup.raidGroupId}`, formData);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["raidGroups"] });
}
});
}
export function useDeleteRaidGroup(){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["deleteRaidGroup"],
mutationFn: async (raidGroupId: string) => {
await api.delete(`/raidGroup/${raidGroupId}`);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["raidGroups"] });
}
});
}

View File

@@ -0,0 +1,86 @@
import { Counter } from "@/interface/Counters";
import { RaidGroupPermissionType } from "@/interface/RaidGroup";
import { RaidGroupRequest } from "@/interface/RaidGroupRequest";
import api from "@/util/AxiosUtil";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
export function useGetRaidGroupRequests(raidGroupId: string, page: number, pageSize: number, searchTerm?: string){
const searchParams = new URLSearchParams();
searchParams.append("page", page.toString());
searchParams.append("pageSize", pageSize.toString());
if(searchTerm){
searchParams.append("searchTerm", searchTerm);
}
return useQuery({
queryKey: ["raidGroupRequest", raidGroupId, {page, pageSize, searchTerm}],
queryFn: async () => {
const response = await api.get(`/raidGroup/${raidGroupId}/raidGroupRequest?${searchParams}`);
return response.data as RaidGroupRequest[];
},
enabled: !!raidGroupId && raidGroupId !== ""
});
}
export function useGetRaidGroupRequestCount(raidGroupId?: string, searchTerm?: string){
const searchParams = new URLSearchParams();
if(searchTerm){
searchParams.append("searchTerm", searchTerm);
}
return useQuery({
queryKey: ["raidGroupRequest", raidGroupId, "count", searchTerm],
queryFn: async () => {
const response = await api.get(`/raidGroup/${raidGroupId}/raidGroupRequest/count?${searchParams}`);
return (response.data as Counter).count;
},
enabled: !!raidGroupId && raidGroupId !== ""
});
}
export function useCreateRaidGroupRequest(raidGroupId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["raidGroupRequest", raidGroupId],
mutationFn: async (requestMessage: string) => {
await api.post(`/raidGroup/${raidGroupId}/raidGroupRequest`, {requestMessage});
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["raidGroupRequest", raidGroupId] });
}
});
}
export function useResolveRaidGroupRequest(raidGroupId: string, raidGroupRequestId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["raidGroupRequest", raidGroupId, raidGroupRequestId],
mutationFn: async (permission: RaidGroupPermissionType) => {
await api.put(`/raidGroup/${raidGroupId}/raidGroupRequest/${raidGroupRequestId}/resolve`, {resolution: permission});
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["raidGroupRequest", raidGroupId] });
}
});
}
export function useDeleteRaidGroupRequest(raidGroupId: string, raidGroupRequestId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["raidGroupRequest", raidGroupId, raidGroupRequestId],
mutationFn: async () => {
await api.delete(`/raidGroup/${raidGroupId}/raidGroupRequest/${raidGroupRequestId}`);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["raidGroupRequest", raidGroupId] });
}
});
}

View File

@@ -0,0 +1,138 @@
import { Counter } from "@/interface/Counters";
import { RaidInstance } from "@/interface/RaidInstance";
import { RaidInstancePersonCharacterXref } from "@/interface/RaidInstancePersonCharacterXref";
import api from "@/util/AxiosUtil";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
export function useGetRaidInstance(raidInstanceId: string, raidGroupId: string){
return useQuery({
queryKey: ["raidInstances", raidInstanceId, raidGroupId],
queryFn: async () => {
const response = await api.get(`/raidGroup/${raidGroupId}/raidInstance/${raidInstanceId}`);
return response.data as RaidInstance;
},
enabled: !!raidInstanceId && !!raidGroupId
});
}
export function useGetRaidInstancesByRaidGroup(raidGroupId: string, page: number, pageSize: number, searchTerm?: string){
return useQuery({
queryKey: ["raidInstances", raidGroupId, {page, pageSize, searchTerm}],
queryFn: async () => {
const params = new URLSearchParams();
params.append("page", page.toString());
params.append("pageSize", pageSize.toString());
if(searchTerm){
params.append("searchTerm", searchTerm);
}
const response = await api.get(`/raidGroup/${raidGroupId}/raidInstance?${params}`);
return response.data as RaidInstance[];
}
});
}
export function useGetRaidInstancesByRaidGroupCount(raidGroupId: string, searchTerm?: string){
return useQuery({
queryKey: ["raidInstances", raidGroupId, "count", {searchTerm}],
queryFn: async () => {
const params = new URLSearchParams();
if(searchTerm){
params.append("searchTerm", searchTerm);
}
const response = await api.get(`/raidGroup/${raidGroupId}/raidInstance/count?${params}`);
return (response.data as Counter).count;
}
});
}
export function useCreateRaidInstance(raidGroupId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["createRaidInstance", raidGroupId],
mutationFn: async (raidInstance: RaidInstance) => {
const response = await api.post(`/raidGroup/${raidGroupId}/raidInstance`, raidInstance);
return (response.data as RaidInstance).raidInstanceId;
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["raidInstances"] });
}
});
}
export function useUpdateRaidInstance(raidGroupId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["updateRaidInstance", raidGroupId],
mutationFn: async (raidInstance: RaidInstance) => {
await api.put(`/raidGroup/${raidGroupId}/raidInstance/${raidInstance.raidInstanceId}`, raidInstance);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["raidInstances"] });
}
});
}
export function useUpdateRaidInstanceNoInvalidation(raidGroupId: string){
return useMutation({
mutationKey: ["updateRaidInstance", raidGroupId],
mutationFn: async (raidInstance: RaidInstance) => {
await api.put(`/raidGroup/${raidGroupId}/raidInstance/${raidInstance.raidInstanceId}`, raidInstance);
}
});
}
export function useDeleteRaidInstance(raidGroupId: string, raidInstanceId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["deleteRaidInstance", raidGroupId, raidInstanceId],
mutationFn: async () => {
await api.delete(`/raidGroup/${raidGroupId}/raidInstance/${raidInstanceId}`);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["raidInstances"] });
}
});
}
export function useGetRaidInstancePersonCharacterXrefs(raidGroupId?: string, raidInstanceId?: string){
return useQuery({
queryKey: ["raidInstancePersonCharacterXrefs", raidGroupId, raidInstanceId],
queryFn: async () => {
const response = await api.get(`/raidGroup/${raidGroupId}/raidInstance/${raidInstanceId}/personCharacterXref`);
return response.data as RaidInstancePersonCharacterXref[];
},
enabled: !!raidGroupId && !!raidInstanceId
});
}
export function useUpdateRaidInstancePersonCharacterXrefs(raidGroupId?: string, raidInstanceId?: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["updateRaidInstancePersonCharacterXrefs", raidGroupId, raidInstanceId],
mutationFn: async (raidInstancePersonCharacterXrefs: RaidInstancePersonCharacterXref[]) => {
await api.post(`/raidGroup/${raidGroupId}/raidInstance/${raidInstanceId}/personCharacterXref`, raidInstancePersonCharacterXrefs);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["raidInstancePersonCharacterXrefs", raidGroupId, raidInstanceId] });
}
});
}

View File

@@ -0,0 +1,107 @@
import { Counter } from "@/interface/Counters";
import { RaidLayout, RaidLayoutClassGroupXref } from "@/interface/RaidLayout";
import api from "@/util/AxiosUtil";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
export function useGetRaidLayout(raidGroupId?: string, raidLayoutId?: string){
return useQuery({
queryKey: ["raidLayout", raidGroupId, raidLayoutId],
queryFn: async () => {
const response = await api.get(`/raidGroup/${raidGroupId}/raidLayout/${raidLayoutId}`);
return response.data as RaidLayout;
},
enabled: !!raidGroupId && !!raidLayoutId
});
}
export function useGetRaidLayoutsByRaidGroup(raidGroupId: string, page: number, pageSize: number, searchTerm?: string){
return useQuery({
queryKey: ["raidLayouts", raidGroupId, {page, pageSize, searchTerm}],
queryFn: async () => {
const params = new URLSearchParams();
params.append("page", page.toString());
params.append("pageSize", pageSize.toString());
if(searchTerm){
params.append("searchTerm", searchTerm);
}
const response = await api.get(`/raidGroup/${raidGroupId}/raidLayout?${params}`);
return response.data as RaidLayout[];
}
});
}
export function useGetRaidLayoutsByRaidGroupCount(raidGroupId: string, searchTerm?: string){
return useQuery({
queryKey: ["raidLayouts", raidGroupId, "count", searchTerm],
queryFn: async () => {
const params = new URLSearchParams();
if(searchTerm){
params.append("searchTerm", searchTerm);
}
const response = await api.get(`/raidGroup/${raidGroupId}/raidLayout/count?${params}`);
return (response.data as Counter).count;
}
});
}
export function useCreateRaidLayout(raidGroupId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["createRaidLayout", raidGroupId],
mutationFn: async ({raidLayout, raidLayoutClassGroupXrefs}:{raidLayout: RaidLayout; raidLayoutClassGroupXrefs: RaidLayoutClassGroupXref[];}) => {
await api.post(`/raidGroup/${raidGroupId}/raidLayout`,
{
raidLayout,
raidLayoutClassGroupXrefs
}
);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["raidLayouts", raidGroupId] });
}
});
}
export function useUpdateRaidLayout(raidGroupId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["updateRaidLayout", raidGroupId],
mutationFn: async ({raidLayout, raidLayoutClassGroupXrefs}:{raidLayout: RaidLayout; raidLayoutClassGroupXrefs: RaidLayoutClassGroupXref[];}) => {
await api.put(`/raidGroup/${raidGroupId}/raidLayout/${raidLayout.raidLayoutId}`,
{
raidLayout,
raidLayoutClassGroupXrefs
}
);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["raidLayouts", raidGroupId] });
void queryClient.invalidateQueries({ queryKey: ["classGroups", "raidLayout"] });
}
});
}
export function useDeleteRaidLayout(raidGroupId: string, raidLayoutId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["deleteRaidLayout", raidGroupId, raidLayoutId],
mutationFn: async () => {
await api.delete(`/raidGroup/${raidGroupId}/raidLayout/${raidLayoutId}`)
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["raidLayouts", raidGroupId] });
}
});
}

82
src/index.css Normal file
View File

@@ -0,0 +1,82 @@
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-neutral-825: oklch(0.253 0 0);
--color-neutral-850: oklch(0.237 0 0);
}
:root.dark {
--text-color: #FFFFFFDE;
--bg-color: var(--color-neutral-825);
color-scheme: dark;
}
:root.light {
--text-color: #213547;
--bg-color: #FFFFFF;
}
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--bg-color);
color: var(--text-color);
}
#root {
width: 100%;
}
a:hover{
color: var(--color-blue-300);
}
a.active {
color: var(--color-blue-400);
}
body {
place-items: center;
min-width: 320px;
min-height: 100vh;
max-width: var(--breakpoint-2xl);
margin-inline: auto;
padding-top: 90px;
padding-inline: 1rem;
text-align: center;
}
button {
cursor: pointer;
}
button:disabled {
cursor: default;
}
nav {
position: fixed;
top: 0;
left: 0;
width: 100%;
}
.navContents {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
max-width: var(--breakpoint-2xl);
margin-inline: auto;
padding: 1rem;
}

26
src/interface/Account.ts Normal file
View File

@@ -0,0 +1,26 @@
export enum AccountStatus {
ACTIVE = "ACTIVE",
LOCKED = "LOCKED",
INACTIVE = "INACTIVE",
DELETED = "DELETED",
UNCONFIRMED = "UNCONFIRMED"
};
export enum AccountPermissionType {
ADMIN = "ADMIN",
USER = "USER"
}
export interface Account {
accountId: string;
username: string;
password: string;
loginDate: Date;
email: string;
forceReset: boolean;
refreshToken?: string;
refreshTokenExpiration?: Date;
accountStatus: AccountStatus;
token?: string;
}

View File

@@ -0,0 +1,8 @@
import { AccountPermissionType } from "./Account";
export interface AccountPermission {
accountPermissionId?: string;
accountId: string;
accountPermissionType: AccountPermissionType;
}

View File

@@ -0,0 +1,15 @@
export enum TutorialStatus {
COMPLETED = "COMPLETED",
NOT_COMPLETED = "NOT_COMPLETED"
}
export interface AccountTutorialStatus {
accountTutorialStatusId?: string;
accountId: string;
gamesTutorialStatus: TutorialStatus;
gameTutorialStatus: TutorialStatus;
raidGroupsTutorialStatus: TutorialStatus;
raidGroupTutorialStatus: TutorialStatus;
instanceTutorialStatus: TutorialStatus;
}

13
src/interface/Calendar.ts Normal file
View File

@@ -0,0 +1,13 @@
export interface CalendarEvent {
eventName: string;
eventDescription: string;
eventStartDate: Date;
eventEndDate: Date;
backgroundColor?: string;
calendarEventId?: string;
gameId?: string;
raidGroupId?: string;
raidInstanceId?: string;
}

View File

@@ -0,0 +1,5 @@
export interface ClassGroup {
classGroupId?: string;
raidGroupId: string;
classGroupName: string;
}

View File

@@ -0,0 +1,3 @@
export interface Counter {
count: number;
}

4
src/interface/Errors.ts Normal file
View File

@@ -0,0 +1,4 @@
export interface ResponseError {
status: string;
errors: string[];
}

5
src/interface/Game.ts Normal file
View File

@@ -0,0 +1,5 @@
export interface Game{
gameId?: string;
gameName: string;
gameIcon?: string;
}

View File

@@ -0,0 +1,6 @@
export interface GameClass {
gameClassId?: string;
gameId: string;
gameClassName: string;
gameClassIcon?: string;
}

View File

@@ -0,0 +1,11 @@
export enum GamePermissionType {
ADMIN = "ADMIN"
}
export interface GamePermission {
gamePermissionId?: string;
accountId: string;
gameId: string;
gamePermissionType: GamePermissionType;
}

View File

@@ -0,0 +1,4 @@
export interface GridLocation {
row: number;
col: number;
}

View File

@@ -0,0 +1,24 @@
import { HTMLProps } from "react";
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 interface ModalBackgroundProps extends HTMLProps<HTMLDivElement>{
backgroundType?: ModalBackgroundType;
close?: () => void;
}
export interface ModalProps extends HTMLProps<HTMLDivElement>{
display?: boolean;
backgroundType?: ModalBackgroundType;
backgroundClassName?: string;
top?: boolean;
close?: () => void;
children: React.ReactNode;
}
export interface ModalHeaderProps extends HTMLProps<HTMLDivElement>{
close?: () => void;
}

6
src/interface/Person.ts Normal file
View File

@@ -0,0 +1,6 @@
export interface Person {
personId?: string;
raidGroupId: string;
personName: string;
discordId?: string;
}

View File

@@ -0,0 +1,8 @@
export interface PersonCharacter {
personCharacterId?: string;
personId: string;
gameClassId: string;
characterName: string;
characterRating?: number;
characterComments?: string;
}

View File

@@ -0,0 +1,13 @@
export enum RaidGroupPermissionType {
ADMIN = "ADMIN",
LEADER = "LEADER",
RAIDER = "RAIDER"
}
export interface RaidGroup {
raidGroupId?: string;
gameId: string;
raidGroupName: string;
raidGroupIcon?: string;
}

View File

@@ -0,0 +1,9 @@
import { RaidGroupPermissionType } from "./RaidGroup";
export interface RaidGroupPermission {
raidGroupPermissionId?: string;
accountId: string;
raidGroupId: string;
permission: RaidGroupPermissionType;
}

View File

@@ -0,0 +1,8 @@
export interface RaidGroupRequest {
raidGroupRequestId?: string;
raidGroupId: string;
accountId: string;
requestMessage: string;
username: string;
}

View File

@@ -0,0 +1,10 @@
export interface RaidInstance {
raidInstanceId?: string;
raidGroupId: string;
raidLayoutId?: string;
raidInstanceName?: string;
raidSize?: number;
numberRuns: number;
raidStartDate: Date;
raidEndDate: Date;
}

View File

@@ -0,0 +1,7 @@
export interface RaidInstancePersonCharacterXref {
raidInstancePersonCharacterXrefId?: string;
raidInstanceId: string;
personCharacterId: string;
groupNumber: number;
positionNumber: number;
}

View File

@@ -0,0 +1,13 @@
export interface RaidLayout {
raidLayoutId?: string;
raidGroupId: string;
raidLayoutName: string;
raidSize: number;
}
export interface RaidLayoutClassGroupXref {
raidLayoutClassGroupXrefId?: string;
raidLayoutId: string;
classGroupId?: string;
layoutLocation: number;
}

Some files were not shown because too many files have changed in this diff Show More