Clean up. Almost ready to deploy maybe. REally wanna rewrite but hey eventually we will.
This commit is contained in:
14
package.json
14
package.json
@ -27,7 +27,7 @@
|
|||||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@sentry/nextjs": "^9.29.0",
|
"@sentry/nextjs": "^9.30.0",
|
||||||
"@supabase/ssr": "^0.6.1",
|
"@supabase/ssr": "^0.6.1",
|
||||||
"@supabase/supabase-js": "^2.50.0",
|
"@supabase/supabase-js": "^2.50.0",
|
||||||
"@t3-oss/env-nextjs": "^0.12.0",
|
"@t3-oss/env-nextjs": "^0.12.0",
|
||||||
@ -40,11 +40,11 @@
|
|||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^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",
|
"require-in-the-middle": "^7.5.2",
|
||||||
"sonner": "^2.0.5",
|
"sonner": "^2.0.5",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"zod": "^3.25.64"
|
"zod": "^3.25.67"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
@ -52,15 +52,15 @@
|
|||||||
"@tanstack/eslint-plugin-query": "^5.78.0",
|
"@tanstack/eslint-plugin-query": "^5.78.0",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.3",
|
"@types/express": "^5.0.3",
|
||||||
"@types/node": "^20.19.0",
|
"@types/node": "^20.19.1",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"eslint": "^9.29.0",
|
"eslint": "^9.29.0",
|
||||||
"eslint-config-next": "^15.3.3",
|
"eslint-config-next": "^15.3.3",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"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",
|
"import-in-the-middle": "^1.14.2",
|
||||||
"postcss": "^8.5.5",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.12",
|
"prettier-plugin-tailwindcss": "^0.6.12",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
@ -68,7 +68,7 @@
|
|||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tw-animate-css": "^1.3.4",
|
"tw-animate-css": "^1.3.4",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.34.0"
|
"typescript-eslint": "^8.34.1"
|
||||||
},
|
},
|
||||||
"ct3aMetadata": {
|
"ct3aMetadata": {
|
||||||
"initVersion": "7.39.3"
|
"initVersion": "7.39.3"
|
||||||
|
376
pnpm-lock.yaml
generated
376
pnpm-lock.yaml
generated
@ -10,7 +10,7 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^5.1.1
|
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':
|
'@radix-ui/react-avatar':
|
||||||
specifier: ^1.1.10
|
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)
|
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
|
specifier: ^1.2.3
|
||||||
version: 1.2.3(@types/react@19.1.8)(react@19.1.0)
|
version: 1.2.3(@types/react@19.1.8)(react@19.1.0)
|
||||||
'@sentry/nextjs':
|
'@sentry/nextjs':
|
||||||
specifier: ^9.29.0
|
specifier: ^9.30.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)
|
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':
|
'@supabase/ssr':
|
||||||
specifier: ^0.6.1
|
specifier: ^0.6.1
|
||||||
version: 0.6.1(@supabase/supabase-js@2.50.0)
|
version: 0.6.1(@supabase/supabase-js@2.50.0)
|
||||||
@ -49,7 +49,7 @@ importers:
|
|||||||
version: 2.50.0
|
version: 2.50.0
|
||||||
'@t3-oss/env-nextjs':
|
'@t3-oss/env-nextjs':
|
||||||
specifier: ^0.12.0
|
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':
|
'@tanstack/react-query':
|
||||||
specifier: ^5.80.7
|
specifier: ^5.80.7
|
||||||
version: 5.80.7(react@19.1.0)
|
version: 5.80.7(react@19.1.0)
|
||||||
@ -78,8 +78,8 @@ importers:
|
|||||||
specifier: ^19.1.0
|
specifier: ^19.1.0
|
||||||
version: 19.1.0(react@19.1.0)
|
version: 19.1.0(react@19.1.0)
|
||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: ^7.58.0
|
specifier: ^7.58.1
|
||||||
version: 7.58.0(react@19.1.0)
|
version: 7.58.1(react@19.1.0)
|
||||||
require-in-the-middle:
|
require-in-the-middle:
|
||||||
specifier: ^7.5.2
|
specifier: ^7.5.2
|
||||||
version: 7.5.2
|
version: 7.5.2
|
||||||
@ -90,8 +90,8 @@ importers:
|
|||||||
specifier: ^1.1.2
|
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)
|
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:
|
zod:
|
||||||
specifier: ^3.25.64
|
specifier: ^3.25.67
|
||||||
version: 3.25.64
|
version: 3.25.67
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@eslint/eslintrc':
|
'@eslint/eslintrc':
|
||||||
specifier: ^3.3.1
|
specifier: ^3.3.1
|
||||||
@ -109,8 +109,8 @@ importers:
|
|||||||
specifier: ^5.0.3
|
specifier: ^5.0.3
|
||||||
version: 5.0.3
|
version: 5.0.3
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.19.0
|
specifier: ^20.19.1
|
||||||
version: 20.19.0
|
version: 20.19.1
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^19.1.8
|
specifier: ^19.1.8
|
||||||
version: 19.1.8
|
version: 19.1.8
|
||||||
@ -127,14 +127,14 @@ importers:
|
|||||||
specifier: ^10.1.5
|
specifier: ^10.1.5
|
||||||
version: 10.1.5(eslint@9.29.0(jiti@2.4.2))
|
version: 10.1.5(eslint@9.29.0(jiti@2.4.2))
|
||||||
eslint-plugin-prettier:
|
eslint-plugin-prettier:
|
||||||
specifier: ^5.4.1
|
specifier: ^5.5.0
|
||||||
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)
|
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:
|
import-in-the-middle:
|
||||||
specifier: ^1.14.2
|
specifier: ^1.14.2
|
||||||
version: 1.14.2
|
version: 1.14.2
|
||||||
postcss:
|
postcss:
|
||||||
specifier: ^8.5.5
|
specifier: ^8.5.6
|
||||||
version: 8.5.5
|
version: 8.5.6
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.5.3
|
specifier: ^3.5.3
|
||||||
version: 3.5.3
|
version: 3.5.3
|
||||||
@ -157,8 +157,8 @@ importers:
|
|||||||
specifier: ^5.8.3
|
specifier: ^5.8.3
|
||||||
version: 5.8.3
|
version: 5.8.3
|
||||||
typescript-eslint:
|
typescript-eslint:
|
||||||
specifier: ^8.34.0
|
specifier: ^8.34.1
|
||||||
version: 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
|
version: 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@ -1122,8 +1122,8 @@ packages:
|
|||||||
rollup:
|
rollup:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rollup/pluginutils@5.1.4':
|
'@rollup/pluginutils@5.2.0':
|
||||||
resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==}
|
resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
|
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
|
||||||
@ -1232,28 +1232,28 @@ packages:
|
|||||||
'@rushstack/eslint-patch@1.11.0':
|
'@rushstack/eslint-patch@1.11.0':
|
||||||
resolution: {integrity: sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==}
|
resolution: {integrity: sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==}
|
||||||
|
|
||||||
'@sentry-internal/browser-utils@9.29.0':
|
'@sentry-internal/browser-utils@9.30.0':
|
||||||
resolution: {integrity: sha512-Wp6UJCDVV2KVK+TG8GwdLZyDy4GtUYDmVhGMpHKPS3G/Qgpf36cY/XHwChwaHZ5P9Bk1sjS9Ok698J59S8L2nw==}
|
resolution: {integrity: sha512-e6ZlN8oWheCB0YJSGlBNUlh6UPnY5Ecj1P+/cgeKBhNm7c3bIx4J50485hB8LQsu+b7Q11L2o/wucZ//Pb6FCg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@sentry-internal/feedback@9.29.0':
|
'@sentry-internal/feedback@9.30.0':
|
||||||
resolution: {integrity: sha512-ADvetGrtr+RfYcQKrQxah4fHs/xDJ/VjbStVMSuaNllzwWPYNkWIGFE6YjQ7wZszj0DQIu5/H+B6lZKsFYk4xw==}
|
resolution: {integrity: sha512-qAZ7xxLqZM7GlEvmSUmTHnoueg+fc7esMQD4vH8pS7HI3n9C5MjGn3HHlndRpD8lL7iUUQ0TPZQgU6McbzMDyw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@sentry-internal/replay-canvas@9.29.0':
|
'@sentry-internal/replay-canvas@9.30.0':
|
||||||
resolution: {integrity: sha512-TrQYhSAVPhyenvu0fNkon7BznFibu1mzS5bCudxhgOWajZluUVrXcbp8Q3WZ3R+AogrcgA3Vy6aumP/+fMKdwg==}
|
resolution: {integrity: sha512-I4MxS27rfV7vnOU29L80y4baZ4I1XqpnYvC/yLN7C17nA8eDCufQ8WVomli41y8JETnfcxlm68z7CS0sO4RCSA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@sentry-internal/replay@9.29.0':
|
'@sentry-internal/replay@9.30.0':
|
||||||
resolution: {integrity: sha512-we/1JPRje8sNowQCyogOV1OYWuDOP/3XmDi48XoFG2HB0XMl2HfL5LI8AvgAvC/5nrqVAAo4ktbjoVLm1fb7rg==}
|
resolution: {integrity: sha512-+6wkqQGLJuFUzvGRzbh3iIhFGyxQx/Oxc0ODDKmz9ag2xYRjCYb3UUQXmQX9navAF0HXUsq8ajoJPm2L1ZyWVg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@sentry/babel-plugin-component-annotate@3.5.0':
|
'@sentry/babel-plugin-component-annotate@3.5.0':
|
||||||
resolution: {integrity: sha512-s2go8w03CDHbF9luFGtBHKJp4cSpsQzNVqgIa9Pfa4wnjipvrK6CxVT4icpLA3YO6kg5u622Yoa5GF3cJdippw==}
|
resolution: {integrity: sha512-s2go8w03CDHbF9luFGtBHKJp4cSpsQzNVqgIa9Pfa4wnjipvrK6CxVT4icpLA3YO6kg5u622Yoa5GF3cJdippw==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
|
|
||||||
'@sentry/browser@9.29.0':
|
'@sentry/browser@9.30.0':
|
||||||
resolution: {integrity: sha512-+GFX/yb+rh6V1fSgTYM6ttAgledl2aUR3T3Rg86HNuegbdX8ym6lOtUOIZ0j9jPK015HR47KIPyIZVZZJ7Rj9g==}
|
resolution: {integrity: sha512-sRyW6A9nIieTTI26MYXk1DmWEhmphTjZevusNWla+vvUigCmSjuH+xZw19w43OyvF3bu261Skypnm/mAalOTwg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@sentry/bundler-plugin-core@3.5.0':
|
'@sentry/bundler-plugin-core@3.5.0':
|
||||||
@ -1306,22 +1306,22 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@sentry/core@9.29.0':
|
'@sentry/core@9.30.0':
|
||||||
resolution: {integrity: sha512-wDyNe45PM+RCGtUn1tK7LzJ08ksv8i8KRUHrst7lsinEfRm83YH+wbWrPmwkVNEngUZvYkHwGLbNXM7xgFUuDQ==}
|
resolution: {integrity: sha512-JfEpeQ8a1qVJEb9DxpFTFy1J1gkNdlgKAPiqYGNnm4yQbnfl2Kb/iEo1if70FkiHc52H8fJwISEF90pzMm6lPg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@sentry/nextjs@9.29.0':
|
'@sentry/nextjs@9.30.0':
|
||||||
resolution: {integrity: sha512-chMSvo/CWsUw3bkGnURiOejW2hI95sofvFQQL2W98KGRhznfkfXhIh6U60fDpO2KaAbXDbbUCgcvBALdmILu9g==}
|
resolution: {integrity: sha512-9Ouf0Tng1HAPeYPaT9xUSj6jt/qV+h9/6Vf2yzIEGR4j1FbP5wdccGMs8LRdkp9msqEwv2ERnU////zHL1fpyA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
next: ^13.2.0 || ^14.0 || ^15.0.0-rc.0
|
next: ^13.2.0 || ^14.0 || ^15.0.0-rc.0
|
||||||
|
|
||||||
'@sentry/node@9.29.0':
|
'@sentry/node@9.30.0':
|
||||||
resolution: {integrity: sha512-oABipgC/fClRuvyMeK43rigv9F+OAaoR84UaMKB7aPXN6iz634wBRVsaoZAwiR3xLL+R7MafEPPA/s9XqlG7ag==}
|
resolution: {integrity: sha512-jHuSSKro2DUaccGcYSBbB8Rj0sG+LRh1iSWrJ+4c4Pj7tJFN9MbeMybC1buMSzAp+rwHUMZ3+ws0kgNVtsRJJg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@sentry/opentelemetry@9.29.0':
|
'@sentry/opentelemetry@9.30.0':
|
||||||
resolution: {integrity: sha512-QTUmre8i5+832RjzQW+g8IQ3UmBe5fbQXGbCF5hQ0UNuHle9r3Z8UZcIff5W8tm5AXMxPqvptTnDEZUUXHgBiA==}
|
resolution: {integrity: sha512-LhTmyGGLAP/LAxs/oXuPs0LC5Z80QSLL1oUoBRB5/+MitK7Huug6n8ZFjPTP3/6bT67XOVqILCdj8BwMlBeXhA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@opentelemetry/api': ^1.9.0
|
'@opentelemetry/api': ^1.9.0
|
||||||
@ -1331,14 +1331,14 @@ packages:
|
|||||||
'@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.0.0
|
'@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.0.0
|
||||||
'@opentelemetry/semantic-conventions': ^1.34.0
|
'@opentelemetry/semantic-conventions': ^1.34.0
|
||||||
|
|
||||||
'@sentry/react@9.29.0':
|
'@sentry/react@9.30.0':
|
||||||
resolution: {integrity: sha512-Wb8oKkIr/1yZ0GRz1UH4CRcjIU48iQLSDLXFKZ8YwPpPdC5qq7l4ALraxlcdB+uWq0JIgEjN5FSLamdt/NHX/w==}
|
resolution: {integrity: sha512-asA49AkZ/g9CCeW0eA0Ent0DF60S4k2IHxbu+Q1mqgbRRmbn859oL2Bgsu/EvzWf5edeQtuUml8LIo4YoFwfMA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.14.0 || 17.x || 18.x || 19.x
|
react: ^16.14.0 || 17.x || 18.x || 19.x
|
||||||
|
|
||||||
'@sentry/vercel-edge@9.29.0':
|
'@sentry/vercel-edge@9.30.0':
|
||||||
resolution: {integrity: sha512-Op31XnkkLwWImIXPVpX2ErEPGmXaWt5YvVxiikyCgH0OzfWUnBKXDl9lcPC2Kn02JCQOop8o9tmQUyOagfJrog==}
|
resolution: {integrity: sha512-zhxXEXQbf1ggyyR6pf/ZED8cj5Ubb2iObnpZaGHNWRccsToq7EecW0LKUJjdWKqDSqlv1DaI5yUAmn4oheQ4zQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@sentry/webpack-plugin@3.5.0':
|
'@sentry/webpack-plugin@3.5.0':
|
||||||
@ -1557,8 +1557,8 @@ packages:
|
|||||||
'@types/mysql@2.15.26':
|
'@types/mysql@2.15.26':
|
||||||
resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==}
|
resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==}
|
||||||
|
|
||||||
'@types/node@20.19.0':
|
'@types/node@20.19.1':
|
||||||
resolution: {integrity: sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==}
|
resolution: {integrity: sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==}
|
||||||
|
|
||||||
'@types/pg-pool@2.0.6':
|
'@types/pg-pool@2.0.6':
|
||||||
resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==}
|
resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==}
|
||||||
@ -1598,63 +1598,63 @@ packages:
|
|||||||
'@types/ws@8.18.1':
|
'@types/ws@8.18.1':
|
||||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.34.0':
|
'@typescript-eslint/eslint-plugin@8.34.1':
|
||||||
resolution: {integrity: sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==}
|
resolution: {integrity: sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@typescript-eslint/parser': ^8.34.0
|
'@typescript-eslint/parser': ^8.34.1
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: '>=4.8.4 <5.9.0'
|
typescript: '>=4.8.4 <5.9.0'
|
||||||
|
|
||||||
'@typescript-eslint/parser@8.34.0':
|
'@typescript-eslint/parser@8.34.1':
|
||||||
resolution: {integrity: sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==}
|
resolution: {integrity: sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: '>=4.8.4 <5.9.0'
|
typescript: '>=4.8.4 <5.9.0'
|
||||||
|
|
||||||
'@typescript-eslint/project-service@8.34.0':
|
'@typescript-eslint/project-service@8.34.1':
|
||||||
resolution: {integrity: sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==}
|
resolution: {integrity: sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.8.4 <5.9.0'
|
typescript: '>=4.8.4 <5.9.0'
|
||||||
|
|
||||||
'@typescript-eslint/scope-manager@8.34.0':
|
'@typescript-eslint/scope-manager@8.34.1':
|
||||||
resolution: {integrity: sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==}
|
resolution: {integrity: sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@typescript-eslint/tsconfig-utils@8.34.0':
|
'@typescript-eslint/tsconfig-utils@8.34.1':
|
||||||
resolution: {integrity: sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==}
|
resolution: {integrity: sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.8.4 <5.9.0'
|
typescript: '>=4.8.4 <5.9.0'
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@8.34.0':
|
'@typescript-eslint/type-utils@8.34.1':
|
||||||
resolution: {integrity: sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==}
|
resolution: {integrity: sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: '>=4.8.4 <5.9.0'
|
typescript: '>=4.8.4 <5.9.0'
|
||||||
|
|
||||||
'@typescript-eslint/types@8.34.0':
|
'@typescript-eslint/types@8.34.1':
|
||||||
resolution: {integrity: sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==}
|
resolution: {integrity: sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@typescript-eslint/typescript-estree@8.34.0':
|
'@typescript-eslint/typescript-estree@8.34.1':
|
||||||
resolution: {integrity: sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==}
|
resolution: {integrity: sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.8.4 <5.9.0'
|
typescript: '>=4.8.4 <5.9.0'
|
||||||
|
|
||||||
'@typescript-eslint/utils@8.34.0':
|
'@typescript-eslint/utils@8.34.1':
|
||||||
resolution: {integrity: sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==}
|
resolution: {integrity: sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: '>=4.8.4 <5.9.0'
|
typescript: '>=4.8.4 <5.9.0'
|
||||||
|
|
||||||
'@typescript-eslint/visitor-keys@8.34.0':
|
'@typescript-eslint/visitor-keys@8.34.1':
|
||||||
resolution: {integrity: sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==}
|
resolution: {integrity: sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@unrs/resolver-binding-android-arm-eabi@1.9.0':
|
'@unrs/resolver-binding-android-arm-eabi@1.9.0':
|
||||||
@ -2091,8 +2091,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
electron-to-chromium@1.5.167:
|
electron-to-chromium@1.5.170:
|
||||||
resolution: {integrity: sha512-LxcRvnYO5ez2bMOFpbuuVuAI5QNeY1ncVytE/KXaL6ZNfzX1yPlAO0nSOyIHx2fVAuUprMqPs/TdVhUFZy7SIQ==}
|
resolution: {integrity: sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA==}
|
||||||
|
|
||||||
emoji-regex@9.2.2:
|
emoji-regex@9.2.2:
|
||||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||||
@ -2212,8 +2212,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9
|
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9
|
||||||
|
|
||||||
eslint-plugin-prettier@5.4.1:
|
eslint-plugin-prettier@5.5.0:
|
||||||
resolution: {integrity: sha512-9dF+KuU/Ilkq27A8idRP7N2DH8iUR6qXcjF3FR2wETY21PZdBrIjwCau8oboyGj9b7etWmTGEeM8e7oOed6ZWg==}
|
resolution: {integrity: sha512-8qsOYwkkGrahrgoUv76NZi23koqXOGiiEzXMrT8Q7VcYaUISR+5MorIUxfWqYXN0fN/31WbSrxCxFkVQ43wwrA==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/eslint': '>=8.0.0'
|
'@types/eslint': '>=8.0.0'
|
||||||
@ -2995,8 +2995,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
|
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
postcss@8.5.5:
|
postcss@8.5.6:
|
||||||
resolution: {integrity: sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==}
|
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
postgres-array@2.0.0:
|
postgres-array@2.0.0:
|
||||||
@ -3108,8 +3108,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.1.0
|
react: ^19.1.0
|
||||||
|
|
||||||
react-hook-form@7.58.0:
|
react-hook-form@7.58.1:
|
||||||
resolution: {integrity: sha512-zGijmEed35oNfOfy7ub99jfjkiLhHwA3dl5AgyKdWC6QQzhnc7tkWewSa+T+A2EpLrc6wo5DUoZctS9kufWJjA==}
|
resolution: {integrity: sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||||
@ -3410,8 +3410,8 @@ packages:
|
|||||||
uglify-js:
|
uglify-js:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
terser@5.42.0:
|
terser@5.43.0:
|
||||||
resolution: {integrity: sha512-UYCvU9YQW2f/Vwl+P0GfhxJxbUGLwd+5QrrGgLajzWAtC/23AX0vcise32kkP7Eu0Wu9VlzzHAXkLObgjQfFlQ==}
|
resolution: {integrity: sha512-CqNNxKSGKSZCunSvwKLTs8u8sGGlp27sxNZ4quGh0QeNuyHM0JSEM/clM9Mf4zUp6J+tO2gUXhgXT2YMMkwfKQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@ -3465,8 +3465,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
|
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
typescript-eslint@8.34.0:
|
typescript-eslint@8.34.1:
|
||||||
resolution: {integrity: sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ==}
|
resolution: {integrity: sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
@ -3613,8 +3613,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
zod@3.25.64:
|
zod@3.25.67:
|
||||||
resolution: {integrity: sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==}
|
resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
@ -3804,10 +3804,10 @@ snapshots:
|
|||||||
|
|
||||||
'@floating-ui/utils@0.2.9': {}
|
'@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:
|
dependencies:
|
||||||
'@standard-schema/utils': 0.3.0
|
'@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': {}
|
'@humanfs/core@0.19.1': {}
|
||||||
|
|
||||||
@ -4584,7 +4584,7 @@ snapshots:
|
|||||||
|
|
||||||
'@rollup/plugin-commonjs@28.0.1(rollup@4.35.0)':
|
'@rollup/plugin-commonjs@28.0.1(rollup@4.35.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rollup/pluginutils': 5.1.4(rollup@4.35.0)
|
'@rollup/pluginutils': 5.2.0(rollup@4.35.0)
|
||||||
commondir: 1.0.1
|
commondir: 1.0.1
|
||||||
estree-walker: 2.0.2
|
estree-walker: 2.0.2
|
||||||
fdir: 6.4.6(picomatch@4.0.2)
|
fdir: 6.4.6(picomatch@4.0.2)
|
||||||
@ -4594,7 +4594,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
rollup: 4.35.0
|
rollup: 4.35.0
|
||||||
|
|
||||||
'@rollup/pluginutils@5.1.4(rollup@4.35.0)':
|
'@rollup/pluginutils@5.2.0(rollup@4.35.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
estree-walker: 2.0.2
|
estree-walker: 2.0.2
|
||||||
@ -4663,33 +4663,33 @@ snapshots:
|
|||||||
|
|
||||||
'@rushstack/eslint-patch@1.11.0': {}
|
'@rushstack/eslint-patch@1.11.0': {}
|
||||||
|
|
||||||
'@sentry-internal/browser-utils@9.29.0':
|
'@sentry-internal/browser-utils@9.30.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sentry/core': 9.29.0
|
'@sentry/core': 9.30.0
|
||||||
|
|
||||||
'@sentry-internal/feedback@9.29.0':
|
'@sentry-internal/feedback@9.30.0':
|
||||||
dependencies:
|
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:
|
dependencies:
|
||||||
'@sentry-internal/replay': 9.29.0
|
'@sentry-internal/replay': 9.30.0
|
||||||
'@sentry/core': 9.29.0
|
'@sentry/core': 9.30.0
|
||||||
|
|
||||||
'@sentry-internal/replay@9.29.0':
|
'@sentry-internal/replay@9.30.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sentry-internal/browser-utils': 9.29.0
|
'@sentry-internal/browser-utils': 9.30.0
|
||||||
'@sentry/core': 9.29.0
|
'@sentry/core': 9.30.0
|
||||||
|
|
||||||
'@sentry/babel-plugin-component-annotate@3.5.0': {}
|
'@sentry/babel-plugin-component-annotate@3.5.0': {}
|
||||||
|
|
||||||
'@sentry/browser@9.29.0':
|
'@sentry/browser@9.30.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sentry-internal/browser-utils': 9.29.0
|
'@sentry-internal/browser-utils': 9.30.0
|
||||||
'@sentry-internal/feedback': 9.29.0
|
'@sentry-internal/feedback': 9.30.0
|
||||||
'@sentry-internal/replay': 9.29.0
|
'@sentry-internal/replay': 9.30.0
|
||||||
'@sentry-internal/replay-canvas': 9.29.0
|
'@sentry-internal/replay-canvas': 9.30.0
|
||||||
'@sentry/core': 9.29.0
|
'@sentry/core': 9.30.0
|
||||||
|
|
||||||
'@sentry/bundler-plugin-core@3.5.0':
|
'@sentry/bundler-plugin-core@3.5.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -4745,19 +4745,19 @@ snapshots:
|
|||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
'@opentelemetry/semantic-conventions': 1.34.0
|
'@opentelemetry/semantic-conventions': 1.34.0
|
||||||
'@rollup/plugin-commonjs': 28.0.1(rollup@4.35.0)
|
'@rollup/plugin-commonjs': 28.0.1(rollup@4.35.0)
|
||||||
'@sentry-internal/browser-utils': 9.29.0
|
'@sentry-internal/browser-utils': 9.30.0
|
||||||
'@sentry/core': 9.29.0
|
'@sentry/core': 9.30.0
|
||||||
'@sentry/node': 9.29.0
|
'@sentry/node': 9.30.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/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.29.0(react@19.1.0)
|
'@sentry/react': 9.30.0(react@19.1.0)
|
||||||
'@sentry/vercel-edge': 9.29.0
|
'@sentry/vercel-edge': 9.30.0
|
||||||
'@sentry/webpack-plugin': 3.5.0(webpack@5.99.9)
|
'@sentry/webpack-plugin': 3.5.0(webpack@5.99.9)
|
||||||
chalk: 3.0.0
|
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)
|
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
|
- supports-color
|
||||||
- webpack
|
- webpack
|
||||||
|
|
||||||
'@sentry/node@9.29.0':
|
'@sentry/node@9.30.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
'@opentelemetry/context-async-hooks': 1.30.1(@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/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0)
|
||||||
'@opentelemetry/semantic-conventions': 1.34.0
|
'@opentelemetry/semantic-conventions': 1.34.0
|
||||||
'@prisma/instrumentation': 6.8.2(@opentelemetry/api@1.9.0)
|
'@prisma/instrumentation': 6.8.2(@opentelemetry/api@1.9.0)
|
||||||
'@sentry/core': 9.29.0
|
'@sentry/core': 9.30.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/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
|
import-in-the-middle: 1.14.2
|
||||||
minimatch: 9.0.5
|
minimatch: 9.0.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
'@opentelemetry/context-async-hooks': 1.30.1(@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/instrumentation': 0.57.2(@opentelemetry/api@1.9.0)
|
||||||
'@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0)
|
'@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0)
|
||||||
'@opentelemetry/semantic-conventions': 1.34.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:
|
dependencies:
|
||||||
'@sentry/browser': 9.29.0
|
'@sentry/browser': 9.30.0
|
||||||
'@sentry/core': 9.29.0
|
'@sentry/core': 9.30.0
|
||||||
hoist-non-react-statics: 3.3.2
|
hoist-non-react-statics: 3.3.2
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
|
|
||||||
'@sentry/vercel-edge@9.29.0':
|
'@sentry/vercel-edge@9.30.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@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)':
|
'@sentry/webpack-plugin@3.5.0(webpack@5.99.9)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -4900,17 +4900,17 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
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:
|
optionalDependencies:
|
||||||
typescript: 5.8.3
|
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:
|
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:
|
optionalDependencies:
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
zod: 3.25.64
|
zod: 3.25.67
|
||||||
|
|
||||||
'@tailwindcss/node@4.1.10':
|
'@tailwindcss/node@4.1.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -4981,12 +4981,12 @@ snapshots:
|
|||||||
'@alloc/quick-lru': 5.2.0
|
'@alloc/quick-lru': 5.2.0
|
||||||
'@tailwindcss/node': 4.1.10
|
'@tailwindcss/node': 4.1.10
|
||||||
'@tailwindcss/oxide': 4.1.10
|
'@tailwindcss/oxide': 4.1.10
|
||||||
postcss: 8.5.5
|
postcss: 8.5.6
|
||||||
tailwindcss: 4.1.10
|
tailwindcss: 4.1.10
|
||||||
|
|
||||||
'@tanstack/eslint-plugin-query@5.78.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)':
|
'@tanstack/eslint-plugin-query@5.78.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)':
|
||||||
dependencies:
|
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)
|
eslint: 9.29.0(jiti@2.4.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@ -5007,15 +5007,15 @@ snapshots:
|
|||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
'@types/node': 20.19.0
|
'@types/node': 20.19.1
|
||||||
|
|
||||||
'@types/connect@3.4.38':
|
'@types/connect@3.4.38':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.19.0
|
'@types/node': 20.19.1
|
||||||
|
|
||||||
'@types/cors@2.8.19':
|
'@types/cors@2.8.19':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.19.0
|
'@types/node': 20.19.1
|
||||||
|
|
||||||
'@types/eslint-scope@3.7.7':
|
'@types/eslint-scope@3.7.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -5033,7 +5033,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/express-serve-static-core@5.0.6':
|
'@types/express-serve-static-core@5.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.19.0
|
'@types/node': 20.19.1
|
||||||
'@types/qs': 6.14.0
|
'@types/qs': 6.14.0
|
||||||
'@types/range-parser': 1.2.7
|
'@types/range-parser': 1.2.7
|
||||||
'@types/send': 0.17.5
|
'@types/send': 0.17.5
|
||||||
@ -5054,9 +5054,9 @@ snapshots:
|
|||||||
|
|
||||||
'@types/mysql@2.15.26':
|
'@types/mysql@2.15.26':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.19.0
|
'@types/node': 20.19.1
|
||||||
|
|
||||||
'@types/node@20.19.0':
|
'@types/node@20.19.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
@ -5066,7 +5066,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/pg@8.6.1':
|
'@types/pg@8.6.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.19.0
|
'@types/node': 20.19.1
|
||||||
pg-protocol: 1.10.0
|
pg-protocol: 1.10.0
|
||||||
pg-types: 2.2.0
|
pg-types: 2.2.0
|
||||||
|
|
||||||
@ -5087,32 +5087,32 @@ snapshots:
|
|||||||
'@types/send@0.17.5':
|
'@types/send@0.17.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mime': 1.3.5
|
'@types/mime': 1.3.5
|
||||||
'@types/node': 20.19.0
|
'@types/node': 20.19.1
|
||||||
|
|
||||||
'@types/serve-static@1.15.8':
|
'@types/serve-static@1.15.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/http-errors': 2.0.5
|
'@types/http-errors': 2.0.5
|
||||||
'@types/node': 20.19.0
|
'@types/node': 20.19.1
|
||||||
'@types/send': 0.17.5
|
'@types/send': 0.17.5
|
||||||
|
|
||||||
'@types/shimmer@1.2.0': {}
|
'@types/shimmer@1.2.0': {}
|
||||||
|
|
||||||
'@types/tedious@4.0.14':
|
'@types/tedious@4.0.14':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.19.0
|
'@types/node': 20.19.1
|
||||||
|
|
||||||
'@types/ws@8.18.1':
|
'@types/ws@8.18.1':
|
||||||
dependencies:
|
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:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.1
|
'@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/parser': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
|
||||||
'@typescript-eslint/scope-manager': 8.34.0
|
'@typescript-eslint/scope-manager': 8.34.1
|
||||||
'@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)
|
||||||
'@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)
|
||||||
'@typescript-eslint/visitor-keys': 8.34.0
|
'@typescript-eslint/visitor-keys': 8.34.1
|
||||||
eslint: 9.29.0(jiti@2.4.2)
|
eslint: 9.29.0(jiti@2.4.2)
|
||||||
graphemer: 1.4.0
|
graphemer: 1.4.0
|
||||||
ignore: 7.0.5
|
ignore: 7.0.5
|
||||||
@ -5122,40 +5122,40 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
'@typescript-eslint/scope-manager': 8.34.0
|
'@typescript-eslint/scope-manager': 8.34.1
|
||||||
'@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)
|
||||||
'@typescript-eslint/visitor-keys': 8.34.0
|
'@typescript-eslint/visitor-keys': 8.34.1
|
||||||
debug: 4.4.1
|
debug: 4.4.1
|
||||||
eslint: 9.29.0(jiti@2.4.2)
|
eslint: 9.29.0(jiti@2.4.2)
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
'@typescript-eslint/tsconfig-utils': 8.34.0(typescript@5.8.3)
|
'@typescript-eslint/tsconfig-utils': 8.34.1(typescript@5.8.3)
|
||||||
'@typescript-eslint/types': 8.34.0
|
'@typescript-eslint/types': 8.34.1
|
||||||
debug: 4.4.1
|
debug: 4.4.1
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/scope-manager@8.34.0':
|
'@typescript-eslint/scope-manager@8.34.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.34.0
|
'@typescript-eslint/types': 8.34.1
|
||||||
'@typescript-eslint/visitor-keys': 8.34.0
|
'@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:
|
dependencies:
|
||||||
typescript: 5.8.3
|
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:
|
dependencies:
|
||||||
'@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3)
|
'@typescript-eslint/typescript-estree': 8.34.1(typescript@5.8.3)
|
||||||
'@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)
|
||||||
debug: 4.4.1
|
debug: 4.4.1
|
||||||
eslint: 9.29.0(jiti@2.4.2)
|
eslint: 9.29.0(jiti@2.4.2)
|
||||||
ts-api-utils: 2.1.0(typescript@5.8.3)
|
ts-api-utils: 2.1.0(typescript@5.8.3)
|
||||||
@ -5163,14 +5163,14 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
'@typescript-eslint/project-service': 8.34.0(typescript@5.8.3)
|
'@typescript-eslint/project-service': 8.34.1(typescript@5.8.3)
|
||||||
'@typescript-eslint/tsconfig-utils': 8.34.0(typescript@5.8.3)
|
'@typescript-eslint/tsconfig-utils': 8.34.1(typescript@5.8.3)
|
||||||
'@typescript-eslint/types': 8.34.0
|
'@typescript-eslint/types': 8.34.1
|
||||||
'@typescript-eslint/visitor-keys': 8.34.0
|
'@typescript-eslint/visitor-keys': 8.34.1
|
||||||
debug: 4.4.1
|
debug: 4.4.1
|
||||||
fast-glob: 3.3.3
|
fast-glob: 3.3.3
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@ -5181,20 +5181,20 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0(jiti@2.4.2))
|
'@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0(jiti@2.4.2))
|
||||||
'@typescript-eslint/scope-manager': 8.34.0
|
'@typescript-eslint/scope-manager': 8.34.1
|
||||||
'@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)
|
||||||
eslint: 9.29.0(jiti@2.4.2)
|
eslint: 9.29.0(jiti@2.4.2)
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/visitor-keys@8.34.0':
|
'@typescript-eslint/visitor-keys@8.34.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.34.0
|
'@typescript-eslint/types': 8.34.1
|
||||||
eslint-visitor-keys: 4.2.1
|
eslint-visitor-keys: 4.2.1
|
||||||
|
|
||||||
'@unrs/resolver-binding-android-arm-eabi@1.9.0':
|
'@unrs/resolver-binding-android-arm-eabi@1.9.0':
|
||||||
@ -5491,7 +5491,7 @@ snapshots:
|
|||||||
browserslist@4.25.0:
|
browserslist@4.25.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
caniuse-lite: 1.0.30001723
|
caniuse-lite: 1.0.30001723
|
||||||
electron-to-chromium: 1.5.167
|
electron-to-chromium: 1.5.170
|
||||||
node-releases: 2.0.19
|
node-releases: 2.0.19
|
||||||
update-browserslist-db: 1.1.3(browserslist@4.25.0)
|
update-browserslist-db: 1.1.3(browserslist@4.25.0)
|
||||||
|
|
||||||
@ -5652,7 +5652,7 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
gopd: 1.2.0
|
gopd: 1.2.0
|
||||||
|
|
||||||
electron-to-chromium@1.5.167: {}
|
electron-to-chromium@1.5.170: {}
|
||||||
|
|
||||||
emoji-regex@9.2.2: {}
|
emoji-regex@9.2.2: {}
|
||||||
|
|
||||||
@ -5772,12 +5772,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@next/eslint-plugin-next': 15.3.3
|
'@next/eslint-plugin-next': 15.3.3
|
||||||
'@rushstack/eslint-patch': 1.11.0
|
'@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/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.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: 9.29.0(jiti@2.4.2)
|
||||||
eslint-import-resolver-node: 0.3.9
|
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-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-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: 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))
|
eslint-plugin-react-hooks: 5.2.0(eslint@9.29.0(jiti@2.4.2))
|
||||||
@ -5811,22 +5811,22 @@ snapshots:
|
|||||||
tinyglobby: 0.2.14
|
tinyglobby: 0.2.14
|
||||||
unrs-resolver: 1.9.0
|
unrs-resolver: 1.9.0
|
||||||
optionalDependencies:
|
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:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
optionalDependencies:
|
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: 9.29.0(jiti@2.4.2)
|
||||||
eslint-import-resolver-node: 0.3.9
|
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-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.29.0(jiti@2.4.2))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
'@rtsao/scc': 1.1.0
|
'@rtsao/scc': 1.1.0
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
@ -5837,7 +5837,7 @@ snapshots:
|
|||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 9.29.0(jiti@2.4.2)
|
eslint: 9.29.0(jiti@2.4.2)
|
||||||
eslint-import-resolver-node: 0.3.9
|
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
|
hasown: 2.0.2
|
||||||
is-core-module: 2.16.1
|
is-core-module: 2.16.1
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@ -5849,7 +5849,7 @@ snapshots:
|
|||||||
string.prototype.trimend: 1.0.9
|
string.prototype.trimend: 1.0.9
|
||||||
tsconfig-paths: 3.15.0
|
tsconfig-paths: 3.15.0
|
||||||
optionalDependencies:
|
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:
|
transitivePeerDependencies:
|
||||||
- eslint-import-resolver-typescript
|
- eslint-import-resolver-typescript
|
||||||
- eslint-import-resolver-webpack
|
- eslint-import-resolver-webpack
|
||||||
@ -5874,7 +5874,7 @@ snapshots:
|
|||||||
safe-regex-test: 1.1.0
|
safe-regex-test: 1.1.0
|
||||||
string.prototype.includes: 2.0.1
|
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:
|
dependencies:
|
||||||
eslint: 9.29.0(jiti@2.4.2)
|
eslint: 9.29.0(jiti@2.4.2)
|
||||||
prettier: 3.5.3
|
prettier: 3.5.3
|
||||||
@ -6326,7 +6326,7 @@ snapshots:
|
|||||||
|
|
||||||
jest-worker@27.5.1:
|
jest-worker@27.5.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.19.0
|
'@types/node': 20.19.1
|
||||||
merge-stream: 2.0.0
|
merge-stream: 2.0.0
|
||||||
supports-color: 8.1.1
|
supports-color: 8.1.1
|
||||||
|
|
||||||
@ -6657,7 +6657,7 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
postcss@8.5.5:
|
postcss@8.5.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
nanoid: 3.3.11
|
nanoid: 3.3.11
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
@ -6708,7 +6708,7 @@ snapshots:
|
|||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
scheduler: 0.26.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:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
|
|
||||||
@ -7087,10 +7087,10 @@ snapshots:
|
|||||||
jest-worker: 27.5.1
|
jest-worker: 27.5.1
|
||||||
schema-utils: 4.3.2
|
schema-utils: 4.3.2
|
||||||
serialize-javascript: 6.0.2
|
serialize-javascript: 6.0.2
|
||||||
terser: 5.42.0
|
terser: 5.43.0
|
||||||
webpack: 5.99.9
|
webpack: 5.99.9
|
||||||
|
|
||||||
terser@5.42.0:
|
terser@5.43.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/source-map': 0.3.6
|
'@jridgewell/source-map': 0.3.6
|
||||||
acorn: 8.15.0
|
acorn: 8.15.0
|
||||||
@ -7162,11 +7162,11 @@ snapshots:
|
|||||||
possible-typed-array-names: 1.1.0
|
possible-typed-array-names: 1.1.0
|
||||||
reflect.getprototypeof: 1.0.10
|
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:
|
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/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.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)
|
||||||
'@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)
|
eslint: 9.29.0(jiti@2.4.2)
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@ -7358,4 +7358,4 @@ snapshots:
|
|||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
zod@3.25.64: {}
|
zod@3.25.67: {}
|
||||||
|
@ -390,7 +390,8 @@ const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
|||||||
return (
|
return (
|
||||||
<html lang='en' className={`${geist.variable}`} suppressHydrationWarning>
|
<html lang='en' className={`${geist.variable}`} suppressHydrationWarning>
|
||||||
<body
|
<body
|
||||||
className={cn('bg-background text-foreground font-sans antialiased')}
|
className={cn('bg-background text-foreground font-sans antialiased m-10\
|
||||||
|
leading-relaxed px-10')}
|
||||||
>
|
>
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
attribute='class'
|
attribute='class'
|
||||||
|
@ -9,6 +9,10 @@ export const generateMetadata = (): Metadata => {
|
|||||||
const SignInLayout = ({
|
const SignInLayout = ({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{ children: React.ReactNode }>) => {
|
}: Readonly<{ children: React.ReactNode }>) => {
|
||||||
return <div>{children}</div>;
|
return (
|
||||||
|
<div className=''>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
export default SignInLayout;
|
export default SignInLayout;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { StatusList } from '@/components/status/List';
|
import { StatusList } from '@/components/status';
|
||||||
import { getUser, getRecentUsersWithStatuses } from '@/lib/actions';
|
import { getUser, getRecentUsersWithStatuses } from '@/lib/actions';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ const AvatarDropdown = () => {
|
|||||||
<BasedAvatar
|
<BasedAvatar
|
||||||
src={profile?.avatar_url}
|
src={profile?.avatar_url}
|
||||||
fullName={profile?.full_name}
|
fullName={profile?.full_name}
|
||||||
className='h-12 w-12 my-auto'
|
className='lg:h-12 lg:w-12 my-auto'
|
||||||
fallbackClassName='text-xl font-semibold'
|
fallbackClassName='text-xl font-semibold'
|
||||||
userIconSize={32}
|
userIconSize={32}
|
||||||
/>
|
/>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { ThemeToggle, useTVMode } from '@/components/context';
|
import { ThemeToggle, useTVMode } from '@/components/context';
|
||||||
@ -9,28 +8,38 @@ import AvatarDropdown from './AvatarDropdown';
|
|||||||
const Header = () => {
|
const Header = () => {
|
||||||
const { tvMode } = useTVMode();
|
const { tvMode } = useTVMode();
|
||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
return tvMode ? (
|
|
||||||
<div className='w-full py-2 pt-6 md:py-5'>
|
// Controls component for both modes
|
||||||
<div className='absolute top-8 right-16'>
|
const Controls = () => (
|
||||||
<div className='flex flex-row my-auto items-center'>
|
<div className='flex flex-row items-center'>
|
||||||
<ThemeToggle className='mr-4' />
|
<ThemeToggle className='mr-4' />
|
||||||
{isAuthenticated && <AvatarDropdown />}
|
{isAuthenticated && <AvatarDropdown />}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tvMode) {
|
||||||
|
return (
|
||||||
|
<div className='absolute top-10 right-37'>
|
||||||
|
<Controls />
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className='w-full mb-8'>
|
||||||
|
<div className='container mx-auto px-4 md:px-6 lg:px-20'>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
{/* Left spacer for perfect centering */}
|
||||||
|
<div className='flex flex-1 justify-start'>
|
||||||
|
<div className='sm:w-[120px] md:w-[160px]' />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<header className='w-full py-2 md:py-5'>
|
{/* Centered logo and title */}
|
||||||
<div className='absolute top-8 right-16'>
|
<div className='flex-shrink-0'>
|
||||||
<div className='flex flex-row my-auto items-center'>
|
|
||||||
<ThemeToggle className='mr-4' />
|
|
||||||
{isAuthenticated && <AvatarDropdown />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Link
|
<Link
|
||||||
href='/'
|
href='/'
|
||||||
scroll={false}
|
scroll={false}
|
||||||
className='flex flex-row items-center text-center
|
className='flex flex-row items-center justify-center px-4'
|
||||||
justify-center sm:ml-0 p-4 mt-10 sm:mt-0'
|
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src='/favicon.png'
|
src='/favicon.png'
|
||||||
@ -39,15 +48,24 @@ const Header = () => {
|
|||||||
height={100}
|
height={100}
|
||||||
className='max-w-[40px] md:max-w-[120px]'
|
className='max-w-[40px] md:max-w-[120px]'
|
||||||
/>
|
/>
|
||||||
<h1
|
<h1 className='title-text text-sm md:text-4xl lg:text-8xl
|
||||||
className='title-text text-sm md:text-4xl lg:text-8xl
|
bg-gradient-to-r from-[#281A65] via-[#363354] to-accent-foreground
|
||||||
bg-gradient-to-r from-[#281A65] via-[#363354] to-accent-foreground dark:from-[#bec8e6] dark:via-[#F0EEE4] dark:to-[#FFF8E7]
|
dark:from-[#bec8e6] dark:via-[#F0EEE4] dark:to-[#FFF8E7]
|
||||||
font-bold pl-2 md:pl-12 text-transparent bg-clip-text'
|
font-bold pl-2 md:pl-12 text-transparent bg-clip-text'
|
||||||
>
|
>
|
||||||
Tech Tracker
|
Tech Tracker
|
||||||
</h1>
|
</h1>
|
||||||
</Link>
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right-aligned controls */}
|
||||||
|
<div className='flex-1 flex justify-end'>
|
||||||
|
<Controls />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
||||||
|
68
src/components/status/ConnectionStatus.tsx
Normal file
68
src/components/status/ConnectionStatus.tsx
Normal file
@ -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 <Wifi className='w-4 h-4 text-green-500' />;
|
||||||
|
case 'connecting':
|
||||||
|
return <Wifi className='w-4 h-4 text-yellow-500 animate-pulse' />;
|
||||||
|
case 'disconnected':
|
||||||
|
return <WifiOff className='w-4 h-4 text-red-500' />;
|
||||||
|
case 'updating':
|
||||||
|
return <RefreshCw className='w-4 h-4 text-blue-500 animate-spin' />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
size='sm'
|
||||||
|
onClick={onReconnect}
|
||||||
|
className={`flex items-center gap-2 cursor-pointer ${className}`}
|
||||||
|
>
|
||||||
|
{getConnectionIcon(status)}
|
||||||
|
<span className='text-base'>{getConnectionText(status)}</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant='outline'
|
||||||
|
className={`flex items-center gap-2 ${className}`}
|
||||||
|
>
|
||||||
|
{getConnectionIcon(status)}
|
||||||
|
<span className='text-base'>{getConnectionText(status)}</span>
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
412
src/components/status/List.tsx
Executable file → Normal file
412
src/components/status/List.tsx
Executable file → Normal file
@ -1,35 +1,28 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useAuth, useTVMode } from '@/components/context';
|
import { useAuth, useTVMode } from '@/components/context';
|
||||||
import {
|
import type { UserWithStatus } from '@/lib/hooks';
|
||||||
getRecentUsersWithStatuses,
|
|
||||||
updateStatuses,
|
|
||||||
updateUserStatus,
|
|
||||||
type UserWithStatus,
|
|
||||||
} from '@/lib/hooks';
|
|
||||||
import { BasedAvatar, Drawer, DrawerTrigger, Loading } from '@/components/ui';
|
import { BasedAvatar, Drawer, DrawerTrigger, Loading } from '@/components/ui';
|
||||||
import { StatusMessage, SubmitButton } from '@/components/default';
|
import { StatusMessage, SubmitButton } from '@/components/default';
|
||||||
import { toast } from 'sonner';
|
import { ConnectionStatus, HistoryDrawer } from '@/components/status';
|
||||||
import { HistoryDrawer } from '@/components/status';
|
|
||||||
import type { Profile } from '@/utils/supabase';
|
import type { Profile } from '@/utils/supabase';
|
||||||
import { makeConditionalClassName } from '@/lib/utils';
|
import { makeConditionalClassName } from '@/lib/utils';
|
||||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { RefreshCw, Clock, Wifi, WifiOff, Calendar } from 'lucide-react';
|
import { RefreshCw, Clock, Calendar } from 'lucide-react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useStatusData, useSharedStatusSubscription } from '@/lib/hooks';
|
||||||
import { QueryErrorCodes } from '@/components/context';
|
import { formatTime, formatDate } from '@/lib/utils';
|
||||||
import { createClient } from '@/utils/supabase';
|
import Link from 'next/link';
|
||||||
import type { RealtimeChannel } from '@supabase/supabase-js';
|
|
||||||
|
|
||||||
type ListProps = { initialStatuses: UserWithStatus[] };
|
type ListProps = {
|
||||||
|
initialStatuses: UserWithStatus[]
|
||||||
|
};
|
||||||
|
|
||||||
export const StatusList = ({ initialStatuses = [] }: ListProps) => {
|
export const StatusList = ({ initialStatuses = [] }: ListProps) => {
|
||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
const { tvMode } = useTVMode();
|
const { tvMode } = useTVMode();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const [selectedUsers, setSelectedUsers] = useState<UserWithStatus[]>([]);
|
const [selectedUsers, setSelectedUsers] = useState<UserWithStatus[]>([]);
|
||||||
const [selectAll, setSelectAll] = useState(false);
|
const [selectAll, setSelectAll] = useState(false);
|
||||||
@ -38,216 +31,64 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
|
|||||||
useState<Profile | null>(null);
|
useState<Profile | null>(null);
|
||||||
const [updateStatusMessage, setUpdateStatusMessage] = useState('');
|
const [updateStatusMessage, setUpdateStatusMessage] = useState('');
|
||||||
|
|
||||||
const [connectionStatus, setConnectionStatus] = useState<
|
|
||||||
'connecting' | 'connected' | 'disconnected' | 'updating'
|
|
||||||
>('connecting');
|
|
||||||
const [newStatuses, setNewStatuses] = useState<Set<UserWithStatus>>(new Set());
|
|
||||||
const channelRef = useRef<RealtimeChannel | null>(null);
|
|
||||||
const supabaseRef = useRef(createClient());
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: usersWithStatuses = initialStatuses,
|
data: usersWithStatuses = initialStatuses,
|
||||||
isLoading: loading,
|
isLoading: loading,
|
||||||
error,
|
error,
|
||||||
refetch,
|
refetch,
|
||||||
isFetching,
|
newStatuses,
|
||||||
dataUpdatedAt,
|
updateStatusMutation,
|
||||||
} = useQuery({
|
} = useStatusData({
|
||||||
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,
|
|
||||||
initialData: initialStatuses,
|
initialData: initialStatuses,
|
||||||
meta: { errCode: QueryErrorCodes.USERS_FETCH_FAILED },
|
enabled: isAuthenticated,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
// In your StatusList component
|
||||||
if (!isAuthenticated) return;
|
const { connectionStatus, connect: reconnect } = useSharedStatusSubscription(() => {
|
||||||
|
|
||||||
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) => {
|
refetch().catch((error) => {
|
||||||
console.error(`Error refetching statuses: ${error as Error}`)
|
console.error('Error refetching statuses:', 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(() => {
|
//const { connectionStatus, connect: reconnect } = useStatusSubscription({
|
||||||
if (isComponentMounted) setUpRealtimeConnection();
|
//enabled: isAuthenticated,
|
||||||
}, 1000);
|
//onStatusUpdate: () => {
|
||||||
|
//refetch().catch((error) => {
|
||||||
return () => {
|
//console.error('Error refetching statuses:', error);
|
||||||
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<UserWithStatus[]>([
|
|
||||||
'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}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleUpdateStatus = () => {
|
const handleUpdateStatus = () => {
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
setUpdateStatusMessage('You must be signed in to update technican statuses!')
|
setUpdateStatusMessage(
|
||||||
return;
|
'Error: You must be signed in to update technician statuses!'
|
||||||
} else if (!statusInput.trim()) {
|
);
|
||||||
setUpdateStatusMessage('Your status must be in between 3 & 80 characters long!');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (statusInput.length < 3 || statusInput.length > 80) {
|
||||||
|
setUpdateStatusMessage(
|
||||||
|
'Error: Your status must be between 3 & 80 characters long!'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
updateStatusMutation.mutate({
|
updateStatusMutation.mutate({
|
||||||
usersWithStatuses: selectedUsers,
|
usersWithStatuses: selectedUsers,
|
||||||
status: statusInput.trim(),
|
status: statusInput.trim(),
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
|
setSelectedUsers([]);
|
||||||
|
setStatusInput('');
|
||||||
|
setUpdateStatusMessage('');
|
||||||
|
};
|
||||||
|
|
||||||
const handleCheckboxChange = (user: UserWithStatus) => {
|
const handleCheckboxChange = (user: UserWithStatus) => {
|
||||||
setSelectedUsers((prev) =>
|
setSelectedUsers((prev) =>
|
||||||
prev.some((u) => u.user.id === user.user.id)
|
prev.some((u) => u.user.id === user.user.id)
|
||||||
? prev.filter((prevUser) => prevUser.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(() => {
|
useEffect(() => {
|
||||||
setSelectAll(
|
setSelectAll(
|
||||||
selectedUsers.length === usersWithStatuses.length &&
|
selectedUsers.length === usersWithStatuses.length &&
|
||||||
usersWithStatuses.length > 0,
|
usersWithStatuses.length > 0
|
||||||
);
|
);
|
||||||
}, [selectedUsers.length, usersWithStatuses.length]);
|
}, [selectedUsers.length, usersWithStatuses.length]);
|
||||||
|
|
||||||
const getConnectionIcon = () => {
|
|
||||||
switch (connectionStatus) {
|
|
||||||
case 'connected':
|
|
||||||
return <Wifi className='w-4 h-4 text-green-500' />;
|
|
||||||
case 'connecting':
|
|
||||||
return <Wifi className='w-4 h-4 text-yellow-500 animate-pulse' />;
|
|
||||||
case 'disconnected':
|
|
||||||
return <WifiOff className='w-4 h-4 text-red-500' />;
|
|
||||||
case 'updating':
|
|
||||||
return <RefreshCw className='w-4 h-4 text-blue-500 animate-spin' />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className='flex justify-center items-center min-h-[400px]'>
|
<div className='flex justify-center items-center min-h-[400px]'>
|
||||||
@ -360,38 +159,56 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClassName}>
|
<div className={containerClassName}>
|
||||||
|
|
||||||
<div className={headerClassName}>
|
<div className={headerClassName}>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-10'>
|
||||||
|
<div className='flex gap-2'>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id='select-all'
|
id='select-all'
|
||||||
checked={selectAll}
|
checked={selectAll}
|
||||||
onCheckedChange={handleSelectAllChange}
|
onCheckedChange={handleSelectAllChange}
|
||||||
|
className='size-6'
|
||||||
/>
|
/>
|
||||||
<label htmlFor='select-all' className='text-sm font-medium'>
|
<label htmlFor='select-all' className='font-medium'>
|
||||||
Select All
|
Select All
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
{!tvMode && (
|
||||||
|
<div className='flex flex-row gap-2'>
|
||||||
|
<p>Miss the old table?</p>
|
||||||
|
<Link
|
||||||
|
href='/status/table'
|
||||||
|
className='italic font-semibold text-accent-foreground'
|
||||||
|
>
|
||||||
|
Find it here!
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Badge variant='outline' className='flex items-center gap-2'>
|
<ConnectionStatus
|
||||||
{getConnectionIcon()}
|
status={connectionStatus}
|
||||||
<span className='text-sm'>{getConnectionText()}</span>
|
onReconnect={reconnect}
|
||||||
</Badge>
|
showAsButton={connectionStatus === 'disconnected'}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cardContainerClassName}>
|
<div className={cardContainerClassName}>
|
||||||
{usersWithStatuses.map((userWithStatus) => {
|
{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 isNewStatus = newStatuses.has(userWithStatus);
|
||||||
const isUpdatedByOther = userWithStatus.updated_by &&
|
const isUpdatedByOther =
|
||||||
|
userWithStatus.updated_by &&
|
||||||
userWithStatus.updated_by.id !== userWithStatus.user.id;
|
userWithStatus.updated_by.id !== userWithStatus.user.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={userWithStatus.user.id}
|
key={userWithStatus.user.id}
|
||||||
className={`
|
className={`
|
||||||
${cardClassName}
|
${cardClassName}
|
||||||
${isSelected ? 'ring2 ring-primary' : ''}
|
${isSelected ? 'ring-2 ring-primary' : ''}
|
||||||
${isNewStatus ? 'animate-pulse bg-primary/5 border-primary/20' : ''}
|
${isNewStatus ? 'animate-pulse bg-primary/5 border-primary/20' : ''}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
@ -401,7 +218,9 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
|
|||||||
{!tvMode && (
|
{!tvMode && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onCheckedChange={() => handleCheckboxChange(userWithStatus)}
|
onCheckedChange={() =>
|
||||||
|
handleCheckboxChange(userWithStatus)
|
||||||
|
}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -411,7 +230,9 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
|
|||||||
className={tvMode ? 'w-24 h-24' : 'w-16 h-16'}
|
className={tvMode ? 'w-24 h-24' : 'w-16 h-16'}
|
||||||
/>
|
/>
|
||||||
<div className='my-auto'>
|
<div className='my-auto'>
|
||||||
<h3 className={`font-semibold ${tvMode ? 'text-5xl' : 'text-2xl'}`}>
|
<h3
|
||||||
|
className={`font-semibold ${tvMode ? 'text-5xl' : 'text-2xl'}`}
|
||||||
|
>
|
||||||
{userWithStatus.user.full_name}
|
{userWithStatus.user.full_name}
|
||||||
</h3>
|
</h3>
|
||||||
{isUpdatedByOther && (
|
{isUpdatedByOther && (
|
||||||
@ -421,7 +242,9 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
|
|||||||
fullName={userWithStatus.updated_by?.full_name}
|
fullName={userWithStatus.updated_by?.full_name}
|
||||||
className='w-5 h-5'
|
className='w-5 h-5'
|
||||||
/>
|
/>
|
||||||
<span className={`${tvMode ? 'text-3xl' : 'text-sm'}`}>
|
<span
|
||||||
|
className={`${tvMode ? 'text-3xl' : 'text-sm'}`}
|
||||||
|
>
|
||||||
Updated by {userWithStatus.updated_by?.full_name}
|
Updated by {userWithStatus.updated_by?.full_name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -436,7 +259,9 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2 text-muted-foreground'>
|
<div className='flex items-center gap-2 text-muted-foreground'>
|
||||||
<Calendar className={`${tvMode ? 'w-8 h-8' : 'w-6 h-6'}`} />
|
<Calendar
|
||||||
|
className={`${tvMode ? 'w-8 h-8' : 'w-6 h-6'}`}
|
||||||
|
/>
|
||||||
<span className={`${tvMode ? 'text-3xl' : 'text-xl'}`}>
|
<span className={`${tvMode ? 'text-3xl' : 'text-xl'}`}>
|
||||||
{formatDate(userWithStatus.created_at)}
|
{formatDate(userWithStatus.created_at)}
|
||||||
</span>
|
</span>
|
||||||
@ -444,7 +269,6 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className='pt-0'>
|
<CardContent className='pt-0'>
|
||||||
<Drawer>
|
<Drawer>
|
||||||
<DrawerTrigger asChild>
|
<DrawerTrigger asChild>
|
||||||
@ -454,7 +278,9 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
|
|||||||
transition-colors cursor-pointer text-left
|
transition-colors cursor-pointer text-left
|
||||||
${tvMode ? 'text-4xl' : 'text-xl'}
|
${tvMode ? 'text-4xl' : 'text-xl'}
|
||||||
`}
|
`}
|
||||||
onClick={() => setSelectedHistoryUser(userWithStatus.user)}
|
onClick={() =>
|
||||||
|
setSelectedHistoryUser(userWithStatus.user)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<p className='font-medium'>{userWithStatus.status}</p>
|
<p className='font-medium'>{userWithStatus.status}</p>
|
||||||
</div>
|
</div>
|
||||||
@ -471,13 +297,77 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
|
|||||||
|
|
||||||
{usersWithStatuses.length === 0 && (
|
{usersWithStatuses.length === 0 && (
|
||||||
<Card className='p-8 text-center'>
|
<Card className='p-8 text-center'>
|
||||||
<p className={`text-muted-foreground ${tvMode ? 'text-4xl' : 'text-lg'}`}>
|
<p
|
||||||
|
className={`text-muted-foreground ${tvMode ? 'text-4xl' : 'text-lg'}`}
|
||||||
|
>
|
||||||
No status updates have been made in the past day.
|
No status updates have been made in the past day.
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!tvMode && (
|
||||||
|
<Card className='p-6 mt-6'>
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
<h3 className='text-lg font-semibold'>Update Status</h3>
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
<div className='flex gap-4'>
|
||||||
|
<Input
|
||||||
|
autoFocus
|
||||||
|
type='text'
|
||||||
|
placeholder='Enter status'
|
||||||
|
className='flex-1 text-base'
|
||||||
|
value={statusInput}
|
||||||
|
disabled={updateStatusMutation.isPending}
|
||||||
|
onChange={(e) => setStatusInput(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (
|
||||||
|
e.key === 'Enter' &&
|
||||||
|
!e.shiftKey &&
|
||||||
|
!updateStatusMutation.isPending
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleUpdateStatus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SubmitButton
|
||||||
|
onClick={handleUpdateStatus}
|
||||||
|
disabled={updateStatusMutation.isPending}
|
||||||
|
className='px-6'
|
||||||
|
>
|
||||||
|
{selectedUsers.length > 0
|
||||||
|
? `Update status for ${selectedUsers.length}
|
||||||
|
${selectedUsers.length > 1 ? 'users' : 'user'}`
|
||||||
|
: 'Update status'
|
||||||
|
}
|
||||||
|
</SubmitButton>
|
||||||
|
</div>
|
||||||
|
{updateStatusMessage &&
|
||||||
|
(updateStatusMessage.includes('Error') ||
|
||||||
|
updateStatusMessage.includes('error') ||
|
||||||
|
updateStatusMessage.includes('failed') ||
|
||||||
|
updateStatusMessage.includes('invalid') ? (
|
||||||
|
<StatusMessage message={{ error: updateStatusMessage }} />
|
||||||
|
) : (
|
||||||
|
<StatusMessage message={{ message: updateStatusMessage }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-center mt-2'>
|
||||||
|
<Drawer>
|
||||||
|
<DrawerTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
className={tvMode ? 'text-3xl p-6' : ''}
|
||||||
|
>
|
||||||
|
View All Status History
|
||||||
|
</Button>
|
||||||
|
</DrawerTrigger>
|
||||||
|
<HistoryDrawer />
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -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<UserWithStatus[]>([]);
|
|
||||||
const [selectAll, setSelectAll] = useState(false);
|
|
||||||
const [statusInput, setStatusInput] = useState('');
|
|
||||||
const [selectedHistoryUser, setSelectedHistoryUser] =
|
|
||||||
useState<Profile | null>(null);
|
|
||||||
|
|
||||||
const [connectionStatus, setConnectionStatus] = useState<
|
|
||||||
'connecting' | 'connected' | 'disconnected'
|
|
||||||
>('connecting');
|
|
||||||
const [newStatusIds, setNewStatusIds] = useState<Set<string>>(new Set());
|
|
||||||
const channelRef = useRef<RealtimeChannel | null>(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<UserWithStatus[]>([
|
|
||||||
'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 <Wifi className='w-4 h-4 text-green-500' />;
|
|
||||||
case 'connecting':
|
|
||||||
return <Wifi className='w-4 h-4 text-yellow-500 animate-pulse' />;
|
|
||||||
case 'disconnected':
|
|
||||||
return <WifiOff className='w-4 h-4 text-red-500' />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div className='flex justify-center items-center min-h-[400px]'>
|
|
||||||
<Loading className='w-full' alpha={0.5} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col justify-center items-center min-h-[400px] gap-4'>
|
|
||||||
<p className='text-red-500'>Error loading status updates</p>
|
|
||||||
<Button onClick={() => refetch()} variant='outline'>
|
|
||||||
<RefreshCw className='w-4 h-4 mr-2' />
|
|
||||||
Retry
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div className={containerClassName}>
|
|
||||||
{/* Status Header */}
|
|
||||||
<div className='flex items-center justify-between mb-6'>
|
|
||||||
<div className='flex items-center gap-2'>
|
|
||||||
<h2 className={`font-bold ${tvMode ? 'text-6xl' : 'text-2xl'}`}>
|
|
||||||
Tech Status
|
|
||||||
</h2>
|
|
||||||
{isFetching ? (
|
|
||||||
<Badge variant='outline' className='flex items-center gap-2'>
|
|
||||||
<RefreshCw className='w-4 h-4 animate-spin text-blue-500' />
|
|
||||||
<span className='text-xs'>Updating...</span>
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
<Badge variant='outline' className='flex items-center gap-2'>
|
|
||||||
{getConnectionIcon()}
|
|
||||||
<span className='text-xs'>{getConnectionText()}</span>
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{!tvMode && usersWithStatuses.length > 0 && (
|
|
||||||
<div className='flex items-center gap-2'>
|
|
||||||
<Checkbox
|
|
||||||
id='select-all'
|
|
||||||
checked={selectAll}
|
|
||||||
onCheckedChange={handleSelectAllChange}
|
|
||||||
/>
|
|
||||||
<label htmlFor='select-all' className='text-sm font-medium'>
|
|
||||||
Select All ({selectedUsers.length} selected)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Status Cards */}
|
|
||||||
<div className='space-y-3'>
|
|
||||||
{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 (
|
|
||||||
<Card
|
|
||||||
key={userWithStatus.user.id}
|
|
||||||
className={`
|
|
||||||
${cardClassName}
|
|
||||||
${isSelected ? 'ring-2 ring-primary' : ''}
|
|
||||||
${isNewStatus ? 'animate-pulse bg-primary/5 border-primary/20' : ''}
|
|
||||||
hover:bg-muted/50 cursor-pointer
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<CardHeader className='pb-3'>
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<div className='flex items-center gap-3'>
|
|
||||||
{!tvMode && (
|
|
||||||
<Checkbox
|
|
||||||
checked={isSelected}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
handleCheckboxChange(userWithStatus)
|
|
||||||
}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<BasedAvatar
|
|
||||||
src={userWithStatus.user.avatar_url}
|
|
||||||
fullName={userWithStatus.user.full_name}
|
|
||||||
className={tvMode ? 'w-16 h-16' : 'w-12 h-12'}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<h3
|
|
||||||
className={`font-semibold ${tvMode ? 'text-5xl' : 'text-lg'}`}
|
|
||||||
>
|
|
||||||
{userWithStatus.user.full_name ?? 'Unknown User'}
|
|
||||||
</h3>
|
|
||||||
{isUpdatedByOther && (
|
|
||||||
<div className='flex items-center gap-1 text-muted-foreground'>
|
|
||||||
<BasedAvatar
|
|
||||||
src={userWithStatus.updated_by?.avatar_url}
|
|
||||||
fullName={userWithStatus.updated_by?.full_name}
|
|
||||||
className='w-3 h-3'
|
|
||||||
/>
|
|
||||||
{userWithStatus.updated_by && (
|
|
||||||
<span
|
|
||||||
className={`text-xs ${tvMode ? 'text-3xl' : ''}`}
|
|
||||||
>
|
|
||||||
Updated by {userWithStatus.updated_by.full_name}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center gap-2 text-muted-foreground'>
|
|
||||||
<Clock className={`${tvMode ? 'w-8 h-8' : 'w-4 h-4'}`} />
|
|
||||||
<span className={`text-sm ${tvMode ? 'text-3xl' : ''}`}>
|
|
||||||
{formatTime(userWithStatus.created_at)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className='pt-0'>
|
|
||||||
<Drawer>
|
|
||||||
<DrawerTrigger asChild>
|
|
||||||
<div
|
|
||||||
className={`
|
|
||||||
p-4 rounded-lg bg-muted/30 hover:bg-muted/50
|
|
||||||
transition-colors cursor-pointer text-left
|
|
||||||
${tvMode ? 'text-4xl' : 'text-base'}
|
|
||||||
`}
|
|
||||||
onClick={() =>
|
|
||||||
setSelectedHistoryUser(userWithStatus.user)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<p className='font-medium'>{userWithStatus.status}</p>
|
|
||||||
</div>
|
|
||||||
</DrawerTrigger>
|
|
||||||
{selectedHistoryUser === userWithStatus.user && (
|
|
||||||
<HistoryDrawer user={selectedHistoryUser} />
|
|
||||||
)}
|
|
||||||
</Drawer>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{usersWithStatuses.length === 0 && (
|
|
||||||
<Card className='p-8 text-center'>
|
|
||||||
<p
|
|
||||||
className={`text-muted-foreground ${tvMode ? 'text-4xl' : 'text-lg'}`}
|
|
||||||
>
|
|
||||||
No status updates yet
|
|
||||||
</p>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Status Update Input */}
|
|
||||||
{!tvMode && (
|
|
||||||
<Card className='p-6 mt-6'>
|
|
||||||
<div className='flex flex-col gap-4'>
|
|
||||||
<h3 className='text-lg font-semibold'>Update Status</h3>
|
|
||||||
<div className='flex gap-4'>
|
|
||||||
<Input
|
|
||||||
autoFocus
|
|
||||||
type='text'
|
|
||||||
placeholder="What's your status?"
|
|
||||||
className='flex-1 text-base'
|
|
||||||
value={statusInput}
|
|
||||||
onChange={(e) => setStatusInput(e.target.value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (
|
|
||||||
e.key === 'Enter' &&
|
|
||||||
!e.shiftKey &&
|
|
||||||
!updateStatusMutation.isPending
|
|
||||||
) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleUpdateStatus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={updateStatusMutation.isPending}
|
|
||||||
/>
|
|
||||||
<SubmitButton
|
|
||||||
onClick={handleUpdateStatus}
|
|
||||||
disabled={updateStatusMutation.isPending}
|
|
||||||
className='px-6'
|
|
||||||
>
|
|
||||||
{updateStatusMutation.isPending
|
|
||||||
? 'Updating...'
|
|
||||||
: selectedUsers.length > 0
|
|
||||||
? `Update ${selectedUsers.length} Users`
|
|
||||||
: 'Update Status'}
|
|
||||||
</SubmitButton>
|
|
||||||
</div>
|
|
||||||
{selectedUsers.length > 0 && (
|
|
||||||
<p className='text-sm text-muted-foreground'>
|
|
||||||
Updating status for {selectedUsers.length} selected users
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Global Status History Drawer */}
|
|
||||||
<div className='flex justify-center mt-6'>
|
|
||||||
<Drawer>
|
|
||||||
<DrawerTrigger asChild>
|
|
||||||
<Button variant='outline' className={tvMode ? 'text-3xl p-6' : ''}>
|
|
||||||
View All Status History
|
|
||||||
</Button>
|
|
||||||
</DrawerTrigger>
|
|
||||||
<HistoryDrawer />
|
|
||||||
</Drawer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
377
src/components/status/Table.tsx
Normal file
377
src/components/status/Table.tsx
Normal file
@ -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<UserWithStatus[]>([]);
|
||||||
|
const [selectAll, setSelectAll] = useState(false);
|
||||||
|
const [statusInput, setStatusInput] = useState('');
|
||||||
|
const [selectedHistoryUser, setSelectedHistoryUser] =
|
||||||
|
useState<Profile | null>(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 (
|
||||||
|
<div className='flex justify-center items-center min-h-[400px]'>
|
||||||
|
<Loading className='w-full' alpha={0.5} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col justify-center items-center min-h-[400px] gap-4'>
|
||||||
|
<p className='text-red-500'>Error loading status updates</p>
|
||||||
|
<Button onClick={() => refetch()} variant='outline'>
|
||||||
|
<RefreshCw className='w-4 h-4 mr-2' />
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={containerClassName}>
|
||||||
|
<div className={headerClassName}>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<ConnectionStatus
|
||||||
|
status={connectionStatus}
|
||||||
|
onReconnect={reconnect}
|
||||||
|
showAsButton={connectionStatus === 'disconnected'}
|
||||||
|
/>
|
||||||
|
{!tvMode && (
|
||||||
|
<div className='flex flex-row gap-2'>
|
||||||
|
<p>Tired of the old table? {' '}</p>
|
||||||
|
<Link
|
||||||
|
href='/status/list'
|
||||||
|
className='italic font-semibold text-accent-foreground'
|
||||||
|
>
|
||||||
|
Try out the new status list!
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table className='w-full text-center rounded-md'>
|
||||||
|
<thead>
|
||||||
|
<tr className='bg-muted'>
|
||||||
|
{!tvMode && (
|
||||||
|
<th className={tCheckboxClassName}>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className={checkBoxClassName}
|
||||||
|
checked={selectAll}
|
||||||
|
onChange={handleSelectAllChange}
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
)}
|
||||||
|
<th className={thClassName}>Technician</th>
|
||||||
|
<th className={thClassName}>
|
||||||
|
<Drawer>
|
||||||
|
<DrawerTrigger className='hover:text-foreground/60 cursor-pointer'>
|
||||||
|
Status
|
||||||
|
</DrawerTrigger>
|
||||||
|
<HistoryDrawer />
|
||||||
|
</Drawer>
|
||||||
|
</th>
|
||||||
|
<th className={thClassName}>Updated At</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{usersWithStatuses.map((userWithStatus, index) => {
|
||||||
|
const isSelected = selectedUsers.some(
|
||||||
|
(u) => u.user.id === userWithStatus.user.id,
|
||||||
|
);
|
||||||
|
const isNewStatus = newStatuses.has(userWithStatus);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={userWithStatus.user.id}
|
||||||
|
className={`
|
||||||
|
${index % 2 === 0 ? 'bg-muted/50' : 'bg-background'}
|
||||||
|
${isSelected ? 'ring-2 ring-primary' : ''}
|
||||||
|
${isNewStatus ? 'animate-pulse bg-primary/5 border-primary/20' : ''}
|
||||||
|
hover:bg-muted/75 transition-all duration-300
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{!tvMode && (
|
||||||
|
<td className={tCheckboxClassName}>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className={checkBoxClassName}
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={() => handleCheckboxChange(userWithStatus)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td className={tdClassName}>
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<BasedAvatar
|
||||||
|
src={userWithStatus.user.avatar_url}
|
||||||
|
fullName={userWithStatus.user.full_name}
|
||||||
|
className={tvMode ? 'w-16 h-16' : 'w-12 h-12'}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p
|
||||||
|
className={`font-semibold ${tvMode ? 'text-5xl' : 'text-4xl'}`}
|
||||||
|
>
|
||||||
|
{userWithStatus.user.full_name ?? 'Unknown User'}
|
||||||
|
</p>
|
||||||
|
{userWithStatus.updated_by &&
|
||||||
|
userWithStatus.updated_by.id !==
|
||||||
|
userWithStatus.user.id && (
|
||||||
|
<div className='flex items-center gap-1 text-muted-foreground'>
|
||||||
|
<BasedAvatar
|
||||||
|
src={userWithStatus.updated_by?.avatar_url}
|
||||||
|
fullName={userWithStatus.updated_by?.full_name}
|
||||||
|
className='w-5 h-5'
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={tvMode ? 'text-lg' : 'text-base'}
|
||||||
|
>
|
||||||
|
Updated by {userWithStatus.updated_by.full_name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className={tdClassName}>
|
||||||
|
<Drawer>
|
||||||
|
<DrawerTrigger
|
||||||
|
className='text-center w-full p-2 rounded-md hover:bg-muted transition-colors'
|
||||||
|
onClick={() =>
|
||||||
|
setSelectedHistoryUser(userWithStatus.user)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{userWithStatus.status}
|
||||||
|
</DrawerTrigger>
|
||||||
|
{selectedHistoryUser === userWithStatus.user && (
|
||||||
|
<HistoryDrawer user={selectedHistoryUser} />
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
</td>
|
||||||
|
<td className={tdClassName}>
|
||||||
|
<div className='flex w-full'>
|
||||||
|
<div className='flex items-start xl:w-1/6'></div>
|
||||||
|
<div className='flex flex-col my-auto items-start'>
|
||||||
|
<div className='flex gap-4 my-1'>
|
||||||
|
<Clock className={`${tvMode ? 'lg:w-11 lg:h-11' : 'lg:w-9 lg:h-9'}`} />
|
||||||
|
{formatTime(userWithStatus.created_at)}
|
||||||
|
</div>
|
||||||
|
<div className='flex gap-4 my-1'>
|
||||||
|
<Calendar
|
||||||
|
className={`${tvMode ? 'lg:w-11 lg:h-11' : 'lg:w-9 lg:h-9'}`}
|
||||||
|
/>
|
||||||
|
{formatDate(userWithStatus.created_at)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{usersWithStatuses.length === 0 && (
|
||||||
|
<div className='p-8 text-center'>
|
||||||
|
<p
|
||||||
|
className={`text-muted-foreground ${tvMode ? 'text-4xl' : 'text-lg'}`}
|
||||||
|
>
|
||||||
|
No status updates yet
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{updateStatusMessage &&
|
||||||
|
(updateStatusMessage.includes('Error') ||
|
||||||
|
updateStatusMessage.includes('error') ||
|
||||||
|
updateStatusMessage.includes('failed') ||
|
||||||
|
updateStatusMessage.includes('invalid') ? (
|
||||||
|
<StatusMessage message={{ error: updateStatusMessage }} />
|
||||||
|
) : (
|
||||||
|
<StatusMessage message={{ message: updateStatusMessage }} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{!tvMode && (
|
||||||
|
<div className='mx-auto flex flex-row items-center justify-center py-5 gap-4'>
|
||||||
|
<Input
|
||||||
|
autoFocus
|
||||||
|
type='text'
|
||||||
|
placeholder='New Status'
|
||||||
|
className={
|
||||||
|
'min-w-[120px] lg:max-w-[400px] py-6 px-3 rounded-xl \
|
||||||
|
border bg-background lg:text-2xl focus:outline-none \
|
||||||
|
focus:ring-2 focus:ring-primary'
|
||||||
|
}
|
||||||
|
value={statusInput}
|
||||||
|
onChange={(e) => setStatusInput(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && !updateStatusMutation.isPending) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleUpdateStatus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={updateStatusMutation.isPending}
|
||||||
|
/>
|
||||||
|
<SubmitButton
|
||||||
|
size='xl'
|
||||||
|
className={
|
||||||
|
'px-8 rounded-xl font-semibold lg:text-2xl \
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed \
|
||||||
|
cursor-pointer'
|
||||||
|
}
|
||||||
|
onClick={handleUpdateStatus}
|
||||||
|
disabled={updateStatusMutation.isPending}
|
||||||
|
pendingText='Updating...'
|
||||||
|
>
|
||||||
|
{selectedUsers.length > 0
|
||||||
|
? `Update status for ${selectedUsers.length}
|
||||||
|
${selectedUsers.length > 1 ? 'users' : 'user'}`
|
||||||
|
: 'Update status'
|
||||||
|
}
|
||||||
|
</SubmitButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Global Status History Drawer */}
|
||||||
|
{!tvMode && (
|
||||||
|
<div className='flex justify-center mt-6'>
|
||||||
|
<Drawer>
|
||||||
|
<DrawerTrigger asChild>
|
||||||
|
<Button variant='outline' className={tvMode ? 'text-3xl p-6' : ''}>
|
||||||
|
View All Status History
|
||||||
|
</Button>
|
||||||
|
</DrawerTrigger>
|
||||||
|
<HistoryDrawer />
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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<UserWithStatus[]>([]);
|
|
||||||
const [selectAll, setSelectAll] = useState(false);
|
|
||||||
const [statusInput, setStatusInput] = useState('');
|
|
||||||
const [selectedHistoryUser, setSelectedHistoryUser] =
|
|
||||||
useState<Profile | null>(null);
|
|
||||||
const [connectionStatus, setConnectionStatus] = useState<
|
|
||||||
'connecting' | 'connected' | 'disconnected'
|
|
||||||
>('connecting');
|
|
||||||
const [newStatusIds, setNewStatusIds] = useState<Set<string>>(new Set());
|
|
||||||
const channelRef = useRef<RealtimeChannel | null>(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<UserWithStatus[]>([
|
|
||||||
'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 <Wifi className='w-4 h-4 text-green-500' />;
|
|
||||||
case 'connecting':
|
|
||||||
return <Wifi className='w-4 h-4 text-yellow-500 animate-pulse' />;
|
|
||||||
case 'disconnected':
|
|
||||||
return <WifiOff className='w-4 h-4 text-red-500' />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div className='flex justify-center items-center min-h-[400px]'>
|
|
||||||
<Loading className='w-full' alpha={0.5} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col justify-center items-center min-h-[400px] gap-4'>
|
|
||||||
<p className='text-red-500'>Error loading status updates</p>
|
|
||||||
<Button onClick={() => refetch()} variant='outline'>
|
|
||||||
<RefreshCw className='w-4 h-4 mr-2' />
|
|
||||||
Retry
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div className={containerClassName}>
|
|
||||||
{/* Status Header */}
|
|
||||||
<div className='flex items-center justify-between mb-6'>
|
|
||||||
{isFetching ? (
|
|
||||||
<Badge variant='outline' className='flex items-center gap-2'>
|
|
||||||
<RefreshCw className='w-4 h-4 animate-spin text-blue-500' />
|
|
||||||
<span className='text-xs'>Updating...</span>
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
<Badge variant='outline' className='flex items-center gap-2'>
|
|
||||||
{getConnectionIcon()}
|
|
||||||
<span className='text-xs'>{getConnectionText()}</span>
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table className='w-full text-center rounded-md'>
|
|
||||||
<thead>
|
|
||||||
<tr className='bg-muted'>
|
|
||||||
{!tvMode && (
|
|
||||||
<th className={tCheckboxClassName}>
|
|
||||||
<input
|
|
||||||
type='checkbox'
|
|
||||||
className={checkBoxClassName}
|
|
||||||
checked={selectAll}
|
|
||||||
onChange={handleSelectAllChange}
|
|
||||||
/>
|
|
||||||
</th>
|
|
||||||
)}
|
|
||||||
<th className={thClassName}>Technician</th>
|
|
||||||
<th className={thClassName}>
|
|
||||||
<Drawer>
|
|
||||||
<DrawerTrigger className='hover:text-foreground/60 cursor-pointer'>
|
|
||||||
Status
|
|
||||||
</DrawerTrigger>
|
|
||||||
<HistoryDrawer />
|
|
||||||
</Drawer>
|
|
||||||
</th>
|
|
||||||
<th className={thClassName}>Updated At</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{usersWithStatuses.map((userWithStatus, index) => {
|
|
||||||
const isSelected = selectedUsers.some(
|
|
||||||
(u) => u.user.id === userWithStatus.user.id,
|
|
||||||
);
|
|
||||||
const isNewStatus = newStatusIds.has(userWithStatus.user.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={userWithStatus.user.id}
|
|
||||||
className={`
|
|
||||||
${index % 2 === 0 ? 'bg-muted/50' : 'bg-background'}
|
|
||||||
${isSelected ? 'ring-2 ring-primary' : ''}
|
|
||||||
${isNewStatus ? 'animate-pulse bg-primary/5 border-primary/20' : ''}
|
|
||||||
hover:bg-muted/75 transition-all duration-300
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{!tvMode && (
|
|
||||||
<td className={tCheckboxClassName}>
|
|
||||||
<input
|
|
||||||
type='checkbox'
|
|
||||||
className={checkBoxClassName}
|
|
||||||
checked={isSelected}
|
|
||||||
onChange={() => handleCheckboxChange(userWithStatus)}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
<td className={tdClassName}>
|
|
||||||
<div className='flex items-center gap-3'>
|
|
||||||
<BasedAvatar
|
|
||||||
src={userWithStatus.user.avatar_url}
|
|
||||||
fullName={userWithStatus.user.full_name}
|
|
||||||
className={tvMode ? 'w-16 h-16' : 'w-12 h-12'}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<p
|
|
||||||
className={`font-semibold ${tvMode ? 'text-5xl' : 'text-lg'}`}
|
|
||||||
>
|
|
||||||
{userWithStatus.user.full_name ?? 'Unknown User'}
|
|
||||||
</p>
|
|
||||||
{userWithStatus.updated_by &&
|
|
||||||
userWithStatus.updated_by.id !==
|
|
||||||
userWithStatus.user.id && (
|
|
||||||
<div className='flex items-center gap-1 text-muted-foreground'>
|
|
||||||
<BasedAvatar
|
|
||||||
src={userWithStatus.updated_by?.avatar_url}
|
|
||||||
fullName={userWithStatus.updated_by?.full_name}
|
|
||||||
className='w-3 h-3'
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={`text-xs ${tvMode ? 'text-3xl' : ''}`}
|
|
||||||
>
|
|
||||||
Updated by {userWithStatus.updated_by.full_name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className={tdClassName}>
|
|
||||||
<Drawer>
|
|
||||||
<DrawerTrigger
|
|
||||||
className='text-center w-full p-2 rounded-md hover:bg-muted transition-colors'
|
|
||||||
onClick={() =>
|
|
||||||
setSelectedHistoryUser(userWithStatus.user)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{userWithStatus.status}
|
|
||||||
</DrawerTrigger>
|
|
||||||
{selectedHistoryUser === userWithStatus.user && (
|
|
||||||
<HistoryDrawer user={selectedHistoryUser} />
|
|
||||||
)}
|
|
||||||
</Drawer>
|
|
||||||
</td>
|
|
||||||
<td className={tdClassName}>
|
|
||||||
{formatTime(userWithStatus.created_at)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{usersWithStatuses.length === 0 && (
|
|
||||||
<div className='p-8 text-center'>
|
|
||||||
<p
|
|
||||||
className={`text-muted-foreground ${tvMode ? 'text-4xl' : 'text-lg'}`}
|
|
||||||
>
|
|
||||||
No status updates yet
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!tvMode && (
|
|
||||||
<div className='mx-auto flex flex-row items-center justify-center py-5 gap-4'>
|
|
||||||
<input
|
|
||||||
autoFocus
|
|
||||||
type='text'
|
|
||||||
placeholder='New Status'
|
|
||||||
className={
|
|
||||||
'min-w-[120px] lg:min-w-[400px] py-2 px-3 rounded-xl \
|
|
||||||
border bg-background lg:text-2xl focus:outline-none \
|
|
||||||
focus:ring-2 focus:ring-primary'
|
|
||||||
}
|
|
||||||
value={statusInput}
|
|
||||||
onChange={(e) => setStatusInput(e.target.value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' && !updateStatusMutation.isPending) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleUpdateStatus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={updateStatusMutation.isPending}
|
|
||||||
/>
|
|
||||||
<SubmitButton
|
|
||||||
size='xl'
|
|
||||||
className={
|
|
||||||
'px-8 rounded-xl font-semibold lg:text-2xl \
|
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed \
|
|
||||||
cursor-pointer'
|
|
||||||
}
|
|
||||||
onClick={handleUpdateStatus}
|
|
||||||
disabled={!statusInput.trim() || updateStatusMutation.isPending}
|
|
||||||
disabledNotLoading={true}
|
|
||||||
>
|
|
||||||
{updateStatusMutation.isPending
|
|
||||||
? 'Updating...'
|
|
||||||
: selectedUsers.length > 0
|
|
||||||
? `Update ${selectedUsers.length} Users`
|
|
||||||
: 'Update Status'}
|
|
||||||
</SubmitButton>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedUsers.length > 0 && !tvMode && (
|
|
||||||
<div className='text-center mt-4'>
|
|
||||||
<p className='text-sm text-muted-foreground'>
|
|
||||||
Updating status for {selectedUsers.length} selected users
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Global Status History Drawer */}
|
|
||||||
<div className='flex justify-center mt-6'>
|
|
||||||
<Drawer>
|
|
||||||
<DrawerTrigger asChild>
|
|
||||||
<Button variant='outline' className={tvMode ? 'text-3xl p-6' : ''}>
|
|
||||||
View All Status History
|
|
||||||
</Button>
|
|
||||||
</DrawerTrigger>
|
|
||||||
<HistoryDrawer />
|
|
||||||
</Drawer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,3 +1,4 @@
|
|||||||
|
export * from './ConnectionStatus';
|
||||||
export * from './HistoryDrawer';
|
export * from './HistoryDrawer';
|
||||||
export * from './StatusList';
|
export * from './List';
|
||||||
export * from './TechTable';
|
export * from './Table';
|
||||||
|
@ -3,6 +3,8 @@ export * from './public';
|
|||||||
export * from './status';
|
export * from './status';
|
||||||
export * from './storage';
|
export * from './storage';
|
||||||
export * from './useFileUpload';
|
export * from './useFileUpload';
|
||||||
|
export * from './useSharedStatusSubscription';
|
||||||
|
export * from './useStatusData';
|
||||||
|
|
||||||
export type Result<T> =
|
export type Result<T> =
|
||||||
| { success: true; data: T }
|
| { success: true; data: T }
|
||||||
|
@ -135,7 +135,7 @@ export const updateStatuses = async (
|
|||||||
)
|
)
|
||||||
.select();
|
.select();
|
||||||
|
|
||||||
if (insertedStatusesError) throw new Error("Error inserting statuses!");
|
if (insertedStatusesError) throw new Error('Error inserting statuses!');
|
||||||
else if (insertedStatuses) {
|
else if (insertedStatuses) {
|
||||||
const createdAtFallback = new Date(Date.now()).toISOString();
|
const createdAtFallback = new Date(Date.now()).toISOString();
|
||||||
const statusUpdates = usersWithStatuses.map((s, i) => {
|
const statusUpdates = usersWithStatuses.map((s, i) => {
|
||||||
@ -144,7 +144,7 @@ export const updateStatuses = async (
|
|||||||
status: status,
|
status: status,
|
||||||
created_at: insertedStatuses[i]?.created_at ?? createdAtFallback,
|
created_at: insertedStatuses[i]?.created_at ?? createdAtFallback,
|
||||||
updated_by: user,
|
updated_by: user,
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
await broadcastStatusUpdates(statusUpdates);
|
await broadcastStatusUpdates(statusUpdates);
|
||||||
return { success: true, data: statusUpdates };
|
return { success: true, data: statusUpdates };
|
||||||
|
131
src/lib/hooks/useSharedStatusSubscription.ts
Normal file
131
src/lib/hooks/useSharedStatusSubscription.ts
Normal file
@ -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<ConnectionStatus>(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,
|
||||||
|
};
|
||||||
|
};
|
132
src/lib/hooks/useStatusData.ts
Normal file
132
src/lib/hooks/useStatusData.ts
Normal file
@ -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<Set<UserWithStatus>>(
|
||||||
|
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<UserWithStatus[]>([
|
||||||
|
'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,
|
||||||
|
};
|
||||||
|
};
|
147
src/lib/hooks/useStatusSubscription.ts
Normal file
147
src/lib/hooks/useStatusSubscription.ts
Normal file
@ -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<ConnectionStatus>('disconnected');
|
||||||
|
const channelRef = useRef<RealtimeChannel | null>(null);
|
||||||
|
const supabaseRef = useRef(createClient());
|
||||||
|
const reconnectAttemptsRef = useRef(0);
|
||||||
|
const reconnectTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||||
|
const isComponentMountedRef = useRef(true);
|
||||||
|
const visibilityTimeoutRef = useRef<NodeJS.Timeout | undefined>(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,
|
||||||
|
};
|
||||||
|
};
|
@ -18,3 +18,19 @@ export const makeConditionalClassName = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return defaultClassName + ' ' + (context ? on : off);
|
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}`;
|
||||||
|
};
|
||||||
|
@ -110,7 +110,9 @@ export const middleware = async (request: NextRequest) => {
|
|||||||
|
|
||||||
if (shouldBan) {
|
if (shouldBan) {
|
||||||
console.log(`🔨 IP ${ip} has been banned for suspicious activity`);
|
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
|
// Return 404 to not reveal the blocking mechanism
|
||||||
|
Reference in New Issue
Block a user