10 Commits
Author SHA1 Message Date
phoenix 5b979c2890 Adding license (#20)
Reviewed-on: #20
2026-07-14 15:40:32 -04:00
phoenix 5c498310fb tsk-18: Updating name (#19)
Closes #18

Reviewed-on: phoenix/textsender#19
2026-07-13 18:17:09 -04:00
phoenix 291cd3c2b1 tsk-10: View scheduled messages (#17)
Closes #10

Reviewed-on: phoenix/textsender#17
2026-07-04 19:39:12 -04:00
phoenix 07f9db2298 tsk-2: Blank screen bug fix (#16)
Closes #2

Reviewed-on: phoenix/textsender#16
2026-07-04 16:56:35 -04:00
phoenix 7a357f6108 tsk-11: Make URLs more dynamic (#15)
Closes #11

Reviewed-on: phoenix/textsender#15
2026-07-04 16:25:45 -04:00
phoenix 53b80e3d2c tsk-4: Fix contacts not being saved (#14)
Closes #4

Reviewed-on: phoenix/textsender#14
2026-07-04 10:53:33 -04:00
phoenix cde21b09c8 tsk-6: Add feature to update password in profile (#13)
Closes #6

Reviewed-on: phoenix/textsender#13
2026-07-02 23:24:31 -04:00
phoenix be1238e24d tsk-5: Add profile page (#12)
Closes #5

Reviewed-on: phoenix/textsender#12
2026-07-01 23:56:22 -04:00
phoenix f2bdb60d55 tsk-7: Schedule message (#8)
Closes #7

Reviewed-on: phoenix/textsender#8
2026-06-30 12:13:52 -04:00
phoenix 833a044ff6 Updating packages (#1)
Reviewed-on: phoenix/textsender#1
2026-06-28 21:52:54 -04:00
33 changed files with 1460 additions and 593 deletions
+2
View File
@@ -0,0 +1,2 @@
VITE_AUTH_URL="http://localhost:9080"
VITE_API_URL="http://localhost:9081"
+2
View File
@@ -0,0 +1,2 @@
VITE_AUTH_URL="http://localhost:9080"
VITE_API_URL="http://localhost:9081"
+4
View File
@@ -22,3 +22,7 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# Environment
.env.production
.env.development
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) 2026 Kun Deng.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
+2 -11
View File
@@ -1,15 +1,6 @@
# React + Vite
# schedtxt
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
Frontend web app used to send messages
# Getting started
+1 -2
View File
@@ -2,9 +2,8 @@
<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</title>
<title>schedtxt</title>
</head>
<body>
<div id="root"></div>
+270 -251
View File
@@ -1,37 +1,38 @@
{
"name": "textsender",
"version": "0.0.4",
"name": "schedtxt",
"version": "0.0.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "textsender",
"version": "0.0.4",
"name": "schedtxt",
"version": "0.0.11",
"dependencies": {
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-router-dom": "^7.14.0"
"dotenv": "^17.4.2",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-router-dom": "^7.18.0"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@types/react": "^19.2.14",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"@vitejs/plugin-react": "^6.0.3",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.5.0",
"vite": "^8.0.8"
"vite": "^8.1.0"
}
},
"node_modules/@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.28.5",
"@babel/helper-validator-identifier": "^7.29.7",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
@@ -40,9 +41,9 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -50,21 +51,21 @@
}
},
"node_modules/@babel/core": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-module-transforms": "^7.28.6",
"@babel/helpers": "^7.28.6",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/traverse": "^7.29.0",
"@babel/types": "^7.29.0",
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
"@babel/helper-compilation-targets": "^7.29.7",
"@babel/helper-module-transforms": "^7.29.7",
"@babel/helpers": "^7.29.7",
"@babel/parser": "^7.29.7",
"@babel/template": "^7.29.7",
"@babel/traverse": "^7.29.7",
"@babel/types": "^7.29.7",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -81,14 +82,14 @@
}
},
"node_modules/@babel/generator": {
"version": "7.29.1",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
"integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.0",
"@babel/types": "^7.29.0",
"@babel/parser": "^7.29.7",
"@babel/types": "^7.29.7",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -98,14 +99,14 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.28.6",
"@babel/helper-validator-option": "^7.27.1",
"@babel/compat-data": "^7.29.7",
"@babel/helper-validator-option": "^7.29.7",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
@@ -115,9 +116,9 @@
}
},
"node_modules/@babel/helper-globals": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
"integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -125,29 +126,29 @@
}
},
"node_modules/@babel/helper-module-imports": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
"integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.28.6",
"@babel/types": "^7.28.6"
"@babel/traverse": "^7.29.7",
"@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
"integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.28.6",
"@babel/helper-validator-identifier": "^7.28.5",
"@babel/traverse": "^7.28.6"
"@babel/helper-module-imports": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7",
"@babel/traverse": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
@@ -157,9 +158,9 @@
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -167,9 +168,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -177,9 +178,9 @@
}
},
"node_modules/@babel/helper-validator-option": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -187,27 +188,27 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
"integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.28.6",
"@babel/types": "^7.29.0"
"@babel/template": "^7.29.7",
"@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
"integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.29.0"
"@babel/types": "^7.29.7"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -217,33 +218,33 @@
}
},
"node_modules/@babel/template": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/parser": "^7.28.6",
"@babel/types": "^7.28.6"
"@babel/code-frame": "^7.29.7",
"@babel/parser": "^7.29.7",
"@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
"integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/types": "^7.29.0",
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
"@babel/helper-globals": "^7.29.7",
"@babel/parser": "^7.29.7",
"@babel/template": "^7.29.7",
"@babel/types": "^7.29.7",
"debug": "^4.3.1"
},
"engines": {
@@ -251,35 +252,35 @@
}
},
"node_modules/@babel/types": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
"@babel/helper-string-parser": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"@emnapi/wasi-threads": "1.2.2",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -288,9 +289,9 @@
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -558,14 +559,14 @@
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz",
"integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==",
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
"integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.1"
"@tybys/wasm-util": "^0.10.3"
},
"funding": {
"type": "github",
@@ -577,9 +578,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.124.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
"integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
"version": "0.137.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz",
"integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -587,9 +588,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
"integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz",
"integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==",
"cpu": [
"arm64"
],
@@ -604,9 +605,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz",
"integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz",
"integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==",
"cpu": [
"arm64"
],
@@ -621,9 +622,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz",
"integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz",
"integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==",
"cpu": [
"x64"
],
@@ -638,9 +639,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz",
"integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz",
"integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==",
"cpu": [
"x64"
],
@@ -655,9 +656,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz",
"integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz",
"integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==",
"cpu": [
"arm"
],
@@ -672,9 +673,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz",
"integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz",
"integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==",
"cpu": [
"arm64"
],
@@ -692,9 +693,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz",
"integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz",
"integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==",
"cpu": [
"arm64"
],
@@ -712,9 +713,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz",
"integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz",
"integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==",
"cpu": [
"ppc64"
],
@@ -732,9 +733,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz",
"integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz",
"integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==",
"cpu": [
"s390x"
],
@@ -752,9 +753,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz",
"integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz",
"integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==",
"cpu": [
"x64"
],
@@ -772,9 +773,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz",
"integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz",
"integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==",
"cpu": [
"x64"
],
@@ -792,9 +793,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz",
"integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz",
"integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==",
"cpu": [
"arm64"
],
@@ -809,9 +810,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz",
"integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz",
"integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==",
"cpu": [
"wasm32"
],
@@ -819,18 +820,18 @@
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "1.9.2",
"@emnapi/runtime": "1.9.2",
"@napi-rs/wasm-runtime": "^1.1.3"
"@emnapi/core": "1.11.1",
"@emnapi/runtime": "1.11.1",
"@napi-rs/wasm-runtime": "^1.1.6"
},
"engines": {
"node": ">=14.0.0"
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
"integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz",
"integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==",
"cpu": [
"arm64"
],
@@ -845,9 +846,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
"integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz",
"integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==",
"cpu": [
"x64"
],
@@ -862,16 +863,16 @@
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.7",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
"integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true,
"license": "MIT"
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
"integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -894,9 +895,9 @@
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"version": "19.2.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -914,13 +915,13 @@
}
},
"node_modules/@vitejs/plugin-react": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
"integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz",
"integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rolldown/pluginutils": "1.0.0-rc.7"
"@rolldown/pluginutils": "^1.0.1"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
@@ -1010,9 +1011,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.18",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz",
"integrity": "sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==",
"version": "2.10.40",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz",
"integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -1034,9 +1035,9 @@
}
},
"node_modules/browserslist": {
"version": "4.28.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
"integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
"version": "4.28.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz",
"integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==",
"dev": true,
"funding": [
{
@@ -1054,10 +1055,10 @@
],
"license": "MIT",
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
"electron-to-chromium": "^1.5.328",
"node-releases": "^2.0.36",
"baseline-browser-mapping": "^2.10.38",
"caniuse-lite": "^1.0.30001799",
"electron-to-chromium": "^1.5.376",
"node-releases": "^2.0.48",
"update-browserslist-db": "^1.2.3"
},
"bin": {
@@ -1078,9 +1079,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001787",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz",
"integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==",
"version": "1.0.30001799",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
"integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
"dev": true,
"funding": [
{
@@ -1219,10 +1220,22 @@
"node": ">=8"
}
},
"node_modules/dotenv": {
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.335",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz",
"integrity": "sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==",
"version": "1.5.380",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.380.tgz",
"integrity": "sha512-W6d5AbuEoRayO447cqrg6lKJIlscgRnnxOZl/08kfV71BQDoEBC7Wwis68z87LjyK6f4kWyTaubuDbhHKrZkbA==",
"dev": true,
"license": "ISC"
},
@@ -1676,10 +1689,20 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -2086,9 +2109,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"version": "3.3.15",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
"dev": true,
"funding": [
{
@@ -2112,11 +2135,14 @@
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.37",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
"integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
"version": "2.0.50",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz",
"integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==",
"dev": true,
"license": "MIT"
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/optionator": {
"version": "0.9.4",
@@ -2222,9 +2248,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.9",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
"integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
"version": "8.5.16",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
"dev": true,
"funding": [
{
@@ -2242,7 +2268,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -2271,30 +2297,30 @@
}
},
"node_modules/react": {
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
"integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.5"
"react": "^19.2.7"
}
},
"node_modules/react-router": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz",
"integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==",
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz",
"integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
@@ -2314,12 +2340,12 @@
}
},
"node_modules/react-router-dom": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz",
"integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==",
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz",
"integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==",
"license": "MIT",
"dependencies": {
"react-router": "7.14.0"
"react-router": "7.18.0"
},
"engines": {
"node": ">=20.0.0"
@@ -2340,14 +2366,14 @@
}
},
"node_modules/rolldown": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
"integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz",
"integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.124.0",
"@rolldown/pluginutils": "1.0.0-rc.15"
"@oxc-project/types": "=0.137.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
"rolldown": "bin/cli.mjs"
@@ -2356,30 +2382,23 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-rc.15",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.15",
"@rolldown/binding-darwin-x64": "1.0.0-rc.15",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.15",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.15",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.15",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.15",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
"@rolldown/binding-android-arm64": "1.1.3",
"@rolldown/binding-darwin-arm64": "1.1.3",
"@rolldown/binding-darwin-x64": "1.1.3",
"@rolldown/binding-freebsd-x64": "1.1.3",
"@rolldown/binding-linux-arm-gnueabihf": "1.1.3",
"@rolldown/binding-linux-arm64-gnu": "1.1.3",
"@rolldown/binding-linux-arm64-musl": "1.1.3",
"@rolldown/binding-linux-ppc64-gnu": "1.1.3",
"@rolldown/binding-linux-s390x-gnu": "1.1.3",
"@rolldown/binding-linux-x64-gnu": "1.1.3",
"@rolldown/binding-linux-x64-musl": "1.1.3",
"@rolldown/binding-openharmony-arm64": "1.1.3",
"@rolldown/binding-wasm32-wasi": "1.1.3",
"@rolldown/binding-win32-arm64-msvc": "1.1.3",
"@rolldown/binding-win32-x64-msvc": "1.1.3"
}
},
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
"integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
"dev": true,
"license": "MIT"
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -2462,9 +2481,9 @@
}
},
"node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2541,17 +2560,17 @@
}
},
"node_modules/vite": {
"version": "8.0.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.0.tgz",
"integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.8",
"rolldown": "1.0.0-rc.15",
"tinyglobby": "^0.2.15"
"postcss": "^8.5.15",
"rolldown": "~1.1.2",
"tinyglobby": "^0.2.17"
},
"bin": {
"vite": "bin/vite.js"
@@ -2567,7 +2586,7 @@
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.1.0",
"@vitejs/devtools": "^0.3.0",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
+9 -8
View File
@@ -1,7 +1,7 @@
{
"name": "textsender",
"name": "schedtxt",
"private": true,
"version": "0.0.4",
"version": "0.0.11",
"type": "module",
"scripts": {
"dev": "vite",
@@ -12,19 +12,20 @@
"format:check": "npx prettier --check ."
},
"dependencies": {
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-router-dom": "^7.14.0"
"dotenv": "^17.4.2",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-router-dom": "^7.18.0"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@types/react": "^19.2.14",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"@vitejs/plugin-react": "^6.0.3",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.5.0",
"vite": "^8.0.8"
"vite": "^8.1.0"
}
}
View File
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

+2
View File
@@ -8,6 +8,7 @@ import {
import Dashboard from './components/Dashboard';
import Login from './components/Login';
import Register from './components/Register';
import ProfileForm from './components/ProfileForm';
import './App.css';
@@ -19,6 +20,7 @@ function App() {
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/user/profile" element={<ProfileForm />} />
</Routes>
</Router>
);
+6 -5
View File
@@ -1,17 +1,18 @@
import { API_BASE_URL } from '../constants/api';
export class CreateContactRequest {
first_name = '';
last_name = '';
firstname = '';
lastname = '';
nickname = '';
phone_number = '';
user_id = '';
}
export class UpdateContactNamesRequest {
first_name = '';
last_name = '';
firstname = '';
lastname = '';
nickname = '';
contact_id = '';
id = '';
user_id = '';
}
+47
View File
@@ -20,3 +20,50 @@ export const loginApi = {
}
},
};
export const userApi = {
get_user_profile: async (userId) => {
const reponse = await fetch(
`${AUTH_API_BASE_URL}/api/v1/user/profile/${userId}`,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
);
if (reponse.ok) {
return reponse.json();
} else {
throw new Error('Error logging in');
}
},
update_password: async (
userId,
currentPassword,
updatedPassword,
confirmedPassword
) => {
const request = {
user_id: userId,
current_password: currentPassword,
updated_password: updatedPassword,
confirmed_password: confirmedPassword,
};
const reponse = await fetch(
`${AUTH_API_BASE_URL}/api/v1/user/password/update`,
{
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
}
);
if (reponse.ok) {
return reponse.json();
} else {
throw new Error('Error logging in');
}
},
};
+63
View File
@@ -0,0 +1,63 @@
import { API_BASE_URL } from '../constants/api';
export const scheduleApi = {
scheduleMessage: async (reqBody, authBearerToken) => {
console.log('Scheduling message');
const response = await fetch(`${API_BASE_URL}/api/v1/schedule/message`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: authBearerToken,
},
body: JSON.stringify(reqBody),
});
if (!response.ok) {
throw new Error('Error scheduling message');
} else {
return response.json();
}
},
getScheduledMessage: async (userId, authBearerToken) => {
const response = await fetch(
`${API_BASE_URL}/api/v1/schedule/message?user_id=${userId}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: authBearerToken,
},
}
);
if (!response.ok) {
throw new Error('Error getting scheduled message');
} else {
return response.json();
}
},
prepareMessage: async (reqBody, authBearerToken) => {
console.log('Preparing Scheduled message');
const response = await fetch(
`${API_BASE_URL}/api/v1/schedule/message/status/update`,
{
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Authorization: authBearerToken,
},
body: JSON.stringify(reqBody),
}
);
if (!response.ok) {
throw new Error('Error updating status of scheduled message');
} else {
return response.json();
}
},
};
+25
View File
@@ -0,0 +1,25 @@
import { API_BASE_URL } from '../constants/api';
export const scheduleEventApi = {
scheduleMessageEvent: async (reqBody, authBearerToken) => {
console.log('Scheduling message Event');
const response = await fetch(
`${API_BASE_URL}/api/v1/schedule/message/event`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: authBearerToken,
},
body: JSON.stringify(reqBody),
}
);
if (!response.ok) {
throw new Error('Error scheduling message event');
} else {
return response.json();
}
},
};
+21 -2
View File
@@ -11,6 +11,7 @@ const AddContact = ({ isOpen, onClose, onSuccess }) => {
phoneNumber: '',
firstName: '',
lastName: '',
nickName: '',
});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -66,8 +67,9 @@ const AddContact = ({ isOpen, onClose, onSuccess }) => {
const userId = tokenService.getUserId();
let contact = new CreateContactRequest();
contact.phone_number = formData.phoneNumber;
contact.first_name = formData.firstName;
contact.last_name = formData.lastName;
contact.firstname = formData.firstName;
contact.lastname = formData.lastName;
contact.nickname = formData.nickName;
contact.user_id = userId;
const result = await contactApi.createContact(
contact,
@@ -178,6 +180,23 @@ const AddContact = ({ isOpen, onClose, onSuccess }) => {
<span className="form-hint">Optional field</span>
</div>
<div className="form-group">
<label htmlFor="nickName" className="form-label">
Nick Name
</label>
<input
id="nickName"
name="nickName"
type="text"
className="form-input"
value={formData.nickName}
onChange={handleChange}
placeholder="Enter nick name (optional)"
disabled={isSubmitting}
/>
<span className="form-hint">Optional field</span>
</div>
<div className="form-actions">
<button
type="button"
+10 -21
View File
@@ -1,20 +1,22 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import AddContact from './AddContact';
// import ViewContact from './ViewContact';
import ViewSentMessages from './ViewSentMessages';
import './Dashboard.css';
import SendMessageWizardModal from './SendMessageWizard/SendMessageWizardModal';
import ViewContactWizardModal from './ViewContactWizard/ViewContactWizardModal';
import { APP_NAME } from '../constants/app';
const Dashboard = () => {
const navigate = useNavigate();
const [activeView, setActiveView] = useState('dashboard');
const [isWizardOpen, setIsWizardOpen] = useState(false);
const [isViewContactWizardOpen, setIsViewContactWizardOpen] = useState(false);
const [isViewSentMessagesOpen, setViewSentMessagesOpen] = useState(false);
const [showContactForm, setShowContactForm] = useState(false);
// const [isPopupOpen, setIsPopupOpen] = useState(false);
const handleAddContactClick = () => {
setShowContactForm(true);
@@ -22,15 +24,9 @@ const Dashboard = () => {
const handleContactSuccess = (contactData) => {
console.log('Contact created successfully:', contactData);
// You could:
// 1. Refresh the contacts list if you have one
// 2. Show a success notification
// 3. Update local state with the new contact
};
const handleWizardComplete = () => {
// Add your logic here for each button
setIsWizardOpen(false);
};
@@ -47,7 +43,11 @@ const Dashboard = () => {
};
const handleNavClick = (view) => {
setActiveView(view);
if (view === 'profile') {
navigate('/user/profile');
} else {
setActiveView(view);
}
};
return (
@@ -55,7 +55,7 @@ const Dashboard = () => {
{/* Navbar */}
<nav className="dashboard-nav">
<div className="nav-logo">
<span className="logo-text">Dashboard</span>
<span className="logo-text">{APP_NAME}</span>
</div>
<div className="nav-links">
<button
@@ -64,12 +64,6 @@ const Dashboard = () => {
>
Dashboard
</button>
<button
className={`nav-link ${activeView === 'settings' ? 'active' : ''}`}
onClick={() => handleNavClick('settings')}
>
Settings
</button>
<button
className={`nav-link ${activeView === 'profile' ? 'active' : ''}`}
onClick={() => handleNavClick('profile')}
@@ -161,11 +155,6 @@ const Dashboard = () => {
Dashboard view is active. Select one of the buttons above.
</p>
)}
{activeView === 'settings' && (
<p className="view-text">
Settings view - Configuration options will go here.
</p>
)}
{activeView === 'profile' && (
<p className="view-text">
Profile view - User information will go here.
+95
View File
@@ -0,0 +1,95 @@
.profile-form {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 40px;
width: 100%;
max-width: 420px;
margin: 0 auto;
}
.profile-form h2 {
color: #333;
font-size: 28px;
font-weight: 600;
text-align: center;
margin-bottom: 30px;
}
.profile-form form {
margin-bottom: 25px;
}
.profile-form div {
margin-bottom: 20px;
}
.profile-form label {
display: block;
color: #555;
font-size: 14px;
font-weight: 500;
margin-bottom: 6px;
}
.profile-form input[type='text'],
.profile-form input[type='tel'],
.profile-form input[type='password'] {
width: 100%;
padding: 12px 16px;
border: 2px solid #e1e5e9;
border-radius: 8px;
font-size: 16px;
transition: all 0.3s ease;
box-sizing: border-box;
}
.profile-form input[type='text']:focus,
.profile-form input[type='tel']:focus,
.profile-form input[type='password']:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.profile-form input[readonly] {
background-color: #f9fafb;
cursor: not-allowed;
}
.profile-form button[type='submit'] {
width: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
padding: 14px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 10px;
}
.profile-form button[type='submit']:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
}
.profile-form button[type='button'] {
width: 100%;
background: #f1f5f9;
color: #334155;
border: 1px solid #cbd5e1;
border-radius: 8px;
padding: 14px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 10px;
}
.profile-form button[type='button']:hover {
background: #e2e8f0;
}
+252
View File
@@ -0,0 +1,252 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { tokenService } from '../services/TokenService';
import { userApi } from '../api/loginApi';
import { APP_NAME } from '../constants/app';
import './ProfileForm.css';
const ProfileForm = () => {
const navigate = useNavigate();
const [activeView, setActiveView] = useState('profile');
const [subView, setSubView] = useState('details');
const [formData, setFormData] = useState({
firstname: '',
lastname: '',
phone: '',
username: '',
createdDate: new Date().toISOString().split('T')[0],
lastLoginDate: new Date().toISOString().split('T')[0],
});
const [passwordData, setPasswordData] = useState({
currentPassword: '',
newPassword: '',
confirmNewPassword: '',
});
useEffect(() => {
const fetchData = async () => {
const userId = tokenService.getUserId();
const response = await userApi.get_user_profile(userId);
if (response) {
const userProfile = response.data[0];
setFormData((prev) => ({
...prev,
firstname: userProfile.firstname,
lastname: userProfile.lastname,
phone: userProfile.phone_number,
username: userProfile.username,
createdDate: userProfile.created,
lastLoginDate: userProfile.last_login,
}));
} else {
console.error('Error getting user profile');
}
};
fetchData();
}, []);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
const handlePasswordChange = (e) => {
const { name, value } = e.target;
setPasswordData((prev) => ({
...prev,
[name]: value,
}));
};
const handleNavClick = (view) => {
if (view === 'profile') {
navigate('/user/profile');
} else {
navigate(`/${view}`);
}
};
const handlePasswordSubmit = async (e) => {
e.preventDefault();
try {
const userId = tokenService.getUserId();
const currentPassword = passwordData.currentPassword;
const newPassword = passwordData.newPassword;
const confirmNewPassword = passwordData.confirmNewPassword;
const response = await userApi.update_password(
userId,
currentPassword,
newPassword,
confirmNewPassword
);
if (response) {
console.log('Successfully updated password');
} else {
console.error('Error updating password');
}
} catch (error) {
console.error('Login error:', error);
} finally {
}
};
const renderPasswordForm = () => (
<div className="profile-form">
<h2>Change Password</h2>
<form onSubmit={handlePasswordSubmit}>
<div>
<label htmlFor="currentPassword">Current Password</label>
<input
type="password"
id="currentPassword"
name="currentPassword"
value={passwordData.currentPassword}
onChange={handlePasswordChange}
required
/>
</div>
<div>
<label htmlFor="newPassword">New Password</label>
<input
type="password"
id="newPassword"
name="newPassword"
value={passwordData.newPassword}
onChange={handlePasswordChange}
required
/>
</div>
<div>
<label htmlFor="confirmNewPassword">Confirm New Password</label>
<input
type="password"
id="confirmNewPassword"
name="confirmNewPassword"
value={passwordData.confirmNewPassword}
onChange={handlePasswordChange}
required
/>
</div>
<button type="submit">Update Password</button>
<button type="button" onClick={() => setSubView('details')}>
Back to Profile
</button>
</form>
</div>
);
const renderProfileForm = () => (
<div className="profile-form">
<h2>User Profile</h2>
<form>
<div>
<label htmlFor="firstname">First Name</label>
<input
type="text"
id="firstname"
name="firstname"
value={formData.firstname}
onChange={handleChange}
readOnly
/>
</div>
<div>
<label htmlFor="lastname">Last Name</label>
<input
type="text"
id="lastname"
name="lastname"
value={formData.lastname}
onChange={handleChange}
readOnly
/>
</div>
<div>
<label htmlFor="phone">Phone</label>
<input
type="tel"
id="phone"
name="phone"
value={formData.phone}
onChange={handleChange}
readOnly
/>
</div>
<div>
<label htmlFor="username">Username</label>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
readOnly
/>
</div>
<div>
<label htmlFor="createdDate">Created Date</label>
<input
type="text"
id="createdDate"
name="createdDate"
value={formData.createdDate}
readOnly
/>
</div>
<div>
<label htmlFor="lastLoginDate">Last Login Date</label>
<input
type="text"
id="lastLoginDate"
name="lastLoginDate"
value={formData.lastLoginDate}
readOnly
/>
</div>
</form>
<div>
<button type="button" onClick={() => setSubView('password')}>
Change Password
</button>
</div>
</div>
);
return (
<div className="dashboard-container">
{/* Navbar */}
<nav className="dashboard-nav">
<div className="nav-logo">
<span className="logo-text">{APP_NAME}</span>
</div>
<div className="nav-links">
<button
className={`nav-link ${activeView === 'dashboard' ? 'active' : ''}`}
onClick={() => handleNavClick('dashboard')}
>
Dashboard
</button>
<button
className={`nav-link ${activeView === 'profile' ? 'active' : ''}`}
onClick={() => handleNavClick('profile')}
>
Profile
</button>
</div>
</nav>
{/* Main Content */}
{subView === 'details' ? renderProfileForm() : renderPasswordForm()}
</div>
);
};
export default ProfileForm;
@@ -5,6 +5,7 @@ function ConfirmationStep({
selectedContacts,
createdMessage,
message,
scheduledTime,
isSubmitting,
}) {
// In a real app, you might fetch full contact details here
@@ -88,6 +89,12 @@ function ConfirmationStep({
</div>
</div>
{/* Scheduled time Section */}
<div>
Scheduled time:{' '}
{scheduledTime ? scheduledTime.toISOString() : 'No schedule date'}
</div>
{/* Summary Section */}
<div className={styles.confirmationSection}>
<div className={styles.sectionHeader}>
@@ -0,0 +1,89 @@
import React, { useState } from 'react';
function FutureTimeForm({ scheduledTime, setScheduledTime }) {
const [selectedTime, setSelectedTime] = useState(new Date());
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const handleTimeChange = (e) => {
setSelectedTime(e.target.value);
setScheduledTime(new Date(e.target.value));
setError('');
setSuccess('');
};
const validateAndSubmit = (e) => {
e.preventDefault();
if (!selectedTime) {
setError('Please select a date and time.');
return;
}
const now = new Date();
const inputDate = new Date(selectedTime);
// Calculate the minimum allowed time (Now + 10 minutes)
const minAllowedTime = new Date(now.getTime() + 10 * 60000);
if (inputDate < minAllowedTime) {
setError('The time must be at least 10 minutes in the future.');
setSuccess('');
setScheduledTime(null);
} else {
setError('');
setSuccess('Time validated successfully!');
setScheduledTime(inputDate);
}
};
return (
<div
style={{ maxWidth: '400px', margin: '2rem auto', fontFamily: 'Arial' }}
>
<h2>Schedule Event</h2>
<form onSubmit={validateAndSubmit}>
<div style={{ marginBottom: '1rem' }}>
<label
htmlFor="future-time"
style={{ display: 'block', marginBottom: '0.5rem' }}
>
Select Date and Time:
</label>
<input
type="datetime-local"
id="future-time"
value={selectedTime}
onChange={handleTimeChange}
style={{
padding: '8px',
width: '100%',
borderColor: error ? 'red' : '#ccc',
}}
/>
</div>
{error && <p style={{ color: 'red', fontSize: '0.9rem' }}>{error}</p>}
{success && (
<p style={{ color: 'green', fontSize: '0.9rem' }}>{success}</p>
)}
<button
type="submit"
style={{
padding: '10px 20px',
backgroundColor: '#007BFF',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Submit
</button>
</form>
</div>
);
}
export default FutureTimeForm;
@@ -3,9 +3,15 @@ import styles from './SendMessageWizard.module.css';
import { tokenService } from '../../services/TokenService';
import { messageApi, CreateMessageRequest } from '../../api/messageApi';
function MessageForm({ message, onMessageChange, onMessageCreated }) {
function MessageForm({
message,
onMessageChange,
onMessageCreated,
onToggleScheduled,
}) {
const [characterCount, setCharacterCount] = useState(message.length);
const [isCreatingMessage, setIsCreatingMessage] = useState(false);
const [isScheduledChecked, setIsScheduledChecked] = useState(false);
const [createdMessage, setCreatedMessage] = useState(null);
const handleMessageChange = (e) => {
@@ -14,6 +20,11 @@ function MessageForm({ message, onMessageChange, onMessageCreated }) {
setCharacterCount(newMessage.length);
};
const handleIsScheduledCheckbox = (event) => {
setIsScheduledChecked(event.target.checked);
onToggleScheduled(event.target.checked);
};
const createMessage = async () => {
if (!message.trim()) {
return;
@@ -38,10 +49,8 @@ function MessageForm({ message, onMessageChange, onMessageCreated }) {
const createdMessage = response.data[0];
setCreatedMessage(createdMessage);
if (onMessageCreated) {
console.log('Setting createdMessage');
onMessageCreated(createdMessage);
}
console.log('Created message: ', createdMessage);
}
} catch (error) {
console.error(error);
@@ -97,6 +106,17 @@ function MessageForm({ message, onMessageChange, onMessageCreated }) {
<li>Check spelling and grammar</li>
</ul>
</div>
<div>
<label>
<input
type="checkbox"
checked={isScheduledChecked}
onChange={handleIsScheduledCheckbox}
readOnly
/>
Schedule Message
</label>
</div>
</div>
);
}
@@ -1,5 +1,6 @@
import React from 'react';
import ContactSelection from './ContactSelection';
import FutureTimeForm from './FutureTimeForm';
import MessageForm from './MessageForm';
import ConfirmationStep from './ConfirmationStep';
import StepNavigation from './StepNavigation';
@@ -26,6 +27,14 @@ function SendMessageWizardModal({ isOpen, onClose, onComplete }) {
message={wizard.message}
onMessageChange={wizard.setMessage}
onMessageCreated={wizard.setCreatedMessage}
onToggleScheduled={wizard.toggleScheduled}
/>
);
case 'schedule':
return (
<FutureTimeForm
scheduledTime={wizard.scheduledTime}
setScheduledTime={wizard.setScheduledTime}
/>
);
case 'confirm':
@@ -34,6 +43,7 @@ function SendMessageWizardModal({ isOpen, onClose, onComplete }) {
selectedContacts={wizard.selectedContacts}
createdMessage={wizard.createdMessage}
message={wizard.message}
scheduledTime={wizard.scheduledTime}
isSubmitting={wizard.isSubmitting}
/>
);
@@ -74,6 +84,7 @@ function SendMessageWizardModal({ isOpen, onClose, onComplete }) {
const stepTitles = {
select: 'Select Contacts',
message: 'Compose Message',
schedule: 'Schedule message',
confirm: 'Review & Send',
result: 'Result',
};
@@ -34,6 +34,25 @@ function StepNavigation({
</button>
</>
);
case 'schedule':
return (
<>
<button
onClick={onBack}
disabled={!canGoBack}
className={`${styles.navButton} ${styles.backButton}`}
>
Back
</button>
<button
onClick={onNext}
disabled={!canProceed}
className={`${styles.navButton} ${styles.nextButton}`}
>
{currentStep === 'schedule' ? 'Next' : 'Review'}
</button>
</>
);
case 'confirm':
return (
<>
@@ -5,14 +5,18 @@ import {
sendMessageApi,
SendInstantMessageRequest,
} from '../../api/sendMessageApi';
import { scheduleApi } from '../../api/scheduleApi.js';
import { scheduleEventApi } from '../../api/scheduleEventApi.js';
const useSendMessageWizard = () => {
const [state, setState] = useState({
step: 'select',
selectedContacts: [],
scheduledTime: null,
message: '',
createdMessage: null,
isSubmitting: false,
isScheduled: false,
submissionError: null,
});
@@ -24,13 +28,38 @@ const useSendMessageWizard = () => {
case 'select':
return state.selectedContacts.length > 0;
case 'message':
return state.message.trim().length > 0;
if (state.message.trim().length > 0) {
if (state.createdMessage != null) {
return true;
} else {
return false;
}
} else {
return false;
}
case 'schedule':
console.log('Checking to see if schedule can go to the next step');
if (state.scheduledTime == null) {
return false;
}
console.log('Scheduled time is not null');
const result =
state.scheduledTime instanceof Date &&
isNaN(state.scheduledTime.getTime());
return !result;
case 'confirm':
return true;
default:
return false;
}
}, [state.step, state.selectedContacts.length, state.message]);
}, [
state.step,
state.selectedContacts.length,
state.message,
state.createdMessage,
state.scheduledTime,
]);
const canGoBack = state.step !== 'select';
@@ -44,6 +73,14 @@ const useSendMessageWizard = () => {
}));
};
const toggleScheduled = (scheduled) => {
console.log('Yup');
setState((prev) => ({
...prev,
isScheduled: scheduled,
}));
};
const setMessage = (message) => {
setState((prev) => ({ ...prev, message }));
};
@@ -53,29 +90,67 @@ const useSendMessageWizard = () => {
setState((prev) => ({ ...prev, createdMessage: message }));
};
const setScheduledTime = (selectedTime) => {
console.log('Setting scheduled time');
setState((prev) => ({ ...prev, scheduledTime: selectedTime }));
};
const nextStep = () => {
if (!canProceedToNextStep()) return;
const stepOrder = ['select', 'message', 'confirm', 'result'];
const stepOrder = ['select', 'message', 'schedule', 'confirm', 'result'];
const currentIndex = stepOrder.indexOf(state.step);
if (currentIndex < stepOrder.length - 1) {
setState((prev) => ({
...prev,
step: stepOrder[currentIndex + 1],
submissionError: null,
}));
if (state.step === 'message') {
if (state.isScheduled) {
setState((prev) => ({
...prev,
step: stepOrder[currentIndex + 1],
submissionError: null,
}));
} else {
setState((prev) => ({
...prev,
step: stepOrder[currentIndex + 2],
submissionError: null,
}));
}
} else {
setState((prev) => ({
...prev,
step: stepOrder[currentIndex + 1],
submissionError: null,
}));
}
}
};
const prevStep = () => {
const stepOrder = ['select', 'message', 'confirm', 'result'];
const stepOrder = ['select', 'message', 'schedule', 'confirm', 'result'];
const currentIndex = stepOrder.indexOf(state.step);
if (currentIndex > 0) {
setState((prev) => ({
...prev,
step: stepOrder[currentIndex - 1],
submissionError: null,
}));
if (state.step === 'confirm') {
if (state.isScheduled) {
setState((prev) => ({
...prev,
step: stepOrder[currentIndex - 1],
submissionError: null,
}));
} else {
setState((prev) => ({
...prev,
step: stepOrder[currentIndex - 2],
submissionError: null,
}));
}
} else {
setState((prev) => ({
...prev,
step: stepOrder[currentIndex - 1],
submissionError: null,
}));
}
}
};
@@ -91,25 +166,77 @@ const useSendMessageWizard = () => {
}));
try {
console.log('Sending message');
let reqBod = new SendInstantMessageRequest();
reqBod.user_id = tokenService.getUserId();
reqBod.message_id = state.createdMessage.id;
state.selectedContacts.forEach((selectedContact) => {
reqBod.contact_ids.push(selectedContact);
});
const response = await sendMessageApi.sendInstantMessage(
reqBod,
tokenService.bearerToken()
);
if (state.scheduledTime != null) {
console.log('Going to schedule message');
if (response) {
// Success - move to result step
setState((prev) => ({
...prev,
step: 'result',
isSubmitting: false,
}));
const schMsgReq = {
scheduled: state.scheduledTime.toISOString(),
status: 'PENDING',
user_id: tokenService.getUserId(),
};
const token = tokenService.bearerToken();
const schMsgResponse = await scheduleApi.scheduleMessage(
schMsgReq,
token
);
if (schMsgResponse) {
console.log('Message scheduled');
const scheduledMessageId = schMsgResponse.data[0].id;
for (let contact of state.selectedContacts) {
let schMsgEvtReq = {
contact_id: contact,
message_id: state.createdMessage.id,
scheduled_message_id: scheduledMessageId,
};
const schMsgEvtResponse =
await scheduleEventApi.scheduleMessageEvent(schMsgEvtReq, token);
}
console.log('Updating message status');
const req = {
scheduled_message_id: scheduledMessageId,
status: 'READY',
};
console.log(req);
const response = await scheduleApi.prepareMessage(req, token);
if (response) {
console.log('Message scheduled');
setState((prev) => ({
...prev,
step: 'result',
isSubmitting: false,
isScheduled: false,
scheduledTime: null,
}));
}
}
} else {
console.log('Sending message');
let reqBod = new SendInstantMessageRequest();
reqBod.user_id = tokenService.getUserId();
reqBod.message_id = state.createdMessage.id;
state.selectedContacts.forEach((selectedContact) => {
reqBod.contact_ids.push(selectedContact);
});
const response = await sendMessageApi.sendInstantMessage(
reqBod,
tokenService.bearerToken()
);
if (response) {
// Success - move to result step
setState((prev) => ({
...prev,
step: 'result',
isSubmitting: false,
isScheduled: false,
scheduledTime: null,
}));
}
}
} catch (error) {
if (error.name === 'AbortError') {
@@ -121,6 +248,8 @@ const useSendMessageWizard = () => {
...prev,
submissionError: error.message || 'Failed to send messages',
isSubmitting: false,
isScheduled: false,
scheduledTime: null,
}));
}
};
@@ -130,9 +259,11 @@ const useSendMessageWizard = () => {
setState({
step: 'select',
selectedContacts: [],
scheduledTime: null,
message: '',
createdMessage: null,
isSubmitting: false,
isScheduled: false,
submissionError: null,
});
};
@@ -146,17 +277,21 @@ const useSendMessageWizard = () => {
// Getters
currentStep: state.step,
selectedContacts: state.selectedContacts,
scheduledTime: state.scheduledTime,
message: state.message,
createdMessage: state.createdMessage,
isSubmitting: state.isSubmitting,
isScheduled: state.isScheduled,
submissionError: state.submissionError,
canProceedToNextStep: canProceedToNextStep(),
canGoBack,
// Actions
toggleContact,
toggleScheduled,
setMessage,
setCreatedMessage,
setScheduledTime,
nextStep,
prevStep,
submit,
@@ -33,11 +33,11 @@ function ConfirmationStep({ selectedContact, contactDetails, isSubmitting }) {
<span className={styles.reviewLabel}>First Name</span>
<div className={styles.reviewComparison}>
<span className={styles.oldValue}>
{selectedContact.first_name || 'Not set'}
{selectedContact.firstname || 'Not set'}
</span>
<span className={styles.arrow}></span>
<span className={styles.newValue}>
{contactDetails.first_name || 'Not set'}
{contactDetails.firstname || 'Not set'}
</span>
</div>
</div>
@@ -46,11 +46,11 @@ function ConfirmationStep({ selectedContact, contactDetails, isSubmitting }) {
<span className={styles.reviewLabel}>Last Name</span>
<div className={styles.reviewComparison}>
<span className={styles.oldValue}>
{selectedContact.last_name || 'Not set'}
{selectedContact.lastname || 'Not set'}
</span>
<span className={styles.arrow}></span>
<span className={styles.newValue}>
{contactDetails.last_name || 'Not set'}
{contactDetails.lastname || 'Not set'}
</span>
</div>
</div>
+12 -12
View File
@@ -3,8 +3,8 @@ import styles from './ModifyForm.module.css';
function ModifyForm({ contactDetails, setContactDetails }) {
const [formData, setFormData] = useState({
first_name: '',
last_name: '',
firstname: '',
lastname: '',
nickname: '',
});
@@ -12,8 +12,8 @@ function ModifyForm({ contactDetails, setContactDetails }) {
useEffect(() => {
if (contactDetails) {
setFormData({
first_name: contactDetails.first_name || '',
last_name: contactDetails.last_name || '',
firstname: contactDetails.firstname || '',
lastname: contactDetails.lastname || '',
nickname: contactDetails.nickname || '',
});
}
@@ -37,16 +37,16 @@ function ModifyForm({ contactDetails, setContactDetails }) {
<h3 className={styles.sectionTitle}>Edit Contact Information</h3>
<div className={styles.formGroup}>
<label htmlFor="first_name" className={styles.formLabel}>
<label htmlFor="firstname" className={styles.formLabel}>
First Name
<span className={styles.requiredIndicator}>*</span>
</label>
<div className={styles.inputContainer}>
<input
type="text"
id="first_name"
name="first_name"
value={formData.first_name}
id="firstname"
name="firstname"
value={formData.firstname}
onChange={handleChange}
className={styles.formInput}
placeholder="Enter contact's first name"
@@ -61,16 +61,16 @@ function ModifyForm({ contactDetails, setContactDetails }) {
</div>
<div className={styles.formGroup}>
<label htmlFor="last_name" className={styles.formLabel}>
<label htmlFor="lastname" className={styles.formLabel}>
Last Name
<span className={styles.requiredIndicator}>*</span>
</label>
<div className={styles.inputContainer}>
<input
type="text"
id="last_name"
name="last_name"
value={formData.last_name}
id="lastname"
name="lastname"
value={formData.lastname}
onChange={handleChange}
className={styles.formInput}
placeholder="Enter contact's last name"
@@ -49,15 +49,15 @@ function ViewContact({ selectedContact, onSelectedContact }) {
const filteredContacts = contacts.filter(
(contact) =>
contact.phone_number.toLowerCase().includes(searchTerm.toLowerCase()) ||
contact.first_name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
contact.last_name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
contact.firstname?.toLowerCase().includes(searchTerm.toLowerCase()) ||
contact.lastname?.toLowerCase().includes(searchTerm.toLowerCase()) ||
contact.nickname?.toLowerCase().includes(searchTerm.toLowerCase())
);
const formatContactDisplay = (contact) => {
const phone = contact.phone_number || 'No phone';
const name =
[contact.first_name, contact.last_name].filter(Boolean).join(' ') ||
[contact.firstname, contact.lastname].filter(Boolean).join(' ') ||
'Unnamed';
const nickname = contact.nickname ? `"${contact.nickname}"` : '';
@@ -117,7 +117,7 @@ function ViewContact({ selectedContact, onSelectedContact }) {
onClick={() => onSelectedContact(contact)}
>
<div className={styles.contactAvatar}>
{contact.first_name?.[0]?.toUpperCase() || '?'}
{contact.firstname?.[0]?.toUpperCase() || '?'}
</div>
<div className={styles.contactInfo}>
<div className={styles.contactMainInfo}>
@@ -131,7 +131,7 @@ function ViewContact({ selectedContact, onSelectedContact }) {
)}
</div>
<div className={styles.contactName}>
{[contact.first_name, contact.last_name]
{[contact.firstname, contact.lastname]
.filter(Boolean)
.join(' ') || 'Unnamed Contact'}
</div>
@@ -21,13 +21,13 @@ const useViewContactWizard = () => {
case 'modify':
if (!state.contactDetails) return false;
if (
!state.contactDetails.first_name ||
state.contactDetails.first_name.trim() === ''
!state.contactDetails.firstname ||
state.contactDetails.firstname.trim() === ''
) {
return false;
} else if (
!state.contactDetails.last_name ||
state.contactDetails.last_name.trim() === ''
!state.contactDetails.lastname ||
state.contactDetails.lastname.trim() === ''
) {
return false;
} else {
@@ -61,8 +61,8 @@ const useViewContactWizard = () => {
? null
: {
id: contactId,
first_name: contact.first_name,
last_name: contact.last_name,
firstname: contact.firstname,
lastname: contact.lastname,
phone_number: contact.phone_number,
user_id: contact.user_id,
nickname: contact.nickname || '',
@@ -90,8 +90,8 @@ const useViewContactWizard = () => {
step: nextStepValue,
submissionError: null,
contactDetails: {
first_name: prev.selectedContact.first_name || '',
last_name: prev.selectedContact.last_name || '',
firstname: prev.selectedContact.firstname || '',
lastname: prev.selectedContact.lastname || '',
nickname: prev.selectedContact.nickname || '',
phone_number: prev.selectedContact.phone_number || '',
id: prev.selectedContact.id,
@@ -139,10 +139,10 @@ const useViewContactWizard = () => {
console.log('Contact details to update: ', state.contactDetails);
let reqBody = new UpdateContactNamesRequest();
reqBody.first_name = state.contactDetails.first_name;
reqBody.last_name = state.contactDetails.last_name;
reqBody.firstname = state.contactDetails.firstname;
reqBody.lastname = state.contactDetails.lastname;
reqBody.nickname = state.contactDetails.nickname || ''; // Send empty string if no nickname
reqBody.contact_id = state.selectedContact.id;
reqBody.id = state.selectedContact.id;
reqBody.user_id = userId;
const response = await contactApi.updateContactNames(
+99
View File
@@ -355,3 +355,102 @@
.view-sent-message-list-container::-webkit-scrollbar-thumb:hover {
background: #a1a1a1;
}
.view-sent-message-tabs {
display: flex;
margin-bottom: 20px;
border-bottom: 1px solid #e5e7eb;
}
.tab-button {
padding: 10px 20px;
background: none;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
font-weight: 500;
color: #6b7280;
transition: all 0.3s ease;
}
.tab-button.active {
color: #3b82f6;
border-bottom-color: #3b82f6;
}
.tab-button:hover {
color: #374151;
background-color: #f9fafb;
}
.search-container {
margin-bottom: 20px;
}
.search-input {
width: 100%;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 14px;
}
.search-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.section-header h3 {
margin: 0;
color: #111827;
font-size: 18px;
font-weight: 600;
}
.last-updated {
font-size: 12px;
color: #6b7280;
}
.no-messages {
text-align: center;
padding: 40px;
color: #6b7280;
font-style: italic;
}
.status-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
}
.status-processing {
background-color: #fef3c7;
color: #92400e;
}
.status-sent {
background-color: #dcfce7;
color: #166534;
}
.status-failed {
background-color: #fee2e2;
color: #991b1b;
}
.status-default {
background-color: #f3f4f6;
color: #374151;
}
+177 -223
View File
@@ -1,20 +1,21 @@
import { useState, useEffect, useCallback } from 'react';
import { tokenService } from '../services/TokenService';
import { messageEventResponseApi } from '../api/messageEventResponseApi';
import { scheduleApi } from '../api/scheduleApi';
import './ViewSentMessages.css';
const ViewSentMessages = ({
isOpen,
onClose,
title = 'View Sent Messages ',
title = 'View Sent Messages',
}) => {
const [sentMessages, setSentMessages] = useState([]);
const [scheduledMessages, setScheduledMessages] = useState([]);
const [lastUpdated, setLastUpdated] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const [activeTab, setActiveTab] = useState('sent');
// Fetch the sent messages or Message Event Responses
const fetchSentMessages = useCallback(async () => {
@@ -33,7 +34,6 @@ const ViewSentMessages = ({
setLastUpdated(new Date());
setSentMessages(response.data);
} else {
setLoading(false);
throw new Error(response.message || 'Failed to load sent messages');
}
} catch (err) {
@@ -44,8 +44,32 @@ const ViewSentMessages = ({
}
}, []);
const fetchScheduledMessages = useCallback(async () => {
try {
const userId = tokenService.getUserId();
const response = await scheduleApi.getScheduledMessage(
userId,
tokenService.bearerToken()
);
if (response) {
const scheduledMessages = response.data;
setScheduledMessages(scheduledMessages);
} else {
throw new Error(response.message || 'Failed to get scheduled messages');
}
} catch (err) {
console.error('Failed to load scheduled messages:', err);
setScheduledMessages([]);
}
}, []);
const handleRefresh = () => {
fetchSentMessages();
if (activeTab === 'sent') {
fetchSentMessages();
} else {
fetchScheduledMessages();
}
};
useEffect(() => {
@@ -55,6 +79,13 @@ const ViewSentMessages = ({
}
}, [isOpen, fetchSentMessages]);
// Fetch scheduled messages when tab is switched
useEffect(() => {
if (activeTab === 'scheduled' && isOpen) {
fetchScheduledMessages();
}
}, [activeTab, isOpen, fetchScheduledMessages]);
const filteredSentMessages = sentMessages.filter((sentMessage) => {
const searchLower = searchTerm.toLowerCase();
return (
@@ -66,6 +97,15 @@ const ViewSentMessages = ({
);
});
// Filter scheduled messages
const filteredScheduledMessages = scheduledMessages.filter((message) => {
const searchLower = searchTerm.toLowerCase();
return (
(message.status && message.status.toLowerCase().includes(searchLower)) ||
(message.id && message.id.toString().includes(searchTerm))
);
});
const formatLastUpdated = () => {
if (!lastUpdated) return 'Never';
return lastUpdated.toLocaleTimeString([], {
@@ -74,6 +114,25 @@ const ViewSentMessages = ({
});
};
// Format date for scheduled messages
const formatScheduledDate = (dateString) => {
return new Date(dateString).toLocaleString();
};
// Get status badge styling
const getStatusBadgeClass = (status) => {
switch (status) {
case 'PROCESSING':
return 'status-processing';
case 'SENT':
return 'status-sent';
case 'FAILED':
return 'status-failed';
default:
return 'status-default';
}
};
if (!isOpen) return null;
return (
@@ -90,13 +149,40 @@ const ViewSentMessages = ({
</button>
</div>
{/* Add tabs for navigation */}
<div className="view-sent-message-tabs">
<button
className={`tab-button ${activeTab === 'sent' ? 'active' : ''}`}
onClick={() => setActiveTab('sent')}
>
Sent Messages
</button>
<button
className={`tab-button ${activeTab === 'scheduled' ? 'active' : ''}`}
onClick={() => setActiveTab('scheduled')}
>
Scheduled Messages
</button>
</div>
{/* Search input */}
<div className="search-container">
<input
type="text"
placeholder="Search messages..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
</div>
<div className="view-sent-message-form">
{error && (
<div className="submit-error">
<div style={{ marginBottom: '8px' }}>{error}</div>
<button
onClick={handleRefresh}
onStype={{
style={{
background: '#ef4444',
color: 'white',
border: 'none',
@@ -113,233 +199,101 @@ const ViewSentMessages = ({
{loading ? (
<div style={{ textAlign: 'center', padding: '40px' }}>
<div className="loafing-spinner" />
<p style={{ marginTop: '20px', color: '#4b5563' }}>
Loading sent messages
</p>
<div className="loading-spinner" />
<p>Loading messages...</p>
</div>
) : (
<>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '20px',
flexWrap: 'wrap',
gap: '10px',
}}
>
<button
onClick={handleRefresh}
style={{
background: '#f3f4f6',
color: '#4b5563',
border: '1px solid #e5e7eb',
borderRadius: '6px',
padding: '8px 16px',
cursor: 'pointer',
fontSize: '14px',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
<span></span> Refresh
</button>
<input
type="text"
placeholder="Search sent messages..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{
flex: 1,
minWidth: '200px',
maxWidth: '300px',
padding: '8px 12px',
border: '1px solid #e5e7eb',
borderRadius: '6px',
fontSize: '14px',
}}
/>
<div
style={{
fontSize: '12px',
color: '#6b7280',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
<span>Updated: {formatLastUpdated()}</span>
<span
style={{
display: 'inline-block',
width: '8px',
height: '8px',
borderRadius: '50%',
background: lastUpdated ? '#10b981' : '#9ca3af',
}}
/>
</div>
</div>
{/* Sent Messages List */}
{filteredSentMessages.length === 0 ? (
<div
style={{
textAlign: 'center',
padding: '40px',
color: '#4b5563',
}}
>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>
X
{/* Sent Messages Tab Content */}
{activeTab === 'sent' && (
<div className="messages-section">
<div className="section-header">
<h3>Sent Messages</h3>
<span className="last-updated">
Last updated: {formatLastUpdated()}
</span>
</div>
<p
style={{
fontSize: '18px',
fontWeight: '500',
marginBottom: '8px',
}}
>
{searchTerm
? 'No matching sent messages found'
: 'No sent messages available'}
</p>
<p style={{ marginBottom: '20px' }}>
{searchTerm
? 'Try a different search term'
: 'Create a contact and send a message to get started'}
</p>
{searchTerm && (
<button
onClick={() => setSearchTerm('')}
style={{
background: '#f3f4f6',
color: '#4b5563',
border: '1px solid #e5e7eb',
borderRadius: '6px',
padding: '8px 16px',
cursor: 'pointer',
fontSize: '14px',
marginBottom: '10px',
}}
>
Clear Search
</button>
{filteredSentMessages.length > 0 ? (
<div className="view-sent-message-list-container">
<table className="view-sent-message-table">
<thead>
<tr>
<th>ID</th>
<th>Status</th>
<th>Sent Time</th>
</tr>
</thead>
<tbody>
{filteredSentMessages.map((message) => (
<tr key={message.id}>
<td>{message.id.substring(0, 8)}...</td>
<td>
<span
className={`status-badge ${getStatusBadgeClass(message.status)}`}
>
{message.status}
</span>
</td>
<td>{message.sent || 'N/A'}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="no-messages">No sent messages found.</div>
)}
</div>
) : (
<>
<div
style={{
maxHeight: '400px',
overflowY: 'auto',
border: '1px solid #e5e7eb',
borderRadius: '8px',
background: '#fafafa',
}}
>
<div style={{ padding: '4px' }}>
{filteredSentMessages.map((sentMessage, index) => (
<div
key={sentMessage.id}
style={{
padding: '16px',
borderBottom:
index < filteredSentMessages.length - 1
? '1px solid #e5e7eb'
: 'none',
background: 'white',
transition: 'all 0.2s',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: '8px',
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
}}
>
<span
style={{
background: '#667eea',
color: 'white',
width: '28px',
height: '28px',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '12px',
fontWeight: '600',
}}
>
{index + 1}
</span>
<div>
<h4
style={{
margin: 0,
color: '#111827',
fontSize: '16px',
fontWeight: '600',
}}
>
{sentMessage.status}
</h4>
<h5>Sent: {sentMessage.sent}</h5>
</div>
</div>
<span
style={{
fontSize: '12px',
color: '#9ca3af',
background: '#f3f4f6',
padding: '2px 8px',
borderRadius: '4px',
}}
>
Id: {sentMessage.id}
</span>
</div>
</div>
))}
</div>
)}
{/* Scheduled Messages Tab Content */}
{activeTab === 'scheduled' && (
<div className="messages-section">
<div className="section-header">
<h3>Scheduled Messages</h3>
</div>
</>
{filteredScheduledMessages.length > 0 ? (
<div className="view-sent-message-list-container">
<table className="view-sent-message-table">
<thead>
<tr>
<th>ID</th>
<th>Scheduled Time</th>
<th>Created Time</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{filteredScheduledMessages.map((message) => (
<tr key={message.id}>
<td>{message.id.substring(0, 8)}...</td>
<td>
{formatScheduledDate(message.scheduled)}
</td>
<td>{formatScheduledDate(message.created)}</td>
<td>
<span
className={`status-badge ${getStatusBadgeClass(message.status)}`}
>
{message.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="no-messages">
No scheduled messages found.
</div>
)}
</div>
)}
</>
)}
<div className="form-actions">
<button
type="button"
className="cancel-btn"
onClick={onClose}
disabled={loading}
>
Close
</button>
<button
type="button"
className="submit-btn"
onClick={handleRefresh}
disabled={loading}
>
{loading ? 'Refreshing...' : 'Refresh List'}
</button>
</div>
</div>
</div>
</div>
+2 -2
View File
@@ -1,2 +1,2 @@
export const AUTH_API_BASE_URL = 'http://localhost:9080';
export const API_BASE_URL = 'http://localhost:8080';
export const AUTH_API_BASE_URL = import.meta.env.VITE_AUTH_URL;
export const API_BASE_URL = import.meta.env.VITE_API_URL;
+1
View File
@@ -2,3 +2,4 @@ export const DATA_KEY_ACCESS_TOKEN = 'access_token';
export const DATA_KEY_REGISTERED_USER_ID = 'registered_user_id';
export const DATA_KEY_USER_ID = 'user_id';
export const DATA_KEY_ADDED_CONTACT_ID = 'added_contact_id';
export const APP_NAME = 'schedtxt';