diff --git a/package.json b/package.json index 29c7e73..9493fef 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", - "@sentry/nextjs": "^9.29.0", + "@sentry/nextjs": "^9.30.0", "@supabase/ssr": "^0.6.1", "@supabase/supabase-js": "^2.50.0", "@t3-oss/env-nextjs": "^0.12.0", @@ -40,11 +40,11 @@ "next-themes": "^0.4.6", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-hook-form": "^7.58.0", + "react-hook-form": "^7.58.1", "require-in-the-middle": "^7.5.2", "sonner": "^2.0.5", "vaul": "^1.1.2", - "zod": "^3.25.64" + "zod": "^3.25.67" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", @@ -52,15 +52,15 @@ "@tanstack/eslint-plugin-query": "^5.78.0", "@types/cors": "^2.8.19", "@types/express": "^5.0.3", - "@types/node": "^20.19.0", + "@types/node": "^20.19.1", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "eslint": "^9.29.0", "eslint-config-next": "^15.3.3", "eslint-config-prettier": "^10.1.5", - "eslint-plugin-prettier": "^5.4.1", + "eslint-plugin-prettier": "^5.5.0", "import-in-the-middle": "^1.14.2", - "postcss": "^8.5.5", + "postcss": "^8.5.6", "prettier": "^3.5.3", "prettier-plugin-tailwindcss": "^0.6.12", "tailwind-merge": "^3.3.1", @@ -68,7 +68,7 @@ "tailwindcss-animate": "^1.0.7", "tw-animate-css": "^1.3.4", "typescript": "^5.8.3", - "typescript-eslint": "^8.34.0" + "typescript-eslint": "^8.34.1" }, "ct3aMetadata": { "initVersion": "7.39.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be4567c..4371aaf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: '@hookform/resolvers': specifier: ^5.1.1 - version: 5.1.1(react-hook-form@7.58.0(react@19.1.0)) + version: 5.1.1(react-hook-form@7.58.1(react@19.1.0)) '@radix-ui/react-avatar': specifier: ^1.1.10 version: 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -39,8 +39,8 @@ importers: specifier: ^1.2.3 version: 1.2.3(@types/react@19.1.8)(react@19.1.0) '@sentry/nextjs': - specifier: ^9.29.0 - version: 9.29.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.99.9) + specifier: ^9.30.0 + version: 9.30.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.99.9) '@supabase/ssr': specifier: ^0.6.1 version: 0.6.1(@supabase/supabase-js@2.50.0) @@ -49,7 +49,7 @@ importers: version: 2.50.0 '@t3-oss/env-nextjs': specifier: ^0.12.0 - version: 0.12.0(typescript@5.8.3)(zod@3.25.64) + version: 0.12.0(typescript@5.8.3)(zod@3.25.67) '@tanstack/react-query': specifier: ^5.80.7 version: 5.80.7(react@19.1.0) @@ -78,8 +78,8 @@ importers: specifier: ^19.1.0 version: 19.1.0(react@19.1.0) react-hook-form: - specifier: ^7.58.0 - version: 7.58.0(react@19.1.0) + specifier: ^7.58.1 + version: 7.58.1(react@19.1.0) require-in-the-middle: specifier: ^7.5.2 version: 7.5.2 @@ -90,8 +90,8 @@ importers: specifier: ^1.1.2 version: 1.1.2(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) zod: - specifier: ^3.25.64 - version: 3.25.64 + specifier: ^3.25.67 + version: 3.25.67 devDependencies: '@eslint/eslintrc': specifier: ^3.3.1 @@ -109,8 +109,8 @@ importers: specifier: ^5.0.3 version: 5.0.3 '@types/node': - specifier: ^20.19.0 - version: 20.19.0 + specifier: ^20.19.1 + version: 20.19.1 '@types/react': specifier: ^19.1.8 version: 19.1.8 @@ -127,14 +127,14 @@ importers: specifier: ^10.1.5 version: 10.1.5(eslint@9.29.0(jiti@2.4.2)) eslint-plugin-prettier: - specifier: ^5.4.1 - version: 5.4.1(@types/eslint@9.6.1)(eslint-config-prettier@10.1.5(eslint@9.29.0(jiti@2.4.2)))(eslint@9.29.0(jiti@2.4.2))(prettier@3.5.3) + specifier: ^5.5.0 + version: 5.5.0(@types/eslint@9.6.1)(eslint-config-prettier@10.1.5(eslint@9.29.0(jiti@2.4.2)))(eslint@9.29.0(jiti@2.4.2))(prettier@3.5.3) import-in-the-middle: specifier: ^1.14.2 version: 1.14.2 postcss: - specifier: ^8.5.5 - version: 8.5.5 + specifier: ^8.5.6 + version: 8.5.6 prettier: specifier: ^3.5.3 version: 3.5.3 @@ -157,8 +157,8 @@ importers: specifier: ^5.8.3 version: 5.8.3 typescript-eslint: - specifier: ^8.34.0 - version: 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + specifier: ^8.34.1 + version: 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) packages: @@ -1122,8 +1122,8 @@ packages: rollup: optional: true - '@rollup/pluginutils@5.1.4': - resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + '@rollup/pluginutils@5.2.0': + resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==} engines: {node: '>=14.0.0'} peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 @@ -1232,28 +1232,28 @@ packages: '@rushstack/eslint-patch@1.11.0': resolution: {integrity: sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==} - '@sentry-internal/browser-utils@9.29.0': - resolution: {integrity: sha512-Wp6UJCDVV2KVK+TG8GwdLZyDy4GtUYDmVhGMpHKPS3G/Qgpf36cY/XHwChwaHZ5P9Bk1sjS9Ok698J59S8L2nw==} + '@sentry-internal/browser-utils@9.30.0': + resolution: {integrity: sha512-e6ZlN8oWheCB0YJSGlBNUlh6UPnY5Ecj1P+/cgeKBhNm7c3bIx4J50485hB8LQsu+b7Q11L2o/wucZ//Pb6FCg==} engines: {node: '>=18'} - '@sentry-internal/feedback@9.29.0': - resolution: {integrity: sha512-ADvetGrtr+RfYcQKrQxah4fHs/xDJ/VjbStVMSuaNllzwWPYNkWIGFE6YjQ7wZszj0DQIu5/H+B6lZKsFYk4xw==} + '@sentry-internal/feedback@9.30.0': + resolution: {integrity: sha512-qAZ7xxLqZM7GlEvmSUmTHnoueg+fc7esMQD4vH8pS7HI3n9C5MjGn3HHlndRpD8lL7iUUQ0TPZQgU6McbzMDyw==} engines: {node: '>=18'} - '@sentry-internal/replay-canvas@9.29.0': - resolution: {integrity: sha512-TrQYhSAVPhyenvu0fNkon7BznFibu1mzS5bCudxhgOWajZluUVrXcbp8Q3WZ3R+AogrcgA3Vy6aumP/+fMKdwg==} + '@sentry-internal/replay-canvas@9.30.0': + resolution: {integrity: sha512-I4MxS27rfV7vnOU29L80y4baZ4I1XqpnYvC/yLN7C17nA8eDCufQ8WVomli41y8JETnfcxlm68z7CS0sO4RCSA==} engines: {node: '>=18'} - '@sentry-internal/replay@9.29.0': - resolution: {integrity: sha512-we/1JPRje8sNowQCyogOV1OYWuDOP/3XmDi48XoFG2HB0XMl2HfL5LI8AvgAvC/5nrqVAAo4ktbjoVLm1fb7rg==} + '@sentry-internal/replay@9.30.0': + resolution: {integrity: sha512-+6wkqQGLJuFUzvGRzbh3iIhFGyxQx/Oxc0ODDKmz9ag2xYRjCYb3UUQXmQX9navAF0HXUsq8ajoJPm2L1ZyWVg==} engines: {node: '>=18'} '@sentry/babel-plugin-component-annotate@3.5.0': resolution: {integrity: sha512-s2go8w03CDHbF9luFGtBHKJp4cSpsQzNVqgIa9Pfa4wnjipvrK6CxVT4icpLA3YO6kg5u622Yoa5GF3cJdippw==} engines: {node: '>= 14'} - '@sentry/browser@9.29.0': - resolution: {integrity: sha512-+GFX/yb+rh6V1fSgTYM6ttAgledl2aUR3T3Rg86HNuegbdX8ym6lOtUOIZ0j9jPK015HR47KIPyIZVZZJ7Rj9g==} + '@sentry/browser@9.30.0': + resolution: {integrity: sha512-sRyW6A9nIieTTI26MYXk1DmWEhmphTjZevusNWla+vvUigCmSjuH+xZw19w43OyvF3bu261Skypnm/mAalOTwg==} engines: {node: '>=18'} '@sentry/bundler-plugin-core@3.5.0': @@ -1306,22 +1306,22 @@ packages: engines: {node: '>= 10'} hasBin: true - '@sentry/core@9.29.0': - resolution: {integrity: sha512-wDyNe45PM+RCGtUn1tK7LzJ08ksv8i8KRUHrst7lsinEfRm83YH+wbWrPmwkVNEngUZvYkHwGLbNXM7xgFUuDQ==} + '@sentry/core@9.30.0': + resolution: {integrity: sha512-JfEpeQ8a1qVJEb9DxpFTFy1J1gkNdlgKAPiqYGNnm4yQbnfl2Kb/iEo1if70FkiHc52H8fJwISEF90pzMm6lPg==} engines: {node: '>=18'} - '@sentry/nextjs@9.29.0': - resolution: {integrity: sha512-chMSvo/CWsUw3bkGnURiOejW2hI95sofvFQQL2W98KGRhznfkfXhIh6U60fDpO2KaAbXDbbUCgcvBALdmILu9g==} + '@sentry/nextjs@9.30.0': + resolution: {integrity: sha512-9Ouf0Tng1HAPeYPaT9xUSj6jt/qV+h9/6Vf2yzIEGR4j1FbP5wdccGMs8LRdkp9msqEwv2ERnU////zHL1fpyA==} engines: {node: '>=18'} peerDependencies: next: ^13.2.0 || ^14.0 || ^15.0.0-rc.0 - '@sentry/node@9.29.0': - resolution: {integrity: sha512-oABipgC/fClRuvyMeK43rigv9F+OAaoR84UaMKB7aPXN6iz634wBRVsaoZAwiR3xLL+R7MafEPPA/s9XqlG7ag==} + '@sentry/node@9.30.0': + resolution: {integrity: sha512-jHuSSKro2DUaccGcYSBbB8Rj0sG+LRh1iSWrJ+4c4Pj7tJFN9MbeMybC1buMSzAp+rwHUMZ3+ws0kgNVtsRJJg==} engines: {node: '>=18'} - '@sentry/opentelemetry@9.29.0': - resolution: {integrity: sha512-QTUmre8i5+832RjzQW+g8IQ3UmBe5fbQXGbCF5hQ0UNuHle9r3Z8UZcIff5W8tm5AXMxPqvptTnDEZUUXHgBiA==} + '@sentry/opentelemetry@9.30.0': + resolution: {integrity: sha512-LhTmyGGLAP/LAxs/oXuPs0LC5Z80QSLL1oUoBRB5/+MitK7Huug6n8ZFjPTP3/6bT67XOVqILCdj8BwMlBeXhA==} engines: {node: '>=18'} peerDependencies: '@opentelemetry/api': ^1.9.0 @@ -1331,14 +1331,14 @@ packages: '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.0.0 '@opentelemetry/semantic-conventions': ^1.34.0 - '@sentry/react@9.29.0': - resolution: {integrity: sha512-Wb8oKkIr/1yZ0GRz1UH4CRcjIU48iQLSDLXFKZ8YwPpPdC5qq7l4ALraxlcdB+uWq0JIgEjN5FSLamdt/NHX/w==} + '@sentry/react@9.30.0': + resolution: {integrity: sha512-asA49AkZ/g9CCeW0eA0Ent0DF60S4k2IHxbu+Q1mqgbRRmbn859oL2Bgsu/EvzWf5edeQtuUml8LIo4YoFwfMA==} engines: {node: '>=18'} peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x - '@sentry/vercel-edge@9.29.0': - resolution: {integrity: sha512-Op31XnkkLwWImIXPVpX2ErEPGmXaWt5YvVxiikyCgH0OzfWUnBKXDl9lcPC2Kn02JCQOop8o9tmQUyOagfJrog==} + '@sentry/vercel-edge@9.30.0': + resolution: {integrity: sha512-zhxXEXQbf1ggyyR6pf/ZED8cj5Ubb2iObnpZaGHNWRccsToq7EecW0LKUJjdWKqDSqlv1DaI5yUAmn4oheQ4zQ==} engines: {node: '>=18'} '@sentry/webpack-plugin@3.5.0': @@ -1557,8 +1557,8 @@ packages: '@types/mysql@2.15.26': resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} - '@types/node@20.19.0': - resolution: {integrity: sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==} + '@types/node@20.19.1': + resolution: {integrity: sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==} '@types/pg-pool@2.0.6': resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} @@ -1598,63 +1598,63 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.34.0': - resolution: {integrity: sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==} + '@typescript-eslint/eslint-plugin@8.34.1': + resolution: {integrity: sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.34.0 + '@typescript-eslint/parser': ^8.34.1 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/parser@8.34.0': - resolution: {integrity: sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==} + '@typescript-eslint/parser@8.34.1': + resolution: {integrity: sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/project-service@8.34.0': - resolution: {integrity: sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==} + '@typescript-eslint/project-service@8.34.1': + resolution: {integrity: sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@8.34.0': - resolution: {integrity: sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==} + '@typescript-eslint/scope-manager@8.34.1': + resolution: {integrity: sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.34.0': - resolution: {integrity: sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==} + '@typescript-eslint/tsconfig-utils@8.34.1': + resolution: {integrity: sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/type-utils@8.34.0': - resolution: {integrity: sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==} + '@typescript-eslint/type-utils@8.34.1': + resolution: {integrity: sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/types@8.34.0': - resolution: {integrity: sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==} + '@typescript-eslint/types@8.34.1': + resolution: {integrity: sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.34.0': - resolution: {integrity: sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==} + '@typescript-eslint/typescript-estree@8.34.1': + resolution: {integrity: sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.34.0': - resolution: {integrity: sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==} + '@typescript-eslint/utils@8.34.1': + resolution: {integrity: sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@8.34.0': - resolution: {integrity: sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==} + '@typescript-eslint/visitor-keys@8.34.1': + resolution: {integrity: sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@unrs/resolver-binding-android-arm-eabi@1.9.0': @@ -2091,8 +2091,8 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - electron-to-chromium@1.5.167: - resolution: {integrity: sha512-LxcRvnYO5ez2bMOFpbuuVuAI5QNeY1ncVytE/KXaL6ZNfzX1yPlAO0nSOyIHx2fVAuUprMqPs/TdVhUFZy7SIQ==} + electron-to-chromium@1.5.170: + resolution: {integrity: sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} @@ -2212,8 +2212,8 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - eslint-plugin-prettier@5.4.1: - resolution: {integrity: sha512-9dF+KuU/Ilkq27A8idRP7N2DH8iUR6qXcjF3FR2wETY21PZdBrIjwCau8oboyGj9b7etWmTGEeM8e7oOed6ZWg==} + eslint-plugin-prettier@5.5.0: + resolution: {integrity: sha512-8qsOYwkkGrahrgoUv76NZi23koqXOGiiEzXMrT8Q7VcYaUISR+5MorIUxfWqYXN0fN/31WbSrxCxFkVQ43wwrA==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/eslint': '>=8.0.0' @@ -2995,8 +2995,8 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.5: - resolution: {integrity: sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==} + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -3108,8 +3108,8 @@ packages: peerDependencies: react: ^19.1.0 - react-hook-form@7.58.0: - resolution: {integrity: sha512-zGijmEed35oNfOfy7ub99jfjkiLhHwA3dl5AgyKdWC6QQzhnc7tkWewSa+T+A2EpLrc6wo5DUoZctS9kufWJjA==} + react-hook-form@7.58.1: + resolution: {integrity: sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 @@ -3410,8 +3410,8 @@ packages: uglify-js: optional: true - terser@5.42.0: - resolution: {integrity: sha512-UYCvU9YQW2f/Vwl+P0GfhxJxbUGLwd+5QrrGgLajzWAtC/23AX0vcise32kkP7Eu0Wu9VlzzHAXkLObgjQfFlQ==} + terser@5.43.0: + resolution: {integrity: sha512-CqNNxKSGKSZCunSvwKLTs8u8sGGlp27sxNZ4quGh0QeNuyHM0JSEM/clM9Mf4zUp6J+tO2gUXhgXT2YMMkwfKQ==} engines: {node: '>=10'} hasBin: true @@ -3465,8 +3465,8 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript-eslint@8.34.0: - resolution: {integrity: sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ==} + typescript-eslint@8.34.1: + resolution: {integrity: sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -3613,8 +3613,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zod@3.25.64: - resolution: {integrity: sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==} + zod@3.25.67: + resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==} snapshots: @@ -3804,10 +3804,10 @@ snapshots: '@floating-ui/utils@0.2.9': {} - '@hookform/resolvers@5.1.1(react-hook-form@7.58.0(react@19.1.0))': + '@hookform/resolvers@5.1.1(react-hook-form@7.58.1(react@19.1.0))': dependencies: '@standard-schema/utils': 0.3.0 - react-hook-form: 7.58.0(react@19.1.0) + react-hook-form: 7.58.1(react@19.1.0) '@humanfs/core@0.19.1': {} @@ -4584,7 +4584,7 @@ snapshots: '@rollup/plugin-commonjs@28.0.1(rollup@4.35.0)': dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.35.0) + '@rollup/pluginutils': 5.2.0(rollup@4.35.0) commondir: 1.0.1 estree-walker: 2.0.2 fdir: 6.4.6(picomatch@4.0.2) @@ -4594,7 +4594,7 @@ snapshots: optionalDependencies: rollup: 4.35.0 - '@rollup/pluginutils@5.1.4(rollup@4.35.0)': + '@rollup/pluginutils@5.2.0(rollup@4.35.0)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 @@ -4663,33 +4663,33 @@ snapshots: '@rushstack/eslint-patch@1.11.0': {} - '@sentry-internal/browser-utils@9.29.0': + '@sentry-internal/browser-utils@9.30.0': dependencies: - '@sentry/core': 9.29.0 + '@sentry/core': 9.30.0 - '@sentry-internal/feedback@9.29.0': + '@sentry-internal/feedback@9.30.0': dependencies: - '@sentry/core': 9.29.0 + '@sentry/core': 9.30.0 - '@sentry-internal/replay-canvas@9.29.0': + '@sentry-internal/replay-canvas@9.30.0': dependencies: - '@sentry-internal/replay': 9.29.0 - '@sentry/core': 9.29.0 + '@sentry-internal/replay': 9.30.0 + '@sentry/core': 9.30.0 - '@sentry-internal/replay@9.29.0': + '@sentry-internal/replay@9.30.0': dependencies: - '@sentry-internal/browser-utils': 9.29.0 - '@sentry/core': 9.29.0 + '@sentry-internal/browser-utils': 9.30.0 + '@sentry/core': 9.30.0 '@sentry/babel-plugin-component-annotate@3.5.0': {} - '@sentry/browser@9.29.0': + '@sentry/browser@9.30.0': dependencies: - '@sentry-internal/browser-utils': 9.29.0 - '@sentry-internal/feedback': 9.29.0 - '@sentry-internal/replay': 9.29.0 - '@sentry-internal/replay-canvas': 9.29.0 - '@sentry/core': 9.29.0 + '@sentry-internal/browser-utils': 9.30.0 + '@sentry-internal/feedback': 9.30.0 + '@sentry-internal/replay': 9.30.0 + '@sentry-internal/replay-canvas': 9.30.0 + '@sentry/core': 9.30.0 '@sentry/bundler-plugin-core@3.5.0': dependencies: @@ -4745,19 +4745,19 @@ snapshots: - encoding - supports-color - '@sentry/core@9.29.0': {} + '@sentry/core@9.30.0': {} - '@sentry/nextjs@9.29.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.99.9)': + '@sentry/nextjs@9.30.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.99.9)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.34.0 '@rollup/plugin-commonjs': 28.0.1(rollup@4.35.0) - '@sentry-internal/browser-utils': 9.29.0 - '@sentry/core': 9.29.0 - '@sentry/node': 9.29.0 - '@sentry/opentelemetry': 9.29.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0) - '@sentry/react': 9.29.0(react@19.1.0) - '@sentry/vercel-edge': 9.29.0 + '@sentry-internal/browser-utils': 9.30.0 + '@sentry/core': 9.30.0 + '@sentry/node': 9.30.0 + '@sentry/opentelemetry': 9.30.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0) + '@sentry/react': 9.30.0(react@19.1.0) + '@sentry/vercel-edge': 9.30.0 '@sentry/webpack-plugin': 3.5.0(webpack@5.99.9) chalk: 3.0.0 next: 15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -4774,7 +4774,7 @@ snapshots: - supports-color - webpack - '@sentry/node@9.29.0': + '@sentry/node@9.30.0': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) @@ -4806,14 +4806,14 @@ snapshots: '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.34.0 '@prisma/instrumentation': 6.8.2(@opentelemetry/api@1.9.0) - '@sentry/core': 9.29.0 - '@sentry/opentelemetry': 9.29.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0) + '@sentry/core': 9.30.0 + '@sentry/opentelemetry': 9.30.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0) import-in-the-middle: 1.14.2 minimatch: 9.0.5 transitivePeerDependencies: - supports-color - '@sentry/opentelemetry@9.29.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)': + '@sentry/opentelemetry@9.30.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) @@ -4821,19 +4821,19 @@ snapshots: '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.34.0 - '@sentry/core': 9.29.0 + '@sentry/core': 9.30.0 - '@sentry/react@9.29.0(react@19.1.0)': + '@sentry/react@9.30.0(react@19.1.0)': dependencies: - '@sentry/browser': 9.29.0 - '@sentry/core': 9.29.0 + '@sentry/browser': 9.30.0 + '@sentry/core': 9.30.0 hoist-non-react-statics: 3.3.2 react: 19.1.0 - '@sentry/vercel-edge@9.29.0': + '@sentry/vercel-edge@9.30.0': dependencies: '@opentelemetry/api': 1.9.0 - '@sentry/core': 9.29.0 + '@sentry/core': 9.30.0 '@sentry/webpack-plugin@3.5.0(webpack@5.99.9)': dependencies: @@ -4900,17 +4900,17 @@ snapshots: dependencies: tslib: 2.8.1 - '@t3-oss/env-core@0.12.0(typescript@5.8.3)(zod@3.25.64)': + '@t3-oss/env-core@0.12.0(typescript@5.8.3)(zod@3.25.67)': optionalDependencies: typescript: 5.8.3 - zod: 3.25.64 + zod: 3.25.67 - '@t3-oss/env-nextjs@0.12.0(typescript@5.8.3)(zod@3.25.64)': + '@t3-oss/env-nextjs@0.12.0(typescript@5.8.3)(zod@3.25.67)': dependencies: - '@t3-oss/env-core': 0.12.0(typescript@5.8.3)(zod@3.25.64) + '@t3-oss/env-core': 0.12.0(typescript@5.8.3)(zod@3.25.67) optionalDependencies: typescript: 5.8.3 - zod: 3.25.64 + zod: 3.25.67 '@tailwindcss/node@4.1.10': dependencies: @@ -4981,12 +4981,12 @@ snapshots: '@alloc/quick-lru': 5.2.0 '@tailwindcss/node': 4.1.10 '@tailwindcss/oxide': 4.1.10 - postcss: 8.5.5 + postcss: 8.5.6 tailwindcss: 4.1.10 '@tanstack/eslint-plugin-query@5.78.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/utils': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.29.0(jiti@2.4.2) transitivePeerDependencies: - supports-color @@ -5007,15 +5007,15 @@ snapshots: '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 20.19.0 + '@types/node': 20.19.1 '@types/connect@3.4.38': dependencies: - '@types/node': 20.19.0 + '@types/node': 20.19.1 '@types/cors@2.8.19': dependencies: - '@types/node': 20.19.0 + '@types/node': 20.19.1 '@types/eslint-scope@3.7.7': dependencies: @@ -5033,7 +5033,7 @@ snapshots: '@types/express-serve-static-core@5.0.6': dependencies: - '@types/node': 20.19.0 + '@types/node': 20.19.1 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 0.17.5 @@ -5054,9 +5054,9 @@ snapshots: '@types/mysql@2.15.26': dependencies: - '@types/node': 20.19.0 + '@types/node': 20.19.1 - '@types/node@20.19.0': + '@types/node@20.19.1': dependencies: undici-types: 6.21.0 @@ -5066,7 +5066,7 @@ snapshots: '@types/pg@8.6.1': dependencies: - '@types/node': 20.19.0 + '@types/node': 20.19.1 pg-protocol: 1.10.0 pg-types: 2.2.0 @@ -5087,32 +5087,32 @@ snapshots: '@types/send@0.17.5': dependencies: '@types/mime': 1.3.5 - '@types/node': 20.19.0 + '@types/node': 20.19.1 '@types/serve-static@1.15.8': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 20.19.0 + '@types/node': 20.19.1 '@types/send': 0.17.5 '@types/shimmer@1.2.0': {} '@types/tedious@4.0.14': dependencies: - '@types/node': 20.19.0 + '@types/node': 20.19.1 '@types/ws@8.18.1': dependencies: - '@types/node': 20.19.0 + '@types/node': 20.19.1 - '@typescript-eslint/eslint-plugin@8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.34.0 - '@typescript-eslint/type-utils': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.34.0 + '@typescript-eslint/parser': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.34.1 + '@typescript-eslint/type-utils': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.34.1 eslint: 9.29.0(jiti@2.4.2) graphemer: 1.4.0 ignore: 7.0.5 @@ -5122,40 +5122,40 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/scope-manager': 8.34.0 - '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.34.0 + '@typescript-eslint/scope-manager': 8.34.1 + '@typescript-eslint/types': 8.34.1 + '@typescript-eslint/typescript-estree': 8.34.1(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.34.1 debug: 4.4.1 eslint: 9.29.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.34.0(typescript@5.8.3)': + '@typescript-eslint/project-service@8.34.1(typescript@5.8.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.34.0(typescript@5.8.3) - '@typescript-eslint/types': 8.34.0 + '@typescript-eslint/tsconfig-utils': 8.34.1(typescript@5.8.3) + '@typescript-eslint/types': 8.34.1 debug: 4.4.1 typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.34.0': + '@typescript-eslint/scope-manager@8.34.1': dependencies: - '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/visitor-keys': 8.34.0 + '@typescript-eslint/types': 8.34.1 + '@typescript-eslint/visitor-keys': 8.34.1 - '@typescript-eslint/tsconfig-utils@8.34.0(typescript@5.8.3)': + '@typescript-eslint/tsconfig-utils@8.34.1(typescript@5.8.3)': dependencies: typescript: 5.8.3 - '@typescript-eslint/type-utils@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.34.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) debug: 4.4.1 eslint: 9.29.0(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) @@ -5163,14 +5163,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.34.0': {} + '@typescript-eslint/types@8.34.1': {} - '@typescript-eslint/typescript-estree@8.34.0(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.34.1(typescript@5.8.3)': dependencies: - '@typescript-eslint/project-service': 8.34.0(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.34.0(typescript@5.8.3) - '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/visitor-keys': 8.34.0 + '@typescript-eslint/project-service': 8.34.1(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.34.1(typescript@5.8.3) + '@typescript-eslint/types': 8.34.1 + '@typescript-eslint/visitor-keys': 8.34.1 debug: 4.4.1 fast-glob: 3.3.3 is-glob: 4.0.3 @@ -5181,20 +5181,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/utils@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0(jiti@2.4.2)) - '@typescript-eslint/scope-manager': 8.34.0 - '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.34.1 + '@typescript-eslint/types': 8.34.1 + '@typescript-eslint/typescript-estree': 8.34.1(typescript@5.8.3) eslint: 9.29.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.34.0': + '@typescript-eslint/visitor-keys@8.34.1': dependencies: - '@typescript-eslint/types': 8.34.0 + '@typescript-eslint/types': 8.34.1 eslint-visitor-keys: 4.2.1 '@unrs/resolver-binding-android-arm-eabi@1.9.0': @@ -5491,7 +5491,7 @@ snapshots: browserslist@4.25.0: dependencies: caniuse-lite: 1.0.30001723 - electron-to-chromium: 1.5.167 + electron-to-chromium: 1.5.170 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.0) @@ -5652,7 +5652,7 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - electron-to-chromium@1.5.167: {} + electron-to-chromium@1.5.170: {} emoji-regex@9.2.2: {} @@ -5772,12 +5772,12 @@ snapshots: dependencies: '@next/eslint-plugin-next': 15.3.3 '@rushstack/eslint-patch': 1.11.0 - '@typescript-eslint/eslint-plugin': 8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/parser': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/eslint-plugin': 8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.29.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.29.0(jiti@2.4.2)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.29.0(jiti@2.4.2)) eslint-plugin-react: 7.37.5(eslint@9.29.0(jiti@2.4.2)) eslint-plugin-react-hooks: 5.2.0(eslint@9.29.0(jiti@2.4.2)) @@ -5811,22 +5811,22 @@ snapshots: tinyglobby: 0.2.14 unrs-resolver: 1.9.0 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.29.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.29.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.4.2)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.4.2)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -5837,7 +5837,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.29.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -5849,7 +5849,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -5874,7 +5874,7 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-prettier@5.4.1(@types/eslint@9.6.1)(eslint-config-prettier@10.1.5(eslint@9.29.0(jiti@2.4.2)))(eslint@9.29.0(jiti@2.4.2))(prettier@3.5.3): + eslint-plugin-prettier@5.5.0(@types/eslint@9.6.1)(eslint-config-prettier@10.1.5(eslint@9.29.0(jiti@2.4.2)))(eslint@9.29.0(jiti@2.4.2))(prettier@3.5.3): dependencies: eslint: 9.29.0(jiti@2.4.2) prettier: 3.5.3 @@ -6326,7 +6326,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 20.19.0 + '@types/node': 20.19.1 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -6657,7 +6657,7 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.5: + postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -6708,7 +6708,7 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 - react-hook-form@7.58.0(react@19.1.0): + react-hook-form@7.58.1(react@19.1.0): dependencies: react: 19.1.0 @@ -7087,10 +7087,10 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.42.0 + terser: 5.43.0 webpack: 5.99.9 - terser@5.42.0: + terser@5.43.0: dependencies: '@jridgewell/source-map': 0.3.6 acorn: 8.15.0 @@ -7162,11 +7162,11 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3): + typescript-eslint@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/parser': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.34.0(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/eslint-plugin': 8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.29.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: @@ -7358,4 +7358,4 @@ snapshots: yocto-queue@0.1.0: {} - zod@3.25.64: {} + zod@3.25.67: {} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f56e11d..63a99a8 100755 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -390,7 +390,8 @@ const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => { return ( { const SignInLayout = ({ children, }: Readonly<{ children: React.ReactNode }>) => { - return
{children}
; + return ( +
+ {children} +
+ ); }; export default SignInLayout; diff --git a/src/app/status/list/page.tsx b/src/app/status/list/page.tsx index a3878e2..eaf66fd 100644 --- a/src/app/status/list/page.tsx +++ b/src/app/status/list/page.tsx @@ -1,6 +1,6 @@ 'use server'; -import { StatusList } from '@/components/status/List'; +import { StatusList } from '@/components/status'; import { getUser, getRecentUsersWithStatuses } from '@/lib/actions'; import { redirect } from 'next/navigation'; diff --git a/src/components/default/header/AvatarDropdown.tsx b/src/components/default/header/AvatarDropdown.tsx index 7378279..b72240d 100644 --- a/src/components/default/header/AvatarDropdown.tsx +++ b/src/components/default/header/AvatarDropdown.tsx @@ -33,7 +33,7 @@ const AvatarDropdown = () => { diff --git a/src/components/default/header/index.tsx b/src/components/default/header/index.tsx index 2b8cecb..9252ca9 100644 --- a/src/components/default/header/index.tsx +++ b/src/components/default/header/index.tsx @@ -1,5 +1,4 @@ 'use client'; - import Image from 'next/image'; import Link from 'next/link'; import { ThemeToggle, useTVMode } from '@/components/context'; @@ -9,45 +8,64 @@ import AvatarDropdown from './AvatarDropdown'; const Header = () => { const { tvMode } = useTVMode(); const { isAuthenticated } = useAuth(); - return tvMode ? ( -
-
-
- - {isAuthenticated && } -
-
+ + // Controls component for both modes + const Controls = () => ( +
+ + {isAuthenticated && }
- ) : ( -
-
-
- - {isAuthenticated && } + ); + + if (tvMode) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+ {/* Left spacer for perfect centering */} +
+
+
+ + {/* Centered logo and title */} +
+ + Tech Tracker Logo +

+ Tech Tracker +

+ +
+ + {/* Right-aligned controls */} +
+ +
- - Tech Tracker Logo -

- Tech Tracker -

-
); }; + export default Header; diff --git a/src/components/status/ConnectionStatus.tsx b/src/components/status/ConnectionStatus.tsx new file mode 100644 index 0000000..1b94636 --- /dev/null +++ b/src/components/status/ConnectionStatus.tsx @@ -0,0 +1,68 @@ +'use client'; +import { Wifi, WifiOff, RefreshCw } from 'lucide-react'; +import { Badge, Button } from '@/components/ui'; +import type { ConnectionStatus as ConnectionStatusType } from '@/lib/hooks'; + +type ConnectionStatusProps = { + status: ConnectionStatusType; + onReconnect?: () => void; + showAsButton?: boolean; + className?: string; +} + +const getConnectionIcon = (status: ConnectionStatusType) => { + switch (status) { + case 'connected': + return ; + case 'connecting': + return ; + case 'disconnected': + return ; + case 'updating': + return ; + } +}; + +const getConnectionText = (status: ConnectionStatusType) => { + switch (status) { + case 'connected': + return 'Connected'; + case 'connecting': + return 'Connecting...'; + case 'disconnected': + return 'Disconnected'; + case 'updating': + return 'Updating...'; + } +}; + +export const ConnectionStatus = ({ + status, + onReconnect, + showAsButton = false, + className = '', +}: ConnectionStatusProps) => { + if (showAsButton && status === 'disconnected' && onReconnect) { + return ( + + ); + } + + return ( + + {getConnectionIcon(status)} + {getConnectionText(status)} + + ); +}; diff --git a/src/components/status/List.tsx b/src/components/status/List.tsx old mode 100755 new mode 100644 index 54db958..326fbbd --- a/src/components/status/List.tsx +++ b/src/components/status/List.tsx @@ -1,35 +1,28 @@ 'use client'; -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect } from 'react'; import { useAuth, useTVMode } from '@/components/context'; -import { - getRecentUsersWithStatuses, - updateStatuses, - updateUserStatus, - type UserWithStatus, -} from '@/lib/hooks'; +import type { UserWithStatus } from '@/lib/hooks'; import { BasedAvatar, Drawer, DrawerTrigger, Loading } from '@/components/ui'; import { StatusMessage, SubmitButton } from '@/components/default'; -import { toast } from 'sonner'; -import { HistoryDrawer } from '@/components/status'; +import { ConnectionStatus, HistoryDrawer } from '@/components/status'; import type { Profile } from '@/utils/supabase'; import { makeConditionalClassName } from '@/lib/utils'; import { Card, CardContent, CardHeader } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; -import { RefreshCw, Clock, Wifi, WifiOff, Calendar } from 'lucide-react'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { QueryErrorCodes } from '@/components/context'; -import { createClient } from '@/utils/supabase'; -import type { RealtimeChannel } from '@supabase/supabase-js'; +import { RefreshCw, Clock, Calendar } from 'lucide-react'; +import { useStatusData, useSharedStatusSubscription } from '@/lib/hooks'; +import { formatTime, formatDate } from '@/lib/utils'; +import Link from 'next/link'; -type ListProps = { initialStatuses: UserWithStatus[] }; +type ListProps = { + initialStatuses: UserWithStatus[] +}; export const StatusList = ({ initialStatuses = [] }: ListProps) => { const { isAuthenticated } = useAuth(); const { tvMode } = useTVMode(); - const queryClient = useQueryClient(); const [selectedUsers, setSelectedUsers] = useState([]); const [selectAll, setSelectAll] = useState(false); @@ -38,216 +31,64 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => { useState(null); const [updateStatusMessage, setUpdateStatusMessage] = useState(''); - const [connectionStatus, setConnectionStatus] = useState< - 'connecting' | 'connected' | 'disconnected' | 'updating' - >('connecting'); - const [newStatuses, setNewStatuses] = useState>(new Set()); - const channelRef = useRef(null); - const supabaseRef = useRef(createClient()); - const { data: usersWithStatuses = initialStatuses, isLoading: loading, error, refetch, - isFetching, - dataUpdatedAt, - } = useQuery({ - queryKey: ['users-with-statuses'], - queryFn: async () => { - try { - const response = await getRecentUsersWithStatuses(); - if (!response.success) throw new Error(response.error); - return response.data; - } catch (error) { - toast.error(`Error fetching technicians: ${error as Error}`) - throw error; - } - }, - enabled: isAuthenticated, - refetchInterval: 30000, // 30 Second interval as we should rely on subscription. - refetchOnWindowFocus: true, - refetchOnMount: true, + newStatuses, + updateStatusMutation, + } = useStatusData({ initialData: initialStatuses, - meta: { errCode: QueryErrorCodes.USERS_FETCH_FAILED }, + enabled: isAuthenticated, }); - useEffect(() => { - if (!isAuthenticated) return; - - const maxReconnectAttempts = 3; - let reconnectAttempts = 0; - let reconnectTimeout: NodeJS.Timeout; - let isComponentMounted = true; - let currentChannel: RealtimeChannel | null = null; - - const setUpRealtimeConnection = () => { - if (!isComponentMounted) return; - - if (currentChannel) { - supabaseRef.current.removeChannel(currentChannel) - .catch((error) => { - setConnectionStatus('disconnected'); - console.error(`Error unsubscribing: ${error}`); - }) - currentChannel = null; - } - - setConnectionStatus('connecting'); - const channel = supabaseRef.current - .channel('status_updates') - .on('broadcast', { event: 'status_updated' }, () => { - refetch().catch((error) => { - console.error(`Error refetching statuses: ${error as Error}`) - }); - }) - .subscribe((status) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison - if (status === 'SUBSCRIBED') { - setConnectionStatus('connected'); - reconnectAttempts = 0; - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison - } else if (status === 'CHANNEL_ERROR') { - setConnectionStatus('disconnected'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison - } else if (status === 'CLOSED') { - setConnectionStatus('disconnected') - if (isComponentMounted && reconnectAttempts < maxReconnectAttempts) { - reconnectAttempts++; - const delay = 2000 * reconnectAttempts; - console.log( - `Reconnecting after close. - ${reconnectAttempts} attempts of - ${maxReconnectAttempts} in ${delay}ms - `); - if (reconnectTimeout) clearTimeout(reconnectTimeout); - reconnectTimeout = setTimeout(() => { - if (isComponentMounted) setUpRealtimeConnection(); - }, delay); - } else { - console.warn('Max reconnection attempts reached or component is not mounted.'); - setConnectionStatus('disconnected'); - } - } - }); - currentChannel = channel; - channelRef.current = channel; - }; - - const initialTimeout = setTimeout(() => { - if (isComponentMounted) setUpRealtimeConnection(); - }, 1000); - - return () => { - isComponentMounted = false; - if (initialTimeout) clearTimeout(initialTimeout); - if (reconnectTimeout) clearTimeout(reconnectTimeout); - if (currentChannel) { - supabaseRef.current.removeChannel(currentChannel) - .catch((error) => { - console.error(`Error unsubscribing: ${error as Error}`) - }); - channelRef.current = null; - } - setConnectionStatus('disconnected'); - } - }, [isAuthenticated, refetch]); - - const updateStatusMutation = useMutation({ - - mutationFn: async ({ - usersWithStatuses, - status, - }: { - usersWithStatuses: UserWithStatus[]; - status: string; - }) => { - const previousConnectionStatus = connectionStatus; - try { - if (usersWithStatuses.length <= 0) { - const result = await updateUserStatus(status); - if (!result.success) throw new Error(result.error); - return result.data; - } else { - const result = await updateStatuses(usersWithStatuses, status); - if (!result.success) throw new Error(result.error); - return result.data; - } - } catch (error) { - console.error(`Error updating statuses: ${error as Error}`); - } - }, - meta: { errCode: QueryErrorCodes.STATUS_UPDATE_FAILED }, - - onMutate: async ({ usersWithStatuses, status }) => { - // Optimistic update logic - await queryClient.cancelQueries({ queryKey: ['users-with-statuses']} ); - const previousData = queryClient.getQueryData([ - 'users-with-statuses', - ]); - if (previousData && usersWithStatuses.length > 0) { - const now = new Date().toISOString(); - const optimisticData = previousData.map((userStatus) => { - if (usersWithStatuses.some((selected) => selected.user.id === userStatus.user.id)) - return { ...userStatus, status, created_at: now}; - return userStatus; - }); - queryClient.setQueryData(['users-with-statuses'], optimisticData); - - // Add animation to optimisticly updated statuses - const updatedStatuses = usersWithStatuses; - setNewStatuses((prev) => new Set([...prev, ...updatedStatuses])) - setTimeout(() => { - setNewStatuses((prev) => { - const updated = new Set(prev); - updatedStatuses.forEach((updatedStatus) => updated.delete(updatedStatus)); - return updated; - }); - }, 1000) - } - return { previousData }; - }, - - onSuccess: (data) => { - queryClient.invalidateQueries({ queryKey: ['users-with-statuses']} ) - .catch((error) => console.error(`Error invalidating query: ${error}`)); - if (!data) return; - data.forEach((statusUpdate) => { - toast.success(` - ${statusUpdate.user.full_name}'s status updated to '${statusUpdate.status}'. - `); - }); - setSelectedUsers([]); - setStatusInput(''); - }, - - onError: (error, _variables, context) => { - if (context?.previousData) - queryClient.setQueryData(['users-with-statuses'], context.previousData); - toast.error(`Error updating statuses: ${error}`); - }, + // In your StatusList component + const { connectionStatus, connect: reconnect } = useSharedStatusSubscription(() => { + refetch().catch((error) => { + console.error('Error refetching statuses:', error); + }); }); + //const { connectionStatus, connect: reconnect } = useStatusSubscription({ + //enabled: isAuthenticated, + //onStatusUpdate: () => { + //refetch().catch((error) => { + //console.error('Error refetching statuses:', error); + //}); + //}, + //}); + const handleUpdateStatus = () => { if (!isAuthenticated) { - setUpdateStatusMessage('You must be signed in to update technican statuses!') - return; - } else if (!statusInput.trim()) { - setUpdateStatusMessage('Your status must be in between 3 & 80 characters long!'); + setUpdateStatusMessage( + 'Error: You must be signed in to update technician statuses!' + ); return; } + + if (statusInput.length < 3 || statusInput.length > 80) { + setUpdateStatusMessage( + 'Error: Your status must be between 3 & 80 characters long!' + ); + return; + } + updateStatusMutation.mutate({ usersWithStatuses: selectedUsers, status: statusInput.trim(), }); - }; + setSelectedUsers([]); + setStatusInput(''); + setUpdateStatusMessage(''); + }; const handleCheckboxChange = (user: UserWithStatus) => { setSelectedUsers((prev) => prev.some((u) => u.user.id === user.user.id) ? prev.filter((prevUser) => prevUser.user.id !== user.user.id) - : [...prev, user], + : [...prev, user] ); }; @@ -263,52 +104,10 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => { useEffect(() => { setSelectAll( selectedUsers.length === usersWithStatuses.length && - usersWithStatuses.length > 0, + usersWithStatuses.length > 0 ); }, [selectedUsers.length, usersWithStatuses.length]); - const getConnectionIcon = () => { - switch (connectionStatus) { - case 'connected': - return ; - case 'connecting': - return ; - case 'disconnected': - return ; - case 'updating': - return ; - } - }; - - const getConnectionText = () => { - switch (connectionStatus) { - case 'connected': - return 'Connected'; - case 'connecting': - return 'Connecting...'; - case 'disconnected': - return 'Disconnected'; - case 'updating': - return 'Updating...'; - } - }; - - const formatTime = (timestamp: string) => { - const date = new Date(timestamp); - const time = date.toLocaleTimeString('en-US', { - hour: 'numeric', - minute: 'numeric', - }); - return `${time}`; - }; - - const formatDate = (timestamp: string) => { - const date = new Date(timestamp); - const day = date.getDate(); - const month = date.toLocaleString('default', { month: 'long' }); - return `${month} ${day}`; - }; - if (loading) { return (
@@ -360,38 +159,56 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => { return (
-
+
+
+ + +
+ {!tvMode && ( +
+

Miss the old table?

+ + Find it here! + +
+ )} +
- - -
-
- - {getConnectionIcon()} - {getConnectionText()} -
{usersWithStatuses.map((userWithStatus) => { - const isSelected = selectedUsers.some((u) => u.user.id === userWithStatus.user.id); + const isSelected = selectedUsers.some( + (u) => u.user.id === userWithStatus.user.id + ); const isNewStatus = newStatuses.has(userWithStatus); - const isUpdatedByOther = userWithStatus.updated_by && + const isUpdatedByOther = + userWithStatus.updated_by && userWithStatus.updated_by.id !== userWithStatus.user.id; + return ( @@ -401,7 +218,9 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => { {!tvMode && ( handleCheckboxChange(userWithStatus)} + onCheckedChange={() => + handleCheckboxChange(userWithStatus) + } onClick={(e) => e.stopPropagation()} /> )} @@ -411,7 +230,9 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => { className={tvMode ? 'w-24 h-24' : 'w-16 h-16'} />
-

+

{userWithStatus.user.full_name}

{isUpdatedByOther && ( @@ -421,7 +242,9 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => { fullName={userWithStatus.updated_by?.full_name} className='w-5 h-5' /> - + Updated by {userWithStatus.updated_by?.full_name}
@@ -436,7 +259,9 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
- + {formatDate(userWithStatus.created_at)} @@ -444,7 +269,6 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
- @@ -454,14 +278,16 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => { transition-colors cursor-pointer text-left ${tvMode ? 'text-4xl' : 'text-xl'} `} - onClick={() => setSelectedHistoryUser(userWithStatus.user)} + onClick={() => + setSelectedHistoryUser(userWithStatus.user) + } >

{userWithStatus.status}

- {selectedHistoryUser === userWithStatus.user && ( - - )} + {selectedHistoryUser === userWithStatus.user && ( + + )} @@ -471,13 +297,77 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => { {usersWithStatuses.length === 0 && ( -

+

No status updates have been made in the past day.

)} + {!tvMode && ( + +
+

Update Status

+
+
+ setStatusInput(e.target.value)} + onKeyDown={(e) => { + if ( + e.key === 'Enter' && + !e.shiftKey && + !updateStatusMutation.isPending + ) { + e.preventDefault(); + handleUpdateStatus(); + } + }} + /> + + {selectedUsers.length > 0 + ? `Update status for ${selectedUsers.length} + ${selectedUsers.length > 1 ? 'users' : 'user'}` + : 'Update status' + } + +
+ {updateStatusMessage && + (updateStatusMessage.includes('Error') || + updateStatusMessage.includes('error') || + updateStatusMessage.includes('failed') || + updateStatusMessage.includes('invalid') ? ( + + ) : ( + + ))} +
+
+ + + + + + +
+
+
+ )}
); - }; diff --git a/src/components/status/StatusList.tsx b/src/components/status/StatusList.tsx deleted file mode 100644 index 1bb4cb4..0000000 --- a/src/components/status/StatusList.tsx +++ /dev/null @@ -1,564 +0,0 @@ -'use client'; -import { useState, useEffect, useRef } from 'react'; -import { useAuth, useTVMode } from '@/components/context'; -import { - getRecentUsersWithStatuses, - updateStatuses, - updateUserStatus, - type UserWithStatus, -} from '@/lib/hooks'; -import { BasedAvatar, Drawer, DrawerTrigger, Loading } from '@/components/ui'; -import { SubmitButton } from '@/components/default'; -import { toast } from 'sonner'; -import { HistoryDrawer } from '@/components/status'; -import type { Profile } from '@/utils/supabase'; -import { makeConditionalClassName } from '@/lib/utils'; -import { Card, CardContent, CardHeader } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; -import { Checkbox } from '@/components/ui/checkbox'; -import { Input } from '@/components/ui/input'; -import { Button } from '@/components/ui/button'; -import { RefreshCw, Clock, Wifi, WifiOff } from 'lucide-react'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { QueryErrorCodes } from '@/components/context'; -import { createClient } from '@/utils/supabase'; -import type { RealtimeChannel } from '@supabase/supabase-js'; - -type StatusListProps = { - initialStatuses: UserWithStatus[]; -}; - -export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { - // Fixed props destructuring - const { isAuthenticated } = useAuth(); - const { tvMode } = useTVMode(); - const queryClient = useQueryClient(); - - const [selectedUsers, setSelectedUsers] = useState([]); - const [selectAll, setSelectAll] = useState(false); - const [statusInput, setStatusInput] = useState(''); - const [selectedHistoryUser, setSelectedHistoryUser] = - useState(null); - - const [connectionStatus, setConnectionStatus] = useState< - 'connecting' | 'connected' | 'disconnected' - >('connecting'); - const [newStatusIds, setNewStatusIds] = useState>(new Set()); - const channelRef = useRef(null); - const supabaseRef = useRef(createClient()); - - // Keep all your existing React Query code exactly as is - const { - data: usersWithStatuses = initialStatuses, - isLoading: loading, - error, - refetch, - isFetching, - dataUpdatedAt, - } = useQuery({ - queryKey: ['users-with-statuses'], - queryFn: async () => { - try { - const response = await getRecentUsersWithStatuses(); - if (!response.success) throw new Error(response.error); - return response.data; - } catch (error) { - toast.error( - `Error fetching technicians: ${error instanceof Error ? error.message : 'Unknown error'}`, - ); - throw error; - } - }, - enabled: isAuthenticated, - refetchInterval: 30000, // Changed to 30 seconds as backup - refetchOnWindowFocus: true, - refetchOnMount: true, - initialData: initialStatuses, - meta: { errCode: QueryErrorCodes.USERS_FETCH_FAILED }, - }); - - // Add this new useEffect for realtime enhancement - useEffect(() => { - if (!isAuthenticated) return; - - let reconnectAttempts = 0; - const maxReconnectAttempts = 3; - let reconnectTimeout: NodeJS.Timeout; - let isComponentMounted = true; - let currentChannel: RealtimeChannel | null = null; - - const setUpRealtimeConnection = () => { - if (!isComponentMounted) return; - if (currentChannel) { - supabaseRef.current.removeChannel(currentChannel).catch((error) => { - console.error(`Error unsubscribing: ${error}`); - }); - currentChannel = null; - } - setConnectionStatus('connecting'); - const channel = supabaseRef.current - .channel('status_updates') - .on('broadcast', { event: 'status_updated' }, (payload) => { - console.log('Realtime update received, triggering refetch...'); - refetch().catch((error) => { - console.error(`Error refetching: ${error}`); - }); - }) - .subscribe((status) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison - if (status === 'SUBSCRIBED') { - console.log('Realtime connection established'); - setConnectionStatus('connected'); - reconnectAttempts = 0; - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison - } else if (status === 'CHANNEL_ERROR') { - console.log('Realtime connection failed, relying on polling'); - setConnectionStatus('disconnected'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison - } else if (status === 'CLOSED') { - console.log('Realtime connection closed'); - setConnectionStatus('disconnected'); - if ( - isComponentMounted && - reconnectAttempts < maxReconnectAttempts - ) { - reconnectAttempts++; - const delay = 2000 * reconnectAttempts; - - console.log( - `Reconnecting after close ${reconnectAttempts}/${maxReconnectAttempts} in ${delay}ms`, - ); - - if (reconnectTimeout) { - clearTimeout(reconnectTimeout); - } - - reconnectTimeout = setTimeout(() => { - if (isComponentMounted) { - setUpRealtimeConnection(); - } - }, delay); - } else { - console.log( - 'Max reconnection attempts reached or component unmounted', - ); - } - } - }); - currentChannel = channel; - channelRef.current = channel; - }; - - const initialTimeout = setTimeout(() => { - if (isComponentMounted) { - setUpRealtimeConnection(); - } - }, 1000); - - return () => { - isComponentMounted = false; - if (initialTimeout) clearTimeout(initialTimeout); - if (reconnectTimeout) clearTimeout(reconnectTimeout); - - if (currentChannel) { - console.log('Cleaning up realtime connection...'); - supabaseRef.current.removeChannel(currentChannel).catch((error) => { - console.error(`Error unsubscribing: ${error}`); - }); - channelRef.current = null; - } - }; - }, [isAuthenticated, refetch]); - - // Updated mutation - const updateStatusMutation = useMutation({ - mutationFn: async ({ - usersWithStatuses, - status, - }: { - usersWithStatuses: UserWithStatus[]; - status: string; - }) => { - if (usersWithStatuses.length === 0) { - const result = await updateUserStatus(status); - if (!result.success) throw new Error(result.error); - return { type: 'single', result }; - } else { - const result = await updateStatuses(usersWithStatuses, status); - if (!result.success) throw new Error(result.error); - return { type: 'multiple', result, count: usersWithStatuses.length }; - } - }, - meta: { errCode: QueryErrorCodes.STATUS_UPDATE_FAILED }, - onMutate: async ({ usersWithStatuses, status }) => { - // Optimistic update logic - await queryClient.cancelQueries({ queryKey: ['users-with-statuses'] }); - const previousData = queryClient.getQueryData([ - 'users-with-statuses', - ]); - - if (previousData && usersWithStatuses.length > 0) { - const now = new Date().toISOString(); - const optimisticData = previousData.map((userStatus) => { - if ( - usersWithStatuses.some( - (selected) => selected.user.id === userStatus.user.id, - ) - ) { - return { - ...userStatus, - status, - created_at: now, - }; - } - return userStatus; - }); - queryClient.setQueryData(['users-with-statuses'], optimisticData); - - // Add animation for optimistic updates - const updatedIds = usersWithStatuses.map((u) => u.user.id); - setNewStatusIds((prev) => new Set([...prev, ...updatedIds])); - - // Remove animation after 1 second - setTimeout(() => { - setNewStatusIds((prev) => { - const updated = new Set(prev); - updatedIds.forEach((id) => updated.delete(id)); - return updated; - }); - }, 1000); - } - - return { previousData }; - }, - onSuccess: (data) => { - // Handle success in the mutation function - void queryClient.invalidateQueries({ queryKey: ['users-with-statuses'] }); // Fixed floating promise - - if (data.type === 'single') { - toast.success('Status updated for signed in user.'); - } else { - toast.success(`Status updated for ${data.count} selected users.`); - } - - setSelectedUsers([]); - setStatusInput(''); - }, - onError: (error, _variables, context) => { - // Fixed unused variables - // Rollback optimistic update - if (context?.previousData) { - queryClient.setQueryData(['users-with-statuses'], context.previousData); - } - // Error handling is done in the global mutation cache - console.error('Status update failed:', error); - }, - }); - - const handleUpdateStatus = () => { - if (!isAuthenticated) { - toast.error('You must be signed in to update statuses.'); - return; - } - if (!statusInput.trim()) { - toast.error('Please enter a valid status.'); - return; - } - - updateStatusMutation.mutate({ - usersWithStatuses: selectedUsers, - status: statusInput.trim(), - }); - }; - - const handleCheckboxChange = (user: UserWithStatus) => { - setSelectedUsers((prev) => - prev.some((u) => u.user.id === user.user.id) - ? prev.filter((prevUser) => prevUser.user.id !== user.user.id) - : [...prev, user], - ); - }; - - const handleSelectAllChange = () => { - if (selectAll) { - setSelectedUsers([]); - } else { - setSelectedUsers(usersWithStatuses); - } - setSelectAll(!selectAll); - }; - - useEffect(() => { - setSelectAll( - selectedUsers.length === usersWithStatuses.length && - usersWithStatuses.length > 0, - ); - }, [selectedUsers.length, usersWithStatuses.length]); - - const getConnectionIcon = () => { - switch (connectionStatus) { - case 'connected': - return ; - case 'connecting': - return ; - case 'disconnected': - return ; - } - }; - - const getConnectionText = () => { - switch (connectionStatus) { - case 'connected': - return 'Connected'; - case 'connecting': - return 'Connecting...'; - case 'disconnected': - return 'Disconnected'; - } - }; - - const formatTime = (timestamp: string) => { - const date = new Date(timestamp); - const time = date.toLocaleTimeString('en-US', { - hour: 'numeric', - minute: 'numeric', - }); - const day = date.getDate(); - const month = date.toLocaleString('default', { month: 'long' }); - return `${time} - ${month} ${day}`; - }; - - if (loading) { - return ( -
- -
- ); - } - - if (error) { - return ( -
-

Error loading status updates

- -
- ); - } - - const containerClassName = makeConditionalClassName({ - context: tvMode, - defaultClassName: 'mx-auto space-y-4', - on: 'lg:w-11/12 w-full mt-15', - off: 'w-5/6', - }); - - const cardClassName = makeConditionalClassName({ - context: tvMode, - defaultClassName: 'transition-all duration-300 hover:shadow-md', - on: 'lg:text-4xl', - off: 'lg:text-base', - }); - - return ( -
- {/* Status Header */} -
-
-

- Tech Status -

- {isFetching ? ( - - - Updating... - - ) : ( - - {getConnectionIcon()} - {getConnectionText()} - - )} -
- {!tvMode && usersWithStatuses.length > 0 && ( -
- - -
- )} -
- - {/* Status Cards */} -
- {usersWithStatuses.map((userWithStatus) => { - const isSelected = selectedUsers.some( - (u) => u.user.id === userWithStatus.user.id, - ); - const isNewStatus = newStatusIds.has(userWithStatus.user.id); - const isUpdatedByOther = - userWithStatus.updated_by && - userWithStatus.updated_by.id !== userWithStatus.user.id; - - return ( - - -
-
- {!tvMode && ( - - handleCheckboxChange(userWithStatus) - } - onClick={(e) => e.stopPropagation()} - /> - )} - -
-

- {userWithStatus.user.full_name ?? 'Unknown User'} -

- {isUpdatedByOther && ( -
- - {userWithStatus.updated_by && ( - - Updated by {userWithStatus.updated_by.full_name} - - )} -
- )} -
-
-
- - - {formatTime(userWithStatus.created_at)} - -
-
-
- - - -
- setSelectedHistoryUser(userWithStatus.user) - } - > -

{userWithStatus.status}

-
-
- {selectedHistoryUser === userWithStatus.user && ( - - )} -
-
-
- ); - })} -
- - {usersWithStatuses.length === 0 && ( - -

- No status updates yet -

-
- )} - - {/* Status Update Input */} - {!tvMode && ( - -
-

Update Status

-
- setStatusInput(e.target.value)} - onKeyDown={(e) => { - if ( - e.key === 'Enter' && - !e.shiftKey && - !updateStatusMutation.isPending - ) { - e.preventDefault(); - handleUpdateStatus(); - } - }} - disabled={updateStatusMutation.isPending} - /> - - {updateStatusMutation.isPending - ? 'Updating...' - : selectedUsers.length > 0 - ? `Update ${selectedUsers.length} Users` - : 'Update Status'} - -
- {selectedUsers.length > 0 && ( -

- Updating status for {selectedUsers.length} selected users -

- )} -
-
- )} - - {/* Global Status History Drawer */} -
- - - - - - -
-
- ); -}; diff --git a/src/components/status/Table.tsx b/src/components/status/Table.tsx new file mode 100644 index 0000000..2b72662 --- /dev/null +++ b/src/components/status/Table.tsx @@ -0,0 +1,377 @@ +'use client'; +import { useState, useEffect } from 'react'; +import { useAuth, useTVMode } from '@/components/context'; +import type { UserWithStatus } from '@/lib/hooks'; +import { BasedAvatar, Drawer, DrawerTrigger, Loading } from '@/components/ui'; +import { StatusMessage, SubmitButton } from '@/components/default'; +import { ConnectionStatus, HistoryDrawer } from '@/components/status'; +import type { Profile } from '@/utils/supabase'; +import { makeConditionalClassName } from '@/lib/utils'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { RefreshCw, Clock, Calendar } from 'lucide-react'; +import { useSharedStatusSubscription, useStatusData } from '@/lib/hooks'; +import { formatTime, formatDate } from '@/lib/utils'; +import Link from 'next/link'; + + +type TableProps = { + initialStatuses: UserWithStatus[]; +}; + +export const TechTable = ({ initialStatuses = [] }: TableProps) => { + const { isAuthenticated } = useAuth(); + const { tvMode } = useTVMode(); + + const [selectedUsers, setSelectedUsers] = useState([]); + const [selectAll, setSelectAll] = useState(false); + const [statusInput, setStatusInput] = useState(''); + const [selectedHistoryUser, setSelectedHistoryUser] = + useState(null); + const [updateStatusMessage, setUpdateStatusMessage] = useState(''); + + const { + data: usersWithStatuses = initialStatuses, + isLoading: loading, + error, + refetch, + newStatuses, + updateStatusMutation, + } = useStatusData({ + initialData: initialStatuses, + enabled: isAuthenticated, + }); + // In your StatusList component + const { connectionStatus, connect: reconnect } = useSharedStatusSubscription(() => { + refetch().catch((error) => { + console.error('Error refetching statuses:', error); + }); + }); + //const { connectionStatus, connect: reconnect } = useStatusSubscription({ + //enabled: isAuthenticated, + //onStatusUpdate: () => { + //refetch().catch((error) => { + //console.error('Error refetching statuses:', error); + //}); + //}, + //}); + + const handleUpdateStatus = () => { + if (!isAuthenticated) { + setUpdateStatusMessage( + 'Error: You must be signed in to update technician statuses!' + ); + return; + } + + if (statusInput.length < 3 || statusInput.length > 80) { + setUpdateStatusMessage( + 'Error: Your status must be between 3 & 80 characters long!' + ); + return; + } + + updateStatusMutation.mutate({ + usersWithStatuses: selectedUsers, + status: statusInput.trim(), + }); + + setSelectedUsers([]); + setStatusInput(''); + setUpdateStatusMessage(''); + }; + + const handleCheckboxChange = (user: UserWithStatus) => { + setSelectedUsers((prev) => + prev.some((u) => u.user.id === user.user.id) + ? prev.filter((prevUser) => prevUser.user.id !== user.user.id) + : [...prev, user] + ); + }; + + const handleSelectAllChange = () => { + if (selectAll) { + setSelectedUsers([]); + } else { + setSelectedUsers(usersWithStatuses); + } + setSelectAll(!selectAll); + }; + + useEffect(() => { + setSelectAll( + selectedUsers.length === usersWithStatuses.length && + usersWithStatuses.length > 0 + ); + }, [selectedUsers.length, usersWithStatuses.length]); + + if (loading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+

Error loading status updates

+ +
+ ); + } + + const containerClassName = makeConditionalClassName({ + context: tvMode, + defaultClassName: 'mx-auto', + on: 'lg:w-11/12 w-full', + off: 'w-5/6', + }); + const headerClassName = makeConditionalClassName({ + context: tvMode, + defaultClassName: 'w-full mb-2 flex justify-between', + on: 'mt-25', + off: 'mb-2', + }); + const thClassName = makeConditionalClassName({ + context: tvMode, + defaultClassName: 'py-4 px-4 border font-semibold xl:min-w-[420px]', + on: 'lg:text-6xl', + off: 'lg:text-5xl', + }); + const tdClassName = makeConditionalClassName({ + context: tvMode, + defaultClassName: 'py-2 px-2 border', + on: 'lg:text-5xl', + off: 'lg:text-4xl', + }); + const tCheckboxClassName = `py-3 px-4 border`; + const checkBoxClassName = `lg:scale-200 cursor-pointer`; + + return ( +
+
+
+ + {!tvMode && ( +
+

Tired of the old table? {' '}

+ + Try out the new status list! + +
+ )} +
+
+ + + + + {!tvMode && ( + + )} + + + + + + + {usersWithStatuses.map((userWithStatus, index) => { + const isSelected = selectedUsers.some( + (u) => u.user.id === userWithStatus.user.id, + ); + const isNewStatus = newStatuses.has(userWithStatus); + + return ( + + {!tvMode && ( + + )} + + + + + ); + })} + +
+ + Technician + + + Status + + + + Updated At
+ handleCheckboxChange(userWithStatus)} + /> + +
+ +
+

+ {userWithStatus.user.full_name ?? 'Unknown User'} +

+ {userWithStatus.updated_by && + userWithStatus.updated_by.id !== + userWithStatus.user.id && ( +
+ + + Updated by {userWithStatus.updated_by.full_name} + +
+ )} +
+
+
+ + + setSelectedHistoryUser(userWithStatus.user) + } + > + {userWithStatus.status} + + {selectedHistoryUser === userWithStatus.user && ( + + )} + + +
+
+
+
+ + {formatTime(userWithStatus.created_at)} +
+
+ + {formatDate(userWithStatus.created_at)} +
+
+
+
+ + {usersWithStatuses.length === 0 && ( +
+

+ No status updates yet +

+
+ )} + + {updateStatusMessage && + (updateStatusMessage.includes('Error') || + updateStatusMessage.includes('error') || + updateStatusMessage.includes('failed') || + updateStatusMessage.includes('invalid') ? ( + + ) : ( + + ))} + + {!tvMode && ( +
+ setStatusInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && !updateStatusMutation.isPending) { + e.preventDefault(); + handleUpdateStatus(); + } + }} + disabled={updateStatusMutation.isPending} + /> + + {selectedUsers.length > 0 + ? `Update status for ${selectedUsers.length} + ${selectedUsers.length > 1 ? 'users' : 'user'}` + : 'Update status' + } + +
+ )} + + {/* Global Status History Drawer */} + {!tvMode && ( +
+ + + + + + +
+ )} +
+ ); +}; + diff --git a/src/components/status/TechTable.tsx b/src/components/status/TechTable.tsx deleted file mode 100755 index 877d244..0000000 --- a/src/components/status/TechTable.tsx +++ /dev/null @@ -1,551 +0,0 @@ -'use client'; -import { useState, useEffect, useRef } from 'react'; -import { useAuth, useTVMode } from '@/components/context'; -import { - getRecentUsersWithStatuses, - updateStatuses, - updateUserStatus, - type UserWithStatus, -} from '@/lib/hooks'; -import { BasedAvatar, Drawer, DrawerTrigger, Loading } from '@/components/ui'; -import { SubmitButton } from '@/components/default'; -import { toast } from 'sonner'; -import { HistoryDrawer } from '@/components/status'; -import type { Profile } from '@/utils/supabase'; -import { makeConditionalClassName } from '@/lib/utils'; -import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; -import { RefreshCw, Wifi, WifiOff } from 'lucide-react'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { QueryErrorCodes } from '@/components/context'; -import { createClient } from '@/utils/supabase'; -import type { RealtimeChannel } from '@supabase/supabase-js'; - -type TechTableProps = { - initialStatuses: UserWithStatus[]; -}; - -export const TechTable = ({ initialStatuses = [] }: TechTableProps) => { - const { isAuthenticated } = useAuth(); - const { tvMode } = useTVMode(); - const queryClient = useQueryClient(); - const [selectedUsers, setSelectedUsers] = useState([]); - const [selectAll, setSelectAll] = useState(false); - const [statusInput, setStatusInput] = useState(''); - const [selectedHistoryUser, setSelectedHistoryUser] = - useState(null); - const [connectionStatus, setConnectionStatus] = useState< - 'connecting' | 'connected' | 'disconnected' - >('connecting'); - const [newStatusIds, setNewStatusIds] = useState>(new Set()); - const channelRef = useRef(null); - const supabaseRef = useRef(createClient()); - - // Keep all your existing React Query code exactly as is - const { - data: usersWithStatuses = initialStatuses, - isLoading: loading, - error, - refetch, - isFetching, - dataUpdatedAt, - } = useQuery({ - queryKey: ['users-with-statuses'], - queryFn: async () => { - try { - const response = await getRecentUsersWithStatuses(); - if (!response.success) throw new Error(response.error); - return response.data; - } catch (error) { - toast.error( - `Error fetching technicians: ${error instanceof Error ? error.message : 'Unknown error'}`, - ); - throw error; - } - }, - enabled: isAuthenticated, - refetchInterval: 30000, // Changed to 30 seconds as backup - refetchOnWindowFocus: true, - refetchOnMount: true, - initialData: initialStatuses, - meta: { errCode: QueryErrorCodes.USERS_FETCH_FAILED }, - }); - - // Add this new useEffect for realtime enhancement - useEffect(() => { - if (!isAuthenticated) return; - let reconnectAttempts = 0; - const maxReconnectAttempts = 3; - let reconnectTimeout: NodeJS.Timeout; - let isComponentMounted = true; - let currentChannel: RealtimeChannel | null = null; - - const setUpRealtimeConnection = () => { - if (!isComponentMounted) return; - if (currentChannel) { - supabaseRef.current.removeChannel(currentChannel).catch((error) => { - console.error(`Error unsubscribing: ${error}`); - }); - currentChannel = null; - } - setConnectionStatus('connecting'); - const channel = supabaseRef.current - .channel('status_updates') - .on('broadcast', { event: 'status_updated' }, (payload) => { - console.log('Realtime update received, triggering refetch...'); - refetch().catch((error) => { - console.error(`Error refetching: ${error}`); - }); - }) - .subscribe((status) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison - if (status === 'SUBSCRIBED') { - console.log('Realtime connection established'); - setConnectionStatus('connected'); - reconnectAttempts = 0; - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison - } else if (status === 'CHANNEL_ERROR') { - console.log('Realtime connection failed, relying on polling'); - setConnectionStatus('disconnected'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison - } else if (status === 'CLOSED') { - console.log('Realtime connection closed'); - setConnectionStatus('disconnected'); - if ( - isComponentMounted && - reconnectAttempts < maxReconnectAttempts - ) { - reconnectAttempts++; - const delay = 2000 * reconnectAttempts; - console.log( - `Reconnecting after close ${reconnectAttempts}/${maxReconnectAttempts} in ${delay}ms`, - ); - if (reconnectTimeout) { - clearTimeout(reconnectTimeout); - } - reconnectTimeout = setTimeout(() => { - if (isComponentMounted) { - setUpRealtimeConnection(); - } - }, delay); - } else { - console.log( - 'Max reconnection attempts reached or component unmounted', - ); - } - } - }); - currentChannel = channel; - channelRef.current = channel; - }; - - const initialTimeout = setTimeout(() => { - if (isComponentMounted) { - setUpRealtimeConnection(); - } - }, 1000); - - return () => { - isComponentMounted = false; - if (initialTimeout) clearTimeout(initialTimeout); - if (reconnectTimeout) clearTimeout(reconnectTimeout); - if (currentChannel) { - console.log('Cleaning up realtime connection...'); - supabaseRef.current.removeChannel(currentChannel).catch((error) => { - console.error(`Error unsubscribing: ${error}`); - }); - channelRef.current = null; - } - }; - }, [isAuthenticated, refetch]); - - // Updated mutation - const updateStatusMutation = useMutation({ - mutationFn: async ({ - usersWithStatuses, - status, - }: { - usersWithStatuses: UserWithStatus[]; - status: string; - }) => { - if (usersWithStatuses.length === 0) { - const result = await updateUserStatus(status); - if (!result.success) throw new Error(result.error); - return { type: 'single', result }; - } else { - const result = await updateStatuses(usersWithStatuses, status); - if (!result.success) throw new Error(result.error); - return { type: 'multiple', result, count: usersWithStatuses.length }; - } - }, - meta: { errCode: QueryErrorCodes.STATUS_UPDATE_FAILED }, - onMutate: async ({ usersWithStatuses, status }) => { - // Optimistic update logic - await queryClient.cancelQueries({ queryKey: ['users-with-statuses'] }); - const previousData = queryClient.getQueryData([ - 'users-with-statuses', - ]); - if (previousData && usersWithStatuses.length > 0) { - const now = new Date().toISOString(); - const optimisticData = previousData.map((userStatus) => { - if ( - usersWithStatuses.some( - (selected) => selected.user.id === userStatus.user.id, - ) - ) { - return { - ...userStatus, - status, - created_at: now, - }; - } - return userStatus; - }); - queryClient.setQueryData(['users-with-statuses'], optimisticData); - // Add animation for optimistic updates - const updatedIds = usersWithStatuses.map((u) => u.user.id); - setNewStatusIds((prev) => new Set([...prev, ...updatedIds])); - // Remove animation after 1 second - setTimeout(() => { - setNewStatusIds((prev) => { - const updated = new Set(prev); - updatedIds.forEach((id) => updated.delete(id)); - return updated; - }); - }, 1000); - } - return { previousData }; - }, - onSuccess: (data) => { - // Handle success in the mutation function - void queryClient.invalidateQueries({ queryKey: ['users-with-statuses'] }); // Fixed floating promise - if (data.type === 'single') { - toast.success('Status updated for signed in user.'); - } else { - toast.success(`Status updated for ${data.count} selected users.`); - } - setSelectedUsers([]); - setStatusInput(''); - }, - onError: (error, _variables, context) => { - // Fixed unused variables - // Rollback optimistic update - if (context?.previousData) { - queryClient.setQueryData(['users-with-statuses'], context.previousData); - } - // Error handling is done in the global mutation cache - console.error('Status update failed:', error); - }, - }); - - const handleUpdateStatus = () => { - if (!isAuthenticated) { - toast.error('You must be signed in to update statuses.'); - return; - } - if (!statusInput.trim()) { - toast.error('Please enter a valid status.'); - return; - } - updateStatusMutation.mutate({ - usersWithStatuses: selectedUsers, - status: statusInput.trim(), - }); - }; - - const handleCheckboxChange = (user: UserWithStatus) => { - setSelectedUsers((prev) => - prev.some((u) => u.user.id === user.user.id) - ? prev.filter((prevUser) => prevUser.user.id !== user.user.id) - : [...prev, user], - ); - }; - - const handleSelectAllChange = () => { - if (selectAll) { - setSelectedUsers([]); - } else { - setSelectedUsers(usersWithStatuses); - } - setSelectAll(!selectAll); - }; - - useEffect(() => { - setSelectAll( - selectedUsers.length === usersWithStatuses.length && - usersWithStatuses.length > 0, - ); - }, [selectedUsers.length, usersWithStatuses.length]); - - const getConnectionIcon = () => { - switch (connectionStatus) { - case 'connected': - return ; - case 'connecting': - return ; - case 'disconnected': - return ; - } - }; - - const getConnectionText = () => { - switch (connectionStatus) { - case 'connected': - return 'Connected'; - case 'connecting': - return 'Connecting...'; - case 'disconnected': - return 'Disconnected'; - } - }; - - const formatTime = (timestamp: string) => { - const date = new Date(timestamp); - const time = date.toLocaleTimeString('en-US', { - hour: 'numeric', - minute: 'numeric', - }); - const day = date.getDate(); - const month = date.toLocaleString('default', { month: 'long' }); - return `${time} - ${month} ${day}`; - }; - - if (loading) { - return ( -
- -
- ); - } - - if (error) { - return ( -
-

Error loading status updates

- -
- ); - } - - const containerClassName = makeConditionalClassName({ - context: tvMode, - defaultClassName: 'mx-auto', - on: 'lg:w-11/12 w-full mt-15', - off: 'w-5/6', - }); - const thClassName = makeConditionalClassName({ - context: tvMode, - defaultClassName: 'py-3 px-4 border font-semibold', - on: 'lg:text-6xl', - off: 'lg:text-5xl', - }); - const tdClassName = makeConditionalClassName({ - context: tvMode, - defaultClassName: 'py-3 px-4 border', - on: 'lg:text-5xl', - off: 'lg:text-4xl', - }); - const tCheckboxClassName = `py-3 px-4 border`; - const checkBoxClassName = `lg:scale-200 cursor-pointer`; - - return ( -
- {/* Status Header */} -
- {isFetching ? ( - - - Updating... - - ) : ( - - {getConnectionIcon()} - {getConnectionText()} - - )} -
- - - - - {!tvMode && ( - - )} - - - - - - - {usersWithStatuses.map((userWithStatus, index) => { - const isSelected = selectedUsers.some( - (u) => u.user.id === userWithStatus.user.id, - ); - const isNewStatus = newStatusIds.has(userWithStatus.user.id); - - return ( - - {!tvMode && ( - - )} - - - - - ); - })} - -
- - Technician - - - Status - - - - Updated At
- handleCheckboxChange(userWithStatus)} - /> - -
- -
-

- {userWithStatus.user.full_name ?? 'Unknown User'} -

- {userWithStatus.updated_by && - userWithStatus.updated_by.id !== - userWithStatus.user.id && ( -
- - - Updated by {userWithStatus.updated_by.full_name} - -
- )} -
-
-
- - - setSelectedHistoryUser(userWithStatus.user) - } - > - {userWithStatus.status} - - {selectedHistoryUser === userWithStatus.user && ( - - )} - - - {formatTime(userWithStatus.created_at)} -
- - {usersWithStatuses.length === 0 && ( -
-

- No status updates yet -

-
- )} - - {!tvMode && ( -
- setStatusInput(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter' && !updateStatusMutation.isPending) { - e.preventDefault(); - handleUpdateStatus(); - } - }} - disabled={updateStatusMutation.isPending} - /> - - {updateStatusMutation.isPending - ? 'Updating...' - : selectedUsers.length > 0 - ? `Update ${selectedUsers.length} Users` - : 'Update Status'} - -
- )} - - {selectedUsers.length > 0 && !tvMode && ( -
-

- Updating status for {selectedUsers.length} selected users -

-
- )} - - {/* Global Status History Drawer */} -
- - - - - - -
-
- ); -}; diff --git a/src/components/status/index.tsx b/src/components/status/index.tsx index d75607f..2bca4d1 100644 --- a/src/components/status/index.tsx +++ b/src/components/status/index.tsx @@ -1,3 +1,4 @@ +export * from './ConnectionStatus'; export * from './HistoryDrawer'; -export * from './StatusList'; -export * from './TechTable'; +export * from './List'; +export * from './Table'; diff --git a/src/lib/hooks/index.ts b/src/lib/hooks/index.ts index a79dac8..41d8332 100755 --- a/src/lib/hooks/index.ts +++ b/src/lib/hooks/index.ts @@ -3,6 +3,8 @@ export * from './public'; export * from './status'; export * from './storage'; export * from './useFileUpload'; +export * from './useSharedStatusSubscription'; +export * from './useStatusData'; export type Result = | { success: true; data: T } diff --git a/src/lib/hooks/status.ts b/src/lib/hooks/status.ts index 632d5cb..89a963e 100644 --- a/src/lib/hooks/status.ts +++ b/src/lib/hooks/status.ts @@ -135,7 +135,7 @@ export const updateStatuses = async ( ) .select(); - if (insertedStatusesError) throw new Error("Error inserting statuses!"); + if (insertedStatusesError) throw new Error('Error inserting statuses!'); else if (insertedStatuses) { const createdAtFallback = new Date(Date.now()).toISOString(); const statusUpdates = usersWithStatuses.map((s, i) => { @@ -144,7 +144,7 @@ export const updateStatuses = async ( status: status, created_at: insertedStatuses[i]?.created_at ?? createdAtFallback, updated_by: user, - } + }; }); await broadcastStatusUpdates(statusUpdates); return { success: true, data: statusUpdates }; diff --git a/src/lib/hooks/useSharedStatusSubscription.ts b/src/lib/hooks/useSharedStatusSubscription.ts new file mode 100644 index 0000000..ea83c89 --- /dev/null +++ b/src/lib/hooks/useSharedStatusSubscription.ts @@ -0,0 +1,131 @@ +'use client'; +import { useState, useEffect, useCallback } from 'react'; +import { createClient } from '@/utils/supabase'; +import type { RealtimeChannel } from '@supabase/supabase-js'; + +export type ConnectionStatus = + | 'connecting' + | 'connected' + | 'disconnected' + | 'updating'; + +// Singleton state +let sharedChannel: RealtimeChannel | null = null; +let sharedConnectionStatus: ConnectionStatus = 'disconnected'; +const subscribers = new Set<(status: ConnectionStatus) => void>(); +const statusUpdateCallbacks = new Set<() => void>(); +//const subscribers: Set<(status: ConnectionStatus) => void> = new Set(); +//const statusUpdateCallbacks: Set<() => void> = new Set(); +let reconnectAttempts = 0; +let reconnectTimeout: NodeJS.Timeout | undefined; +const supabase = createClient(); + +const notifySubscribers = (status: ConnectionStatus) => { + sharedConnectionStatus = status; + subscribers.forEach(callback => callback(status)); +}; + +const notifyStatusUpdate = () => { + statusUpdateCallbacks.forEach(callback => callback()); +}; + +const cleanup = () => { + if (reconnectTimeout) { + clearTimeout(reconnectTimeout); + reconnectTimeout = undefined; + } + + if (sharedChannel) { + supabase.removeChannel(sharedChannel).catch((error) => { + console.error('Error removing shared channel:', error); + }); + sharedChannel = null; + } +}; + +const connect = () => { + if (sharedChannel) return; // Already connected or connecting + + cleanup(); + notifySubscribers('connecting'); + + const channel = supabase + .channel('shared_status_updates', { + config: { broadcast: {self: true }} + }) + .on('broadcast', { event: 'status_updated' }, () => { + notifyStatusUpdate(); + }) + .subscribe((status) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison + if (status === 'SUBSCRIBED') { + notifySubscribers('connected'); + reconnectAttempts = 0; + // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison + } else if (status === 'CHANNEL_ERROR' || status === 'CLOSED') { + notifySubscribers('disconnected'); + + if (reconnectAttempts < 5) { + reconnectAttempts++; + const delay = 2000 * reconnectAttempts; + + reconnectTimeout = setTimeout(() => { + if (subscribers.size > 0) { // Only reconnect if there are active subscribers + connect(); + } + }, delay); + } + } + }); + + sharedChannel = channel; +}; + +const disconnect = () => { + cleanup(); + notifySubscribers('disconnected'); +}; + +export const useSharedStatusSubscription = (onStatusUpdate?: () => void) => { + const [connectionStatus, setConnectionStatus] = useState(sharedConnectionStatus); + + useEffect(() => { + // Subscribe to status changes + subscribers.add(setConnectionStatus); + + // Subscribe to status updates + if (onStatusUpdate) { + statusUpdateCallbacks.add(onStatusUpdate); + } + + // Connect if this is the first subscriber + if (subscribers.size === 1) { + const timeout = setTimeout(connect, 1000); + return () => clearTimeout(timeout); + } + + return () => { + // Cleanup subscriptions + subscribers.delete(setConnectionStatus); + if (onStatusUpdate) { + statusUpdateCallbacks.delete(onStatusUpdate); + } + + // Disconnect if no more subscribers + if (subscribers.size === 0) { + disconnect(); + } + }; + }, [onStatusUpdate]); + + const reconnect = useCallback(() => { + reconnectAttempts = 0; + connect(); + }, []); + + return { + connectionStatus, + connect: reconnect, + disconnect, + }; +}; diff --git a/src/lib/hooks/useStatusData.ts b/src/lib/hooks/useStatusData.ts new file mode 100644 index 0000000..4f290de --- /dev/null +++ b/src/lib/hooks/useStatusData.ts @@ -0,0 +1,132 @@ +'use client'; +import { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { + getRecentUsersWithStatuses, + updateStatuses, + updateUserStatus, + type UserWithStatus, +} from '@/lib/hooks'; +import { QueryErrorCodes } from '@/components/context'; +import { toast } from 'sonner'; + +type UseStatusDataOptions = { + initialData?: UserWithStatus[]; + enabled?: boolean; +} + +export const useStatusData = ({ + initialData = [], + enabled = true +}: UseStatusDataOptions = {}) => { + const queryClient = useQueryClient(); + const [newStatuses, setNewStatuses] = useState>( + new Set() + ); + + const query = useQuery({ + queryKey: ['users-with-statuses'], + queryFn: async () => { + try { + const response = await getRecentUsersWithStatuses(); + if (!response.success) throw new Error(response.error); + return response.data; + } catch (error) { + toast.error(`Error fetching technicians: ${error as Error}`); + throw error; + } + }, + enabled, + refetchInterval: 30000, + refetchOnWindowFocus: true, + refetchOnMount: true, + initialData, + meta: { errCode: QueryErrorCodes.USERS_FETCH_FAILED }, + }); + + const updateStatusMutation = useMutation({ + mutationFn: async ({ + usersWithStatuses, + status, + }: { + usersWithStatuses: UserWithStatus[]; + status: string; + }) => { + try { + if (usersWithStatuses.length <= 0) { + const result = await updateUserStatus(status); + if (!result.success) throw new Error(result.error); + return result.data; + } else { + const result = await updateStatuses(usersWithStatuses, status); + if (!result.success) throw new Error(result.error); + return result.data; + } + } catch (error) { + console.error(`Error updating statuses: ${error as Error}`); + throw error; + } + }, + meta: { errCode: QueryErrorCodes.STATUS_UPDATE_FAILED }, + onMutate: async ({ usersWithStatuses, status }) => { + await queryClient.cancelQueries({ queryKey: ['users-with-statuses'] }); + const previousData = queryClient.getQueryData([ + 'users-with-statuses', + ]); + + if (previousData && usersWithStatuses.length > 0) { + const now = new Date().toISOString(); + const optimisticData = previousData.map((userStatus) => { + if ( + usersWithStatuses.some( + (selected) => selected.user.id === userStatus.user.id + ) + ) { + return { ...userStatus, status, created_at: now }; + } + return userStatus; + }); + queryClient.setQueryData(['users-with-statuses'], optimisticData); + + // Add animation to optimistically updated statuses + setNewStatuses((prev) => new Set([...prev, ...usersWithStatuses])); + setTimeout(() => { + setNewStatuses((prev) => { + const updated = new Set(prev); + usersWithStatuses.forEach((updatedStatus) => + updated.delete(updatedStatus) + ); + return updated; + }); + }, 1000); + } + + return { previousData }; + }, + onSuccess: (data) => { + queryClient + .invalidateQueries({ queryKey: ['users-with-statuses'] }) + .catch((error) => console.error(`Error invalidating query: ${error}`)); + + if (!data) return; + + data.forEach((statusUpdate) => { + toast.success( + `${statusUpdate.user.full_name}'s status updated to '${statusUpdate.status}'.` + ); + }); + }, + onError: (error, _variables, context) => { + if (context?.previousData) { + queryClient.setQueryData(['users-with-statuses'], context.previousData); + } + toast.error(`Error updating statuses: ${error}`); + }, + }); + + return { + ...query, + newStatuses, + updateStatusMutation, + }; +}; diff --git a/src/lib/hooks/useStatusSubscription.ts b/src/lib/hooks/useStatusSubscription.ts new file mode 100644 index 0000000..60944c7 --- /dev/null +++ b/src/lib/hooks/useStatusSubscription.ts @@ -0,0 +1,147 @@ +'use client'; +import { useState, useEffect, useRef, useCallback } from 'react'; +import { createClient } from '@/utils/supabase'; +import type { RealtimeChannel } from '@supabase/supabase-js'; + +export type ConnectionStatus = + | 'connecting' + | 'connected' + | 'disconnected' + | 'updating'; + +type UseStatusSubscriptionOptions = { + enabled?: boolean; + onStatusUpdate?: () => void; + maxReconnectAttempts?: number; + reconnectDelay?: number; +} + +export const useStatusSubscription = ({ + enabled = true, + onStatusUpdate, + maxReconnectAttempts = 5, + reconnectDelay = 2000, +}: UseStatusSubscriptionOptions = {}) => { + const [connectionStatus, setConnectionStatus] = + useState('disconnected'); + const channelRef = useRef(null); + const supabaseRef = useRef(createClient()); + const reconnectAttemptsRef = useRef(0); + const reconnectTimeoutRef = useRef(undefined); + const isComponentMountedRef = useRef(true); + const visibilityTimeoutRef = useRef(undefined); + + const cleanup = useCallback(() => { + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current); + reconnectTimeoutRef.current = undefined; + } + if (visibilityTimeoutRef.current) { + clearTimeout(visibilityTimeoutRef.current); + visibilityTimeoutRef.current = undefined; + } + if (channelRef.current) { + supabaseRef.current.removeChannel(channelRef.current).catch((error) => { + console.error('❌ cleanup: Error removing channel:', error); + }); + channelRef.current = null; + } + }, []); + + const connect = useCallback(() => { + if (!enabled || !isComponentMountedRef.current) return; + + cleanup(); + setConnectionStatus('connecting'); + + const channel = supabaseRef.current + .channel('status_updates', { + config: { broadcast: {self: true }} + }); + channel + .on('broadcast', { event: 'status_updated' }, (payload) => { + onStatusUpdate?.(); + }) + .subscribe((status) => { + if (!isComponentMountedRef.current) return; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison + if (status === 'SUBSCRIBED') { + setConnectionStatus('connected'); + reconnectAttemptsRef.current = 0; + // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison + } else if (status === 'CHANNEL_ERROR' || status === 'CLOSED') { + setConnectionStatus('disconnected'); + if (reconnectAttemptsRef.current < maxReconnectAttempts) { + reconnectAttemptsRef.current++; + const delay = reconnectDelay * reconnectAttemptsRef.current; + + reconnectTimeoutRef.current = setTimeout(() => { + if (isComponentMountedRef.current) connect(); + }, delay); + } else { + console.warn('⚠️ connect: Max reconnection attempts reached'); + setConnectionStatus('disconnected'); + } + } + }); + + channelRef.current = channel; + }, [enabled, onStatusUpdate, maxReconnectAttempts, reconnectDelay, cleanup]); + + const disconnect = useCallback(() => { + cleanup(); + setConnectionStatus('disconnected'); + }, [cleanup]); + + const reconnect = useCallback(() => { + reconnectAttemptsRef.current = 0; + connect(); + }, [connect]); + + // Handle visibility change for better reconnection + useEffect(() => { + const handleVisibilityChange = () => { + if (!enabled) return; + if (document.visibilityState === 'visible') { + visibilityTimeoutRef.current = setTimeout(() => { + if (connectionStatus === 'disconnected' && isComponentMountedRef.current) { + reconnect(); + } + }, 1000); + } + }; + document.addEventListener('visibilitychange', handleVisibilityChange); + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; + }, [enabled, connectionStatus, reconnect]); + + // Initial connection - SIMPLIFIED to avoid dependency issues + useEffect(() => { + if (!enabled) { + disconnect(); + return; + } + const initialTimeout = setTimeout(() => { + if (isComponentMountedRef.current) connect(); + }, 1000); + + return () => { + clearTimeout(initialTimeout); + }; + }, [enabled]); + + // Cleanup on unmount + useEffect(() => { + return () => { + cleanup(); + }; + }, [cleanup]); + + return { + connectionStatus, + connect: reconnect, + disconnect, + }; +}; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index cc7a7c5..b5559a7 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -18,3 +18,19 @@ export const makeConditionalClassName = ({ }) => { return defaultClassName + ' ' + (context ? on : off); }; + +export const formatTime = (timestamp: string) => { + const date = new Date(timestamp); + const time = date.toLocaleTimeString('en-US', { + hour: 'numeric', + minute: 'numeric', + }); + return time; +}; + +export const formatDate = (timestamp: string) => { + const date = new Date(timestamp); + const day = date.getDate(); + const month = date.toLocaleString('default', { month: 'long' }); + return `${month} ${day}`; +}; diff --git a/src/middleware.ts b/src/middleware.ts index 6d9d3a4..ebd72fc 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -110,7 +110,9 @@ export const middleware = async (request: NextRequest) => { if (shouldBan) { console.log(`🔨 IP ${ip} has been banned for suspicious activity`); - return new NextResponse('Access denied - IP banned. Please fuck off.', { status: 403 }); + return new NextResponse('Access denied - IP banned. Please fuck off.', { + status: 403, + }); } // Return 404 to not reveal the blocking mechanism