Authorization working
This commit is contained in:
28
eslint.config.js
Normal file
28
eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4331
package-lock.json
generated
Normal file
4331
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
package.json
Normal file
45
package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "raid-builder-web-react",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fullcalendar/core": "^6.1.15",
|
||||
"@fullcalendar/daygrid": "^6.1.15",
|
||||
"@fullcalendar/interaction": "^6.1.15",
|
||||
"@fullcalendar/react": "^6.1.15",
|
||||
"@tailwindcss/vite": "^4.0.7",
|
||||
"@tanstack/react-query": "^5.66.9",
|
||||
"@types/node": "^22.13.4",
|
||||
"axios": "^1.7.9",
|
||||
"clsx": "^2.1.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-joyride": "^3.0.0-7",
|
||||
"react-router": "^7.2.0",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"tailwindcss": "^4.0.7",
|
||||
"use-debounce": "^10.0.4",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.18",
|
||||
"globals": "^15.14.0",
|
||||
"typescript": "~5.7.2",
|
||||
"typescript-eslint": "^8.22.0",
|
||||
"vite": "^6.1.0"
|
||||
}
|
||||
}
|
||||
309
public/raidBuilderIcon.svg
Normal file
309
public/raidBuilderIcon.svg
Normal 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 |
10
src/App.css
Normal file
10
src/App.css
Normal file
@@ -0,0 +1,10 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@theme {
|
||||
|
||||
}
|
||||
115
src/App.tsx
Normal file
115
src/App.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import "@/App.css";
|
||||
import { BrowserRouter, Route, Routes } from "react-router";
|
||||
import NavBar from "./components/nav/NavBar";
|
||||
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 HomePage from "./pages/public/HomePage";
|
||||
import LoginPage from "./pages/public/LoginPage";
|
||||
import SignupPage from "./pages/public/SignupPage";
|
||||
import { ProtectedRoute } from "./providers/AuthProvider";
|
||||
|
||||
|
||||
const publicRoutes: { path: string; element: React.ReactElement; }[] = [
|
||||
{
|
||||
path: "/",
|
||||
element: <HomePage/>
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
element: <LoginPage/>
|
||||
},
|
||||
{
|
||||
path: "/signup",
|
||||
element: <SignupPage/>
|
||||
}
|
||||
];
|
||||
|
||||
const protectedRoutes: { path: string; element: React.ReactElement; }[] = [
|
||||
{
|
||||
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 (
|
||||
<div
|
||||
className="mt-20"
|
||||
>
|
||||
<BrowserRouter>
|
||||
{/* Nav Bar */}
|
||||
<NavBar/>
|
||||
|
||||
{/* Routing */}
|
||||
<Routes>
|
||||
{/* Public Routes */}
|
||||
{
|
||||
publicRoutes.map((route) => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={route.element}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{/* Protected Routes */}
|
||||
<Route element={<ProtectedRoute/>}>
|
||||
{
|
||||
protectedRoutes.map((route) => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={route.element}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
18
src/components/button/PrimaryButton.tsx
Normal file
18
src/components/button/PrimaryButton.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import clsx from "clsx";
|
||||
import { ComponentProps } from "react";
|
||||
|
||||
|
||||
export default function PrimaryButton(props: ComponentProps<"button">){
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={clsx(
|
||||
props.className,
|
||||
"rounded-lg py-2 px-4",
|
||||
"bg-blue-500 dark:bg-blue-600 text-white"
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
63
src/components/input/PasswordInput.tsx
Normal file
63
src/components/input/PasswordInput.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
64
src/components/input/TextInput.tsx
Normal file
64
src/components/input/TextInput.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
32
src/components/nav/DarkModeToggle.tsx
Normal file
32
src/components/nav/DarkModeToggle.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
56
src/components/nav/NavBar.tsx
Normal file
56
src/components/nav/NavBar.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import clsx from "clsx";
|
||||
import { BsList } from "react-icons/bs";
|
||||
import { Link } from "react-router";
|
||||
import DarkModeToggle from "./DarkModeToggle";
|
||||
import ProtectedNavLinks from "./ProtectedNavLinks";
|
||||
import PublicNavLinks from "./PublicNavLinks";
|
||||
import raidBuilderIcon from "/raidBuilderIcon.svg";
|
||||
|
||||
|
||||
export default function NavBar(){
|
||||
return (
|
||||
<nav
|
||||
className={clsx(
|
||||
"fixed w-full top-0 left-0 border-b-2 z-40",
|
||||
"bg-gray-700 border-gray-600 dark:bg-zinc-900 dark:border-neutral-850 text-white"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="max-w-(--breakpoint-xl) flex flex-nowrap flex-row items-center justify-between mx-auto p-4"
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
48
src/components/nav/ProtectedNavLinks.tsx
Normal file
48
src/components/nav/ProtectedNavLinks.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useAuth } from "@/providers/AuthProvider";
|
||||
import { NavLink } from "react-router";
|
||||
|
||||
|
||||
const protectedLinks = [
|
||||
{
|
||||
name: "Games",
|
||||
path: "/game"
|
||||
},
|
||||
{
|
||||
name: "Groups",
|
||||
path: "/raidGroup"
|
||||
},
|
||||
{
|
||||
name: "Admin",
|
||||
path: "/admin"
|
||||
},
|
||||
{
|
||||
name: "Logout",
|
||||
path: "/logout"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
export default function ProtectedNavLinks(){
|
||||
const { jwt } = useAuth();
|
||||
|
||||
|
||||
if(!jwt){
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
protectedLinks.map((link) => (
|
||||
<NavLink
|
||||
key={link.name}
|
||||
to={link.path}
|
||||
>
|
||||
{link.name}
|
||||
</NavLink>
|
||||
))
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
39
src/components/nav/PublicNavLinks.tsx
Normal file
39
src/components/nav/PublicNavLinks.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
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>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
3
src/hooks/AuthHooks.ts
Normal file
3
src/hooks/AuthHooks.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
|
||||
|
||||
54
src/index.css
Normal file
54
src/index.css
Normal file
@@ -0,0 +1,54 @@
|
||||
@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 {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: var(--color-neutral-825);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a.active {
|
||||
color: var(--color-blue-400);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
23
src/main.tsx
Normal file
23
src/main.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
import { AuthProvider } from './providers/AuthProvider.tsx'
|
||||
import { ThemeProvider } from './providers/ThemeProvider.tsx'
|
||||
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<ThemeProvider
|
||||
defaultTheme="dark"
|
||||
storageKey="vite-ui-theme"
|
||||
>
|
||||
<AuthProvider
|
||||
jwtStorageKey="jwt"
|
||||
refreshTokenStorageKey="refreshToken"
|
||||
>
|
||||
<App />
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
10
src/pages/protected/AdminPage.tsx
Normal file
10
src/pages/protected/AdminPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function AdminPage(){
|
||||
//TODO:
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
Admin Page
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/pages/protected/GamePage.tsx
Normal file
10
src/pages/protected/GamePage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function GamePage(){
|
||||
//TODO:
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
Game Page
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/pages/protected/GamesPage.tsx
Normal file
10
src/pages/protected/GamesPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function GamesPage(){
|
||||
//TODO:
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
Games Page
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
src/pages/protected/LogoutPage.tsx
Normal file
32
src/pages/protected/LogoutPage.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useAuth } from "@/providers/AuthProvider";
|
||||
import { api } from "@/util/AxiosUtil";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
|
||||
export default function LogoutPage(){
|
||||
const { setJwt } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
const logout = async () => {
|
||||
const response = await api.get("/auth/logout");
|
||||
if(response.status === 200){
|
||||
setJwt(null);
|
||||
navigate("/");
|
||||
}
|
||||
else{
|
||||
//TODO: Handle error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={logout}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/pages/protected/PersonPage.tsx
Normal file
10
src/pages/protected/PersonPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function PersonPage(){
|
||||
//TODO:
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
Person Page
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/pages/protected/RaidGroupPage.tsx
Normal file
10
src/pages/protected/RaidGroupPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function RaidGroupPage(){
|
||||
//TODO:
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
Raid Group Page
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/pages/protected/RaidGroupsPage.tsx
Normal file
10
src/pages/protected/RaidGroupsPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function RaidGroupsPage(){
|
||||
//TODO:
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
Raid Groups Page
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/pages/protected/RaidInstancePage.tsx
Normal file
10
src/pages/protected/RaidInstancePage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function RaidInstancePage(){
|
||||
//TODO:
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
Raid Instance Page
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/pages/protected/RaidLayoutPage.tsx
Normal file
10
src/pages/protected/RaidLayoutPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function RaidLayoutPage(){
|
||||
//TODO:
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
Raid Layout Page
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
src/pages/public/HomePage.tsx
Normal file
7
src/pages/public/HomePage.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function HomePage(){
|
||||
return (
|
||||
<main>
|
||||
Home Page
|
||||
</main>
|
||||
);
|
||||
}
|
||||
65
src/pages/public/LoginPage.tsx
Normal file
65
src/pages/public/LoginPage.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import PrimaryButton from "@/components/button/PrimaryButton";
|
||||
import PasswordInput from "@/components/input/PasswordInput";
|
||||
import TextInput from "@/components/input/TextInput";
|
||||
import { useAuth } from "@/providers/AuthProvider";
|
||||
import { Navigate, useNavigate } from "react-router";
|
||||
|
||||
|
||||
export default function LoginPage(){
|
||||
const { jwt, setJwt } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
const login = async (formData: FormData) => {
|
||||
const username = formData.get("username") as string;
|
||||
const password = formData.get("password") as string;
|
||||
|
||||
|
||||
const response = await fetch(`${import.meta.env.VITE_API_URL}/auth/token`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: "Basic " + btoa(`${username}:${password}`)
|
||||
},
|
||||
credentials: "include"
|
||||
});
|
||||
|
||||
if(response.status === 200){
|
||||
const json = await response.json();
|
||||
setJwt(json.token);
|
||||
navigate("/raidGroup");
|
||||
}
|
||||
else{
|
||||
//TODO: Handle error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(jwt){
|
||||
return <Navigate to="/"/>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<form
|
||||
action={login}
|
||||
className="flex flex-col justify-center space-y-8"
|
||||
>
|
||||
<TextInput
|
||||
id="username"
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
/>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
/>
|
||||
<PrimaryButton
|
||||
className="mx-auto"
|
||||
type="submit"
|
||||
>
|
||||
Login
|
||||
</PrimaryButton>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
7
src/pages/public/SignupPage.tsx
Normal file
7
src/pages/public/SignupPage.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function SignupPage(){
|
||||
return (
|
||||
<div>
|
||||
Signup Page
|
||||
</div>
|
||||
);
|
||||
}
|
||||
112
src/providers/AuthProvider.tsx
Normal file
112
src/providers/AuthProvider.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { api } from "@/util/AxiosUtil";
|
||||
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useState } from "react";
|
||||
import { Navigate, Outlet } from "react-router";
|
||||
|
||||
|
||||
type AuthProviderProps = {
|
||||
children: React.ReactNode;
|
||||
jwtStorageKey?: string;
|
||||
refreshTokenStorageKey?: string;
|
||||
}
|
||||
|
||||
type AuthProviderState = {
|
||||
jwt: string | null;
|
||||
setJwt: (token: string | null) => void;
|
||||
expiration: Date | null;
|
||||
setExpiration: (expiration: Date | null) => void;
|
||||
}
|
||||
|
||||
const initialState: AuthProviderState = {
|
||||
jwt: null,
|
||||
setJwt: () => null,
|
||||
expiration: null,
|
||||
setExpiration: () => null
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthProviderState>(initialState);
|
||||
|
||||
|
||||
export function AuthProvider({
|
||||
children
|
||||
}: AuthProviderProps){
|
||||
const [ jwt, setJwt ] = useState<string | null>(null);
|
||||
const [ expiration, setExpiration ] = useState<Date | null>(null)
|
||||
|
||||
|
||||
const fetchToken = useCallback(async () => {
|
||||
try{
|
||||
const response = await api.get("/auth/refresh");
|
||||
//If the token is retrieved
|
||||
if((response.status === 200) && (!response.data.errors)){
|
||||
setJwt(response.data.token);
|
||||
setExpiration(new Date(atob(response.data.token.split(".")[1])));
|
||||
}
|
||||
//If the token cannot be retrieved
|
||||
else{
|
||||
setJwt(null);
|
||||
setExpiration(null);
|
||||
}
|
||||
}
|
||||
//If the token cannot be retrieved
|
||||
catch{
|
||||
setJwt(null);
|
||||
setExpiration(null);
|
||||
}
|
||||
}, [ setJwt, setExpiration ]);
|
||||
|
||||
|
||||
//Add the token to all queries
|
||||
useLayoutEffect(() => {
|
||||
if((expiration) && (expiration < new Date())){
|
||||
fetchToken();
|
||||
}
|
||||
|
||||
const authInterceptor = api.interceptors.request.use(config => {
|
||||
config.headers.Authorization = jwt ? `Bearer ${jwt}` : config.headers.Authorization;
|
||||
return config;
|
||||
});
|
||||
|
||||
return () => { api.interceptors.request.eject(authInterceptor); };
|
||||
}, [ jwt, expiration, fetchToken ]);
|
||||
|
||||
//Try to get the token on page load
|
||||
useEffect(() => {
|
||||
fetchToken();
|
||||
}, [ fetchToken ]);
|
||||
|
||||
|
||||
const currentTokens = useMemo(() => ({
|
||||
jwt,
|
||||
setJwt,
|
||||
expiration,
|
||||
setExpiration
|
||||
}), [ jwt, setJwt, expiration, setExpiration ]);
|
||||
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={currentTokens}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProtectedRoute(){
|
||||
const { jwt } = useAuth();
|
||||
|
||||
if(!jwt){
|
||||
return <Navigate to="/login"/>;
|
||||
}
|
||||
|
||||
return <Outlet/>;
|
||||
}
|
||||
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
|
||||
if(context === undefined){
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
70
src/providers/ThemeProvider.tsx
Normal file
70
src/providers/ThemeProvider.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||
|
||||
type Theme = "dark" | "light" | "system";
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
}
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
}
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: "system",
|
||||
setTheme: () => null
|
||||
}
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "vite-ui-theme"
|
||||
}: ThemeProviderProps){
|
||||
const [ theme, setTheme ] = useState<Theme>(localStorage.getItem(storageKey) as Theme || defaultTheme);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
root.classList.remove("light", "dark");
|
||||
|
||||
if(theme === "system"){
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
root.classList.add(systemTheme);
|
||||
}
|
||||
else{
|
||||
root.classList.add(theme);
|
||||
}
|
||||
}, [ theme ]);
|
||||
|
||||
|
||||
const currentTheme = useMemo(() => ({
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
}
|
||||
}), [ theme, setTheme, storageKey ]);
|
||||
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider value={currentTheme}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeProviderContext);
|
||||
|
||||
if(context === undefined){
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
8
src/util/AxiosUtil.ts
Normal file
8
src/util/AxiosUtil.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import axios from "axios";
|
||||
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: `${import.meta.env.VITE_API_URL}`,
|
||||
timeout: 10000,
|
||||
withCredentials: true
|
||||
});
|
||||
10
src/vite-env.d.ts
vendored
Normal file
10
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_URL: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
31
tsconfig.app.json
Normal file
31
tsconfig.app.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"#root": ["."]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
24
tsconfig.node.json
Normal file
24
tsconfig.node.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
19
vite.config.ts
Normal file
19
vite.config.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { resolve } from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss()
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "./src"),
|
||||
"#root": resolve(__dirname)
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user