Rewriting status card to make it look good and to go over the code

This commit is contained in:
2025-06-17 13:06:35 -05:00
parent a28af1f629
commit 43acc20a40
25 changed files with 981 additions and 520 deletions

View File

@ -2,15 +2,13 @@ import type { Metadata } from 'next';
export const generateMetadata = (): Metadata => { export const generateMetadata = (): Metadata => {
return { return {
title: 'Forgot Password' title: 'Forgot Password',
}; };
}; };
const ForgotPasswordLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => { const ForgotPasswordLayout = ({
return ( children,
<div> }: Readonly<{ children: React.ReactNode }>) => {
{children} return <div>{children}</div>;
</div>
);
}; };
export default ForgotPasswordLayout; export default ForgotPasswordLayout;

View File

@ -2,15 +2,13 @@ import type { Metadata } from 'next';
export const generateMetadata = (): Metadata => { export const generateMetadata = (): Metadata => {
return { return {
title: 'Profile' title: 'Profile',
}; };
}; };
const ProfileLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => { const ProfileLayout = ({
return ( children,
<div> }: Readonly<{ children: React.ReactNode }>) => {
{children} return <div>{children}</div>;
</div>
);
}; };
export default ProfileLayout; export default ProfileLayout;

View File

@ -2,15 +2,13 @@ import type { Metadata } from 'next';
export const generateMetadata = (): Metadata => { export const generateMetadata = (): Metadata => {
return { return {
title: 'Sign In' title: 'Sign In',
}; };
}; };
const SignInLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => { const SignInLayout = ({
return ( children,
<div> }: Readonly<{ children: React.ReactNode }>) => {
{children} return <div>{children}</div>;
</div>
);
}; };
export default SignInLayout; export default SignInLayout;

View File

@ -2,15 +2,13 @@ import type { Metadata } from 'next';
export const generateMetadata = (): Metadata => { export const generateMetadata = (): Metadata => {
return { return {
title: 'Sign Up' title: 'Sign Up',
}; };
}; };
const SignUpLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => { const SignUpLayout = ({
return ( children,
<div> }: Readonly<{ children: React.ReactNode }>) => {
{children} return <div>{children}</div>;
</div>
);
}; };
export default SignUpLayout; export default SignUpLayout;

View File

@ -386,7 +386,6 @@ 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>

View File

@ -2,15 +2,13 @@ import type { Metadata } from 'next';
export const generateMetadata = (): Metadata => { export const generateMetadata = (): Metadata => {
return { return {
title: 'Status List' title: 'Status List',
}; };
}; };
const SignInLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => { const SignInLayout = ({
return ( children,
<div> }: Readonly<{ children: React.ReactNode }>) => {
{children} return <div>{children}</div>;
</div>
);
}; };
export default SignInLayout; export default SignInLayout;

View File

@ -1,6 +1,6 @@
'use server'; 'use server';
import { StatusList } from '@/components/status'; import { StatusList } from '@/components/status/List';
import { getUser, getRecentUsersWithStatuses } from '@/lib/actions'; import { getUser, getRecentUsersWithStatuses } from '@/lib/actions';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';

View File

@ -1,17 +1,14 @@
import type { Metadata } from 'next'; import type { Metadata } from 'next';
export const generateMetadata = (): Metadata => { export const generateMetadata = (): Metadata => {
return { return {
title: 'Status Table' title: 'Status Table',
}; };
}; };
const SignInLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => { const SignInLayout = ({
return ( children,
<div> }: Readonly<{ children: React.ReactNode }>) => {
{children} return <div>{children}</div>;
</div>
);
}; };
export default SignInLayout; export default SignInLayout;

View File

@ -92,7 +92,6 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
const { const {
data: { subscription }, data: { subscription },
} = supabase.auth.onAuthStateChange(async (event, _session) => { } = supabase.auth.onAuthStateChange(async (event, _session) => {
console.log('Auth state change:', event); // Debug log console.log('Auth state change:', event); // Debug log
if (event === 'SIGNED_IN') { if (event === 'SIGNED_IN') {
// Background refresh without loading state // Background refresh without loading state

View File

@ -1,7 +1,12 @@
// src/components/providers/query-provider.tsx // src/components/providers/query-provider.tsx
'use client'; 'use client';
import { QueryClient, QueryClientProvider, QueryCache, MutationCache } from '@tanstack/react-query'; import {
QueryClient,
QueryClientProvider,
QueryCache,
MutationCache,
} from '@tanstack/react-query';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
@ -13,7 +18,8 @@ export const enum QueryErrorCodes {
} }
const queryCacheOnError = (error: unknown, query: any) => { const queryCacheOnError = (error: unknown, query: any) => {
const errorMessage = error instanceof Error ? error.message : 'Something went wrong'; const errorMessage =
error instanceof Error ? error.message : 'Something went wrong';
switch (query.meta?.errCode) { switch (query.meta?.errCode) {
case QueryErrorCodes.USERS_FETCH_FAILED: case QueryErrorCodes.USERS_FETCH_FAILED:
@ -26,8 +32,14 @@ const queryCacheOnError = (error: unknown, query: any) => {
} }
}; };
const mutationCacheOnError = (error: unknown, variables: unknown, context: unknown, mutation: any) => { const mutationCacheOnError = (
const errorMessage = error instanceof Error ? error.message : 'Something went wrong'; error: unknown,
variables: unknown,
context: unknown,
mutation: any,
) => {
const errorMessage =
error instanceof Error ? error.message : 'Something went wrong';
switch (mutation.meta?.errCode) { switch (mutation.meta?.errCode) {
case QueryErrorCodes.STATUS_UPDATE_FAILED: case QueryErrorCodes.STATUS_UPDATE_FAILED:
@ -44,7 +56,9 @@ type QueryProviderProps = {
}; };
export const QueryProvider = ({ children }: QueryProviderProps) => { export const QueryProvider = ({ children }: QueryProviderProps) => {
const [queryClient] = useState(() => new QueryClient({ const [queryClient] = useState(
() =>
new QueryClient({
queryCache: new QueryCache({ queryCache: new QueryCache({
onError: queryCacheOnError, onError: queryCacheOnError,
}), }),
@ -67,11 +81,10 @@ export const QueryProvider = ({ children }: QueryProviderProps) => {
retry: 1, retry: 1,
}, },
}, },
})); }),
);
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
{children}
</QueryClientProvider>
); );
}; };

View File

@ -40,13 +40,15 @@ export const SignInWithApple = ({
if (!profile.provider) { if (!profile.provider) {
const updateResponse = await updateProfile({ const updateResponse = await updateProfile({
provider: result.data.provider, provider: result.data.provider,
}) });
if (!updateResponse.success) throw new Error('Could not update provider!'); if (!updateResponse.success)
throw new Error('Could not update provider!');
} else { } else {
const updateResponse = await updateProfile({ const updateResponse = await updateProfile({
provider: profile.provider + ' ' + result.data.provider, provider: profile.provider + ' ' + result.data.provider,
}) });
if (!updateResponse.success) throw new Error('Could not update provider!'); if (!updateResponse.success)
throw new Error('Could not update provider!');
} }
} }
// Redirect to Apple OAuth page // Redirect to Apple OAuth page

View File

@ -39,13 +39,15 @@ export const SignInWithMicrosoft = ({
if (!profile.provider) { if (!profile.provider) {
const updateResponse = await updateProfile({ const updateResponse = await updateProfile({
provider: result.data.provider, provider: result.data.provider,
}) });
if (!updateResponse.success) throw new Error('Could not update provider!'); if (!updateResponse.success)
throw new Error('Could not update provider!');
} else { } else {
const updateResponse = await updateProfile({ const updateResponse = await updateProfile({
provider: profile.provider + ' ' + result.data.provider, provider: profile.provider + ' ' + result.data.provider,
}) });
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; window.location.href = result.data.url;

View File

@ -14,7 +14,12 @@ const Footer = () => {
hover:bg-gradient-to-tr hover:from-[#35363F] hover:to-[#23242F] hover:bg-gradient-to-tr hover:from-[#35363F] hover:to-[#23242F]
flex items-center gap-2 transition-all duration-200' flex items-center gap-2 transition-all duration-200'
> >
<Image src='/icons/misc/gitea.svg' alt='Gitea' width={20} height={20} /> <Image
src='/icons/misc/gitea.svg'
alt='Gitea'
width={20}
height={20}
/>
<span className='text-white'>View Source Code on Gitea</span> <span className='text-white'>View Source Code on Gitea</span>
</Link> </Link>
</div> </div>

View File

@ -1,9 +1,6 @@
import { useFileUpload } from '@/lib/hooks/useFileUpload'; import { useFileUpload } from '@/lib/hooks/useFileUpload';
import { useAuth } from '@/components/context'; import { useAuth } from '@/components/context';
import { import { BasedAvatar, CardContent } from '@/components/ui';
BasedAvatar,
CardContent,
} from '@/components/ui';
import { Loader2, Pencil, Upload } from 'lucide-react'; import { Loader2, Pencil, Upload } from 'lucide-react';
type AvatarUploadProps = { type AvatarUploadProps = {

369
src/components/status/List.tsx Executable file
View File

@ -0,0 +1,369 @@
'use client';
import { useState, useEffect, 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 { StatusMessage, SubmitButton } from '@/components/default';
import { toast } from 'sonner';
import { HistoryDrawer } from '@/components/status';
import type { Profile } from '@/utils/supabase';
import { makeConditionalClassName } from '@/lib/utils';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
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 ListProps = { initialStatuses: UserWithStatus[] };
export const StatusList = ({ initialStatuses = [] }: ListProps) => {
const { isAuthenticated } = useAuth();
const { tvMode } = useTVMode();
const queryClient = useQueryClient();
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 [connectionStatus, setConnectionStatus] = useState<
'connecting' | 'connected' | 'disconnected' | 'updating'
>('connecting');
const [newStatuses, setNewStatuses] = useState<Set<UserWithStatus>>(new Set());
const channelRef = useRef<RealtimeChannel | null>(null);
const supabaseRef = useRef(createClient());
const {
data: usersWithStatuses = initialStatuses,
isLoading: loading,
error,
refetch,
isFetching,
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 as Error}`)
throw error;
}
},
enabled: isAuthenticated,
refetchInterval: 30000, // 30 Second interval as we should rely on subscription.
refetchOnWindowFocus: true,
refetchOnMount: true,
initialData: initialStatuses,
meta: { errCode: QueryErrorCodes.USERS_FETCH_FAILED },
});
useEffect(() => {
if (!isAuthenticated) return;
const maxReconnectAttempts = 3;
let reconnectAttempts = 0;
let reconnectTimeout: NodeJS.Timeout;
let isComponentMounted = true;
let currentChannel: RealtimeChannel | null = null;
const setUpRealtimeConnection = () => {
if (!isComponentMounted) return;
if (currentChannel) {
supabaseRef.current.removeChannel(currentChannel)
.catch((error) => {
setConnectionStatus('disconnected');
console.error(`Error unsubscribing: ${error}`);
})
currentChannel = null;
}
setConnectionStatus('connecting');
const channel = supabaseRef.current
.channel('status_updates')
.on('broadcast', { event: 'status_updated' }, () => {
refetch().catch((error) => {
console.error(`Error refetching statuses: ${error as Error}`)
});
})
.subscribe((status) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
if (status === 'SUBSCRIBED') {
setConnectionStatus('connected');
reconnectAttempts = 0;
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
} else if (status === 'CHANNEL_ERROR') {
setConnectionStatus('disconnected');
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
} else if (status === 'CLOSED') {
setConnectionStatus('disconnected')
if (isComponentMounted && reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
const delay = 2000 * reconnectAttempts;
console.log(
`Reconnecting after close.
${reconnectAttempts} attempts of
${maxReconnectAttempts} in ${delay}ms
`);
if (reconnectTimeout) clearTimeout(reconnectTimeout);
reconnectTimeout = setTimeout(() => {
if (isComponentMounted) setUpRealtimeConnection();
}, delay);
} else {
console.warn('Max reconnection attempts reached or component is not mounted.');
setConnectionStatus('disconnected');
}
}
});
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) {
supabaseRef.current.removeChannel(currentChannel)
.catch((error) => {
console.error(`Error unsubscribing: ${error as Error}`)
});
channelRef.current = null;
}
setConnectionStatus('disconnected');
}
}, [isAuthenticated, refetch]);
const updateStatusMutation = useMutation({
mutationFn: async ({
usersWithStatuses,
status,
}: {
usersWithStatuses: UserWithStatus[];
status: string;
}) => {
const previousConnectionStatus = connectionStatus;
try {
if (usersWithStatuses.length <= 0) {
const result = await updateUserStatus(status);
if (!result.success) throw new Error(result.error);
return result.data;
} else {
const result = await updateStatuses(usersWithStatuses, status);
if (!result.success) throw new Error(result.error);
return result.data;
}
} catch (error) {
console.error(`Error updating statuses: ${error as Error}`);
}
},
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 to optimisticly updated statuses
const updatedStatuses = usersWithStatuses;
setNewStatuses((prev) => new Set([...prev, ...updatedStatuses]))
setTimeout(() => {
setNewStatuses((prev) => {
const updated = new Set(prev);
updatedStatuses.forEach((updatedStatus) => updated.delete(updatedStatus));
return updated;
});
}, 1000)
}
return { previousData };
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['users-with-statuses']} )
.catch((error) => console.error(`Error invalidating query: ${error}`));
if (!data) return;
data.forEach((statusUpdate) => {
toast.success(`
${statusUpdate.user.full_name}'s status updated to '${statusUpdate.status}'.
`);
});
setSelectedUsers([]);
setStatusInput('');
},
onError: (error, _variables, context) => {
if (context?.previousData)
queryClient.setQueryData(['users-with-statuses'], context.previousData);
toast.error(`Error updating statuses: ${error}`);
},
});
const handleUpdateStatus = () => {
if (!isAuthenticated) {
setUpdateStatusMessage('You must be signed in to update technican statuses!')
return;
} else if (!statusInput.trim()) {
setUpdateStatusMessage('Your status must be in between 3 & 80 characters long!');
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 = () => {
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' />;
case 'updating':
return <RefreshCw className='w-4 h-4 text-blue-500 animate-spin' />;
}
};
const getConnectionText = () => {
switch (connectionStatus) {
case 'connected':
return 'Connected';
case 'connecting':
return 'Connecting...';
case 'disconnected':
return 'Disconnected';
case 'updating':
return 'Updating...';
}
};
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) {
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: 'mx-auto space-y-4',
on: 'lg:w-11/12 w-full mt-15',
off: 'w-5/6',
});
const headerClassName = makeConditionalClassName({
context: tvMode,
defaultClassName: '',
on: 'none',
off: 'flex items-center justify-between mb-4',
})
const cardClassName = makeConditionalClassName({
context: tvMode,
defaultClassName: 'transition-all duration-300 hover:shadow-md',
on: 'lg:text-4xl',
off: 'lg:text-base',
});
return (
<div className={containerClassName}>
<div className={headerClassName}>
<div className='flex items-center gap-2'>
<Checkbox
id='select-all'
checked={selectAll}
onCheckedChange={handleSelectAllChange}
/>
<label htmlFor='select-all' className='text-sm font-medium'>
Select All
</label>
<Badge variant='outline' className='flex items-center gap-2'>
{getConnectionIcon()}
<span className='text-sm'>{getConnectionText()}</span>
</Badge>
</div>
</div>
</div>
);
};

View File

@ -1,29 +1,35 @@
"use client" 'use client';
import { useState, useEffect, useRef } from "react" import { useState, useEffect, 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 {
import { BasedAvatar, Drawer, DrawerTrigger, Loading } from "@/components/ui" getRecentUsersWithStatuses,
import { SubmitButton } from "@/components/default" updateStatuses,
import { toast } from "sonner" updateUserStatus,
import { HistoryDrawer } from "@/components/status" type UserWithStatus,
import type { Profile } from "@/utils/supabase" } from '@/lib/hooks';
import { makeConditionalClassName } from "@/lib/utils" import { BasedAvatar, Drawer, DrawerTrigger, Loading } from '@/components/ui';
import { Card, CardContent, CardHeader } from "@/components/ui/card" import { SubmitButton } from '@/components/default';
import { Badge } from "@/components/ui/badge" import { toast } from 'sonner';
import { Checkbox } from "@/components/ui/checkbox" import { HistoryDrawer } from '@/components/status';
import { Input } from "@/components/ui/input" import type { Profile } from '@/utils/supabase';
import { Button } from "@/components/ui/button" import { makeConditionalClassName } from '@/lib/utils';
import { RefreshCw, Clock, Wifi, WifiOff } from "lucide-react" import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Badge } from '@/components/ui/badge';
import { QueryErrorCodes } from "@/components/context"; import { Checkbox } from '@/components/ui/checkbox';
import { createClient } from "@/utils/supabase"; import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
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'; import type { RealtimeChannel } from '@supabase/supabase-js';
type StatusListProps = { type StatusListProps = {
initialStatuses: UserWithStatus[] initialStatuses: UserWithStatus[];
} };
export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fixed props destructuring export const StatusList = ({ initialStatuses = [] }: StatusListProps) => {
// Fixed props destructuring
const { isAuthenticated } = useAuth(); const { isAuthenticated } = useAuth();
const { tvMode } = useTVMode(); const { tvMode } = useTVMode();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@ -31,14 +37,16 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
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 [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());
// Keep all your existing React Query code exactly as is // Keep all your existing React Query code exactly as is
const { const {
data: usersWithStatuses = initialStatuses, data: usersWithStatuses = initialStatuses,
@ -55,7 +63,9 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
if (!response.success) throw new Error(response.error); if (!response.success) throw new Error(response.error);
return response.data; return response.data;
} catch (error) { } catch (error) {
toast.error(`Error fetching technicians: ${error instanceof Error ? error.message : 'Unknown error'}`); toast.error(
`Error fetching technicians: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
throw error; throw error;
} }
}, },
@ -71,14 +81,14 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
useEffect(() => { useEffect(() => {
if (!isAuthenticated) return; 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;
if (currentChannel) { if (currentChannel) {
supabaseRef.current.removeChannel(currentChannel).catch((error) => { supabaseRef.current.removeChannel(currentChannel).catch((error) => {
console.error(`Error unsubscribing: ${error}`); console.error(`Error unsubscribing: ${error}`);
@ -99,7 +109,7 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
if (status === 'SUBSCRIBED') { if (status === 'SUBSCRIBED') {
console.log('Realtime connection established'); console.log('Realtime connection established');
setConnectionStatus('connected'); setConnectionStatus('connected');
reconnectAttempts = 0 reconnectAttempts = 0;
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
} else if (status === 'CHANNEL_ERROR') { } else if (status === 'CHANNEL_ERROR') {
console.log('Realtime connection failed, relying on polling'); console.log('Realtime connection failed, relying on polling');
@ -108,42 +118,47 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
} else if (status === 'CLOSED') { } else if (status === 'CLOSED') {
console.log('Realtime connection closed'); console.log('Realtime connection closed');
setConnectionStatus('disconnected'); setConnectionStatus('disconnected');
if (isComponentMounted && reconnectAttempts < maxReconnectAttempts) { if (
reconnectAttempts++ isComponentMounted &&
const delay = 2000 * reconnectAttempts reconnectAttempts < maxReconnectAttempts
) {
reconnectAttempts++;
const delay = 2000 * reconnectAttempts;
console.log(`Reconnecting after close ${reconnectAttempts}/${maxReconnectAttempts} in ${delay}ms`) console.log(
`Reconnecting after close ${reconnectAttempts}/${maxReconnectAttempts} in ${delay}ms`,
);
if (reconnectTimeout) { if (reconnectTimeout) {
clearTimeout(reconnectTimeout) clearTimeout(reconnectTimeout);
} }
reconnectTimeout = setTimeout(() => { reconnectTimeout = setTimeout(() => {
if (isComponentMounted) { if (isComponentMounted) {
setUpRealtimeConnection() setUpRealtimeConnection();
} }
}, delay) }, delay);
} else { } else {
console.log("Max reconnection attempts reached or component unmounted") console.log(
'Max reconnection attempts reached or component unmounted',
);
} }
} }
}); });
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) clearTimeout(reconnectTimeout);
if (reconnectTimeout)
clearTimeout(reconnectTimeout)
if (currentChannel) { if (currentChannel) {
console.log('Cleaning up realtime connection...'); console.log('Cleaning up realtime connection...');
@ -157,7 +172,13 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
// Updated mutation // Updated mutation
const updateStatusMutation = useMutation({ const updateStatusMutation = useMutation({
mutationFn: async ({ usersWithStatuses, status }: { usersWithStatuses: UserWithStatus[], status: string }) => { mutationFn: async ({
usersWithStatuses,
status,
}: {
usersWithStatuses: UserWithStatus[];
status: string;
}) => {
if (usersWithStatuses.length === 0) { if (usersWithStatuses.length === 0) {
const result = await updateUserStatus(status); const result = await updateUserStatus(status);
if (!result.success) throw new Error(result.error); if (!result.success) throw new Error(result.error);
@ -172,12 +193,18 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
onMutate: async ({ usersWithStatuses, status }) => { onMutate: async ({ usersWithStatuses, status }) => {
// Optimistic update logic // Optimistic update logic
await queryClient.cancelQueries({ queryKey: ['users-with-statuses'] }); await queryClient.cancelQueries({ queryKey: ['users-with-statuses'] });
const previousData = queryClient.getQueryData<UserWithStatus[]>(['users-with-statuses']); const previousData = queryClient.getQueryData<UserWithStatus[]>([
'users-with-statuses',
]);
if (previousData && usersWithStatuses.length > 0) { if (previousData && usersWithStatuses.length > 0) {
const now = new Date().toISOString(); const now = new Date().toISOString();
const optimisticData = previousData.map(userStatus => { const optimisticData = previousData.map((userStatus) => {
if (usersWithStatuses.some(selected => selected.user.id === userStatus.user.id)) { if (
usersWithStatuses.some(
(selected) => selected.user.id === userStatus.user.id,
)
) {
return { return {
...userStatus, ...userStatus,
status, status,
@ -189,14 +216,14 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
queryClient.setQueryData(['users-with-statuses'], optimisticData); queryClient.setQueryData(['users-with-statuses'], optimisticData);
// Add animation for optimistic updates // Add animation for optimistic updates
const updatedIds = usersWithStatuses.map(u => u.user.id); const updatedIds = usersWithStatuses.map((u) => u.user.id);
setNewStatusIds(prev => new Set([...prev, ...updatedIds])); setNewStatusIds((prev) => new Set([...prev, ...updatedIds]));
// Remove animation after 1 second // Remove animation after 1 second
setTimeout(() => { setTimeout(() => {
setNewStatusIds(prev => { setNewStatusIds((prev) => {
const updated = new Set(prev); const updated = new Set(prev);
updatedIds.forEach(id => updated.delete(id)); updatedIds.forEach((id) => updated.delete(id));
return updated; return updated;
}); });
}, 1000); }, 1000);
@ -217,7 +244,8 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
setSelectedUsers([]); setSelectedUsers([]);
setStatusInput(''); setStatusInput('');
}, },
onError: (error, _variables, context) => { // Fixed unused variables onError: (error, _variables, context) => {
// Fixed unused variables
// Rollback optimistic update // Rollback optimistic update
if (context?.previousData) { if (context?.previousData) {
queryClient.setQueryData(['users-with-statuses'], context.previousData); queryClient.setQueryData(['users-with-statuses'], context.previousData);
@ -239,15 +267,15 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
updateStatusMutation.mutate({ updateStatusMutation.mutate({
usersWithStatuses: selectedUsers, usersWithStatuses: selectedUsers,
status: statusInput.trim() status: statusInput.trim(),
}); });
}; };
const handleCheckboxChange = (user: UserWithStatus) => { const handleCheckboxChange = (user: UserWithStatus) => {
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],
); );
}; };
@ -263,31 +291,31 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
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]);
const getConnectionIcon = () => { const getConnectionIcon = () => {
switch (connectionStatus) { switch (connectionStatus) {
case "connected": case 'connected':
return <Wifi className="w-4 h-4 text-green-500" /> return <Wifi className='w-4 h-4 text-green-500' />;
case "connecting": case 'connecting':
return <Wifi className="w-4 h-4 text-yellow-500 animate-pulse" /> return <Wifi className='w-4 h-4 text-yellow-500 animate-pulse' />;
case "disconnected": case 'disconnected':
return <WifiOff className="w-4 h-4 text-red-500" /> return <WifiOff className='w-4 h-4 text-red-500' />;
}
} }
};
const getConnectionText = () => { const getConnectionText = () => {
switch (connectionStatus) { switch (connectionStatus) {
case "connected": case 'connected':
return "Connected" return 'Connected';
case "connecting": case 'connecting':
return "Connecting..." return 'Connecting...';
case "disconnected": case 'disconnected':
return "Disconnected" return 'Disconnected';
}
} }
};
const formatTime = (timestamp: string) => { const formatTime = (timestamp: string) => {
const date = new Date(timestamp); const date = new Date(timestamp);
@ -343,12 +371,12 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
Tech Status Tech Status
</h2> </h2>
{isFetching ? ( {isFetching ? (
<Badge variant="outline" className="flex items-center gap-2"> <Badge variant='outline' className='flex items-center gap-2'>
<RefreshCw className='w-4 h-4 animate-spin text-blue-500' /> <RefreshCw className='w-4 h-4 animate-spin text-blue-500' />
<span className='text-xs'>Updating...</span> <span className='text-xs'>Updating...</span>
</Badge> </Badge>
) : ( ) : (
<Badge variant="outline" className="flex items-center gap-2"> <Badge variant='outline' className='flex items-center gap-2'>
{getConnectionIcon()} {getConnectionIcon()}
<span className='text-xs'>{getConnectionText()}</span> <span className='text-xs'>{getConnectionText()}</span>
</Badge> </Badge>
@ -371,9 +399,12 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
{/* Status Cards */} {/* Status Cards */}
<div className='space-y-3'> <div className='space-y-3'>
{usersWithStatuses.map((userWithStatus) => { {usersWithStatuses.map((userWithStatus) => {
const isSelected = selectedUsers.some(u => u.user.id === 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 && const isUpdatedByOther =
userWithStatus.updated_by &&
userWithStatus.updated_by.id !== userWithStatus.user.id; userWithStatus.updated_by.id !== userWithStatus.user.id;
return ( return (
@ -392,7 +423,9 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
{!tvMode && ( {!tvMode && (
<Checkbox <Checkbox
checked={isSelected} checked={isSelected}
onCheckedChange={() => handleCheckboxChange(userWithStatus)} onCheckedChange={() =>
handleCheckboxChange(userWithStatus)
}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
/> />
)} )}
@ -402,7 +435,9 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
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 && (
@ -413,7 +448,9 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
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>
)} )}
@ -438,7 +475,9 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
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>
@ -455,7 +494,9 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
{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'}`}> <p
className={`text-muted-foreground ${tvMode ? 'text-4xl' : 'text-lg'}`}
>
No status updates yet No status updates yet
</p> </p>
</Card> </Card>
@ -475,7 +516,11 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
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 && !updateStatusMutation.isPending) { if (
e.key === 'Enter' &&
!e.shiftKey &&
!updateStatusMutation.isPending
) {
e.preventDefault(); e.preventDefault();
handleUpdateStatus(); handleUpdateStatus();
} }
@ -491,8 +536,7 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
? 'Updating...' ? 'Updating...'
: selectedUsers.length > 0 : selectedUsers.length > 0
? `Update ${selectedUsers.length} Users` ? `Update ${selectedUsers.length} Users`
: 'Update Status' : 'Update Status'}
}
</SubmitButton> </SubmitButton>
</div> </div>
{selectedUsers.length > 0 && ( {selectedUsers.length > 0 && (

View File

@ -7,12 +7,7 @@ import {
updateUserStatus, updateUserStatus,
type UserWithStatus, type UserWithStatus,
} from '@/lib/hooks'; } from '@/lib/hooks';
import { import { BasedAvatar, Drawer, DrawerTrigger, Loading } from '@/components/ui';
BasedAvatar,
Drawer,
DrawerTrigger,
Loading
} from '@/components/ui';
import { SubmitButton } from '@/components/default'; import { SubmitButton } from '@/components/default';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { HistoryDrawer } from '@/components/status'; import { HistoryDrawer } from '@/components/status';
@ -30,17 +25,18 @@ type TechTableProps = {
initialStatuses: UserWithStatus[]; initialStatuses: UserWithStatus[];
}; };
export const TechTable = ({ export const TechTable = ({ initialStatuses = [] }: TechTableProps) => {
initialStatuses = [],
}: TechTableProps) => {
const { isAuthenticated } = useAuth(); const { isAuthenticated } = useAuth();
const { tvMode } = useTVMode(); const { tvMode } = useTVMode();
const queryClient = useQueryClient(); 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 [selectedHistoryUser, setSelectedHistoryUser] = useState<Profile | null>(null); const [selectedHistoryUser, setSelectedHistoryUser] =
const [connectionStatus, setConnectionStatus] = useState<"connecting" | "connected" | "disconnected">("connecting"); useState<Profile | null>(null);
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());
@ -61,7 +57,9 @@ export const TechTable = ({
if (!response.success) throw new Error(response.error); if (!response.success) throw new Error(response.error);
return response.data; return response.data;
} catch (error) { } catch (error) {
toast.error(`Error fetching technicians: ${error instanceof Error ? error.message : 'Unknown error'}`); toast.error(
`Error fetching technicians: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
throw error; throw error;
} }
}, },
@ -76,14 +74,14 @@ export const TechTable = ({
// Add this new useEffect for realtime enhancement // Add this new useEffect for realtime enhancement
useEffect(() => { useEffect(() => {
if (!isAuthenticated) return; 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;
if (currentChannel) { if (currentChannel) {
supabaseRef.current.removeChannel(currentChannel).catch((error) => { supabaseRef.current.removeChannel(currentChannel).catch((error) => {
console.error(`Error unsubscribing: ${error}`); console.error(`Error unsubscribing: ${error}`);
@ -104,7 +102,7 @@ export const TechTable = ({
if (status === 'SUBSCRIBED') { if (status === 'SUBSCRIBED') {
console.log('Realtime connection established'); console.log('Realtime connection established');
setConnectionStatus('connected'); setConnectionStatus('connected');
reconnectAttempts = 0 reconnectAttempts = 0;
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
} else if (status === 'CHANNEL_ERROR') { } else if (status === 'CHANNEL_ERROR') {
console.log('Realtime connection failed, relying on polling'); console.log('Realtime connection failed, relying on polling');
@ -113,39 +111,44 @@ export const TechTable = ({
} else if (status === 'CLOSED') { } else if (status === 'CLOSED') {
console.log('Realtime connection closed'); console.log('Realtime connection closed');
setConnectionStatus('disconnected'); setConnectionStatus('disconnected');
if (isComponentMounted && reconnectAttempts < maxReconnectAttempts) { if (
reconnectAttempts++ isComponentMounted &&
const delay = 2000 * reconnectAttempts reconnectAttempts < maxReconnectAttempts
console.log(`Reconnecting after close ${reconnectAttempts}/${maxReconnectAttempts} in ${delay}ms`) ) {
reconnectAttempts++;
const delay = 2000 * reconnectAttempts;
console.log(
`Reconnecting after close ${reconnectAttempts}/${maxReconnectAttempts} in ${delay}ms`,
);
if (reconnectTimeout) { if (reconnectTimeout) {
clearTimeout(reconnectTimeout) clearTimeout(reconnectTimeout);
} }
reconnectTimeout = setTimeout(() => { reconnectTimeout = setTimeout(() => {
if (isComponentMounted) { if (isComponentMounted) {
setUpRealtimeConnection() setUpRealtimeConnection();
} }
}, delay) }, delay);
} else { } else {
console.log("Max reconnection attempts reached or component unmounted") console.log(
'Max reconnection attempts reached or component unmounted',
);
} }
} }
}); });
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) clearTimeout(reconnectTimeout);
if (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) => {
@ -158,7 +161,13 @@ export const TechTable = ({
// Updated mutation // Updated mutation
const updateStatusMutation = useMutation({ const updateStatusMutation = useMutation({
mutationFn: async ({ usersWithStatuses, status }: { usersWithStatuses: UserWithStatus[], status: string }) => { mutationFn: async ({
usersWithStatuses,
status,
}: {
usersWithStatuses: UserWithStatus[];
status: string;
}) => {
if (usersWithStatuses.length === 0) { if (usersWithStatuses.length === 0) {
const result = await updateUserStatus(status); const result = await updateUserStatus(status);
if (!result.success) throw new Error(result.error); if (!result.success) throw new Error(result.error);
@ -173,11 +182,17 @@ export const TechTable = ({
onMutate: async ({ usersWithStatuses, status }) => { onMutate: async ({ usersWithStatuses, status }) => {
// Optimistic update logic // Optimistic update logic
await queryClient.cancelQueries({ queryKey: ['users-with-statuses'] }); await queryClient.cancelQueries({ queryKey: ['users-with-statuses'] });
const previousData = queryClient.getQueryData<UserWithStatus[]>(['users-with-statuses']); const previousData = queryClient.getQueryData<UserWithStatus[]>([
'users-with-statuses',
]);
if (previousData && usersWithStatuses.length > 0) { if (previousData && usersWithStatuses.length > 0) {
const now = new Date().toISOString(); const now = new Date().toISOString();
const optimisticData = previousData.map(userStatus => { const optimisticData = previousData.map((userStatus) => {
if (usersWithStatuses.some(selected => selected.user.id === userStatus.user.id)) { if (
usersWithStatuses.some(
(selected) => selected.user.id === userStatus.user.id,
)
) {
return { return {
...userStatus, ...userStatus,
status, status,
@ -188,13 +203,13 @@ export const TechTable = ({
}); });
queryClient.setQueryData(['users-with-statuses'], optimisticData); queryClient.setQueryData(['users-with-statuses'], optimisticData);
// Add animation for optimistic updates // Add animation for optimistic updates
const updatedIds = usersWithStatuses.map(u => u.user.id); const updatedIds = usersWithStatuses.map((u) => u.user.id);
setNewStatusIds(prev => new Set([...prev, ...updatedIds])); setNewStatusIds((prev) => new Set([...prev, ...updatedIds]));
// Remove animation after 1 second // Remove animation after 1 second
setTimeout(() => { setTimeout(() => {
setNewStatusIds(prev => { setNewStatusIds((prev) => {
const updated = new Set(prev); const updated = new Set(prev);
updatedIds.forEach(id => updated.delete(id)); updatedIds.forEach((id) => updated.delete(id));
return updated; return updated;
}); });
}, 1000); }, 1000);
@ -212,7 +227,8 @@ export const TechTable = ({
setSelectedUsers([]); setSelectedUsers([]);
setStatusInput(''); setStatusInput('');
}, },
onError: (error, _variables, context) => { // Fixed unused variables onError: (error, _variables, context) => {
// Fixed unused variables
// Rollback optimistic update // Rollback optimistic update
if (context?.previousData) { if (context?.previousData) {
queryClient.setQueryData(['users-with-statuses'], context.previousData); queryClient.setQueryData(['users-with-statuses'], context.previousData);
@ -233,15 +249,15 @@ export const TechTable = ({
} }
updateStatusMutation.mutate({ updateStatusMutation.mutate({
usersWithStatuses: selectedUsers, usersWithStatuses: selectedUsers,
status: statusInput.trim() status: statusInput.trim(),
}); });
}; };
const handleCheckboxChange = (user: UserWithStatus) => { const handleCheckboxChange = (user: UserWithStatus) => {
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],
); );
}; };
@ -257,31 +273,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]);
const getConnectionIcon = () => { const getConnectionIcon = () => {
switch (connectionStatus) { switch (connectionStatus) {
case "connected": case 'connected':
return <Wifi className="w-4 h-4 text-green-500" /> return <Wifi className='w-4 h-4 text-green-500' />;
case "connecting": case 'connecting':
return <Wifi className="w-4 h-4 text-yellow-500 animate-pulse" /> return <Wifi className='w-4 h-4 text-yellow-500 animate-pulse' />;
case "disconnected": case 'disconnected':
return <WifiOff className="w-4 h-4 text-red-500" /> return <WifiOff className='w-4 h-4 text-red-500' />;
}
} }
};
const getConnectionText = () => { const getConnectionText = () => {
switch (connectionStatus) { switch (connectionStatus) {
case "connected": case 'connected':
return "Connected" return 'Connected';
case "connecting": case 'connecting':
return "Connecting..." return 'Connecting...';
case "disconnected": case 'disconnected':
return "Disconnected" return 'Disconnected';
}
} }
};
const formatTime = (timestamp: string) => { const formatTime = (timestamp: string) => {
const date = new Date(timestamp); const date = new Date(timestamp);
@ -340,12 +356,12 @@ export const TechTable = ({
{/* Status Header */} {/* Status Header */}
<div className='flex items-center justify-between mb-6'> <div className='flex items-center justify-between mb-6'>
{isFetching ? ( {isFetching ? (
<Badge variant="outline" className="flex items-center gap-2"> <Badge variant='outline' className='flex items-center gap-2'>
<RefreshCw className='w-4 h-4 animate-spin text-blue-500' /> <RefreshCw className='w-4 h-4 animate-spin text-blue-500' />
<span className='text-xs'>Updating...</span> <span className='text-xs'>Updating...</span>
</Badge> </Badge>
) : ( ) : (
<Badge variant="outline" className="flex items-center gap-2"> <Badge variant='outline' className='flex items-center gap-2'>
{getConnectionIcon()} {getConnectionIcon()}
<span className='text-xs'>{getConnectionText()}</span> <span className='text-xs'>{getConnectionText()}</span>
</Badge> </Badge>
@ -368,9 +384,7 @@ export const TechTable = ({
<th className={thClassName}>Technician</th> <th className={thClassName}>Technician</th>
<th className={thClassName}> <th className={thClassName}>
<Drawer> <Drawer>
<DrawerTrigger <DrawerTrigger className='hover:text-foreground/60 cursor-pointer'>
className='hover:text-foreground/60 cursor-pointer'
>
Status Status
</DrawerTrigger> </DrawerTrigger>
<HistoryDrawer /> <HistoryDrawer />
@ -381,7 +395,9 @@ export const TechTable = ({
</thead> </thead>
<tbody> <tbody>
{usersWithStatuses.map((userWithStatus, index) => { {usersWithStatuses.map((userWithStatus, index) => {
const isSelected = selectedUsers.some(u => u.user.id === 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);
return ( return (
@ -412,18 +428,23 @@ export const TechTable = ({
className={tvMode ? 'w-16 h-16' : 'w-12 h-12'} className={tvMode ? 'w-16 h-16' : 'w-12 h-12'}
/> />
<div> <div>
<p className={`font-semibold ${tvMode ? 'text-5xl' : 'text-lg'}`}> <p
className={`font-semibold ${tvMode ? 'text-5xl' : 'text-lg'}`}
>
{userWithStatus.user.full_name ?? 'Unknown User'} {userWithStatus.user.full_name ?? 'Unknown User'}
</p> </p>
{userWithStatus.updated_by && {userWithStatus.updated_by &&
userWithStatus.updated_by.id !== userWithStatus.user.id && ( userWithStatus.updated_by.id !==
userWithStatus.user.id && (
<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'
/> />
<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>
</div> </div>
@ -435,7 +456,9 @@ export const TechTable = ({
<Drawer> <Drawer>
<DrawerTrigger <DrawerTrigger
className='text-center w-full p-2 rounded-md hover:bg-muted transition-colors' className='text-center w-full p-2 rounded-md hover:bg-muted transition-colors'
onClick={() => setSelectedHistoryUser(userWithStatus.user)} onClick={() =>
setSelectedHistoryUser(userWithStatus.user)
}
> >
{userWithStatus.status} {userWithStatus.status}
</DrawerTrigger> </DrawerTrigger>
@ -455,7 +478,9 @@ export const TechTable = ({
{usersWithStatuses.length === 0 && ( {usersWithStatuses.length === 0 && (
<div className='p-8 text-center'> <div className='p-8 text-center'>
<p className={`text-muted-foreground ${tvMode ? 'text-4xl' : 'text-lg'}`}> <p
className={`text-muted-foreground ${tvMode ? 'text-4xl' : 'text-lg'}`}
>
No status updates yet No status updates yet
</p> </p>
</div> </div>
@ -497,8 +522,7 @@ export const TechTable = ({
? 'Updating...' ? 'Updating...'
: selectedUsers.length > 0 : selectedUsers.length > 0
? `Update ${selectedUsers.length} Users` ? `Update ${selectedUsers.length} Users`
: 'Update Status' : 'Update Status'}
}
</SubmitButton> </SubmitButton>
</div> </div>
)} )}

View File

@ -5,7 +5,6 @@ import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { User } from 'lucide-react'; import { User } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
type BasedAvatarProps = React.ComponentProps<typeof AvatarPrimitive.Root> & { type BasedAvatarProps = React.ComponentProps<typeof AvatarPrimitive.Root> & {
src?: string | null; src?: string | null;
fullName?: string | null; fullName?: string | null;
@ -33,10 +32,7 @@ function BasedAvatar({
{...props} {...props}
> >
{src ? ( {src ? (
<AvatarImage <AvatarImage src={src} className={imageClassName} />
src={src}
className={imageClassName}
/>
) : ( ) : (
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
data-slot='avatar-fallback' data-slot='avatar-fallback'
@ -92,7 +88,9 @@ function AvatarImage({
function AvatarFallback({ function AvatarFallback({
className, className,
...props ...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback & {fullName: string}>) { }: React.ComponentProps<
typeof AvatarPrimitive.Fallback & { fullName: string }
>) {
return ( return (
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
data-slot='avatar-fallback' data-slot='avatar-fallback'

View File

@ -32,7 +32,7 @@ export const Loading: React.FC<Loading_Props> = ({
}, [intervalMs, alpha]); }, [intervalMs, alpha]);
return ( return (
<div className="items-center justify-center w-1/3 m-auto pt-20"> <div className='items-center justify-center w-1/3 m-auto pt-20'>
<Progress value={progress} className={className} {...props} /> <Progress value={progress} className={className} {...props} />
</div> </div>
); );

View File

@ -10,8 +10,7 @@ export const getProfile = async (
try { try {
if (userId == null) { if (userId == null) {
const user = await getUser(); const user = await getUser();
if (!user.success || !user.data.id) if (!user.success || !user.data.id) throw new Error('User not found');
throw new Error('User not found');
userId = user.data.id; userId = user.data.id;
} }
const supabase = await createServerClient(); const supabase = await createServerClient();
@ -35,7 +34,7 @@ export const getProfile = async (
}; };
export const getProfileWithAvatar = async ( export const getProfileWithAvatar = async (
userId: string | null = null userId: string | null = null,
): Promise<Result<Profile>> => { ): Promise<Result<Profile>> => {
try { try {
if (userId === null) { if (userId === null) {
@ -93,7 +92,8 @@ export const updateProfile = async ({
email === undefined && email === undefined &&
avatar_url === undefined && avatar_url === undefined &&
provider === undefined provider === undefined
) throw new Error('No profile data provided'); )
throw new Error('No profile data provided');
const userResponse = await getUser(); const userResponse = await getUser();
if (!userResponse.success || userResponse.data === undefined) if (!userResponse.success || userResponse.data === undefined)

View File

@ -40,12 +40,14 @@ export const getRecentUsersWithStatuses = async (): Promise<
const { data, error } = (await supabase const { data, error } = (await supabase
.from('statuses') .from('statuses')
.select(` .select(
`
user:profiles!user_id(*), user:profiles!user_id(*),
status, status,
created_at, created_at,
updated_by:profiles!updated_by_id(*) updated_by:profiles!updated_by_id(*)
`) `,
)
.gte('created_at', oneDayAgo.toISOString()) .gte('created_at', oneDayAgo.toISOString())
.order('created_at', { ascending: false })) as { .order('created_at', { ascending: false })) as {
data: UserWithStatus[]; data: UserWithStatus[];
@ -66,11 +68,13 @@ export const getRecentUsersWithStatuses = async (): Promise<
const filteredWithAvatars = new Array<UserWithStatus>(); const filteredWithAvatars = new Array<UserWithStatus>();
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(
await getAvatarUrl(userWithStatus.user.avatar_url); userWithStatus.user.avatar_url,
);
if (userWithStatus.updated_by?.avatar_url) if (userWithStatus.updated_by?.avatar_url)
userWithStatus.updated_by.avatar_url = userWithStatus.updated_by.avatar_url = await getAvatarUrl(
await getAvatarUrl(userWithStatus.updated_by?.avatar_url); userWithStatus.updated_by?.avatar_url,
);
filteredWithAvatars.push(userWithStatus); filteredWithAvatars.push(userWithStatus);
} }
@ -119,27 +123,31 @@ export const updateStatuses = async (
if (!profileResponse.success) throw new Error('Not authenticated!'); if (!profileResponse.success) throw new Error('Not authenticated!');
const user = profileResponse.data; const user = profileResponse.data;
const { const { data: insertedStatuses, error: insertedStatusesError } =
data: insertedStatuses, await supabase
error: insertedStatusesError
} = await supabase
.from('statuses') .from('statuses')
.insert(usersWithStatuses.map((userWithStatus) => ({ .insert(
usersWithStatuses.map((userWithStatus) => ({
user_id: userWithStatus.user.id, user_id: userWithStatus.user.id,
status, status,
updated_by_id: user.id, updated_by_id: user.id,
}))) })),
)
.select(); .select();
if (insertedStatusesError) throw new Error('Couldn\'t insert statuses!'); if (insertedStatusesError) throw new Error("Couldn't insert statuses!");
else if (insertedStatuses) { else if (insertedStatuses) {
const createdAtFallback = new Date(Date.now()).toISOString(); const createdAtFallback = new Date(Date.now()).toISOString();
await broadcastStatusUpdates(usersWithStatuses.map((s, i) => { return { await broadcastStatusUpdates(
usersWithStatuses.map((s, i) => {
return {
user: s.user, user: s.user,
status: status, status: status,
created_at: insertedStatuses[i]?.created_at ?? createdAtFallback, created_at: insertedStatuses[i]?.created_at ?? createdAtFallback,
updated_by: user, updated_by: user,
}})); };
}),
);
} }
return { success: true, data: undefined }; return { success: true, data: undefined };
} catch (error) { } catch (error) {
@ -171,12 +179,14 @@ export const updateUserStatus = async (
.single(); .single();
if (insertedStatusError) throw insertedStatusError as Error; if (insertedStatusError) throw insertedStatusError as Error;
await broadcastStatusUpdates([{ await broadcastStatusUpdates([
{
user: userProfile, user: userProfile,
status: insertedStatus.status, status: insertedStatus.status,
created_at: insertedStatus.created_at, created_at: insertedStatus.created_at,
updated_by: userProfile, updated_by: userProfile,
}]); },
]);
return { success: true, data: undefined }; return { success: true, data: undefined };
} catch (error) { } catch (error) {

View File

@ -10,8 +10,7 @@ export const getProfile = async (
try { try {
if (userId == null) { if (userId == null) {
const user = await getUser(); const user = await getUser();
if (!user.success || !user.data.id) if (!user.success || !user.data.id) throw new Error('User not found');
throw new Error('User not found');
userId = user.data.id; userId = user.data.id;
} }
const supabase = createClient(); const supabase = createClient();
@ -35,7 +34,7 @@ export const getProfile = async (
}; };
export const getProfileWithAvatar = async ( export const getProfileWithAvatar = async (
userId: string | null = null userId: string | null = null,
): Promise<Result<Profile>> => { ): Promise<Result<Profile>> => {
try { try {
if (userId === null) { if (userId === null) {
@ -93,7 +92,8 @@ export const updateProfile = async ({
email === undefined && email === undefined &&
avatar_url === undefined && avatar_url === undefined &&
provider === undefined provider === undefined
) throw new Error('No profile data provided'); )
throw new Error('No profile data provided');
const userResponse = await getUser(); const userResponse = await getUser();
if (!userResponse.success || userResponse.data === undefined) if (!userResponse.success || userResponse.data === undefined)

View File

@ -40,12 +40,14 @@ export const getRecentUsersWithStatuses = async (): Promise<
const { data, error } = (await supabase const { data, error } = (await supabase
.from('statuses') .from('statuses')
.select(` .select(
`
user:profiles!user_id(*), user:profiles!user_id(*),
status, status,
created_at, created_at,
updated_by:profiles!updated_by_id(*) updated_by:profiles!updated_by_id(*)
`) `,
)
.gte('created_at', oneDayAgo.toISOString()) .gte('created_at', oneDayAgo.toISOString())
.order('created_at', { ascending: false })) as { .order('created_at', { ascending: false })) as {
data: UserWithStatus[]; data: UserWithStatus[];
@ -66,11 +68,13 @@ export const getRecentUsersWithStatuses = async (): Promise<
const filteredWithAvatars = new Array<UserWithStatus>(); const filteredWithAvatars = new Array<UserWithStatus>();
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(
await getAvatarUrl(userWithStatus.user.avatar_url); userWithStatus.user.avatar_url,
);
if (userWithStatus.updated_by?.avatar_url) if (userWithStatus.updated_by?.avatar_url)
userWithStatus.updated_by.avatar_url = userWithStatus.updated_by.avatar_url = await getAvatarUrl(
await getAvatarUrl(userWithStatus.updated_by?.avatar_url); userWithStatus.updated_by?.avatar_url,
);
filteredWithAvatars.push(userWithStatus); filteredWithAvatars.push(userWithStatus);
} }
@ -112,47 +116,52 @@ export const broadcastStatusUpdates = async (
export const updateStatuses = async ( export const updateStatuses = async (
usersWithStatuses: UserWithStatus[], usersWithStatuses: UserWithStatus[],
status: string, status: string,
): Promise<Result<void>> => { ): Promise<Result<UserWithStatus[]>> => {
try { try {
const supabase = createClient(); const supabase = createClient();
const profileResponse = await getProfileWithAvatar(); const profileResponse = await getProfileWithAvatar();
if (!profileResponse.success) throw new Error('Not authenticated!'); if (!profileResponse.success) throw new Error('Not authenticated!');
const user = profileResponse.data; const user = profileResponse.data;
const { const { data: insertedStatuses, error: insertedStatusesError } =
data: insertedStatuses, await supabase
error: insertedStatusesError
} = await supabase
.from('statuses') .from('statuses')
.insert(usersWithStatuses.map((userWithStatus) => ({ .insert(
usersWithStatuses.map((userWithStatus) => ({
user_id: userWithStatus.user.id, user_id: userWithStatus.user.id,
status, status,
updated_by_id: user.id, updated_by_id: user.id,
}))) })),
)
.select(); .select();
if (insertedStatusesError) throw new Error('Couldn\'t insert statuses!'); if (insertedStatusesError) throw new Error("Error inserting statuses!");
else if (insertedStatuses) { else if (insertedStatuses) {
const createdAtFallback = new Date(Date.now()).toISOString(); const createdAtFallback = new Date(Date.now()).toISOString();
await broadcastStatusUpdates(usersWithStatuses.map((s, i) => { return { const statusUpdates = usersWithStatuses.map((s, i) => {
return {
user: s.user, user: s.user,
status: status, status: status,
created_at: insertedStatuses[i]?.created_at ?? createdAtFallback, created_at: insertedStatuses[i]?.created_at ?? createdAtFallback,
updated_by: user, updated_by: user,
}}));
} }
return { success: true, data: undefined }; });
await broadcastStatusUpdates(statusUpdates);
return { success: true, data: statusUpdates };
} else {
return { success: false, error: 'No inserted statuses returned!' };
}
} catch (error) { } catch (error) {
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Unknown error', error: `Error updating statuses: ${error as Error}`,
}; };
} }
}; };
export const updateUserStatus = async ( export const updateUserStatus = async (
status: string, status: string,
): Promise<Result<void>> => { ): Promise<Result<UserWithStatus[]>> => {
try { try {
const supabase = createClient(); const supabase = createClient();
const profileResponse = await getProfileWithAvatar(); const profileResponse = await getProfileWithAvatar();
@ -171,14 +180,15 @@ export const updateUserStatus = async (
.single(); .single();
if (insertedStatusError) throw insertedStatusError as Error; if (insertedStatusError) throw insertedStatusError as Error;
await broadcastStatusUpdates([{ const statusUpdate = {
user: userProfile, user: userProfile,
status: insertedStatus.status, status: insertedStatus.status,
created_at: insertedStatus.created_at, created_at: insertedStatus.created_at,
updated_by: userProfile, updated_by: userProfile,
}]); };
await broadcastStatusUpdates([statusUpdate]);
return { success: true, data: undefined }; return { success: true, data: [statusUpdate] };
} catch (error) { } catch (error) {
return { return {
success: false, success: false,

View File

@ -4,6 +4,8 @@ import { updateSession } from '@/utils/supabase/middleware';
// In-memory store for tracking IPs (use Redis in production) // In-memory store for tracking IPs (use Redis in production)
const ipAttempts = new Map<string, { count: number; lastAttempt: number }>(); const ipAttempts = new Map<string, { count: number; lastAttempt: number }>();
const bannedIPs = new Set<string>(); const bannedIPs = new Set<string>();
// Ban Arctic Wolf Explicitly
bannedIPs.add('::ffff:10.0.1.49');
// Suspicious patterns that indicate malicious activity // Suspicious patterns that indicate malicious activity
const MALICIOUS_PATTERNS = [ const MALICIOUS_PATTERNS = [
@ -93,7 +95,7 @@ export const middleware = async (request: NextRequest) => {
// Check if IP is already banned // Check if IP is already banned
if (bannedIPs.has(ip)) { if (bannedIPs.has(ip)) {
console.log(`🚫 Blocked banned IP: ${ip} trying to access ${pathname}`); //console.log(`🚫 Blocked banned IP: ${ip} trying to access ${pathname}`);
return new NextResponse('Access denied.', { status: 403 }); return new NextResponse('Access denied.', { status: 403 });
} }
@ -102,13 +104,13 @@ export const middleware = async (request: NextRequest) => {
const isSuspiciousMethod = isMethodSuspicious(method); const isSuspiciousMethod = isMethodSuspicious(method);
if (isSuspiciousPath || isSuspiciousMethod) { if (isSuspiciousPath || isSuspiciousMethod) {
console.log(`⚠️ Suspicious activity from ${ip}: ${method} ${pathname}`); //console.log(`⚠️ Suspicious activity from ${ip}: ${method} ${pathname}`);
const shouldBan = updateIPAttempts(ip); const shouldBan = updateIPAttempts(ip);
if (shouldBan) { if (shouldBan) {
console.log(`🔨 IP ${ip} has been banned for suspicious activity`); console.log(`🔨 IP ${ip} has been banned for suspicious activity`);
return new NextResponse('Access denied - IP banned', { status: 403 }); return new NextResponse('Access denied - IP banned. Please fuck off.', { status: 403 });
} }
// Return 404 to not reveal the blocking mechanism // Return 404 to not reveal the blocking mechanism

View File

@ -4,200 +4,200 @@ export type Json =
| boolean | boolean
| null | null
| { [key: string]: Json | undefined } | { [key: string]: Json | undefined }
| Json[] | Json[];
export type Database = { export type Database = {
public: { public: {
Tables: { Tables: {
profiles: { profiles: {
Row: { Row: {
avatar_url: string | null avatar_url: string | null;
email: string | null email: string | null;
full_name: string | null full_name: string | null;
id: string id: string;
provider: string | null provider: string | null;
updated_at: string | null updated_at: string | null;
} };
Insert: { Insert: {
avatar_url?: string | null avatar_url?: string | null;
email?: string | null email?: string | null;
full_name?: string | null full_name?: string | null;
id: string id: string;
provider?: string | null provider?: string | null;
updated_at?: string | null updated_at?: string | null;
} };
Update: { Update: {
avatar_url?: string | null avatar_url?: string | null;
email?: string | null email?: string | null;
full_name?: string | null full_name?: string | null;
id?: string id?: string;
provider?: string | null provider?: string | null;
updated_at?: string | null updated_at?: string | null;
} };
Relationships: [] Relationships: [];
} };
statuses: { statuses: {
Row: { Row: {
created_at: string created_at: string;
id: string id: string;
status: string status: string;
updated_by_id: string | null updated_by_id: string | null;
user_id: string user_id: string;
} };
Insert: { Insert: {
created_at?: string created_at?: string;
id?: string id?: string;
status: string status: string;
updated_by_id?: string | null updated_by_id?: string | null;
user_id: string user_id: string;
} };
Update: { Update: {
created_at?: string created_at?: string;
id?: string id?: string;
status?: string status?: string;
updated_by_id?: string | null updated_by_id?: string | null;
user_id?: string user_id?: string;
} };
Relationships: [ Relationships: [
{ {
foreignKeyName: "statuses_updated_by_id_fkey" foreignKeyName: 'statuses_updated_by_id_fkey';
columns: ["updated_by_id"] columns: ['updated_by_id'];
isOneToOne: false isOneToOne: false;
referencedRelation: "profiles" referencedRelation: 'profiles';
referencedColumns: ["id"] referencedColumns: ['id'];
}, },
{ {
foreignKeyName: "statuses_user_id_fkey" foreignKeyName: 'statuses_user_id_fkey';
columns: ["user_id"] columns: ['user_id'];
isOneToOne: false isOneToOne: false;
referencedRelation: "profiles" referencedRelation: 'profiles';
referencedColumns: ["id"] referencedColumns: ['id'];
}, },
] ];
} };
} };
Views: { Views: {
[_ in never]: never [_ in never]: never;
} };
Functions: { Functions: {
[_ in never]: never [_ in never]: never;
} };
Enums: { Enums: {
[_ in never]: never [_ in never]: never;
} };
CompositeTypes: { CompositeTypes: {
[_ in never]: never [_ in never]: never;
} };
} };
} };
type DefaultSchema = Database[Extract<keyof Database, "public">] type DefaultSchema = Database[Extract<keyof Database, 'public'>];
export type Tables< export type Tables<
DefaultSchemaTableNameOrOptions extends DefaultSchemaTableNameOrOptions extends
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"]) | keyof (DefaultSchema['Tables'] & DefaultSchema['Views'])
| { schema: keyof Database }, | { schema: keyof Database },
TableName extends DefaultSchemaTableNameOrOptions extends { TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof Database schema: keyof Database;
} }
? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & ? keyof (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"]) Database[DefaultSchemaTableNameOrOptions['schema']]['Views'])
: never = never, : never = never,
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } > = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
? (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & ? (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends { Database[DefaultSchemaTableNameOrOptions['schema']]['Views'])[TableName] extends {
Row: infer R Row: infer R;
} }
? R ? R
: never : never
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] & : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema['Tables'] &
DefaultSchema["Views"]) DefaultSchema['Views'])
? (DefaultSchema["Tables"] & ? (DefaultSchema['Tables'] &
DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends { DefaultSchema['Views'])[DefaultSchemaTableNameOrOptions] extends {
Row: infer R Row: infer R;
} }
? R ? R
: never : never
: never : never;
export type TablesInsert< export type TablesInsert<
DefaultSchemaTableNameOrOptions extends DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema["Tables"] | keyof DefaultSchema['Tables']
| { schema: keyof Database }, | { schema: keyof Database },
TableName extends DefaultSchemaTableNameOrOptions extends { TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof Database schema: keyof Database;
} }
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] ? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables']
: never = never, : never = never,
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } > = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { ? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
Insert: infer I Insert: infer I;
} }
? I ? I
: never : never
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables']
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { ? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends {
Insert: infer I Insert: infer I;
} }
? I ? I
: never : never
: never : never;
export type TablesUpdate< export type TablesUpdate<
DefaultSchemaTableNameOrOptions extends DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema["Tables"] | keyof DefaultSchema['Tables']
| { schema: keyof Database }, | { schema: keyof Database },
TableName extends DefaultSchemaTableNameOrOptions extends { TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof Database schema: keyof Database;
} }
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] ? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables']
: never = never, : never = never,
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } > = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { ? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
Update: infer U Update: infer U;
} }
? U ? U
: never : never
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables']
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { ? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends {
Update: infer U Update: infer U;
} }
? U ? U
: never : never
: never : never;
export type Enums< export type Enums<
DefaultSchemaEnumNameOrOptions extends DefaultSchemaEnumNameOrOptions extends
| keyof DefaultSchema["Enums"] | keyof DefaultSchema['Enums']
| { schema: keyof Database }, | { schema: keyof Database },
EnumName extends DefaultSchemaEnumNameOrOptions extends { EnumName extends DefaultSchemaEnumNameOrOptions extends {
schema: keyof Database schema: keyof Database;
} }
? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"] ? keyof Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums']
: never = never, : never = never,
> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database } > = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
? Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName] ? Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums'][EnumName]
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"] : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums']
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions] ? DefaultSchema['Enums'][DefaultSchemaEnumNameOrOptions]
: never : never;
export type CompositeTypes< export type CompositeTypes<
PublicCompositeTypeNameOrOptions extends PublicCompositeTypeNameOrOptions extends
| keyof DefaultSchema["CompositeTypes"] | keyof DefaultSchema['CompositeTypes']
| { schema: keyof Database }, | { schema: keyof Database },
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
schema: keyof Database schema: keyof Database;
} }
? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"] ? keyof Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes']
: never = never, : never = never,
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database } > = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName] ? Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName]
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"] : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema['CompositeTypes']
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions] ? DefaultSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions]
: never : never;
export const Constants = { export const Constants = {
public: { public: {
Enums: {}, Enums: {},
}, },
} as const } as const;