This commit is contained in:
jjbrunton committed 2025-12-30 11:15:47 +00:00
1 parent a15089f607
commit 456459bb00
41 files changed
+6018

No files matched your search

@@ -0,0 +1,30 @@
import { Navigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import { Loader2 } from 'lucide-react';
interface ProtectedRouteProps {
children: React.ReactNode;
requireSuperAdmin?: boolean;
}
export function ProtectedRoute({ children, requireSuperAdmin = false }: ProtectedRouteProps) {
const { session, loading, isSuperAdmin } = useAuth();
if (loading) {
return (
<div className="flex h-screen items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
);
}
if (!session) {
return <Navigate to="/login" replace />;
}
if (requireSuperAdmin && !isSuperAdmin) {
return <Navigate to="/" replace />;
}
return <>{children}</>;
}
@@ -0,0 +1,56 @@
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Sparkles, Loader2 } from 'lucide-react';
import { polishContent } from '@/lib/openai';
import { toast } from 'sonner';
interface AIPolishButtonProps {
text: string;
type: 'question' | 'partner_text' | 'pack_name' | 'pack_description' | 'category_name';
onPolished: (newText: string) => void;
className?: string;
disabled?: boolean;
explicit?: boolean;
}
export function AIPolishButton({ text, type, onPolished, className, disabled, explicit = false }: AIPolishButtonProps) {
const [loading, setLoading] = useState(false);
const handlePolish = async (e: React.MouseEvent) => {
e.preventDefault(); // Prevent form submission
if (!text || !text.trim()) {
toast.error('Enter some text first to polish');
return;
}
setLoading(true);
try {
const polished = await polishContent(text, type, explicit);
onPolished(polished);
toast.success('Text polished!');
} catch (error) {
console.error(error);
toast.error('Failed to polish text');
} finally {
setLoading(false);
}
};
return (
<Button
variant="ghost"
size="icon"
className={className}
onClick={handlePolish}
disabled={loading || disabled || !text}
type="button"
title="Polish with AI"
>
{loading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Sparkles className="h-4 w-4 text-purple-500" />
)}
</Button>
);
}
@@ -0,0 +1,281 @@
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Sparkles, Loader2, RefreshCw, Check } from 'lucide-react';
import { generatePack, generateQuestions, suggestCategories, suggestPacks, type GeneratedPack, type GeneratedQuestion, type GeneratedCategoryIdea, type GeneratedPackIdea } from '@/lib/openai';
import { Switch } from '@/components/ui/switch';
import { toast } from 'sonner';
interface AiGeneratorDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
type: 'pack' | 'questions' | 'category-ideas' | 'category-pack-ideas';
context?: {
categoryName?: string;
packName?: string;
existingCategories?: string[];
existingPacks?: string[];
};
onGenerated: (result: any) => void;
}
export function AiGeneratorDialog({
open,
onOpenChange,
type,
context,
onGenerated,
}: AiGeneratorDialogProps) {
const [loading, setLoading] = useState(false);
const [count, setCount] = useState(10);
const [isExplicit, setIsExplicit] = useState(false);
// Generated results
const [generatedPack, setGeneratedPack] = useState<GeneratedPack | null>(null);
const [generatedQuestions, setGeneratedQuestions] = useState<GeneratedQuestion[]>([]);
const [generatedIdeas, setGeneratedIdeas] = useState<GeneratedCategoryIdea[]>([]);
const [generatedPackIdeas, setGeneratedPackIdeas] = useState<GeneratedPackIdea[]>([]);
const handleGenerate = async () => {
setLoading(true);
try {
if (type === 'pack') {
const result = await generatePack(context?.categoryName, isExplicit);
setGeneratedPack(result);
} else if (type === 'questions') {
if (!context?.packName) {
toast.error('Pack name is required');
return;
}
const result = await generateQuestions(context.packName, count, undefined, isExplicit);
setGeneratedQuestions(result);
} else if (type === 'category-ideas') {
const result = await suggestCategories(context?.existingCategories || [], isExplicit);
setGeneratedIdeas(result);
} else if (type === 'category-pack-ideas') {
if (!context?.categoryName) {
toast.error('Category name is required');
return;
}
const result = await suggestPacks(context.categoryName, context?.existingPacks || [], isExplicit);
setGeneratedPackIdeas(result);
}
} catch (error) {
console.error('AI generation error:', error);
toast.error('Failed to generate content. Check your OpenAI API key.');
} finally {
setLoading(false);
}
};
const handleUse = () => {
if (type === 'pack' && generatedPack) {
onGenerated(generatedPack);
} else if (type === 'questions' && generatedQuestions.length > 0) {
onGenerated(generatedQuestions);
}
};
const handleClose = () => {
// Reset state
setGeneratedPack(null);
setGeneratedQuestions([]);
setGeneratedIdeas([]);
setGeneratedPackIdeas([]);
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-primary" />
AI {type === 'pack' ? 'Pack' : type === 'questions' ? 'Question' : type === 'category-pack-ideas' ? 'Pack Ideas' : 'Category'} Generator
</DialogTitle>
<DialogDescription>
{type === 'pack'
? 'Generate a creative question pack name and description using AI.'
: type === 'questions'
? 'Generate multiple questions for this pack using AI.'
: type === 'category-pack-ideas'
? 'Generate unique pack ideas for this category.'
: 'Get creative category ideas based on your existing ones.'}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
{/* Configuration */}
{type === 'questions' && (
<div className="space-y-2">
<Label htmlFor="count">Number of Questions</Label>
<Input
id="count"
type="number"
min={1}
max={50}
value={count}
onChange={(e) => setCount(parseInt(e.target.value) || 10)}
className="w-32"
/>
</div>
)}
{/* Explicit Toggle */}
{(type === 'category-ideas' || type === 'category-pack-ideas' || type === 'pack' || type === 'questions') && (
<div className="flex items-center space-x-2">
<Switch
id="explicit-mode"
checked={isExplicit}
onCheckedChange={setIsExplicit}
/>
<Label htmlFor="explicit-mode">Suggest Explicit Ideas</Label>
</div>
)}
{/* Context info */}
{context?.categoryName && (
<div className="rounded-md bg-muted p-3 text-sm">
<strong>Category:</strong> {context.categoryName}
</div>
)}
{context?.packName && (
<div className="rounded-md bg-muted p-3 text-sm">
<strong>Pack:</strong> {context.packName}
</div>
)}
{/* Generate button */}
{!generatedPack && generatedQuestions.length === 0 && generatedIdeas.length === 0 && generatedPackIdeas.length === 0 && (
<Button onClick={handleGenerate} disabled={loading} className="w-full">
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating...
</>
) : (
<>
<Sparkles className="mr-2 h-4 w-4" />
Generate with AI
</>
)}
</Button>
)}
{/* Generated Pack Preview */}
{generatedPack && (
<div className="space-y-3">
<h4 className="font-medium">Generated Pack</h4>
<div className="rounded-lg border bg-card p-4 space-y-2">
<p className="font-semibold text-lg">{generatedPack.name}</p>
<p className="text-muted-foreground">{generatedPack.description}</p>
</div>
</div>
)}
{/* Generated Questions Preview */}
{generatedQuestions.length > 0 && (
<div className="space-y-3">
<h4 className="font-medium">Generated Questions ({generatedQuestions.length})</h4>
<div className="space-y-2 max-h-64 overflow-y-auto">
{generatedQuestions.map((q, i) => (
<div key={i} className="rounded-lg border bg-card p-3 text-sm">
<p>{q.text}</p>
{q.partner_text && (
<p className="text-muted-foreground text-xs mt-1">
Partner: {q.partner_text}
</p>
)}
<span className="inline-block mt-1 text-xs px-2 py-0.5 rounded bg-muted">
{q.intensity} - {['Light', 'Mild', 'Moderate', 'Spicy', 'Very Intense'][q.intensity - 1]}
</span>
</div>
))}
</div>
</div>
)}
{/* Generated Ideas Preview */}
{generatedIdeas.length > 0 && (
<div className="space-y-3">
<h4 className="font-medium">Suggested Categories</h4>
<div className="grid grid-cols-1 gap-2">
{generatedIdeas.map((idea, i) => (
<div
key={i}
className="flex items-center justify-between rounded-lg border bg-card p-3 hover:bg-accent/50 cursor-pointer transition-colors"
onClick={() => onGenerated(idea)}
>
<div className="flex items-center gap-3">
<span className="text-2xl">{idea.icon}</span>
<span className="font-medium">{idea.name}</span>
</div>
<Button size="sm" variant="ghost">Use</Button>
</div>
))}
</div>
<p className="text-xs text-muted-foreground text-center">Click an item to use it</p>
</div>
)}
{/* Generated Pack Ideas Preview */}
{generatedPackIdeas.length > 0 && (
<div className="space-y-3">
<h4 className="font-medium">Suggested Packs</h4>
<div className="grid grid-cols-1 gap-2">
{generatedPackIdeas.map((idea, i) => (
<div
key={i}
className="flex items-center justify-between rounded-lg border bg-card p-3 hover:bg-accent/50 cursor-pointer transition-colors"
onClick={() => onGenerated(idea)}
>
<div className="flex items-center gap-3">
<span className="text-2xl">{idea.icon}</span>
<div>
<span className="font-medium block">{idea.name}</span>
<span className="text-xs text-muted-foreground">{idea.description}</span>
</div>
</div>
<Button size="sm" variant="ghost">Use</Button>
</div>
))}
</div>
<p className="text-xs text-muted-foreground text-center">Click an item to use it</p>
</div>
)}
</div>
<DialogFooter>
{(generatedPack || generatedQuestions.length > 0 || generatedIdeas.length > 0 || generatedPackIdeas.length > 0) && (
<>
<Button variant="outline" onClick={handleGenerate} disabled={loading}>
<RefreshCw className={`mr-2 h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
Regenerate
</Button>
{type !== 'category-ideas' && type !== 'category-pack-ideas' && (
<Button onClick={handleUse}>
<Check className="mr-2 h-4 w-4" />
Use This
</Button>
)}
</>
)}
{!generatedPack && generatedQuestions.length === 0 && generatedIdeas.length === 0 && generatedPackIdeas.length === 0 && (
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,242 @@
import { useState } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Loader2, ShieldCheck, ArrowRight, Check } from 'lucide-react';
import { analyzeQuestionTargets, TargetAnalysis } from '@/lib/openai';
import { supabase } from '@/config';
import { toast } from 'sonner';
// Define local compatible interface
interface Question {
id: string;
text: string;
partner_text: string | null;
intensity: number;
allowed_couple_genders: string[] | null;
}
interface FixTargetsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
questions: Question[];
onUpdated: () => void;
}
export function FixTargetsDialog({ open, onOpenChange, questions, onUpdated }: FixTargetsDialogProps) {
const [analyzing, setAnalyzing] = useState(false);
const [suggestions, setSuggestions] = useState<TargetAnalysis[]>([]);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [step, setStep] = useState<'initial' | 'review'>('initial');
const [applying, setApplying] = useState(false);
const handleAnalyze = async () => {
setAnalyzing(true);
try {
const results = await analyzeQuestionTargets(questions);
setSuggestions(results);
// Default select all
setSelectedIds(new Set(results.map(r => r.id)));
setStep('review');
} catch (error) {
console.error(error);
toast.error('Failed to analyze questions');
} finally {
setAnalyzing(false);
}
};
const handleApply = async () => {
setApplying(true);
try {
const updates = suggestions.filter(s => selectedIds.has(s.id));
if (updates.length === 0) {
onOpenChange(false);
return;
}
// Apply updates in parallel
await Promise.all(updates.map(update =>
supabase
.from('questions')
.update({ allowed_couple_genders: update.suggested_targets && update.suggested_targets.length > 0 ? update.suggested_targets : null })
.eq('id', update.id)
));
toast.success(`Updated ${updates.length} questions`);
onUpdated();
handleClose();
} catch (error) {
console.error(error);
toast.error('Failed to apply updates');
} finally {
setApplying(false);
}
};
const handleClose = () => {
onOpenChange(false);
// Reset state after transition
setTimeout(() => {
setStep('initial');
setSuggestions([]);
}, 300);
};
const toggleSelection = (id: string) => {
const next = new Set(selectedIds);
if (next.has(id)) next.delete(id);
else next.add(id);
setSelectedIds(next);
};
const formatTargets = (targets: string[] | null) => {
if (!targets || targets.length === 0) return 'All';
return targets.map(t => t.replace('male+male', 'M+M').replace('female+male', 'M+F').replace('female+female', 'F+F')).join(', ');
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-4xl max-h-[90vh] flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<ShieldCheck className="h-5 w-5 text-indigo-500" />
Fix Question Targets
</DialogTitle>
<DialogDescription>
Use AI to detect anatomical or configuration constraints in your questions.
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-y-auto py-4">
{step === 'initial' ? (
<div className="flex flex-col items-center justify-center py-12 text-center space-y-4">
<div className="bg-indigo-50 p-4 rounded-full">
<ShieldCheck className="h-12 w-12 text-indigo-500" />
</div>
<div className="space-y-2">
<h3 className="text-lg font-medium">Ready to analyze {questions.length} questions</h3>
<p className="text-muted-foreground max-w-md mx-auto">
The AI will look for anatomical requirements (e.g., acts requiring specific genitalia) and suggest target restrictions.
</p>
</div>
<Button onClick={handleAnalyze} disabled={analyzing} size="lg">
{analyzing ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Analyzing...
</>
) : (
'Start Analysis'
)}
</Button>
</div>
) : (
<div className="space-y-4">
{suggestions.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<Check className="h-8 w-8 mx-auto mb-2 text-green-500" />
No issues found! All questions seem to have appropriate targets.
</div>
) : (
<div className="border rounded-md">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">
<input
type="checkbox"
className="h-4 w-4 rounded border-gray-300 accent-primary"
checked={selectedIds.size === suggestions.length && suggestions.length > 0}
onChange={(e) => {
if (e.target.checked) setSelectedIds(new Set(suggestions.map(s => s.id)));
else setSelectedIds(new Set());
}}
/>
</TableHead>
<TableHead>Question</TableHead>
<TableHead>Current</TableHead>
<TableHead>Suggested</TableHead>
<TableHead>Reason</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{suggestions.map((suggestion) => {
const question = questions.find(q => q.id === suggestion.id);
if (!question) return null;
return (
<TableRow key={suggestion.id}>
<TableCell>
<input
type="checkbox"
className="h-4 w-4 rounded border-gray-300 accent-primary"
checked={selectedIds.has(suggestion.id)}
onChange={() => toggleSelection(suggestion.id)}
/>
</TableCell>
<TableCell className="max-w-xs">
<div className="line-clamp-2 text-sm font-medium">{question.text}</div>
{question.partner_text && (
<div className="text-xs text-muted-foreground mt-1 line-clamp-1">
Partner: {question.partner_text}
</div>
)}
</TableCell>
<TableCell className="text-xs whitespace-nowrap">
<Badge variant="outline" className="text-muted-foreground">
{formatTargets(question.allowed_couple_genders)}
</Badge>
</TableCell>
<TableCell className="text-xs whitespace-nowrap">
<div className="flex items-center gap-1">
<ArrowRight className="h-3 w-3 text-muted-foreground" />
<Badge variant="secondary" className="bg-indigo-100 text-indigo-700 hover:bg-indigo-100">
{formatTargets(suggestion.suggested_targets)}
</Badge>
</div>
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{suggestion.reason}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
)}
</div>
)}
</div>
<DialogFooter>
{step === 'review' && (
<>
<Button variant="outline" onClick={() => setStep('initial')}>
Back
</Button>
<Button onClick={handleApply} disabled={applying || suggestions.length === 0 || selectedIds.size === 0}>
{applying ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Applying...
</>
) : (
`Apply ${selectedIds.size} Fixes`
)}
</Button>
</>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,24 @@
import { Outlet } from 'react-router-dom';
import { Sidebar } from './Sidebar';
import { Breadcrumbs } from './Breadcrumbs';
import { Toaster } from 'sonner';
export function AppLayout() {
return (
<div className="flex h-screen overflow-hidden bg-background">
<Sidebar />
<div className="flex flex-1 flex-col overflow-hidden">
{/* Header */}
<header className="flex h-16 items-center border-b px-6">
<Breadcrumbs />
</header>
{/* Main content */}
<main className="flex-1 overflow-auto p-6">
<Outlet />
</main>
</div>
<Toaster position="top-right" richColors />
</div>
);
}
@@ -0,0 +1,81 @@
import { Link, useLocation, useParams } from 'react-router-dom';
import { ChevronRight, Home } from 'lucide-react';
interface BreadcrumbItem {
label: string;
href?: string;
}
// Generate breadcrumbs based on current path and params
function useBreadcrumbs(): BreadcrumbItem[] {
const location = useLocation();
const params = useParams();
const pathSegments = location.pathname.split('/').filter(Boolean);
const breadcrumbs: BreadcrumbItem[] = [];
let currentPath = '';
for (const segment of pathSegments) {
currentPath += `/${segment}`;
// Map segments to readable labels
if (segment === 'categories') {
breadcrumbs.push({ label: 'Categories', href: '/categories' });
} else if (segment === 'packs' && params.categoryId) {
breadcrumbs.push({ label: 'Packs', href: currentPath });
} else if (segment === 'questions' && params.packId) {
breadcrumbs.push({ label: 'Questions', href: currentPath });
} else if (segment === 'users') {
breadcrumbs.push({ label: 'Users', href: '/users' });
} else if (segment === 'matches' && params.userId) {
breadcrumbs.push({ label: 'Match Chat' });
} else if (segment === 'audit-logs') {
breadcrumbs.push({ label: 'Audit Logs', href: '/audit-logs' });
} else if (segment === 'new') {
breadcrumbs.push({ label: 'Create New' });
} else if (segment === 'edit') {
breadcrumbs.push({ label: 'Edit' });
}
// Skip UUID segments as they're handled with their parent
}
// Remove href from last item (current page)
if (breadcrumbs.length > 0) {
breadcrumbs[breadcrumbs.length - 1].href = undefined;
}
return breadcrumbs;
}
export function Breadcrumbs() {
const breadcrumbs = useBreadcrumbs();
if (breadcrumbs.length === 0) return null;
return (
<nav className="flex items-center space-x-1 text-sm text-muted-foreground">
<Link
to="/"
className="flex items-center hover:text-foreground transition-colors"
>
<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" />
{item.href ? (
<Link
to={item.href}
className="hover:text-foreground transition-colors"
>
{item.label}
</Link>
) : (
<span className="text-foreground font-medium">{item.label}</span>
)}
</div>
))}
</nav>
);
}
@@ -0,0 +1,170 @@
import { Link, useLocation } from 'react-router-dom';
import { cn } from '@/lib/utils';
import { useAuth } from '@/contexts/AuthContext';
import {
LayoutGrid,
Users,
ClipboardList,
ChevronLeft,
ChevronRight,
LogOut,
Sparkles,
Shield,
} 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';
interface NavItem {
label: string;
href: string;
icon: React.ReactNode;
requireSuperAdmin?: boolean;
}
const contentNav: NavItem[] = [
{
label: 'Categories',
href: '/categories',
icon: <LayoutGrid className="h-5 w-5" />,
},
];
const usersNav: NavItem[] = [
{
label: 'Users',
href: '/users',
icon: <Users className="h-5 w-5" />,
requireSuperAdmin: true,
},
];
const systemNav: NavItem[] = [
{
label: 'Admins',
href: '/admins',
icon: <Shield className="h-5 w-5" />,
requireSuperAdmin: true,
},
{
label: 'Audit Logs',
href: '/audit-logs',
icon: <ClipboardList className="h-5 w-5" />,
requireSuperAdmin: true,
},
];
export function Sidebar() {
const location = useLocation();
const { user, permissions, logout, isSuperAdmin } = useAuth();
const [collapsed, setCollapsed] = useState(false);
const NavLink = ({ item }: { item: NavItem }) => {
if (item.requireSuperAdmin && !isSuperAdmin) return null;
const isActive = location.pathname.startsWith(item.href);
return (
<Link
to={item.href}
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors",
isActive
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
collapsed && "justify-center px-2"
)}
>
{item.icon}
{!collapsed && <span>{item.label}</span>}
</Link>
);
};
const NavSection = ({ title, items }: { title: string; items: NavItem[] }) => {
const visibleItems = items.filter(item => !item.requireSuperAdmin || isSuperAdmin);
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">
{title}
</h4>
)}
{visibleItems.map(item => (
<NavLink key={item.href} item={item} />
))}
</div>
);
};
return (
<div
className={cn(
"flex h-screen flex-col border-r bg-card transition-all duration-300",
collapsed ? "w-16" : "w-64"
)}
>
{/* Logo */}
<div className="flex h-16 items-center justify-between border-b px-4">
{!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>
)}
<Button
variant="ghost"
size="icon"
onClick={() => setCollapsed(!collapsed)}
className={cn(collapsed && "mx-auto")}
>
{collapsed ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronLeft className="h-4 w-4" />
)}
</Button>
</div>
{/* Navigation */}
<div className="flex-1 space-y-6 overflow-auto p-4">
<NavSection title="Content" items={contentNav} />
<NavSection title="Users" items={usersNav} />
<NavSection title="System" items={systemNav} />
</div>
<Separator />
{/* 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>
{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="text-xs text-muted-foreground capitalize">
{permissions?.role?.replace('_', ' ')}
</p>
</div>
)}
</div>
<Button
variant="ghost"
size={collapsed ? "icon" : "sm"}
className={cn("mt-3", collapsed ? "w-8 h-8" : "w-full")}
onClick={logout}
>
<LogOut className="h-4 w-4" />
{!collapsed && <span className="ml-2">Logout</span>}
</Button>
</div>
</div>
);
}
+47
View File
@@ -0,0 +1,47 @@
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils";
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback };
+39
View File
@@ -0,0 +1,39 @@
import * as React from "react";
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",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
success:
"border-transparent bg-green-500 text-white hover:bg-green-500/80",
warning:
"border-transparent bg-yellow-500 text-white hover:bg-yellow-500/80",
},
},
defaultVariants: {
variant: "default",
},
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> { }
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };
+55
View File
@@ -0,0 +1,55 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
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",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-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",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };
+78
View File
@@ -0,0 +1,78 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
+119
View File
@@ -0,0 +1,119 @@
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
+24
View File
@@ -0,0 +1,24 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> { }
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<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",
className
)}
ref={ref}
{...props}
/>
);
}
);
Input.displayName = "Input";
export { Input };
+23
View File
@@ -0,0 +1,23 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };
+160
View File
@@ -0,0 +1,160 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
@@ -0,0 +1,28 @@
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };
+15
View File
@@ -0,0 +1,15 @@
import { cn } from "@/lib/utils";
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
);
}
export { Skeleton };
+26
View File
@@ -0,0 +1,26 @@
import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cn } from "@/lib/utils";
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };
+116
View File
@@ -0,0 +1,116 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
));
Table.displayName = "Table";
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
));
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
));
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
));
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
));
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
));
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
));
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
));
TableCaption.displayName = "TableCaption";
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};
+52
View File
@@ -0,0 +1,52 @@
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };
+23
View File
@@ -0,0 +1,23 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { }
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background 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",
className
)}
ref={ref}
{...props}
/>
);
}
);
Textarea.displayName = "Textarea";
export { Textarea };