So its like broken but we are rewriting status.ts & TechTable & HistoryTable

This commit is contained in:
2025-06-12 16:55:24 -05:00
parent 185a7ea500
commit 653fe64bbf
23 changed files with 2536 additions and 647 deletions

View File

@ -0,0 +1,236 @@
'use client';
import React, { useState, useEffect, useCallback } from 'react';
import Image from 'next/image';
import {
DrawerClose,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerTitle,
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationNext,
PaginationPrevious,
ScrollArea,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
Button,
} from '@/components/ui';
import {
getUserHistory,
getAllHistory,
type HistoryEntry,
} from '@/lib/hooks';
import { toast } from 'sonner';
type HistoryDrawerProps = {
user_id: string;
};
export const HistoryDrawer: React.FC<HistoryDrawerProps> = ({ user_id }) => {
const [history, setHistory] = useState<HistoryEntry[]>([]);
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);
}
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]);
useEffect(() => {
void fetchHistory(page);
}, [page, fetchHistory]);
const handlePageChange = (newPage: number) => {
if (newPage >= 1 && newPage <= totalPages) {
setPage(newPage);
}
};
const formatTime = (timestamp: string) => {
const date = new Date(timestamp);
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true,
});
};
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'>
<DrawerHeader>
<DrawerTitle>
<div className='flex flex-row items-center justify-center py-4'>
<Image
src='/favicon.png'
alt='Tech Tracker Logo'
width={60}
height={60}
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'}
</h1>
</div>
{totalCount > 0 && (
<p className='text-sm text-muted-foreground text-center'>
{totalCount} total entries
</p>
)}
</DrawerTitle>
</DrawerHeader>
<div className='px-4'>
<ScrollArea className='h-96 w-full'>
{loading ? (
<div className='flex justify-center items-center h-32'>
<div className='animate-spin rounded-full h-8 w-8 border-b-2 border-primary'></div>
</div>
) : history.length === 0 ? (
<div className='flex justify-center items-center h-32'>
<p className='text-muted-foreground'>No history found</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<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>
</TableRow>
</TableHeader>
<TableBody>
{history.map((entry) => (
<TableRow key={entry.id}>
<TableCell className='font-medium'>
{getDisplayName(entry)}
</TableCell>
<TableCell className='max-w-xs'>
<div className='truncate' title={entry.status}>
{entry.status}
</div>
</TableCell>
<TableCell className='text-sm text-muted-foreground'>
{getUpdatedByName(entry)}
</TableCell>
<TableCell className='text-right text-sm'>
{formatTime(entry.created_at)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</ScrollArea>
</div>
<DrawerFooter>
{totalPages > 1 && (
<Pagination>
<PaginationContent>
{page > 1 && (
<PaginationItem>
<PaginationPrevious
href='#'
onClick={(e) => {
e.preventDefault();
handlePageChange(page - 1);
}}
/>
</PaginationItem>
)}
{totalPages > 10 ? (
<div className='flex items-center gap-2 text-sm text-muted-foreground'>
<span>Page</span>
<span className='font-bold text-foreground'>{page}</span>
<span>of</span>
<span className='font-semibold text-foreground'>{totalPages}</span>
</div>
) : (
Array.from({ length: totalPages }).map((_, idx) => (
<PaginationItem key={idx}>
<PaginationLink
href='#'
isActive={page === idx + 1}
onClick={(e) => {
e.preventDefault();
handlePageChange(idx + 1);
}}
>
{idx + 1}
</PaginationLink>
</PaginationItem>
))
)}
{page < totalPages && (
<PaginationItem>
<PaginationNext
href='#'
onClick={(e) => {
e.preventDefault();
handlePageChange(page + 1);
}}
/>
</PaginationItem>
)}
</PaginationContent>
</Pagination>
)}
<DrawerClose asChild>
<Button variant='outline' className='mt-4'>
Close
</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
);
};

View File

@ -0,0 +1,333 @@
'use client';
import { useState, useEffect, useCallback, useRef } from 'react';
import { useAuth, useTVMode } from '@/components/context';
import {
getUserStatuses,
updateUserStatus,
updateCurrentUserStatus,
type UserStatus,
} from '@/lib/hooks';
import {
Drawer,
DrawerTrigger,
Progress,
} from '@/components/ui';
import { toast } from 'sonner';
import { HistoryDrawer } from '@/components/status';
import { createClient } from '@/utils/supabase';
import type { RealtimeChannel } from '@supabase/supabase-js';
export const TechTable = () => {
const { isAuthenticated, profile } = useAuth();
const { tvMode } = useTVMode();
const [loading, setLoading] = useState(true);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [selectAll, setSelectAll] = useState(false);
const [statusInput, setStatusInput] = useState('');
const [technicianData, setTechnicianData] = useState<UserStatus[]>([]);
const [selectedUserId, setSelectedUserId] = useState('');
const [recentlyUpdatedIds, setRecentlyUpdatedIds] = useState<Set<string>>(new Set());
const channelRef = useRef<RealtimeChannel | null>(null);
const supabase = createClient();
const fetchTechnicians = useCallback(async () => {
try {
const response = await getUserStatuses();
if (!response.success) throw new Error(response.error);
return response.data;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
toast.error(`Error fetching technicians: ${errorMessage}`);
return [];
}
}, []);
// Setup realtime broadcast subscription
const setupRealtimeSubscription = useCallback(() => {
console.log('Setting up realtime broadcast subscription');
const channel = supabase.channel('status_updates');
channel
.on('broadcast', { event: 'status_updated' }, (payload) => {
console.log('Status update received:', payload);
const userStatus = payload.payload.user_status as UserStatus;
// Update the technician data
setTechnicianData(prevData => {
const newData = [...prevData];
const existingIndex = newData.findIndex(tech => tech.id === userStatus.id);
if (existingIndex !== -1) {
// Update existing user if this status is more recent
if (new Date(userStatus.created_at) > new Date(newData[existingIndex].created_at)) {
newData[existingIndex] = userStatus;
// Mark as recently updated
setRecentlyUpdatedIds(prev => {
const newSet = new Set(prev);
newSet.add(userStatus.id);
return newSet;
});
// Remove highlight after 3 seconds
setTimeout(() => {
setRecentlyUpdatedIds(current => {
const updatedSet = new Set(current);
updatedSet.delete(userStatus.id);
return updatedSet;
});
}, 3000);
}
} else {
// Add new user
newData.push(userStatus);
// Mark as recently updated
setRecentlyUpdatedIds(prev => {
const newSet = new Set(prev);
newSet.add(userStatus.id);
return newSet;
});
// Remove highlight after 3 seconds
setTimeout(() => {
setRecentlyUpdatedIds(current => {
const updatedSet = new Set(current);
updatedSet.delete(userStatus.id);
return updatedSet;
});
}, 3000);
}
// Sort by most recent
newData.sort((a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
);
return newData;
});
})
.subscribe((status) => {
console.log('Realtime subscription status:', status);
});
channelRef.current = channel;
return channel;
}, [supabase]);
const updateStatus = useCallback(async () => {
if (!isAuthenticated) {
toast.error('You must be signed in to update status.');
return;
}
if (!statusInput.trim()) {
toast.error('Please enter a status.');
return;
}
try {
if (selectedIds.length === 0) {
// Update current user - find them by profile match
let targetUserId = null;
if (profile?.full_name) {
const currentUserInTable = technicianData.find(tech =>
tech.full_name === profile.full_name || tech.id === profile.id
);
targetUserId = currentUserInTable?.id;
}
if (targetUserId) {
const result = await updateUserStatus([targetUserId], statusInput);
if (!result.success) throw new Error(result.error);
toast.success('Your status has been updated.');
} else {
const result = await updateCurrentUserStatus(statusInput);
if (!result.success) throw new Error(result.error);
toast.success('Your status has been updated.');
}
} else {
const result = await updateUserStatus(selectedIds, statusInput);
if (!result.success) throw new Error(result.error);
toast.success(`Status updated for ${selectedIds.length} technician${selectedIds.length > 1 ? 's' : ''}.`);
}
setSelectedIds([]);
setStatusInput('');
// No need to manually fetch - broadcast will handle updates
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
toast.error(`Failed to update status: ${errorMessage}`);
}
}, [isAuthenticated, statusInput, selectedIds, technicianData, profile]);
const handleCheckboxChange = (id: string) => {
setSelectedIds(prev =>
prev.includes(id) ? prev.filter(prevId => prevId !== id) : [...prev, id]
);
};
const handleSelectAllChange = () => {
if (selectAll) {
setSelectedIds([]);
} else {
setSelectedIds(technicianData.map(tech => tech.id));
}
setSelectAll(!selectAll);
};
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}`;
};
// Initial load and setup realtime subscription
useEffect(() => {
const loadData = async () => {
const data = await fetchTechnicians();
setTechnicianData(data);
setLoading(false);
};
void loadData();
// Setup realtime subscription
const channel = setupRealtimeSubscription();
// Cleanup function
return () => {
if (channel) {
console.log('Removing broadcast channel');
void supabase.removeChannel(channel);
channelRef.current = null;
}
};
}, [fetchTechnicians, setupRealtimeSubscription, supabase]);
// Update select all state
useEffect(() => {
setSelectAll(
selectedIds.length === technicianData.length &&
technicianData.length > 0
);
}, [selectedIds.length, technicianData.length]);
if (loading) {
return (
<div className='flex justify-center items-center min-h-[400px]'>
<Progress value={33} className='w-64' />
</div>
);
}
return (
<div className='w-full max-w-7xl mx-auto px-4'>
<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 && (
<th className='py-3 px-3 border'>
<input
type='checkbox'
className='scale-125 cursor-pointer'
checked={selectAll}
onChange={handleSelectAllChange}
/>
</th>
)}
<th className='py-3 px-4 border font-semibold'>Name</th>
<th className='py-3 px-4 border font-semibold'>
<Drawer>
<DrawerTrigger className='hover:underline'>
Status
</DrawerTrigger>
<HistoryDrawer user_id='' />
</Drawer>
</th>
<th className='py-3 px-4 border font-semibold'>Updated At</th>
</tr>
</thead>
<tbody>
{technicianData.map((technician, index) => (
<tr
key={technician.id}
className={`
${index % 2 === 0 ? 'bg-muted/50' : 'bg-background'}
hover:bg-muted/75 transition-all duration-300
${recentlyUpdatedIds.has(technician.id) ? 'ring-2 ring-blue-500 ring-opacity-75 bg-blue-50 dark:bg-blue-900/20' : ''}
`}
>
{!tvMode && (
<td className='py-2 px-3 border'>
<input
type='checkbox'
className='scale-125 cursor-pointer'
checked={selectedIds.includes(technician.id)}
onChange={() => handleCheckboxChange(technician.id)}
/>
</td>
)}
<td className='py-3 px-4 border font-medium'>
{technician.full_name ?? 'Unknown User'}
</td>
<td className='py-3 px-4 border'>
<Drawer>
<DrawerTrigger
className='text-left w-full p-2 rounded hover:bg-muted transition-colors'
onClick={() => setSelectedUserId(technician.id)}
>
{technician.status}
</DrawerTrigger>
{selectedUserId === technician.id && (
<HistoryDrawer
key={selectedUserId}
user_id={selectedUserId}
/>
)}
</Drawer>
</td>
<td className='py-3 px-4 border text-muted-foreground'>
{formatTime(technician.created_at)}
</td>
</tr>
))}
</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') {
void updateStatus();
}
}}
/>
<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>
);
};

View File

@ -0,0 +1,2 @@
export * from './HistoryDrawer';
export * from './TechTable';

View File

@ -0,0 +1,135 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.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
)}
{...props}
/>
)
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.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
)}
{...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" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
)
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
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
)}
{...props}
/>
)
}
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)}
{...props}
/>
)
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

View File

@ -1,11 +1,16 @@
export * from '@/components/ui/avatar';
export * from '@/components/ui/badge';
export * from '@/components/ui/button';
export * from '@/components/ui/card';
export * from '@/components/ui/checkbox';
export * from '@/components/ui/dropdown-menu';
export * from '@/components/ui/form';
export * from '@/components/ui/input';
export * from '@/components/ui/label';
export * from '@/components/ui/separator';
export * from '@/components/ui/sonner';
export * from './avatar';
export * from './badge';
export * from './button';
export * from './card';
export * from './checkbox';
export * from './drawer';
export * from './dropdown-menu';
export * from './form';
export * from './input';
export * from './label';
export * from './pagination';
export * from './progress';
export * from './scroll-area';
export * from './separator';
export * from './sonner';
export * from './table';

View File

@ -0,0 +1,127 @@
import * as React from "react"
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
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)}
{...props}
/>
)
}
function PaginationContent({
className,
...props
}: React.ComponentProps<"ul">) {
return (
<ul
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} />
}
type PaginationLinkProps = {
isActive?: boolean
} & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a">
function PaginationLink({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
return (
<a
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
)
}
function PaginationPrevious({
className,
...props
}: 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)}
{...props}
>
<ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink>
)
}
function PaginationNext({
className,
...props
}: 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)}
{...props}
>
<span className="hidden sm:block">Next</span>
<ChevronRightIcon />
</PaginationLink>
)
}
function PaginationEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden
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>
</span>
)
}
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}

View File

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"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"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
}
export { Progress }

View File

@ -0,0 +1,58 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
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"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
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
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }

116
src/components/ui/table.tsx Normal file
View File

@ -0,0 +1,116 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
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
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"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">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}