Finding a stopping point :(
This commit is contained in:
@ -40,7 +40,7 @@
|
|||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-hook-form": "^7.57.0",
|
"react-hook-form": "^7.58.0",
|
||||||
"require-in-the-middle": "^7.5.2",
|
"require-in-the-middle": "^7.5.2",
|
||||||
"sonner": "^2.0.5",
|
"sonner": "^2.0.5",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
|
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@ -10,7 +10,7 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^5.1.1
|
specifier: ^5.1.1
|
||||||
version: 5.1.1(react-hook-form@7.57.0(react@19.1.0))
|
version: 5.1.1(react-hook-form@7.58.0(react@19.1.0))
|
||||||
'@radix-ui/react-avatar':
|
'@radix-ui/react-avatar':
|
||||||
specifier: ^1.1.10
|
specifier: ^1.1.10
|
||||||
version: 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
@ -78,8 +78,8 @@ importers:
|
|||||||
specifier: ^19.1.0
|
specifier: ^19.1.0
|
||||||
version: 19.1.0(react@19.1.0)
|
version: 19.1.0(react@19.1.0)
|
||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: ^7.57.0
|
specifier: ^7.58.0
|
||||||
version: 7.57.0(react@19.1.0)
|
version: 7.58.0(react@19.1.0)
|
||||||
require-in-the-middle:
|
require-in-the-middle:
|
||||||
specifier: ^7.5.2
|
specifier: ^7.5.2
|
||||||
version: 7.5.2
|
version: 7.5.2
|
||||||
@ -3108,8 +3108,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.1.0
|
react: ^19.1.0
|
||||||
|
|
||||||
react-hook-form@7.57.0:
|
react-hook-form@7.58.0:
|
||||||
resolution: {integrity: sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg==}
|
resolution: {integrity: sha512-zGijmEed35oNfOfy7ub99jfjkiLhHwA3dl5AgyKdWC6QQzhnc7tkWewSa+T+A2EpLrc6wo5DUoZctS9kufWJjA==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||||
@ -3804,10 +3804,10 @@ snapshots:
|
|||||||
|
|
||||||
'@floating-ui/utils@0.2.9': {}
|
'@floating-ui/utils@0.2.9': {}
|
||||||
|
|
||||||
'@hookform/resolvers@5.1.1(react-hook-form@7.57.0(react@19.1.0))':
|
'@hookform/resolvers@5.1.1(react-hook-form@7.58.0(react@19.1.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@standard-schema/utils': 0.3.0
|
'@standard-schema/utils': 0.3.0
|
||||||
react-hook-form: 7.57.0(react@19.1.0)
|
react-hook-form: 7.58.0(react@19.1.0)
|
||||||
|
|
||||||
'@humanfs/core@0.19.1': {}
|
'@humanfs/core@0.19.1': {}
|
||||||
|
|
||||||
@ -6708,7 +6708,7 @@ snapshots:
|
|||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
scheduler: 0.26.0
|
scheduler: 0.26.0
|
||||||
|
|
||||||
react-hook-form@7.57.0(react@19.1.0):
|
react-hook-form@7.58.0(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { Geist } from 'next/font/google';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import {
|
import {
|
||||||
AuthProvider,
|
AuthProvider,
|
||||||
|
QueryProvider,
|
||||||
ThemeProvider,
|
ThemeProvider,
|
||||||
TVModeProvider,
|
TVModeProvider,
|
||||||
} from '@/components/context';
|
} from '@/components/context';
|
||||||
@ -385,6 +386,7 @@ const geist = Geist({
|
|||||||
variable: '--font-geist-sans',
|
variable: '--font-geist-sans',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||||
return (
|
return (
|
||||||
<html lang='en' className={`${geist.variable}`} suppressHydrationWarning>
|
<html lang='en' className={`${geist.variable}`} suppressHydrationWarning>
|
||||||
@ -397,23 +399,25 @@ const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
|||||||
enableSystem
|
enableSystem
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<AuthProvider>
|
<QueryProvider>
|
||||||
<PlausibleProvider
|
<AuthProvider>
|
||||||
domain='techtracker.gbrown.org'
|
<PlausibleProvider
|
||||||
customDomain='https://plausible.gbrown.org'
|
domain='techtracker.gbrown.org'
|
||||||
trackOutboundLinks={true}
|
customDomain='https://plausible.gbrown.org'
|
||||||
selfHosted={true}
|
trackOutboundLinks={true}
|
||||||
>
|
selfHosted={true}
|
||||||
<TVModeProvider>
|
>
|
||||||
<main className='min-h-screen'>
|
<TVModeProvider>
|
||||||
<Header />
|
<main className='min-h-screen'>
|
||||||
{children}
|
<Header />
|
||||||
<Toaster />
|
{children}
|
||||||
</main>
|
<Toaster />
|
||||||
<Footer />
|
</main>
|
||||||
</TVModeProvider>
|
<Footer />
|
||||||
</PlausibleProvider>
|
</TVModeProvider>
|
||||||
</AuthProvider>
|
</PlausibleProvider>
|
||||||
|
</AuthProvider>
|
||||||
|
</QueryProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -8,22 +8,7 @@ const Home = async () => {
|
|||||||
if (!userResponse.success) {
|
if (!userResponse.success) {
|
||||||
redirect('/sign-in');
|
redirect('/sign-in');
|
||||||
} else if (userResponse.data) {
|
} else if (userResponse.data) {
|
||||||
redirect('/status');
|
redirect('/status/list');
|
||||||
} else return <div />;
|
} else return <div />;
|
||||||
};
|
};
|
||||||
export default Home;
|
export default Home;
|
||||||
|
|
||||||
//'use client';
|
|
||||||
|
|
||||||
////import { TechTable } from '@/components/status';
|
|
||||||
//import { redirect } from 'next/navigation';
|
|
||||||
//import { useAuth } from '@/components/context';
|
|
||||||
|
|
||||||
//const HomePage = () => {
|
|
||||||
//const { isAuthenticated } = useAuth();
|
|
||||||
//if (!isAuthenticated) {
|
|
||||||
//redirect('/sign-in');
|
|
||||||
//}
|
|
||||||
//redirect('/profile');
|
|
||||||
//};
|
|
||||||
//export default HomePage;
|
|
||||||
|
16
src/app/status/list/layout.tsx
Normal file
16
src/app/status/list/layout.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const generateMetadata = (): Metadata => {
|
||||||
|
return {
|
||||||
|
title: 'Status List'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const SignInLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default SignInLayout;
|
@ -1,6 +1,6 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { StatusList, TechTable } from '@/components/status';
|
import { StatusList } from '@/components/status';
|
||||||
import { getUser, getRecentUsersWithStatuses } from '@/lib/actions';
|
import { getUser, getRecentUsersWithStatuses } from '@/lib/actions';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
@ -12,7 +12,6 @@ const Status = async () => {
|
|||||||
const response = await getRecentUsersWithStatuses();
|
const response = await getRecentUsersWithStatuses();
|
||||||
if (!response.success) throw new Error(response.error);
|
if (!response.success) throw new Error(response.error);
|
||||||
const usersWithStatuses = response.data;
|
const usersWithStatuses = response.data;
|
||||||
//return <TechTable initialStatuses={usersWithStatuses} />;
|
|
||||||
return <StatusList initialStatuses={usersWithStatuses} />;
|
return <StatusList initialStatuses={usersWithStatuses} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
export const generateMetadata = (): Metadata => {
|
export const generateMetadata = (): Metadata => {
|
||||||
@ -6,11 +7,11 @@ export const generateMetadata = (): Metadata => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatusLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
const SignInLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default StatusLayout;
|
export default SignInLayout;
|
18
src/app/status/table/page.tsx
Normal file
18
src/app/status/table/page.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { TechTable } from '@/components/status';
|
||||||
|
import { getUser, getRecentUsersWithStatuses } from '@/lib/actions';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
const Status = async () => {
|
||||||
|
const userRespoonse = await getUser();
|
||||||
|
if (!userRespoonse.success) {
|
||||||
|
redirect('/sign-in');
|
||||||
|
} else {
|
||||||
|
const response = await getRecentUsersWithStatuses();
|
||||||
|
if (!response.success) throw new Error(response.error);
|
||||||
|
const usersWithStatuses = response.data;
|
||||||
|
return <TechTable initialStatuses={usersWithStatuses} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default Status;
|
77
src/components/context/Query.tsx
Normal file
77
src/components/context/Query.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// src/components/providers/query-provider.tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { QueryClient, QueryClientProvider, QueryCache, MutationCache } from '@tanstack/react-query';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
// Define error codes for different types of errors
|
||||||
|
export const enum QueryErrorCodes {
|
||||||
|
USERS_FETCH_FAILED = 'USERS_FETCH_FAILED',
|
||||||
|
STATUS_UPDATE_FAILED = 'STATUS_UPDATE_FAILED',
|
||||||
|
// Add more as needed
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryCacheOnError = (error: unknown, query: any) => {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Something went wrong';
|
||||||
|
|
||||||
|
switch (query.meta?.errCode) {
|
||||||
|
case QueryErrorCodes.USERS_FETCH_FAILED:
|
||||||
|
// Don't show toast for user fetch errors - handle in component
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Only show generic errors for unexpected failures
|
||||||
|
console.error('Query error:', error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mutationCacheOnError = (error: unknown, variables: unknown, context: unknown, mutation: any) => {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Something went wrong';
|
||||||
|
|
||||||
|
switch (mutation.meta?.errCode) {
|
||||||
|
case QueryErrorCodes.STATUS_UPDATE_FAILED:
|
||||||
|
toast.error(`Failed to update status: ${errorMessage}`);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
toast.error(`Operation failed: ${errorMessage}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type QueryProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QueryProvider = ({ children }: QueryProviderProps) => {
|
||||||
|
const [queryClient] = useState(() => new QueryClient({
|
||||||
|
queryCache: new QueryCache({
|
||||||
|
onError: queryCacheOnError,
|
||||||
|
}),
|
||||||
|
mutationCache: new MutationCache({
|
||||||
|
onError: mutationCacheOnError,
|
||||||
|
}),
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 30 * 1000, // 30 seconds
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
retry: (failureCount, error) => {
|
||||||
|
// Don't retry on 4xx errors
|
||||||
|
if (error instanceof Error && error.message.includes('4')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return failureCount < 3;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
retry: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
{children}
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -1,3 +1,4 @@
|
|||||||
export { AuthProvider, useAuth } from './Auth';
|
export { AuthProvider, useAuth } from './Auth';
|
||||||
export { ThemeProvider, ThemeToggle } from './Theme';
|
export { ThemeProvider, ThemeToggle } from './Theme';
|
||||||
export { TVModeProvider, useTVMode, TVToggle } from './TVMode';
|
export { TVModeProvider, useTVMode, TVToggle } from './TVMode';
|
||||||
|
export * from './Query';
|
||||||
|
@ -48,7 +48,7 @@ export const SignInWithMicrosoft = ({
|
|||||||
if (!updateResponse.success) throw new Error('Could not update provider!');
|
if (!updateResponse.success) throw new Error('Could not update provider!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.location.href = result.data.url + `?provider=${result.data.provider}`;
|
window.location.href = result.data.url;
|
||||||
} else {
|
} else {
|
||||||
setStatusMessage(`Error: Could not sign in with Microsoft!`);
|
setStatusMessage(`Error: Could not sign in with Microsoft!`);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
import { useState, useEffect, useRef } from "react"
|
||||||
import { createClient } from "@/utils/supabase"
|
|
||||||
import { useState, useEffect, useCallback, useRef } from "react"
|
|
||||||
import { useAuth, useTVMode } from "@/components/context"
|
import { useAuth, useTVMode } from "@/components/context"
|
||||||
import { getRecentUsersWithStatuses, updateStatuses, updateUserStatus, type UserWithStatus } from "@/lib/hooks"
|
import { getRecentUsersWithStatuses, updateStatuses, updateUserStatus, type UserWithStatus } from "@/lib/hooks"
|
||||||
import { BasedAvatar, Drawer, DrawerTrigger, Loading } from "@/components/ui"
|
import { BasedAvatar, Drawer, DrawerTrigger, Loading } from "@/components/ui"
|
||||||
@ -9,249 +7,265 @@ import { SubmitButton } from "@/components/default"
|
|||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { HistoryDrawer } from "@/components/status"
|
import { HistoryDrawer } from "@/components/status"
|
||||||
import type { Profile } from "@/utils/supabase"
|
import type { Profile } from "@/utils/supabase"
|
||||||
import type { RealtimeChannel } from "@supabase/supabase-js"
|
|
||||||
import { makeConditionalClassName } from "@/lib/utils"
|
import { makeConditionalClassName } from "@/lib/utils"
|
||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader } from "@/components/ui/card"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
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 { Wifi, WifiOff, Clock, User } from "lucide-react"
|
import { RefreshCw, Clock, Wifi, WifiOff } from "lucide-react"
|
||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { QueryErrorCodes } from "@/components/context";
|
||||||
|
import { createClient } from "@/utils/supabase";
|
||||||
|
import type { RealtimeChannel } from '@supabase/supabase-js';
|
||||||
|
|
||||||
type StatusListProps = {
|
type StatusListProps = {
|
||||||
initialStatuses: UserWithStatus[]
|
initialStatuses: UserWithStatus[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StatusList = ({ initialStatuses = [] }: StatusListProps) => {
|
export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fixed props destructuring
|
||||||
const { isAuthenticated } = useAuth()
|
const { isAuthenticated } = useAuth();
|
||||||
const { tvMode } = useTVMode()
|
const { tvMode } = useTVMode();
|
||||||
const [loading, setLoading] = useState(true)
|
const queryClient = useQueryClient();
|
||||||
const [selectedUsers, setSelectedUsers] = useState<string[]>([])
|
|
||||||
const [selectAll, setSelectAll] = useState(false)
|
const [selectedUsers, setSelectedUsers] = useState<UserWithStatus[]>([]);
|
||||||
const [updatingStatus, setUpdatingStatus] = useState(false)
|
const [selectAll, setSelectAll] = useState(false);
|
||||||
const [statusInput, setStatusInput] = useState("")
|
const [statusInput, setStatusInput] = useState('');
|
||||||
const [usersWithStatuses, setUsersWithStatuses] = useState<UserWithStatus[]>(initialStatuses)
|
const [selectedHistoryUser, setSelectedHistoryUser] = useState<Profile | null>(null);
|
||||||
const [selectedHistoryUser, setSelectedHistoryUser] = useState<Profile | null>(null)
|
|
||||||
const [connectionStatus, setConnectionStatus] = useState<"connecting" | "connected" | "disconnected">("connecting")
|
const [connectionStatus, setConnectionStatus] = useState<"connecting" | "connected" | "disconnected">("connecting")
|
||||||
const [newStatusIds, setNewStatusIds] = useState<Set<string>>(new Set())
|
const [newStatusIds, setNewStatusIds] = useState<Set<string>>(new Set());
|
||||||
const channelRef = useRef<RealtimeChannel | null>(null)
|
const channelRef = useRef<RealtimeChannel | null>(null);
|
||||||
const supabaseRef = useRef(createClient())
|
const supabaseRef = useRef(createClient());
|
||||||
|
|
||||||
const fetchRecentUsersWithStatuses = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await getRecentUsersWithStatuses()
|
|
||||||
if (!response.success) throw new Error(response.error)
|
|
||||||
return response.data
|
|
||||||
} catch (error) {
|
|
||||||
toast.error(`Error fetching technicians: ${error as Error}`)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Initial load
|
// Keep all your existing React Query code exactly as is
|
||||||
useEffect(() => {
|
const {
|
||||||
const loadData = async () => {
|
data: usersWithStatuses = initialStatuses,
|
||||||
const data = await fetchRecentUsersWithStatuses()
|
isLoading: loading,
|
||||||
setUsersWithStatuses(data)
|
error,
|
||||||
setLoading(false)
|
refetch,
|
||||||
}
|
isFetching,
|
||||||
loadData().catch((error) => {
|
dataUpdatedAt,
|
||||||
console.error("Error loading data:", error)
|
} = useQuery({
|
||||||
})
|
queryKey: ['users-with-statuses'],
|
||||||
}, [fetchRecentUsersWithStatuses, isAuthenticated])
|
queryFn: async () => {
|
||||||
|
try {
|
||||||
const updateStatus = useCallback(async () => {
|
const response = await getRecentUsersWithStatuses();
|
||||||
if (!isAuthenticated) {
|
if (!response.success) throw new Error(response.error);
|
||||||
toast.error("You must be signed in to update statuses.")
|
return response.data;
|
||||||
return
|
} catch (error) {
|
||||||
}
|
toast.error(`Error fetching technicians: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
if (!statusInput.trim()) {
|
throw error;
|
||||||
toast.error("Please enter a valid status.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setUpdatingStatus(true)
|
|
||||||
if (selectedUsers.length === 0) {
|
|
||||||
const result = await updateUserStatus(statusInput)
|
|
||||||
if (!result.success) throw new Error(result.error)
|
|
||||||
toast.success("Status updated for signed in user.")
|
|
||||||
} else {
|
|
||||||
const selectedUserObjects = usersWithStatuses.filter((u) => selectedUsers.includes(u.user.id))
|
|
||||||
const result = await updateStatuses(selectedUserObjects, statusInput)
|
|
||||||
if (!result.success) throw new Error(result.error)
|
|
||||||
toast.success(`Status updated for ${selectedUsers.length} selected users.`)
|
|
||||||
}
|
}
|
||||||
setSelectedUsers([])
|
},
|
||||||
setStatusInput("")
|
enabled: isAuthenticated,
|
||||||
} catch (error) {
|
refetchInterval: 30000, // Changed to 30 seconds as backup
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
refetchOnWindowFocus: true,
|
||||||
toast.error(`Failed to update status: ${errorMessage}`)
|
refetchOnMount: true,
|
||||||
} finally {
|
initialData: initialStatuses,
|
||||||
setUpdatingStatus(false)
|
meta: { errCode: QueryErrorCodes.USERS_FETCH_FAILED },
|
||||||
}
|
});
|
||||||
}, [isAuthenticated, statusInput, selectedUsers, usersWithStatuses])
|
|
||||||
|
|
||||||
const handleCheckboxChange = (id: string) => {
|
|
||||||
setSelectedUsers((prev) => (prev.includes(id) ? prev.filter((prevId) => prevId !== id) : [...prev, id]))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSelectAllChange = () => {
|
|
||||||
if (selectAll) {
|
|
||||||
setSelectedUsers([])
|
|
||||||
} else {
|
|
||||||
setSelectedUsers(usersWithStatuses.map((user) => user.user.id))
|
|
||||||
}
|
|
||||||
setSelectAll(!selectAll)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Add this new useEffect for realtime enhancement
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectAll(selectedUsers.length === usersWithStatuses.length && usersWithStatuses.length > 0)
|
if (!isAuthenticated) return;
|
||||||
}, [selectedUsers.length, usersWithStatuses.length])
|
|
||||||
|
|
||||||
// Real-time connection setup
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isAuthenticated) return
|
|
||||||
|
|
||||||
let reconnectAttempts = 0
|
let reconnectAttempts = 0
|
||||||
const maxReconnectAttempts = 3
|
const maxReconnectAttempts = 3
|
||||||
let reconnectTimeout: NodeJS.Timeout
|
let reconnectTimeout: NodeJS.Timeout
|
||||||
let isComponentMounted = true
|
let isComponentMounted = true
|
||||||
let currentChannel: RealtimeChannel | null = null
|
let currentChannel: RealtimeChannel | null = null;
|
||||||
|
|
||||||
const setupRealtimeConnection = () => {
|
const setUpRealtimeConnection = () => {
|
||||||
if (!isComponentMounted) return
|
if (!isComponentMounted) return
|
||||||
|
|
||||||
// Clean up any existing channel first
|
|
||||||
if (currentChannel) {
|
if (currentChannel) {
|
||||||
supabaseRef.current.removeChannel(currentChannel).catch(console.error)
|
supabaseRef.current.removeChannel(currentChannel).catch((error) => {
|
||||||
currentChannel = null
|
console.error(`Error unsubscribing: ${error}`);
|
||||||
|
});
|
||||||
|
currentChannel = null;
|
||||||
}
|
}
|
||||||
|
setConnectionStatus('connecting');
|
||||||
console.log("Setting up new realtime connection...")
|
|
||||||
setConnectionStatus("connecting")
|
|
||||||
|
|
||||||
const channel = supabaseRef.current
|
const channel = supabaseRef.current
|
||||||
.channel(`status_updates`, {
|
.channel('status_updates')
|
||||||
config: {
|
.on('broadcast', { event: 'status_updated' }, (payload) => {
|
||||||
broadcast: { self: true },
|
console.log('Realtime update received, triggering refetch...');
|
||||||
},
|
refetch().catch((error) => {
|
||||||
})
|
console.error(`Error refetching: ${error}`);
|
||||||
.on("broadcast", { event: "status_updated" }, (payload) => {
|
});
|
||||||
const { user_status } = payload.payload as {
|
})
|
||||||
user_status: UserWithStatus
|
.subscribe((status) => {
|
||||||
timestamp: string
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
||||||
}
|
if (status === 'SUBSCRIBED') {
|
||||||
console.log("Received status update:", user_status)
|
console.log('Realtime connection established');
|
||||||
|
setConnectionStatus('connected');
|
||||||
|
reconnectAttempts = 0
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
||||||
|
} else if (status === 'CHANNEL_ERROR') {
|
||||||
|
console.log('Realtime connection failed, relying on polling');
|
||||||
|
setConnectionStatus('disconnected');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
||||||
|
} else if (status === 'CLOSED') {
|
||||||
|
console.log('Realtime connection closed');
|
||||||
|
setConnectionStatus('disconnected');
|
||||||
|
if (isComponentMounted && reconnectAttempts < maxReconnectAttempts) {
|
||||||
|
reconnectAttempts++
|
||||||
|
const delay = 2000 * reconnectAttempts
|
||||||
|
|
||||||
// Add animation class for new status
|
console.log(`Reconnecting after close ${reconnectAttempts}/${maxReconnectAttempts} in ${delay}ms`)
|
||||||
setNewStatusIds((prev) => new Set([...prev, user_status.user.id]))
|
|
||||||
|
|
||||||
// Remove animation class after animation completes
|
if (reconnectTimeout) {
|
||||||
setTimeout(() => {
|
clearTimeout(reconnectTimeout)
|
||||||
setNewStatusIds((prev) => {
|
|
||||||
const newSet = new Set(prev)
|
|
||||||
newSet.delete(user_status.user.id)
|
|
||||||
return newSet
|
|
||||||
})
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
setUsersWithStatuses((prevUsers) => {
|
|
||||||
const existingUserIndex = prevUsers.findIndex((u) => u.user.id === user_status.user.id)
|
|
||||||
|
|
||||||
if (existingUserIndex !== -1) {
|
|
||||||
const updatedUsers = [...prevUsers]
|
|
||||||
updatedUsers[existingUserIndex] = {
|
|
||||||
user: user_status.user,
|
|
||||||
status: user_status.status,
|
|
||||||
created_at: user_status.created_at,
|
|
||||||
updated_by: user_status.updated_by,
|
|
||||||
}
|
|
||||||
return updatedUsers
|
|
||||||
} else {
|
|
||||||
return [user_status, ...prevUsers]
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
})
|
|
||||||
.subscribe((status) => {
|
|
||||||
console.log("Subscription status:", status)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
||||||
if (status === "SUBSCRIBED") {
|
|
||||||
console.log("Successfully subscribed to status updates!")
|
|
||||||
setConnectionStatus("connected")
|
|
||||||
reconnectAttempts = 0
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
||||||
} else if (status === "CLOSED") {
|
|
||||||
console.log("Connection closed")
|
|
||||||
setConnectionStatus("disconnected")
|
|
||||||
|
|
||||||
if (isComponentMounted && reconnectAttempts < maxReconnectAttempts) {
|
reconnectTimeout = setTimeout(() => {
|
||||||
reconnectAttempts++
|
if (isComponentMounted) {
|
||||||
const delay = 2000 * reconnectAttempts
|
setUpRealtimeConnection()
|
||||||
|
|
||||||
console.log(`Reconnecting after close ${reconnectAttempts}/${maxReconnectAttempts} in ${delay}ms`)
|
|
||||||
|
|
||||||
if (reconnectTimeout) {
|
|
||||||
clearTimeout(reconnectTimeout)
|
|
||||||
}
|
}
|
||||||
|
}, delay)
|
||||||
reconnectTimeout = setTimeout(() => {
|
} else {
|
||||||
if (isComponentMounted) {
|
console.log("Max reconnection attempts reached or component unmounted")
|
||||||
setupRealtimeConnection()
|
|
||||||
}
|
|
||||||
}, delay)
|
|
||||||
} else {
|
|
||||||
console.log("Max reconnection attempts reached or component unmounted")
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
||||||
} else if (status === "CHANNEL_ERROR") {
|
|
||||||
console.error("Channel error - stopping reconnection attempts")
|
|
||||||
setConnectionStatus("disconnected")
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
});
|
||||||
currentChannel = channel
|
currentChannel = channel;
|
||||||
channelRef.current = channel
|
channelRef.current = channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialTimeout = setTimeout(() => {
|
const initialTimeout = setTimeout(() => {
|
||||||
if (isComponentMounted) {
|
if (isComponentMounted) {
|
||||||
setupRealtimeConnection()
|
setUpRealtimeConnection()
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isComponentMounted = false
|
isComponentMounted = false
|
||||||
|
if (initialTimeout)
|
||||||
if (initialTimeout) {
|
|
||||||
clearTimeout(initialTimeout)
|
clearTimeout(initialTimeout)
|
||||||
}
|
if (reconnectTimeout)
|
||||||
|
|
||||||
if (reconnectTimeout) {
|
|
||||||
clearTimeout(reconnectTimeout)
|
clearTimeout(reconnectTimeout)
|
||||||
}
|
|
||||||
|
|
||||||
if (currentChannel) {
|
if (currentChannel) {
|
||||||
console.log("Cleaning up realtime connection...")
|
console.log('Cleaning up realtime connection...');
|
||||||
supabaseRef.current.removeChannel(currentChannel).catch((error) => {
|
supabaseRef.current.removeChannel(currentChannel).catch((error) => {
|
||||||
console.error(`Error unsubscribing: ${error}`)
|
console.error(`Error unsubscribing: ${error}`);
|
||||||
})
|
});
|
||||||
|
channelRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isAuthenticated, refetch]);
|
||||||
|
|
||||||
|
// Updated mutation
|
||||||
|
const updateStatusMutation = useMutation({
|
||||||
|
mutationFn: async ({ usersWithStatuses, status }: { usersWithStatuses: UserWithStatus[], status: string }) => {
|
||||||
|
if (usersWithStatuses.length === 0) {
|
||||||
|
const result = await updateUserStatus(status);
|
||||||
|
if (!result.success) throw new Error(result.error);
|
||||||
|
return { type: 'single', result };
|
||||||
|
} else {
|
||||||
|
const result = await updateStatuses(usersWithStatuses, status);
|
||||||
|
if (!result.success) throw new Error(result.error);
|
||||||
|
return { type: 'multiple', result, count: usersWithStatuses.length };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
meta: { errCode: QueryErrorCodes.STATUS_UPDATE_FAILED },
|
||||||
|
onMutate: async ({ usersWithStatuses, status }) => {
|
||||||
|
// Optimistic update logic
|
||||||
|
await queryClient.cancelQueries({ queryKey: ['users-with-statuses'] });
|
||||||
|
const previousData = queryClient.getQueryData<UserWithStatus[]>(['users-with-statuses']);
|
||||||
|
|
||||||
|
if (previousData && usersWithStatuses.length > 0) {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const optimisticData = previousData.map(userStatus => {
|
||||||
|
if (usersWithStatuses.some(selected => selected.user.id === userStatus.user.id)) {
|
||||||
|
return {
|
||||||
|
...userStatus,
|
||||||
|
status,
|
||||||
|
created_at: now,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return userStatus;
|
||||||
|
});
|
||||||
|
queryClient.setQueryData(['users-with-statuses'], optimisticData);
|
||||||
|
|
||||||
|
// Add animation for optimistic updates
|
||||||
|
const updatedIds = usersWithStatuses.map(u => u.user.id);
|
||||||
|
setNewStatusIds(prev => new Set([...prev, ...updatedIds]));
|
||||||
|
|
||||||
|
// Remove animation after 1 second
|
||||||
|
setTimeout(() => {
|
||||||
|
setNewStatusIds(prev => {
|
||||||
|
const updated = new Set(prev);
|
||||||
|
updatedIds.forEach(id => updated.delete(id));
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
channelRef.current = null
|
return { previousData };
|
||||||
}
|
},
|
||||||
}, [isAuthenticated])
|
onSuccess: (data) => {
|
||||||
|
// Handle success in the mutation function
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ['users-with-statuses'] }); // Fixed floating promise
|
||||||
|
|
||||||
const formatTime = (timestamp: string) => {
|
if (data.type === 'single') {
|
||||||
const date = new Date(timestamp)
|
toast.success('Status updated for signed in user.');
|
||||||
const time = date.toLocaleTimeString("en-US", {
|
} else {
|
||||||
hour: "numeric",
|
toast.success(`Status updated for ${data.count} selected users.`);
|
||||||
minute: "numeric",
|
}
|
||||||
})
|
|
||||||
const day = date.getDate()
|
setSelectedUsers([]);
|
||||||
const month = date.toLocaleString("default", { month: "long" })
|
setStatusInput('');
|
||||||
return `${time} - ${month} ${day}`
|
},
|
||||||
}
|
onError: (error, _variables, context) => { // Fixed unused variables
|
||||||
|
// Rollback optimistic update
|
||||||
|
if (context?.previousData) {
|
||||||
|
queryClient.setQueryData(['users-with-statuses'], context.previousData);
|
||||||
|
}
|
||||||
|
// Error handling is done in the global mutation cache
|
||||||
|
console.error('Status update failed:', error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleUpdateStatus = () => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
toast.error('You must be signed in to update statuses.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!statusInput.trim()) {
|
||||||
|
toast.error('Please enter a valid status.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatusMutation.mutate({
|
||||||
|
usersWithStatuses: selectedUsers,
|
||||||
|
status: statusInput.trim()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckboxChange = (user: UserWithStatus) => {
|
||||||
|
setSelectedUsers((prev) =>
|
||||||
|
prev.some(u => u.user.id === user.user.id)
|
||||||
|
? prev.filter((prevUser) => prevUser.user.id !== user.user.id)
|
||||||
|
: [...prev, user]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectAllChange = () => {
|
||||||
|
if (selectAll) {
|
||||||
|
setSelectedUsers([]);
|
||||||
|
} else {
|
||||||
|
setSelectedUsers(usersWithStatuses);
|
||||||
|
}
|
||||||
|
setSelectAll(!selectAll);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectAll(
|
||||||
|
selectedUsers.length === usersWithStatuses.length &&
|
||||||
|
usersWithStatuses.length > 0
|
||||||
|
);
|
||||||
|
}, [selectedUsers.length, usersWithStatuses.length]);
|
||||||
|
|
||||||
const getConnectionIcon = () => {
|
const getConnectionIcon = () => {
|
||||||
switch (connectionStatus) {
|
switch (connectionStatus) {
|
||||||
@ -275,44 +289,79 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatTime = (timestamp: string) => {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const time = date.toLocaleTimeString('en-US', {
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
});
|
||||||
|
const day = date.getDate();
|
||||||
|
const month = date.toLocaleString('default', { month: 'long' });
|
||||||
|
return `${time} - ${month} ${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center min-h-[400px]">
|
<div className='flex justify-center items-center min-h-[400px]'>
|
||||||
<Loading className="w-full" alpha={0.5} />
|
<Loading className='w-full' alpha={0.5} />
|
||||||
</div>
|
</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({
|
const containerClassName = makeConditionalClassName({
|
||||||
context: tvMode,
|
context: tvMode,
|
||||||
defaultClassName: "mx-auto space-y-4",
|
defaultClassName: 'mx-auto space-y-4',
|
||||||
on: "lg:w-11/12 w-full mt-15",
|
on: 'lg:w-11/12 w-full mt-15',
|
||||||
off: "w-5/6",
|
off: 'w-5/6',
|
||||||
})
|
});
|
||||||
|
|
||||||
const cardClassName = makeConditionalClassName({
|
const cardClassName = makeConditionalClassName({
|
||||||
context: tvMode,
|
context: tvMode,
|
||||||
defaultClassName: "transition-all duration-300 hover:shadow-md",
|
defaultClassName: 'transition-all duration-300 hover:shadow-md',
|
||||||
on: "lg:text-4xl",
|
on: 'lg:text-4xl',
|
||||||
off: "lg:text-base",
|
off: 'lg:text-base',
|
||||||
})
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClassName}>
|
<div className={containerClassName}>
|
||||||
{/* Connection Status Header */}
|
{/* Status Header */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className='flex items-center justify-between mb-6'>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<h2 className={`font-bold ${tvMode ? "text-6xl" : "text-2xl"}`}>Tech Status</h2>
|
<h2 className={`font-bold ${tvMode ? 'text-6xl' : 'text-2xl'}`}>
|
||||||
<Badge variant="outline" className="flex items-center gap-2">
|
Tech Status
|
||||||
{getConnectionIcon()}
|
</h2>
|
||||||
<span className="text-xs">{getConnectionText()}</span>
|
{isFetching ? (
|
||||||
</Badge>
|
<Badge variant="outline" className="flex items-center gap-2">
|
||||||
|
<RefreshCw className='w-4 h-4 animate-spin text-blue-500' />
|
||||||
|
<span className='text-xs'>Updating...</span>
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline" className="flex items-center gap-2">
|
||||||
|
{getConnectionIcon()}
|
||||||
|
<span className='text-xs'>{getConnectionText()}</span>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!tvMode && usersWithStatuses.length > 0 && (
|
{!tvMode && usersWithStatuses.length > 0 && (
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<Checkbox id="select-all" checked={selectAll} onCheckedChange={handleSelectAllChange} />
|
<Checkbox
|
||||||
<label htmlFor="select-all" className="text-sm font-medium">
|
id='select-all'
|
||||||
|
checked={selectAll}
|
||||||
|
onCheckedChange={handleSelectAllChange}
|
||||||
|
/>
|
||||||
|
<label htmlFor='select-all' className='text-sm font-medium'>
|
||||||
Select All ({selectedUsers.length} selected)
|
Select All ({selectedUsers.length} selected)
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -320,50 +369,51 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status Cards */}
|
{/* Status Cards */}
|
||||||
<div className="space-y-3">
|
<div className='space-y-3'>
|
||||||
{usersWithStatuses.map((userWithStatus) => {
|
{usersWithStatuses.map((userWithStatus) => {
|
||||||
const isSelected = selectedUsers.includes(userWithStatus.user.id)
|
const isSelected = selectedUsers.some(u => u.user.id === userWithStatus.user.id);
|
||||||
const isNewStatus = newStatusIds.has(userWithStatus.user.id)
|
const isNewStatus = newStatusIds.has(userWithStatus.user.id);
|
||||||
const isUpdatedByOther = userWithStatus.updated_by && userWithStatus.updated_by.id !== userWithStatus.user.id
|
const isUpdatedByOther = userWithStatus.updated_by &&
|
||||||
|
userWithStatus.updated_by.id !== userWithStatus.user.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={userWithStatus.user.id}
|
key={userWithStatus.user.id}
|
||||||
className={`
|
className={`
|
||||||
${cardClassName}
|
${cardClassName}
|
||||||
${isSelected ? "ring-2 ring-primary" : ""}
|
${isSelected ? 'ring-2 ring-primary' : ''}
|
||||||
${isNewStatus ? "animate-pulse bg-primary/5 border-primary/20" : ""}
|
${isNewStatus ? 'animate-pulse bg-primary/5 border-primary/20' : ''}
|
||||||
hover:bg-muted/50 cursor-pointer
|
hover:bg-muted/50 cursor-pointer
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className='pb-3'>
|
||||||
<div className="flex items-center justify-between">
|
<div className='flex items-center justify-between'>
|
||||||
<div className="flex items-center gap-3">
|
<div className='flex items-center gap-3'>
|
||||||
{!tvMode && (
|
{!tvMode && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onCheckedChange={() => handleCheckboxChange(userWithStatus.user.id)}
|
onCheckedChange={() => handleCheckboxChange(userWithStatus)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<BasedAvatar
|
<BasedAvatar
|
||||||
src={userWithStatus.user.avatar_url}
|
src={userWithStatus.user.avatar_url}
|
||||||
fullName={userWithStatus.user.full_name}
|
fullName={userWithStatus.user.full_name}
|
||||||
className={tvMode ? "w-16 h-16" : "w-12 h-12"}
|
className={tvMode ? 'w-16 h-16' : 'w-12 h-12'}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h3 className={`font-semibold ${tvMode ? "text-5xl" : "text-lg"}`}>
|
<h3 className={`font-semibold ${tvMode ? 'text-5xl' : 'text-lg'}`}>
|
||||||
{userWithStatus.user.full_name ?? "Unknown User"}
|
{userWithStatus.user.full_name ?? 'Unknown User'}
|
||||||
</h3>
|
</h3>
|
||||||
{isUpdatedByOther && (
|
{isUpdatedByOther && (
|
||||||
<div className="flex items-center gap-1 text-muted-foreground">
|
<div className='flex items-center gap-1 text-muted-foreground'>
|
||||||
<BasedAvatar
|
<BasedAvatar
|
||||||
src={userWithStatus.updated_by?.avatar_url}
|
src={userWithStatus.updated_by?.avatar_url}
|
||||||
fullName={userWithStatus.updated_by?.full_name}
|
fullName={userWithStatus.updated_by?.full_name}
|
||||||
className="w-3 h-3"
|
className='w-3 h-3'
|
||||||
/>
|
/>
|
||||||
{userWithStatus.updated_by && (
|
{userWithStatus.updated_by && (
|
||||||
<span className={`text-xs ${tvMode ? "text-3xl" : ""}`}>
|
<span className={`text-xs ${tvMode ? 'text-3xl' : ''}`}>
|
||||||
Updated by {userWithStatus.updated_by.full_name}
|
Updated by {userWithStatus.updated_by.full_name}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -371,84 +421,94 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-muted-foreground">
|
<div className='flex items-center gap-2 text-muted-foreground'>
|
||||||
<Clock className={`${tvMode ? "w-8 h-8" : "w-4 h-4"}`} />
|
<Clock className={`${tvMode ? 'w-8 h-8' : 'w-4 h-4'}`} />
|
||||||
<span className={`text-sm ${tvMode ? "text-3xl" : ""}`}>
|
<span className={`text-sm ${tvMode ? 'text-3xl' : ''}`}>
|
||||||
{formatTime(userWithStatus.created_at)}
|
{formatTime(userWithStatus.created_at)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-0">
|
<CardContent className='pt-0'>
|
||||||
<Drawer>
|
<Drawer>
|
||||||
<DrawerTrigger asChild>
|
<DrawerTrigger asChild>
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
p-4 rounded-lg bg-muted/30 hover:bg-muted/50
|
p-4 rounded-lg bg-muted/30 hover:bg-muted/50
|
||||||
transition-colors cursor-pointer text-left
|
transition-colors cursor-pointer text-left
|
||||||
${tvMode ? "text-4xl" : "text-base"}
|
${tvMode ? 'text-4xl' : 'text-base'}
|
||||||
`}
|
`}
|
||||||
onClick={() => setSelectedHistoryUser(userWithStatus.user)}
|
onClick={() => setSelectedHistoryUser(userWithStatus.user)}
|
||||||
>
|
>
|
||||||
<p className="font-medium">{userWithStatus.status}</p>
|
<p className='font-medium'>{userWithStatus.status}</p>
|
||||||
</div>
|
</div>
|
||||||
</DrawerTrigger>
|
</DrawerTrigger>
|
||||||
{selectedHistoryUser === userWithStatus.user && <HistoryDrawer user={selectedHistoryUser} />}
|
{selectedHistoryUser === userWithStatus.user && (
|
||||||
|
<HistoryDrawer user={selectedHistoryUser} />
|
||||||
|
)}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{usersWithStatuses.length === 0 && (
|
{usersWithStatuses.length === 0 && (
|
||||||
<Card className="p-8 text-center">
|
<Card className='p-8 text-center'>
|
||||||
<p className={`text-muted-foreground ${tvMode ? "text-4xl" : "text-lg"}`}>No status updates yet</p>
|
<p className={`text-muted-foreground ${tvMode ? 'text-4xl' : 'text-lg'}`}>
|
||||||
|
No status updates yet
|
||||||
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Status Update Input */}
|
{/* Status Update Input */}
|
||||||
{!tvMode && (
|
{!tvMode && (
|
||||||
<Card className="p-6 mt-6">
|
<Card className='p-6 mt-6'>
|
||||||
<div className="flex flex-col gap-4">
|
<div className='flex flex-col gap-4'>
|
||||||
<h3 className="text-lg font-semibold">Update Status</h3>
|
<h3 className='text-lg font-semibold'>Update Status</h3>
|
||||||
<div className="flex gap-4">
|
<div className='flex gap-4'>
|
||||||
<Input
|
<Input
|
||||||
autoFocus
|
autoFocus
|
||||||
type="text"
|
type='text'
|
||||||
placeholder="What's your status?"
|
placeholder="What's your status?"
|
||||||
className="flex-1 text-base"
|
className='flex-1 text-base'
|
||||||
value={statusInput}
|
value={statusInput}
|
||||||
onChange={(e) => setStatusInput(e.target.value)}
|
onChange={(e) => setStatusInput(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey && !updateStatusMutation.isPending) {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
updateStatus().catch((error) => {
|
handleUpdateStatus();
|
||||||
toast.error(`Failed to update status: ${error as Error}`)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
disabled={updateStatusMutation.isPending}
|
||||||
/>
|
/>
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
onClick={updateStatus}
|
onClick={handleUpdateStatus}
|
||||||
disabled={updatingStatus}
|
disabled={updateStatusMutation.isPending}
|
||||||
className="px-6"
|
className='px-6'
|
||||||
>
|
>
|
||||||
{selectedUsers.length > 0 ? `Update ${selectedUsers.length} Users` : "Update Status"}
|
{updateStatusMutation.isPending
|
||||||
|
? 'Updating...'
|
||||||
|
: selectedUsers.length > 0
|
||||||
|
? `Update ${selectedUsers.length} Users`
|
||||||
|
: 'Update Status'
|
||||||
|
}
|
||||||
</SubmitButton>
|
</SubmitButton>
|
||||||
</div>
|
</div>
|
||||||
{selectedUsers.length > 0 && (
|
{selectedUsers.length > 0 && (
|
||||||
<p className="text-sm text-muted-foreground">Updating status for {selectedUsers.length} selected users</p>
|
<p className='text-sm text-muted-foreground'>
|
||||||
|
Updating status for {selectedUsers.length} selected users
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Global Status History Drawer */}
|
{/* Global Status History Drawer */}
|
||||||
<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>
|
||||||
@ -456,82 +516,5 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => {
|
|||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
//'use client';
|
|
||||||
|
|
||||||
//import { createClient } from '@/utils/supabase';
|
|
||||||
//import { useState, useEffect, useCallback, useRef } from 'react';
|
|
||||||
//import { useAuth, useTVMode } from '@/components/context';
|
|
||||||
//import {
|
|
||||||
//getRecentUsersWithStatuses,
|
|
||||||
//updateStatuses,
|
|
||||||
//updateUserStatus,
|
|
||||||
//type UserWithStatus,
|
|
||||||
//} from '@/lib/hooks';
|
|
||||||
//import {
|
|
||||||
//BasedAvatar,
|
|
||||||
//Drawer,
|
|
||||||
//DrawerTrigger,
|
|
||||||
//Loading,
|
|
||||||
//} from '@/components/ui';
|
|
||||||
//import { SubmitButton } from '@/components/default';
|
|
||||||
//import { toast } from 'sonner';
|
|
||||||
//import { HistoryDrawer } from '@/components/status';
|
|
||||||
//import type { Profile } from '@/utils/supabase';
|
|
||||||
//import type { RealtimeChannel } from '@supabase/supabase-js';
|
|
||||||
//import { makeConditionalClassName } from '@/lib/utils';
|
|
||||||
|
|
||||||
//export const StatusList = (initialStatuses: UserWithStatus[]) => {
|
|
||||||
//const { isAuthenticated } = useAuth();
|
|
||||||
//const { tvMode } = useTVMode();
|
|
||||||
//const [loading, setLoading] = useState(true);
|
|
||||||
//const [selectedUsers, setSelectedUsers] = useState<UserWithStatus[]>([]);
|
|
||||||
//const [selectAll, setSelectAll] = useState(false);
|
|
||||||
//const [updatingStatus, setUpdatingStatus] = useState(false);
|
|
||||||
//const [statusInput, setStatusInput] = useState('');
|
|
||||||
//const [usersWithStatuses, setUsersWithStatuses] = useState<UserWithStatus[]>(initialStatuses);
|
|
||||||
//const [selectedHistoryUser, setSelectedHistoryUser] = useState<UserWithStatus | null>(null);
|
|
||||||
//const channelRef = useRef<RealtimeChannel | null>(null);
|
|
||||||
//const supabaseRef = useRef(createClient());
|
|
||||||
|
|
||||||
//const fetchRecentUsersWithStatuses = useCallback(async () => {
|
|
||||||
//const response = await getRecentUsersWithStatuses();
|
|
||||||
//if (response.success) return response.data;
|
|
||||||
//else toast.error('Error fetching users!');
|
|
||||||
//return [];
|
|
||||||
//}, []);
|
|
||||||
|
|
||||||
//useEffect(() => {
|
|
||||||
//const loadData = async () => {
|
|
||||||
//const data = await fetchRecentUsersWithStatuses();
|
|
||||||
//setUsersWithStatuses(data);
|
|
||||||
//};
|
|
||||||
//loadData()
|
|
||||||
//.catch(()=> toast.error('Error fetching users.'))
|
|
||||||
//.finally(() => setLoading(false));
|
|
||||||
//}, [fetchRecentUsersWithStatuses, isAuthenticated]);
|
|
||||||
|
|
||||||
//const updateStatus = useCallback(async () => {
|
|
||||||
//try {
|
|
||||||
//if (!isAuthenticated) throw new Error('Not authenticated.');
|
|
||||||
//if (!statusInput.trim()) throw new Error('Not a valid status.')
|
|
||||||
|
|
||||||
//setUpdatingStatus(true);
|
|
||||||
//if (selectedUsers.length === 0) {
|
|
||||||
//const result = await updateUserStatus(statusInput);
|
|
||||||
//if (!result.success) throw new Error('Could not updateUserStatus!')
|
|
||||||
//toast.success('Status updated!');
|
|
||||||
//} else {
|
|
||||||
//const result = await updateStatuses(selectedUsers, statusInput);
|
|
||||||
//}
|
|
||||||
|
|
||||||
//} catch (error) {
|
|
||||||
//toast.error(`Error updating statuses: ${error as Error}`);
|
|
||||||
//}
|
|
||||||
//});
|
|
||||||
|
|
||||||
//return (
|
|
||||||
//<div/>
|
|
||||||
//);
|
|
||||||
//};
|
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { createClient } from '@/utils/supabase';
|
|
||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
||||||
import { useAuth, useTVMode } from '@/components/context';
|
import { useAuth, useTVMode } from '@/components/context';
|
||||||
import {
|
import {
|
||||||
getRecentUsersWithStatuses,
|
getRecentUsersWithStatuses,
|
||||||
@ -19,8 +17,14 @@ import { SubmitButton } from '@/components/default';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { HistoryDrawer } from '@/components/status';
|
import { HistoryDrawer } from '@/components/status';
|
||||||
import type { Profile } from '@/utils/supabase';
|
import type { Profile } from '@/utils/supabase';
|
||||||
import type { RealtimeChannel } from '@supabase/supabase-js';
|
|
||||||
import { makeConditionalClassName } from '@/lib/utils';
|
import { makeConditionalClassName } from '@/lib/utils';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { RefreshCw, Wifi, WifiOff } from 'lucide-react';
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { QueryErrorCodes } from '@/components/context';
|
||||||
|
import { createClient } from '@/utils/supabase';
|
||||||
|
import type { RealtimeChannel } from '@supabase/supabase-js';
|
||||||
|
|
||||||
type TechTableProps = {
|
type TechTableProps = {
|
||||||
initialStatuses: UserWithStatus[];
|
initialStatuses: UserWithStatus[];
|
||||||
@ -31,41 +35,194 @@ export const TechTable = ({
|
|||||||
}: TechTableProps) => {
|
}: TechTableProps) => {
|
||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
const { tvMode } = useTVMode();
|
const { tvMode } = useTVMode();
|
||||||
const [loading, setLoading] = useState(true);
|
const queryClient = useQueryClient();
|
||||||
const [selectedUsers, setSelectedUsers] = useState<UserWithStatus[]>([]);
|
const [selectedUsers, setSelectedUsers] = useState<UserWithStatus[]>([]);
|
||||||
const [selectAll, setSelectAll] = useState(false);
|
const [selectAll, setSelectAll] = useState(false);
|
||||||
const [statusInput, setStatusInput] = useState('');
|
const [statusInput, setStatusInput] = useState('');
|
||||||
const [usersWithStatuses, setUsersWithStatuses] =
|
const [selectedHistoryUser, setSelectedHistoryUser] = useState<Profile | null>(null);
|
||||||
useState<UserWithStatus[]>(initialStatuses);
|
const [connectionStatus, setConnectionStatus] = useState<"connecting" | "connected" | "disconnected">("connecting");
|
||||||
const [selectedHistoryUser, setSelectedHistoryUser] =
|
const [newStatusIds, setNewStatusIds] = useState<Set<string>>(new Set());
|
||||||
useState<UserWithStatus | null>(null);
|
|
||||||
const channelRef = useRef<RealtimeChannel | null>(null);
|
const channelRef = useRef<RealtimeChannel | null>(null);
|
||||||
const supabaseRef = useRef(createClient());
|
const supabaseRef = useRef(createClient());
|
||||||
|
|
||||||
const fetchRecentUsersWithStatuses = useCallback(async () => {
|
// Keep all your existing React Query code exactly as is
|
||||||
try {
|
const {
|
||||||
const response = await getRecentUsersWithStatuses();
|
data: usersWithStatuses = initialStatuses,
|
||||||
if (!response.success) throw new Error(response.error);
|
isLoading: loading,
|
||||||
return response.data;
|
error,
|
||||||
} catch (error) {
|
refetch,
|
||||||
toast.error(`Error fetching technicians: ${error as Error}`);
|
isFetching,
|
||||||
return [];
|
dataUpdatedAt,
|
||||||
}
|
} = useQuery({
|
||||||
}, []);
|
queryKey: ['users-with-statuses'],
|
||||||
|
queryFn: async () => {
|
||||||
|
try {
|
||||||
|
const response = await getRecentUsersWithStatuses();
|
||||||
|
if (!response.success) throw new Error(response.error);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(`Error fetching technicians: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled: isAuthenticated,
|
||||||
|
refetchInterval: 30000, // Changed to 30 seconds as backup
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
refetchOnMount: true,
|
||||||
|
initialData: initialStatuses,
|
||||||
|
meta: { errCode: QueryErrorCodes.USERS_FETCH_FAILED },
|
||||||
|
});
|
||||||
|
|
||||||
// Initial load
|
// Add this new useEffect for realtime enhancement
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
if (!isAuthenticated) return;
|
||||||
const data = await fetchRecentUsersWithStatuses();
|
let reconnectAttempts = 0
|
||||||
setUsersWithStatuses(data);
|
const maxReconnectAttempts = 3
|
||||||
setLoading(false);
|
let reconnectTimeout: NodeJS.Timeout
|
||||||
};
|
let isComponentMounted = true
|
||||||
loadData().catch((error) => {
|
let currentChannel: RealtimeChannel | null = null;
|
||||||
console.error('Error loading data:', error);
|
|
||||||
});
|
|
||||||
}, [fetchRecentUsersWithStatuses, isAuthenticated]);
|
|
||||||
|
|
||||||
const updateStatus = useCallback(async () => {
|
const setUpRealtimeConnection = () => {
|
||||||
|
if (!isComponentMounted) return
|
||||||
|
if (currentChannel) {
|
||||||
|
supabaseRef.current.removeChannel(currentChannel).catch((error) => {
|
||||||
|
console.error(`Error unsubscribing: ${error}`);
|
||||||
|
});
|
||||||
|
currentChannel = null;
|
||||||
|
}
|
||||||
|
setConnectionStatus('connecting');
|
||||||
|
const channel = supabaseRef.current
|
||||||
|
.channel('status_updates')
|
||||||
|
.on('broadcast', { event: 'status_updated' }, (payload) => {
|
||||||
|
console.log('Realtime update received, triggering refetch...');
|
||||||
|
refetch().catch((error) => {
|
||||||
|
console.error(`Error refetching: ${error}`);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.subscribe((status) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
||||||
|
if (status === 'SUBSCRIBED') {
|
||||||
|
console.log('Realtime connection established');
|
||||||
|
setConnectionStatus('connected');
|
||||||
|
reconnectAttempts = 0
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
||||||
|
} else if (status === 'CHANNEL_ERROR') {
|
||||||
|
console.log('Realtime connection failed, relying on polling');
|
||||||
|
setConnectionStatus('disconnected');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
||||||
|
} else if (status === 'CLOSED') {
|
||||||
|
console.log('Realtime connection closed');
|
||||||
|
setConnectionStatus('disconnected');
|
||||||
|
if (isComponentMounted && reconnectAttempts < maxReconnectAttempts) {
|
||||||
|
reconnectAttempts++
|
||||||
|
const delay = 2000 * reconnectAttempts
|
||||||
|
console.log(`Reconnecting after close ${reconnectAttempts}/${maxReconnectAttempts} in ${delay}ms`)
|
||||||
|
if (reconnectTimeout) {
|
||||||
|
clearTimeout(reconnectTimeout)
|
||||||
|
}
|
||||||
|
reconnectTimeout = setTimeout(() => {
|
||||||
|
if (isComponentMounted) {
|
||||||
|
setUpRealtimeConnection()
|
||||||
|
}
|
||||||
|
}, delay)
|
||||||
|
} else {
|
||||||
|
console.log("Max reconnection attempts reached or component unmounted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
currentChannel = channel;
|
||||||
|
channelRef.current = channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialTimeout = setTimeout(() => {
|
||||||
|
if (isComponentMounted) {
|
||||||
|
setUpRealtimeConnection()
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isComponentMounted = false
|
||||||
|
if (initialTimeout)
|
||||||
|
clearTimeout(initialTimeout)
|
||||||
|
if (reconnectTimeout)
|
||||||
|
clearTimeout(reconnectTimeout)
|
||||||
|
if (currentChannel) {
|
||||||
|
console.log('Cleaning up realtime connection...');
|
||||||
|
supabaseRef.current.removeChannel(currentChannel).catch((error) => {
|
||||||
|
console.error(`Error unsubscribing: ${error}`);
|
||||||
|
});
|
||||||
|
channelRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isAuthenticated, refetch]);
|
||||||
|
|
||||||
|
// Updated mutation
|
||||||
|
const updateStatusMutation = useMutation({
|
||||||
|
mutationFn: async ({ usersWithStatuses, status }: { usersWithStatuses: UserWithStatus[], status: string }) => {
|
||||||
|
if (usersWithStatuses.length === 0) {
|
||||||
|
const result = await updateUserStatus(status);
|
||||||
|
if (!result.success) throw new Error(result.error);
|
||||||
|
return { type: 'single', result };
|
||||||
|
} else {
|
||||||
|
const result = await updateStatuses(usersWithStatuses, status);
|
||||||
|
if (!result.success) throw new Error(result.error);
|
||||||
|
return { type: 'multiple', result, count: usersWithStatuses.length };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
meta: { errCode: QueryErrorCodes.STATUS_UPDATE_FAILED },
|
||||||
|
onMutate: async ({ usersWithStatuses, status }) => {
|
||||||
|
// Optimistic update logic
|
||||||
|
await queryClient.cancelQueries({ queryKey: ['users-with-statuses'] });
|
||||||
|
const previousData = queryClient.getQueryData<UserWithStatus[]>(['users-with-statuses']);
|
||||||
|
if (previousData && usersWithStatuses.length > 0) {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const optimisticData = previousData.map(userStatus => {
|
||||||
|
if (usersWithStatuses.some(selected => selected.user.id === userStatus.user.id)) {
|
||||||
|
return {
|
||||||
|
...userStatus,
|
||||||
|
status,
|
||||||
|
created_at: now,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return userStatus;
|
||||||
|
});
|
||||||
|
queryClient.setQueryData(['users-with-statuses'], optimisticData);
|
||||||
|
// Add animation for optimistic updates
|
||||||
|
const updatedIds = usersWithStatuses.map(u => u.user.id);
|
||||||
|
setNewStatusIds(prev => new Set([...prev, ...updatedIds]));
|
||||||
|
// Remove animation after 1 second
|
||||||
|
setTimeout(() => {
|
||||||
|
setNewStatusIds(prev => {
|
||||||
|
const updated = new Set(prev);
|
||||||
|
updatedIds.forEach(id => updated.delete(id));
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
return { previousData };
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
// Handle success in the mutation function
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ['users-with-statuses'] }); // Fixed floating promise
|
||||||
|
if (data.type === 'single') {
|
||||||
|
toast.success('Status updated for signed in user.');
|
||||||
|
} else {
|
||||||
|
toast.success(`Status updated for ${data.count} selected users.`);
|
||||||
|
}
|
||||||
|
setSelectedUsers([]);
|
||||||
|
setStatusInput('');
|
||||||
|
},
|
||||||
|
onError: (error, _variables, context) => { // Fixed unused variables
|
||||||
|
// Rollback optimistic update
|
||||||
|
if (context?.previousData) {
|
||||||
|
queryClient.setQueryData(['users-with-statuses'], context.previousData);
|
||||||
|
}
|
||||||
|
// Error handling is done in the global mutation cache
|
||||||
|
console.error('Status update failed:', error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleUpdateStatus = () => {
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
toast.error('You must be signed in to update statuses.');
|
toast.error('You must be signed in to update statuses.');
|
||||||
return;
|
return;
|
||||||
@ -74,32 +231,17 @@ export const TechTable = ({
|
|||||||
toast.error('Please enter a valid status.');
|
toast.error('Please enter a valid status.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
updateStatusMutation.mutate({
|
||||||
if (selectedUsers.length === 0) {
|
usersWithStatuses: selectedUsers,
|
||||||
const result = await updateUserStatus(statusInput);
|
status: statusInput.trim()
|
||||||
if (!result.success) throw new Error(result.error);
|
});
|
||||||
toast.success(`Status updated for signed in user.`);
|
};
|
||||||
} else {
|
|
||||||
const result = await updateStatuses(selectedUsers, statusInput);
|
|
||||||
if (!result.success) throw new Error(result.error);
|
|
||||||
toast.success(
|
|
||||||
`Status updated for ${selectedUsers.length} selected users.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
setSelectedUsers([]);
|
|
||||||
setStatusInput('');
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error ? error.message : String(error);
|
|
||||||
toast.error(`Failed to update status: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
}, [isAuthenticated, statusInput, selectedUsers]);
|
|
||||||
|
|
||||||
const handleCheckboxChange = (id: string) => {
|
const handleCheckboxChange = (user: UserWithStatus) => {
|
||||||
setSelectedUsers((prev) =>
|
setSelectedUsers((prev) =>
|
||||||
prev.includes(id)
|
prev.some(u => u.user.id === user.user.id)
|
||||||
? prev.filter((prevId) => prevId !== id)
|
? prev.filter((prevUser) => prevUser.user.id !== user.user.id)
|
||||||
: [...prev, id],
|
: [...prev, user]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -107,7 +249,7 @@ export const TechTable = ({
|
|||||||
if (selectAll) {
|
if (selectAll) {
|
||||||
setSelectedUsers([]);
|
setSelectedUsers([]);
|
||||||
} else {
|
} else {
|
||||||
setSelectedUsers(usersWithStatuses.map((tech) => tech.user.id));
|
setSelectedUsers(usersWithStatuses);
|
||||||
}
|
}
|
||||||
setSelectAll(!selectAll);
|
setSelectAll(!selectAll);
|
||||||
};
|
};
|
||||||
@ -115,132 +257,31 @@ export const TechTable = ({
|
|||||||
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]);
|
||||||
|
|
||||||
useEffect(() => {
|
const getConnectionIcon = () => {
|
||||||
if (!isAuthenticated) return;
|
switch (connectionStatus) {
|
||||||
|
case "connected":
|
||||||
|
return <Wifi className="w-4 h-4 text-green-500" />
|
||||||
|
case "connecting":
|
||||||
|
return <Wifi className="w-4 h-4 text-yellow-500 animate-pulse" />
|
||||||
|
case "disconnected":
|
||||||
|
return <WifiOff className="w-4 h-4 text-red-500" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let reconnectAttempts = 0;
|
const getConnectionText = () => {
|
||||||
const maxReconnectAttempts = 3; // Reduced from 5
|
switch (connectionStatus) {
|
||||||
let reconnectTimeout: NodeJS.Timeout;
|
case "connected":
|
||||||
let isComponentMounted = true;
|
return "Connected"
|
||||||
let currentChannel: RealtimeChannel | null = null;
|
case "connecting":
|
||||||
|
return "Connecting..."
|
||||||
const setupRealtimeConnection = () => {
|
case "disconnected":
|
||||||
if (!isComponentMounted) return;
|
return "Disconnected"
|
||||||
|
}
|
||||||
// Clean up any existing channel first
|
}
|
||||||
if (currentChannel) {
|
|
||||||
supabaseRef.current.removeChannel(currentChannel).catch(console.error);
|
|
||||||
currentChannel = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Setting up new realtime connection...');
|
|
||||||
|
|
||||||
const channel = supabaseRef.current
|
|
||||||
.channel(`status_updates`, { // Unique channel name
|
|
||||||
config: {
|
|
||||||
broadcast: { self: true }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on('broadcast', { event: 'status_updated' }, (payload) => {
|
|
||||||
const { user_status } = payload.payload as {
|
|
||||||
user_status: UserWithStatus;
|
|
||||||
timestamp: string;
|
|
||||||
};
|
|
||||||
console.log('Received status update:', user_status);
|
|
||||||
|
|
||||||
setUsersWithStatuses((prevUsers) => {
|
|
||||||
const existingUserIndex = prevUsers.findIndex((u) =>
|
|
||||||
u.user.id === user_status.user.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingUserIndex !== -1) {
|
|
||||||
const updatedUsers = [...prevUsers];
|
|
||||||
updatedUsers[existingUserIndex] = {
|
|
||||||
user: user_status.user,
|
|
||||||
status: user_status.status,
|
|
||||||
created_at: user_status.created_at,
|
|
||||||
updated_by: user_status.updated_by,
|
|
||||||
};
|
|
||||||
return updatedUsers;
|
|
||||||
} else {
|
|
||||||
return [user_status, ...prevUsers];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.subscribe((status) => {
|
|
||||||
console.log('Subscription status:', status);
|
|
||||||
// ignore this enum error please
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
||||||
if (status === 'SUBSCRIBED') {
|
|
||||||
console.log('Successfully subscribed to status updates!');
|
|
||||||
reconnectAttempts = 0;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
||||||
} else if (status === 'CLOSED') {
|
|
||||||
console.log('Connection closed');
|
|
||||||
|
|
||||||
// Only reconnect if we haven't exceeded max attempts and component is still mounted
|
|
||||||
if (isComponentMounted && reconnectAttempts < maxReconnectAttempts) {
|
|
||||||
reconnectAttempts++;
|
|
||||||
const delay = 2000 * reconnectAttempts; // Linear backoff instead of exponential
|
|
||||||
|
|
||||||
console.log(`Reconnecting after close ${reconnectAttempts}/${maxReconnectAttempts} in ${delay}ms`);
|
|
||||||
|
|
||||||
// Clear any existing timeout
|
|
||||||
if (reconnectTimeout) {
|
|
||||||
clearTimeout(reconnectTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
reconnectTimeout = setTimeout(() => {
|
|
||||||
if (isComponentMounted) {
|
|
||||||
setupRealtimeConnection();
|
|
||||||
}
|
|
||||||
}, delay);
|
|
||||||
} else {
|
|
||||||
console.log('Max reconnection attempts reached or component unmounted');
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (status === 'CHANNEL_ERROR') {
|
|
||||||
console.error('Channel error - stopping reconnection attempts');
|
|
||||||
// Don't reconnect on channel errors to avoid infinite loops
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
currentChannel = channel;
|
|
||||||
channelRef.current = channel;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add a small delay before initial connection to ensure auth is stable
|
|
||||||
const initialTimeout = setTimeout(() => {
|
|
||||||
if (isComponentMounted) {
|
|
||||||
setupRealtimeConnection();
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isComponentMounted = false;
|
|
||||||
|
|
||||||
if (initialTimeout) {
|
|
||||||
clearTimeout(initialTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reconnectTimeout) {
|
|
||||||
clearTimeout(reconnectTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentChannel) {
|
|
||||||
console.log('Cleaning up realtime connection...');
|
|
||||||
supabaseRef.current.removeChannel(currentChannel).catch((error) => {
|
|
||||||
console.error(`Error unsubscribing: ${error}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
channelRef.current = null;
|
|
||||||
};
|
|
||||||
}, [isAuthenticated]); // Keep this dependency but make the connection more stable
|
|
||||||
|
|
||||||
const formatTime = (timestamp: string) => {
|
const formatTime = (timestamp: string) => {
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
@ -261,6 +302,18 @@ export const TechTable = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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({
|
const containerClassName = makeConditionalClassName({
|
||||||
context: tvMode,
|
context: tvMode,
|
||||||
defaultClassName: 'mx-auto',
|
defaultClassName: 'mx-auto',
|
||||||
@ -284,6 +337,21 @@ export const TechTable = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClassName}>
|
<div className={containerClassName}>
|
||||||
|
{/* Status Header */}
|
||||||
|
<div className='flex items-center justify-between mb-6'>
|
||||||
|
{isFetching ? (
|
||||||
|
<Badge variant="outline" className="flex items-center gap-2">
|
||||||
|
<RefreshCw className='w-4 h-4 animate-spin text-blue-500' />
|
||||||
|
<span className='text-xs'>Updating...</span>
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline" className="flex items-center gap-2">
|
||||||
|
{getConnectionIcon()}
|
||||||
|
<span className='text-xs'>{getConnectionText()}</span>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<table className='w-full text-center rounded-md'>
|
<table className='w-full text-center rounded-md'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr className='bg-muted'>
|
<tr className='bg-muted'>
|
||||||
@ -312,55 +380,87 @@ export const TechTable = ({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{usersWithStatuses.map((userWithStatus, index) => (
|
{usersWithStatuses.map((userWithStatus, index) => {
|
||||||
<tr
|
const isSelected = selectedUsers.some(u => u.user.id === userWithStatus.user.id);
|
||||||
key={userWithStatus.user.id}
|
const isNewStatus = newStatusIds.has(userWithStatus.user.id);
|
||||||
className={`
|
|
||||||
${index % 2 === 0 ? 'bg-muted/50' : 'bg-background'}
|
return (
|
||||||
hover:bg-muted/75 transition-all duration-300
|
<tr
|
||||||
`}
|
key={userWithStatus.user.id}
|
||||||
>
|
className={`
|
||||||
{!tvMode && (
|
${index % 2 === 0 ? 'bg-muted/50' : 'bg-background'}
|
||||||
<td className={tCheckboxClassName}>
|
${isSelected ? 'ring-2 ring-primary' : ''}
|
||||||
<input
|
${isNewStatus ? 'animate-pulse bg-primary/5 border-primary/20' : ''}
|
||||||
type='checkbox'
|
hover:bg-muted/75 transition-all duration-300
|
||||||
className={checkBoxClassName}
|
`}
|
||||||
checked={selectedUsers.includes(userWithStatus.user.id)}
|
>
|
||||||
onChange={() =>
|
{!tvMode && (
|
||||||
handleCheckboxChange(userWithStatus.user.id)
|
<td className={tCheckboxClassName}>
|
||||||
}
|
<input
|
||||||
/>
|
type='checkbox'
|
||||||
|
className={checkBoxClassName}
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={() => handleCheckboxChange(userWithStatus)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td className={tdClassName}>
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<BasedAvatar
|
||||||
|
src={userWithStatus.user.avatar_url}
|
||||||
|
fullName={userWithStatus.user.full_name}
|
||||||
|
className={tvMode ? 'w-16 h-16' : 'w-12 h-12'}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className={`font-semibold ${tvMode ? 'text-5xl' : 'text-lg'}`}>
|
||||||
|
{userWithStatus.user.full_name ?? 'Unknown User'}
|
||||||
|
</p>
|
||||||
|
{userWithStatus.updated_by &&
|
||||||
|
userWithStatus.updated_by.id !== userWithStatus.user.id && (
|
||||||
|
<div className='flex items-center gap-1 text-muted-foreground'>
|
||||||
|
<BasedAvatar
|
||||||
|
src={userWithStatus.updated_by?.avatar_url}
|
||||||
|
fullName={userWithStatus.updated_by?.full_name}
|
||||||
|
className='w-3 h-3'
|
||||||
|
/>
|
||||||
|
<span className={`text-xs ${tvMode ? 'text-3xl' : ''}`}>
|
||||||
|
Updated by {userWithStatus.updated_by.full_name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
<td className={tdClassName}>
|
||||||
<td className={tdClassName}>
|
<Drawer>
|
||||||
<div className='flex'>
|
<DrawerTrigger
|
||||||
<BasedAvatar
|
className='text-center w-full p-2 rounded-md hover:bg-muted transition-colors'
|
||||||
src={userWithStatus.user.avatar_url}
|
onClick={() => setSelectedHistoryUser(userWithStatus.user)}
|
||||||
fullName={userWithStatus.user.full_name}
|
>
|
||||||
/>
|
{userWithStatus.status}
|
||||||
<p>{userWithStatus.user.full_name ?? 'Unknown User'}</p>
|
</DrawerTrigger>
|
||||||
</div>
|
{selectedHistoryUser === userWithStatus.user && (
|
||||||
</td>
|
<HistoryDrawer user={selectedHistoryUser} />
|
||||||
<td className={tdClassName}>
|
)}
|
||||||
<Drawer>
|
</Drawer>
|
||||||
<DrawerTrigger
|
</td>
|
||||||
className='text-center w-full p-2 rounded-md hover:bg-muted transition-colors'
|
<td className={tdClassName}>
|
||||||
onClick={() => setSelectedHistoryUser(userWithStatus.user)}
|
{formatTime(userWithStatus.created_at)}
|
||||||
>
|
</td>
|
||||||
{userWithStatus.status}
|
</tr>
|
||||||
</DrawerTrigger>
|
);
|
||||||
{selectedHistoryUser === userWithStatus.user && (
|
})}
|
||||||
<HistoryDrawer user={selectedHistoryUser} />
|
|
||||||
)}
|
|
||||||
</Drawer>
|
|
||||||
</td>
|
|
||||||
<td className={tdClassName}>
|
|
||||||
{formatTime(userWithStatus.created_at)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
{usersWithStatuses.length === 0 && (
|
||||||
|
<div className='p-8 text-center'>
|
||||||
|
<p className={`text-muted-foreground ${tvMode ? 'text-4xl' : 'text-lg'}`}>
|
||||||
|
No status updates yet
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!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'>
|
||||||
<input
|
<input
|
||||||
@ -375,12 +475,12 @@ export const TechTable = ({
|
|||||||
value={statusInput}
|
value={statusInput}
|
||||||
onChange={(e) => setStatusInput(e.target.value)}
|
onChange={(e) => setStatusInput(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter' && !updateStatusMutation.isPending) {
|
||||||
updateStatus().catch((error) => {
|
e.preventDefault();
|
||||||
toast.error(`Failed to update status: ${error as Error}`);
|
handleUpdateStatus();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
disabled={updateStatusMutation.isPending}
|
||||||
/>
|
/>
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
size='xl'
|
size='xl'
|
||||||
@ -389,14 +489,39 @@ export const TechTable = ({
|
|||||||
disabled:opacity-50 disabled:cursor-not-allowed \
|
disabled:opacity-50 disabled:cursor-not-allowed \
|
||||||
cursor-pointer'
|
cursor-pointer'
|
||||||
}
|
}
|
||||||
onClick={() => updateStatus()}
|
onClick={handleUpdateStatus}
|
||||||
disabled={!statusInput.trim()}
|
disabled={!statusInput.trim() || updateStatusMutation.isPending}
|
||||||
disabledNotLoading={true}
|
disabledNotLoading={true}
|
||||||
>
|
>
|
||||||
Update
|
{updateStatusMutation.isPending
|
||||||
|
? 'Updating...'
|
||||||
|
: selectedUsers.length > 0
|
||||||
|
? `Update ${selectedUsers.length} Users`
|
||||||
|
: 'Update Status'
|
||||||
|
}
|
||||||
</SubmitButton>
|
</SubmitButton>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{selectedUsers.length > 0 && !tvMode && (
|
||||||
|
<div className='text-center mt-4'>
|
||||||
|
<p className='text-sm text-muted-foreground'>
|
||||||
|
Updating status for {selectedUsers.length} selected users
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Global Status History Drawer */}
|
||||||
|
<div className='flex justify-center mt-6'>
|
||||||
|
<Drawer>
|
||||||
|
<DrawerTrigger asChild>
|
||||||
|
<Button variant='outline' className={tvMode ? 'text-3xl p-6' : ''}>
|
||||||
|
View All Status History
|
||||||
|
</Button>
|
||||||
|
</DrawerTrigger>
|
||||||
|
<HistoryDrawer />
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,6 +4,36 @@ import { createClient, type Profile } from '@/utils/supabase';
|
|||||||
import { getSignedUrl, getUser } from '@/lib/hooks';
|
import { getSignedUrl, getUser } from '@/lib/hooks';
|
||||||
import type { Result } from '.';
|
import type { Result } from '.';
|
||||||
|
|
||||||
|
export const getOriginalProfile = async (
|
||||||
|
userId: string | null = null,
|
||||||
|
): Promise<Result<Profile>> => {
|
||||||
|
try {
|
||||||
|
if (userId == null) {
|
||||||
|
const user = await getUser();
|
||||||
|
if (!user.success || !user.data.id)
|
||||||
|
throw new Error('User not found');
|
||||||
|
userId = user.data.id;
|
||||||
|
}
|
||||||
|
const supabase = createClient();
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('*')
|
||||||
|
.eq('id', userId)
|
||||||
|
.single();
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return { success: true, data: data as Profile };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'Unknown error getting profile',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const getProfile = async (
|
export const getProfile = async (
|
||||||
userId: string | null = null
|
userId: string | null = null
|
||||||
): Promise<Result<Profile>> => {
|
): Promise<Result<Profile>> => {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { createClient } from '@/utils/supabase';
|
import { createClient } from '@/utils/supabase';
|
||||||
import type { Profile, Result } from '@/utils/supabase';
|
import type { Profile, Result } from '@/utils/supabase';
|
||||||
import { getUser, getProfile, getSignedUrl } from '@/lib/hooks';
|
import { getUser, getOriginalProfile, getSignedUrl } from '@/lib/hooks';
|
||||||
|
|
||||||
export type UserWithStatus = {
|
export type UserWithStatus = {
|
||||||
id?: string;
|
id?: string;
|
||||||
@ -67,9 +67,9 @@ export const getRecentUsersWithStatuses = async (): Promise<
|
|||||||
for (const userWithStatus of filtered) {
|
for (const userWithStatus of filtered) {
|
||||||
if (userWithStatus.user.avatar_url)
|
if (userWithStatus.user.avatar_url)
|
||||||
userWithStatus.user.avatar_url =
|
userWithStatus.user.avatar_url =
|
||||||
await getAvatarUrl(userWithStatus.updated_by?.avatar_url);
|
await getAvatarUrl(userWithStatus.user.avatar_url);
|
||||||
if (userWithStatus.updated_by?.avatar_url)
|
if (userWithStatus.updated_by?.avatar_url)
|
||||||
userWithStatus.user.avatar_url =
|
userWithStatus.updated_by.avatar_url =
|
||||||
await getAvatarUrl(userWithStatus.updated_by?.avatar_url);
|
await getAvatarUrl(userWithStatus.updated_by?.avatar_url);
|
||||||
filteredWithAvatars.push(userWithStatus);
|
filteredWithAvatars.push(userWithStatus);
|
||||||
}
|
}
|
||||||
@ -115,7 +115,7 @@ export const updateStatuses = async (
|
|||||||
): Promise<Result<void>> => {
|
): Promise<Result<void>> => {
|
||||||
try {
|
try {
|
||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
const profileResponse = await getProfile();
|
const profileResponse = await getOriginalProfile();
|
||||||
if (!profileResponse.success) throw new Error('Not authenticated!');
|
if (!profileResponse.success) throw new Error('Not authenticated!');
|
||||||
const user = profileResponse.data;
|
const user = profileResponse.data;
|
||||||
|
|
||||||
@ -155,7 +155,7 @@ export const updateUserStatus = async (
|
|||||||
): Promise<Result<void>> => {
|
): Promise<Result<void>> => {
|
||||||
try {
|
try {
|
||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
const profileResponse = await getProfile();
|
const profileResponse = await getOriginalProfile();
|
||||||
if (!profileResponse.success)
|
if (!profileResponse.success)
|
||||||
throw new Error(`Not authenticated! ${profileResponse.error}`);
|
throw new Error(`Not authenticated! ${profileResponse.error}`);
|
||||||
const userProfile = profileResponse.data;
|
const userProfile = profileResponse.data;
|
||||||
|
Reference in New Issue
Block a user