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

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

View File

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

View File

@ -1,7 +1,12 @@
// src/components/providers/query-provider.tsx
'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 { toast } from 'sonner';
@ -13,7 +18,8 @@ export const enum QueryErrorCodes {
}
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) {
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 errorMessage = error instanceof Error ? error.message : 'Something went wrong';
const mutationCacheOnError = (
error: unknown,
variables: unknown,
context: unknown,
mutation: any,
) => {
const errorMessage =
error instanceof Error ? error.message : 'Something went wrong';
switch (mutation.meta?.errCode) {
case QueryErrorCodes.STATUS_UPDATE_FAILED:
@ -44,34 +56,35 @@ type QueryProviderProps = {
};
export const QueryProvider = ({ children }: QueryProviderProps) => {
const [queryClient] = useState(() => new QueryClient({
queryCache: new QueryCache({
onError: queryCacheOnError,
}),
mutationCache: new MutationCache({
onError: mutationCacheOnError,
}),
defaultOptions: {
queries: {
staleTime: 30 * 1000, // 30 seconds
refetchOnWindowFocus: true,
retry: (failureCount, error) => {
// Don't retry on 4xx errors
if (error instanceof Error && error.message.includes('4')) {
return false;
}
return failureCount < 3;
const [queryClient] = useState(
() =>
new QueryClient({
queryCache: new QueryCache({
onError: queryCacheOnError,
}),
mutationCache: new MutationCache({
onError: mutationCacheOnError,
}),
defaultOptions: {
queries: {
staleTime: 30 * 1000, // 30 seconds
refetchOnWindowFocus: true,
retry: (failureCount, error) => {
// Don't retry on 4xx errors
if (error instanceof Error && error.message.includes('4')) {
return false;
}
return failureCount < 3;
},
},
mutations: {
retry: 1,
},
},
},
mutations: {
retry: 1,
},
},
}));
}),
);
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};

View File

@ -40,13 +40,15 @@ export const SignInWithApple = ({
if (!profile.provider) {
const updateResponse = await updateProfile({
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 {
const updateResponse = await updateProfile({
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

View File

@ -39,13 +39,15 @@ export const SignInWithMicrosoft = ({
if (!profile.provider) {
const updateResponse = await updateProfile({
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 {
const updateResponse = await updateProfile({
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;

View File

@ -14,7 +14,12 @@ const Footer = () => {
hover:bg-gradient-to-tr hover:from-[#35363F] hover:to-[#23242F]
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>
</Link>
</div>

View File

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

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

@ -0,0 +1,369 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { useAuth, useTVMode } from '@/components/context';
import {
getRecentUsersWithStatuses,
updateStatuses,
updateUserStatus,
type UserWithStatus,
} from '@/lib/hooks';
import { BasedAvatar, Drawer, DrawerTrigger, Loading } from '@/components/ui';
import { StatusMessage, SubmitButton } from '@/components/default';
import { toast } from 'sonner';
import { HistoryDrawer } from '@/components/status';
import type { Profile } from '@/utils/supabase';
import { makeConditionalClassName } from '@/lib/utils';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { RefreshCw, Clock, Wifi, WifiOff } from 'lucide-react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { QueryErrorCodes } from '@/components/context';
import { createClient } from '@/utils/supabase';
import type { RealtimeChannel } from '@supabase/supabase-js';
type ListProps = { initialStatuses: UserWithStatus[] };
export const StatusList = ({ initialStatuses = [] }: ListProps) => {
const { isAuthenticated } = useAuth();
const { tvMode } = useTVMode();
const queryClient = useQueryClient();
const [selectedUsers, setSelectedUsers] = useState<UserWithStatus[]>([]);
const [selectAll, setSelectAll] = useState(false);
const [statusInput, setStatusInput] = useState('');
const [selectedHistoryUser, setSelectedHistoryUser] =
useState<Profile | null>(null);
const [updateStatusMessage, setUpdateStatusMessage] = useState('');
const [connectionStatus, setConnectionStatus] = useState<
'connecting' | 'connected' | 'disconnected' | 'updating'
>('connecting');
const [newStatuses, setNewStatuses] = useState<Set<UserWithStatus>>(new Set());
const channelRef = useRef<RealtimeChannel | null>(null);
const supabaseRef = useRef(createClient());
const {
data: usersWithStatuses = initialStatuses,
isLoading: loading,
error,
refetch,
isFetching,
dataUpdatedAt,
} = useQuery({
queryKey: ['users-with-statuses'],
queryFn: async () => {
try {
const response = await getRecentUsersWithStatuses();
if (!response.success) throw new Error(response.error);
return response.data;
} catch (error) {
toast.error(`Error fetching technicians: ${error as Error}`)
throw error;
}
},
enabled: isAuthenticated,
refetchInterval: 30000, // 30 Second interval as we should rely on subscription.
refetchOnWindowFocus: true,
refetchOnMount: true,
initialData: initialStatuses,
meta: { errCode: QueryErrorCodes.USERS_FETCH_FAILED },
});
useEffect(() => {
if (!isAuthenticated) return;
const maxReconnectAttempts = 3;
let reconnectAttempts = 0;
let reconnectTimeout: NodeJS.Timeout;
let isComponentMounted = true;
let currentChannel: RealtimeChannel | null = null;
const setUpRealtimeConnection = () => {
if (!isComponentMounted) return;
if (currentChannel) {
supabaseRef.current.removeChannel(currentChannel)
.catch((error) => {
setConnectionStatus('disconnected');
console.error(`Error unsubscribing: ${error}`);
})
currentChannel = null;
}
setConnectionStatus('connecting');
const channel = supabaseRef.current
.channel('status_updates')
.on('broadcast', { event: 'status_updated' }, () => {
refetch().catch((error) => {
console.error(`Error refetching statuses: ${error as Error}`)
});
})
.subscribe((status) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
if (status === 'SUBSCRIBED') {
setConnectionStatus('connected');
reconnectAttempts = 0;
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
} else if (status === 'CHANNEL_ERROR') {
setConnectionStatus('disconnected');
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
} else if (status === 'CLOSED') {
setConnectionStatus('disconnected')
if (isComponentMounted && reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
const delay = 2000 * reconnectAttempts;
console.log(
`Reconnecting after close.
${reconnectAttempts} attempts of
${maxReconnectAttempts} in ${delay}ms
`);
if (reconnectTimeout) clearTimeout(reconnectTimeout);
reconnectTimeout = setTimeout(() => {
if (isComponentMounted) setUpRealtimeConnection();
}, delay);
} else {
console.warn('Max reconnection attempts reached or component is not mounted.');
setConnectionStatus('disconnected');
}
}
});
currentChannel = channel;
channelRef.current = channel;
};
const initialTimeout = setTimeout(() => {
if (isComponentMounted) setUpRealtimeConnection();
}, 1000);
return () => {
isComponentMounted = false;
if (initialTimeout) clearTimeout(initialTimeout);
if (reconnectTimeout) clearTimeout(reconnectTimeout);
if (currentChannel) {
supabaseRef.current.removeChannel(currentChannel)
.catch((error) => {
console.error(`Error unsubscribing: ${error as Error}`)
});
channelRef.current = null;
}
setConnectionStatus('disconnected');
}
}, [isAuthenticated, refetch]);
const updateStatusMutation = useMutation({
mutationFn: async ({
usersWithStatuses,
status,
}: {
usersWithStatuses: UserWithStatus[];
status: string;
}) => {
const previousConnectionStatus = connectionStatus;
try {
if (usersWithStatuses.length <= 0) {
const result = await updateUserStatus(status);
if (!result.success) throw new Error(result.error);
return result.data;
} else {
const result = await updateStatuses(usersWithStatuses, status);
if (!result.success) throw new Error(result.error);
return result.data;
}
} catch (error) {
console.error(`Error updating statuses: ${error as Error}`);
}
},
meta: { errCode: QueryErrorCodes.STATUS_UPDATE_FAILED },
onMutate: async ({ usersWithStatuses, status }) => {
// Optimistic update logic
await queryClient.cancelQueries({ queryKey: ['users-with-statuses']} );
const previousData = queryClient.getQueryData<UserWithStatus[]>([
'users-with-statuses',
]);
if (previousData && usersWithStatuses.length > 0) {
const now = new Date().toISOString();
const optimisticData = previousData.map((userStatus) => {
if (usersWithStatuses.some((selected) => selected.user.id === userStatus.user.id))
return { ...userStatus, status, created_at: now};
return userStatus;
});
queryClient.setQueryData(['users-with-statuses'], optimisticData);
// Add animation to optimisticly updated statuses
const updatedStatuses = usersWithStatuses;
setNewStatuses((prev) => new Set([...prev, ...updatedStatuses]))
setTimeout(() => {
setNewStatuses((prev) => {
const updated = new Set(prev);
updatedStatuses.forEach((updatedStatus) => updated.delete(updatedStatus));
return updated;
});
}, 1000)
}
return { previousData };
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['users-with-statuses']} )
.catch((error) => console.error(`Error invalidating query: ${error}`));
if (!data) return;
data.forEach((statusUpdate) => {
toast.success(`
${statusUpdate.user.full_name}'s status updated to '${statusUpdate.status}'.
`);
});
setSelectedUsers([]);
setStatusInput('');
},
onError: (error, _variables, context) => {
if (context?.previousData)
queryClient.setQueryData(['users-with-statuses'], context.previousData);
toast.error(`Error updating statuses: ${error}`);
},
});
const handleUpdateStatus = () => {
if (!isAuthenticated) {
setUpdateStatusMessage('You must be signed in to update technican statuses!')
return;
} else if (!statusInput.trim()) {
setUpdateStatusMessage('Your status must be in between 3 & 80 characters long!');
return;
}
updateStatusMutation.mutate({
usersWithStatuses: selectedUsers,
status: statusInput.trim(),
});
};
const handleCheckboxChange = (user: UserWithStatus) => {
setSelectedUsers((prev) =>
prev.some((u) => u.user.id === user.user.id)
? prev.filter((prevUser) => prevUser.user.id !== user.user.id)
: [...prev, user],
);
};
const handleSelectAllChange = () => {
if (selectAll) {
setSelectedUsers([]);
} else {
setSelectedUsers(usersWithStatuses);
}
setSelectAll(!selectAll);
};
useEffect(() => {
setSelectAll(
selectedUsers.length === usersWithStatuses.length &&
usersWithStatuses.length > 0,
);
}, [selectedUsers.length, usersWithStatuses.length]);
const getConnectionIcon = () => {
switch (connectionStatus) {
case 'connected':
return <Wifi className='w-4 h-4 text-green-500' />;
case 'connecting':
return <Wifi className='w-4 h-4 text-yellow-500 animate-pulse' />;
case 'disconnected':
return <WifiOff className='w-4 h-4 text-red-500' />;
case 'updating':
return <RefreshCw className='w-4 h-4 text-blue-500 animate-spin' />;
}
};
const getConnectionText = () => {
switch (connectionStatus) {
case 'connected':
return 'Connected';
case 'connecting':
return 'Connecting...';
case 'disconnected':
return 'Disconnected';
case 'updating':
return 'Updating...';
}
};
const formatTime = (timestamp: string) => {
const date = new Date(timestamp);
const time = date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: 'numeric',
});
const day = date.getDate();
const month = date.toLocaleString('default', { month: 'long' });
return `${time} - ${month} ${day}`;
};
if (loading) {
return (
<div className='flex justify-center items-center min-h-[400px]'>
<Loading className='w-full' alpha={0.5} />
</div>
);
}
if (error) {
return (
<div className='flex flex-col justify-center items-center min-h-[400px] gap-4'>
<p className='text-red-500'>Error loading status updates</p>
<Button onClick={() => refetch()} variant='outline'>
<RefreshCw className='w-4 h-4 mr-2' />
Retry
</Button>
</div>
);
}
const containerClassName = makeConditionalClassName({
context: tvMode,
defaultClassName: 'mx-auto space-y-4',
on: 'lg:w-11/12 w-full mt-15',
off: 'w-5/6',
});
const headerClassName = makeConditionalClassName({
context: tvMode,
defaultClassName: '',
on: 'none',
off: 'flex items-center justify-between mb-4',
})
const cardClassName = makeConditionalClassName({
context: tvMode,
defaultClassName: 'transition-all duration-300 hover:shadow-md',
on: 'lg:text-4xl',
off: 'lg:text-base',
});
return (
<div className={containerClassName}>
<div className={headerClassName}>
<div className='flex items-center gap-2'>
<Checkbox
id='select-all'
checked={selectAll}
onCheckedChange={handleSelectAllChange}
/>
<label htmlFor='select-all' className='text-sm font-medium'>
Select All
</label>
<Badge variant='outline' className='flex items-center gap-2'>
{getConnectionIcon()}
<span className='text-sm'>{getConnectionText()}</span>
</Badge>
</div>
</div>
</div>
);
};

View File

@ -1,29 +1,35 @@
"use client"
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 { 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";
'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 { 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 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 { tvMode } = useTVMode();
const queryClient = useQueryClient();
@ -31,14 +37,16 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
const [selectedUsers, setSelectedUsers] = useState<UserWithStatus[]>([]);
const [selectAll, setSelectAll] = useState(false);
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 channelRef = useRef<RealtimeChannel | null>(null);
const supabaseRef = useRef(createClient());
// Keep all your existing React Query code exactly as is
const {
data: usersWithStatuses = initialStatuses,
@ -55,7 +63,9 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
if (!response.success) throw new Error(response.error);
return response.data;
} catch (error) {
toast.error(`Error fetching technicians: ${error instanceof Error ? error.message : 'Unknown error'}`);
toast.error(
`Error fetching technicians: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
throw error;
}
},
@ -71,14 +81,14 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
useEffect(() => {
if (!isAuthenticated) return;
let reconnectAttempts = 0
const maxReconnectAttempts = 3
let reconnectTimeout: NodeJS.Timeout
let isComponentMounted = true
let reconnectAttempts = 0;
const maxReconnectAttempts = 3;
let reconnectTimeout: NodeJS.Timeout;
let isComponentMounted = true;
let currentChannel: RealtimeChannel | null = null;
const setUpRealtimeConnection = () => {
if (!isComponentMounted) return
if (!isComponentMounted) return;
if (currentChannel) {
supabaseRef.current.removeChannel(currentChannel).catch((error) => {
console.error(`Error unsubscribing: ${error}`);
@ -87,63 +97,68 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
}
setConnectionStatus('connecting');
const channel = supabaseRef.current
.channel('status_updates')
.on('broadcast', { event: 'status_updated' }, (payload) => {
console.log('Realtime update received, triggering refetch...');
refetch().catch((error) => {
console.error(`Error refetching: ${error}`);
});
})
.subscribe((status) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
if (status === 'SUBSCRIBED') {
console.log('Realtime connection established');
setConnectionStatus('connected');
reconnectAttempts = 0
.channel('status_updates')
.on('broadcast', { event: 'status_updated' }, (payload) => {
console.log('Realtime update received, triggering refetch...');
refetch().catch((error) => {
console.error(`Error refetching: ${error}`);
});
})
.subscribe((status) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
} else if (status === 'CHANNEL_ERROR') {
console.log('Realtime connection failed, relying on polling');
setConnectionStatus('disconnected');
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
} else if (status === 'CLOSED') {
console.log('Realtime connection closed');
setConnectionStatus('disconnected');
if (isComponentMounted && reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++
const delay = 2000 * reconnectAttempts
if (status === 'SUBSCRIBED') {
console.log('Realtime connection established');
setConnectionStatus('connected');
reconnectAttempts = 0;
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
} else if (status === 'CHANNEL_ERROR') {
console.log('Realtime connection failed, relying on polling');
setConnectionStatus('disconnected');
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
} else if (status === 'CLOSED') {
console.log('Realtime connection closed');
setConnectionStatus('disconnected');
if (
isComponentMounted &&
reconnectAttempts < maxReconnectAttempts
) {
reconnectAttempts++;
const delay = 2000 * reconnectAttempts;
console.log(`Reconnecting after close ${reconnectAttempts}/${maxReconnectAttempts} in ${delay}ms`)
console.log(
`Reconnecting after close ${reconnectAttempts}/${maxReconnectAttempts} in ${delay}ms`,
);
if (reconnectTimeout) {
clearTimeout(reconnectTimeout)
}
reconnectTimeout = setTimeout(() => {
if (isComponentMounted) {
setUpRealtimeConnection()
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
}
}, delay)
} else {
console.log("Max reconnection attempts reached or component unmounted")
reconnectTimeout = setTimeout(() => {
if (isComponentMounted) {
setUpRealtimeConnection();
}
}, delay);
} else {
console.log(
'Max reconnection attempts reached or component unmounted',
);
}
}
}
});
});
currentChannel = channel;
channelRef.current = channel;
}
};
const initialTimeout = setTimeout(() => {
if (isComponentMounted) {
setUpRealtimeConnection()
setUpRealtimeConnection();
}
}, 1000)
}, 1000);
return () => {
isComponentMounted = false
if (initialTimeout)
clearTimeout(initialTimeout)
if (reconnectTimeout)
clearTimeout(reconnectTimeout)
isComponentMounted = false;
if (initialTimeout) clearTimeout(initialTimeout);
if (reconnectTimeout) clearTimeout(reconnectTimeout);
if (currentChannel) {
console.log('Cleaning up realtime connection...');
@ -157,7 +172,13 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
// Updated mutation
const updateStatusMutation = useMutation({
mutationFn: async ({ usersWithStatuses, status }: { usersWithStatuses: UserWithStatus[], status: string }) => {
mutationFn: async ({
usersWithStatuses,
status,
}: {
usersWithStatuses: UserWithStatus[];
status: string;
}) => {
if (usersWithStatuses.length === 0) {
const result = await updateUserStatus(status);
if (!result.success) throw new Error(result.error);
@ -172,12 +193,18 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
onMutate: async ({ usersWithStatuses, status }) => {
// Optimistic update logic
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) {
const now = new Date().toISOString();
const optimisticData = previousData.map(userStatus => {
if (usersWithStatuses.some(selected => selected.user.id === userStatus.user.id)) {
const optimisticData = previousData.map((userStatus) => {
if (
usersWithStatuses.some(
(selected) => selected.user.id === userStatus.user.id,
)
) {
return {
...userStatus,
status,
@ -189,14 +216,14 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
queryClient.setQueryData(['users-with-statuses'], optimisticData);
// Add animation for optimistic updates
const updatedIds = usersWithStatuses.map(u => u.user.id);
setNewStatusIds(prev => new Set([...prev, ...updatedIds]));
const updatedIds = usersWithStatuses.map((u) => u.user.id);
setNewStatusIds((prev) => new Set([...prev, ...updatedIds]));
// Remove animation after 1 second
setTimeout(() => {
setNewStatusIds(prev => {
setNewStatusIds((prev) => {
const updated = new Set(prev);
updatedIds.forEach(id => updated.delete(id));
updatedIds.forEach((id) => updated.delete(id));
return updated;
});
}, 1000);
@ -217,7 +244,8 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
setSelectedUsers([]);
setStatusInput('');
},
onError: (error, _variables, context) => { // Fixed unused variables
onError: (error, _variables, context) => {
// Fixed unused variables
// Rollback optimistic update
if (context?.previousData) {
queryClient.setQueryData(['users-with-statuses'], context.previousData);
@ -239,15 +267,15 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
updateStatusMutation.mutate({
usersWithStatuses: selectedUsers,
status: statusInput.trim()
status: statusInput.trim(),
});
};
const handleCheckboxChange = (user: UserWithStatus) => {
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, user]
: [...prev, user],
);
};
@ -263,31 +291,31 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
useEffect(() => {
setSelectAll(
selectedUsers.length === usersWithStatuses.length &&
usersWithStatuses.length > 0
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 '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' />;
}
}
};
const getConnectionText = () => {
switch (connectionStatus) {
case "connected":
return "Connected"
case "connecting":
return "Connecting..."
case "disconnected":
return "Disconnected"
case 'connected':
return 'Connected';
case 'connecting':
return 'Connecting...';
case 'disconnected':
return 'Disconnected';
}
}
};
const formatTime = (timestamp: string) => {
const date = new Date(timestamp);
@ -343,12 +371,12 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
Tech Status
</h2>
{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' />
<span className='text-xs'>Updating...</span>
</Badge>
) : (
<Badge variant="outline" className="flex items-center gap-2">
<Badge variant='outline' className='flex items-center gap-2'>
{getConnectionIcon()}
<span className='text-xs'>{getConnectionText()}</span>
</Badge>
@ -371,9 +399,12 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
{/* Status Cards */}
<div className='space-y-3'>
{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 isUpdatedByOther = userWithStatus.updated_by &&
const isUpdatedByOther =
userWithStatus.updated_by &&
userWithStatus.updated_by.id !== userWithStatus.user.id;
return (
@ -392,7 +423,9 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
{!tvMode && (
<Checkbox
checked={isSelected}
onCheckedChange={() => handleCheckboxChange(userWithStatus)}
onCheckedChange={() =>
handleCheckboxChange(userWithStatus)
}
onClick={(e) => e.stopPropagation()}
/>
)}
@ -402,7 +435,9 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
className={tvMode ? 'w-16 h-16' : 'w-12 h-12'}
/>
<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'}
</h3>
{isUpdatedByOther && (
@ -413,7 +448,9 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
className='w-3 h-3'
/>
{userWithStatus.updated_by && (
<span className={`text-xs ${tvMode ? 'text-3xl' : ''}`}>
<span
className={`text-xs ${tvMode ? 'text-3xl' : ''}`}
>
Updated by {userWithStatus.updated_by.full_name}
</span>
)}
@ -438,7 +475,9 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
transition-colors cursor-pointer text-left
${tvMode ? 'text-4xl' : 'text-base'}
`}
onClick={() => setSelectedHistoryUser(userWithStatus.user)}
onClick={() =>
setSelectedHistoryUser(userWithStatus.user)
}
>
<p className='font-medium'>{userWithStatus.status}</p>
</div>
@ -455,7 +494,9 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
{usersWithStatuses.length === 0 && (
<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
</p>
</Card>
@ -475,7 +516,11 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
value={statusInput}
onChange={(e) => setStatusInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && !updateStatusMutation.isPending) {
if (
e.key === 'Enter' &&
!e.shiftKey &&
!updateStatusMutation.isPending
) {
e.preventDefault();
handleUpdateStatus();
}
@ -491,8 +536,7 @@ export const StatusList = ({ initialStatuses = [] }: StatusListProps) => { // Fi
? 'Updating...'
: selectedUsers.length > 0
? `Update ${selectedUsers.length} Users`
: 'Update Status'
}
: 'Update Status'}
</SubmitButton>
</div>
{selectedUsers.length > 0 && (

View File

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

View File

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

View File

@ -32,7 +32,7 @@ export const Loading: React.FC<Loading_Props> = ({
}, [intervalMs, alpha]);
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} />
</div>
);