Tech Table & History components completely rewritten & cleaned up. Just need to implement the realtime subscription now.
This commit is contained in:
@ -19,23 +19,23 @@ const Footer = () => {
|
||||
</Link>
|
||||
</div>
|
||||
<div className='flex-1 flex justify-center'>
|
||||
<div className='text-sm text-center space-y-1'>
|
||||
<div className='flex my-auto'>
|
||||
<Image
|
||||
src='/favicon.png'
|
||||
width={18}
|
||||
height={15}
|
||||
className='mr-1 mt-0.5'
|
||||
alt='Tech Tracker Logo'
|
||||
/>
|
||||
<p>
|
||||
<strong>Tech Tracker</strong> - City of Gulfport IT Department
|
||||
</p>
|
||||
</div>
|
||||
<p className='text-muted-foreground'>
|
||||
Open Source • MIT Licensed • Self-Hosted
|
||||
</p>
|
||||
</div>
|
||||
<div className='text-sm text-center space-y-1'>
|
||||
<div className='flex my-auto'>
|
||||
<Image
|
||||
src='/favicon.png'
|
||||
width={18}
|
||||
height={15}
|
||||
className='mr-1 mt-0.5'
|
||||
alt='Tech Tracker Logo'
|
||||
/>
|
||||
<p>
|
||||
<strong>Tech Tracker</strong> - City of Gulfport IT Department
|
||||
</p>
|
||||
</div>
|
||||
<p className='text-muted-foreground'>
|
||||
Open Source • MIT Licensed • Self-Hosted
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-[160px]'></div> {/* Spacer to balance the layout */}
|
||||
</footer>
|
||||
|
@ -62,7 +62,9 @@ const AvatarDropdown = () => {
|
||||
</Avatar>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel className='font-bold'>{profile?.full_name}</DropdownMenuLabel>
|
||||
<DropdownMenuLabel className='font-bold'>
|
||||
{profile?.full_name}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<button
|
||||
|
@ -22,62 +22,63 @@ import {
|
||||
TableRow,
|
||||
Button,
|
||||
} from '@/components/ui';
|
||||
import {
|
||||
getUserHistory,
|
||||
getAllHistory,
|
||||
type HistoryEntry,
|
||||
} from '@/lib/hooks';
|
||||
import { getUserHistory, getAllHistory } from '@/lib/hooks';
|
||||
import type { Profile } from '@/utils/supabase';
|
||||
import type { UserWithStatus } from '@/lib/hooks';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
type HistoryDrawerProps = {
|
||||
user_id: string;
|
||||
user?: Profile | null;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const HistoryDrawer: React.FC<HistoryDrawerProps> = ({ user_id }) => {
|
||||
const [history, setHistory] = useState<HistoryEntry[]>([]);
|
||||
export const HistoryDrawer: React.FC<HistoryDrawerProps> = ({
|
||||
user = null,
|
||||
className = 'max-w-4xl mx-auto',
|
||||
}) => {
|
||||
const [history, setHistory] = useState<UserWithStatus[]>([]);
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [totalPages, setTotalPages] = useState<number>(1);
|
||||
const [totalCount, setTotalCount] = useState<number>(0);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const perPage = 50;
|
||||
|
||||
const fetchHistory = useCallback(async (currentPage: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
let response;
|
||||
|
||||
if (user_id && user_id !== '') {
|
||||
response = await getUserHistory(user_id, currentPage, perPage);
|
||||
} else {
|
||||
response = await getAllHistory(currentPage, perPage);
|
||||
const fetchHistory = useCallback(
|
||||
async (currentPage: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
let response;
|
||||
if (user && user.id !== '') {
|
||||
response = await getUserHistory(user.id, currentPage, perPage);
|
||||
} else {
|
||||
response = await getAllHistory(currentPage, perPage);
|
||||
}
|
||||
if (!response.success) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
setHistory(response.data.statuses);
|
||||
setTotalPages(response.data.meta.total_pages);
|
||||
setTotalCount(response.data.meta.total_count);
|
||||
} catch (error) {
|
||||
toast.error(`Error fetching history: ${error as Error}`);
|
||||
setHistory([]);
|
||||
setTotalPages(1);
|
||||
setTotalCount(0);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
setHistory(response.data.data);
|
||||
setTotalPages(response.data.meta.total_pages);
|
||||
setTotalCount(response.data.meta.total_count);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Error fetching history: ${errorMessage}`);
|
||||
setHistory([]);
|
||||
setTotalPages(1);
|
||||
setTotalCount(0);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [user_id, perPage]);
|
||||
},
|
||||
[user, perPage],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchHistory(page);
|
||||
fetchHistory(page).catch((error) => {
|
||||
toast.error(`Error fetching history: ${error as Error}`);
|
||||
});
|
||||
}, [page, fetchHistory]);
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
if (newPage >= 1 && newPage <= totalPages) {
|
||||
setPage(newPage);
|
||||
}
|
||||
if (newPage >= 1 && newPage <= totalPages) setPage(newPage);
|
||||
};
|
||||
|
||||
const formatTime = (timestamp: string) => {
|
||||
@ -92,17 +93,8 @@ export const HistoryDrawer: React.FC<HistoryDrawerProps> = ({ user_id }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const getDisplayName = (entry: HistoryEntry) => {
|
||||
return entry.user_profile?.full_name || 'Unknown User';
|
||||
};
|
||||
|
||||
const getUpdatedByName = (entry: HistoryEntry) => {
|
||||
if (!entry.updated_by) return 'Self';
|
||||
return entry.updated_by.full_name || 'Unknown';
|
||||
};
|
||||
|
||||
return (
|
||||
<DrawerContent className='max-w-4xl mx-auto'>
|
||||
<DrawerContent className={className}>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>
|
||||
<div className='flex flex-row items-center justify-center py-4'>
|
||||
@ -114,7 +106,7 @@ export const HistoryDrawer: React.FC<HistoryDrawerProps> = ({ user_id }) => {
|
||||
className='w-8 h-8 md:w-12 md:h-12'
|
||||
/>
|
||||
<h1 className='text-lg md:text-2xl lg:text-4xl font-bold pl-2 md:pl-4'>
|
||||
{user_id && user_id !== '' ? 'User History' : 'All History'}
|
||||
{user && user.id !== '' ? 'User History' : 'All History'}
|
||||
</h1>
|
||||
</div>
|
||||
{totalCount > 0 && (
|
||||
@ -124,7 +116,6 @@ export const HistoryDrawer: React.FC<HistoryDrawerProps> = ({ user_id }) => {
|
||||
)}
|
||||
</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
|
||||
<div className='px-4'>
|
||||
<ScrollArea className='h-96 w-full'>
|
||||
{loading ? (
|
||||
@ -142,14 +133,16 @@ export const HistoryDrawer: React.FC<HistoryDrawerProps> = ({ user_id }) => {
|
||||
<TableHead className='font-semibold'>Name</TableHead>
|
||||
<TableHead className='font-semibold'>Status</TableHead>
|
||||
<TableHead className='font-semibold'>Updated By</TableHead>
|
||||
<TableHead className='font-semibold text-right'>Date & Time</TableHead>
|
||||
<TableHead className='font-semibold text-right'>
|
||||
Date & Time
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{history.map((entry) => (
|
||||
<TableRow key={entry.id}>
|
||||
<TableCell className='font-medium'>
|
||||
{getDisplayName(entry)}
|
||||
{entry.user.full_name}
|
||||
</TableCell>
|
||||
<TableCell className='max-w-xs'>
|
||||
<div className='truncate' title={entry.status}>
|
||||
@ -157,7 +150,7 @@ export const HistoryDrawer: React.FC<HistoryDrawerProps> = ({ user_id }) => {
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='text-sm text-muted-foreground'>
|
||||
{getUpdatedByName(entry)}
|
||||
{entry.updated_by?.full_name ?? ''}
|
||||
</TableCell>
|
||||
<TableCell className='text-right text-sm'>
|
||||
{formatTime(entry.created_at)}
|
||||
@ -169,7 +162,6 @@ export const HistoryDrawer: React.FC<HistoryDrawerProps> = ({ user_id }) => {
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<DrawerFooter>
|
||||
{totalPages > 1 && (
|
||||
<Pagination>
|
||||
@ -191,7 +183,9 @@ export const HistoryDrawer: React.FC<HistoryDrawerProps> = ({ user_id }) => {
|
||||
<span>Page</span>
|
||||
<span className='font-bold text-foreground'>{page}</span>
|
||||
<span>of</span>
|
||||
<span className='font-semibold text-foreground'>{totalPages}</span>
|
||||
<span className='font-semibold text-foreground'>
|
||||
{totalPages}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
Array.from({ length: totalPages }).map((_, idx) => (
|
||||
|
@ -9,13 +9,10 @@ import {
|
||||
updateUserStatus,
|
||||
type UserWithStatus,
|
||||
} from '@/lib/hooks/status';
|
||||
import {
|
||||
Drawer,
|
||||
DrawerTrigger,
|
||||
Progress,
|
||||
} from '@/components/ui';
|
||||
import { Drawer, DrawerTrigger, Progress } from '@/components/ui';
|
||||
import { toast } from 'sonner';
|
||||
import { HistoryDrawer } from '@/components/status';
|
||||
import type { Profile } from '@/utils/supabase';
|
||||
import type { RealtimeChannel } from '@supabase/supabase-js';
|
||||
|
||||
type TechTableProps = {
|
||||
@ -33,10 +30,10 @@ export const TechTable = ({
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
const [selectAll, setSelectAll] = useState(false);
|
||||
const [statusInput, setStatusInput] = useState('');
|
||||
const [usersWithStatuses, setUsersWithStatuses] = useState<UserWithStatus[]>(initialStatuses);
|
||||
const [selectedHistoryUserId, setSelectedHistoryUserId] = useState('');
|
||||
|
||||
const supabase = createClient();
|
||||
const [usersWithStatuses, setUsersWithStatuses] =
|
||||
useState<UserWithStatus[]>(initialStatuses);
|
||||
const [selectedHistoryUser, setSelectedHistoryUser] =
|
||||
useState<Profile | null>(null);
|
||||
|
||||
const fetchRecentUsersWithStatuses = useCallback(async () => {
|
||||
try {
|
||||
@ -44,7 +41,7 @@ export const TechTable = ({
|
||||
if (!response.success) throw new Error(response.error);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
toast.error(`Error fetching technicians: ${error as Error}`)
|
||||
toast.error(`Error fetching technicians: ${error as Error}`);
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
@ -78,19 +75,24 @@ export const TechTable = ({
|
||||
} else {
|
||||
const result = await updateStatuses(selectedIds, statusInput);
|
||||
if (!result.success) throw new Error(result.error);
|
||||
toast.success(`Status updated for ${selectedIds.length} selected users.`);
|
||||
toast.success(
|
||||
`Status updated for ${selectedIds.length} selected users.`,
|
||||
);
|
||||
}
|
||||
setSelectedIds([]);
|
||||
setStatusInput('');
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to update status: ${errorMessage}`);
|
||||
}
|
||||
}, [isAuthenticated, statusInput, selectedIds, usersWithStatuses, profile]);
|
||||
}, [isAuthenticated, statusInput, selectedIds]);
|
||||
|
||||
const handleCheckboxChange = (id: string) => {
|
||||
setSelectedIds((prev) =>
|
||||
prev.includes(id) ? prev.filter(prevId => prevId !== id) : [...prev, id]
|
||||
prev.includes(id)
|
||||
? prev.filter((prevId) => prevId !== id)
|
||||
: [...prev, id],
|
||||
);
|
||||
};
|
||||
|
||||
@ -98,7 +100,7 @@ export const TechTable = ({
|
||||
if (selectAll) {
|
||||
setSelectedIds([]);
|
||||
} else {
|
||||
setSelectedIds(usersWithStatuses.map(tech => tech.user.id));
|
||||
setSelectedIds(usersWithStatuses.map((tech) => tech.user.id));
|
||||
}
|
||||
setSelectAll(!selectAll);
|
||||
};
|
||||
@ -106,7 +108,7 @@ export const TechTable = ({
|
||||
useEffect(() => {
|
||||
setSelectAll(
|
||||
selectedIds.length === usersWithStatuses.length &&
|
||||
usersWithStatuses.length > 0
|
||||
usersWithStatuses.length > 0,
|
||||
);
|
||||
}, [selectedIds.length, usersWithStatuses.length]);
|
||||
|
||||
@ -116,9 +118,9 @@ export const TechTable = ({
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
});
|
||||
const day = date.getDate()
|
||||
const day = date.getDate();
|
||||
const month = date.toLocaleString('default', { month: 'long' });
|
||||
return `${time} = ${month} ${day}`;
|
||||
return `${time} - ${month} ${day}`;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
@ -131,7 +133,10 @@ export const TechTable = ({
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<table className={`w-full text-center border-collapse ${tvMode ? 'text-4xl lg:text-5xl' : 'text-base lg:text-lg'}`}>
|
||||
<table
|
||||
className={`w-full text-center border-collapse \
|
||||
${tvMode ? 'text-4xl lg:text-5xl' : 'text-base lg:text-lg'}`}
|
||||
>
|
||||
<thead>
|
||||
<tr className='bg-muted'>
|
||||
{!tvMode && (
|
||||
@ -150,7 +155,7 @@ export const TechTable = ({
|
||||
<DrawerTrigger className='hover:underline'>
|
||||
Status
|
||||
</DrawerTrigger>
|
||||
<HistoryDrawer user_id='' />
|
||||
<HistoryDrawer />
|
||||
</Drawer>
|
||||
</th>
|
||||
<th className='py-3 px-4 border font-semibold'>Updated At</th>
|
||||
@ -171,7 +176,9 @@ export const TechTable = ({
|
||||
type='checkbox'
|
||||
className='scale-125 cursor-pointer'
|
||||
checked={selectedIds.includes(userWithStatus.user.id)}
|
||||
onChange={() => handleCheckboxChange(userWithStatus.user.id)}
|
||||
onChange={() =>
|
||||
handleCheckboxChange(userWithStatus.user.id)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
@ -182,15 +189,12 @@ export const TechTable = ({
|
||||
<Drawer>
|
||||
<DrawerTrigger
|
||||
className='text-left w-full p-2 rounded hover:bg-muted transition-colors'
|
||||
onClick={() => setSelectedHistoryUserId(userWithStatus.user.id)}
|
||||
onClick={() => setSelectedHistoryUser(userWithStatus.user)}
|
||||
>
|
||||
{userWithStatus.status}
|
||||
</DrawerTrigger>
|
||||
{selectedHistoryUserId === userWithStatus.user.id && (
|
||||
<HistoryDrawer
|
||||
key={selectedHistoryUserId}
|
||||
user_id={selectedHistoryUserId}
|
||||
/>
|
||||
{selectedHistoryUser === userWithStatus.user && (
|
||||
<HistoryDrawer user={selectedHistoryUser} />
|
||||
)}
|
||||
</Drawer>
|
||||
</td>
|
||||
@ -201,7 +205,43 @@ export const TechTable = ({
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{!tvMode && (
|
||||
<div className='mx-auto flex flex-row items-center justify-center py-5 gap-4'>
|
||||
<input
|
||||
autoFocus
|
||||
type='text'
|
||||
placeholder='New Status'
|
||||
className={
|
||||
'min-w-[120px] lg:min-w-[400px] py-2 px-3 rounded-xl \
|
||||
border bg-background lg:text-2xl focus:outline-none \
|
||||
focus:ring-2 focus:ring-primary'
|
||||
}
|
||||
value={statusInput}
|
||||
onChange={(e) => setStatusInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
updateStatus().catch((error) => {
|
||||
toast.error(`Failed to update status: ${error as Error}`);
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type='submit'
|
||||
className={
|
||||
'min-w-[100px] lg:min-w-[160px] py-2 px-4 rounded-xl \
|
||||
text-center font-semibold lg:text-2xl bg-primary \
|
||||
text-primary-foreground hover:bg-primary/90 \
|
||||
transition-colors disabled:opacity-50 \
|
||||
disabled:cursor-not-allowed'
|
||||
}
|
||||
onClick={() => void updateStatus()}
|
||||
disabled={!statusInput.trim()}
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
};
|
||||
|
@ -1,32 +1,32 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
import * as React from 'react';
|
||||
import { Drawer as DrawerPrimitive } from 'vaul';
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Drawer({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
||||
return <DrawerPrimitive.Root data-slot='drawer' {...props} />;
|
||||
}
|
||||
|
||||
function DrawerTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
||||
return <DrawerPrimitive.Trigger data-slot='drawer-trigger' {...props} />;
|
||||
}
|
||||
|
||||
function DrawerPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
||||
return <DrawerPrimitive.Portal data-slot='drawer-portal' {...props} />;
|
||||
}
|
||||
|
||||
function DrawerClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
||||
return <DrawerPrimitive.Close data-slot='drawer-close' {...props} />;
|
||||
}
|
||||
|
||||
function DrawerOverlay({
|
||||
@ -35,14 +35,14 @@ function DrawerOverlay({
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||
return (
|
||||
<DrawerPrimitive.Overlay
|
||||
data-slot="drawer-overlay"
|
||||
data-slot='drawer-overlay'
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerContent({
|
||||
@ -51,48 +51,48 @@ function DrawerContent({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||
return (
|
||||
<DrawerPortal data-slot="drawer-portal">
|
||||
<DrawerPortal data-slot='drawer-portal'>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
data-slot="drawer-content"
|
||||
data-slot='drawer-content'
|
||||
className={cn(
|
||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||
className
|
||||
'group/drawer-content bg-background fixed z-50 flex h-auto flex-col',
|
||||
'data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b',
|
||||
'data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t',
|
||||
'data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm',
|
||||
'data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||
<div className='bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block' />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
data-slot='drawer-header'
|
||||
className={cn(
|
||||
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
|
||||
className
|
||||
'flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
data-slot='drawer-footer'
|
||||
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerTitle({
|
||||
@ -101,11 +101,11 @@ function DrawerTitle({
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||
return (
|
||||
<DrawerPrimitive.Title
|
||||
data-slot="drawer-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
data-slot='drawer-title'
|
||||
className={cn('text-foreground font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerDescription({
|
||||
@ -114,11 +114,11 @@ function DrawerDescription({
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||
return (
|
||||
<DrawerPrimitive.Description
|
||||
data-slot="drawer-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
data-slot='drawer-description'
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -132,4 +132,4 @@ export {
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
}
|
||||
};
|
||||
|
@ -1,68 +1,68 @@
|
||||
import * as React from "react"
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from "lucide-react"
|
||||
} from 'lucide-react';
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button, buttonVariants } from '@/components/ui/button';
|
||||
|
||||
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
data-slot="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
role='navigation'
|
||||
aria-label='pagination'
|
||||
data-slot='pagination'
|
||||
className={cn('mx-auto flex w-full justify-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"ul">) {
|
||||
}: React.ComponentProps<'ul'>) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="pagination-content"
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
data-slot='pagination-content'
|
||||
className={cn('flex flex-row items-center gap-1', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||
return <li data-slot="pagination-item" {...props} />
|
||||
function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
|
||||
return <li data-slot='pagination-item' {...props} />;
|
||||
}
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean
|
||||
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||
React.ComponentProps<"a">
|
||||
isActive?: boolean;
|
||||
} & Pick<React.ComponentProps<typeof Button>, 'size'> &
|
||||
React.ComponentProps<'a'>;
|
||||
|
||||
function PaginationLink({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
size = 'icon',
|
||||
...props
|
||||
}: PaginationLinkProps) {
|
||||
return (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
data-slot="pagination-link"
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
data-slot='pagination-link'
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
variant: isActive ? 'outline' : 'ghost',
|
||||
size,
|
||||
}),
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationPrevious({
|
||||
@ -71,15 +71,15 @@ function PaginationPrevious({
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||
aria-label='Go to previous page'
|
||||
size='default'
|
||||
className={cn('gap-1 px-2.5 sm:pl-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
<span className="hidden sm:block">Previous</span>
|
||||
<span className='hidden sm:block'>Previous</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationNext({
|
||||
@ -88,32 +88,32 @@ function PaginationNext({
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||
aria-label='Go to next page'
|
||||
size='default'
|
||||
className={cn('gap-1 px-2.5 sm:pr-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block">Next</span>
|
||||
<span className='hidden sm:block'>Next</span>
|
||||
<ChevronRightIcon />
|
||||
</PaginationLink>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
}: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
data-slot="pagination-ellipsis"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
data-slot='pagination-ellipsis'
|
||||
className={cn('flex size-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
<MoreHorizontalIcon className='size-4' />
|
||||
<span className='sr-only'>More pages</span>
|
||||
</span>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -124,4 +124,4 @@ export {
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
}
|
||||
};
|
||||
|
@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
import * as React from 'react';
|
||||
import * as ProgressPrimitive from '@radix-ui/react-progress';
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
@ -12,20 +12,20 @@ function Progress({
|
||||
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||
return (
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
data-slot='progress'
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
className
|
||||
'bg-primary/20 relative h-2 w-full overflow-hidden rounded-full',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
data-slot="progress-indicator"
|
||||
className="bg-primary h-full w-full flex-1 transition-all"
|
||||
data-slot='progress-indicator'
|
||||
className='bg-primary h-full w-full flex-1 transition-all'
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Progress }
|
||||
export { Progress };
|
||||
|
@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
import * as React from 'react';
|
||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
@ -12,47 +12,47 @@ function ScrollArea({
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
data-slot='scroll-area'
|
||||
className={cn('relative', className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
data-slot='scroll-area-viewport'
|
||||
className='focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1'
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
orientation = 'vertical',
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
data-slot='scroll-area-scrollbar'
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className
|
||||
'flex touch-none p-px transition-colors select-none',
|
||||
orientation === 'vertical' &&
|
||||
'h-full w-2.5 border-l border-l-transparent',
|
||||
orientation === 'horizontal' &&
|
||||
'h-2.5 flex-col border-t border-t-transparent',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
data-slot='scroll-area-thumb'
|
||||
className='bg-border relative flex-1 rounded-full'
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
export { ScrollArea, ScrollBar };
|
||||
|
@ -1,107 +1,107 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import * as React from "react"
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
function Table({ className, ...props }: React.ComponentProps<'table'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
data-slot='table-container'
|
||||
className='relative w-full overflow-x-auto'
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
data-slot='table'
|
||||
className={cn('w-full caption-bottom text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
data-slot='table-header'
|
||||
className={cn('[&_tr]:border-b', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
data-slot='table-body'
|
||||
className={cn('[&_tr:last-child]:border-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
data-slot='table-footer'
|
||||
className={cn(
|
||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
data-slot='table-row'
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
className
|
||||
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
data-slot='table-head'
|
||||
className={cn(
|
||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
data-slot='table-cell'
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
}: React.ComponentProps<'caption'>) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||
data-slot='table-caption'
|
||||
className={cn('text-muted-foreground mt-4 text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -113,4 +113,4 @@ export {
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
};
|
||||
|
Reference in New Issue
Block a user