Cleaning up. Rewriting. Vibing. Coding. Little bit of vibe coding.
This commit is contained in:
@ -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
44
pnpm-lock.yaml
generated
@ -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
|
||||||
|
@ -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'
|
||||||
|
@ -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;
|
||||||
|
@ -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'
|
||||||
|
@ -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>
|
||||||
|
@ -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 && (
|
||||||
|
@ -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>
|
||||||
|
388
src/components/status/StatusList.tsx
Normal file
388
src/components/status/StatusList.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
@ -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> =
|
||||||
|
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
@ -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}'.`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -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,
|
||||||
|
Reference in New Issue
Block a user