Rewriting status card to make it look good and to go over the code
This commit is contained in:
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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,34 +56,35 @@ type QueryProviderProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const QueryProvider = ({ children }: QueryProviderProps) => {
|
export const QueryProvider = ({ children }: QueryProviderProps) => {
|
||||||
const [queryClient] = useState(() => new QueryClient({
|
const [queryClient] = useState(
|
||||||
queryCache: new QueryCache({
|
() =>
|
||||||
onError: queryCacheOnError,
|
new QueryClient({
|
||||||
}),
|
queryCache: new QueryCache({
|
||||||
mutationCache: new MutationCache({
|
onError: queryCacheOnError,
|
||||||
onError: mutationCacheOnError,
|
}),
|
||||||
}),
|
mutationCache: new MutationCache({
|
||||||
defaultOptions: {
|
onError: mutationCacheOnError,
|
||||||
queries: {
|
}),
|
||||||
staleTime: 30 * 1000, // 30 seconds
|
defaultOptions: {
|
||||||
refetchOnWindowFocus: true,
|
queries: {
|
||||||
retry: (failureCount, error) => {
|
staleTime: 30 * 1000, // 30 seconds
|
||||||
// Don't retry on 4xx errors
|
refetchOnWindowFocus: true,
|
||||||
if (error instanceof Error && error.message.includes('4')) {
|
retry: (failureCount, error) => {
|
||||||
return false;
|
// Don't retry on 4xx errors
|
||||||
}
|
if (error instanceof Error && error.message.includes('4')) {
|
||||||
return failureCount < 3;
|
return false;
|
||||||
|
}
|
||||||
|
return failureCount < 3;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
retry: 1,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
mutations: {
|
);
|
||||||
retry: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
{children}
|
|
||||||
</QueryClientProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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
369
src/components/status/List.tsx
Executable 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>
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
@ -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}`);
|
||||||
@ -87,63 +97,68 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
|
|||||||
}
|
}
|
||||||
setConnectionStatus('connecting');
|
setConnectionStatus('connecting');
|
||||||
const channel = supabaseRef.current
|
const channel = supabaseRef.current
|
||||||
.channel('status_updates')
|
.channel('status_updates')
|
||||||
.on('broadcast', { event: 'status_updated' }, (payload) => {
|
.on('broadcast', { event: 'status_updated' }, (payload) => {
|
||||||
console.log('Realtime update received, triggering refetch...');
|
console.log('Realtime update received, triggering refetch...');
|
||||||
refetch().catch((error) => {
|
refetch().catch((error) => {
|
||||||
console.error(`Error refetching: ${error}`);
|
console.error(`Error refetching: ${error}`);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.subscribe((status) => {
|
.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
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
||||||
} else if (status === 'CHANNEL_ERROR') {
|
if (status === 'SUBSCRIBED') {
|
||||||
console.log('Realtime connection failed, relying on polling');
|
console.log('Realtime connection established');
|
||||||
setConnectionStatus('disconnected');
|
setConnectionStatus('connected');
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
reconnectAttempts = 0;
|
||||||
} else if (status === 'CLOSED') {
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
||||||
console.log('Realtime connection closed');
|
} else if (status === 'CHANNEL_ERROR') {
|
||||||
setConnectionStatus('disconnected');
|
console.log('Realtime connection failed, relying on polling');
|
||||||
if (isComponentMounted && reconnectAttempts < maxReconnectAttempts) {
|
setConnectionStatus('disconnected');
|
||||||
reconnectAttempts++
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
||||||
const delay = 2000 * reconnectAttempts
|
} 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`)
|
console.log(
|
||||||
|
`Reconnecting after close ${reconnectAttempts}/${maxReconnectAttempts} in ${delay}ms`,
|
||||||
|
);
|
||||||
|
|
||||||
if (reconnectTimeout) {
|
if (reconnectTimeout) {
|
||||||
clearTimeout(reconnectTimeout)
|
clearTimeout(reconnectTimeout);
|
||||||
}
|
|
||||||
|
|
||||||
reconnectTimeout = setTimeout(() => {
|
|
||||||
if (isComponentMounted) {
|
|
||||||
setUpRealtimeConnection()
|
|
||||||
}
|
}
|
||||||
}, delay)
|
|
||||||
} else {
|
reconnectTimeout = setTimeout(() => {
|
||||||
console.log("Max reconnection attempts reached or component unmounted")
|
if (isComponentMounted) {
|
||||||
|
setUpRealtimeConnection();
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
} else {
|
||||||
|
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 && (
|
||||||
|
@ -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}`);
|
||||||
@ -92,60 +90,65 @@ export const TechTable = ({
|
|||||||
}
|
}
|
||||||
setConnectionStatus('connecting');
|
setConnectionStatus('connecting');
|
||||||
const channel = supabaseRef.current
|
const channel = supabaseRef.current
|
||||||
.channel('status_updates')
|
.channel('status_updates')
|
||||||
.on('broadcast', { event: 'status_updated' }, (payload) => {
|
.on('broadcast', { event: 'status_updated' }, (payload) => {
|
||||||
console.log('Realtime update received, triggering refetch...');
|
console.log('Realtime update received, triggering refetch...');
|
||||||
refetch().catch((error) => {
|
refetch().catch((error) => {
|
||||||
console.error(`Error refetching: ${error}`);
|
console.error(`Error refetching: ${error}`);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.subscribe((status) => {
|
.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
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
||||||
} else if (status === 'CHANNEL_ERROR') {
|
if (status === 'SUBSCRIBED') {
|
||||||
console.log('Realtime connection failed, relying on polling');
|
console.log('Realtime connection established');
|
||||||
setConnectionStatus('disconnected');
|
setConnectionStatus('connected');
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
reconnectAttempts = 0;
|
||||||
} else if (status === 'CLOSED') {
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
||||||
console.log('Realtime connection closed');
|
} else if (status === 'CHANNEL_ERROR') {
|
||||||
setConnectionStatus('disconnected');
|
console.log('Realtime connection failed, relying on polling');
|
||||||
if (isComponentMounted && reconnectAttempts < maxReconnectAttempts) {
|
setConnectionStatus('disconnected');
|
||||||
reconnectAttempts++
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
||||||
const delay = 2000 * reconnectAttempts
|
} else if (status === 'CLOSED') {
|
||||||
console.log(`Reconnecting after close ${reconnectAttempts}/${maxReconnectAttempts} in ${delay}ms`)
|
console.log('Realtime connection closed');
|
||||||
if (reconnectTimeout) {
|
setConnectionStatus('disconnected');
|
||||||
clearTimeout(reconnectTimeout)
|
if (
|
||||||
}
|
isComponentMounted &&
|
||||||
reconnectTimeout = setTimeout(() => {
|
reconnectAttempts < maxReconnectAttempts
|
||||||
if (isComponentMounted) {
|
) {
|
||||||
setUpRealtimeConnection()
|
reconnectAttempts++;
|
||||||
|
const delay = 2000 * reconnectAttempts;
|
||||||
|
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',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
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);
|
||||||
@ -339,17 +355,17 @@ export const TechTable = ({
|
|||||||
<div className={containerClassName}>
|
<div className={containerClassName}>
|
||||||
{/* 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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table className='w-full text-center rounded-md'>
|
<table className='w-full text-center rounded-md'>
|
||||||
@ -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,22 +428,27 @@ 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 !==
|
||||||
<div className='flex items-center gap-1 text-muted-foreground'>
|
userWithStatus.user.id && (
|
||||||
<BasedAvatar
|
<div className='flex items-center gap-1 text-muted-foreground'>
|
||||||
src={userWithStatus.updated_by?.avatar_url}
|
<BasedAvatar
|
||||||
fullName={userWithStatus.updated_by?.full_name}
|
src={userWithStatus.updated_by?.avatar_url}
|
||||||
className='w-3 h-3'
|
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
|
||||||
</span>
|
className={`text-xs ${tvMode ? 'text-3xl' : ''}`}
|
||||||
</div>
|
>
|
||||||
)}
|
Updated by {userWithStatus.updated_by.full_name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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;
|
||||||
@ -17,7 +16,7 @@ type BasedAvatarProps = React.ComponentProps<typeof AvatarPrimitive.Root> & {
|
|||||||
function BasedAvatar({
|
function BasedAvatar({
|
||||||
src = null,
|
src = null,
|
||||||
fullName = null,
|
fullName = null,
|
||||||
imageClassName ='',
|
imageClassName = '',
|
||||||
fallbackClassName = '',
|
fallbackClassName = '',
|
||||||
userIconSize = 32,
|
userIconSize = 32,
|
||||||
className,
|
className,
|
||||||
@ -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'
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
.from('statuses')
|
||||||
} = await supabase
|
.insert(
|
||||||
.from('statuses')
|
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(
|
||||||
user: s.user,
|
usersWithStatuses.map((s, i) => {
|
||||||
status: status,
|
return {
|
||||||
created_at: insertedStatuses[i]?.created_at ?? createdAtFallback,
|
user: s.user,
|
||||||
updated_by: user,
|
status: status,
|
||||||
}}));
|
created_at: insertedStatuses[i]?.created_at ?? createdAtFallback,
|
||||||
|
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,
|
{
|
||||||
status: insertedStatus.status,
|
user: userProfile,
|
||||||
created_at: insertedStatus.created_at,
|
status: insertedStatus.status,
|
||||||
updated_by: userProfile,
|
created_at: insertedStatus.created_at,
|
||||||
}]);
|
updated_by: userProfile,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
return { success: true, data: undefined };
|
return { success: true, data: undefined };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
.from('statuses')
|
||||||
} = await supabase
|
.insert(
|
||||||
.from('statuses')
|
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) => {
|
||||||
user: s.user,
|
return {
|
||||||
status: status,
|
user: s.user,
|
||||||
created_at: insertedStatuses[i]?.created_at ?? createdAtFallback,
|
status: status,
|
||||||
updated_by: user,
|
created_at: insertedStatuses[i]?.created_at ?? createdAtFallback,
|
||||||
}}));
|
updated_by: user,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await broadcastStatusUpdates(statusUpdates);
|
||||||
|
return { success: true, data: statusUpdates };
|
||||||
|
} else {
|
||||||
|
return { success: false, error: 'No inserted statuses returned!' };
|
||||||
}
|
}
|
||||||
return { success: true, data: undefined };
|
|
||||||
} 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,
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
Reference in New Issue
Block a user