diff --git a/package.json b/package.json
index 29c7e73..9493fef 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,7 @@
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
- "@sentry/nextjs": "^9.29.0",
+ "@sentry/nextjs": "^9.30.0",
"@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.50.0",
"@t3-oss/env-nextjs": "^0.12.0",
@@ -40,11 +40,11 @@
"next-themes": "^0.4.6",
"react": "^19.1.0",
"react-dom": "^19.1.0",
- "react-hook-form": "^7.58.0",
+ "react-hook-form": "^7.58.1",
"require-in-the-middle": "^7.5.2",
"sonner": "^2.0.5",
"vaul": "^1.1.2",
- "zod": "^3.25.64"
+ "zod": "^3.25.67"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
@@ -52,15 +52,15 @@
"@tanstack/eslint-plugin-query": "^5.78.0",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.3",
- "@types/node": "^20.19.0",
+ "@types/node": "^20.19.1",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"eslint": "^9.29.0",
"eslint-config-next": "^15.3.3",
"eslint-config-prettier": "^10.1.5",
- "eslint-plugin-prettier": "^5.4.1",
+ "eslint-plugin-prettier": "^5.5.0",
"import-in-the-middle": "^1.14.2",
- "postcss": "^8.5.5",
+ "postcss": "^8.5.6",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.12",
"tailwind-merge": "^3.3.1",
@@ -68,7 +68,7 @@
"tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.3.4",
"typescript": "^5.8.3",
- "typescript-eslint": "^8.34.0"
+ "typescript-eslint": "^8.34.1"
},
"ct3aMetadata": {
"initVersion": "7.39.3"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index be4567c..4371aaf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -10,7 +10,7 @@ importers:
dependencies:
'@hookform/resolvers':
specifier: ^5.1.1
- version: 5.1.1(react-hook-form@7.58.0(react@19.1.0))
+ version: 5.1.1(react-hook-form@7.58.1(react@19.1.0))
'@radix-ui/react-avatar':
specifier: ^1.1.10
version: 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -39,8 +39,8 @@ importers:
specifier: ^1.2.3
version: 1.2.3(@types/react@19.1.8)(react@19.1.0)
'@sentry/nextjs':
- specifier: ^9.29.0
- version: 9.29.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.99.9)
+ specifier: ^9.30.0
+ version: 9.30.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.99.9)
'@supabase/ssr':
specifier: ^0.6.1
version: 0.6.1(@supabase/supabase-js@2.50.0)
@@ -49,7 +49,7 @@ importers:
version: 2.50.0
'@t3-oss/env-nextjs':
specifier: ^0.12.0
- version: 0.12.0(typescript@5.8.3)(zod@3.25.64)
+ version: 0.12.0(typescript@5.8.3)(zod@3.25.67)
'@tanstack/react-query':
specifier: ^5.80.7
version: 5.80.7(react@19.1.0)
@@ -78,8 +78,8 @@ importers:
specifier: ^19.1.0
version: 19.1.0(react@19.1.0)
react-hook-form:
- specifier: ^7.58.0
- version: 7.58.0(react@19.1.0)
+ specifier: ^7.58.1
+ version: 7.58.1(react@19.1.0)
require-in-the-middle:
specifier: ^7.5.2
version: 7.5.2
@@ -90,8 +90,8 @@ importers:
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
zod:
- specifier: ^3.25.64
- version: 3.25.64
+ specifier: ^3.25.67
+ version: 3.25.67
devDependencies:
'@eslint/eslintrc':
specifier: ^3.3.1
@@ -109,8 +109,8 @@ importers:
specifier: ^5.0.3
version: 5.0.3
'@types/node':
- specifier: ^20.19.0
- version: 20.19.0
+ specifier: ^20.19.1
+ version: 20.19.1
'@types/react':
specifier: ^19.1.8
version: 19.1.8
@@ -127,14 +127,14 @@ importers:
specifier: ^10.1.5
version: 10.1.5(eslint@9.29.0(jiti@2.4.2))
eslint-plugin-prettier:
- specifier: ^5.4.1
- version: 5.4.1(@types/eslint@9.6.1)(eslint-config-prettier@10.1.5(eslint@9.29.0(jiti@2.4.2)))(eslint@9.29.0(jiti@2.4.2))(prettier@3.5.3)
+ specifier: ^5.5.0
+ version: 5.5.0(@types/eslint@9.6.1)(eslint-config-prettier@10.1.5(eslint@9.29.0(jiti@2.4.2)))(eslint@9.29.0(jiti@2.4.2))(prettier@3.5.3)
import-in-the-middle:
specifier: ^1.14.2
version: 1.14.2
postcss:
- specifier: ^8.5.5
- version: 8.5.5
+ specifier: ^8.5.6
+ version: 8.5.6
prettier:
specifier: ^3.5.3
version: 3.5.3
@@ -157,8 +157,8 @@ importers:
specifier: ^5.8.3
version: 5.8.3
typescript-eslint:
- specifier: ^8.34.0
- version: 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ specifier: ^8.34.1
+ version: 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
packages:
@@ -1122,8 +1122,8 @@ packages:
rollup:
optional: true
- '@rollup/pluginutils@5.1.4':
- resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==}
+ '@rollup/pluginutils@5.2.0':
+ resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
@@ -1232,28 +1232,28 @@ packages:
'@rushstack/eslint-patch@1.11.0':
resolution: {integrity: sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==}
- '@sentry-internal/browser-utils@9.29.0':
- resolution: {integrity: sha512-Wp6UJCDVV2KVK+TG8GwdLZyDy4GtUYDmVhGMpHKPS3G/Qgpf36cY/XHwChwaHZ5P9Bk1sjS9Ok698J59S8L2nw==}
+ '@sentry-internal/browser-utils@9.30.0':
+ resolution: {integrity: sha512-e6ZlN8oWheCB0YJSGlBNUlh6UPnY5Ecj1P+/cgeKBhNm7c3bIx4J50485hB8LQsu+b7Q11L2o/wucZ//Pb6FCg==}
engines: {node: '>=18'}
- '@sentry-internal/feedback@9.29.0':
- resolution: {integrity: sha512-ADvetGrtr+RfYcQKrQxah4fHs/xDJ/VjbStVMSuaNllzwWPYNkWIGFE6YjQ7wZszj0DQIu5/H+B6lZKsFYk4xw==}
+ '@sentry-internal/feedback@9.30.0':
+ resolution: {integrity: sha512-qAZ7xxLqZM7GlEvmSUmTHnoueg+fc7esMQD4vH8pS7HI3n9C5MjGn3HHlndRpD8lL7iUUQ0TPZQgU6McbzMDyw==}
engines: {node: '>=18'}
- '@sentry-internal/replay-canvas@9.29.0':
- resolution: {integrity: sha512-TrQYhSAVPhyenvu0fNkon7BznFibu1mzS5bCudxhgOWajZluUVrXcbp8Q3WZ3R+AogrcgA3Vy6aumP/+fMKdwg==}
+ '@sentry-internal/replay-canvas@9.30.0':
+ resolution: {integrity: sha512-I4MxS27rfV7vnOU29L80y4baZ4I1XqpnYvC/yLN7C17nA8eDCufQ8WVomli41y8JETnfcxlm68z7CS0sO4RCSA==}
engines: {node: '>=18'}
- '@sentry-internal/replay@9.29.0':
- resolution: {integrity: sha512-we/1JPRje8sNowQCyogOV1OYWuDOP/3XmDi48XoFG2HB0XMl2HfL5LI8AvgAvC/5nrqVAAo4ktbjoVLm1fb7rg==}
+ '@sentry-internal/replay@9.30.0':
+ resolution: {integrity: sha512-+6wkqQGLJuFUzvGRzbh3iIhFGyxQx/Oxc0ODDKmz9ag2xYRjCYb3UUQXmQX9navAF0HXUsq8ajoJPm2L1ZyWVg==}
engines: {node: '>=18'}
'@sentry/babel-plugin-component-annotate@3.5.0':
resolution: {integrity: sha512-s2go8w03CDHbF9luFGtBHKJp4cSpsQzNVqgIa9Pfa4wnjipvrK6CxVT4icpLA3YO6kg5u622Yoa5GF3cJdippw==}
engines: {node: '>= 14'}
- '@sentry/browser@9.29.0':
- resolution: {integrity: sha512-+GFX/yb+rh6V1fSgTYM6ttAgledl2aUR3T3Rg86HNuegbdX8ym6lOtUOIZ0j9jPK015HR47KIPyIZVZZJ7Rj9g==}
+ '@sentry/browser@9.30.0':
+ resolution: {integrity: sha512-sRyW6A9nIieTTI26MYXk1DmWEhmphTjZevusNWla+vvUigCmSjuH+xZw19w43OyvF3bu261Skypnm/mAalOTwg==}
engines: {node: '>=18'}
'@sentry/bundler-plugin-core@3.5.0':
@@ -1306,22 +1306,22 @@ packages:
engines: {node: '>= 10'}
hasBin: true
- '@sentry/core@9.29.0':
- resolution: {integrity: sha512-wDyNe45PM+RCGtUn1tK7LzJ08ksv8i8KRUHrst7lsinEfRm83YH+wbWrPmwkVNEngUZvYkHwGLbNXM7xgFUuDQ==}
+ '@sentry/core@9.30.0':
+ resolution: {integrity: sha512-JfEpeQ8a1qVJEb9DxpFTFy1J1gkNdlgKAPiqYGNnm4yQbnfl2Kb/iEo1if70FkiHc52H8fJwISEF90pzMm6lPg==}
engines: {node: '>=18'}
- '@sentry/nextjs@9.29.0':
- resolution: {integrity: sha512-chMSvo/CWsUw3bkGnURiOejW2hI95sofvFQQL2W98KGRhznfkfXhIh6U60fDpO2KaAbXDbbUCgcvBALdmILu9g==}
+ '@sentry/nextjs@9.30.0':
+ resolution: {integrity: sha512-9Ouf0Tng1HAPeYPaT9xUSj6jt/qV+h9/6Vf2yzIEGR4j1FbP5wdccGMs8LRdkp9msqEwv2ERnU////zHL1fpyA==}
engines: {node: '>=18'}
peerDependencies:
next: ^13.2.0 || ^14.0 || ^15.0.0-rc.0
- '@sentry/node@9.29.0':
- resolution: {integrity: sha512-oABipgC/fClRuvyMeK43rigv9F+OAaoR84UaMKB7aPXN6iz634wBRVsaoZAwiR3xLL+R7MafEPPA/s9XqlG7ag==}
+ '@sentry/node@9.30.0':
+ resolution: {integrity: sha512-jHuSSKro2DUaccGcYSBbB8Rj0sG+LRh1iSWrJ+4c4Pj7tJFN9MbeMybC1buMSzAp+rwHUMZ3+ws0kgNVtsRJJg==}
engines: {node: '>=18'}
- '@sentry/opentelemetry@9.29.0':
- resolution: {integrity: sha512-QTUmre8i5+832RjzQW+g8IQ3UmBe5fbQXGbCF5hQ0UNuHle9r3Z8UZcIff5W8tm5AXMxPqvptTnDEZUUXHgBiA==}
+ '@sentry/opentelemetry@9.30.0':
+ resolution: {integrity: sha512-LhTmyGGLAP/LAxs/oXuPs0LC5Z80QSLL1oUoBRB5/+MitK7Huug6n8ZFjPTP3/6bT67XOVqILCdj8BwMlBeXhA==}
engines: {node: '>=18'}
peerDependencies:
'@opentelemetry/api': ^1.9.0
@@ -1331,14 +1331,14 @@ packages:
'@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.0.0
'@opentelemetry/semantic-conventions': ^1.34.0
- '@sentry/react@9.29.0':
- resolution: {integrity: sha512-Wb8oKkIr/1yZ0GRz1UH4CRcjIU48iQLSDLXFKZ8YwPpPdC5qq7l4ALraxlcdB+uWq0JIgEjN5FSLamdt/NHX/w==}
+ '@sentry/react@9.30.0':
+ resolution: {integrity: sha512-asA49AkZ/g9CCeW0eA0Ent0DF60S4k2IHxbu+Q1mqgbRRmbn859oL2Bgsu/EvzWf5edeQtuUml8LIo4YoFwfMA==}
engines: {node: '>=18'}
peerDependencies:
react: ^16.14.0 || 17.x || 18.x || 19.x
- '@sentry/vercel-edge@9.29.0':
- resolution: {integrity: sha512-Op31XnkkLwWImIXPVpX2ErEPGmXaWt5YvVxiikyCgH0OzfWUnBKXDl9lcPC2Kn02JCQOop8o9tmQUyOagfJrog==}
+ '@sentry/vercel-edge@9.30.0':
+ resolution: {integrity: sha512-zhxXEXQbf1ggyyR6pf/ZED8cj5Ubb2iObnpZaGHNWRccsToq7EecW0LKUJjdWKqDSqlv1DaI5yUAmn4oheQ4zQ==}
engines: {node: '>=18'}
'@sentry/webpack-plugin@3.5.0':
@@ -1557,8 +1557,8 @@ packages:
'@types/mysql@2.15.26':
resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==}
- '@types/node@20.19.0':
- resolution: {integrity: sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==}
+ '@types/node@20.19.1':
+ resolution: {integrity: sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==}
'@types/pg-pool@2.0.6':
resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==}
@@ -1598,63 +1598,63 @@ packages:
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
- '@typescript-eslint/eslint-plugin@8.34.0':
- resolution: {integrity: sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==}
+ '@typescript-eslint/eslint-plugin@8.34.1':
+ resolution: {integrity: sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
- '@typescript-eslint/parser': ^8.34.0
+ '@typescript-eslint/parser': ^8.34.1
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <5.9.0'
- '@typescript-eslint/parser@8.34.0':
- resolution: {integrity: sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==}
+ '@typescript-eslint/parser@8.34.1':
+ resolution: {integrity: sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <5.9.0'
- '@typescript-eslint/project-service@8.34.0':
- resolution: {integrity: sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==}
+ '@typescript-eslint/project-service@8.34.1':
+ resolution: {integrity: sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <5.9.0'
- '@typescript-eslint/scope-manager@8.34.0':
- resolution: {integrity: sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==}
+ '@typescript-eslint/scope-manager@8.34.1':
+ resolution: {integrity: sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/tsconfig-utils@8.34.0':
- resolution: {integrity: sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==}
+ '@typescript-eslint/tsconfig-utils@8.34.1':
+ resolution: {integrity: sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <5.9.0'
- '@typescript-eslint/type-utils@8.34.0':
- resolution: {integrity: sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==}
+ '@typescript-eslint/type-utils@8.34.1':
+ resolution: {integrity: sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <5.9.0'
- '@typescript-eslint/types@8.34.0':
- resolution: {integrity: sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==}
+ '@typescript-eslint/types@8.34.1':
+ resolution: {integrity: sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/typescript-estree@8.34.0':
- resolution: {integrity: sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==}
+ '@typescript-eslint/typescript-estree@8.34.1':
+ resolution: {integrity: sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <5.9.0'
- '@typescript-eslint/utils@8.34.0':
- resolution: {integrity: sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==}
+ '@typescript-eslint/utils@8.34.1':
+ resolution: {integrity: sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <5.9.0'
- '@typescript-eslint/visitor-keys@8.34.0':
- resolution: {integrity: sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==}
+ '@typescript-eslint/visitor-keys@8.34.1':
+ resolution: {integrity: sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@unrs/resolver-binding-android-arm-eabi@1.9.0':
@@ -2091,8 +2091,8 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
- electron-to-chromium@1.5.167:
- resolution: {integrity: sha512-LxcRvnYO5ez2bMOFpbuuVuAI5QNeY1ncVytE/KXaL6ZNfzX1yPlAO0nSOyIHx2fVAuUprMqPs/TdVhUFZy7SIQ==}
+ electron-to-chromium@1.5.170:
+ resolution: {integrity: sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA==}
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
@@ -2212,8 +2212,8 @@ packages:
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9
- eslint-plugin-prettier@5.4.1:
- resolution: {integrity: sha512-9dF+KuU/Ilkq27A8idRP7N2DH8iUR6qXcjF3FR2wETY21PZdBrIjwCau8oboyGj9b7etWmTGEeM8e7oOed6ZWg==}
+ eslint-plugin-prettier@5.5.0:
+ resolution: {integrity: sha512-8qsOYwkkGrahrgoUv76NZi23koqXOGiiEzXMrT8Q7VcYaUISR+5MorIUxfWqYXN0fN/31WbSrxCxFkVQ43wwrA==}
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
'@types/eslint': '>=8.0.0'
@@ -2995,8 +2995,8 @@ packages:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
- postcss@8.5.5:
- resolution: {integrity: sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==}
+ postcss@8.5.6:
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
postgres-array@2.0.0:
@@ -3108,8 +3108,8 @@ packages:
peerDependencies:
react: ^19.1.0
- react-hook-form@7.58.0:
- resolution: {integrity: sha512-zGijmEed35oNfOfy7ub99jfjkiLhHwA3dl5AgyKdWC6QQzhnc7tkWewSa+T+A2EpLrc6wo5DUoZctS9kufWJjA==}
+ react-hook-form@7.58.1:
+ resolution: {integrity: sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA==}
engines: {node: '>=18.0.0'}
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
@@ -3410,8 +3410,8 @@ packages:
uglify-js:
optional: true
- terser@5.42.0:
- resolution: {integrity: sha512-UYCvU9YQW2f/Vwl+P0GfhxJxbUGLwd+5QrrGgLajzWAtC/23AX0vcise32kkP7Eu0Wu9VlzzHAXkLObgjQfFlQ==}
+ terser@5.43.0:
+ resolution: {integrity: sha512-CqNNxKSGKSZCunSvwKLTs8u8sGGlp27sxNZ4quGh0QeNuyHM0JSEM/clM9Mf4zUp6J+tO2gUXhgXT2YMMkwfKQ==}
engines: {node: '>=10'}
hasBin: true
@@ -3465,8 +3465,8 @@ packages:
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
engines: {node: '>= 0.4'}
- typescript-eslint@8.34.0:
- resolution: {integrity: sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ==}
+ typescript-eslint@8.34.1:
+ resolution: {integrity: sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
@@ -3613,8 +3613,8 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
- zod@3.25.64:
- resolution: {integrity: sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==}
+ zod@3.25.67:
+ resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==}
snapshots:
@@ -3804,10 +3804,10 @@ snapshots:
'@floating-ui/utils@0.2.9': {}
- '@hookform/resolvers@5.1.1(react-hook-form@7.58.0(react@19.1.0))':
+ '@hookform/resolvers@5.1.1(react-hook-form@7.58.1(react@19.1.0))':
dependencies:
'@standard-schema/utils': 0.3.0
- react-hook-form: 7.58.0(react@19.1.0)
+ react-hook-form: 7.58.1(react@19.1.0)
'@humanfs/core@0.19.1': {}
@@ -4584,7 +4584,7 @@ snapshots:
'@rollup/plugin-commonjs@28.0.1(rollup@4.35.0)':
dependencies:
- '@rollup/pluginutils': 5.1.4(rollup@4.35.0)
+ '@rollup/pluginutils': 5.2.0(rollup@4.35.0)
commondir: 1.0.1
estree-walker: 2.0.2
fdir: 6.4.6(picomatch@4.0.2)
@@ -4594,7 +4594,7 @@ snapshots:
optionalDependencies:
rollup: 4.35.0
- '@rollup/pluginutils@5.1.4(rollup@4.35.0)':
+ '@rollup/pluginutils@5.2.0(rollup@4.35.0)':
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
@@ -4663,33 +4663,33 @@ snapshots:
'@rushstack/eslint-patch@1.11.0': {}
- '@sentry-internal/browser-utils@9.29.0':
+ '@sentry-internal/browser-utils@9.30.0':
dependencies:
- '@sentry/core': 9.29.0
+ '@sentry/core': 9.30.0
- '@sentry-internal/feedback@9.29.0':
+ '@sentry-internal/feedback@9.30.0':
dependencies:
- '@sentry/core': 9.29.0
+ '@sentry/core': 9.30.0
- '@sentry-internal/replay-canvas@9.29.0':
+ '@sentry-internal/replay-canvas@9.30.0':
dependencies:
- '@sentry-internal/replay': 9.29.0
- '@sentry/core': 9.29.0
+ '@sentry-internal/replay': 9.30.0
+ '@sentry/core': 9.30.0
- '@sentry-internal/replay@9.29.0':
+ '@sentry-internal/replay@9.30.0':
dependencies:
- '@sentry-internal/browser-utils': 9.29.0
- '@sentry/core': 9.29.0
+ '@sentry-internal/browser-utils': 9.30.0
+ '@sentry/core': 9.30.0
'@sentry/babel-plugin-component-annotate@3.5.0': {}
- '@sentry/browser@9.29.0':
+ '@sentry/browser@9.30.0':
dependencies:
- '@sentry-internal/browser-utils': 9.29.0
- '@sentry-internal/feedback': 9.29.0
- '@sentry-internal/replay': 9.29.0
- '@sentry-internal/replay-canvas': 9.29.0
- '@sentry/core': 9.29.0
+ '@sentry-internal/browser-utils': 9.30.0
+ '@sentry-internal/feedback': 9.30.0
+ '@sentry-internal/replay': 9.30.0
+ '@sentry-internal/replay-canvas': 9.30.0
+ '@sentry/core': 9.30.0
'@sentry/bundler-plugin-core@3.5.0':
dependencies:
@@ -4745,19 +4745,19 @@ snapshots:
- encoding
- supports-color
- '@sentry/core@9.29.0': {}
+ '@sentry/core@9.30.0': {}
- '@sentry/nextjs@9.29.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.99.9)':
+ '@sentry/nextjs@9.30.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.99.9)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/semantic-conventions': 1.34.0
'@rollup/plugin-commonjs': 28.0.1(rollup@4.35.0)
- '@sentry-internal/browser-utils': 9.29.0
- '@sentry/core': 9.29.0
- '@sentry/node': 9.29.0
- '@sentry/opentelemetry': 9.29.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)
- '@sentry/react': 9.29.0(react@19.1.0)
- '@sentry/vercel-edge': 9.29.0
+ '@sentry-internal/browser-utils': 9.30.0
+ '@sentry/core': 9.30.0
+ '@sentry/node': 9.30.0
+ '@sentry/opentelemetry': 9.30.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)
+ '@sentry/react': 9.30.0(react@19.1.0)
+ '@sentry/vercel-edge': 9.30.0
'@sentry/webpack-plugin': 3.5.0(webpack@5.99.9)
chalk: 3.0.0
next: 15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -4774,7 +4774,7 @@ snapshots:
- supports-color
- webpack
- '@sentry/node@9.29.0':
+ '@sentry/node@9.30.0':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0)
@@ -4806,14 +4806,14 @@ snapshots:
'@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.34.0
'@prisma/instrumentation': 6.8.2(@opentelemetry/api@1.9.0)
- '@sentry/core': 9.29.0
- '@sentry/opentelemetry': 9.29.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)
+ '@sentry/core': 9.30.0
+ '@sentry/opentelemetry': 9.30.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)
import-in-the-middle: 1.14.2
minimatch: 9.0.5
transitivePeerDependencies:
- supports-color
- '@sentry/opentelemetry@9.29.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)':
+ '@sentry/opentelemetry@9.30.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0)
@@ -4821,19 +4821,19 @@ snapshots:
'@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.34.0
- '@sentry/core': 9.29.0
+ '@sentry/core': 9.30.0
- '@sentry/react@9.29.0(react@19.1.0)':
+ '@sentry/react@9.30.0(react@19.1.0)':
dependencies:
- '@sentry/browser': 9.29.0
- '@sentry/core': 9.29.0
+ '@sentry/browser': 9.30.0
+ '@sentry/core': 9.30.0
hoist-non-react-statics: 3.3.2
react: 19.1.0
- '@sentry/vercel-edge@9.29.0':
+ '@sentry/vercel-edge@9.30.0':
dependencies:
'@opentelemetry/api': 1.9.0
- '@sentry/core': 9.29.0
+ '@sentry/core': 9.30.0
'@sentry/webpack-plugin@3.5.0(webpack@5.99.9)':
dependencies:
@@ -4900,17 +4900,17 @@ snapshots:
dependencies:
tslib: 2.8.1
- '@t3-oss/env-core@0.12.0(typescript@5.8.3)(zod@3.25.64)':
+ '@t3-oss/env-core@0.12.0(typescript@5.8.3)(zod@3.25.67)':
optionalDependencies:
typescript: 5.8.3
- zod: 3.25.64
+ zod: 3.25.67
- '@t3-oss/env-nextjs@0.12.0(typescript@5.8.3)(zod@3.25.64)':
+ '@t3-oss/env-nextjs@0.12.0(typescript@5.8.3)(zod@3.25.67)':
dependencies:
- '@t3-oss/env-core': 0.12.0(typescript@5.8.3)(zod@3.25.64)
+ '@t3-oss/env-core': 0.12.0(typescript@5.8.3)(zod@3.25.67)
optionalDependencies:
typescript: 5.8.3
- zod: 3.25.64
+ zod: 3.25.67
'@tailwindcss/node@4.1.10':
dependencies:
@@ -4981,12 +4981,12 @@ snapshots:
'@alloc/quick-lru': 5.2.0
'@tailwindcss/node': 4.1.10
'@tailwindcss/oxide': 4.1.10
- postcss: 8.5.5
+ postcss: 8.5.6
tailwindcss: 4.1.10
'@tanstack/eslint-plugin-query@5.78.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
- '@typescript-eslint/utils': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.29.0(jiti@2.4.2)
transitivePeerDependencies:
- supports-color
@@ -5007,15 +5007,15 @@ snapshots:
'@types/body-parser@1.19.6':
dependencies:
'@types/connect': 3.4.38
- '@types/node': 20.19.0
+ '@types/node': 20.19.1
'@types/connect@3.4.38':
dependencies:
- '@types/node': 20.19.0
+ '@types/node': 20.19.1
'@types/cors@2.8.19':
dependencies:
- '@types/node': 20.19.0
+ '@types/node': 20.19.1
'@types/eslint-scope@3.7.7':
dependencies:
@@ -5033,7 +5033,7 @@ snapshots:
'@types/express-serve-static-core@5.0.6':
dependencies:
- '@types/node': 20.19.0
+ '@types/node': 20.19.1
'@types/qs': 6.14.0
'@types/range-parser': 1.2.7
'@types/send': 0.17.5
@@ -5054,9 +5054,9 @@ snapshots:
'@types/mysql@2.15.26':
dependencies:
- '@types/node': 20.19.0
+ '@types/node': 20.19.1
- '@types/node@20.19.0':
+ '@types/node@20.19.1':
dependencies:
undici-types: 6.21.0
@@ -5066,7 +5066,7 @@ snapshots:
'@types/pg@8.6.1':
dependencies:
- '@types/node': 20.19.0
+ '@types/node': 20.19.1
pg-protocol: 1.10.0
pg-types: 2.2.0
@@ -5087,32 +5087,32 @@ snapshots:
'@types/send@0.17.5':
dependencies:
'@types/mime': 1.3.5
- '@types/node': 20.19.0
+ '@types/node': 20.19.1
'@types/serve-static@1.15.8':
dependencies:
'@types/http-errors': 2.0.5
- '@types/node': 20.19.0
+ '@types/node': 20.19.1
'@types/send': 0.17.5
'@types/shimmer@1.2.0': {}
'@types/tedious@4.0.14':
dependencies:
- '@types/node': 20.19.0
+ '@types/node': 20.19.1
'@types/ws@8.18.1':
dependencies:
- '@types/node': 20.19.0
+ '@types/node': 20.19.1
- '@typescript-eslint/eslint-plugin@8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)':
+ '@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
- '@typescript-eslint/parser': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
- '@typescript-eslint/scope-manager': 8.34.0
- '@typescript-eslint/type-utils': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
- '@typescript-eslint/utils': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
- '@typescript-eslint/visitor-keys': 8.34.0
+ '@typescript-eslint/parser': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ '@typescript-eslint/scope-manager': 8.34.1
+ '@typescript-eslint/type-utils': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ '@typescript-eslint/visitor-keys': 8.34.1
eslint: 9.29.0(jiti@2.4.2)
graphemer: 1.4.0
ignore: 7.0.5
@@ -5122,40 +5122,40 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)':
+ '@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
- '@typescript-eslint/scope-manager': 8.34.0
- '@typescript-eslint/types': 8.34.0
- '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3)
- '@typescript-eslint/visitor-keys': 8.34.0
+ '@typescript-eslint/scope-manager': 8.34.1
+ '@typescript-eslint/types': 8.34.1
+ '@typescript-eslint/typescript-estree': 8.34.1(typescript@5.8.3)
+ '@typescript-eslint/visitor-keys': 8.34.1
debug: 4.4.1
eslint: 9.29.0(jiti@2.4.2)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/project-service@8.34.0(typescript@5.8.3)':
+ '@typescript-eslint/project-service@8.34.1(typescript@5.8.3)':
dependencies:
- '@typescript-eslint/tsconfig-utils': 8.34.0(typescript@5.8.3)
- '@typescript-eslint/types': 8.34.0
+ '@typescript-eslint/tsconfig-utils': 8.34.1(typescript@5.8.3)
+ '@typescript-eslint/types': 8.34.1
debug: 4.4.1
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/scope-manager@8.34.0':
+ '@typescript-eslint/scope-manager@8.34.1':
dependencies:
- '@typescript-eslint/types': 8.34.0
- '@typescript-eslint/visitor-keys': 8.34.0
+ '@typescript-eslint/types': 8.34.1
+ '@typescript-eslint/visitor-keys': 8.34.1
- '@typescript-eslint/tsconfig-utils@8.34.0(typescript@5.8.3)':
+ '@typescript-eslint/tsconfig-utils@8.34.1(typescript@5.8.3)':
dependencies:
typescript: 5.8.3
- '@typescript-eslint/type-utils@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)':
+ '@typescript-eslint/type-utils@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
- '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3)
- '@typescript-eslint/utils': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ '@typescript-eslint/typescript-estree': 8.34.1(typescript@5.8.3)
+ '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
debug: 4.4.1
eslint: 9.29.0(jiti@2.4.2)
ts-api-utils: 2.1.0(typescript@5.8.3)
@@ -5163,14 +5163,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/types@8.34.0': {}
+ '@typescript-eslint/types@8.34.1': {}
- '@typescript-eslint/typescript-estree@8.34.0(typescript@5.8.3)':
+ '@typescript-eslint/typescript-estree@8.34.1(typescript@5.8.3)':
dependencies:
- '@typescript-eslint/project-service': 8.34.0(typescript@5.8.3)
- '@typescript-eslint/tsconfig-utils': 8.34.0(typescript@5.8.3)
- '@typescript-eslint/types': 8.34.0
- '@typescript-eslint/visitor-keys': 8.34.0
+ '@typescript-eslint/project-service': 8.34.1(typescript@5.8.3)
+ '@typescript-eslint/tsconfig-utils': 8.34.1(typescript@5.8.3)
+ '@typescript-eslint/types': 8.34.1
+ '@typescript-eslint/visitor-keys': 8.34.1
debug: 4.4.1
fast-glob: 3.3.3
is-glob: 4.0.3
@@ -5181,20 +5181,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)':
+ '@typescript-eslint/utils@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0(jiti@2.4.2))
- '@typescript-eslint/scope-manager': 8.34.0
- '@typescript-eslint/types': 8.34.0
- '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3)
+ '@typescript-eslint/scope-manager': 8.34.1
+ '@typescript-eslint/types': 8.34.1
+ '@typescript-eslint/typescript-estree': 8.34.1(typescript@5.8.3)
eslint: 9.29.0(jiti@2.4.2)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/visitor-keys@8.34.0':
+ '@typescript-eslint/visitor-keys@8.34.1':
dependencies:
- '@typescript-eslint/types': 8.34.0
+ '@typescript-eslint/types': 8.34.1
eslint-visitor-keys: 4.2.1
'@unrs/resolver-binding-android-arm-eabi@1.9.0':
@@ -5491,7 +5491,7 @@ snapshots:
browserslist@4.25.0:
dependencies:
caniuse-lite: 1.0.30001723
- electron-to-chromium: 1.5.167
+ electron-to-chromium: 1.5.170
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.0)
@@ -5652,7 +5652,7 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
- electron-to-chromium@1.5.167: {}
+ electron-to-chromium@1.5.170: {}
emoji-regex@9.2.2: {}
@@ -5772,12 +5772,12 @@ snapshots:
dependencies:
'@next/eslint-plugin-next': 15.3.3
'@rushstack/eslint-patch': 1.11.0
- '@typescript-eslint/eslint-plugin': 8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
- '@typescript-eslint/parser': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ '@typescript-eslint/eslint-plugin': 8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ '@typescript-eslint/parser': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.29.0(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.29.0(jiti@2.4.2))
- eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.4.2))
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.4.2))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.29.0(jiti@2.4.2))
eslint-plugin-react: 7.37.5(eslint@9.29.0(jiti@2.4.2))
eslint-plugin-react-hooks: 5.2.0(eslint@9.29.0(jiti@2.4.2))
@@ -5811,22 +5811,22 @@ snapshots:
tinyglobby: 0.2.14
unrs-resolver: 1.9.0
optionalDependencies:
- eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.4.2))
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.4.2))
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.4.2)):
+ eslint-module-utils@2.12.0(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.4.2)):
dependencies:
debug: 3.2.7
optionalDependencies:
- '@typescript-eslint/parser': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ '@typescript-eslint/parser': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.29.0(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.29.0(jiti@2.4.2))
transitivePeerDependencies:
- supports-color
- eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.4.2)):
+ eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.4.2)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -5837,7 +5837,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.29.0(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.4.2))
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.4.2))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -5849,7 +5849,7 @@ snapshots:
string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0
optionalDependencies:
- '@typescript-eslint/parser': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ '@typescript-eslint/parser': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
@@ -5874,7 +5874,7 @@ snapshots:
safe-regex-test: 1.1.0
string.prototype.includes: 2.0.1
- eslint-plugin-prettier@5.4.1(@types/eslint@9.6.1)(eslint-config-prettier@10.1.5(eslint@9.29.0(jiti@2.4.2)))(eslint@9.29.0(jiti@2.4.2))(prettier@3.5.3):
+ eslint-plugin-prettier@5.5.0(@types/eslint@9.6.1)(eslint-config-prettier@10.1.5(eslint@9.29.0(jiti@2.4.2)))(eslint@9.29.0(jiti@2.4.2))(prettier@3.5.3):
dependencies:
eslint: 9.29.0(jiti@2.4.2)
prettier: 3.5.3
@@ -6326,7 +6326,7 @@ snapshots:
jest-worker@27.5.1:
dependencies:
- '@types/node': 20.19.0
+ '@types/node': 20.19.1
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -6657,7 +6657,7 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
- postcss@8.5.5:
+ postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
@@ -6708,7 +6708,7 @@ snapshots:
react: 19.1.0
scheduler: 0.26.0
- react-hook-form@7.58.0(react@19.1.0):
+ react-hook-form@7.58.1(react@19.1.0):
dependencies:
react: 19.1.0
@@ -7087,10 +7087,10 @@ snapshots:
jest-worker: 27.5.1
schema-utils: 4.3.2
serialize-javascript: 6.0.2
- terser: 5.42.0
+ terser: 5.43.0
webpack: 5.99.9
- terser@5.42.0:
+ terser@5.43.0:
dependencies:
'@jridgewell/source-map': 0.3.6
acorn: 8.15.0
@@ -7162,11 +7162,11 @@ snapshots:
possible-typed-array-names: 1.1.0
reflect.getprototypeof: 1.0.10
- typescript-eslint@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3):
+ typescript-eslint@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3):
dependencies:
- '@typescript-eslint/eslint-plugin': 8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
- '@typescript-eslint/parser': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
- '@typescript-eslint/utils': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ '@typescript-eslint/eslint-plugin': 8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ '@typescript-eslint/parser': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.29.0(jiti@2.4.2)
typescript: 5.8.3
transitivePeerDependencies:
@@ -7358,4 +7358,4 @@ snapshots:
yocto-queue@0.1.0: {}
- zod@3.25.64: {}
+ zod@3.25.67: {}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index f56e11d..63a99a8 100755
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -390,7 +390,8 @@ const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
return (
{
const SignInLayout = ({
children,
}: Readonly<{ children: React.ReactNode }>) => {
- return {children}
;
+ return (
+
+ {children}
+
+ );
};
export default SignInLayout;
diff --git a/src/app/status/list/page.tsx b/src/app/status/list/page.tsx
index a3878e2..eaf66fd 100644
--- a/src/app/status/list/page.tsx
+++ b/src/app/status/list/page.tsx
@@ -1,6 +1,6 @@
'use server';
-import { StatusList } from '@/components/status/List';
+import { StatusList } from '@/components/status';
import { getUser, getRecentUsersWithStatuses } from '@/lib/actions';
import { redirect } from 'next/navigation';
diff --git a/src/components/default/header/AvatarDropdown.tsx b/src/components/default/header/AvatarDropdown.tsx
index 7378279..b72240d 100644
--- a/src/components/default/header/AvatarDropdown.tsx
+++ b/src/components/default/header/AvatarDropdown.tsx
@@ -33,7 +33,7 @@ const AvatarDropdown = () => {
diff --git a/src/components/default/header/index.tsx b/src/components/default/header/index.tsx
index 2b8cecb..9252ca9 100644
--- a/src/components/default/header/index.tsx
+++ b/src/components/default/header/index.tsx
@@ -1,5 +1,4 @@
'use client';
-
import Image from 'next/image';
import Link from 'next/link';
import { ThemeToggle, useTVMode } from '@/components/context';
@@ -9,45 +8,64 @@ import AvatarDropdown from './AvatarDropdown';
const Header = () => {
const { tvMode } = useTVMode();
const { isAuthenticated } = useAuth();
- return tvMode ? (
-
-
-
-
- {isAuthenticated &&
}
-
-
+
+ // Controls component for both modes
+ const Controls = () => (
+
+
+ {isAuthenticated &&
}
- ) : (
-
-
-
-
- {isAuthenticated &&
}
+ );
+
+ if (tvMode) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {/* Left spacer for perfect centering */}
+
+
+ {/* Centered logo and title */}
+
+
+
+
+ Tech Tracker
+
+
+
+
+ {/* Right-aligned controls */}
+
+
+
-
-
-
- Tech Tracker
-
-
);
};
+
export default Header;
diff --git a/src/components/status/ConnectionStatus.tsx b/src/components/status/ConnectionStatus.tsx
new file mode 100644
index 0000000..1b94636
--- /dev/null
+++ b/src/components/status/ConnectionStatus.tsx
@@ -0,0 +1,68 @@
+'use client';
+import { Wifi, WifiOff, RefreshCw } from 'lucide-react';
+import { Badge, Button } from '@/components/ui';
+import type { ConnectionStatus as ConnectionStatusType } from '@/lib/hooks';
+
+type ConnectionStatusProps = {
+ status: ConnectionStatusType;
+ onReconnect?: () => void;
+ showAsButton?: boolean;
+ className?: string;
+}
+
+const getConnectionIcon = (status: ConnectionStatusType) => {
+ switch (status) {
+ case 'connected':
+ return
;
+ case 'connecting':
+ return
;
+ case 'disconnected':
+ return
;
+ case 'updating':
+ return
;
+ }
+};
+
+const getConnectionText = (status: ConnectionStatusType) => {
+ switch (status) {
+ case 'connected':
+ return 'Connected';
+ case 'connecting':
+ return 'Connecting...';
+ case 'disconnected':
+ return 'Disconnected';
+ case 'updating':
+ return 'Updating...';
+ }
+};
+
+export const ConnectionStatus = ({
+ status,
+ onReconnect,
+ showAsButton = false,
+ className = '',
+}: ConnectionStatusProps) => {
+ if (showAsButton && status === 'disconnected' && onReconnect) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {getConnectionIcon(status)}
+ {getConnectionText(status)}
+
+ );
+};
diff --git a/src/components/status/List.tsx b/src/components/status/List.tsx
old mode 100755
new mode 100644
index 54db958..326fbbd
--- a/src/components/status/List.tsx
+++ b/src/components/status/List.tsx
@@ -1,35 +1,28 @@
'use client';
-import { useState, useEffect, useRef } from 'react';
+import { useState, useEffect } from 'react';
import { useAuth, useTVMode } from '@/components/context';
-import {
- getRecentUsersWithStatuses,
- updateStatuses,
- updateUserStatus,
- type UserWithStatus,
-} from '@/lib/hooks';
+import type { UserWithStatus } from '@/lib/hooks';
import { BasedAvatar, Drawer, DrawerTrigger, Loading } from '@/components/ui';
import { StatusMessage, SubmitButton } from '@/components/default';
-import { toast } from 'sonner';
-import { HistoryDrawer } from '@/components/status';
+import { ConnectionStatus, HistoryDrawer } from '@/components/status';
import type { Profile } from '@/utils/supabase';
import { makeConditionalClassName } from '@/lib/utils';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
-import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
-import { RefreshCw, Clock, Wifi, WifiOff, Calendar } from 'lucide-react';
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { QueryErrorCodes } from '@/components/context';
-import { createClient } from '@/utils/supabase';
-import type { RealtimeChannel } from '@supabase/supabase-js';
+import { RefreshCw, Clock, Calendar } from 'lucide-react';
+import { useStatusData, useSharedStatusSubscription } from '@/lib/hooks';
+import { formatTime, formatDate } from '@/lib/utils';
+import Link from 'next/link';
-type ListProps = { initialStatuses: UserWithStatus[] };
+type ListProps = {
+ initialStatuses: UserWithStatus[]
+};
export const StatusList = ({ initialStatuses = [] }: ListProps) => {
const { isAuthenticated } = useAuth();
const { tvMode } = useTVMode();
- const queryClient = useQueryClient();
const [selectedUsers, setSelectedUsers] = useState
([]);
const [selectAll, setSelectAll] = useState(false);
@@ -38,216 +31,64 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
useState(null);
const [updateStatusMessage, setUpdateStatusMessage] = useState('');
- const [connectionStatus, setConnectionStatus] = useState<
- 'connecting' | 'connected' | 'disconnected' | 'updating'
- >('connecting');
- const [newStatuses, setNewStatuses] = useState>(new Set());
- const channelRef = useRef(null);
- const supabaseRef = useRef(createClient());
-
const {
data: usersWithStatuses = initialStatuses,
isLoading: loading,
error,
refetch,
- isFetching,
- dataUpdatedAt,
- } = useQuery({
- queryKey: ['users-with-statuses'],
- queryFn: async () => {
- try {
- const response = await getRecentUsersWithStatuses();
- if (!response.success) throw new Error(response.error);
- return response.data;
- } catch (error) {
- toast.error(`Error fetching technicians: ${error as Error}`)
- throw error;
- }
- },
- enabled: isAuthenticated,
- refetchInterval: 30000, // 30 Second interval as we should rely on subscription.
- refetchOnWindowFocus: true,
- refetchOnMount: true,
+ newStatuses,
+ updateStatusMutation,
+ } = useStatusData({
initialData: initialStatuses,
- meta: { errCode: QueryErrorCodes.USERS_FETCH_FAILED },
+ enabled: isAuthenticated,
});
- useEffect(() => {
- if (!isAuthenticated) return;
-
- const maxReconnectAttempts = 3;
- let reconnectAttempts = 0;
- let reconnectTimeout: NodeJS.Timeout;
- let isComponentMounted = true;
- let currentChannel: RealtimeChannel | null = null;
-
- const setUpRealtimeConnection = () => {
- if (!isComponentMounted) return;
-
- if (currentChannel) {
- supabaseRef.current.removeChannel(currentChannel)
- .catch((error) => {
- setConnectionStatus('disconnected');
- console.error(`Error unsubscribing: ${error}`);
- })
- currentChannel = null;
- }
-
- setConnectionStatus('connecting');
- const channel = supabaseRef.current
- .channel('status_updates')
- .on('broadcast', { event: 'status_updated' }, () => {
- refetch().catch((error) => {
- console.error(`Error refetching statuses: ${error as Error}`)
- });
- })
- .subscribe((status) => {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
- if (status === 'SUBSCRIBED') {
- setConnectionStatus('connected');
- reconnectAttempts = 0;
- // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
- } else if (status === 'CHANNEL_ERROR') {
- setConnectionStatus('disconnected');
- // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
- } else if (status === 'CLOSED') {
- setConnectionStatus('disconnected')
- if (isComponentMounted && reconnectAttempts < maxReconnectAttempts) {
- reconnectAttempts++;
- const delay = 2000 * reconnectAttempts;
- console.log(
- `Reconnecting after close.
- ${reconnectAttempts} attempts of
- ${maxReconnectAttempts} in ${delay}ms
- `);
- if (reconnectTimeout) clearTimeout(reconnectTimeout);
- reconnectTimeout = setTimeout(() => {
- if (isComponentMounted) setUpRealtimeConnection();
- }, delay);
- } else {
- console.warn('Max reconnection attempts reached or component is not mounted.');
- setConnectionStatus('disconnected');
- }
- }
- });
- currentChannel = channel;
- channelRef.current = channel;
- };
-
- const initialTimeout = setTimeout(() => {
- if (isComponentMounted) setUpRealtimeConnection();
- }, 1000);
-
- return () => {
- isComponentMounted = false;
- if (initialTimeout) clearTimeout(initialTimeout);
- if (reconnectTimeout) clearTimeout(reconnectTimeout);
- if (currentChannel) {
- supabaseRef.current.removeChannel(currentChannel)
- .catch((error) => {
- console.error(`Error unsubscribing: ${error as Error}`)
- });
- channelRef.current = null;
- }
- setConnectionStatus('disconnected');
- }
- }, [isAuthenticated, refetch]);
-
- const updateStatusMutation = useMutation({
-
- mutationFn: async ({
- usersWithStatuses,
- status,
- }: {
- usersWithStatuses: UserWithStatus[];
- status: string;
- }) => {
- const previousConnectionStatus = connectionStatus;
- try {
- if (usersWithStatuses.length <= 0) {
- const result = await updateUserStatus(status);
- if (!result.success) throw new Error(result.error);
- return result.data;
- } else {
- const result = await updateStatuses(usersWithStatuses, status);
- if (!result.success) throw new Error(result.error);
- return result.data;
- }
- } catch (error) {
- console.error(`Error updating statuses: ${error as Error}`);
- }
- },
- meta: { errCode: QueryErrorCodes.STATUS_UPDATE_FAILED },
-
- onMutate: async ({ usersWithStatuses, status }) => {
- // Optimistic update logic
- await queryClient.cancelQueries({ queryKey: ['users-with-statuses']} );
- const previousData = queryClient.getQueryData([
- 'users-with-statuses',
- ]);
- if (previousData && usersWithStatuses.length > 0) {
- const now = new Date().toISOString();
- const optimisticData = previousData.map((userStatus) => {
- if (usersWithStatuses.some((selected) => selected.user.id === userStatus.user.id))
- return { ...userStatus, status, created_at: now};
- return userStatus;
- });
- queryClient.setQueryData(['users-with-statuses'], optimisticData);
-
- // Add animation to optimisticly updated statuses
- const updatedStatuses = usersWithStatuses;
- setNewStatuses((prev) => new Set([...prev, ...updatedStatuses]))
- setTimeout(() => {
- setNewStatuses((prev) => {
- const updated = new Set(prev);
- updatedStatuses.forEach((updatedStatus) => updated.delete(updatedStatus));
- return updated;
- });
- }, 1000)
- }
- return { previousData };
- },
-
- onSuccess: (data) => {
- queryClient.invalidateQueries({ queryKey: ['users-with-statuses']} )
- .catch((error) => console.error(`Error invalidating query: ${error}`));
- if (!data) return;
- data.forEach((statusUpdate) => {
- toast.success(`
- ${statusUpdate.user.full_name}'s status updated to '${statusUpdate.status}'.
- `);
- });
- setSelectedUsers([]);
- setStatusInput('');
- },
-
- onError: (error, _variables, context) => {
- if (context?.previousData)
- queryClient.setQueryData(['users-with-statuses'], context.previousData);
- toast.error(`Error updating statuses: ${error}`);
- },
+ // In your StatusList component
+ const { connectionStatus, connect: reconnect } = useSharedStatusSubscription(() => {
+ refetch().catch((error) => {
+ console.error('Error refetching statuses:', error);
+ });
});
+ //const { connectionStatus, connect: reconnect } = useStatusSubscription({
+ //enabled: isAuthenticated,
+ //onStatusUpdate: () => {
+ //refetch().catch((error) => {
+ //console.error('Error refetching statuses:', error);
+ //});
+ //},
+ //});
+
const handleUpdateStatus = () => {
if (!isAuthenticated) {
- setUpdateStatusMessage('You must be signed in to update technican statuses!')
- return;
- } else if (!statusInput.trim()) {
- setUpdateStatusMessage('Your status must be in between 3 & 80 characters long!');
+ setUpdateStatusMessage(
+ 'Error: You must be signed in to update technician statuses!'
+ );
return;
}
+
+ if (statusInput.length < 3 || statusInput.length > 80) {
+ setUpdateStatusMessage(
+ 'Error: Your status must be between 3 & 80 characters long!'
+ );
+ return;
+ }
+
updateStatusMutation.mutate({
usersWithStatuses: selectedUsers,
status: statusInput.trim(),
});
- };
+ setSelectedUsers([]);
+ setStatusInput('');
+ setUpdateStatusMessage('');
+ };
const handleCheckboxChange = (user: UserWithStatus) => {
setSelectedUsers((prev) =>
prev.some((u) => u.user.id === user.user.id)
? prev.filter((prevUser) => prevUser.user.id !== user.user.id)
- : [...prev, user],
+ : [...prev, user]
);
};
@@ -263,52 +104,10 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
useEffect(() => {
setSelectAll(
selectedUsers.length === usersWithStatuses.length &&
- usersWithStatuses.length > 0,
+ usersWithStatuses.length > 0
);
}, [selectedUsers.length, usersWithStatuses.length]);
- const getConnectionIcon = () => {
- switch (connectionStatus) {
- case 'connected':
- return ;
- case 'connecting':
- return ;
- case 'disconnected':
- return ;
- case 'updating':
- return ;
- }
- };
-
- const getConnectionText = () => {
- switch (connectionStatus) {
- case 'connected':
- return 'Connected';
- case 'connecting':
- return 'Connecting...';
- case 'disconnected':
- return 'Disconnected';
- case 'updating':
- return 'Updating...';
- }
- };
-
- const formatTime = (timestamp: string) => {
- const date = new Date(timestamp);
- const time = date.toLocaleTimeString('en-US', {
- hour: 'numeric',
- minute: 'numeric',
- });
- return `${time}`;
- };
-
- const formatDate = (timestamp: string) => {
- const date = new Date(timestamp);
- const day = date.getDate();
- const month = date.toLocaleString('default', { month: 'long' });
- return `${month} ${day}`;
- };
-
if (loading) {
return (
@@ -360,38 +159,56 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
return (
-
+
+
+
+
+
+ {!tvMode && (
+
+
Miss the old table?
+
+ Find it here!
+
+
+ )}
+
-
-
-
-
-
- {getConnectionIcon()}
- {getConnectionText()}
-
{usersWithStatuses.map((userWithStatus) => {
- const isSelected = selectedUsers.some((u) => u.user.id === userWithStatus.user.id);
+ const isSelected = selectedUsers.some(
+ (u) => u.user.id === userWithStatus.user.id
+ );
const isNewStatus = newStatuses.has(userWithStatus);
- const isUpdatedByOther = userWithStatus.updated_by &&
+ const isUpdatedByOther =
+ userWithStatus.updated_by &&
userWithStatus.updated_by.id !== userWithStatus.user.id;
+
return (
@@ -401,7 +218,9 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
{!tvMode && (
handleCheckboxChange(userWithStatus)}
+ onCheckedChange={() =>
+ handleCheckboxChange(userWithStatus)
+ }
onClick={(e) => e.stopPropagation()}
/>
)}
@@ -411,7 +230,9 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
className={tvMode ? 'w-24 h-24' : 'w-16 h-16'}
/>
-
+
{userWithStatus.user.full_name}
{isUpdatedByOther && (
@@ -421,7 +242,9 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
fullName={userWithStatus.updated_by?.full_name}
className='w-5 h-5'
/>
-
+
Updated by {userWithStatus.updated_by?.full_name}
@@ -436,7 +259,9 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
-
+
{formatDate(userWithStatus.created_at)}
@@ -444,7 +269,6 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
-
@@ -454,14 +278,16 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
transition-colors cursor-pointer text-left
${tvMode ? 'text-4xl' : 'text-xl'}
`}
- onClick={() => setSelectedHistoryUser(userWithStatus.user)}
+ onClick={() =>
+ setSelectedHistoryUser(userWithStatus.user)
+ }
>
{userWithStatus.status}
- {selectedHistoryUser === userWithStatus.user && (
-
- )}
+ {selectedHistoryUser === userWithStatus.user && (
+
+ )}
@@ -471,13 +297,77 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
{usersWithStatuses.length === 0 && (
-
+
No status updates have been made in the past day.
)}
+ {!tvMode && (
+
+
+
Update Status
+
+
+ setStatusInput(e.target.value)}
+ onKeyDown={(e) => {
+ if (
+ e.key === 'Enter' &&
+ !e.shiftKey &&
+ !updateStatusMutation.isPending
+ ) {
+ e.preventDefault();
+ handleUpdateStatus();
+ }
+ }}
+ />
+
+ {selectedUsers.length > 0
+ ? `Update status for ${selectedUsers.length}
+ ${selectedUsers.length > 1 ? 'users' : 'user'}`
+ : 'Update status'
+ }
+
+
+ {updateStatusMessage &&
+ (updateStatusMessage.includes('Error') ||
+ updateStatusMessage.includes('error') ||
+ updateStatusMessage.includes('failed') ||
+ updateStatusMessage.includes('invalid') ? (
+
+ ) : (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ )}
);
-
};
diff --git a/src/components/status/StatusList.tsx b/src/components/status/StatusList.tsx
deleted file mode 100644
index 1bb4cb4..0000000
--- a/src/components/status/StatusList.tsx
+++ /dev/null
@@ -1,564 +0,0 @@
-'use client';
-import { useState, useEffect, useRef } from 'react';
-import { useAuth, useTVMode } from '@/components/context';
-import {
- getRecentUsersWithStatuses,
- updateStatuses,
- updateUserStatus,
- type UserWithStatus,
-} from '@/lib/hooks';
-import { BasedAvatar, Drawer, DrawerTrigger, Loading } from '@/components/ui';
-import { SubmitButton } from '@/components/default';
-import { toast } from 'sonner';
-import { HistoryDrawer } from '@/components/status';
-import type { Profile } from '@/utils/supabase';
-import { makeConditionalClassName } from '@/lib/utils';
-import { Card, CardContent, CardHeader } from '@/components/ui/card';
-import { Badge } from '@/components/ui/badge';
-import { Checkbox } from '@/components/ui/checkbox';
-import { Input } from '@/components/ui/input';
-import { Button } from '@/components/ui/button';
-import { RefreshCw, Clock, Wifi, WifiOff } from 'lucide-react';
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { QueryErrorCodes } from '@/components/context';
-import { createClient } from '@/utils/supabase';
-import type { RealtimeChannel } from '@supabase/supabase-js';
-
-type StatusListProps = {
- initialStatuses: UserWithStatus[];
-};
-
-export const StatusList = ({ initialStatuses = [] }: StatusListProps) => {
- // Fixed props destructuring
- const { isAuthenticated } = useAuth();
- const { tvMode } = useTVMode();
- const queryClient = useQueryClient();
-
- const [selectedUsers, setSelectedUsers] = useState
([]);
- const [selectAll, setSelectAll] = useState(false);
- const [statusInput, setStatusInput] = useState('');
- const [selectedHistoryUser, setSelectedHistoryUser] =
- useState(null);
-
- const [connectionStatus, setConnectionStatus] = useState<
- 'connecting' | 'connected' | 'disconnected'
- >('connecting');
- const [newStatusIds, setNewStatusIds] = useState>(new Set());
- const channelRef = useRef(null);
- const supabaseRef = useRef(createClient());
-
- // Keep all your existing React Query code exactly as is
- const {
- data: usersWithStatuses = initialStatuses,
- isLoading: loading,
- error,
- refetch,
- isFetching,
- dataUpdatedAt,
- } = useQuery({
- queryKey: ['users-with-statuses'],
- queryFn: async () => {
- try {
- const response = await getRecentUsersWithStatuses();
- if (!response.success) throw new Error(response.error);
- return response.data;
- } catch (error) {
- toast.error(
- `Error fetching technicians: ${error instanceof Error ? error.message : 'Unknown error'}`,
- );
- throw error;
- }
- },
- enabled: isAuthenticated,
- refetchInterval: 30000, // Changed to 30 seconds as backup
- refetchOnWindowFocus: true,
- refetchOnMount: true,
- initialData: initialStatuses,
- meta: { errCode: QueryErrorCodes.USERS_FETCH_FAILED },
- });
-
- // Add this new useEffect for realtime enhancement
- useEffect(() => {
- if (!isAuthenticated) return;
-
- let reconnectAttempts = 0;
- const maxReconnectAttempts = 3;
- let reconnectTimeout: NodeJS.Timeout;
- let isComponentMounted = true;
- let currentChannel: RealtimeChannel | null = null;
-
- const setUpRealtimeConnection = () => {
- if (!isComponentMounted) return;
- if (currentChannel) {
- supabaseRef.current.removeChannel(currentChannel).catch((error) => {
- console.error(`Error unsubscribing: ${error}`);
- });
- currentChannel = null;
- }
- setConnectionStatus('connecting');
- const channel = supabaseRef.current
- .channel('status_updates')
- .on('broadcast', { event: 'status_updated' }, (payload) => {
- console.log('Realtime update received, triggering refetch...');
- refetch().catch((error) => {
- console.error(`Error refetching: ${error}`);
- });
- })
- .subscribe((status) => {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
- if (status === 'SUBSCRIBED') {
- console.log('Realtime connection established');
- setConnectionStatus('connected');
- reconnectAttempts = 0;
- // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
- } else if (status === 'CHANNEL_ERROR') {
- console.log('Realtime connection failed, relying on polling');
- setConnectionStatus('disconnected');
- // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
- } else if (status === 'CLOSED') {
- console.log('Realtime connection closed');
- setConnectionStatus('disconnected');
- if (
- isComponentMounted &&
- reconnectAttempts < maxReconnectAttempts
- ) {
- reconnectAttempts++;
- const delay = 2000 * reconnectAttempts;
-
- console.log(
- `Reconnecting after close ${reconnectAttempts}/${maxReconnectAttempts} in ${delay}ms`,
- );
-
- if (reconnectTimeout) {
- clearTimeout(reconnectTimeout);
- }
-
- reconnectTimeout = setTimeout(() => {
- if (isComponentMounted) {
- setUpRealtimeConnection();
- }
- }, delay);
- } else {
- console.log(
- 'Max reconnection attempts reached or component unmounted',
- );
- }
- }
- });
- currentChannel = channel;
- channelRef.current = channel;
- };
-
- const initialTimeout = setTimeout(() => {
- if (isComponentMounted) {
- setUpRealtimeConnection();
- }
- }, 1000);
-
- return () => {
- isComponentMounted = false;
- if (initialTimeout) clearTimeout(initialTimeout);
- if (reconnectTimeout) clearTimeout(reconnectTimeout);
-
- if (currentChannel) {
- console.log('Cleaning up realtime connection...');
- supabaseRef.current.removeChannel(currentChannel).catch((error) => {
- console.error(`Error unsubscribing: ${error}`);
- });
- channelRef.current = null;
- }
- };
- }, [isAuthenticated, refetch]);
-
- // Updated mutation
- const updateStatusMutation = useMutation({
- mutationFn: async ({
- usersWithStatuses,
- status,
- }: {
- usersWithStatuses: UserWithStatus[];
- status: string;
- }) => {
- if (usersWithStatuses.length === 0) {
- const result = await updateUserStatus(status);
- if (!result.success) throw new Error(result.error);
- return { type: 'single', result };
- } else {
- const result = await updateStatuses(usersWithStatuses, status);
- if (!result.success) throw new Error(result.error);
- return { type: 'multiple', result, count: usersWithStatuses.length };
- }
- },
- meta: { errCode: QueryErrorCodes.STATUS_UPDATE_FAILED },
- onMutate: async ({ usersWithStatuses, status }) => {
- // Optimistic update logic
- await queryClient.cancelQueries({ queryKey: ['users-with-statuses'] });
- const previousData = queryClient.getQueryData([
- 'users-with-statuses',
- ]);
-
- if (previousData && usersWithStatuses.length > 0) {
- const now = new Date().toISOString();
- const optimisticData = previousData.map((userStatus) => {
- if (
- usersWithStatuses.some(
- (selected) => selected.user.id === userStatus.user.id,
- )
- ) {
- return {
- ...userStatus,
- status,
- created_at: now,
- };
- }
- return userStatus;
- });
- queryClient.setQueryData(['users-with-statuses'], optimisticData);
-
- // Add animation for optimistic updates
- const updatedIds = usersWithStatuses.map((u) => u.user.id);
- setNewStatusIds((prev) => new Set([...prev, ...updatedIds]));
-
- // Remove animation after 1 second
- setTimeout(() => {
- setNewStatusIds((prev) => {
- const updated = new Set(prev);
- updatedIds.forEach((id) => updated.delete(id));
- return updated;
- });
- }, 1000);
- }
-
- return { previousData };
- },
- onSuccess: (data) => {
- // Handle success in the mutation function
- void queryClient.invalidateQueries({ queryKey: ['users-with-statuses'] }); // Fixed floating promise
-
- if (data.type === 'single') {
- toast.success('Status updated for signed in user.');
- } else {
- toast.success(`Status updated for ${data.count} selected users.`);
- }
-
- setSelectedUsers([]);
- setStatusInput('');
- },
- onError: (error, _variables, context) => {
- // Fixed unused variables
- // Rollback optimistic update
- if (context?.previousData) {
- queryClient.setQueryData(['users-with-statuses'], context.previousData);
- }
- // Error handling is done in the global mutation cache
- console.error('Status update failed:', error);
- },
- });
-
- const handleUpdateStatus = () => {
- if (!isAuthenticated) {
- toast.error('You must be signed in to update statuses.');
- return;
- }
- if (!statusInput.trim()) {
- toast.error('Please enter a valid status.');
- return;
- }
-
- updateStatusMutation.mutate({
- usersWithStatuses: selectedUsers,
- status: statusInput.trim(),
- });
- };
-
- const handleCheckboxChange = (user: UserWithStatus) => {
- setSelectedUsers((prev) =>
- prev.some((u) => u.user.id === user.user.id)
- ? prev.filter((prevUser) => prevUser.user.id !== user.user.id)
- : [...prev, user],
- );
- };
-
- const handleSelectAllChange = () => {
- if (selectAll) {
- setSelectedUsers([]);
- } else {
- setSelectedUsers(usersWithStatuses);
- }
- setSelectAll(!selectAll);
- };
-
- useEffect(() => {
- setSelectAll(
- selectedUsers.length === usersWithStatuses.length &&
- usersWithStatuses.length > 0,
- );
- }, [selectedUsers.length, usersWithStatuses.length]);
-
- const getConnectionIcon = () => {
- switch (connectionStatus) {
- case 'connected':
- return ;
- case 'connecting':
- return ;
- case 'disconnected':
- return ;
- }
- };
-
- const getConnectionText = () => {
- switch (connectionStatus) {
- case 'connected':
- return 'Connected';
- case 'connecting':
- return 'Connecting...';
- case 'disconnected':
- return 'Disconnected';
- }
- };
-
- const formatTime = (timestamp: string) => {
- const date = new Date(timestamp);
- const time = date.toLocaleTimeString('en-US', {
- hour: 'numeric',
- minute: 'numeric',
- });
- const day = date.getDate();
- const month = date.toLocaleString('default', { month: 'long' });
- return `${time} - ${month} ${day}`;
- };
-
- if (loading) {
- return (
-
-
-
- );
- }
-
- if (error) {
- return (
-
-
Error loading status updates
-
-
- );
- }
-
- const containerClassName = makeConditionalClassName({
- context: tvMode,
- defaultClassName: 'mx-auto space-y-4',
- on: 'lg:w-11/12 w-full mt-15',
- off: 'w-5/6',
- });
-
- const cardClassName = makeConditionalClassName({
- context: tvMode,
- defaultClassName: 'transition-all duration-300 hover:shadow-md',
- on: 'lg:text-4xl',
- off: 'lg:text-base',
- });
-
- return (
-
- {/* Status Header */}
-
-
-
- Tech Status
-
- {isFetching ? (
-
-
- Updating...
-
- ) : (
-
- {getConnectionIcon()}
- {getConnectionText()}
-
- )}
-
- {!tvMode && usersWithStatuses.length > 0 && (
-
-
-
-
- )}
-
-
- {/* Status Cards */}
-
- {usersWithStatuses.map((userWithStatus) => {
- const isSelected = selectedUsers.some(
- (u) => u.user.id === userWithStatus.user.id,
- );
- const isNewStatus = newStatusIds.has(userWithStatus.user.id);
- const isUpdatedByOther =
- userWithStatus.updated_by &&
- userWithStatus.updated_by.id !== userWithStatus.user.id;
-
- return (
-
-
-
-
- {!tvMode && (
-
- handleCheckboxChange(userWithStatus)
- }
- onClick={(e) => e.stopPropagation()}
- />
- )}
-
-
-
- {userWithStatus.user.full_name ?? 'Unknown User'}
-
- {isUpdatedByOther && (
-
-
- {userWithStatus.updated_by && (
-
- Updated by {userWithStatus.updated_by.full_name}
-
- )}
-
- )}
-
-
-
-
-
- {formatTime(userWithStatus.created_at)}
-
-
-
-
-
-
-
-
- setSelectedHistoryUser(userWithStatus.user)
- }
- >
-
{userWithStatus.status}
-
-
- {selectedHistoryUser === userWithStatus.user && (
-
- )}
-
-
-
- );
- })}
-
-
- {usersWithStatuses.length === 0 && (
-
-
- No status updates yet
-
-
- )}
-
- {/* Status Update Input */}
- {!tvMode && (
-
-
-
Update Status
-
- setStatusInput(e.target.value)}
- onKeyDown={(e) => {
- if (
- e.key === 'Enter' &&
- !e.shiftKey &&
- !updateStatusMutation.isPending
- ) {
- e.preventDefault();
- handleUpdateStatus();
- }
- }}
- disabled={updateStatusMutation.isPending}
- />
-
- {updateStatusMutation.isPending
- ? 'Updating...'
- : selectedUsers.length > 0
- ? `Update ${selectedUsers.length} Users`
- : 'Update Status'}
-
-
- {selectedUsers.length > 0 && (
-
- Updating status for {selectedUsers.length} selected users
-
- )}
-
-
- )}
-
- {/* Global Status History Drawer */}
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/src/components/status/Table.tsx b/src/components/status/Table.tsx
new file mode 100644
index 0000000..2b72662
--- /dev/null
+++ b/src/components/status/Table.tsx
@@ -0,0 +1,377 @@
+'use client';
+import { useState, useEffect } from 'react';
+import { useAuth, useTVMode } from '@/components/context';
+import type { UserWithStatus } from '@/lib/hooks';
+import { BasedAvatar, Drawer, DrawerTrigger, Loading } from '@/components/ui';
+import { StatusMessage, SubmitButton } from '@/components/default';
+import { ConnectionStatus, HistoryDrawer } from '@/components/status';
+import type { Profile } from '@/utils/supabase';
+import { makeConditionalClassName } from '@/lib/utils';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button';
+import { RefreshCw, Clock, Calendar } from 'lucide-react';
+import { useSharedStatusSubscription, useStatusData } from '@/lib/hooks';
+import { formatTime, formatDate } from '@/lib/utils';
+import Link from 'next/link';
+
+
+type TableProps = {
+ initialStatuses: UserWithStatus[];
+};
+
+export const TechTable = ({ initialStatuses = [] }: TableProps) => {
+ const { isAuthenticated } = useAuth();
+ const { tvMode } = useTVMode();
+
+ const [selectedUsers, setSelectedUsers] = useState([]);
+ const [selectAll, setSelectAll] = useState(false);
+ const [statusInput, setStatusInput] = useState('');
+ const [selectedHistoryUser, setSelectedHistoryUser] =
+ useState(null);
+ const [updateStatusMessage, setUpdateStatusMessage] = useState('');
+
+ const {
+ data: usersWithStatuses = initialStatuses,
+ isLoading: loading,
+ error,
+ refetch,
+ newStatuses,
+ updateStatusMutation,
+ } = useStatusData({
+ initialData: initialStatuses,
+ enabled: isAuthenticated,
+ });
+ // In your StatusList component
+ const { connectionStatus, connect: reconnect } = useSharedStatusSubscription(() => {
+ refetch().catch((error) => {
+ console.error('Error refetching statuses:', error);
+ });
+ });
+ //const { connectionStatus, connect: reconnect } = useStatusSubscription({
+ //enabled: isAuthenticated,
+ //onStatusUpdate: () => {
+ //refetch().catch((error) => {
+ //console.error('Error refetching statuses:', error);
+ //});
+ //},
+ //});
+
+ const handleUpdateStatus = () => {
+ if (!isAuthenticated) {
+ setUpdateStatusMessage(
+ 'Error: You must be signed in to update technician statuses!'
+ );
+ return;
+ }
+
+ if (statusInput.length < 3 || statusInput.length > 80) {
+ setUpdateStatusMessage(
+ 'Error: Your status must be between 3 & 80 characters long!'
+ );
+ return;
+ }
+
+ updateStatusMutation.mutate({
+ usersWithStatuses: selectedUsers,
+ status: statusInput.trim(),
+ });
+
+ setSelectedUsers([]);
+ setStatusInput('');
+ setUpdateStatusMessage('');
+ };
+
+ const handleCheckboxChange = (user: UserWithStatus) => {
+ setSelectedUsers((prev) =>
+ prev.some((u) => u.user.id === user.user.id)
+ ? prev.filter((prevUser) => prevUser.user.id !== user.user.id)
+ : [...prev, user]
+ );
+ };
+
+ const handleSelectAllChange = () => {
+ if (selectAll) {
+ setSelectedUsers([]);
+ } else {
+ setSelectedUsers(usersWithStatuses);
+ }
+ setSelectAll(!selectAll);
+ };
+
+ useEffect(() => {
+ setSelectAll(
+ selectedUsers.length === usersWithStatuses.length &&
+ usersWithStatuses.length > 0
+ );
+ }, [selectedUsers.length, usersWithStatuses.length]);
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
Error loading status updates
+
+
+ );
+ }
+
+ const containerClassName = makeConditionalClassName({
+ context: tvMode,
+ defaultClassName: 'mx-auto',
+ on: 'lg:w-11/12 w-full',
+ off: 'w-5/6',
+ });
+ const headerClassName = makeConditionalClassName({
+ context: tvMode,
+ defaultClassName: 'w-full mb-2 flex justify-between',
+ on: 'mt-25',
+ off: 'mb-2',
+ });
+ const thClassName = makeConditionalClassName({
+ context: tvMode,
+ defaultClassName: 'py-4 px-4 border font-semibold xl:min-w-[420px]',
+ on: 'lg:text-6xl',
+ off: 'lg:text-5xl',
+ });
+ const tdClassName = makeConditionalClassName({
+ context: tvMode,
+ defaultClassName: 'py-2 px-2 border',
+ on: 'lg:text-5xl',
+ off: 'lg:text-4xl',
+ });
+ const tCheckboxClassName = `py-3 px-4 border`;
+ const checkBoxClassName = `lg:scale-200 cursor-pointer`;
+
+ return (
+
+
+
+
+ {!tvMode && (
+
+
Tired of the old table? {' '}
+
+ Try out the new status list!
+
+
+ )}
+
+
+
+
+
+
+ {!tvMode && (
+
+
+ |
+ )}
+ Technician |
+
+
+
+ Status
+
+
+
+ |
+ Updated At |
+
+
+
+ {usersWithStatuses.map((userWithStatus, index) => {
+ const isSelected = selectedUsers.some(
+ (u) => u.user.id === userWithStatus.user.id,
+ );
+ const isNewStatus = newStatuses.has(userWithStatus);
+
+ return (
+
+ {!tvMode && (
+
+ handleCheckboxChange(userWithStatus)}
+ />
+ |
+ )}
+
+
+
+
+
+ {userWithStatus.user.full_name ?? 'Unknown User'}
+
+ {userWithStatus.updated_by &&
+ userWithStatus.updated_by.id !==
+ userWithStatus.user.id && (
+
+
+
+ Updated by {userWithStatus.updated_by.full_name}
+
+
+ )}
+
+
+ |
+
+
+
+ setSelectedHistoryUser(userWithStatus.user)
+ }
+ >
+ {userWithStatus.status}
+
+ {selectedHistoryUser === userWithStatus.user && (
+
+ )}
+
+ |
+
+
+
+
+
+
+ {formatTime(userWithStatus.created_at)}
+
+
+
+ {formatDate(userWithStatus.created_at)}
+
+
+
+ |
+
+ );
+ })}
+
+
+
+ {usersWithStatuses.length === 0 && (
+
+
+ No status updates yet
+
+
+ )}
+
+ {updateStatusMessage &&
+ (updateStatusMessage.includes('Error') ||
+ updateStatusMessage.includes('error') ||
+ updateStatusMessage.includes('failed') ||
+ updateStatusMessage.includes('invalid') ? (
+
+ ) : (
+
+ ))}
+
+ {!tvMode && (
+
+ setStatusInput(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && !updateStatusMutation.isPending) {
+ e.preventDefault();
+ handleUpdateStatus();
+ }
+ }}
+ disabled={updateStatusMutation.isPending}
+ />
+
+ {selectedUsers.length > 0
+ ? `Update status for ${selectedUsers.length}
+ ${selectedUsers.length > 1 ? 'users' : 'user'}`
+ : 'Update status'
+ }
+
+
+ )}
+
+ {/* Global Status History Drawer */}
+ {!tvMode && (
+
+
+
+
+
+
+
+
+ )}
+
+ );
+};
+
diff --git a/src/components/status/TechTable.tsx b/src/components/status/TechTable.tsx
deleted file mode 100755
index 877d244..0000000
--- a/src/components/status/TechTable.tsx
+++ /dev/null
@@ -1,551 +0,0 @@
-'use client';
-import { useState, useEffect, useRef } from 'react';
-import { useAuth, useTVMode } from '@/components/context';
-import {
- getRecentUsersWithStatuses,
- updateStatuses,
- updateUserStatus,
- type UserWithStatus,
-} from '@/lib/hooks';
-import { BasedAvatar, Drawer, DrawerTrigger, Loading } from '@/components/ui';
-import { SubmitButton } from '@/components/default';
-import { toast } from 'sonner';
-import { HistoryDrawer } from '@/components/status';
-import type { Profile } from '@/utils/supabase';
-import { makeConditionalClassName } from '@/lib/utils';
-import { Badge } from '@/components/ui/badge';
-import { Button } from '@/components/ui/button';
-import { RefreshCw, Wifi, WifiOff } from 'lucide-react';
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { QueryErrorCodes } from '@/components/context';
-import { createClient } from '@/utils/supabase';
-import type { RealtimeChannel } from '@supabase/supabase-js';
-
-type TechTableProps = {
- initialStatuses: UserWithStatus[];
-};
-
-export const TechTable = ({ initialStatuses = [] }: TechTableProps) => {
- const { isAuthenticated } = useAuth();
- const { tvMode } = useTVMode();
- const queryClient = useQueryClient();
- const [selectedUsers, setSelectedUsers] = useState([]);
- const [selectAll, setSelectAll] = useState(false);
- const [statusInput, setStatusInput] = useState('');
- const [selectedHistoryUser, setSelectedHistoryUser] =
- useState(null);
- const [connectionStatus, setConnectionStatus] = useState<
- 'connecting' | 'connected' | 'disconnected'
- >('connecting');
- const [newStatusIds, setNewStatusIds] = useState>(new Set());
- const channelRef = useRef(null);
- const supabaseRef = useRef(createClient());
-
- // Keep all your existing React Query code exactly as is
- const {
- data: usersWithStatuses = initialStatuses,
- isLoading: loading,
- error,
- refetch,
- isFetching,
- dataUpdatedAt,
- } = useQuery({
- queryKey: ['users-with-statuses'],
- queryFn: async () => {
- try {
- const response = await getRecentUsersWithStatuses();
- if (!response.success) throw new Error(response.error);
- return response.data;
- } catch (error) {
- toast.error(
- `Error fetching technicians: ${error instanceof Error ? error.message : 'Unknown error'}`,
- );
- throw error;
- }
- },
- enabled: isAuthenticated,
- refetchInterval: 30000, // Changed to 30 seconds as backup
- refetchOnWindowFocus: true,
- refetchOnMount: true,
- initialData: initialStatuses,
- meta: { errCode: QueryErrorCodes.USERS_FETCH_FAILED },
- });
-
- // Add this new useEffect for realtime enhancement
- useEffect(() => {
- if (!isAuthenticated) return;
- let reconnectAttempts = 0;
- const maxReconnectAttempts = 3;
- let reconnectTimeout: NodeJS.Timeout;
- let isComponentMounted = true;
- let currentChannel: RealtimeChannel | null = null;
-
- const setUpRealtimeConnection = () => {
- if (!isComponentMounted) return;
- if (currentChannel) {
- supabaseRef.current.removeChannel(currentChannel).catch((error) => {
- console.error(`Error unsubscribing: ${error}`);
- });
- currentChannel = null;
- }
- setConnectionStatus('connecting');
- const channel = supabaseRef.current
- .channel('status_updates')
- .on('broadcast', { event: 'status_updated' }, (payload) => {
- console.log('Realtime update received, triggering refetch...');
- refetch().catch((error) => {
- console.error(`Error refetching: ${error}`);
- });
- })
- .subscribe((status) => {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
- if (status === 'SUBSCRIBED') {
- console.log('Realtime connection established');
- setConnectionStatus('connected');
- reconnectAttempts = 0;
- // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
- } else if (status === 'CHANNEL_ERROR') {
- console.log('Realtime connection failed, relying on polling');
- setConnectionStatus('disconnected');
- // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
- } else if (status === 'CLOSED') {
- console.log('Realtime connection closed');
- setConnectionStatus('disconnected');
- if (
- isComponentMounted &&
- reconnectAttempts < maxReconnectAttempts
- ) {
- reconnectAttempts++;
- const delay = 2000 * reconnectAttempts;
- console.log(
- `Reconnecting after close ${reconnectAttempts}/${maxReconnectAttempts} in ${delay}ms`,
- );
- if (reconnectTimeout) {
- clearTimeout(reconnectTimeout);
- }
- reconnectTimeout = setTimeout(() => {
- if (isComponentMounted) {
- setUpRealtimeConnection();
- }
- }, delay);
- } else {
- console.log(
- 'Max reconnection attempts reached or component unmounted',
- );
- }
- }
- });
- currentChannel = channel;
- channelRef.current = channel;
- };
-
- const initialTimeout = setTimeout(() => {
- if (isComponentMounted) {
- setUpRealtimeConnection();
- }
- }, 1000);
-
- return () => {
- isComponentMounted = false;
- if (initialTimeout) clearTimeout(initialTimeout);
- if (reconnectTimeout) clearTimeout(reconnectTimeout);
- if (currentChannel) {
- console.log('Cleaning up realtime connection...');
- supabaseRef.current.removeChannel(currentChannel).catch((error) => {
- console.error(`Error unsubscribing: ${error}`);
- });
- channelRef.current = null;
- }
- };
- }, [isAuthenticated, refetch]);
-
- // Updated mutation
- const updateStatusMutation = useMutation({
- mutationFn: async ({
- usersWithStatuses,
- status,
- }: {
- usersWithStatuses: UserWithStatus[];
- status: string;
- }) => {
- if (usersWithStatuses.length === 0) {
- const result = await updateUserStatus(status);
- if (!result.success) throw new Error(result.error);
- return { type: 'single', result };
- } else {
- const result = await updateStatuses(usersWithStatuses, status);
- if (!result.success) throw new Error(result.error);
- return { type: 'multiple', result, count: usersWithStatuses.length };
- }
- },
- meta: { errCode: QueryErrorCodes.STATUS_UPDATE_FAILED },
- onMutate: async ({ usersWithStatuses, status }) => {
- // Optimistic update logic
- await queryClient.cancelQueries({ queryKey: ['users-with-statuses'] });
- const previousData = queryClient.getQueryData([
- 'users-with-statuses',
- ]);
- if (previousData && usersWithStatuses.length > 0) {
- const now = new Date().toISOString();
- const optimisticData = previousData.map((userStatus) => {
- if (
- usersWithStatuses.some(
- (selected) => selected.user.id === userStatus.user.id,
- )
- ) {
- return {
- ...userStatus,
- status,
- created_at: now,
- };
- }
- return userStatus;
- });
- queryClient.setQueryData(['users-with-statuses'], optimisticData);
- // Add animation for optimistic updates
- const updatedIds = usersWithStatuses.map((u) => u.user.id);
- setNewStatusIds((prev) => new Set([...prev, ...updatedIds]));
- // Remove animation after 1 second
- setTimeout(() => {
- setNewStatusIds((prev) => {
- const updated = new Set(prev);
- updatedIds.forEach((id) => updated.delete(id));
- return updated;
- });
- }, 1000);
- }
- return { previousData };
- },
- onSuccess: (data) => {
- // Handle success in the mutation function
- void queryClient.invalidateQueries({ queryKey: ['users-with-statuses'] }); // Fixed floating promise
- if (data.type === 'single') {
- toast.success('Status updated for signed in user.');
- } else {
- toast.success(`Status updated for ${data.count} selected users.`);
- }
- setSelectedUsers([]);
- setStatusInput('');
- },
- onError: (error, _variables, context) => {
- // Fixed unused variables
- // Rollback optimistic update
- if (context?.previousData) {
- queryClient.setQueryData(['users-with-statuses'], context.previousData);
- }
- // Error handling is done in the global mutation cache
- console.error('Status update failed:', error);
- },
- });
-
- const handleUpdateStatus = () => {
- if (!isAuthenticated) {
- toast.error('You must be signed in to update statuses.');
- return;
- }
- if (!statusInput.trim()) {
- toast.error('Please enter a valid status.');
- return;
- }
- updateStatusMutation.mutate({
- usersWithStatuses: selectedUsers,
- status: statusInput.trim(),
- });
- };
-
- const handleCheckboxChange = (user: UserWithStatus) => {
- setSelectedUsers((prev) =>
- prev.some((u) => u.user.id === user.user.id)
- ? prev.filter((prevUser) => prevUser.user.id !== user.user.id)
- : [...prev, user],
- );
- };
-
- const handleSelectAllChange = () => {
- if (selectAll) {
- setSelectedUsers([]);
- } else {
- setSelectedUsers(usersWithStatuses);
- }
- setSelectAll(!selectAll);
- };
-
- useEffect(() => {
- setSelectAll(
- selectedUsers.length === usersWithStatuses.length &&
- usersWithStatuses.length > 0,
- );
- }, [selectedUsers.length, usersWithStatuses.length]);
-
- const getConnectionIcon = () => {
- switch (connectionStatus) {
- case 'connected':
- return ;
- case 'connecting':
- return ;
- case 'disconnected':
- return ;
- }
- };
-
- const getConnectionText = () => {
- switch (connectionStatus) {
- case 'connected':
- return 'Connected';
- case 'connecting':
- return 'Connecting...';
- case 'disconnected':
- return 'Disconnected';
- }
- };
-
- const formatTime = (timestamp: string) => {
- const date = new Date(timestamp);
- const time = date.toLocaleTimeString('en-US', {
- hour: 'numeric',
- minute: 'numeric',
- });
- const day = date.getDate();
- const month = date.toLocaleString('default', { month: 'long' });
- return `${time} - ${month} ${day}`;
- };
-
- if (loading) {
- return (
-
-
-
- );
- }
-
- if (error) {
- return (
-
-
Error loading status updates
-
-
- );
- }
-
- const containerClassName = makeConditionalClassName({
- context: tvMode,
- defaultClassName: 'mx-auto',
- on: 'lg:w-11/12 w-full mt-15',
- off: 'w-5/6',
- });
- const thClassName = makeConditionalClassName({
- context: tvMode,
- defaultClassName: 'py-3 px-4 border font-semibold',
- on: 'lg:text-6xl',
- off: 'lg:text-5xl',
- });
- const tdClassName = makeConditionalClassName({
- context: tvMode,
- defaultClassName: 'py-3 px-4 border',
- on: 'lg:text-5xl',
- off: 'lg:text-4xl',
- });
- const tCheckboxClassName = `py-3 px-4 border`;
- const checkBoxClassName = `lg:scale-200 cursor-pointer`;
-
- return (
-
- {/* Status Header */}
-
- {isFetching ? (
-
-
- Updating...
-
- ) : (
-
- {getConnectionIcon()}
- {getConnectionText()}
-
- )}
-
-
-
-
-
- {!tvMode && (
-
-
- |
- )}
- Technician |
-
-
-
- Status
-
-
-
- |
- Updated At |
-
-
-
- {usersWithStatuses.map((userWithStatus, index) => {
- const isSelected = selectedUsers.some(
- (u) => u.user.id === userWithStatus.user.id,
- );
- const isNewStatus = newStatusIds.has(userWithStatus.user.id);
-
- return (
-
- {!tvMode && (
-
- handleCheckboxChange(userWithStatus)}
- />
- |
- )}
-
-
-
-
-
- {userWithStatus.user.full_name ?? 'Unknown User'}
-
- {userWithStatus.updated_by &&
- userWithStatus.updated_by.id !==
- userWithStatus.user.id && (
-
-
-
- Updated by {userWithStatus.updated_by.full_name}
-
-
- )}
-
-
- |
-
-
-
- setSelectedHistoryUser(userWithStatus.user)
- }
- >
- {userWithStatus.status}
-
- {selectedHistoryUser === userWithStatus.user && (
-
- )}
-
- |
-
- {formatTime(userWithStatus.created_at)}
- |
-
- );
- })}
-
-
-
- {usersWithStatuses.length === 0 && (
-
-
- No status updates yet
-
-
- )}
-
- {!tvMode && (
-
- setStatusInput(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === 'Enter' && !updateStatusMutation.isPending) {
- e.preventDefault();
- handleUpdateStatus();
- }
- }}
- disabled={updateStatusMutation.isPending}
- />
-
- {updateStatusMutation.isPending
- ? 'Updating...'
- : selectedUsers.length > 0
- ? `Update ${selectedUsers.length} Users`
- : 'Update Status'}
-
-
- )}
-
- {selectedUsers.length > 0 && !tvMode && (
-
-
- Updating status for {selectedUsers.length} selected users
-
-
- )}
-
- {/* Global Status History Drawer */}
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/src/components/status/index.tsx b/src/components/status/index.tsx
index d75607f..2bca4d1 100644
--- a/src/components/status/index.tsx
+++ b/src/components/status/index.tsx
@@ -1,3 +1,4 @@
+export * from './ConnectionStatus';
export * from './HistoryDrawer';
-export * from './StatusList';
-export * from './TechTable';
+export * from './List';
+export * from './Table';
diff --git a/src/lib/hooks/index.ts b/src/lib/hooks/index.ts
index a79dac8..41d8332 100755
--- a/src/lib/hooks/index.ts
+++ b/src/lib/hooks/index.ts
@@ -3,6 +3,8 @@ export * from './public';
export * from './status';
export * from './storage';
export * from './useFileUpload';
+export * from './useSharedStatusSubscription';
+export * from './useStatusData';
export type Result =
| { success: true; data: T }
diff --git a/src/lib/hooks/status.ts b/src/lib/hooks/status.ts
index 632d5cb..89a963e 100644
--- a/src/lib/hooks/status.ts
+++ b/src/lib/hooks/status.ts
@@ -135,7 +135,7 @@ export const updateStatuses = async (
)
.select();
- if (insertedStatusesError) throw new Error("Error inserting statuses!");
+ if (insertedStatusesError) throw new Error('Error inserting statuses!');
else if (insertedStatuses) {
const createdAtFallback = new Date(Date.now()).toISOString();
const statusUpdates = usersWithStatuses.map((s, i) => {
@@ -144,7 +144,7 @@ export const updateStatuses = async (
status: status,
created_at: insertedStatuses[i]?.created_at ?? createdAtFallback,
updated_by: user,
- }
+ };
});
await broadcastStatusUpdates(statusUpdates);
return { success: true, data: statusUpdates };
diff --git a/src/lib/hooks/useSharedStatusSubscription.ts b/src/lib/hooks/useSharedStatusSubscription.ts
new file mode 100644
index 0000000..ea83c89
--- /dev/null
+++ b/src/lib/hooks/useSharedStatusSubscription.ts
@@ -0,0 +1,131 @@
+'use client';
+import { useState, useEffect, useCallback } from 'react';
+import { createClient } from '@/utils/supabase';
+import type { RealtimeChannel } from '@supabase/supabase-js';
+
+export type ConnectionStatus =
+ | 'connecting'
+ | 'connected'
+ | 'disconnected'
+ | 'updating';
+
+// Singleton state
+let sharedChannel: RealtimeChannel | null = null;
+let sharedConnectionStatus: ConnectionStatus = 'disconnected';
+const subscribers = new Set<(status: ConnectionStatus) => void>();
+const statusUpdateCallbacks = new Set<() => void>();
+//const subscribers: Set<(status: ConnectionStatus) => void> = new Set();
+//const statusUpdateCallbacks: Set<() => void> = new Set();
+let reconnectAttempts = 0;
+let reconnectTimeout: NodeJS.Timeout | undefined;
+const supabase = createClient();
+
+const notifySubscribers = (status: ConnectionStatus) => {
+ sharedConnectionStatus = status;
+ subscribers.forEach(callback => callback(status));
+};
+
+const notifyStatusUpdate = () => {
+ statusUpdateCallbacks.forEach(callback => callback());
+};
+
+const cleanup = () => {
+ if (reconnectTimeout) {
+ clearTimeout(reconnectTimeout);
+ reconnectTimeout = undefined;
+ }
+
+ if (sharedChannel) {
+ supabase.removeChannel(sharedChannel).catch((error) => {
+ console.error('Error removing shared channel:', error);
+ });
+ sharedChannel = null;
+ }
+};
+
+const connect = () => {
+ if (sharedChannel) return; // Already connected or connecting
+
+ cleanup();
+ notifySubscribers('connecting');
+
+ const channel = supabase
+ .channel('shared_status_updates', {
+ config: { broadcast: {self: true }}
+ })
+ .on('broadcast', { event: 'status_updated' }, () => {
+ notifyStatusUpdate();
+ })
+ .subscribe((status) => {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
+ if (status === 'SUBSCRIBED') {
+ notifySubscribers('connected');
+ reconnectAttempts = 0;
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
+ } else if (status === 'CHANNEL_ERROR' || status === 'CLOSED') {
+ notifySubscribers('disconnected');
+
+ if (reconnectAttempts < 5) {
+ reconnectAttempts++;
+ const delay = 2000 * reconnectAttempts;
+
+ reconnectTimeout = setTimeout(() => {
+ if (subscribers.size > 0) { // Only reconnect if there are active subscribers
+ connect();
+ }
+ }, delay);
+ }
+ }
+ });
+
+ sharedChannel = channel;
+};
+
+const disconnect = () => {
+ cleanup();
+ notifySubscribers('disconnected');
+};
+
+export const useSharedStatusSubscription = (onStatusUpdate?: () => void) => {
+ const [connectionStatus, setConnectionStatus] = useState(sharedConnectionStatus);
+
+ useEffect(() => {
+ // Subscribe to status changes
+ subscribers.add(setConnectionStatus);
+
+ // Subscribe to status updates
+ if (onStatusUpdate) {
+ statusUpdateCallbacks.add(onStatusUpdate);
+ }
+
+ // Connect if this is the first subscriber
+ if (subscribers.size === 1) {
+ const timeout = setTimeout(connect, 1000);
+ return () => clearTimeout(timeout);
+ }
+
+ return () => {
+ // Cleanup subscriptions
+ subscribers.delete(setConnectionStatus);
+ if (onStatusUpdate) {
+ statusUpdateCallbacks.delete(onStatusUpdate);
+ }
+
+ // Disconnect if no more subscribers
+ if (subscribers.size === 0) {
+ disconnect();
+ }
+ };
+ }, [onStatusUpdate]);
+
+ const reconnect = useCallback(() => {
+ reconnectAttempts = 0;
+ connect();
+ }, []);
+
+ return {
+ connectionStatus,
+ connect: reconnect,
+ disconnect,
+ };
+};
diff --git a/src/lib/hooks/useStatusData.ts b/src/lib/hooks/useStatusData.ts
new file mode 100644
index 0000000..4f290de
--- /dev/null
+++ b/src/lib/hooks/useStatusData.ts
@@ -0,0 +1,132 @@
+'use client';
+import { useState } from 'react';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import {
+ getRecentUsersWithStatuses,
+ updateStatuses,
+ updateUserStatus,
+ type UserWithStatus,
+} from '@/lib/hooks';
+import { QueryErrorCodes } from '@/components/context';
+import { toast } from 'sonner';
+
+type UseStatusDataOptions = {
+ initialData?: UserWithStatus[];
+ enabled?: boolean;
+}
+
+export const useStatusData = ({
+ initialData = [],
+ enabled = true
+}: UseStatusDataOptions = {}) => {
+ const queryClient = useQueryClient();
+ const [newStatuses, setNewStatuses] = useState>(
+ new Set()
+ );
+
+ const query = useQuery({
+ queryKey: ['users-with-statuses'],
+ queryFn: async () => {
+ try {
+ const response = await getRecentUsersWithStatuses();
+ if (!response.success) throw new Error(response.error);
+ return response.data;
+ } catch (error) {
+ toast.error(`Error fetching technicians: ${error as Error}`);
+ throw error;
+ }
+ },
+ enabled,
+ refetchInterval: 30000,
+ refetchOnWindowFocus: true,
+ refetchOnMount: true,
+ initialData,
+ meta: { errCode: QueryErrorCodes.USERS_FETCH_FAILED },
+ });
+
+ const updateStatusMutation = useMutation({
+ mutationFn: async ({
+ usersWithStatuses,
+ status,
+ }: {
+ usersWithStatuses: UserWithStatus[];
+ status: string;
+ }) => {
+ try {
+ if (usersWithStatuses.length <= 0) {
+ const result = await updateUserStatus(status);
+ if (!result.success) throw new Error(result.error);
+ return result.data;
+ } else {
+ const result = await updateStatuses(usersWithStatuses, status);
+ if (!result.success) throw new Error(result.error);
+ return result.data;
+ }
+ } catch (error) {
+ console.error(`Error updating statuses: ${error as Error}`);
+ throw error;
+ }
+ },
+ meta: { errCode: QueryErrorCodes.STATUS_UPDATE_FAILED },
+ onMutate: async ({ usersWithStatuses, status }) => {
+ await queryClient.cancelQueries({ queryKey: ['users-with-statuses'] });
+ const previousData = queryClient.getQueryData([
+ 'users-with-statuses',
+ ]);
+
+ if (previousData && usersWithStatuses.length > 0) {
+ const now = new Date().toISOString();
+ const optimisticData = previousData.map((userStatus) => {
+ if (
+ usersWithStatuses.some(
+ (selected) => selected.user.id === userStatus.user.id
+ )
+ ) {
+ return { ...userStatus, status, created_at: now };
+ }
+ return userStatus;
+ });
+ queryClient.setQueryData(['users-with-statuses'], optimisticData);
+
+ // Add animation to optimistically updated statuses
+ setNewStatuses((prev) => new Set([...prev, ...usersWithStatuses]));
+ setTimeout(() => {
+ setNewStatuses((prev) => {
+ const updated = new Set(prev);
+ usersWithStatuses.forEach((updatedStatus) =>
+ updated.delete(updatedStatus)
+ );
+ return updated;
+ });
+ }, 1000);
+ }
+
+ return { previousData };
+ },
+ onSuccess: (data) => {
+ queryClient
+ .invalidateQueries({ queryKey: ['users-with-statuses'] })
+ .catch((error) => console.error(`Error invalidating query: ${error}`));
+
+ if (!data) return;
+
+ data.forEach((statusUpdate) => {
+ toast.success(
+ `${statusUpdate.user.full_name}'s status updated to '${statusUpdate.status}'.`
+ );
+ });
+ },
+ onError: (error, _variables, context) => {
+ if (context?.previousData) {
+ queryClient.setQueryData(['users-with-statuses'], context.previousData);
+ }
+ toast.error(`Error updating statuses: ${error}`);
+ },
+ });
+
+ return {
+ ...query,
+ newStatuses,
+ updateStatusMutation,
+ };
+};
diff --git a/src/lib/hooks/useStatusSubscription.ts b/src/lib/hooks/useStatusSubscription.ts
new file mode 100644
index 0000000..60944c7
--- /dev/null
+++ b/src/lib/hooks/useStatusSubscription.ts
@@ -0,0 +1,147 @@
+'use client';
+import { useState, useEffect, useRef, useCallback } from 'react';
+import { createClient } from '@/utils/supabase';
+import type { RealtimeChannel } from '@supabase/supabase-js';
+
+export type ConnectionStatus =
+ | 'connecting'
+ | 'connected'
+ | 'disconnected'
+ | 'updating';
+
+type UseStatusSubscriptionOptions = {
+ enabled?: boolean;
+ onStatusUpdate?: () => void;
+ maxReconnectAttempts?: number;
+ reconnectDelay?: number;
+}
+
+export const useStatusSubscription = ({
+ enabled = true,
+ onStatusUpdate,
+ maxReconnectAttempts = 5,
+ reconnectDelay = 2000,
+}: UseStatusSubscriptionOptions = {}) => {
+ const [connectionStatus, setConnectionStatus] =
+ useState('disconnected');
+ const channelRef = useRef(null);
+ const supabaseRef = useRef(createClient());
+ const reconnectAttemptsRef = useRef(0);
+ const reconnectTimeoutRef = useRef(undefined);
+ const isComponentMountedRef = useRef(true);
+ const visibilityTimeoutRef = useRef(undefined);
+
+ const cleanup = useCallback(() => {
+ if (reconnectTimeoutRef.current) {
+ clearTimeout(reconnectTimeoutRef.current);
+ reconnectTimeoutRef.current = undefined;
+ }
+ if (visibilityTimeoutRef.current) {
+ clearTimeout(visibilityTimeoutRef.current);
+ visibilityTimeoutRef.current = undefined;
+ }
+ if (channelRef.current) {
+ supabaseRef.current.removeChannel(channelRef.current).catch((error) => {
+ console.error('❌ cleanup: Error removing channel:', error);
+ });
+ channelRef.current = null;
+ }
+ }, []);
+
+ const connect = useCallback(() => {
+ if (!enabled || !isComponentMountedRef.current) return;
+
+ cleanup();
+ setConnectionStatus('connecting');
+
+ const channel = supabaseRef.current
+ .channel('status_updates', {
+ config: { broadcast: {self: true }}
+ });
+ channel
+ .on('broadcast', { event: 'status_updated' }, (payload) => {
+ onStatusUpdate?.();
+ })
+ .subscribe((status) => {
+ if (!isComponentMountedRef.current) return;
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
+ if (status === 'SUBSCRIBED') {
+ setConnectionStatus('connected');
+ reconnectAttemptsRef.current = 0;
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
+ } else if (status === 'CHANNEL_ERROR' || status === 'CLOSED') {
+ setConnectionStatus('disconnected');
+ if (reconnectAttemptsRef.current < maxReconnectAttempts) {
+ reconnectAttemptsRef.current++;
+ const delay = reconnectDelay * reconnectAttemptsRef.current;
+
+ reconnectTimeoutRef.current = setTimeout(() => {
+ if (isComponentMountedRef.current) connect();
+ }, delay);
+ } else {
+ console.warn('⚠️ connect: Max reconnection attempts reached');
+ setConnectionStatus('disconnected');
+ }
+ }
+ });
+
+ channelRef.current = channel;
+ }, [enabled, onStatusUpdate, maxReconnectAttempts, reconnectDelay, cleanup]);
+
+ const disconnect = useCallback(() => {
+ cleanup();
+ setConnectionStatus('disconnected');
+ }, [cleanup]);
+
+ const reconnect = useCallback(() => {
+ reconnectAttemptsRef.current = 0;
+ connect();
+ }, [connect]);
+
+ // Handle visibility change for better reconnection
+ useEffect(() => {
+ const handleVisibilityChange = () => {
+ if (!enabled) return;
+ if (document.visibilityState === 'visible') {
+ visibilityTimeoutRef.current = setTimeout(() => {
+ if (connectionStatus === 'disconnected' && isComponentMountedRef.current) {
+ reconnect();
+ }
+ }, 1000);
+ }
+ };
+ document.addEventListener('visibilitychange', handleVisibilityChange);
+ return () => {
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
+ };
+ }, [enabled, connectionStatus, reconnect]);
+
+ // Initial connection - SIMPLIFIED to avoid dependency issues
+ useEffect(() => {
+ if (!enabled) {
+ disconnect();
+ return;
+ }
+ const initialTimeout = setTimeout(() => {
+ if (isComponentMountedRef.current) connect();
+ }, 1000);
+
+ return () => {
+ clearTimeout(initialTimeout);
+ };
+ }, [enabled]);
+
+ // Cleanup on unmount
+ useEffect(() => {
+ return () => {
+ cleanup();
+ };
+ }, [cleanup]);
+
+ return {
+ connectionStatus,
+ connect: reconnect,
+ disconnect,
+ };
+};
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index cc7a7c5..b5559a7 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -18,3 +18,19 @@ export const makeConditionalClassName = ({
}) => {
return defaultClassName + ' ' + (context ? on : off);
};
+
+export const formatTime = (timestamp: string) => {
+ const date = new Date(timestamp);
+ const time = date.toLocaleTimeString('en-US', {
+ hour: 'numeric',
+ minute: 'numeric',
+ });
+ return time;
+};
+
+export const formatDate = (timestamp: string) => {
+ const date = new Date(timestamp);
+ const day = date.getDate();
+ const month = date.toLocaleString('default', { month: 'long' });
+ return `${month} ${day}`;
+};
diff --git a/src/middleware.ts b/src/middleware.ts
index 6d9d3a4..ebd72fc 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -110,7 +110,9 @@ export const middleware = async (request: NextRequest) => {
if (shouldBan) {
console.log(`🔨 IP ${ip} has been banned for suspicious activity`);
- return new NextResponse('Access denied - IP banned. Please fuck off.', { status: 403 });
+ return new NextResponse('Access denied - IP banned. Please fuck off.', {
+ status: 403,
+ });
}
// Return 404 to not reveal the blocking mechanism