Cleaning up. Rewriting. Vibing. Coding. Little bit of vibe coding.

This commit is contained in:
2025-06-19 14:31:10 -05:00
parent 3dabc27d58
commit d0533b78e8
15 changed files with 697 additions and 412 deletions

View File

@ -31,7 +31,7 @@
"@supabase/ssr": "^0.6.1", "@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.50.0", "@supabase/supabase-js": "^2.50.0",
"@t3-oss/env-nextjs": "^0.12.0", "@t3-oss/env-nextjs": "^0.12.0",
"@tanstack/react-query": "^5.80.7", "@tanstack/react-query": "^5.80.10",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.510.0", "lucide-react": "^0.510.0",
@ -62,7 +62,7 @@
"import-in-the-middle": "^1.14.2", "import-in-the-middle": "^1.14.2",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.12", "prettier-plugin-tailwindcss": "^0.6.13",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.10", "tailwindcss": "^4.1.10",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",

44
pnpm-lock.yaml generated
View File

@ -51,8 +51,8 @@ importers:
specifier: ^0.12.0 specifier: ^0.12.0
version: 0.12.0(typescript@5.8.3)(zod@3.25.67) version: 0.12.0(typescript@5.8.3)(zod@3.25.67)
'@tanstack/react-query': '@tanstack/react-query':
specifier: ^5.80.7 specifier: ^5.80.10
version: 5.80.7(react@19.1.0) version: 5.80.10(react@19.1.0)
class-variance-authority: class-variance-authority:
specifier: ^0.7.1 specifier: ^0.7.1
version: 0.7.1 version: 0.7.1
@ -139,8 +139,8 @@ importers:
specifier: ^3.5.3 specifier: ^3.5.3
version: 3.5.3 version: 3.5.3
prettier-plugin-tailwindcss: prettier-plugin-tailwindcss:
specifier: ^0.6.12 specifier: ^0.6.13
version: 0.6.12(prettier@3.5.3) version: 0.6.13(prettier@3.5.3)
tailwind-merge: tailwind-merge:
specifier: ^3.3.1 specifier: ^3.3.1
version: 3.3.1 version: 3.3.1
@ -1504,11 +1504,11 @@ packages:
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0
'@tanstack/query-core@5.80.7': '@tanstack/query-core@5.80.10':
resolution: {integrity: sha512-s09l5zeUKC8q7DCCCIkVSns8zZrK4ZDT6ryEjxNBFi68G4z2EBobBS7rdOY3r6W1WbUDpc1fe5oY+YO/+2UVUg==} resolution: {integrity: sha512-mUNQOtzxkjL6jLbyChZoSBP6A5gQDVRUiPvW+/zw/9ftOAz+H754zCj3D8PwnzPKyHzGkQ9JbH48ukhym9LK1Q==}
'@tanstack/react-query@5.80.7': '@tanstack/react-query@5.80.10':
resolution: {integrity: sha512-u2F0VK6+anItoEvB3+rfvTO9GEh2vb00Je05OwlUe/A0lkJBgW1HckiY3f9YZa+jx6IOe4dHPh10dyp9aY3iRQ==} resolution: {integrity: sha512-6zM098J8sLy9oU60XAdzUlAH4wVzoMVsWUWiiE/Iz4fd67PplxeyL4sw/MPcVJJVhbwGGXCsHn9GrQt2mlAzig==}
peerDependencies: peerDependencies:
react: ^18 || ^19 react: ^18 || ^19
@ -2091,8 +2091,8 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
electron-to-chromium@1.5.170: electron-to-chromium@1.5.171:
resolution: {integrity: sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA==} resolution: {integrity: sha512-scWpzXEJEMrGJa4Y6m/tVotb0WuvNmasv3wWVzUAeCgKU0ToFOhUW6Z+xWnRQANMYGxN4ngJXIThgBJOqzVPCQ==}
emoji-regex@9.2.2: emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
@ -3023,8 +3023,8 @@ packages:
resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
prettier-plugin-tailwindcss@0.6.12: prettier-plugin-tailwindcss@0.6.13:
resolution: {integrity: sha512-OuTQKoqNwV7RnxTPwXWzOFXy6Jc4z8oeRZYGuMpRyG3WbuR3jjXdQFK8qFBMBx8UHWdHrddARz2fgUenild6aw==} resolution: {integrity: sha512-uQ0asli1+ic8xrrSmIOaElDu0FacR4x69GynTh2oZjFY10JUt6EEumTQl5tB4fMeD6I1naKd+4rXQQ7esT2i1g==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
peerDependencies: peerDependencies:
'@ianvs/prettier-plugin-sort-imports': '*' '@ianvs/prettier-plugin-sort-imports': '*'
@ -3410,8 +3410,8 @@ packages:
uglify-js: uglify-js:
optional: true optional: true
terser@5.43.0: terser@5.43.1:
resolution: {integrity: sha512-CqNNxKSGKSZCunSvwKLTs8u8sGGlp27sxNZ4quGh0QeNuyHM0JSEM/clM9Mf4zUp6J+tO2gUXhgXT2YMMkwfKQ==} resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==}
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
@ -4992,11 +4992,11 @@ snapshots:
- supports-color - supports-color
- typescript - 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: dependencies:
'@tanstack/query-core': 5.80.7 '@tanstack/query-core': 5.80.10
react: 19.1.0 react: 19.1.0
'@tybys/wasm-util@0.9.0': '@tybys/wasm-util@0.9.0':
@ -5491,7 +5491,7 @@ snapshots:
browserslist@4.25.0: browserslist@4.25.0:
dependencies: dependencies:
caniuse-lite: 1.0.30001723 caniuse-lite: 1.0.30001723
electron-to-chromium: 1.5.170 electron-to-chromium: 1.5.171
node-releases: 2.0.19 node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.0) update-browserslist-db: 1.1.3(browserslist@4.25.0)
@ -5652,7 +5652,7 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
gopd: 1.2.0 gopd: 1.2.0
electron-to-chromium@1.5.170: {} electron-to-chromium@1.5.171: {}
emoji-regex@9.2.2: {} emoji-regex@9.2.2: {}
@ -6679,7 +6679,7 @@ snapshots:
dependencies: dependencies:
fast-diff: 1.3.0 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: dependencies:
prettier: 3.5.3 prettier: 3.5.3
@ -7087,10 +7087,10 @@ snapshots:
jest-worker: 27.5.1 jest-worker: 27.5.1
schema-utils: 4.3.2 schema-utils: 4.3.2
serialize-javascript: 6.0.2 serialize-javascript: 6.0.2
terser: 5.43.0 terser: 5.43.1
webpack: 5.99.9 webpack: 5.99.9
terser@5.43.0: terser@5.43.1:
dependencies: dependencies:
'@jridgewell/source-map': 0.3.6 '@jridgewell/source-map': 0.3.6
acorn: 8.15.0 acorn: 8.15.0

View File

@ -390,8 +390,10 @@ const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
return ( return (
<html lang='en' className={`${geist.variable}`} suppressHydrationWarning> <html lang='en' className={`${geist.variable}`} suppressHydrationWarning>
<body <body
className={cn('bg-background text-foreground font-sans antialiased m-10\ className={cn(
leading-relaxed px-10')} 'bg-background text-foreground font-sans antialiased m-10\
leading-relaxed px-10',
)}
> >
<ThemeProvider <ThemeProvider
attribute='class' attribute='class'

View File

@ -9,10 +9,6 @@ export const generateMetadata = (): Metadata => {
const SignInLayout = ({ const SignInLayout = ({
children, children,
}: Readonly<{ children: React.ReactNode }>) => { }: Readonly<{ children: React.ReactNode }>) => {
return ( return <div className=''>{children}</div>;
<div className=''>
{children}
</div>
);
}; };
export default SignInLayout; export default SignInLayout;

View File

@ -48,7 +48,8 @@ const Header = () => {
height={100} height={100}
className='max-w-[40px] md:max-w-[120px]' className='max-w-[40px] md:max-w-[120px]'
/> />
<h1 className='title-text text-sm md:text-4xl lg:text-8xl <h1
className='title-text text-sm md:text-4xl lg:text-8xl
bg-gradient-to-r from-[#281A65] via-[#363354] to-accent-foreground bg-gradient-to-r from-[#281A65] via-[#363354] to-accent-foreground
dark:from-[#bec8e6] dark:via-[#F0EEE4] dark:to-[#FFF8E7] dark:from-[#bec8e6] dark:via-[#F0EEE4] dark:to-[#FFF8E7]
font-bold pl-2 md:pl-12 text-transparent bg-clip-text' font-bold pl-2 md:pl-12 text-transparent bg-clip-text'

View File

@ -8,7 +8,7 @@ type ConnectionStatusProps = {
onReconnect?: () => void; onReconnect?: () => void;
showAsButton?: boolean; showAsButton?: boolean;
className?: string; className?: string;
} };
const getConnectionIcon = (status: ConnectionStatusType) => { const getConnectionIcon = (status: ConnectionStatusType) => {
switch (status) { switch (status) {
@ -57,10 +57,7 @@ export const ConnectionStatus = ({
} }
return ( return (
<Badge <Badge variant='outline' className={`flex items-center gap-2 ${className}`}>
variant='outline'
className={`flex items-center gap-2 ${className}`}
>
{getConnectionIcon(status)} {getConnectionIcon(status)}
<span className='text-base'>{getConnectionText(status)}</span> <span className='text-base'>{getConnectionText(status)}</span>
</Badge> </Badge>

View File

@ -106,7 +106,9 @@ export const HistoryDrawer: React.FC<HistoryDrawerProps> = ({
className='w-8 h-8 md:w-12 md:h-12' className='w-8 h-8 md:w-12 md:h-12'
/> />
<h1 className='text-lg md:text-2xl lg:text-4xl font-bold pl-2 md:pl-4'> <h1 className='text-lg md:text-2xl lg:text-4xl font-bold pl-2 md:pl-4'>
{user && user.id !== '' ? `${user.full_name}'s History` : 'All History'} {user && user.id !== ''
? `${user.full_name}'s History`
: 'All History'}
</h1> </h1>
</div> </div>
{totalCount > 0 && ( {totalCount > 0 && (

View File

@ -12,12 +12,12 @@ import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { RefreshCw, Clock, Calendar } from 'lucide-react'; 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 { formatTime, formatDate } from '@/lib/utils';
import Link from 'next/link'; import Link from 'next/link';
type ListProps = { type ListProps = {
initialStatuses: UserWithStatus[] initialStatuses: UserWithStatus[];
}; };
export const StatusList = ({ initialStatuses = [] }: ListProps) => { export const StatusList = ({ initialStatuses = [] }: ListProps) => {
@ -44,7 +44,7 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
}); });
// In your StatusList component // In your StatusList component
const { connectionStatus, connect: reconnect } = useSharedStatusSubscription(() => { const { connectionStatus, connect: reconnect } = useStatusSubscription(() => {
refetch().catch((error) => { refetch().catch((error) => {
console.error('Error refetching statuses:', error); console.error('Error refetching statuses:', error);
}); });
@ -53,14 +53,14 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
const handleUpdateStatus = () => { const handleUpdateStatus = () => {
if (!isAuthenticated) { if (!isAuthenticated) {
setUpdateStatusMessage( setUpdateStatusMessage(
'Error: You must be signed in to update technician statuses!' 'Error: You must be signed in to update technician statuses!',
); );
return; return;
} }
if (statusInput.length < 3 || statusInput.length > 80) { if (statusInput.length < 3 || statusInput.length > 80) {
setUpdateStatusMessage( setUpdateStatusMessage(
'Error: Your status must be between 3 & 80 characters long!' 'Error: Your status must be between 3 & 80 characters long!',
); );
return; return;
} }
@ -79,7 +79,7 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
setSelectedUsers((prev) => setSelectedUsers((prev) =>
prev.some((u) => u.user.id === user.user.id) prev.some((u) => u.user.id === user.user.id)
? prev.filter((prevUser) => prevUser.user.id !== user.user.id) ? prev.filter((prevUser) => prevUser.user.id !== user.user.id)
: [...prev, user] : [...prev, user],
); );
}; };
@ -95,7 +95,7 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
useEffect(() => { useEffect(() => {
setSelectAll( setSelectAll(
selectedUsers.length === usersWithStatuses.length && selectedUsers.length === usersWithStatuses.length &&
usersWithStatuses.length > 0 usersWithStatuses.length > 0,
); );
}, [selectedUsers.length, usersWithStatuses.length]); }, [selectedUsers.length, usersWithStatuses.length]);
@ -174,7 +174,7 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
</Link> </Link>
</div> </div>
)} )}
</div> </div>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<ConnectionStatus <ConnectionStatus
status={connectionStatus} status={connectionStatus}
@ -187,7 +187,7 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
<div className={cardContainerClassName}> <div className={cardContainerClassName}>
{usersWithStatuses.map((userWithStatus) => { {usersWithStatuses.map((userWithStatus) => {
const isSelected = selectedUsers.some( const isSelected = selectedUsers.some(
(u) => u.user.id === userWithStatus.user.id (u) => u.user.id === userWithStatus.user.id,
); );
const isNewStatus = newStatuses.has(userWithStatus); const isNewStatus = newStatuses.has(userWithStatus);
const isUpdatedByOther = const isUpdatedByOther =
@ -329,8 +329,7 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
{selectedUsers.length > 0 {selectedUsers.length > 0
? `Update status for ${selectedUsers.length} ? `Update status for ${selectedUsers.length}
${selectedUsers.length > 1 ? 'users' : 'user'}` ${selectedUsers.length > 1 ? 'users' : 'user'}`
: 'Update status' : 'Update status'}
}
</SubmitButton> </SubmitButton>
</div> </div>
{updateStatusMessage && {updateStatusMessage &&
@ -341,7 +340,7 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
<StatusMessage message={{ error: updateStatusMessage }} /> <StatusMessage message={{ error: updateStatusMessage }} />
) : ( ) : (
<StatusMessage message={{ message: updateStatusMessage }} /> <StatusMessage message={{ message: updateStatusMessage }} />
))} ))}
</div> </div>
<div className='flex justify-center mt-2'> <div className='flex justify-center mt-2'>
<Drawer> <Drawer>

View File

@ -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<UserWithStatus[]>([]);
const [selectAll, setSelectAll] = useState(false);
const [statusInput, setStatusInput] = useState('');
const [selectedHistoryUser, setSelectedHistoryUser] =
useState<Profile | null>(null);
const [updateStatusMessage, setUpdateStatusMessage] = useState('');
const {
data: usersWithStatuses = initialStatuses,
isLoading: loading,
error,
refetch,
newStatuses,
updateStatusMutation,
} = useStatusData({
initialData: initialStatuses,
enabled: isAuthenticated,
});
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 (
<div className='flex justify-center items-center min-h-[400px]'>
<Loading className='w-full' alpha={0.5} />
</div>
);
}
if (error) {
return (
<div className='flex flex-col justify-center items-center min-h-[400px] gap-4'>
<p className='text-red-500'>Error loading status updates</p>
<Button onClick={() => refetch()} variant='outline'>
<RefreshCw className='w-4 h-4 mr-2' />
Retry
</Button>
</div>
);
}
const containerClassName = makeConditionalClassName({
context: tvMode,
defaultClassName: '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 (
<div className={containerClassName}>
<div className={headerClassName}>
<div className='flex items-center gap-6'>
<Button
variant='outline'
size='sm'
onClick={handleSelectAllChange}
className='flex items-center gap-2'
>
<CheckCircle2
className={`w-4 h-4 ${selectAll ? 'text-primary' : ''}`}
/>
{selectAll ? 'Deselect All' : 'Select All'}
</Button>
{!tvMode && (
<div className='flex items-center gap-2 text-sm text-muted-foreground'>
<span>Miss the old table?</span>
<Link
href='/status/table'
className='font-medium text-primary hover:underline'
>
Find it here!
</Link>
</div>
)}
</div>
<div className='flex items-center gap-2'>
<ConnectionStatus
status={connectionStatus}
onReconnect={reconnect}
showAsButton={connectionStatus === 'disconnected'}
/>
</div>
</div>
<div className={cardContainerClassName}>
{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 (
<Card
key={userWithStatus.user.id}
className={`
relative transition-all duration-200 cursor-pointer hover:shadow-md
${tvMode ? 'p-4' : 'p-3'}
${isSelected ? 'ring-2 ring-primary bg-primary/5 shadow-md' : 'hover:bg-muted/30'}
${isNewStatus ? 'animate-in slide-in-from-top-2 duration-500 bg-green-50 border-green-200' : ''}
`}
onClick={(e) => handleCardSelect(userWithStatus, e)}
>
{isSelected && (
<div className='absolute top-2 right-2 text-primary'>
<CheckCircle2
className={`${tvMode ? 'w-6 h-6' : 'w-5 h-5'}`}
/>
</div>
)}
<CardContent className='p-0'>
<div className='flex items-start gap-3'>
{/* Profile Section - Clickable for history */}
<Drawer>
<DrawerTrigger asChild>
<div
data-profile-trigger
className='flex-shrink-0 cursor-pointer hover:opacity-80 transition-opacity'
onClick={() =>
setSelectedHistoryUser(userWithStatus.user)
}
>
<BasedAvatar
src={userWithStatus.user.avatar_url}
fullName={userWithStatus.user.full_name}
className={tvMode ? 'w-16 h-16' : 'w-12 h-12'}
/>
</div>
</DrawerTrigger>
{selectedHistoryUser === userWithStatus.user && (
<HistoryDrawer user={selectedHistoryUser} />
)}
</Drawer>
{/* Content Section */}
<div className='flex-1 min-w-0'>
{/* Header with name and timestamp */}
<div className='flex items-center justify-between mb-2'>
<Drawer>
<DrawerTrigger asChild>
<h3
data-profile-trigger
className={`
font-semibold cursor-pointer hover:underline truncate
${tvMode ? 'text-2xl' : 'text-base'}
`}
onClick={() =>
setSelectedHistoryUser(userWithStatus.user)
}
>
{userWithStatus.user.full_name}
</h3>
</DrawerTrigger>
{selectedHistoryUser === userWithStatus.user && (
<HistoryDrawer user={selectedHistoryUser} />
)}
</Drawer>
<div className='flex items-center gap-2 text-muted-foreground flex-shrink-0'>
<Clock
className={`${tvMode ? 'w-5 h-5' : 'w-4 h-4'}`}
/>
<span className={`${tvMode ? 'text-lg' : 'text-sm'}`}>
{formatTime(userWithStatus.created_at)}
</span>
</div>
</div>
{/* Status Content */}
<div
className={`
mb-2 leading-relaxed
${tvMode ? 'text-xl' : 'text-sm'}
`}
>
<p>{userWithStatus.status}</p>
</div>
{/* Footer with date and updated by info */}
<div className='flex items-center justify-between text-muted-foreground'>
<div className='flex items-center gap-2'>
<Calendar
className={`${tvMode ? 'w-4 h-4' : 'w-3 h-3'}`}
/>
<span className={`${tvMode ? 'text-base' : 'text-xs'}`}>
{formatDate(userWithStatus.created_at)}
</span>
</div>
{isUpdatedByOther && (
<div className='flex items-center gap-1'>
<BasedAvatar
src={userWithStatus.updated_by?.avatar_url}
fullName={userWithStatus.updated_by?.full_name}
className={`${tvMode ? 'w-5 h-5' : 'w-4 h-4'}`}
/>
<span
className={`${tvMode ? 'text-base' : 'text-xs'}`}
>
Updated by {userWithStatus.updated_by?.full_name}
</span>
</div>
)}
</div>
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
{usersWithStatuses.length === 0 && (
<Card className='p-8 text-center'>
<p
className={`text-muted-foreground ${tvMode ? 'text-2xl' : 'text-base'}`}
>
No status updates have been made in the past day.
</p>
</Card>
)}
{!tvMode && (
<Card className='p-6 mt-6 w-full'>
<div className='flex flex-col gap-4'>
<h3 className='text-lg font-semibold'>Update Status</h3>
<div className='flex flex-col gap-4'>
<div className='flex gap-4'>
<Input
autoFocus
type='text'
placeholder='Enter status update...'
className='flex-1 text-base'
value={statusInput}
disabled={updateStatusMutation.isPending}
onChange={(e) => setStatusInput(e.target.value)}
onKeyDown={(e) => {
if (
e.key === 'Enter' &&
!e.shiftKey &&
!updateStatusMutation.isPending
) {
e.preventDefault();
handleUpdateStatus();
}
}}
/>
<SubmitButton
onClick={handleUpdateStatus}
disabled={updateStatusMutation.isPending}
className='px-6'
>
{selectedUsers.length > 0
? `Update ${selectedUsers.length} ${selectedUsers.length > 1 ? 'users' : 'user'}`
: 'Update Status'}
</SubmitButton>
</div>
{updateStatusMessage &&
(updateStatusMessage.includes('Error') ||
updateStatusMessage.includes('error') ||
updateStatusMessage.includes('failed') ||
updateStatusMessage.includes('invalid') ? (
<StatusMessage message={{ error: updateStatusMessage }} />
) : (
<StatusMessage message={{ message: updateStatusMessage }} />
))}
</div>
<div className='flex justify-center mt-2'>
<Drawer>
<DrawerTrigger asChild>
<Button
variant='outline'
className={tvMode ? 'text-xl p-6' : ''}
>
View All Status History
</Button>
</DrawerTrigger>
<HistoryDrawer />
</Drawer>
</div>
</div>
</Card>
)}
</div>
);
};

View File

@ -10,11 +10,10 @@ import { makeConditionalClassName } from '@/lib/utils';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { RefreshCw, Clock, Calendar } from 'lucide-react'; 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 { formatTime, formatDate } from '@/lib/utils';
import Link from 'next/link'; import Link from 'next/link';
type TableProps = { type TableProps = {
initialStatuses: UserWithStatus[]; initialStatuses: UserWithStatus[];
}; };
@ -42,31 +41,31 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => {
enabled: isAuthenticated, enabled: isAuthenticated,
}); });
// In your StatusList component // In your StatusList component
const { connectionStatus, connect: reconnect } = useSharedStatusSubscription(() => { const { connectionStatus, connect: reconnect } = useStatusSubscription(() => {
refetch().catch((error) => { refetch().catch((error) => {
console.error('Error refetching statuses:', error); console.error('Error refetching statuses:', error);
}); });
}); });
//const { connectionStatus, connect: reconnect } = useStatusSubscription({ //const { connectionStatus, connect: reconnect } = useStatusSubscription({
//enabled: isAuthenticated, //enabled: isAuthenticated,
//onStatusUpdate: () => { //onStatusUpdate: () => {
//refetch().catch((error) => { //refetch().catch((error) => {
//console.error('Error refetching statuses:', error); //console.error('Error refetching statuses:', error);
//}); //});
//}, //},
//}); //});
const handleUpdateStatus = () => { const handleUpdateStatus = () => {
if (!isAuthenticated) { if (!isAuthenticated) {
setUpdateStatusMessage( setUpdateStatusMessage(
'Error: You must be signed in to update technician statuses!' 'Error: You must be signed in to update technician statuses!',
); );
return; return;
} }
if (statusInput.length < 3 || statusInput.length > 80) { if (statusInput.length < 3 || statusInput.length > 80) {
setUpdateStatusMessage( setUpdateStatusMessage(
'Error: Your status must be between 3 & 80 characters long!' 'Error: Your status must be between 3 & 80 characters long!',
); );
return; return;
} }
@ -85,7 +84,7 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => {
setSelectedUsers((prev) => setSelectedUsers((prev) =>
prev.some((u) => u.user.id === user.user.id) prev.some((u) => u.user.id === user.user.id)
? prev.filter((prevUser) => prevUser.user.id !== user.user.id) ? prev.filter((prevUser) => prevUser.user.id !== user.user.id)
: [...prev, user] : [...prev, user],
); );
}; };
@ -101,7 +100,7 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => {
useEffect(() => { useEffect(() => {
setSelectAll( setSelectAll(
selectedUsers.length === usersWithStatuses.length && selectedUsers.length === usersWithStatuses.length &&
usersWithStatuses.length > 0 usersWithStatuses.length > 0,
); );
}, [selectedUsers.length, usersWithStatuses.length]); }, [selectedUsers.length, usersWithStatuses.length]);
@ -163,7 +162,7 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => {
/> />
{!tvMode && ( {!tvMode && (
<div className='flex flex-row gap-2'> <div className='flex flex-row gap-2'>
<p>Tired of the old table? {' '}</p> <p>Tired of the old table? </p>
<Link <Link
href='/status/list' href='/status/list'
className='italic font-semibold text-accent-foreground' className='italic font-semibold text-accent-foreground'
@ -249,9 +248,7 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => {
fullName={userWithStatus.updated_by?.full_name} fullName={userWithStatus.updated_by?.full_name}
className='w-5 h-5' className='w-5 h-5'
/> />
<span <span className={tvMode ? 'text-lg' : 'text-base'}>
className={tvMode ? 'text-lg' : 'text-base'}
>
Updated by {userWithStatus.updated_by.full_name} Updated by {userWithStatus.updated_by.full_name}
</span> </span>
</div> </div>
@ -279,7 +276,9 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => {
<div className='flex items-start xl:w-1/6'></div> <div className='flex items-start xl:w-1/6'></div>
<div className='flex flex-col my-auto items-start'> <div className='flex flex-col my-auto items-start'>
<div className='flex gap-4 my-1'> <div className='flex gap-4 my-1'>
<Clock className={`${tvMode ? 'lg:w-11 lg:h-11' : 'lg:w-9 lg:h-9'}`} /> <Clock
className={`${tvMode ? 'lg:w-11 lg:h-11' : 'lg:w-9 lg:h-9'}`}
/>
{formatTime(userWithStatus.created_at)} {formatTime(userWithStatus.created_at)}
</div> </div>
<div className='flex gap-4 my-1'> <div className='flex gap-4 my-1'>
@ -315,7 +314,7 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => {
<StatusMessage message={{ error: updateStatusMessage }} /> <StatusMessage message={{ error: updateStatusMessage }} />
) : ( ) : (
<StatusMessage message={{ message: updateStatusMessage }} /> <StatusMessage message={{ message: updateStatusMessage }} />
))} ))}
{!tvMode && ( {!tvMode && (
<div className='mx-auto flex flex-row items-center justify-center py-5 gap-4'> <div className='mx-auto flex flex-row items-center justify-center py-5 gap-4'>
@ -352,8 +351,7 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => {
{selectedUsers.length > 0 {selectedUsers.length > 0
? `Update status for ${selectedUsers.length} ? `Update status for ${selectedUsers.length}
${selectedUsers.length > 1 ? 'users' : 'user'}` ${selectedUsers.length > 1 ? 'users' : 'user'}`
: 'Update status' : 'Update status'}
}
</SubmitButton> </SubmitButton>
</div> </div>
)} )}
@ -363,7 +361,10 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => {
<div className='flex justify-center mt-6'> <div className='flex justify-center mt-6'>
<Drawer> <Drawer>
<DrawerTrigger asChild> <DrawerTrigger asChild>
<Button variant='outline' className={tvMode ? 'text-3xl p-6' : ''}> <Button
variant='outline'
className={tvMode ? 'text-3xl p-6' : ''}
>
View All Status History View All Status History
</Button> </Button>
</DrawerTrigger> </DrawerTrigger>
@ -374,4 +375,3 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => {
</div> </div>
); );
}; };

View File

@ -1,4 +1,5 @@
export * from './ConnectionStatus'; export * from './ConnectionStatus';
export * from './HistoryDrawer'; export * from './HistoryDrawer';
export * from './List'; //export * from './List';
export * from './StatusList';
export * from './Table'; export * from './Table';

View File

@ -3,7 +3,7 @@ export * from './public';
export * from './status'; export * from './status';
export * from './storage'; export * from './storage';
export * from './useFileUpload'; export * from './useFileUpload';
export * from './useSharedStatusSubscription'; export * from './useStatusSubscription';
export * from './useStatusData'; export * from './useStatusData';
export type Result<T> = export type Result<T> =

View File

@ -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<ConnectionStatus>(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,
};
};

View File

@ -13,15 +13,15 @@ import { toast } from 'sonner';
type UseStatusDataOptions = { type UseStatusDataOptions = {
initialData?: UserWithStatus[]; initialData?: UserWithStatus[];
enabled?: boolean; enabled?: boolean;
} };
export const useStatusData = ({ export const useStatusData = ({
initialData = [], initialData = [],
enabled = true enabled = true,
}: UseStatusDataOptions = {}) => { }: UseStatusDataOptions = {}) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [newStatuses, setNewStatuses] = useState<Set<UserWithStatus>>( const [newStatuses, setNewStatuses] = useState<Set<UserWithStatus>>(
new Set() new Set(),
); );
const query = useQuery({ const query = useQuery({
@ -79,7 +79,7 @@ export const useStatusData = ({
const optimisticData = previousData.map((userStatus) => { const optimisticData = previousData.map((userStatus) => {
if ( if (
usersWithStatuses.some( usersWithStatuses.some(
(selected) => selected.user.id === userStatus.user.id (selected) => selected.user.id === userStatus.user.id,
) )
) { ) {
return { ...userStatus, status, created_at: now }; return { ...userStatus, status, created_at: now };
@ -94,7 +94,7 @@ export const useStatusData = ({
setNewStatuses((prev) => { setNewStatuses((prev) => {
const updated = new Set(prev); const updated = new Set(prev);
usersWithStatuses.forEach((updatedStatus) => usersWithStatuses.forEach((updatedStatus) =>
updated.delete(updatedStatus) updated.delete(updatedStatus),
); );
return updated; return updated;
}); });
@ -112,7 +112,7 @@ export const useStatusData = ({
data.forEach((statusUpdate) => { data.forEach((statusUpdate) => {
toast.success( toast.success(
`${statusUpdate.user.full_name}'s status updated to '${statusUpdate.status}'.` `${statusUpdate.user.full_name}'s status updated to '${statusUpdate.status}'.`,
); );
}); });
}, },

View File

@ -1,5 +1,5 @@
'use client'; 'use client';
import { useState, useEffect, useRef, useCallback } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { createClient } from '@/utils/supabase'; import { createClient } from '@/utils/supabase';
import type { RealtimeChannel } from '@supabase/supabase-js'; import type { RealtimeChannel } from '@supabase/supabase-js';
@ -9,135 +9,247 @@ export type ConnectionStatus =
| 'disconnected' | 'disconnected'
| 'updating'; | 'updating';
type UseStatusSubscriptionOptions = { // Singleton state
enabled?: boolean; let sharedChannel: RealtimeChannel | null = null;
onStatusUpdate?: () => void; let sharedConnectionStatus: ConnectionStatus = 'disconnected';
maxReconnectAttempts?: number; const subscribers = new Set<(status: ConnectionStatus) => void>();
reconnectDelay?: number; const statusUpdateCallbacks = new Set<() => void>();
} let reconnectAttempts = 0;
let reconnectTimeout: NodeJS.Timeout | undefined;
const supabase = createClient();
export const useStatusSubscription = ({ const notifySubscribers = (status: ConnectionStatus) => {
enabled = true, console.log(
onStatusUpdate, '📢 notifySubscribers: Notifying',
maxReconnectAttempts = 5, subscribers.size,
reconnectDelay = 2000, 'subscribers of status change to:',
}: UseStatusSubscriptionOptions = {}) => { status,
const [connectionStatus, setConnectionStatus] = );
useState<ConnectionStatus>('disconnected'); sharedConnectionStatus = status;
const channelRef = useRef<RealtimeChannel | null>(null); subscribers.forEach((callback, index) => {
const supabaseRef = useRef(createClient()); console.log('📢 notifySubscribers: Calling subscriber', index + 1);
const reconnectAttemptsRef = useRef(0); callback(status);
const reconnectTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined); });
const isComponentMountedRef = useRef(true); console.log('📢 notifySubscribers: All subscribers notified');
const visibilityTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined); };
const cleanup = useCallback(() => { const notifyStatusUpdate = () => {
if (reconnectTimeoutRef.current) { console.log(
clearTimeout(reconnectTimeoutRef.current); '🔄 notifyStatusUpdate: Notifying',
reconnectTimeoutRef.current = undefined; statusUpdateCallbacks.size,
} 'status update callbacks',
if (visibilityTimeoutRef.current) { );
clearTimeout(visibilityTimeoutRef.current); statusUpdateCallbacks.forEach((callback, index) => {
visibilityTimeoutRef.current = undefined; console.log('🔄 notifyStatusUpdate: Calling callback', index + 1);
} callback();
if (channelRef.current) { });
supabaseRef.current.removeChannel(channelRef.current).catch((error) => { console.log('🔄 notifyStatusUpdate: All callbacks executed');
console.error('❌ cleanup: Error removing channel:', error); };
});
channelRef.current = null; 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<ConnectionStatus>(
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(() => { useEffect(() => {
const handleVisibilityChange = () => { console.log('🔧 useSharedStatusSubscription useEffect: Running');
if (!enabled) return; console.log(
if (document.visibilityState === 'visible') { '🔧 useSharedStatusSubscription useEffect: hasInitialized:',
visibilityTimeoutRef.current = setTimeout(() => { hasInitialized.current,
if (connectionStatus === 'disconnected' && isComponentMountedRef.current) { );
reconnect(); console.log(
} '🔧 useSharedStatusSubscription useEffect: Current subscribers count:',
}, 1000); subscribers.size,
} );
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [enabled, connectionStatus, reconnect]);
// Initial connection - SIMPLIFIED to avoid dependency issues // Prevent duplicate initialization
useEffect(() => { if (hasInitialized.current) {
if (!enabled) { console.log(
disconnect(); '🔧 useSharedStatusSubscription useEffect: Already initialized, skipping',
);
return; return;
} }
const initialTimeout = setTimeout(() => {
if (isComponentMountedRef.current) connect(); hasInitialized.current = true;
}, 1000);
// 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 () => { return () => {
clearTimeout(initialTimeout); console.log(
}; '🔧 useSharedStatusSubscription useEffect: Cleanup function running',
}, [enabled]); );
hasInitialized.current = false;
subscribers.delete(setConnectionStatus);
statusUpdateCallbacks.delete(stableOnStatusUpdate);
// Cleanup on unmount if (subscribers.size === 0) {
useEffect(() => { console.log(
return () => { '🔧 useSharedStatusSubscription useEffect: No more subscribers, calling disconnect()',
cleanup(); );
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 { return {
connectionStatus, connectionStatus,