gitscope
← marzneshin / Diff · 4466347 → 4466347
Changes 4466347..4466347
Patch commit
diff --git a/app/marzneshin.py b/app/marzneshin.py
index 4254472..806c096 100644
--- a/app/marzneshin.py
+++ b/app/marzneshin.py
@@ -18,6 +18,7 @@ from app.templates import render_template
from config import (DEBUG, DOCS, HOME_PAGE_TEMPLATE, UVICORN_HOST,
UVICORN_PORT, UVICORN_SSL_CERTFILE, UVICORN_SSL_KEYFILE,
UVICORN_UDS)
+
from . import __version__, telegram
from .routes import api_router
from .tasks import (delete_expired_reminders, nodes_startup,
@@ -97,8 +98,8 @@ async def main():
if not DEBUG:
app.mount(
"/dashboard/",
- StaticFiles(directory="app/dashboard/dist", html=True),
- name="dashboard"
+ StaticFiles(directory="dashboard/dist", html=True),
+ name="dashboard",
)
scheduler.start()
asyncio.create_task(telegram.start_bot())
diff --git a/dashboard/package.json b/dashboard/package.json
index 04cf701..5a7353b 100644
--- a/dashboard/package.json
+++ b/dashboard/package.json
@@ -1,5 +1,5 @@
{
- "name": "dashboard",
+ "name": "marzneshin-dashboard",
"private": true,
"version": "0.0.0",
"type": "module",
@@ -7,11 +7,15 @@
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
- "preview": "vite preview"
+ "preview": "vite preview",
+ "test": "vitest",
+ "test:ui": "vitest --ui",
+ "test:coverage": "vitest run --coverage"
},
"dependencies": {
"@hookform/resolvers": "^3.3.4",
"@radix-ui/react-alert-dialog": "^1.0.5",
+ "@radix-ui/react-aspect-ratio": "^1.0.3",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
@@ -36,6 +40,7 @@
"i18next-http-backend": "^2.5.0",
"lucide-react": "^0.359.0",
"next-themes": "^0.3.0",
+ "ofetch": "^1.3.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.1",
@@ -45,7 +50,8 @@
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.0",
- "zod": "^3.22.4"
+ "zod": "^3.22.4",
+ "zustand": "^4.5.2"
},
"devDependencies": {
"@tanstack/router-cli": "^1.20.0",
@@ -57,6 +63,7 @@
"@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1",
"@vitejs/plugin-react-swc": "^3.5.0",
+ "@vitest/ui": "^1.4.0",
"autoprefixer": "^10.4.18",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
@@ -65,6 +72,7 @@
"tailwindcss": "^3.4.1",
"typescript": "^5.2.2",
"vite": "^5.1.6",
- "vite-tsconfig-paths": "^4.3.2"
+ "vite-tsconfig-paths": "^4.3.2",
+ "vitest": "^1.4.0"
}
}
diff --git a/dashboard/pnpm-lock.yaml b/dashboard/pnpm-lock.yaml
index 9681f65..9d1cb92 100644
--- a/dashboard/pnpm-lock.yaml
+++ b/dashboard/pnpm-lock.yaml
@@ -11,6 +11,9 @@ dependencies:
'@radix-ui/react-alert-dialog':
specifier: ^1.0.5
version: 1.0.5(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-aspect-ratio':
+ specifier: ^1.0.3
+ version: 1.0.3(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-checkbox':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0)
@@ -83,6 +86,9 @@ dependencies:
next-themes:
specifier: ^0.3.0
version: 0.3.0(react-dom@18.2.0)(react@18.2.0)
+ ofetch:
+ specifier: ^1.3.3
+ version: 1.3.3
react:
specifier: ^18.2.0
version: 18.2.0
@@ -113,6 +119,9 @@ dependencies:
zod:
specifier: ^3.22.4
version: 3.22.4
+ zustand:
+ specifier: ^4.5.2
+ version: 4.5.2(@types/react@18.2.64)(react@18.2.0)
devDependencies:
'@tanstack/router-cli':
@@ -142,6 +151,9 @@ devDependencies:
'@vitejs/plugin-react-swc':
specifier: ^3.5.0
version: 3.5.0(vite@5.1.6)
+ '@vitest/ui':
+ specifier: ^1.4.0
+ version: 1.4.0(vitest@1.4.0)
autoprefixer:
specifier: ^10.4.18
version: 10.4.18(postcss@8.4.36)
@@ -169,6 +181,9 @@ devDependencies:
vite-tsconfig-paths:
specifier: ^4.3.2
version: 4.3.2(typescript@5.2.2)(vite@5.1.6)
+ vitest:
+ specifier: ^1.4.0
+ version: 1.4.0(@types/node@20.11.29)(@vitest/ui@1.4.0)
packages:
@@ -498,6 +513,13 @@ packages:
wrap-ansi: 8.1.0
wrap-ansi-cjs: /wrap-ansi@7.0.0
+ /@jest/schemas@29.6.3:
+ resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ dependencies:
+ '@sinclair/typebox': 0.27.8
+ dev: true
+
/@jridgewell/gen-mapping@0.3.5:
resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
engines: {node: '>=6.0.0'}
@@ -547,6 +569,10 @@ packages:
requiresBuild: true
optional: true
+ /@polka/url@1.0.0-next.25:
+ resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==}
+ dev: true
+
/@radix-ui/primitive@1.0.1:
resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
dependencies:
@@ -600,6 +626,27 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
+ /@radix-ui/react-aspect-ratio@1.0.3(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-fXR5kbMan9oQqMuacfzlGG/SQMcmMlZ4wrvpckv8SgUulD0MMpspxJrxg/Gp/ISV3JfV1AeSWTYK9GvxA4ySwA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.24.0
+ '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0)
+ '@types/react': 18.2.64
+ '@types/react-dom': 18.2.21
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: false
+
/@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==}
peerDependencies:
@@ -1433,6 +1480,10 @@ packages:
dev: true
optional: true
+ /@sinclair/typebox@0.27.8:
+ resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
+ dev: true
+
/@swc/core-darwin-arm64@1.4.8:
resolution: {integrity: sha512-hhQCffRTgzpTIbngSnC30vV6IJVTI9FFBF954WEsshsecVoCGFiMwazBbrkLG+RwXENTrMhgeREEFh6R3KRgKQ==}
engines: {node: '>=10'}
@@ -1844,6 +1895,60 @@ packages:
- '@swc/helpers'
dev: true
+ /@vitest/expect@1.4.0:
+ resolution: {integrity: sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==}
+ dependencies:
+ '@vitest/spy': 1.4.0
+ '@vitest/utils': 1.4.0
+ chai: 4.4.1
+ dev: true
+
+ /@vitest/runner@1.4.0:
+ resolution: {integrity: sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==}
+ dependencies:
+ '@vitest/utils': 1.4.0
+ p-limit: 5.0.0
+ pathe: 1.1.2
+ dev: true
+
+ /@vitest/snapshot@1.4.0:
+ resolution: {integrity: sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==}
+ dependencies:
+ magic-string: 0.30.8
+ pathe: 1.1.2
+ pretty-format: 29.7.0
+ dev: true
+
+ /@vitest/spy@1.4.0:
+ resolution: {integrity: sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==}
+ dependencies:
+ tinyspy: 2.2.1
+ dev: true
+
+ /@vitest/ui@1.4.0(vitest@1.4.0):
+ resolution: {integrity: sha512-XC6CMhN1gzYcGbpn6/Oanj4Au2EXwQEX6vpcOeLlZv8dy7g11Ukx8zwtYQbwxs9duK2s9j2o5rbQiCP5DPAcmw==}
+ peerDependencies:
+ vitest: 1.4.0
+ dependencies:
+ '@vitest/utils': 1.4.0
+ fast-glob: 3.3.2
+ fflate: 0.8.2
+ flatted: 3.3.1
+ pathe: 1.1.2
+ picocolors: 1.0.0
+ sirv: 2.0.4
+ vitest: 1.4.0(@types/node@20.11.29)(@vitest/ui@1.4.0)
+ dev: true
+
+ /@vitest/utils@1.4.0:
+ resolution: {integrity: sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==}
+ dependencies:
+ diff-sequences: 29.6.3
+ estree-walker: 3.0.3
+ loupe: 2.3.7
+ pretty-format: 29.7.0
+ dev: true
+
/acorn-jsx@5.3.2(acorn@8.11.3):
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -1852,6 +1957,11 @@ packages:
acorn: 8.11.3
dev: true
+ /acorn-walk@8.3.2:
+ resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
+ engines: {node: '>=0.4.0'}
+ dev: true
+
/acorn@8.11.3:
resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==}
engines: {node: '>=0.4.0'}
@@ -1881,6 +1991,11 @@ packages:
dependencies:
color-convert: 2.0.1
+ /ansi-styles@5.2.0:
+ resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
+ engines: {node: '>=10'}
+ dev: true
+
/ansi-styles@6.2.1:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'}
@@ -1914,6 +2029,10 @@ packages:
engines: {node: '>=8'}
dev: true
+ /assertion-error@1.1.0:
+ resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
+ dev: true
+
/autoprefixer@10.4.18(postcss@8.4.36):
resolution: {integrity: sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==}
engines: {node: ^10 || ^12 || >=14}
@@ -1966,6 +2085,11 @@ packages:
update-browserslist-db: 1.0.13(browserslist@4.23.0)
dev: true
+ /cac@6.7.14:
+ resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
+ engines: {node: '>=8'}
+ dev: true
+
/callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
@@ -1979,6 +2103,19 @@ packages:
resolution: {integrity: sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==}
dev: true
+ /chai@4.4.1:
+ resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==}
+ engines: {node: '>=4'}
+ dependencies:
+ assertion-error: 1.1.0
+ check-error: 1.0.3
+ deep-eql: 4.1.3
+ get-func-name: 2.0.2
+ loupe: 2.3.7
+ pathval: 1.1.1
+ type-detect: 4.0.8
+ dev: true
+
/chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
@@ -1987,6 +2124,12 @@ packages:
supports-color: 7.2.0
dev: true
+ /check-error@1.0.3:
+ resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==}
+ dependencies:
+ get-func-name: 2.0.2
+ dev: true
+
/chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
@@ -2089,10 +2232,21 @@ packages:
ms: 2.1.2
dev: true
+ /deep-eql@4.1.3:
+ resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==}
+ engines: {node: '>=6'}
+ dependencies:
+ type-detect: 4.0.8
+ dev: true
+
/deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
dev: true
+ /destr@2.0.3:
+ resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==}
+ dev: false
+
/detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
dev: false
@@ -2100,6 +2254,11 @@ packages:
/didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
+ /diff-sequences@29.6.3:
+ resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ dev: true
+
/dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
@@ -2276,11 +2435,32 @@ packages:
engines: {node: '>=4.0'}
dev: true
+ /estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+ dependencies:
+ '@types/estree': 1.0.5
+ dev: true
+
/esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
dev: true
+ /execa@8.0.1:
+ resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
+ engines: {node: '>=16.17'}
+ dependencies:
+ cross-spawn: 7.0.3
+ get-stream: 8.0.1
+ human-signals: 5.0.0
+ is-stream: 3.0.0
+ merge-stream: 2.0.0
+ npm-run-path: 5.3.0
+ onetime: 6.0.0
+ signal-exit: 4.1.0
+ strip-final-newline: 3.0.0
+ dev: true
+
/fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: true
@@ -2308,6 +2488,10 @@ packages:
dependencies:
reusify: 1.0.4
+ /fflate@0.8.2:
+ resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
+ dev: true
+
/file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
@@ -2372,11 +2556,20 @@ packages:
engines: {node: 6.* || 8.* || >= 10.*}
dev: true
+ /get-func-name@2.0.2:
+ resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
+ dev: true
+
/get-nonce@1.0.1:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'}
dev: false
+ /get-stream@8.0.1:
+ resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
+ engines: {node: '>=16'}
+ dev: true
+
/glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -2463,6 +2656,11 @@ packages:
void-elements: 3.1.0
dev: false
+ /human-signals@5.0.0:
+ resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
+ engines: {node: '>=16.17.0'}
+ dev: true
+
/i18next-browser-languagedetector@7.2.0:
resolution: {integrity: sha512-U00DbDtFIYD3wkWsr2aVGfXGAj2TgnELzOX9qv8bT0aJtvPV9CRO77h+vgmHFBMe7LAxdwvT/7VkCWGya6L3tA==}
dependencies:
@@ -2552,6 +2750,11 @@ packages:
engines: {node: '>=8'}
dev: true
+ /is-stream@3.0.0:
+ resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dev: true
+
/isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@@ -2570,6 +2773,10 @@ packages:
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+ /js-tokens@8.0.3:
+ resolution: {integrity: sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==}
+ dev: true
+
/js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
@@ -2589,6 +2796,10 @@ packages:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
dev: true
+ /jsonc-parser@3.2.1:
+ resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==}
+ dev: true
+
/keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
dependencies:
@@ -2614,6 +2825,14 @@ packages:
/lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+ /local-pkg@0.5.0:
+ resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==}
+ engines: {node: '>=14'}
+ dependencies:
+ mlly: 1.6.1
+ pkg-types: 1.0.3
+ dev: true
+
/locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@@ -2638,6 +2857,12 @@ packages:
dependencies:
js-tokens: 4.0.0
+ /loupe@2.3.7:
+ resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
+ dependencies:
+ get-func-name: 2.0.2
+ dev: true
+
/lru-cache@10.2.0:
resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==}
engines: {node: 14 || >=16.14}
@@ -2657,6 +2882,17 @@ packages:
react: 18.2.0
dev: false
+ /magic-string@0.30.8:
+ resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.4.15
+ dev: true
+
+ /merge-stream@2.0.0:
+ resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
+ dev: true
+
/merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@@ -2668,6 +2904,11 @@ packages:
braces: 3.0.2
picomatch: 2.3.1
+ /mimic-fn@4.0.0:
+ resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
+ engines: {node: '>=12'}
+ dev: true
+
/mini-svg-data-uri@1.4.4:
resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
hasBin: true
@@ -2689,6 +2930,20 @@ packages:
resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==}
engines: {node: '>=16 || 14 >=14.17'}
+ /mlly@1.6.1:
+ resolution: {integrity: sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==}
+ dependencies:
+ acorn: 8.11.3
+ pathe: 1.1.2
+ pkg-types: 1.0.3
+ ufo: 1.5.2
+ dev: true
+
+ /mrmime@2.0.0:
+ resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==}
+ engines: {node: '>=10'}
+ dev: true
+
/ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
@@ -2719,6 +2974,10 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
+ /node-fetch-native@1.6.2:
+ resolution: {integrity: sha512-69mtXOFZ6hSkYiXAVB5SqaRvrbITC/NPyqv7yuu/qw0nmgPyYbIMYYNIDhNtwPrzk0ptrimrLz/hhjvm4w5Z+w==}
+ dev: false
+
/node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
@@ -2744,6 +3003,13 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /npm-run-path@5.3.0:
+ resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dependencies:
+ path-key: 4.0.0
+ dev: true
+
/object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@@ -2752,12 +3018,27 @@ packages:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
+ /ofetch@1.3.3:
+ resolution: {integrity: sha512-s1ZCMmQWXy4b5K/TW9i/DtiN8Ku+xCiHcjQ6/J/nDdssirrQNOoB165Zu8EqLMA2lln1JUth9a0aW9Ap2ctrUg==}
+ dependencies:
+ destr: 2.0.3
+ node-fetch-native: 1.6.2
+ ufo: 1.5.2
+ dev: false
+
/once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies:
wrappy: 1.0.2
dev: true
+ /onetime@6.0.0:
+ resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ mimic-fn: 4.0.0
+ dev: true
+
/optionator@0.9.3:
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
engines: {node: '>= 0.8.0'}
@@ -2777,6 +3058,13 @@ packages:
yocto-queue: 0.1.0
dev: true
+ /p-limit@5.0.0:
+ resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==}
+ engines: {node: '>=18'}
+ dependencies:
+ yocto-queue: 1.0.0
+ dev: true
+
/p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
@@ -2805,6 +3093,11 @@ packages:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
+ /path-key@4.0.0:
+ resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
+ engines: {node: '>=12'}
+ dev: true
+
/path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
@@ -2820,6 +3113,14 @@ packages:
engines: {node: '>=8'}
dev: true
+ /pathe@1.1.2:
+ resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
+ dev: true
+
+ /pathval@1.1.1:
+ resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
+ dev: true
+
/picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
@@ -2835,6 +3136,14 @@ packages:
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
engines: {node: '>= 6'}
+ /pkg-types@1.0.3:
+ resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==}
+ dependencies:
+ jsonc-parser: 3.2.1
+ mlly: 1.6.1
+ pathe: 1.1.2
+ dev: true
+
/postcss-import@15.1.0(postcss@8.4.36):
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
engines: {node: '>=14.0.0'}
@@ -2917,6 +3226,15 @@ packages:
hasBin: true
dev: true
+ /pretty-format@29.7.0:
+ resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ dependencies:
+ '@jest/schemas': 29.6.3
+ ansi-styles: 5.2.0
+ react-is: 18.2.0
+ dev: true
+
/punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -2963,6 +3281,10 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
+ /react-is@18.2.0:
+ resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
+ dev: true
+
/react-remove-scroll-bar@2.3.6(@types/react@18.2.64)(react@18.2.0):
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
engines: {node: '>=10'}
@@ -3125,10 +3447,23 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
+ /siginfo@2.0.0:
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+ dev: true
+
/signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
+ /sirv@2.0.4:
+ resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
+ engines: {node: '>= 10'}
+ dependencies:
+ '@polka/url': 1.0.0-next.25
+ mrmime: 2.0.0
+ totalist: 3.0.1
+ dev: true
+
/slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
@@ -3148,6 +3483,14 @@ packages:
resolution: {integrity: sha512-9vC2SfsJzlej6MAaMPLu8HiBSHGdRAJ9hVFYN1ibZoNkeanmDmLUcIrj6G9DGL7XMJ54AKg/G75akXl1/izTOw==}
engines: {node: '>=0.10.0'}
+ /stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+ dev: true
+
+ /std-env@3.7.0:
+ resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==}
+ dev: true
+
/string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@@ -3176,11 +3519,22 @@ packages:
dependencies:
ansi-regex: 6.0.1
+ /strip-final-newline@3.0.0:
+ resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
+ engines: {node: '>=12'}
+ dev: true
+
/strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
dev: true
+ /strip-literal@2.0.0:
+ resolution: {integrity: sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==}
+ dependencies:
+ js-tokens: 8.0.3
+ dev: true
+
/sucrase@3.35.0:
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -3270,12 +3624,31 @@ packages:
/tiny-warning@1.0.3:
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
+ /tinybench@2.6.0:
+ resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==}
+ dev: true
+
+ /tinypool@0.8.2:
+ resolution: {integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==}
+ engines: {node: '>=14.0.0'}
+ dev: true
+
+ /tinyspy@2.2.1:
+ resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==}
+ engines: {node: '>=14.0.0'}
+ dev: true
+
/to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
dependencies:
is-number: 7.0.0
+ /totalist@3.0.1:
+ resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
+ engines: {node: '>=6'}
+ dev: true
+
/tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
dev: false
@@ -3316,6 +3689,11 @@ packages:
prelude-ls: 1.2.1
dev: true
+ /type-detect@4.0.8:
+ resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
+ engines: {node: '>=4'}
+ dev: true
+
/type-fest@0.20.2:
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
engines: {node: '>=10'}
@@ -3327,6 +3705,10 @@ packages:
hasBin: true
dev: true
+ /ufo@1.5.2:
+ resolution: {integrity: sha512-eiutMaL0J2MKdhcOM1tUy13pIrYnyR87fEd8STJQFrrAwImwvlXkxlZEjaKah8r2viPohld08lt73QfLG1NxMg==}
+ dev: true
+
/undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
dev: true
@@ -3403,6 +3785,27 @@ packages:
- '@types/react-dom'
dev: false
+ /vite-node@1.4.0(@types/node@20.11.29):
+ resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+ dependencies:
+ cac: 6.7.14
+ debug: 4.3.4
+ pathe: 1.1.2
+ picocolors: 1.0.0
+ vite: 5.1.6(@types/node@20.11.29)
+ transitivePeerDependencies:
+ - '@types/node'
+ - less
+ - lightningcss
+ - sass
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+ dev: true
+
/vite-tsconfig-paths@4.3.2(typescript@5.2.2)(vite@5.1.6):
resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==}
peerDependencies:
@@ -3456,6 +3859,63 @@ packages:
fsevents: 2.3.3
dev: true
+ /vitest@1.4.0(@types/node@20.11.29)(@vitest/ui@1.4.0):
+ resolution: {integrity: sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@types/node': ^18.0.0 || >=20.0.0
+ '@vitest/browser': 1.4.0
+ '@vitest/ui': 1.4.0
+ happy-dom: '*'
+ jsdom: '*'
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+ dependencies:
+ '@types/node': 20.11.29
+ '@vitest/expect': 1.4.0
+ '@vitest/runner': 1.4.0
+ '@vitest/snapshot': 1.4.0
+ '@vitest/spy': 1.4.0
+ '@vitest/ui': 1.4.0(vitest@1.4.0)
+ '@vitest/utils': 1.4.0
+ acorn-walk: 8.3.2
+ chai: 4.4.1
+ debug: 4.3.4
+ execa: 8.0.1
+ local-pkg: 0.5.0
+ magic-string: 0.30.8
+ pathe: 1.1.2
+ picocolors: 1.0.0
+ std-env: 3.7.0
+ strip-literal: 2.0.0
+ tinybench: 2.6.0
+ tinypool: 0.8.2
+ vite: 5.1.6(@types/node@20.11.29)
+ vite-node: 1.4.0(@types/node@20.11.29)
+ why-is-node-running: 2.2.2
+ transitivePeerDependencies:
+ - less
+ - lightningcss
+ - sass
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+ dev: true
+
/void-elements@3.1.0:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
@@ -3479,6 +3939,15 @@ packages:
dependencies:
isexe: 2.0.0
+ /why-is-node-running@2.2.2:
+ resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==}
+ engines: {node: '>=8'}
+ hasBin: true
+ dependencies:
+ siginfo: 2.0.0
+ stackback: 0.0.2
+ dev: true
+
/wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
@@ -3536,5 +4005,30 @@ packages:
engines: {node: '>=10'}
dev: true
+ /yocto-queue@1.0.0:
+ resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
+ engines: {node: '>=12.20'}
+ dev: true
+
/zod@3.22.4:
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
+
+ /zustand@4.5.2(@types/react@18.2.64)(react@18.2.0):
+ resolution: {integrity: sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==}
+ engines: {node: '>=12.7.0'}
+ peerDependencies:
+ '@types/react': '>=16.8'
+ immer: '>=9.0.6'
+ react: '>=16.8'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+ dependencies:
+ '@types/react': 18.2.64
+ react: 18.2.0
+ use-sync-external-store: 1.2.0(react@18.2.0)
+ dev: false
diff --git a/dashboard/public/locales/en.json b/dashboard/public/locales/en.json
index f668ba9..75021a0 100644
--- a/dashboard/public/locales/en.json
+++ b/dashboard/public/locales/en.json
@@ -3,5 +3,23 @@
"users": "Users",
"nodes": "Nodes",
"services": "Services",
- "hosts": "Hosts"
+ "hosts": "Hosts",
+ "username": "Username",
+ "password": "Password",
+ "submit": "Submit",
+ "login": "Login",
+ "page": {
+ "login": {
+ "username": {
+ "error": {
+ "isRequired": "Username is required"
+ }
+ },
+ "password": {
+ "error": {
+ "is-required": "Password is required"
+ }
+ }
+ }
+ }
}
diff --git a/dashboard/src/components/ui/aspect-ratio.tsx b/dashboard/src/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..c4abbf3
--- /dev/null
+++ b/dashboard/src/components/ui/aspect-ratio.tsx
@@ -0,0 +1,5 @@
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+const AspectRatio = AspectRatioPrimitive.Root
+
+export { AspectRatio }
diff --git a/dashboard/src/components/ui/index.ts b/dashboard/src/components/ui/index.ts
index 3d2f46a..0c00d3b 100644
--- a/dashboard/src/components/ui/index.ts
+++ b/dashboard/src/components/ui/index.ts
@@ -21,3 +21,4 @@ export * from "./switch"
export * from "./table"
export * from "./toggle"
export * from "./tooltip"
+export * from "./aspect-ratio"
diff --git a/dashboard/src/features/auth/auth.store.ts b/dashboard/src/features/auth/auth.store.ts
new file mode 100644
index 0000000..5955a08
--- /dev/null
+++ b/dashboard/src/features/auth/auth.store.ts
@@ -0,0 +1,27 @@
+import { create } from 'zustand';
+import { subscribeWithSelector } from 'zustand/middleware';
+
+type AdminStateType = {
+ isLoggedIn: boolean,
+ getAuthToken: () => string | null;
+ setAuthToken: (token: string) => void;
+ removeAuthToken: () => void;
+}
+
+export const useAuth = create(
+ subscribeWithSelector<AdminStateType>((set,) => ({
+ isLoggedIn: false,
+ getAuthToken: () => {
+ return localStorage.getItem('token');
+ },
+ setAuthToken: (token: string) => {
+ localStorage.setItem('token', token);
+ set({ isLoggedIn: true });
+ },
+
+ removeAuthToken: () => {
+ localStorage.removeItem('token');
+ set({ isLoggedIn: false });
+ },
+ }))
+)
diff --git a/dashboard/src/features/auth/index.ts b/dashboard/src/features/auth/index.ts
new file mode 100644
index 0000000..08f0a6a
--- /dev/null
+++ b/dashboard/src/features/auth/index.ts
@@ -0,0 +1,2 @@
+export * from './auth.store';
+export * from './login';
diff --git a/dashboard/src/features/auth/login/form-error.tsx b/dashboard/src/features/auth/login/form-error.tsx
new file mode 100644
index 0000000..154a015
--- /dev/null
+++ b/dashboard/src/features/auth/login/form-error.tsx
@@ -0,0 +1,17 @@
+import { Alert, AlertDescription, AlertTitle } from "@marzneshin/components"
+import { cn } from "@marzneshin/utils";
+
+interface FormErrorProps {
+ className: string;
+ title: string;
+ desc: string;
+}
+
+export const FormError = ({ className, title, desc }: FormErrorProps) => {
+ return (
+ <Alert variant='destructive' className={cn(className, "p-3 bg-destructive-accent")}>
+ <AlertTitle className="font-semibold font-header">{title}</AlertTitle>
+ <AlertDescription>{desc}</AlertDescription>
+ </Alert>
+ )
+}
diff --git a/dashboard/src/features/auth/login/form.tsx b/dashboard/src/features/auth/login/form.tsx
new file mode 100644
index 0000000..04a1f61
--- /dev/null
+++ b/dashboard/src/features/auth/login/form.tsx
@@ -0,0 +1,75 @@
+
+import { fetch } from "@marzneshin/utils"
+import { LoginSchema, useAuth } from "@marzneshin/features/auth";
+import { useNavigate } from "@tanstack/react-router";
+import { useState } from "react";
+import { FieldValues, useForm } from "react-hook-form";
+import { zodResolver } from '@hookform/resolvers/zod';
+import { Button, Form, FormControl, FormField, FormItem, FormLabel, FormMessage, Input } from "@marzneshin/components";
+import { useTranslation } from "react-i18next";
+import { FormError } from "./form-error";
+
+export const LoginForm = () => {
+ const { setAuthToken } = useAuth();
+ const form = useForm({
+ resolver: zodResolver(LoginSchema),
+ defaultValues: {
+ username: '',
+ password: '',
+ }
+ })
+ const navigate = useNavigate({ from: '/login' });
+ const [error, setError] = useState<string>('');
+ const { t } = useTranslation();
+
+ const submit = (values: FieldValues) => {
+ setError('');
+ const formData = new FormData();
+ formData.append('username', values.username);
+ formData.append('password', values.password);
+ formData.append('grant_type', 'password');
+ fetch('/admins/token', { method: 'post', body: formData })
+ .then(({ access_token: token }) => {
+ setAuthToken(token);
+ navigate({ to: '/' });
+ })
+ .catch((err) => {
+ setError(err.response._data?.detail);
+ });
+ };
+
+ return (
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(submit)}>
+ <FormField
+ control={form.control}
+ name="username"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="font-semibold text-primary">{t('username')}</FormLabel>
+ <FormControl>
+ <Input placeholder="username" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="password"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="font-semibold text-primary">{t('password')}</FormLabel>
+ <FormControl>
+ <Input placeholder="password" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <Button className="mt-3 w-full" type="submit">{t('login')}</Button>
+ {error && <FormError className="mt-2" title='Submission failed' desc={error} />}
+ </form>
+ </Form>
+ )
+}
diff --git a/dashboard/src/features/auth/login/index.ts b/dashboard/src/features/auth/login/index.ts
new file mode 100644
index 0000000..3e72409
--- /dev/null
+++ b/dashboard/src/features/auth/login/index.ts
@@ -0,0 +1,2 @@
+export * from './form';
+export * from './schema';
diff --git a/dashboard/src/features/auth/login/schema.ts b/dashboard/src/features/auth/login/schema.ts
new file mode 100644
index 0000000..fe1b598
--- /dev/null
+++ b/dashboard/src/features/auth/login/schema.ts
@@ -0,0 +1,5 @@
+import { z } from 'zod';
+export const LoginSchema = z.object({
+ username: z.string().nonempty('Username is required'),
+ password: z.string().nonempty('Password is required'),
+});
diff --git a/dashboard/src/globals.css b/dashboard/src/globals.css
index 6a75725..62bcaa8 100644
--- a/dashboard/src/globals.css
+++ b/dashboard/src/globals.css
@@ -1,7 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
-
+
@layer base {
:root {
--background: 0 0% 100%;
@@ -9,68 +9,82 @@
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
-
+
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
-
+
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
-
+
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
-
+
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
-
+
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
-
+
--destructive: 0 84.2% 60.2%;
+ --destructive-accent: 0 27% 100%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
-
+
--radius: 0.5rem;
}
-
+
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
-
+
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
-
+
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
-
+
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
-
+
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
-
+
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
-
+
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
-
+
--destructive: 0 62.8% 30.6%;
+ --destructive-accent: 0 55% 57%;
--destructive-foreground: 210 40% 98%;
-
+
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
+
+
}
-
+
+
@layer base {
+ .font-body {
+ font-family: 'Lato';
+ }
+
+ .font-header {
+ font-family: 'Ubuntu';
+ }
+
* {
- @apply border-border;
+ @apply border-border font-body;
}
+
body {
@apply bg-background text-foreground;
}
-}
\ No newline at end of file
+}
diff --git a/dashboard/src/routeTree.gen.ts b/dashboard/src/routeTree.gen.ts
index 16a118c..eb5d344 100644
--- a/dashboard/src/routeTree.gen.ts
+++ b/dashboard/src/routeTree.gen.ts
@@ -8,18 +8,13 @@
// This file is auto-generated by TanStack Router
-import { createFileRoute } from '@tanstack/react-router'
-
// Import Routes
import { Route as rootRoute } from './routes/__root'
import { Route as DashboardImport } from './routes/_dashboard'
import { Route as AuthImport } from './routes/_auth'
import { Route as DashboardIndexImport } from './routes/_dashboard/index'
-
-// Create Virtual Routes
-
-const IndexLazyImport = createFileRoute('/')()
+import { Route as AuthLoginImport } from './routes/_auth/login'
// Create/Update Routes
@@ -33,24 +28,20 @@ const AuthRoute = AuthImport.update({
getParentRoute: () => rootRoute,
} as any)
-const IndexLazyRoute = IndexLazyImport.update({
- path: '/',
- getParentRoute: () => rootRoute,
-} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))
-
const DashboardIndexRoute = DashboardIndexImport.update({
path: '/',
getParentRoute: () => DashboardRoute,
} as any)
+const AuthLoginRoute = AuthLoginImport.update({
+ path: '/login',
+ getParentRoute: () => AuthRoute,
+} as any)
+
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
- '/': {
- preLoaderRoute: typeof IndexLazyImport
- parentRoute: typeof rootRoute
- }
'/_auth': {
preLoaderRoute: typeof AuthImport
parentRoute: typeof rootRoute
@@ -59,6 +50,10 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DashboardImport
parentRoute: typeof rootRoute
}
+ '/_auth/login': {
+ preLoaderRoute: typeof AuthLoginImport
+ parentRoute: typeof AuthImport
+ }
'/_dashboard/': {
preLoaderRoute: typeof DashboardIndexImport
parentRoute: typeof DashboardImport
@@ -69,7 +64,7 @@ declare module '@tanstack/react-router' {
// Create and export the route tree
export const routeTree = rootRoute.addChildren([
- IndexLazyRoute,
+ AuthRoute.addChildren([AuthLoginRoute]),
DashboardRoute.addChildren([DashboardIndexRoute]),
])
diff --git a/dashboard/src/routes/_auth.tsx b/dashboard/src/routes/_auth.tsx
index da57e82..b9aa0eb 100644
--- a/dashboard/src/routes/_auth.tsx
+++ b/dashboard/src/routes/_auth.tsx
@@ -1,5 +1,16 @@
-import { createFileRoute } from '@tanstack/react-router'
+import { Outlet, createFileRoute } from '@tanstack/react-router'
+
+const AuthLayout = () => {
+ return (
+ <div className='grid-cols-2 w-screen h-screen md:grid bg-primary md:bg-primary-foreground'>
+ <div className="w-full h-full">
+ <Outlet />
+ </div>
+ <div className='hidden w-full h-full md:block bg-primary'></div>
+ </div>
+ )
+}
export const Route = createFileRoute('/_auth')({
- component: () => <div>Hello /_auth!</div>
-})
\ No newline at end of file
+ component: () => <AuthLayout />,
+})
diff --git a/dashboard/src/routes/_auth/login.tsx b/dashboard/src/routes/_auth/login.tsx
new file mode 100644
index 0000000..d4c89a9
--- /dev/null
+++ b/dashboard/src/routes/_auth/login.tsx
@@ -0,0 +1,27 @@
+import { Card, CardContent, CardHeader, CardTitle } from '@marzneshin/components';
+import { LoginForm } from '@marzneshin/features/auth';
+import { createFileRoute } from '@tanstack/react-router'
+import { FC } from 'react'
+import { useTranslation } from 'react-i18next';
+
+const LoginPage: FC = () => {
+ const { t } = useTranslation();
+ return (
+ <div className='flex flex-row justify-center items-center p-4 w-full h-full'>
+ <Card className="p-1 w-full md:w-2/3">
+ <CardHeader>
+ <CardTitle className="font-header text-primary">
+ {t('login')}
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <LoginForm />
+ </CardContent>
+ </Card>
+ </div>
+ );
+};
+
+export const Route = createFileRoute('/_auth/login')({
+ component: () => <LoginPage />
+})
diff --git a/dashboard/src/routes/_dashboard.tsx b/dashboard/src/routes/_dashboard.tsx
index d7997a5..857bbc9 100644
--- a/dashboard/src/routes/_dashboard.tsx
+++ b/dashboard/src/routes/_dashboard.tsx
@@ -1,5 +1,14 @@
-import { createFileRoute } from '@tanstack/react-router'
+import { useAuth } from '@marzneshin/features/auth'
+import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/_dashboard')({
- component: () => <div>Hello /_dashboard!</div>
-})
\ No newline at end of file
+ component: () => <div>Hello /_dashboard!</div>,
+
+ beforeLoad: async () => {
+ if (!useAuth.getState().isLoggedIn) {
+ throw redirect({
+ to: '/login',
+ })
+ }
+ }
+})
diff --git a/dashboard/src/routes/_dashboard/index.tsx b/dashboard/src/routes/_dashboard/index.tsx
index 14dc0a7..4c7427a 100644
--- a/dashboard/src/routes/_dashboard/index.tsx
+++ b/dashboard/src/routes/_dashboard/index.tsx
@@ -2,4 +2,4 @@ import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_dashboard/')({
component: () => <div>Hello /_dashboard/!</div>
-})
\ No newline at end of file
+})
diff --git a/dashboard/src/routes/index.lazy.tsx b/dashboard/src/routes/index.lazy.tsx
deleted file mode 100644
index 7f957cd..0000000
--- a/dashboard/src/routes/index.lazy.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Button } from '@marzneshin/components'
-import { ThemeToggle } from '@marzneshin/features/theme-switch'
-import { createLazyFileRoute } from '@tanstack/react-router'
-import { useTranslation } from 'react-i18next';
-
-export const Route = createLazyFileRoute('/')({
- component: Index,
-})
-
-function Index() {
- const { t } = useTranslation();
- return (
- <div className="p-2">
- <ThemeToggle />
- <Button>{t('name')}</Button>
- </div>
- )
-}
diff --git a/dashboard/src/utils/fetch.ts b/dashboard/src/utils/fetch.ts
new file mode 100644
index 0000000..15d588f
--- /dev/null
+++ b/dashboard/src/utils/fetch.ts
@@ -0,0 +1,31 @@
+
+import { FetchOptions, ofetch } from 'ofetch';
+import { useAuth } from '@marzneshin/features/auth';
+
+export const $fetch = ofetch.create({
+ baseURL: import.meta.env.VITE_BASE_API,
+});
+
+export const fetcher = <T = any>(
+ url: string,
+ ops: FetchOptions<'json'> = {}
+) => {
+ const token = useAuth.getState().getAuthToken();
+ if (token) {
+ ops['headers'] = {
+ ...(ops?.headers || {}),
+ Authorization: `Bearer ${token}`,
+ };
+ }
+ return $fetch<T>(url, ops);
+};
+
+export const fetch = fetcher;
+
+export const fetchAdminLoader = () => {
+ return fetch('/admins', {
+ headers: {
+ Authorization: `Bearer ${useAuth.getState().getAuthToken()}`,
+ },
+ });
+};
diff --git a/dashboard/src/utils/index.ts b/dashboard/src/utils/index.ts
index c5c1882..3adb899 100644
--- a/dashboard/src/utils/index.ts
+++ b/dashboard/src/utils/index.ts
@@ -1 +1,2 @@
export * from './cn'
+export * from './fetch'
diff --git a/dashboard/tailwind.config.js b/dashboard/tailwind.config.js
index 30101d0..5085b7c 100644
--- a/dashboard/tailwind.config.js
+++ b/dashboard/tailwind.config.js
@@ -39,6 +39,7 @@ const config = {
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
+ accent: "hsl(var(--destructive-accent))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
@@ -76,6 +77,10 @@ const config = {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
+ fontFamily: {
+ 'font-body': ['Lato'],
+ 'font-header': ['Ubuntu'],
+ }
},
},
plugins: [