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,