From d0533b78e8adfcae106047af41533064575d1038 Mon Sep 17 00:00:00 2001 From: Gib Date: Thu, 19 Jun 2025 14:31:10 -0500 Subject: [PATCH] Cleaning up. Rewriting. Vibing. Coding. Little bit of vibe coding. --- package.json | 4 +- pnpm-lock.yaml | 44 +-- src/app/layout.tsx | 6 +- src/app/status/list/layout.tsx | 6 +- src/components/default/header/index.tsx | 3 +- src/components/status/ConnectionStatus.tsx | 7 +- src/components/status/HistoryDrawer.tsx | 4 +- src/components/status/List.tsx | 23 +- src/components/status/StatusList.tsx | 388 +++++++++++++++++++ src/components/status/Table.tsx | 46 +-- src/components/status/index.tsx | 3 +- src/lib/hooks/index.ts | 2 +- src/lib/hooks/useSharedStatusSubscription.ts | 213 ---------- src/lib/hooks/useStatusData.ts | 12 +- src/lib/hooks/useStatusSubscription.ts | 348 +++++++++++------ 15 files changed, 697 insertions(+), 412 deletions(-) create mode 100644 src/components/status/StatusList.tsx delete mode 100644 src/lib/hooks/useSharedStatusSubscription.ts diff --git a/package.json b/package.json index 4f71293..f39f519 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@supabase/ssr": "^0.6.1", "@supabase/supabase-js": "^2.50.0", "@t3-oss/env-nextjs": "^0.12.0", - "@tanstack/react-query": "^5.80.7", + "@tanstack/react-query": "^5.80.10", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.510.0", @@ -62,7 +62,7 @@ "import-in-the-middle": "^1.14.2", "postcss": "^8.5.6", "prettier": "^3.5.3", - "prettier-plugin-tailwindcss": "^0.6.12", + "prettier-plugin-tailwindcss": "^0.6.13", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.10", "tailwindcss-animate": "^1.0.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf51575..2aa1f47 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,8 +51,8 @@ importers: specifier: ^0.12.0 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) + specifier: ^5.80.10 + version: 5.80.10(react@19.1.0) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -139,8 +139,8 @@ importers: specifier: ^3.5.3 version: 3.5.3 prettier-plugin-tailwindcss: - specifier: ^0.6.12 - version: 0.6.12(prettier@3.5.3) + specifier: ^0.6.13 + version: 0.6.13(prettier@3.5.3) tailwind-merge: specifier: ^3.3.1 version: 3.3.1 @@ -1504,11 +1504,11 @@ packages: peerDependencies: eslint: ^8.57.0 || ^9.0.0 - '@tanstack/query-core@5.80.7': - resolution: {integrity: sha512-s09l5zeUKC8q7DCCCIkVSns8zZrK4ZDT6ryEjxNBFi68G4z2EBobBS7rdOY3r6W1WbUDpc1fe5oY+YO/+2UVUg==} + '@tanstack/query-core@5.80.10': + resolution: {integrity: sha512-mUNQOtzxkjL6jLbyChZoSBP6A5gQDVRUiPvW+/zw/9ftOAz+H754zCj3D8PwnzPKyHzGkQ9JbH48ukhym9LK1Q==} - '@tanstack/react-query@5.80.7': - resolution: {integrity: sha512-u2F0VK6+anItoEvB3+rfvTO9GEh2vb00Je05OwlUe/A0lkJBgW1HckiY3f9YZa+jx6IOe4dHPh10dyp9aY3iRQ==} + '@tanstack/react-query@5.80.10': + resolution: {integrity: sha512-6zM098J8sLy9oU60XAdzUlAH4wVzoMVsWUWiiE/Iz4fd67PplxeyL4sw/MPcVJJVhbwGGXCsHn9GrQt2mlAzig==} peerDependencies: react: ^18 || ^19 @@ -2091,8 +2091,8 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - electron-to-chromium@1.5.170: - resolution: {integrity: sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA==} + electron-to-chromium@1.5.171: + resolution: {integrity: sha512-scWpzXEJEMrGJa4Y6m/tVotb0WuvNmasv3wWVzUAeCgKU0ToFOhUW6Z+xWnRQANMYGxN4ngJXIThgBJOqzVPCQ==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} @@ -3023,8 +3023,8 @@ packages: resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} - prettier-plugin-tailwindcss@0.6.12: - resolution: {integrity: sha512-OuTQKoqNwV7RnxTPwXWzOFXy6Jc4z8oeRZYGuMpRyG3WbuR3jjXdQFK8qFBMBx8UHWdHrddARz2fgUenild6aw==} + prettier-plugin-tailwindcss@0.6.13: + resolution: {integrity: sha512-uQ0asli1+ic8xrrSmIOaElDu0FacR4x69GynTh2oZjFY10JUt6EEumTQl5tB4fMeD6I1naKd+4rXQQ7esT2i1g==} engines: {node: '>=14.21.3'} peerDependencies: '@ianvs/prettier-plugin-sort-imports': '*' @@ -3410,8 +3410,8 @@ packages: uglify-js: optional: true - terser@5.43.0: - resolution: {integrity: sha512-CqNNxKSGKSZCunSvwKLTs8u8sGGlp27sxNZ4quGh0QeNuyHM0JSEM/clM9Mf4zUp6J+tO2gUXhgXT2YMMkwfKQ==} + terser@5.43.1: + resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} engines: {node: '>=10'} hasBin: true @@ -4992,11 +4992,11 @@ snapshots: - supports-color - typescript - '@tanstack/query-core@5.80.7': {} + '@tanstack/query-core@5.80.10': {} - '@tanstack/react-query@5.80.7(react@19.1.0)': + '@tanstack/react-query@5.80.10(react@19.1.0)': dependencies: - '@tanstack/query-core': 5.80.7 + '@tanstack/query-core': 5.80.10 react: 19.1.0 '@tybys/wasm-util@0.9.0': @@ -5491,7 +5491,7 @@ snapshots: browserslist@4.25.0: dependencies: caniuse-lite: 1.0.30001723 - electron-to-chromium: 1.5.170 + electron-to-chromium: 1.5.171 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.170: {} + electron-to-chromium@1.5.171: {} emoji-regex@9.2.2: {} @@ -6679,7 +6679,7 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier-plugin-tailwindcss@0.6.12(prettier@3.5.3): + prettier-plugin-tailwindcss@0.6.13(prettier@3.5.3): dependencies: prettier: 3.5.3 @@ -7087,10 +7087,10 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.43.0 + terser: 5.43.1 webpack: 5.99.9 - terser@5.43.0: + terser@5.43.1: dependencies: '@jridgewell/source-map': 0.3.6 acorn: 8.15.0 diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 63a99a8..c4ba284 100755 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -390,8 +390,10 @@ 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/components/default/header/index.tsx b/src/components/default/header/index.tsx index 9252ca9..b629db1 100644 --- a/src/components/default/header/index.tsx +++ b/src/components/default/header/index.tsx @@ -48,7 +48,8 @@ const Header = () => { height={100} className='max-w-[40px] md:max-w-[120px]' /> -

void; showAsButton?: boolean; className?: string; -} +}; const getConnectionIcon = (status: ConnectionStatusType) => { switch (status) { @@ -57,10 +57,7 @@ export const ConnectionStatus = ({ } return ( - + {getConnectionIcon(status)} {getConnectionText(status)} diff --git a/src/components/status/HistoryDrawer.tsx b/src/components/status/HistoryDrawer.tsx index 6d80d27..3ce0948 100644 --- a/src/components/status/HistoryDrawer.tsx +++ b/src/components/status/HistoryDrawer.tsx @@ -106,7 +106,9 @@ export const HistoryDrawer: React.FC = ({ className='w-8 h-8 md:w-12 md:h-12' />

- {user && user.id !== '' ? `${user.full_name}'s History` : 'All History'} + {user && user.id !== '' + ? `${user.full_name}'s History` + : 'All History'}

{totalCount > 0 && ( diff --git a/src/components/status/List.tsx b/src/components/status/List.tsx index 3eef001..87bf212 100644 --- a/src/components/status/List.tsx +++ b/src/components/status/List.tsx @@ -12,12 +12,12 @@ import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { RefreshCw, Clock, Calendar } from 'lucide-react'; -import { useStatusData, useSharedStatusSubscription } from '@/lib/hooks'; +import { useStatusData, useStatusSubscription } from '@/lib/hooks'; import { formatTime, formatDate } from '@/lib/utils'; import Link from 'next/link'; type ListProps = { - initialStatuses: UserWithStatus[] + initialStatuses: UserWithStatus[]; }; export const StatusList = ({ initialStatuses = [] }: ListProps) => { @@ -44,7 +44,7 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => { }); // In your StatusList component - const { connectionStatus, connect: reconnect } = useSharedStatusSubscription(() => { + const { connectionStatus, connect: reconnect } = useStatusSubscription(() => { refetch().catch((error) => { console.error('Error refetching statuses:', error); }); @@ -53,14 +53,14 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => { const handleUpdateStatus = () => { if (!isAuthenticated) { setUpdateStatusMessage( - 'Error: You must be signed in to update technician statuses!' + '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!' + 'Error: Your status must be between 3 & 80 characters long!', ); return; } @@ -79,7 +79,7 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => { setSelectedUsers((prev) => prev.some((u) => u.user.id === user.user.id) ? prev.filter((prevUser) => prevUser.user.id !== user.user.id) - : [...prev, user] + : [...prev, user], ); }; @@ -95,7 +95,7 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => { useEffect(() => { setSelectAll( selectedUsers.length === usersWithStatuses.length && - usersWithStatuses.length > 0 + usersWithStatuses.length > 0, ); }, [selectedUsers.length, usersWithStatuses.length]); @@ -174,7 +174,7 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => { )} - +
{
{usersWithStatuses.map((userWithStatus) => { const isSelected = selectedUsers.some( - (u) => u.user.id === userWithStatus.user.id + (u) => u.user.id === userWithStatus.user.id, ); const isNewStatus = newStatuses.has(userWithStatus); const isUpdatedByOther = @@ -329,8 +329,7 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => { {selectedUsers.length > 0 ? `Update status for ${selectedUsers.length} ${selectedUsers.length > 1 ? 'users' : 'user'}` - : 'Update status' - } + : 'Update status'}
{updateStatusMessage && @@ -341,7 +340,7 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => { ) : ( - ))} + ))}
diff --git a/src/components/status/StatusList.tsx b/src/components/status/StatusList.tsx new file mode 100644 index 0000000..9148f57 --- /dev/null +++ b/src/components/status/StatusList.tsx @@ -0,0 +1,388 @@ +'use client'; +import { useState, useEffect } from 'react'; +import type React 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 { Card, CardContent } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { RefreshCw, Clock, Calendar, CheckCircle2 } from 'lucide-react'; +import { useStatusData, useStatusSubscription } from '@/lib/hooks'; +import { formatTime, formatDate } from '@/lib/utils'; +import Link from 'next/link'; + +type ListProps = { + initialStatuses: UserWithStatus[]; +}; + +export const StatusList = ({ initialStatuses = [] }: ListProps) => { + 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, + }); + + const { connectionStatus, connect: reconnect } = useStatusSubscription(() => { + 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 handleCardSelect = (user: UserWithStatus, e: React.MouseEvent) => { + // Prevent selection if clicking on profile elements + if ((e.target as HTMLElement).closest('[data-profile-trigger]')) { + return; + } + + 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: 'flex flex-col mx-auto space-y-3 items-center', + on: 'lg:w-11/12 w-full mt-8', + off: 'sm:w-5/6 md:w-3/4 lg:w-2/3 xl:w-1/2', + }); + + const headerClassName = makeConditionalClassName({ + context: tvMode, + defaultClassName: 'w-full', + on: 'hidden', + off: 'flex mb-6 justify-between items-center', + }); + + const cardContainerClassName = makeConditionalClassName({ + context: tvMode, + defaultClassName: 'w-full', + on: 'grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4', + off: 'space-y-2 w-full', + }); + + return ( +
+
+
+ + {!tvMode && ( +
+ Miss the old table? + + Find it here! + +
+ )} +
+
+ +
+
+ +
+ {usersWithStatuses.map((userWithStatus) => { + const isSelected = selectedUsers.some( + (u) => u.user.id === userWithStatus.user.id, + ); + const isNewStatus = newStatuses.has(userWithStatus); + const isUpdatedByOther = + userWithStatus.updated_by && + userWithStatus.updated_by.id !== userWithStatus.user.id; + + return ( + handleCardSelect(userWithStatus, e)} + > + {isSelected && ( +
+ +
+ )} + + +
+ {/* Profile Section - Clickable for history */} + + +
+ setSelectedHistoryUser(userWithStatus.user) + } + > + +
+
+ {selectedHistoryUser === userWithStatus.user && ( + + )} +
+ + {/* Content Section */} +
+ {/* Header with name and timestamp */} +
+ + +

+ setSelectedHistoryUser(userWithStatus.user) + } + > + {userWithStatus.user.full_name} +

+
+ {selectedHistoryUser === userWithStatus.user && ( + + )} +
+ +
+ + + {formatTime(userWithStatus.created_at)} + +
+
+ + {/* Status Content */} +
+

{userWithStatus.status}

+
+ + {/* Footer with date and updated by info */} +
+
+ + + {formatDate(userWithStatus.created_at)} + +
+ + {isUpdatedByOther && ( +
+ + + Updated by {userWithStatus.updated_by?.full_name} + +
+ )} +
+
+
+
+
+ ); + })} +
+ + {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 ${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/Table.tsx b/src/components/status/Table.tsx index 96d5cbd..6002dc5 100644 --- a/src/components/status/Table.tsx +++ b/src/components/status/Table.tsx @@ -10,11 +10,10 @@ 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 { useStatusSubscription, useStatusData } from '@/lib/hooks'; import { formatTime, formatDate } from '@/lib/utils'; import Link from 'next/link'; - type TableProps = { initialStatuses: UserWithStatus[]; }; @@ -42,31 +41,31 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => { enabled: isAuthenticated, }); // In your StatusList component - const { connectionStatus, connect: reconnect } = useSharedStatusSubscription(() => { + const { connectionStatus, connect: reconnect } = useStatusSubscription(() => { 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); - //}); - //}, + //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!' + '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!' + 'Error: Your status must be between 3 & 80 characters long!', ); return; } @@ -85,7 +84,7 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => { setSelectedUsers((prev) => prev.some((u) => u.user.id === user.user.id) ? prev.filter((prevUser) => prevUser.user.id !== user.user.id) - : [...prev, user] + : [...prev, user], ); }; @@ -101,7 +100,7 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => { useEffect(() => { setSelectAll( selectedUsers.length === usersWithStatuses.length && - usersWithStatuses.length > 0 + usersWithStatuses.length > 0, ); }, [selectedUsers.length, usersWithStatuses.length]); @@ -163,7 +162,7 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => { /> {!tvMode && (
-

Tired of the old table? {' '}

+

Tired of the old table?

{ fullName={userWithStatus.updated_by?.full_name} className='w-5 h-5' /> - + Updated by {userWithStatus.updated_by.full_name}
@@ -279,7 +276,9 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => {
- + {formatTime(userWithStatus.created_at)}
@@ -315,7 +314,7 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => { ) : ( - ))} + ))} {!tvMode && (
@@ -352,8 +351,7 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => { {selectedUsers.length > 0 ? `Update status for ${selectedUsers.length} ${selectedUsers.length > 1 ? 'users' : 'user'}` - : 'Update status' - } + : 'Update status'}
)} @@ -363,7 +361,10 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => {
- @@ -374,4 +375,3 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => {
); }; - diff --git a/src/components/status/index.tsx b/src/components/status/index.tsx index 2bca4d1..719a4c5 100644 --- a/src/components/status/index.tsx +++ b/src/components/status/index.tsx @@ -1,4 +1,5 @@ export * from './ConnectionStatus'; export * from './HistoryDrawer'; -export * from './List'; +//export * from './List'; +export * from './StatusList'; export * from './Table'; diff --git a/src/lib/hooks/index.ts b/src/lib/hooks/index.ts index 41d8332..7cc015f 100755 --- a/src/lib/hooks/index.ts +++ b/src/lib/hooks/index.ts @@ -3,7 +3,7 @@ export * from './public'; export * from './status'; export * from './storage'; export * from './useFileUpload'; -export * from './useSharedStatusSubscription'; +export * from './useStatusSubscription'; export * from './useStatusData'; export type Result = diff --git a/src/lib/hooks/useSharedStatusSubscription.ts b/src/lib/hooks/useSharedStatusSubscription.ts deleted file mode 100644 index 34819cc..0000000 --- a/src/lib/hooks/useSharedStatusSubscription.ts +++ /dev/null @@ -1,213 +0,0 @@ -'use client'; -import { useState, useEffect, useCallback, useRef } 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>(); -let reconnectAttempts = 0; -let reconnectTimeout: NodeJS.Timeout | undefined; -const supabase = createClient(); - -const notifySubscribers = (status: ConnectionStatus) => { - console.log('๐Ÿ“ข notifySubscribers: Notifying', subscribers.size, 'subscribers of status change to:', status); - sharedConnectionStatus = status; - subscribers.forEach((callback, index) => { - console.log('๐Ÿ“ข notifySubscribers: Calling subscriber', index + 1); - callback(status); - }); - console.log('๐Ÿ“ข notifySubscribers: All subscribers notified'); -}; - -const notifyStatusUpdate = () => { - console.log('๐Ÿ”„ notifyStatusUpdate: Notifying', statusUpdateCallbacks.size, 'status update callbacks'); - statusUpdateCallbacks.forEach((callback, index) => { - console.log('๐Ÿ”„ notifyStatusUpdate: Calling callback', index + 1); - callback(); - }); - console.log('๐Ÿ”„ notifyStatusUpdate: All callbacks executed'); -}; - -const cleanup = () => { - console.log('๐Ÿงน cleanup: Starting cleanup process'); - - if (reconnectTimeout) { - console.log('๐Ÿงน cleanup: Clearing reconnect timeout'); - clearTimeout(reconnectTimeout); - reconnectTimeout = undefined; - } - - if (sharedChannel) { - console.log('๐Ÿงน cleanup: Removing shared channel'); - supabase.removeChannel(sharedChannel).catch((error) => { - console.error('โŒ cleanup: Error removing shared channel:', error); - }); - sharedChannel = null; - } - - console.log('โœ… cleanup: Cleanup completed'); -}; - -const connect = () => { - console.log('๐Ÿ”Œ connect: Function called'); - console.log('๐Ÿ”Œ connect: sharedChannel exists:', !!sharedChannel); - console.log('๐Ÿ”Œ connect: subscribers count:', subscribers.size); - - if (sharedChannel) { - console.log('โŒ connect: Already connected or connecting, returning early'); - return; - } - - console.log('๐Ÿ”Œ connect: Starting connection process'); - cleanup(); - notifySubscribers('connecting'); - - console.log('๐Ÿ”Œ connect: Creating new channel'); - const channel = supabase - .channel('shared_status_updates', { - config: { broadcast: {self: true }} - }) - .on('broadcast', { event: 'status_updated' }, (payload) => { - console.log('๐Ÿ“ก connect: Broadcast event received:', payload); - notifyStatusUpdate(); - }) - .subscribe((status) => { - console.log('๐Ÿ“ก connect: Subscription status changed to:', status); - - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison - if (status === 'SUBSCRIBED') { - console.log('โœ… connect: Successfully subscribed to realtime'); - notifySubscribers('connected'); - reconnectAttempts = 0; - console.log('โœ… connect: Reset reconnect attempts to 0'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison - } else if (status === 'CHANNEL_ERROR' || status === 'CLOSED') { - console.log('โŒ connect: Channel error or closed, status:', status); - notifySubscribers('disconnected'); - - if (reconnectAttempts < 5) { - reconnectAttempts++; - const delay = 2000 * reconnectAttempts; - console.log('๐Ÿ”„ connect: Scheduling reconnection attempt', reconnectAttempts, 'in', delay, 'ms'); - - reconnectTimeout = setTimeout(() => { - console.log('๐Ÿ”„ connect: Reconnection timeout executed'); - if (subscribers.size > 0) { - console.log('๐Ÿ”„ connect: Calling connect() for reconnection'); - connect(); - } else { - console.log('โŒ connect: No active subscribers, skipping reconnection'); - } - }, delay); - } else { - console.warn('โš ๏ธ connect: Max reconnection attempts (5) reached'); - } - } - }); - - sharedChannel = channel; - console.log('๐Ÿ”Œ connect: Channel stored in sharedChannel variable'); -}; - -const disconnect = () => { - console.log('๐Ÿ”Œ disconnect: Function called'); - cleanup(); - notifySubscribers('disconnected'); -}; - -export const useSharedStatusSubscription = (onStatusUpdate?: () => void) => { - console.log('๐Ÿš€ useSharedStatusSubscription: Hook called'); - - const [connectionStatus, setConnectionStatus] = useState(sharedConnectionStatus); - const onStatusUpdateRef = useRef(onStatusUpdate); - const hasInitialized = useRef(false); - - // Keep the ref updated - onStatusUpdateRef.current = onStatusUpdate; - - // Create a stable callback - const stableOnStatusUpdate = useCallback(() => { - onStatusUpdateRef.current?.(); - }, []); - - useEffect(() => { - console.log('๐Ÿ”ง useSharedStatusSubscription useEffect: Running'); - console.log('๐Ÿ”ง useSharedStatusSubscription useEffect: hasInitialized:', hasInitialized.current); - console.log('๐Ÿ”ง useSharedStatusSubscription useEffect: Current subscribers count:', subscribers.size); - - // Prevent duplicate initialization - if (hasInitialized.current) { - console.log('๐Ÿ”ง useSharedStatusSubscription useEffect: Already initialized, skipping'); - return; - } - - hasInitialized.current = true; - - // Subscribe to status changes - subscribers.add(setConnectionStatus); - console.log('๐Ÿ”ง useSharedStatusSubscription useEffect: Added setConnectionStatus to subscribers'); - - // Subscribe to status updates - if (onStatusUpdate) { - statusUpdateCallbacks.add(stableOnStatusUpdate); - console.log('๐Ÿ”ง useSharedStatusSubscription useEffect: Added stable onStatusUpdate callback'); - } - - // Connect if this is the first subscriber - if (subscribers.size === 1) { - console.log('๐Ÿ”ง useSharedStatusSubscription useEffect: First subscriber, setting up connection'); - const timeout = setTimeout(() => { - console.log('๐Ÿ”ง useSharedStatusSubscription useEffect: Connection timeout executed, calling connect()'); - connect(); - }, 1000); - - return () => { - console.log('๐Ÿ”ง useSharedStatusSubscription useEffect: Cleanup - clearing connection timeout'); - clearTimeout(timeout); - hasInitialized.current = false; - subscribers.delete(setConnectionStatus); - statusUpdateCallbacks.delete(stableOnStatusUpdate); - - if (subscribers.size === 0) { - disconnect(); - } - }; - } - - return () => { - console.log('๐Ÿ”ง useSharedStatusSubscription useEffect: Cleanup function running'); - hasInitialized.current = false; - subscribers.delete(setConnectionStatus); - statusUpdateCallbacks.delete(stableOnStatusUpdate); - - if (subscribers.size === 0) { - console.log('๐Ÿ”ง useSharedStatusSubscription useEffect: No more subscribers, calling disconnect()'); - disconnect(); - } - }; - }, []); // Empty dependency array! - - const reconnect = useCallback(() => { - console.log('๐Ÿ”„ reconnect: Function called'); - reconnectAttempts = 0; - console.log('๐Ÿ”„ reconnect: Reset reconnectAttempts to 0, calling connect()'); - connect(); - }, []); - - console.log('๐Ÿ useSharedStatusSubscription: connectionStatus:', connectionStatus); - - return { - connectionStatus, - connect: reconnect, - disconnect, - }; -}; diff --git a/src/lib/hooks/useStatusData.ts b/src/lib/hooks/useStatusData.ts index aeedb00..d5f180f 100644 --- a/src/lib/hooks/useStatusData.ts +++ b/src/lib/hooks/useStatusData.ts @@ -13,15 +13,15 @@ import { toast } from 'sonner'; type UseStatusDataOptions = { initialData?: UserWithStatus[]; enabled?: boolean; -} +}; export const useStatusData = ({ initialData = [], - enabled = true + enabled = true, }: UseStatusDataOptions = {}) => { const queryClient = useQueryClient(); const [newStatuses, setNewStatuses] = useState>( - new Set() + new Set(), ); const query = useQuery({ @@ -79,7 +79,7 @@ export const useStatusData = ({ const optimisticData = previousData.map((userStatus) => { if ( usersWithStatuses.some( - (selected) => selected.user.id === userStatus.user.id + (selected) => selected.user.id === userStatus.user.id, ) ) { return { ...userStatus, status, created_at: now }; @@ -94,7 +94,7 @@ export const useStatusData = ({ setNewStatuses((prev) => { const updated = new Set(prev); usersWithStatuses.forEach((updatedStatus) => - updated.delete(updatedStatus) + updated.delete(updatedStatus), ); return updated; }); @@ -112,7 +112,7 @@ export const useStatusData = ({ data.forEach((statusUpdate) => { toast.success( - `${statusUpdate.user.full_name}'s status updated to '${statusUpdate.status}'.` + `${statusUpdate.user.full_name}'s status updated to '${statusUpdate.status}'.`, ); }); }, diff --git a/src/lib/hooks/useStatusSubscription.ts b/src/lib/hooks/useStatusSubscription.ts index 60944c7..a8df130 100644 --- a/src/lib/hooks/useStatusSubscription.ts +++ b/src/lib/hooks/useStatusSubscription.ts @@ -1,5 +1,5 @@ 'use client'; -import { useState, useEffect, useRef, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { createClient } from '@/utils/supabase'; import type { RealtimeChannel } from '@supabase/supabase-js'; @@ -9,135 +9,247 @@ export type ConnectionStatus = | 'disconnected' | 'updating'; -type UseStatusSubscriptionOptions = { - enabled?: boolean; - onStatusUpdate?: () => void; - maxReconnectAttempts?: number; - reconnectDelay?: number; -} +// Singleton state +let sharedChannel: RealtimeChannel | null = null; +let sharedConnectionStatus: ConnectionStatus = 'disconnected'; +const subscribers = new Set<(status: ConnectionStatus) => void>(); +const statusUpdateCallbacks = new Set<() => void>(); +let reconnectAttempts = 0; +let reconnectTimeout: NodeJS.Timeout | undefined; +const supabase = createClient(); -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 notifySubscribers = (status: ConnectionStatus) => { + console.log( + '๐Ÿ“ข notifySubscribers: Notifying', + subscribers.size, + 'subscribers of status change to:', + status, + ); + sharedConnectionStatus = status; + subscribers.forEach((callback, index) => { + console.log('๐Ÿ“ข notifySubscribers: Calling subscriber', index + 1); + callback(status); + }); + console.log('๐Ÿ“ข notifySubscribers: All subscribers notified'); +}; - 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 notifyStatusUpdate = () => { + console.log( + '๐Ÿ”„ notifyStatusUpdate: Notifying', + statusUpdateCallbacks.size, + 'status update callbacks', + ); + statusUpdateCallbacks.forEach((callback, index) => { + console.log('๐Ÿ”„ notifyStatusUpdate: Calling callback', index + 1); + callback(); + }); + console.log('๐Ÿ”„ notifyStatusUpdate: All callbacks executed'); +}; + +const cleanup = () => { + console.log('๐Ÿงน cleanup: Starting cleanup process'); + + if (reconnectTimeout) { + console.log('๐Ÿงน cleanup: Clearing reconnect timeout'); + clearTimeout(reconnectTimeout); + reconnectTimeout = undefined; + } + + if (sharedChannel) { + console.log('๐Ÿงน cleanup: Removing shared channel'); + supabase.removeChannel(sharedChannel).catch((error) => { + console.error('โŒ cleanup: Error removing shared channel:', error); + }); + sharedChannel = null; + } + + console.log('โœ… cleanup: Cleanup completed'); +}; + +const connect = () => { + console.log('๐Ÿ”Œ connect: Function called'); + console.log('๐Ÿ”Œ connect: sharedChannel exists:', !!sharedChannel); + console.log('๐Ÿ”Œ connect: subscribers count:', subscribers.size); + + if (sharedChannel) { + console.log('โŒ connect: Already connected or connecting, returning early'); + return; + } + + console.log('๐Ÿ”Œ connect: Starting connection process'); + cleanup(); + notifySubscribers('connecting'); + + console.log('๐Ÿ”Œ connect: Creating new channel'); + const channel = supabase + .channel('status_updates', { + config: { broadcast: { self: true } }, + }) + .on('broadcast', { event: 'status_updated' }, (payload) => { + console.log('๐Ÿ“ก connect: Broadcast event received:', payload); + notifyStatusUpdate(); + }) + .subscribe((status) => { + console.log('๐Ÿ“ก connect: Subscription status changed to:', status); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison + if (status === 'SUBSCRIBED') { + console.log('โœ… connect: Successfully subscribed to realtime'); + notifySubscribers('connected'); + reconnectAttempts = 0; + console.log('โœ… connect: Reset reconnect attempts to 0'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison + } else if (status === 'CHANNEL_ERROR' || status === 'CLOSED') { + console.log('โŒ connect: Channel error or closed, status:', status); + notifySubscribers('disconnected'); + + if (reconnectAttempts < 5) { + reconnectAttempts++; + const delay = 2000 * reconnectAttempts; + console.log( + '๐Ÿ”„ connect: Scheduling reconnection attempt', + reconnectAttempts, + 'in', + delay, + 'ms', + ); + + reconnectTimeout = setTimeout(() => { + console.log('๐Ÿ”„ connect: Reconnection timeout executed'); + if (subscribers.size > 0) { + console.log('๐Ÿ”„ connect: Calling connect() for reconnection'); + connect(); + } else { + console.log( + 'โŒ connect: No active subscribers, skipping reconnection', + ); + } + }, delay); + } else { + console.warn('โš ๏ธ connect: Max reconnection attempts (5) reached'); + } + } + }); + + sharedChannel = channel; + console.log('๐Ÿ”Œ connect: Channel stored in sharedChannel variable'); +}; + +const disconnect = () => { + console.log('๐Ÿ”Œ disconnect: Function called'); + cleanup(); + notifySubscribers('disconnected'); +}; + +export const useStatusSubscription = (onStatusUpdate?: () => void) => { + console.log('๐Ÿš€ useSharedStatusSubscription: Hook called'); + + const [connectionStatus, setConnectionStatus] = useState( + sharedConnectionStatus, + ); + const onStatusUpdateRef = useRef(onStatusUpdate); + const hasInitialized = useRef(false); + + // Keep the ref updated + onStatusUpdateRef.current = onStatusUpdate; + + // Create a stable callback + const stableOnStatusUpdate = useCallback(() => { + onStatusUpdateRef.current?.(); }, []); - 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]); + console.log('๐Ÿ”ง useSharedStatusSubscription useEffect: Running'); + console.log( + '๐Ÿ”ง useSharedStatusSubscription useEffect: hasInitialized:', + hasInitialized.current, + ); + console.log( + '๐Ÿ”ง useSharedStatusSubscription useEffect: Current subscribers count:', + subscribers.size, + ); - // Initial connection - SIMPLIFIED to avoid dependency issues - useEffect(() => { - if (!enabled) { - disconnect(); + // Prevent duplicate initialization + if (hasInitialized.current) { + console.log( + '๐Ÿ”ง useSharedStatusSubscription useEffect: Already initialized, skipping', + ); return; } - const initialTimeout = setTimeout(() => { - if (isComponentMountedRef.current) connect(); - }, 1000); + + hasInitialized.current = true; + + // Subscribe to status changes + subscribers.add(setConnectionStatus); + console.log( + '๐Ÿ”ง useSharedStatusSubscription useEffect: Added setConnectionStatus to subscribers', + ); + + // Subscribe to status updates + if (onStatusUpdate) { + statusUpdateCallbacks.add(stableOnStatusUpdate); + console.log( + '๐Ÿ”ง useSharedStatusSubscription useEffect: Added stable onStatusUpdate callback', + ); + } + + // Connect if this is the first subscriber + if (subscribers.size === 1) { + console.log( + '๐Ÿ”ง useSharedStatusSubscription useEffect: First subscriber, setting up connection', + ); + const timeout = setTimeout(() => { + console.log( + '๐Ÿ”ง useSharedStatusSubscription useEffect: Connection timeout executed, calling connect()', + ); + connect(); + }, 1000); + + return () => { + console.log( + '๐Ÿ”ง useSharedStatusSubscription useEffect: Cleanup - clearing connection timeout', + ); + clearTimeout(timeout); + hasInitialized.current = false; + subscribers.delete(setConnectionStatus); + statusUpdateCallbacks.delete(stableOnStatusUpdate); + + if (subscribers.size === 0) { + disconnect(); + } + }; + } return () => { - clearTimeout(initialTimeout); - }; - }, [enabled]); + console.log( + '๐Ÿ”ง useSharedStatusSubscription useEffect: Cleanup function running', + ); + hasInitialized.current = false; + subscribers.delete(setConnectionStatus); + statusUpdateCallbacks.delete(stableOnStatusUpdate); - // Cleanup on unmount - useEffect(() => { - return () => { - cleanup(); + if (subscribers.size === 0) { + console.log( + '๐Ÿ”ง useSharedStatusSubscription useEffect: No more subscribers, calling disconnect()', + ); + disconnect(); + } }; - }, [cleanup]); + }, []); // Empty dependency array! + + const reconnect = useCallback(() => { + console.log('๐Ÿ”„ reconnect: Function called'); + reconnectAttempts = 0; + console.log( + '๐Ÿ”„ reconnect: Reset reconnectAttempts to 0, calling connect()', + ); + connect(); + }, []); + + console.log( + '๐Ÿ useSharedStatusSubscription: connectionStatus:', + connectionStatus, + ); return { connectionStatus,