mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-01 18:19:43 +00:00
Update
This commit is contained in:
1 parent
e7379ccdb1
commit
d18f9d32b0
49 files changed
+3739
-665
No files matched your search
@@ -1,14 +1,15 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useAuth, PermissionKey } from '@/contexts/AuthContext';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode;
|
||||
requireSuperAdmin?: boolean;
|
||||
requiredPermission?: PermissionKey;
|
||||
}
|
||||
|
||||
export function ProtectedRoute({ children, requireSuperAdmin = false }: ProtectedRouteProps) {
|
||||
const { session, loading, isSuperAdmin } = useAuth();
|
||||
export function ProtectedRoute({ children, requireSuperAdmin = false, requiredPermission }: ProtectedRouteProps) {
|
||||
const { session, loading, isSuperAdmin, hasPermission } = useAuth();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -26,5 +27,9 @@ export function ProtectedRoute({ children, requireSuperAdmin = false }: Protecte
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
if (requiredPermission && !hasPermission(requiredPermission)) {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -6,19 +6,33 @@ import { Toaster } from 'sonner';
|
||||
export function AppLayout() {
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-background">
|
||||
{/* Subtle gradient overlay */}
|
||||
<div className="fixed inset-0 bg-gradient-to-br from-primary/[0.02] via-transparent to-transparent pointer-events-none" />
|
||||
|
||||
<Sidebar />
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<div className="flex flex-1 flex-col overflow-hidden relative">
|
||||
{/* Header */}
|
||||
<header className="flex h-16 items-center border-b px-6">
|
||||
<header className="flex h-16 items-center border-b border-white/5 px-8 bg-card/50 backdrop-blur-sm">
|
||||
<Breadcrumbs />
|
||||
</header>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 overflow-auto p-6">
|
||||
<Outlet />
|
||||
<main className="flex-1 overflow-auto p-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<Toaster position="top-right" richColors />
|
||||
<Toaster
|
||||
position="top-right"
|
||||
toastOptions={{
|
||||
style: {
|
||||
background: 'hsl(240 8% 10%)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
color: 'hsl(30 20% 95%)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -13,11 +13,17 @@ interface PackInfo {
|
||||
category_name: string | null;
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
id: string;
|
||||
name: string | null;
|
||||
}
|
||||
|
||||
// Generate breadcrumbs based on current path and params
|
||||
function useBreadcrumbs(): { breadcrumbs: BreadcrumbItem[]; loading: boolean } {
|
||||
const location = useLocation();
|
||||
const params = useParams();
|
||||
const [packInfo, setPackInfo] = useState<PackInfo | null>(null);
|
||||
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Fetch pack info when on questions page to get category_id
|
||||
@@ -51,6 +57,37 @@ function useBreadcrumbs(): { breadcrumbs: BreadcrumbItem[]; loading: boolean } {
|
||||
fetchPackInfo();
|
||||
}, [params.packId, params.categoryId]);
|
||||
|
||||
// Fetch user info when on user detail or match chat page
|
||||
useEffect(() => {
|
||||
const fetchUserInfo = async () => {
|
||||
if (params.userId) {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from('profiles')
|
||||
.select('id, name')
|
||||
.eq('id', params.userId)
|
||||
.single();
|
||||
|
||||
if (data) {
|
||||
setUserInfo({
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user info for breadcrumbs:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} else {
|
||||
setUserInfo(null);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserInfo();
|
||||
}, [params.userId]);
|
||||
|
||||
const pathSegments = location.pathname.split('/').filter(Boolean);
|
||||
const breadcrumbs: BreadcrumbItem[] = [];
|
||||
|
||||
@@ -74,6 +111,13 @@ function useBreadcrumbs(): { breadcrumbs: BreadcrumbItem[]; loading: boolean } {
|
||||
breadcrumbs.push({ label: 'Questions', href: currentPath });
|
||||
} else if (segment === 'users') {
|
||||
breadcrumbs.push({ label: 'Users', href: '/users' });
|
||||
// If we have a userId, add the user's name as a breadcrumb
|
||||
if (params.userId && userInfo) {
|
||||
breadcrumbs.push({
|
||||
label: userInfo.name || 'User',
|
||||
href: `/users/${userInfo.id}`,
|
||||
});
|
||||
}
|
||||
} else if (segment === 'matches' && params.userId) {
|
||||
breadcrumbs.push({ label: 'Match Chat' });
|
||||
} else if (segment === 'audit-logs') {
|
||||
@@ -100,26 +144,28 @@ export function Breadcrumbs() {
|
||||
if (breadcrumbs.length === 0 && !loading) return null;
|
||||
|
||||
return (
|
||||
<nav className="flex items-center space-x-1 text-sm text-muted-foreground">
|
||||
<nav className="flex items-center space-x-1 text-sm">
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center hover:text-foreground transition-colors"
|
||||
className="flex items-center text-muted-foreground hover:text-primary transition-colors p-1 rounded-md hover:bg-white/5"
|
||||
>
|
||||
<Home className="h-4 w-4" />
|
||||
</Link>
|
||||
|
||||
{breadcrumbs.map((item, index) => (
|
||||
<div key={index} className="flex items-center">
|
||||
<ChevronRight className="h-4 w-4 mx-1" />
|
||||
<ChevronRight className="h-4 w-4 mx-1 text-muted-foreground/50" />
|
||||
{item.href ? (
|
||||
<Link
|
||||
to={item.href}
|
||||
className="hover:text-foreground transition-colors"
|
||||
className="text-muted-foreground hover:text-primary transition-colors px-2 py-1 rounded-md hover:bg-white/5"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-foreground font-medium">{item.label}</span>
|
||||
<span className="text-foreground font-medium px-2 py-1">
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useAuth, PERMISSION_KEYS, PermissionKey } from '@/contexts/AuthContext';
|
||||
import {
|
||||
LayoutGrid,
|
||||
Users,
|
||||
@@ -8,20 +8,22 @@ import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
Shield,
|
||||
Ticket,
|
||||
MessageSquareText,
|
||||
Target,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { useState } from 'react';
|
||||
import logoImage from '@/assets/logo.png';
|
||||
|
||||
interface NavItem {
|
||||
label: string;
|
||||
href: string;
|
||||
icon: React.ReactNode;
|
||||
requireSuperAdmin?: boolean;
|
||||
requiredPermission?: PermissionKey;
|
||||
}
|
||||
|
||||
const contentNav: NavItem[] = [
|
||||
@@ -29,6 +31,7 @@ const contentNav: NavItem[] = [
|
||||
label: 'Categories',
|
||||
href: '/categories',
|
||||
icon: <LayoutGrid className="h-5 w-5" />,
|
||||
requiredPermission: PERMISSION_KEYS.MANAGE_CATEGORIES,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -37,7 +40,19 @@ const usersNav: NavItem[] = [
|
||||
label: 'Users',
|
||||
href: '/users',
|
||||
icon: <Users className="h-5 w-5" />,
|
||||
requireSuperAdmin: true,
|
||||
requiredPermission: PERMISSION_KEYS.VIEW_USERS,
|
||||
},
|
||||
{
|
||||
label: 'Usage Insights',
|
||||
href: '/usage-insights',
|
||||
icon: <Target className="h-5 w-5" />,
|
||||
requiredPermission: PERMISSION_KEYS.VIEW_USERS,
|
||||
},
|
||||
{
|
||||
label: 'Feedback',
|
||||
href: '/feedback',
|
||||
icon: <MessageSquareText className="h-5 w-5" />,
|
||||
requiredPermission: PERMISSION_KEYS.VIEW_USERS,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -46,29 +61,29 @@ const systemNav: NavItem[] = [
|
||||
label: 'Admins',
|
||||
href: '/admins',
|
||||
icon: <Shield className="h-5 w-5" />,
|
||||
requireSuperAdmin: true,
|
||||
requiredPermission: PERMISSION_KEYS.MANAGE_ADMINS,
|
||||
},
|
||||
{
|
||||
label: 'Redemption Codes',
|
||||
href: '/redemption-codes',
|
||||
icon: <Ticket className="h-5 w-5" />,
|
||||
requireSuperAdmin: true,
|
||||
requiredPermission: PERMISSION_KEYS.MANAGE_CODES,
|
||||
},
|
||||
{
|
||||
label: 'Audit Logs',
|
||||
href: '/audit-logs',
|
||||
icon: <ClipboardList className="h-5 w-5" />,
|
||||
requireSuperAdmin: true,
|
||||
requiredPermission: PERMISSION_KEYS.VIEW_AUDIT_LOGS,
|
||||
},
|
||||
];
|
||||
|
||||
export function Sidebar() {
|
||||
const location = useLocation();
|
||||
const { user, permissions, logout, isSuperAdmin } = useAuth();
|
||||
const { user, permissions, logout, hasPermission } = useAuth();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
const NavLink = ({ item }: { item: NavItem }) => {
|
||||
if (item.requireSuperAdmin && !isSuperAdmin) return null;
|
||||
if (item.requiredPermission && !hasPermission(item.requiredPermission)) return null;
|
||||
|
||||
const isActive = location.pathname.startsWith(item.href);
|
||||
|
||||
@@ -76,27 +91,32 @@ export function Sidebar() {
|
||||
<Link
|
||||
to={item.href}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors",
|
||||
"group flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition-all duration-200",
|
||||
isActive
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
||||
? "bg-gradient-rose text-white shadow-lg glow-rose-sm"
|
||||
: "text-muted-foreground hover:bg-white/5 hover:text-foreground",
|
||||
collapsed && "justify-center px-2"
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
<span className={cn(
|
||||
"transition-transform duration-200",
|
||||
!isActive && "group-hover:scale-110"
|
||||
)}>
|
||||
{item.icon}
|
||||
</span>
|
||||
{!collapsed && <span>{item.label}</span>}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const NavSection = ({ title, items }: { title: string; items: NavItem[] }) => {
|
||||
const visibleItems = items.filter(item => !item.requireSuperAdmin || isSuperAdmin);
|
||||
const visibleItems = items.filter(item => !item.requiredPermission || hasPermission(item.requiredPermission));
|
||||
if (visibleItems.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{!collapsed && (
|
||||
<h4 className="px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
<h4 className="px-3 py-2 text-[11px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||
{title}
|
||||
</h4>
|
||||
)}
|
||||
@@ -110,23 +130,50 @@ export function Sidebar() {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-screen flex-col border-r bg-card transition-all duration-300",
|
||||
collapsed ? "w-16" : "w-64"
|
||||
"flex h-screen flex-col border-r border-white/5 bg-gradient-to-b from-card to-background transition-all duration-300",
|
||||
collapsed ? "w-20" : "w-72"
|
||||
)}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div className="flex h-16 items-center justify-between border-b px-4">
|
||||
<div className="flex h-20 items-center justify-between px-4 border-b border-white/5">
|
||||
{!collapsed && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="h-6 w-6 text-primary" />
|
||||
<span className="text-lg font-bold">Sauci Admin</span>
|
||||
</div>
|
||||
<Link to="/" className="flex items-center gap-3 group">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={logoImage}
|
||||
alt="Sauci"
|
||||
className="h-10 w-10 object-contain transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-rose opacity-0 group-hover:opacity-20 rounded-full blur-xl transition-opacity duration-300" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xl font-bold tracking-tight text-gradient-sunset">
|
||||
Sauci
|
||||
</span>
|
||||
<span className="text-[10px] font-medium uppercase tracking-widest text-muted-foreground">
|
||||
Admin Portal
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
{collapsed && (
|
||||
<Link to="/" className="mx-auto">
|
||||
<img
|
||||
src={logoImage}
|
||||
alt="Sauci"
|
||||
className="h-10 w-10 object-contain"
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Collapse button */}
|
||||
<div className="flex justify-end px-2 py-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
className={cn(collapsed && "mx-auto")}
|
||||
className="h-8 w-8 rounded-lg text-muted-foreground hover:text-foreground hover:bg-white/5"
|
||||
>
|
||||
{collapsed ? (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
@@ -137,25 +184,30 @@ export function Sidebar() {
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex-1 space-y-6 overflow-auto p-4">
|
||||
<div className="flex-1 space-y-6 overflow-auto px-3 py-2">
|
||||
<NavSection title="Content" items={contentNav} />
|
||||
<NavSection title="Users" items={usersNav} />
|
||||
<NavSection title="System" items={systemNav} />
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<Separator className="bg-white/5" />
|
||||
|
||||
{/* User info */}
|
||||
<div className={cn("p-4", collapsed && "flex flex-col items-center")}>
|
||||
<div className={cn("flex items-center gap-3", collapsed && "flex-col")}>
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarFallback>
|
||||
<div className={cn(
|
||||
"flex items-center gap-3 rounded-xl p-2 transition-colors hover:bg-white/5",
|
||||
collapsed && "flex-col p-2"
|
||||
)}>
|
||||
<Avatar className="h-9 w-9 ring-2 ring-primary/20">
|
||||
<AvatarFallback className="bg-gradient-rose text-white font-semibold">
|
||||
{user?.email?.charAt(0).toUpperCase() || 'A'}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
{!collapsed && (
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<p className="truncate text-sm font-medium">{user?.email}</p>
|
||||
<p className="truncate text-sm font-medium text-foreground">
|
||||
{user?.email}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground capitalize">
|
||||
{permissions?.role?.replace('_', ' ')}
|
||||
</p>
|
||||
@@ -165,11 +217,14 @@ export function Sidebar() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size={collapsed ? "icon" : "sm"}
|
||||
className={cn("mt-3", collapsed ? "w-8 h-8" : "w-full")}
|
||||
className={cn(
|
||||
"mt-2 text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors",
|
||||
collapsed ? "w-9 h-9" : "w-full justify-start"
|
||||
)}
|
||||
onClick={logout}
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
{!collapsed && <span className="ml-2">Logout</span>}
|
||||
{!collapsed && <span className="ml-2">Sign Out</span>}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,21 +3,21 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
"inline-flex items-center rounded-lg border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
"border-primary/30 bg-primary text-white",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
"border-white/20 bg-white/10 text-white",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
"border-red-500/30 bg-red-500 text-white",
|
||||
outline: "border-white/30 bg-transparent text-white",
|
||||
success:
|
||||
"border-transparent bg-green-500 text-white hover:bg-green-500/80",
|
||||
"border-emerald-500/30 bg-emerald-500 text-white",
|
||||
warning:
|
||||
"border-transparent bg-yellow-500 text-white hover:bg-yellow-500/80",
|
||||
"border-amber-500/30 bg-amber-500 text-black",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -4,24 +4,24 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-xl text-sm font-medium ring-offset-background transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm hover:shadow-md",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
"border border-white/10 bg-transparent hover:bg-white/5 hover:border-white/20",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
ghost: "hover:bg-white/5 hover:text-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
sm: "h-9 rounded-lg px-3",
|
||||
lg: "h-11 rounded-xl px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ const Card = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
"rounded-xl border border-white/5 bg-card text-card-foreground shadow-sm transition-all duration-200",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -35,7 +35,7 @@ const CardTitle = React.forwardRef<
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
"text-xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -10,7 +10,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex h-10 w-full rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:border-primary/50 disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-200",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -5,7 +5,7 @@ const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<div className="relative w-full overflow-auto rounded-xl border border-white/5">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
@@ -19,7 +19,7 @@ const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b [&_tr]:border-white/5 bg-white/[0.02]", className)} {...props} />
|
||||
));
|
||||
TableHeader.displayName = "TableHeader";
|
||||
|
||||
@@ -42,7 +42,7 @@ const TableFooter = React.forwardRef<
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
"border-t border-white/5 bg-white/[0.02] font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -57,7 +57,7 @@ const TableRow = React.forwardRef<
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
"border-b border-white/5 transition-colors hover:bg-white/[0.02] data-[state=selected]:bg-white/5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -72,7 +72,7 @@ const TableHead = React.forwardRef<
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
"h-12 px-4 text-left align-middle font-semibold text-muted-foreground text-xs uppercase tracking-wider [&:has([role=checkbox])]:pr-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in new issue
Block a user