From f43fcc391eeb412190f7fc1a2925ed5ec4d7d0b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Taupin?= Date: Wed, 30 Jul 2025 14:47:50 +0200 Subject: [PATCH] V 1.0.0 --- .dockerignore | 62 +++ .gitignore | 33 ++ eslint.config.js | 29 ++ index.html | 21 + nginx.conf | 31 ++ package.json | 84 ++++ postcss.config.js | 6 + public/robots.txt | 2 + src/App.css | 0 src/App.tsx | 38 ++ src/components/layout/Navbar.tsx | 75 ++++ src/components/ui/accordion.tsx | 50 +++ src/components/ui/alert-dialog.tsx | 96 +++++ src/components/ui/alert.tsx | 36 ++ src/components/ui/aspect-ratio.tsx | 5 + src/components/ui/avatar.tsx | 33 ++ src/components/ui/badge.tsx | 29 ++ src/components/ui/breadcrumb.tsx | 68 +++ src/components/ui/button.tsx | 43 ++ src/components/ui/calendar.tsx | 51 +++ src/components/ui/card.tsx | 35 ++ src/components/ui/carousel.tsx | 212 +++++++++ src/components/ui/chart.tsx | 281 ++++++++++++ src/components/ui/checkbox.tsx | 26 ++ src/components/ui/collapsible.tsx | 9 + src/components/ui/command.tsx | 110 +++++ src/components/ui/context-menu.tsx | 178 ++++++++ src/components/ui/dialog.tsx | 91 ++++ src/components/ui/drawer.tsx | 80 ++++ src/components/ui/dropdown-menu.tsx | 175 ++++++++ src/components/ui/form.tsx | 122 ++++++ src/components/ui/hover-card.tsx | 27 ++ src/components/ui/input-otp.tsx | 58 +++ src/components/ui/input.tsx | 20 + src/components/ui/label.tsx | 15 + src/components/ui/menubar.tsx | 203 +++++++++ src/components/ui/navigation-menu.tsx | 116 +++++ src/components/ui/pagination.tsx | 66 +++ src/components/ui/popover.tsx | 29 ++ src/components/ui/progress.tsx | 19 + src/components/ui/radio-group.tsx | 36 ++ src/components/ui/resizable.tsx | 37 ++ src/components/ui/scroll-area.tsx | 38 ++ src/components/ui/select.tsx | 134 ++++++ src/components/ui/separator.tsx | 20 + src/components/ui/sheet.tsx | 93 ++++ src/components/ui/sidebar.tsx | 598 ++++++++++++++++++++++++++ src/components/ui/skeleton.tsx | 7 + src/components/ui/slider.tsx | 18 + src/components/ui/sonner.tsx | 27 ++ src/components/ui/switch.tsx | 27 ++ src/components/ui/table.tsx | 53 +++ src/components/ui/tabs.tsx | 49 +++ src/components/ui/textarea.tsx | 21 + src/components/ui/toast.tsx | 107 +++++ src/components/ui/toaster.tsx | 24 ++ src/components/ui/toggle-group.tsx | 49 +++ src/components/ui/toggle.tsx | 37 ++ src/components/ui/tooltip.tsx | 28 ++ src/components/ui/use-toast.ts | 3 + src/hooks/use-mobile.tsx | 19 + src/hooks/use-toast.ts | 186 ++++++++ src/index.css | 101 +++++ src/lib/store.ts | 186 ++++++++ src/lib/types.ts | 39 ++ src/lib/utils.ts | 6 + src/main.tsx | 5 + src/pages/About.tsx | 108 +++++ src/pages/Admin.tsx | 210 +++++++++ src/pages/Blog.tsx | 100 +++++ src/pages/Index.tsx | 61 +++ src/pages/Login.tsx | 115 +++++ src/pages/NotFound.tsx | 24 ++ src/pages/PostDetail.tsx | 204 +++++++++ src/pages/Profile.tsx | 167 +++++++ src/pages/Register.tsx | 122 ++++++ src/vite-env.d.ts | 1 + tailwind.config.ts | 93 ++++ tsconfig.app.json | 30 ++ tsconfig.json | 19 + tsconfig.node.json | 22 + vite.config.ts | 15 + 82 files changed, 5903 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 nginx.conf create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 public/robots.txt create mode 100644 src/App.css create mode 100644 src/App.tsx create mode 100644 src/components/layout/Navbar.tsx create mode 100644 src/components/ui/accordion.tsx create mode 100644 src/components/ui/alert-dialog.tsx create mode 100644 src/components/ui/alert.tsx create mode 100644 src/components/ui/aspect-ratio.tsx create mode 100644 src/components/ui/avatar.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/breadcrumb.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/calendar.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/carousel.tsx create mode 100644 src/components/ui/chart.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/components/ui/collapsible.tsx create mode 100644 src/components/ui/command.tsx create mode 100644 src/components/ui/context-menu.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/drawer.tsx create mode 100644 src/components/ui/dropdown-menu.tsx create mode 100644 src/components/ui/form.tsx create mode 100644 src/components/ui/hover-card.tsx create mode 100644 src/components/ui/input-otp.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/menubar.tsx create mode 100644 src/components/ui/navigation-menu.tsx create mode 100644 src/components/ui/pagination.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 src/components/ui/progress.tsx create mode 100644 src/components/ui/radio-group.tsx create mode 100644 src/components/ui/resizable.tsx create mode 100644 src/components/ui/scroll-area.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/separator.tsx create mode 100644 src/components/ui/sheet.tsx create mode 100644 src/components/ui/sidebar.tsx create mode 100644 src/components/ui/skeleton.tsx create mode 100644 src/components/ui/slider.tsx create mode 100644 src/components/ui/sonner.tsx create mode 100644 src/components/ui/switch.tsx create mode 100644 src/components/ui/table.tsx create mode 100644 src/components/ui/tabs.tsx create mode 100644 src/components/ui/textarea.tsx create mode 100644 src/components/ui/toast.tsx create mode 100644 src/components/ui/toaster.tsx create mode 100644 src/components/ui/toggle-group.tsx create mode 100644 src/components/ui/toggle.tsx create mode 100644 src/components/ui/tooltip.tsx create mode 100644 src/components/ui/use-toast.ts create mode 100644 src/hooks/use-mobile.tsx create mode 100644 src/hooks/use-toast.ts create mode 100644 src/index.css create mode 100644 src/lib/store.ts create mode 100644 src/lib/types.ts create mode 100644 src/lib/utils.ts create mode 100644 src/main.tsx create mode 100644 src/pages/About.tsx create mode 100644 src/pages/Admin.tsx create mode 100644 src/pages/Blog.tsx create mode 100644 src/pages/Index.tsx create mode 100644 src/pages/Login.tsx create mode 100644 src/pages/NotFound.tsx create mode 100644 src/pages/PostDetail.tsx create mode 100644 src/pages/Profile.tsx create mode 100644 src/pages/Register.tsx create mode 100644 src/vite-env.d.ts create mode 100644 tailwind.config.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8d9b7b0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,62 @@ +# Dépendances +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Production +/dist +/build + +# Environnement local +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs +*.log + +# Cache +.cache +.parcel-cache + +# IDE +.vscode +.idea +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Git +.git +.gitignore + +# Docker +Dockerfile +docker-compose.yml +.dockerignore + +# Documentation +README.md +*.md + +# Tests +__tests__ +*.test.js +*.test.ts +*.spec.js +*.spec.ts +coverage + +# Autres +.eslintrc* +.prettierrc* +tsconfig.json +vite.config.ts \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8bf42f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +dist + +node_modules +deploy.sh +docker-compose.yml +Dockerfile + +package-lock.json \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..e67846f --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,29 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist"] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + "@typescript-eslint/no-unused-vars": "off", + }, + } +); diff --git a/index.html b/index.html new file mode 100644 index 0000000..a80f5cf --- /dev/null +++ b/index.html @@ -0,0 +1,21 @@ + + + + + + + VulnBlog - Sécurité Éducative + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..51a8f39 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,31 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html index.htm; + + # Gestion des routes React Router + location / { + try_files $uri $uri/ /index.html; + } + + # Cache pour les assets statiques + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Sécurité - masquer la version Nginx + server_tokens off; + + # Headers de sécurité + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # Compression gzip + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..63d7590 --- /dev/null +++ b/package.json @@ -0,0 +1,84 @@ +{ + "name": "shadcnui", + "type": "module", + "packageManager": "pnpm@8.10.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint --quiet ./src", + "preview": "vite preview" + }, + "dependencies": { + "@hookform/resolvers": "^3.9.0", + "@radix-ui/react-accordion": "^1.2.0", + "@radix-ui/react-alert-dialog": "^1.1.1", + "@radix-ui/react-aspect-ratio": "^1.1.0", + "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-collapsible": "^1.1.0", + "@radix-ui/react-context-menu": "^2.2.1", + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-hover-card": "^1.1.1", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-menubar": "^1.1.1", + "@radix-ui/react-navigation-menu": "^1.2.0", + "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-radio-group": "^1.2.0", + "@radix-ui/react-scroll-area": "^1.1.0", + "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slider": "^1.2.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.0", + "@radix-ui/react-toast": "^1.2.1", + "@radix-ui/react-toggle": "^1.1.0", + "@radix-ui/react-toggle-group": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.4", + "@supabase/supabase-js": "^2.50.3", + "@tanstack/react-query": "^5.56.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.0.0", + "date-fns": "^3.6.0", + "embla-carousel-react": "^8.3.0", + "input-otp": "^1.2.4", + "lucide-react": "^0.462.0", + "next-themes": "^0.3.0", + "react": "^18.3.1", + "react-day-picker": "^8.10.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.53.0", + "react-resizable-panels": "^2.1.3", + "react-router-dom": "^6.26.2", + "recharts": "^2.12.7", + "sonner": "^1.5.0", + "tailwind-merge": "^2.5.2", + "tailwindcss-animate": "^1.0.7", + "vaul": "^0.9.3", + "zod": "^3.23.8", + "zustand": "^5.0.6" + }, + "devDependencies": { + "@eslint/js": "^9.9.0", + "@tailwindcss/aspect-ratio": "^0.4.2", + "@tailwindcss/typography": "^0.5.15", + "@types/node": "^22.5.5", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "autoprefixer": "^10.4.20", + "eslint": "^9.9.0", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", + "globals": "^15.9.0", + "lovable-tagger": "^1.1.7", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.11", + "typescript": "^5.5.3", + "typescript-eslint": "^8.0.1", + "vite": "^5.4.1" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..c2a49f4 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..e69de29 diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..fca1c6a --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,38 @@ +import { Toaster } 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 Blog from './pages/Blog'; +import PostDetail from './pages/PostDetail'; +import Login from './pages/Login'; +import Register from './pages/Register'; +import Profile from './pages/Profile'; +import Admin from './pages/Admin'; +import About from './pages/About'; +import NotFound from './pages/NotFound'; + +const queryClient = new QueryClient(); + +const App = () => ( + + + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + +); + +export default App; \ No newline at end of file diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx new file mode 100644 index 0000000..3eb8540 --- /dev/null +++ b/src/components/layout/Navbar.tsx @@ -0,0 +1,75 @@ +import { useEffect, useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { Button } from '@/components/ui/button'; +import { useStore } from '@/lib/store'; +import { User } from '@/lib/types'; + +export default function Navbar() { + const { currentUser, logout } = useStore(); + const [user, setUser] = useState(null); + const navigate = useNavigate(); + + // Vulnérabilité intentionnelle: restauration de session non sécurisée + useEffect(() => { + if (currentUser) { + setUser(currentUser); + } else { + const storedToken = localStorage.getItem('authToken'); + const storedUserId = localStorage.getItem('userId'); + + if (storedToken && storedUserId) { + // Restauration automatique de session sans vérification + const userId = parseInt(storedUserId); + // Cette vulnérabilité suppose qu'on fait confiance au userId stocké localement + // sans vérifier l'authenticité du token + const users = useStore.getState().users; + const foundUser = users.find(u => u.id === userId); + + if (foundUser) { + setUser({ ...foundUser, token: storedToken }); + } + } + } + }, [currentUser]); + + const handleLogout = () => { + logout(); + setUser(null); + navigate('/'); + }; + + return ( + + ); +} \ No newline at end of file diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx new file mode 100644 index 0000000..ba71b90 --- /dev/null +++ b/src/components/ui/accordion.tsx @@ -0,0 +1,50 @@ +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 0000000..088be71 --- /dev/null +++ b/src/components/ui/alert-dialog.tsx @@ -0,0 +1,96 @@ +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 0000000..5610263 --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,36 @@ +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 & VariantProps>( + ({ className, variant, ...props }, ref) =>
+); +Alert.displayName = 'Alert'; + +const AlertTitle = React.forwardRef>(({ className, ...props }, ref) => ( +
+)); +AlertTitle.displayName = 'AlertTitle'; + +const AlertDescription = React.forwardRef>( + ({ 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 0000000..5dfdf1e --- /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 0000000..33bc9e0 --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import * as AvatarPrimitive from '@radix-ui/react-avatar'; + +import { cn } from '@/lib/utils'; + +const Avatar = React.forwardRef, 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 0000000..feca274 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,29 @@ +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 0000000..240949d --- /dev/null +++ b/src/components/ui/breadcrumb.tsx @@ -0,0 +1,68 @@ +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) =>