diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000000000000000000000000000000000000..b9d355df2a5956b526c004531b7b0ffe412461e0 --- /dev/null +++ b/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000000000000000000000000000000000000..18daf2e9004ae5e366e0127f288b7ca9d3a71db5 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,27 @@ +import { Toaster } from "@/components/ui/toaster"; +import { Toaster as Sonner } from "@/components/ui/sonner"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import Index from "./pages/Index"; +import NotFound from "./pages/NotFound"; + +const queryClient = new QueryClient(); + +const App = () => ( + + + + + + + } /> + {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} + } /> + + + + +); + +export default App; diff --git a/src/components/ActionButton.tsx b/src/components/ActionButton.tsx new file mode 100644 index 0000000000000000000000000000000000000000..32045718afb46fa95c13a4b55bc9be006056dffe --- /dev/null +++ b/src/components/ActionButton.tsx @@ -0,0 +1,40 @@ + +import React from 'react'; + +interface ActionButtonProps { + label: string; + onClick: () => void; + color: string; + icon: string; + disabled?: boolean; +} + +const ActionButton: React.FC = ({ label, onClick, color, icon, disabled = false }) => { + return ( + + {icon} + {label} + {!disabled && ( + + + + )} + + ); +}; + +export default ActionButton; diff --git a/src/components/ActivityLog.tsx b/src/components/ActivityLog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8d0c7d1d0ea1ff0a8bbf5c6ec213af3698743acc --- /dev/null +++ b/src/components/ActivityLog.tsx @@ -0,0 +1,70 @@ + +import React from 'react'; + +interface LogEntry { + id: string; + message: string; + type: 'success' | 'warning' | 'error' | 'info'; + timestamp: Date; +} + +interface ActivityLogProps { + entries: LogEntry[]; +} + +const ActivityLog: React.FC = ({ entries }) => { + const getLogColor = (type: LogEntry['type']) => { + switch (type) { + case 'success': return 'bg-green-100 text-green-800 border-green-200'; + case 'warning': return 'bg-yellow-100 text-yellow-800 border-yellow-200'; + case 'error': return 'bg-red-100 text-red-800 border-red-200'; + case 'info': return 'bg-blue-100 text-blue-800 border-blue-200'; + default: return 'bg-gray-100 text-gray-800 border-gray-200'; + } + }; + + const getLogIcon = (type: LogEntry['type']) => { + switch (type) { + case 'success': return '✅'; + case 'warning': return '⚠️'; + case 'error': return '❌'; + case 'info': return 'ℹ️'; + default: return '📝'; + } + }; + + return ( + + + 📋 Recent Activity + + + + {entries.length === 0 ? ( + + No recent activity... Your pet is waiting! 🐾 + + ) : ( + entries.map((entry) => ( + + + {getLogIcon(entry.type)} + + {entry.message} + + {entry.timestamp.toLocaleTimeString()} + + + + + )) + )} + + + ); +}; + +export default ActivityLog; diff --git a/src/components/CompactPetAvatar.tsx b/src/components/CompactPetAvatar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e7a910d24bb6f5963bba1323aff68b938b542075 --- /dev/null +++ b/src/components/CompactPetAvatar.tsx @@ -0,0 +1,92 @@ + +import React, { useState, useEffect } from 'react'; + +interface CompactPetAvatarProps { + health: number; + energy: number; + attention: number; +} + +const CompactPetAvatar: React.FC = ({ health, energy, attention }) => { + const [isBlinking, setIsBlinking] = useState(false); + const [mood, setMood] = useState<'happy' | 'normal' | 'tired' | 'sick'>('normal'); + + useEffect(() => { + const blinkInterval = setInterval(() => { + setIsBlinking(true); + setTimeout(() => setIsBlinking(false), 150); + }, 3000 + Math.random() * 2000); + + return () => clearInterval(blinkInterval); + }, []); + + useEffect(() => { + const avgStats = (health + energy + attention) / 3; + if (avgStats >= 80) setMood('happy'); + else if (avgStats >= 60) setMood('normal'); + else if (avgStats >= 30) setMood('tired'); + else setMood('sick'); + }, [health, energy, attention]); + + const getMoodColor = () => { + switch (mood) { + case 'happy': return '#A7F3D0'; + case 'normal': return '#BFDBFE'; + case 'tired': return '#FDE68A'; + case 'sick': return '#FECACA'; + default: return '#BFDBFE'; + } + }; + + return ( + + + {/* Pet body with gentle bounce animation */} + + {/* Eyes */} + + + {!isBlinking && } + + + {!isBlinking && } + + + + {/* Mouth based on mood */} + + {mood === 'happy' && ( + + )} + {mood === 'normal' && ( + + )} + {(mood === 'tired' || mood === 'sick') && ( + + )} + + + {/* Cheek blush for happy mood */} + {mood === 'happy' && ( + <> + + + > + )} + + + {/* Floating particles for happy mood */} + {mood === 'happy' && ( + + + + )} + + + ); +}; + +export default CompactPetAvatar; diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fa9cef1787650cabb506729895904c28b9fd6066 --- /dev/null +++ b/src/components/Dashboard.tsx @@ -0,0 +1,154 @@ + +import React from 'react'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts'; + +interface DashboardProps { + health: number; + energy: number; + attention: number; +} + +const Dashboard: React.FC = ({ health, energy, attention }) => { + // Generate sample data for the last 15 time periods + const generateChartData = () => { + const data = []; + for (let i = 0; i < 15; i++) { + data.push({ + time: i + 1, + health: Math.max(0, health + (Math.random() - 0.5) * 40), + energy: Math.max(0, energy + (Math.random() - 0.5) * 30), + attention: Math.max(0, attention + (Math.random() - 0.5) * 35), + dataQuality: Math.random() * 100, + processedSamples: Math.floor(Math.random() * 1000) + 500, + }); + } + return data; + }; + + const chartData = generateChartData(); + + const metrics = [ + { label: 'Dataset Quality', value: '94.2%', color: '#10B981', trend: '+2.1%' }, + { label: 'Processed Samples', value: '12.5K', color: '#F59E0B', trend: '+8.3%' }, + { label: 'Model Accuracy', value: '89.7%', color: '#3B82F6', trend: '+1.4%' }, + { label: 'Clean Records', value: '98.1%', color: '#8B5CF6', trend: '+0.9%' }, + ]; + + return ( + + + 📊 Pet Performance Dashboard + + + + {/* Pet Stats Chart */} + + Pet Vital Stats Over Time + + + + + + + + + + + + + + {/* Data Processing Chart */} + + Data Processing Activity + + + + + + + + + + + + + {/* Metrics Grid */} + + {metrics.map((metric, index) => ( + + + {metric.value} + + {metric.label} + + {metric.trend} + + + ))} + + + ); +}; + +export default Dashboard; diff --git a/src/components/DataVisualization.tsx b/src/components/DataVisualization.tsx new file mode 100644 index 0000000000000000000000000000000000000000..68bc2b2e73d65dfb45991efbf666ebe7a5f1d7fa --- /dev/null +++ b/src/components/DataVisualization.tsx @@ -0,0 +1,178 @@ + +import React from 'react'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; + +const DataVisualization: React.FC = () => { + // Generate sample data similar to your reference image + const generateTimeSeriesData = () => { + const data = []; + for (let i = 0; i <= 15; i++) { + data.push({ + time: i, + shoulder_pan_pos: 100 - i * 8 + Math.sin(i * 0.5) * 20, + shoulder_lift_pos: -30 + Math.cos(i * 0.3) * 15, + elbow_flex_pos: 50 - i * 2 + Math.sin(i * 0.8) * 10, + wrist_flex_pos: 80 - i * 3 + Math.cos(i * 0.6) * 25, + wrist_roll_pos: 10 + Math.sin(i * 0.4) * 8, + gripper_pos: -5 + Math.cos(i * 0.2) * 12, + }); + } + return data; + }; + + const leftChartData = generateTimeSeriesData(); + const rightChartData = generateTimeSeriesData().map(item => ({ + ...item, + wrist_flex_pos: item.wrist_flex_pos + 20, + wrist_roll_pos: item.wrist_roll_pos - 10, + gripper_pos: item.gripper_pos + 15, + })); + + const metricsList = [ + { name: 'shoulder_pan_pos', color: '#ef4444', value: '-15.33' }, + { name: 'shoulder_lift_pos', color: '#22c55e', value: '-18.39' }, + { name: 'elbow_flex_pos', color: '#3b82f6', value: '34.99' }, + ]; + + const rightMetricsList = [ + { name: 'wrist_flex_pos', color: '#ef4444', value: '68.87' }, + { name: 'wrist_roll_pos', color: '#22c55e', value: '-4.01' }, + { name: 'gripper_pos', color: '#3b82f6', value: '19.82' }, + ]; + + return ( + + {/* Header with observation images */} + + + + observation_images_front + + + × + + + + 📦 Dataset Sample View + + + + + + observation_images_side + + + × + + + + 🎯 Model Predictions + + + + + {/* Time series charts */} + + {/* Left Chart */} + + + + + + + + + + + + + + {/* Metrics table */} + + {metricsList.map((metric, index) => ( + + + + {metric.name} + + + action + {metric.value} + + + ))} + + + + {/* Right Chart */} + + + + + + + + + + + + + + {/* Metrics table */} + + {rightMetricsList.map((metric, index) => ( + + + + {metric.name} + + + action + {metric.value} + + + ))} + + + + + ); +}; + +export default DataVisualization; diff --git a/src/components/ModeToggle.tsx b/src/components/ModeToggle.tsx new file mode 100644 index 0000000000000000000000000000000000000000..541fbec93cb9deec953ef5e1af1c292ed0184d4c --- /dev/null +++ b/src/components/ModeToggle.tsx @@ -0,0 +1,44 @@ + +import React from 'react'; +import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; + +interface ModeToggleProps { + currentMode: 'pet' | 'data' | 'hybrid'; + onModeChange: (mode: 'pet' | 'data' | 'hybrid') => void; +} + +const ModeToggle: React.FC = ({ currentMode, onModeChange }) => { + return ( + + + value && onModeChange(value as 'pet' | 'data' | 'hybrid')} + className="gap-1" + > + + 🐾 Pet Mode + + + 🔄 Hybrid View + + + 📊 Data Mode + + + + + ); +}; + +export default ModeToggle; diff --git a/src/components/PetAvatar.tsx b/src/components/PetAvatar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ae7f6639456364434838fc88ecb85e0bcde24546 --- /dev/null +++ b/src/components/PetAvatar.tsx @@ -0,0 +1,92 @@ + +import React, { useState, useEffect } from 'react'; + +interface PetAvatarProps { + health: number; + energy: number; + attention: number; +} + +const PetAvatar: React.FC = ({ health, energy, attention }) => { + const [isBlinking, setIsBlinking] = useState(false); + const [mood, setMood] = useState<'happy' | 'normal' | 'tired' | 'sick'>('normal'); + + useEffect(() => { + const blinkInterval = setInterval(() => { + setIsBlinking(true); + setTimeout(() => setIsBlinking(false), 150); + }, 3000 + Math.random() * 2000); + + return () => clearInterval(blinkInterval); + }, []); + + useEffect(() => { + const avgStats = (health + energy + attention) / 3; + if (avgStats >= 80) setMood('happy'); + else if (avgStats >= 60) setMood('normal'); + else if (avgStats >= 30) setMood('tired'); + else setMood('sick'); + }, [health, energy, attention]); + + const getMoodColor = () => { + switch (mood) { + case 'happy': return '#A7F3D0'; + case 'normal': return '#BFDBFE'; + case 'tired': return '#FDE68A'; + case 'sick': return '#FECACA'; + default: return '#BFDBFE'; + } + }; + + return ( + + + {/* Pet body with gentle bounce animation */} + + {/* Eyes */} + + + {!isBlinking && } + + + {!isBlinking && } + + + + {/* Mouth based on mood */} + + {mood === 'happy' && ( + + )} + {mood === 'normal' && ( + + )} + {(mood === 'tired' || mood === 'sick') && ( + + )} + + + {/* Cheek blush for happy mood */} + {mood === 'happy' && ( + <> + + + > + )} + + + {/* Floating particles for happy mood */} + {mood === 'happy' && ( + + + + )} + + + ); +}; + +export default PetAvatar; diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7f555cc5330b53881e9249bb3101f17a5aea73cd --- /dev/null +++ b/src/components/StatusBar.tsx @@ -0,0 +1,55 @@ + +import React from 'react'; + +interface StatusBarProps { + label: string; + value: number; + maxValue: number; + color: string; + icon: string; +} + +const StatusBar: React.FC = ({ label, value, maxValue, color, icon }) => { + const percentage = (value / maxValue) * 100; + + const getBarColor = () => { + if (percentage >= 70) return color; + if (percentage >= 40) return '#FDE68A'; + return '#FECACA'; + }; + + return ( + + + + {icon} + {label.replace(/^[^\s]+\s/, '')} + + {value}/{maxValue} + + + + + {percentage > 10 && ( + + )} + + + + {percentage < 30 && ( + + ⚠️ Low {label.split(' ')[1] || label} + + )} + + ); +}; + +export default StatusBar; diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e6a723d06574ee5cec8b00759b98f3fbe1ac7cc9 --- /dev/null +++ b/src/components/ui/accordion.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + +)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8722561cf6bda62d62f9a0c67730aefda971873a --- /dev/null +++ b/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( + +) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( + +) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 0000000000000000000000000000000000000000..41fa7e0561a3fdb5f986c1213a35e563de740e96 --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( + +)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/src/components/ui/aspect-ratio.tsx b/src/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c4abbf37f217c715a0eaade7f45ac78600df419f --- /dev/null +++ b/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,5 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +const AspectRatio = AspectRatioPrimitive.Root + +export { AspectRatio } diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..991f56ecb117e96284bf0f6cad3b14ea2fdf5264 --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f000e3ef5176395b067dfc3f3e1256a80c450015 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +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", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000000000000000000000000000000000000..71a5c325cdce2e6898d11cfeb4f2fdd458e3e2da --- /dev/null +++ b/src/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) => ) +Breadcrumb.displayName = "Breadcrumb" + +const BreadcrumbList = React.forwardRef< + HTMLOListElement, + React.ComponentPropsWithoutRef<"ol"> +>(({ className, ...props }, ref) => ( + +)) +BreadcrumbList.displayName = "BreadcrumbList" + +const BreadcrumbItem = React.forwardRef< + HTMLLIElement, + React.ComponentPropsWithoutRef<"li"> +>(({ className, ...props }, ref) => ( + +)) +BreadcrumbItem.displayName = "BreadcrumbItem" + +const BreadcrumbLink = React.forwardRef< + HTMLAnchorElement, + React.ComponentPropsWithoutRef<"a"> & { + asChild?: boolean + } +>(({ asChild, className, ...props }, ref) => { + const Comp = asChild ? Slot : "a" + + return ( + + ) +}) +BreadcrumbLink.displayName = "BreadcrumbLink" + +const BreadcrumbPage = React.forwardRef< + HTMLSpanElement, + React.ComponentPropsWithoutRef<"span"> +>(({ className, ...props }, ref) => ( + +)) +BreadcrumbPage.displayName = "BreadcrumbPage" + +const BreadcrumbSeparator = ({ + children, + className, + ...props +}: React.ComponentProps<"li">) => ( + svg]:size-3.5", className)} + {...props} + > + {children ?? } + +) +BreadcrumbSeparator.displayName = "BreadcrumbSeparator" + +const BreadcrumbEllipsis = ({ + className, + ...props +}: React.ComponentProps<"span">) => ( + + + More + +) +BreadcrumbEllipsis.displayName = "BreadcrumbElipssis" + +export { + Breadcrumb, + BreadcrumbList, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbPage, + BreadcrumbSeparator, + BreadcrumbEllipsis, +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000000000000000000000000000000000000..36496a28727a3643b4212a14225d4f6cbd50bda5 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +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 gap-2 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + 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, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3160ad0f396d96ec2f499e3e76aae44656f58497 --- /dev/null +++ b/src/components/ui/calendar.tsx @@ -0,0 +1,64 @@ +import * as React from "react"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { DayPicker } from "react-day-picker"; + +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; + +export type CalendarProps = React.ComponentProps; + +function Calendar({ + className, + classNames, + showOutsideDays = true, + ...props +}: CalendarProps) { + return ( + , + IconRight: ({ ..._props }) => , + }} + {...props} + /> + ); +} +Calendar.displayName = "Calendar"; + +export { Calendar }; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000000000000000000000000000000000000..afa13ecfa3bd0f4a553a510b856c5800382e139b --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9c2b9bf3705d8421bef00704c0c52e83d371ca11 --- /dev/null +++ b/src/components/ui/carousel.tsx @@ -0,0 +1,260 @@ +import * as React from "react" +import useEmblaCarousel, { + type UseEmblaCarouselType, +} from "embla-carousel-react" +import { ArrowLeft, ArrowRight } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +type CarouselApi = UseEmblaCarouselType[1] +type UseCarouselParameters = Parameters +type CarouselOptions = UseCarouselParameters[0] +type CarouselPlugin = UseCarouselParameters[1] + +type CarouselProps = { + opts?: CarouselOptions + plugins?: CarouselPlugin + orientation?: "horizontal" | "vertical" + setApi?: (api: CarouselApi) => void +} + +type CarouselContextProps = { + carouselRef: ReturnType[0] + api: ReturnType[1] + scrollPrev: () => void + scrollNext: () => void + canScrollPrev: boolean + canScrollNext: boolean +} & CarouselProps + +const CarouselContext = React.createContext(null) + +function useCarousel() { + const context = React.useContext(CarouselContext) + + if (!context) { + throw new Error("useCarousel must be used within a ") + } + + return context +} + +const Carousel = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & CarouselProps +>( + ( + { + orientation = "horizontal", + opts, + setApi, + plugins, + className, + children, + ...props + }, + ref + ) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === "horizontal" ? "x" : "y", + }, + plugins + ) + const [canScrollPrev, setCanScrollPrev] = React.useState(false) + const [canScrollNext, setCanScrollNext] = React.useState(false) + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return + } + + setCanScrollPrev(api.canScrollPrev()) + setCanScrollNext(api.canScrollNext()) + }, []) + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev() + }, [api]) + + const scrollNext = React.useCallback(() => { + api?.scrollNext() + }, [api]) + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "ArrowLeft") { + event.preventDefault() + scrollPrev() + } else if (event.key === "ArrowRight") { + event.preventDefault() + scrollNext() + } + }, + [scrollPrev, scrollNext] + ) + + React.useEffect(() => { + if (!api || !setApi) { + return + } + + setApi(api) + }, [api, setApi]) + + React.useEffect(() => { + if (!api) { + return + } + + onSelect(api) + api.on("reInit", onSelect) + api.on("select", onSelect) + + return () => { + api?.off("select", onSelect) + } + }, [api, onSelect]) + + return ( + + + {children} + + + ) + } +) +Carousel.displayName = "Carousel" + +const CarouselContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel() + + return ( + + + + ) +}) +CarouselContent.displayName = "CarouselContent" + +const CarouselItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { orientation } = useCarousel() + + return ( + + ) +}) +CarouselItem.displayName = "CarouselItem" + +const CarouselPrevious = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel() + + return ( + + + Previous slide + + ) +}) +CarouselPrevious.displayName = "CarouselPrevious" + +const CarouselNext = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel() + + return ( + + + Next slide + + ) +}) +CarouselNext.displayName = "CarouselNext" + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, +} diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a21d77ee708c3d861fb246ecab7f6dc36e0e605b --- /dev/null +++ b/src/components/ui/chart.tsx @@ -0,0 +1,363 @@ +import * as React from "react" +import * as RechartsPrimitive from "recharts" + +import { cn } from "@/lib/utils" + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode + icon?: React.ComponentType + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +} + +type ChartContextProps = { + config: ChartConfig +} + +const ChartContext = React.createContext(null) + +function useChart() { + const context = React.useContext(ChartContext) + + if (!context) { + throw new Error("useChart must be used within a ") + } + + return context +} + +const ChartContainer = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + config: ChartConfig + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"] + } +>(({ id, className, children, config, ...props }, ref) => { + const uniqueId = React.useId() + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` + + return ( + + + + + {children} + + + + ) +}) +ChartContainer.displayName = "Chart" + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([_, config]) => config.theme || config.color + ) + + if (!colorConfig.length) { + return null + } + + return ( +
{entry.message}
+ {entry.timestamp.toLocaleTimeString()} +