commit 09d2c5cbe17bf55c06361e94bd0551eb0528e56e Author: Julien Froidefond Date: Wed Aug 20 15:43:24 2025 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b93089 --- /dev/null +++ b/.gitignore @@ -0,0 +1,88 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# Temporary folders +tmp/ +temp/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..507d0ff --- /dev/null +++ b/README.md @@ -0,0 +1,105 @@ +# Design System Template + +Template Next.js avec un design system complet basé sur shadcn/ui, Tailwind CSS et Radix UI. + +## 🚀 Usage rapide + +1. **Copier le template** + + ```bash + cp -r design-system-template my-new-app + cd my-new-app + ``` + +2. **Installer les dépendances** + + ```bash + pnpm install + # ou npm install / yarn install + ``` + +3. **Lancer le dev server** + + ```bash + pnpm dev + ``` + +4. **Personnaliser** + - Changer le nom dans `package.json` + - Modifier `app/layout.tsx` (title, description) + - Customiser `app/page.tsx` + +## 📦 Inclus + +### Composants UI (shadcn/ui) + +- **Tous les composants** : Button, Card, Dialog, Form, Input, Select, etc. +- **Layout** : ThemeProvider, ThemeToggle +- **Hooks** : use-mobile, use-toast +- **Utils** : clsx, tailwind-merge + +### Styling + +- **Tailwind CSS v4** avec PostCSS +- **Variables CSS** pour theming +- **Dark/Light mode** automatique +- **Animations custom** (glitch, holo, matrix) +- **Scrollbar custom** + +### Config + +- **TypeScript** setup complet +- **Path aliases** (@/components, @/lib, etc.) +- **ESLint/Prettier** ready +- **Next.js 15** avec App Router + +## 🎨 Customisation + +### Couleurs + +Modifier les variables CSS dans `app/globals.css`: + +```css +:root { + --primary: oklch(0.205 0 0); + --background: oklch(0.98 0.005 240); + /* ... */ +} +``` + +### Composants + +Tous les composants sont dans `/components/ui/` et modifiables. + +### Ajout de nouveaux composants + +```bash +npx shadcn@latest add [component-name] +``` + +## 🛠 Structure + +``` +├── app/ +│ ├── globals.css # Styles + variables CSS +│ ├── layout.tsx # Layout racine +│ └── page.tsx # Page d'accueil +├── components/ +│ ├── ui/ # Tous les composants shadcn +│ └── layout/ # ThemeProvider, etc. +├── lib/ +│ └── utils.ts # Utilities (cn, etc.) +├── hooks/ # Hooks réutilisables +└── package.json # Dépendances complètes +``` + +## 📚 Documentation + +- [shadcn/ui](https://ui.shadcn.com/) +- [Tailwind CSS](https://tailwindcss.com/) +- [Radix UI](https://www.radix-ui.com/) +- [Next.js](https://nextjs.org/) + +--- + +**Prêt à développer !** 🎉 diff --git a/app/evaluation/page.tsx b/app/evaluation/page.tsx new file mode 100644 index 0000000..4897a1b --- /dev/null +++ b/app/evaluation/page.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { useEffect } from "react"; +import { useEvaluation } from "@/hooks/use-evaluation"; +import { ProfileForm } from "@/components/profile-form"; +import { SkillEvaluation } from "@/components/skill-evaluation"; +import { useUser } from "@/hooks/use-user-context"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +export default function EvaluationPage() { + const { + userEvaluation, + skillCategories, + teams, + loading, + updateProfile, + updateSkillLevel, + addSkillToEvaluation, + removeSkillFromEvaluation, + initializeEmptyEvaluation, + } = useEvaluation(); + + const { setUserInfo } = useUser(); + + // Update user info in navigation when user evaluation is loaded + useEffect(() => { + if (userEvaluation) { + const teamName = + teams.find((t) => t.id === userEvaluation.profile.teamId)?.name || ""; + setUserInfo({ + firstName: userEvaluation.profile.firstName, + lastName: userEvaluation.profile.lastName, + teamName, + }); + } else { + setUserInfo(null); + } + }, [userEvaluation, teams, setUserInfo]); + + if (loading) { + return ( +
+
+
+
+

Chargement...

+
+
+
+ ); + } + + // If no user evaluation exists, show profile form + if (!userEvaluation) { + const handleProfileSubmit = (profile: any) => { + initializeEmptyEvaluation(profile); + }; + + return ( +
+
+
+ +
+
+

+ Commencer l'évaluation +

+

+ Renseignez vos informations pour débuter votre auto-évaluation +

+
+ +
+ +
+
+
+ ); + } + + return ( +
+ {/* Skill Evaluation */} + {skillCategories.length > 0 && userEvaluation.evaluations.length > 0 && ( + + )} +
+ ); +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..d511237 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,466 @@ +@import "tailwindcss"; + +/* Modern grid background effect */ +.bg-grid-slate-700\/25 { + background-image: radial-gradient( + circle, + rgb(51 65 85 / 0.25) 1px, + transparent 1px + ); +} + +/* Cyberpunk glow effects */ +.glow-violet { + box-shadow: 0 0 20px rgb(139 92 246 / 0.3); +} + +.glow-cyan { + box-shadow: 0 0 20px rgb(34 211 238 / 0.3); +} + +/* Animated gradient text */ +.gradient-text { + background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); + background-size: 400% 400%; + animation: gradient 15s ease infinite; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +@keyframes gradient { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} + +/* Floating animation for tech elements */ +.float { + animation: float 6s ease-in-out infinite; +} + +@keyframes float { + 0% { + transform: translateY(0px); + } + 50% { + transform: translateY(-10px); + } + 100% { + transform: translateY(0px); + } +} + +/* Pulse effect for active elements */ +.pulse-slow { + animation: pulse-slow 3s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +@keyframes pulse-slow { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.8; + } +} + +/* Tech border animations */ +.tech-border { + position: relative; + overflow: hidden; +} + +.tech-border::before { + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient( + 90deg, + transparent, + rgba(139, 92, 246, 0.4), + transparent + ); + transition: left 0.5s; +} + +.tech-border:hover::before { + left: 100%; +} +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(0.98 0.005 240); + --foreground: oklch(0.145 0 0); + --card: oklch(0.995 0.003 240); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(0.995 0.003 240); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.955 0.01 240); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.955 0.01 240); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.955 0.01 240); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.895 0.015 240); + --input: oklch(0.895 0.015 240); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.98 0.005 240); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.955 0.01 240); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.895 0.015 240); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.11 0.015 245); + --foreground: oklch(0.985 0 0); + --card: oklch(0.18 0.02 245); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.18 0.02 245); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.25 0.025 245); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.25 0.025 245); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.25 0.025 245); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.32 0.03 245); + --input: oklch(0.32 0.03 245); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.16 0.02 245); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.25 0.025 245); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.32 0.03 245); + --sidebar-ring: oklch(0.439 0 0); +} + +@theme inline { + --font-sans: var(--font-inter); + --font-mono: var(--font-jetbrains-mono); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} + +/* Custom scrollbar */ +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: #f1f5f9; +} + +::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} + +/* Dark mode scrollbar */ +.dark ::-webkit-scrollbar-track { + background: #1e293b; +} + +.dark ::-webkit-scrollbar-thumb { + background: #475569; +} + +.dark ::-webkit-scrollbar-thumb:hover { + background: #64748b; +} + +/* Code syntax highlighting */ +pre code { + font-family: var(--font-mono); +} + +/* Adding tech-style custom animations and effects */ +@keyframes matrix-rain { + 0% { + transform: translateY(-100vh); + opacity: 0; + } + 10% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + transform: translateY(100vh); + opacity: 0; + } +} + +@keyframes glow-pulse { + 0%, + 100% { + box-shadow: 0 0 5px currentColor; + } + 50% { + box-shadow: 0 0 20px currentColor, 0 0 30px currentColor; + } +} + +.matrix-rain { + animation: matrix-rain 3s linear infinite; +} + +.glow-pulse { + animation: glow-pulse 2s ease-in-out infinite; +} + +/* Tech grid background */ +.tech-grid { + background-image: linear-gradient( + rgba(59, 130, 246, 0.1) 1px, + transparent 1px + ), + linear-gradient(90deg, rgba(59, 130, 246, 0.1) 1px, transparent 1px); + background-size: 50px 50px; +} + +/* Glitch effect for headers */ +.glitch { + position: relative; +} + +.glitch::before, +.glitch::after { + content: attr(data-text); + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.glitch::before { + animation: glitch-1 0.5s infinite; + color: #ff0000; + z-index: -1; +} + +.glitch::after { + animation: glitch-2 0.5s infinite; + color: #00ff00; + z-index: -2; +} + +@keyframes glitch-1 { + 0%, + 14%, + 15%, + 49%, + 50%, + 99%, + 100% { + transform: translate(0); + } + 15%, + 49% { + transform: translate(-2px, -1px); + } +} + +@keyframes glitch-2 { + 0%, + 20%, + 21%, + 62%, + 63%, + 99%, + 100% { + transform: translate(0); + } + 21%, + 62% { + transform: translate(2px, 1px); + } +} + +/* Terminal cursor */ +.terminal-cursor::after { + content: "▋"; + animation: blink 1s infinite; + color: #00ff00; +} + +@keyframes blink { + 0%, + 50% { + opacity: 1; + } + 51%, + 100% { + opacity: 0; + } +} + +/* Holographic animations */ +@keyframes holo-shift { + 0% { + filter: hue-rotate(0deg); + } + 25% { + filter: hue-rotate(90deg); + } + 50% { + filter: hue-rotate(180deg); + } + 75% { + filter: hue-rotate(270deg); + } + 100% { + filter: hue-rotate(360deg); + } +} + +@keyframes holo-scan { + 0% { + transform: translateX(-100%) skewX(-12deg); + } + 100% { + transform: translateX(300%) skewX(-12deg); + } +} + +@keyframes holo-line-1 { + 0%, + 100% { + opacity: 0; + transform: scaleX(0); + } + 50% { + opacity: 1; + transform: scaleX(1); + } +} + +@keyframes holo-line-2 { + 0%, + 100% { + opacity: 0; + transform: scaleX(0); + } + 60% { + opacity: 1; + transform: scaleX(1); + } +} + +@keyframes holo-line-3 { + 0%, + 100% { + opacity: 0; + transform: scaleX(0); + } + 70% { + opacity: 1; + transform: scaleX(1); + } +} + +.animate-holo-shift { + animation: holo-shift 3s ease-in-out infinite; +} + +.animate-holo-scan { + animation: holo-scan 2s ease-out infinite; +} + +.animate-holo-line-1 { + animation: holo-line-1 2s ease-in-out infinite; +} + +.animate-holo-line-2 { + animation: holo-line-2 2.2s ease-in-out infinite; +} + +.animate-holo-line-3 { + animation: holo-line-3 2.4s ease-in-out infinite; +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..811155c --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,41 @@ +import type { Metadata } from "next"; +import { GeistSans } from "geist/font/sans"; +import { GeistMono } from "geist/font/mono"; +import "./globals.css"; +import { ThemeProvider } from "@/components/layout/theme-provider"; +import { Toaster } from "@/components/ui/sonner"; +import { UserProvider } from "@/hooks/use-user-context"; +import { NavigationWrapper } from "@/components/layout/navigation-wrapper"; + +export const metadata: Metadata = { + title: "PeakSkills - Auto-évaluation de compétences", + description: + "Plateforme d'auto-évaluation de compétences techniques pour les équipes", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + + +
{children}
+ +
+
+ + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..c909dc9 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,201 @@ +"use client"; + +import { useEffect } from "react"; +import { useEvaluation } from "@/hooks/use-evaluation"; +import { ProfileForm } from "@/components/profile-form"; +import { SkillsRadarChart } from "@/components/radar-chart"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { generateRadarData } from "@/lib/evaluation-utils"; +import { useUser } from "@/hooks/use-user-context"; +import Link from "next/link"; +import { Code2 } from "lucide-react"; + +export default function HomePage() { + const { userEvaluation, skillCategories, teams, loading, updateProfile } = + useEvaluation(); + + const { setUserInfo } = useUser(); + + // Update user info in navigation when user evaluation is loaded + useEffect(() => { + if (userEvaluation) { + const teamName = + teams.find((t) => t.id === userEvaluation.profile.teamId)?.name || ""; + setUserInfo({ + firstName: userEvaluation.profile.firstName, + lastName: userEvaluation.profile.lastName, + teamName, + }); + } else { + setUserInfo(null); + } + }, [userEvaluation, teams, setUserInfo]); + + if (loading) { + return ( +
+
+
+ +
+
+
+
+

Chargement...

+
+
+
+
+ ); + } + + if (!userEvaluation) { + return ( +
+
+
+ +
+
+

+ Bienvenue sur PeakSkills +

+

+ Évaluez vos compétences techniques et suivez votre progression +

+
+ +
+ +
+
+
+ ); + } + + const radarData = generateRadarData( + userEvaluation.evaluations, + skillCategories + ); + + return ( +
+ {/* Background Effects */} +
+
+
+ +
+ {/* Header */} +
+
+ + + Tableau de bord + +
+ +

+ Bonjour {userEvaluation.profile.firstName} ! +

+ +

+ Voici un aperçu de vos compétences techniques +

+
+ + {/* Main Content Grid */} +
+ {/* Radar Chart */} +
+
+

+ Vue d'ensemble de vos compétences +

+

+ Graphique radar représentant votre niveau moyen par catégorie +

+
+
+ +
+
+ + {/* Category Breakdown */} +
+
+

+ Détail par catégorie +

+

+ Vue détaillée de votre progression dans chaque domaine +

+
+
+ {radarData.map((category) => { + const categoryEval = userEvaluation.evaluations.find( + (e) => e.category === category.category + ); + const skillsCount = categoryEval?.selectedSkillIds?.length || 0; + const evaluatedCount = + categoryEval?.skills.filter((s) => s.level !== null).length || + 0; + + return ( +
+
+

+ {category.category} +

+
+ + {category.score.toFixed(1)}/3 + +
+
+
+ {evaluatedCount}/{skillsCount} compétences sélectionnées + évaluées +
+
+
+
+
+ ); + })} +
+
+
+ + {/* Action Button */} +
+ +
+
+
+ ); +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..4ee62ee --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/components/evaluation/category-tabs.tsx b/components/evaluation/category-tabs.tsx new file mode 100644 index 0000000..d092106 --- /dev/null +++ b/components/evaluation/category-tabs.tsx @@ -0,0 +1,41 @@ +import { ChevronRight } from "lucide-react"; +import { SkillCategory } from "@/lib/types"; + +interface CategoryTabsProps { + categories: SkillCategory[]; + selectedCategory: string; + onCategoryChange: (category: string) => void; +} + +export function CategoryTabs({ + categories, + selectedCategory, + onCategoryChange, +}: CategoryTabsProps) { + return ( +
+ {categories.map((category) => { + const isActive = selectedCategory === category.category; + return ( + + ); + })} +
+ ); +} diff --git a/components/evaluation/evaluation-header.tsx b/components/evaluation/evaluation-header.tsx new file mode 100644 index 0000000..4dbc057 --- /dev/null +++ b/components/evaluation/evaluation-header.tsx @@ -0,0 +1,22 @@ +import { Code2 } from "lucide-react"; + +export function EvaluationHeader() { + return ( +
+
+ + + Évaluation de compétences + +
+ +

+ Mes compétences techniques +

+ +

+ Sélectionne tes technologies et définis ton niveau de maîtrise +

+
+ ); +} diff --git a/components/evaluation/index.ts b/components/evaluation/index.ts new file mode 100644 index 0000000..3d8026d --- /dev/null +++ b/components/evaluation/index.ts @@ -0,0 +1,5 @@ +export { EvaluationHeader } from "./evaluation-header"; +export { SkillLevelLegend } from "./skill-level-legend"; +export { CategoryTabs } from "./category-tabs"; +export { SkillEvaluationGrid } from "./skill-evaluation-grid"; +export { SkillEvaluationCard } from "./skill-evaluation-card"; diff --git a/components/evaluation/skill-evaluation-card.tsx b/components/evaluation/skill-evaluation-card.tsx new file mode 100644 index 0000000..65ef445 --- /dev/null +++ b/components/evaluation/skill-evaluation-card.tsx @@ -0,0 +1,157 @@ +import { ExternalLink, BookOpen, Target, Info } from "lucide-react"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { Skill, SkillLevel, SKILL_LEVELS } from "@/lib/types"; +import { getTechIcon } from "@/components/icons/tech-icons"; + +interface SkillEvaluationCardProps { + skill: Skill; + currentLevel: SkillLevel; + onUpdateSkill: (skillId: string, level: SkillLevel) => void; +} + +function getLevelColor(level: Exclude) { + switch (level) { + case "never": + return "bg-red-500"; + case "not-autonomous": + return "bg-amber-500"; + case "autonomous": + return "bg-green-500"; + case "expert": + return "bg-violet-500"; + } +} + +function getLevelColorBorder(level: Exclude) { + switch (level) { + case "never": + return "border-red-500"; + case "not-autonomous": + return "border-amber-500"; + case "autonomous": + return "border-green-500"; + case "expert": + return "border-violet-500"; + } +} + +export function SkillEvaluationCard({ + skill, + currentLevel, + onUpdateSkill, +}: SkillEvaluationCardProps) { + const TechIcon = getTechIcon(skill.id); + + return ( +
+
+ {/* Skill Info */} +
+
+ +
+ +
+

+ {skill.name} +

+ + {/* Info tooltip */} + + + + + +
+

{skill.description}

+ {skill.links.length > 0 && ( +
+

Liens utiles :

+ {skill.links.map((link, index) => ( + + + {new URL(link).hostname} + + ))} +
+ )} +
+
+
+
+
+ + {/* Level Selection */} +
+ {Object.entries(SKILL_LEVELS).map(([value, label]) => { + const isSelected = currentLevel === value; + const levelValue = value as Exclude; + return ( + + + + + +

{label}

+
+
+ ); + })} + + {/* Goal buttons */} +
+ + + + + +

Envie d'apprendre

+
+
+ + + + + + +

Peut encadrer

+
+
+
+
+
+
+ ); +} diff --git a/components/evaluation/skill-evaluation-grid.tsx b/components/evaluation/skill-evaluation-grid.tsx new file mode 100644 index 0000000..ca9d92e --- /dev/null +++ b/components/evaluation/skill-evaluation-grid.tsx @@ -0,0 +1,61 @@ +import { SkillCategory, CategoryEvaluation, SkillLevel } from "@/lib/types"; +import { SkillEvaluationCard } from "./skill-evaluation-card"; + +interface SkillEvaluationGridProps { + currentCategory: SkillCategory; + currentEvaluation: CategoryEvaluation; + onUpdateSkill: (category: string, skillId: string, level: SkillLevel) => void; +} + +export function SkillEvaluationGrid({ + currentCategory, + currentEvaluation, + onUpdateSkill, +}: SkillEvaluationGridProps) { + const getSkillLevel = (skillId: string): SkillLevel => { + const skillEval = currentEvaluation?.skills.find( + (s) => s.skillId === skillId + ); + return skillEval?.level || null; + }; + + if (!currentEvaluation.selectedSkillIds.length) { + return null; + } + + return ( +
+
+

+ {currentCategory.category} +

+
+ + {currentEvaluation.selectedSkillIds.length} compétence + {currentEvaluation.selectedSkillIds.length > 1 ? "s" : ""} + +
+
+ +
+ {currentEvaluation.selectedSkillIds.map((skillId) => { + const skill = currentCategory.skills.find((s) => s.id === skillId); + if (!skill) return null; + + const currentLevel = getSkillLevel(skill.id); + + return ( + + onUpdateSkill(currentCategory.category, skillId, level) + } + /> + ); + })} +
+
+ ); +} diff --git a/components/evaluation/skill-level-legend.tsx b/components/evaluation/skill-level-legend.tsx new file mode 100644 index 0000000..f884e88 --- /dev/null +++ b/components/evaluation/skill-level-legend.tsx @@ -0,0 +1,99 @@ +import { Zap, Circle, Minus, CheckCircle2 } from "lucide-react"; +import { SKILL_LEVELS, SkillLevel } from "@/lib/types"; + +function getLevelConfig(level: Exclude) { + switch (level) { + case "never": + return { + bg: "bg-gradient-to-r from-red-500/20 to-red-600/20", + border: "border-red-500/30", + text: "text-red-600 dark:text-red-400", + glow: "shadow-red-500/20", + icon: Circle, + }; + case "not-autonomous": + return { + bg: "bg-gradient-to-r from-amber-500/20 to-yellow-600/20", + border: "border-amber-500/30", + text: "text-amber-600 dark:text-amber-400", + glow: "shadow-amber-500/20", + icon: Minus, + }; + case "autonomous": + return { + bg: "bg-gradient-to-r from-green-500/20 to-emerald-600/20", + border: "border-green-500/30", + text: "text-green-600 dark:text-green-400", + glow: "shadow-green-500/20", + icon: CheckCircle2, + }; + case "expert": + return { + bg: "bg-gradient-to-r from-violet-500/20 to-purple-600/20", + border: "border-violet-500/30", + text: "text-violet-600 dark:text-violet-400", + glow: "shadow-violet-500/20", + icon: Zap, + }; + } +} + +const levelDescriptions = { + never: + "Je n'ai jamais utilisé cette technologie ou je n'en ai qu'une connaissance théorique très limitée.", + "not-autonomous": + "J'ai déjà utilisé cette technologie mais j'ai besoin d'aide ou de supervision pour l'utiliser efficacement.", + autonomous: + "Je maîtrise cette technologie et peux travailler de manière autonome dessus dans la plupart des situations.", + expert: + "J'ai une expertise avancée, je peux former d'autres personnes et résoudre des problèmes complexes.", +}; + +export function SkillLevelLegend() { + return ( +
+
+

+ + Légende des niveaux de compétence +

+ +
+ {Object.entries(SKILL_LEVELS).map(([value, label]) => { + const config = getLevelConfig(value as Exclude); + const Icon = config.icon; + const description = + levelDescriptions[value as keyof typeof levelDescriptions]; + + return ( +
+
+
+ +
+
+
+

{label}

+

+ {description} +

+
+
+ ); + })} +
+ +
+

+ 💡 Astuce : Soyez honnête dans votre + auto-évaluation. Cela vous aidera à identifier vos points forts et + les domaines à améliorer. +

+
+
+
+ ); +} diff --git a/components/icons/smart-tech-icon.tsx b/components/icons/smart-tech-icon.tsx new file mode 100644 index 0000000..ff50722 --- /dev/null +++ b/components/icons/smart-tech-icon.tsx @@ -0,0 +1,108 @@ +"use client"; + +import { getTechIcon } from "./tech-icons"; + +interface SmartTechIconProps { + techId: string; + techName: string; + className?: string; + fallbackStyle?: "initial" | "emoji" | "gradient"; +} + +// Emojis pour certaines technologies populaires +const TECH_EMOJIS: Record = { + react: "⚛️", + vue: "💚", + typescript: "🔷", + nextjs: "▲", + nodejs: "🟢", + python: "🐍", + javascript: "💛", + html: "🧡", + css: "💙", + docker: "🐳", + kubernetes: "☸️", + aws: "☁️", + git: "🌿", + github: "🐙", + mongodb: "🍃", + postgresql: "🐘", + redis: "🔴", + mysql: "🐬", + flutter: "💙", + swift: "🦉", + kotlin: "🟣", + java: "☕", + php: "🐘", + ruby: "💎", + go: "🐹", + rust: "🦀", + csharp: "💜", +}; + +export function SmartTechIcon({ + techId, + techName, + className = "w-6 h-6", + fallbackStyle = "initial", +}: SmartTechIconProps) { + // Essayer d'abord l'icône SVG locale + const TechIcon = getTechIcon(techId); + + // Si ce n'est pas l'icône par défaut, l'utiliser + if (TechIcon !== getTechIcon("default")) { + return ; + } + + // Sinon, utiliser le style de fallback + switch (fallbackStyle) { + case "emoji": + const emoji = TECH_EMOJIS[techId]; + if (emoji) { + return ( +
+ {emoji} +
+ ); + } + break; + + case "gradient": + const gradientColors = [ + "from-blue-500 to-purple-500", + "from-green-500 to-blue-500", + "from-red-500 to-pink-500", + "from-yellow-500 to-orange-500", + "from-purple-500 to-indigo-500", + ]; + const colorIndex = techId.length % gradientColors.length; + + return ( +
+ {techName.charAt(0).toUpperCase()} +
+ ); + + default: // "initial" + return ( +
+ {techName.charAt(0).toUpperCase()} +
+ ); + } + + // Fallback final + return ( +
+ {techName.charAt(0).toUpperCase()} +
+ ); +} diff --git a/components/icons/tech-icon.tsx b/components/icons/tech-icon.tsx new file mode 100644 index 0000000..db3c2bd --- /dev/null +++ b/components/icons/tech-icon.tsx @@ -0,0 +1,77 @@ +"use client"; + +import Image from "next/image"; +import { useState } from "react"; + +interface TechIconExternalProps { + techId: string; + techName: string; + className?: string; + size?: number; +} + +// Service Simple Icons pour récupérer les icônes SVG +export function TechIconExternal({ + techId, + techName, + className = "w-6 h-6", + size = 24, +}: TechIconExternalProps) { + const [imageError, setImageError] = useState(false); + + // URL pour Simple Icons + const simpleIconUrl = `https://cdn.jsdelivr.net/npm/simple-icons@v9/${techId}.svg`; + + // URL pour DevIcons + const devIconUrl = `https://cdn.jsdelivr.net/gh/devicons/devicon/icons/${techId}/${techId}-original.svg`; + + if (imageError) { + // Fallback vers initiale si l'icône n'existe pas + return ( +
+ {techName.charAt(0).toUpperCase()} +
+ ); + } + + return ( + {`${techName} setImageError(true)} + style={{ filter: "brightness(0) saturate(100%) invert(0.5)" }} // Pour s'adapter au thème + /> + ); +} + +// Mapping des IDs pour Simple Icons (ils utilisent parfois des noms différents) +export const SIMPLE_ICONS_MAPPING: Record = { + react: "react", + vue: "vuedotjs", + typescript: "typescript", + nextjs: "nextdotjs", + nodejs: "nodedotjs", + python: "python", + express: "express", + postgresql: "postgresql", + mongodb: "mongodb", + redis: "redis", + docker: "docker", + kubernetes: "kubernetes", + aws: "amazonwebservices", + terraform: "terraform", + jenkins: "jenkins", + githubactions: "githubactions", + reactnative: "react", + flutter: "flutter", + swift: "swift", + kotlin: "kotlin", + expo: "expo", + tailwindcss: "tailwindcss", + webpack: "webpack", +}; diff --git a/components/icons/tech-icons.tsx b/components/icons/tech-icons.tsx new file mode 100644 index 0000000..53de2db --- /dev/null +++ b/components/icons/tech-icons.tsx @@ -0,0 +1,66 @@ +import React from "react"; + +interface TechIconProps { + className?: string; +} + +export const ReactIcon = ({ className = "w-6 h-6" }: TechIconProps) => ( + + + +); + +export const VueIcon = ({ className = "w-6 h-6" }: TechIconProps) => ( + + + +); + +export const TypeScriptIcon = ({ className = "w-6 h-6" }: TechIconProps) => ( + + + +); + +export const NextJSIcon = ({ className = "w-6 h-6" }: TechIconProps) => ( + + + +); + +export const NodeJSIcon = ({ className = "w-6 h-6" }: TechIconProps) => ( + + + +); + +export const PythonIcon = ({ className = "w-6 h-6" }: TechIconProps) => ( + + + +); + +// Map des icônes par ID de technologie +export const TECH_ICONS: Record> = { + react: ReactIcon, + vue: VueIcon, + typescript: TypeScriptIcon, + nextjs: NextJSIcon, + nodejs: NodeJSIcon, + python: PythonIcon, + // Icône par défaut pour les technologies sans icône spécifique + default: ({ className = "w-6 h-6" }: TechIconProps) => ( +
+ ? +
+ ), +}; + +// Fonction utilitaire pour obtenir l'icône d'une technologie +export const getTechIcon = ( + techId: string +): React.ComponentType => { + return TECH_ICONS[techId] || TECH_ICONS.default; +}; diff --git a/components/layout/index.ts b/components/layout/index.ts new file mode 100644 index 0000000..1a63b39 --- /dev/null +++ b/components/layout/index.ts @@ -0,0 +1,3 @@ +// Theme and layout components +export { ThemeProvider } from "./theme-provider"; +export { ThemeToggle } from "./theme-toggle"; diff --git a/components/layout/navigation-wrapper.tsx b/components/layout/navigation-wrapper.tsx new file mode 100644 index 0000000..2ea053a --- /dev/null +++ b/components/layout/navigation-wrapper.tsx @@ -0,0 +1,10 @@ +"use client"; + +import { Navigation } from "./navigation"; +import { useUser } from "@/hooks/use-user-context"; + +export function NavigationWrapper() { + const { userInfo } = useUser(); + + return ; +} diff --git a/components/layout/navigation.tsx b/components/layout/navigation.tsx new file mode 100644 index 0000000..596fb51 --- /dev/null +++ b/components/layout/navigation.tsx @@ -0,0 +1,84 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { ThemeToggle } from "@/components/layout/theme-toggle"; +import { BarChart3, User, Settings } from "lucide-react"; + +interface NavigationProps { + userInfo?: { + firstName: string; + lastName: string; + teamName: string; + }; +} + +export function Navigation({ userInfo }: NavigationProps = {}) { + const pathname = usePathname(); + + const navItems = [ + { + href: "/", + label: "Tableau de bord", + icon: BarChart3, + }, + { + href: "/evaluation", + label: "Évaluation", + icon: User, + }, + ]; + + return ( +
+
+
+ + PeakSkills + + + +
+ +
+ {userInfo && ( +
+
+ +
+
+

+ {userInfo.firstName} {userInfo.lastName} +

+

+ {userInfo.teamName} +

+
+
+ )} + +
+
+
+ ); +} diff --git a/components/layout/theme-provider.tsx b/components/layout/theme-provider.tsx new file mode 100644 index 0000000..55c2f6e --- /dev/null +++ b/components/layout/theme-provider.tsx @@ -0,0 +1,11 @@ +'use client' + +import * as React from 'react' +import { + ThemeProvider as NextThemesProvider, + type ThemeProviderProps, +} from 'next-themes' + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children} +} diff --git a/components/layout/theme-toggle.tsx b/components/layout/theme-toggle.tsx new file mode 100644 index 0000000..85d80d2 --- /dev/null +++ b/components/layout/theme-toggle.tsx @@ -0,0 +1,24 @@ +"use client"; + +import * as React from "react"; +import { Moon, Sun } from "lucide-react"; +import { useTheme } from "next-themes"; + +import { Button } from "@/components/ui/button"; + +export function ThemeToggle() { + const { theme, setTheme } = useTheme(); + + return ( + + ); +} diff --git a/components/profile-form.tsx b/components/profile-form.tsx new file mode 100644 index 0000000..c540a34 --- /dev/null +++ b/components/profile-form.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { UserProfile, Team } from "@/lib/types"; + +interface ProfileFormProps { + teams: Team[]; + initialProfile?: UserProfile; + onSubmit: (profile: UserProfile) => void; +} + +export function ProfileForm({ + teams, + initialProfile, + onSubmit, +}: ProfileFormProps) { + const [firstName, setFirstName] = useState(initialProfile?.firstName || ""); + const [lastName, setLastName] = useState(initialProfile?.lastName || ""); + const [teamId, setTeamId] = useState(initialProfile?.teamId || ""); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (firstName && lastName && teamId) { + onSubmit({ firstName, lastName, teamId }); + } + }; + + const isValid = + firstName.length > 0 && lastName.length > 0 && teamId.length > 0; + + // Group teams by direction + const teamsByDirection = teams.reduce((acc, team) => { + if (!acc[team.direction]) { + acc[team.direction] = []; + } + acc[team.direction].push(team); + return acc; + }, {} as Record); + + return ( + + + Informations personnelles + + Renseignez vos informations pour commencer votre auto-évaluation + + + +
+
+ + setFirstName(e.target.value)} + placeholder="Votre prénom" + required + /> +
+ +
+ + setLastName(e.target.value)} + placeholder="Votre nom" + required + /> +
+ +
+ + +
+ + +
+
+
+ ); +} diff --git a/components/radar-chart.tsx b/components/radar-chart.tsx new file mode 100644 index 0000000..4edfa8e --- /dev/null +++ b/components/radar-chart.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { + Radar, + RadarChart, + PolarGrid, + PolarAngleAxis, + PolarRadiusAxis, + ResponsiveContainer, +} from "recharts"; +import { RadarChartData } from "@/lib/types"; + +interface SkillsRadarChartProps { + data: RadarChartData[]; +} + +export function SkillsRadarChart({ data }: SkillsRadarChartProps) { + // Transform data for the chart + const chartData = data.map((item) => ({ + category: item.category, + score: item.score, + maxScore: item.maxScore, + })); + + if (data.length === 0) { + return ( +
+

Aucune donnée disponible

+
+ ); + } + + return ( +
+ + + + + + + + +
+ ); +} diff --git a/components/skill-evaluation.tsx b/components/skill-evaluation.tsx new file mode 100644 index 0000000..4829cc7 --- /dev/null +++ b/components/skill-evaluation.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { useState } from "react"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { SkillCategory, SkillLevel, CategoryEvaluation } from "@/lib/types"; +import { SkillSelector } from "./skill-selector"; +import { + EvaluationHeader, + SkillLevelLegend, + CategoryTabs, + SkillEvaluationGrid, +} from "./evaluation"; + +interface SkillEvaluationProps { + categories: SkillCategory[]; + evaluations: CategoryEvaluation[]; + onUpdateSkill: (category: string, skillId: string, level: SkillLevel) => void; + onAddSkill: (category: string, skillId: string) => void; + onRemoveSkill: (category: string, skillId: string) => void; +} + +export function SkillEvaluation({ + categories, + evaluations, + onUpdateSkill, + onAddSkill, + onRemoveSkill, +}: SkillEvaluationProps) { + const [selectedCategory, setSelectedCategory] = useState( + categories[0]?.category || "" + ); + + const currentCategory = categories.find( + (cat) => cat.category === selectedCategory + ); + const currentEvaluation = evaluations.find( + (evaluation) => evaluation.category === selectedCategory + ); + + if (!currentCategory) { + return
Aucune catégorie disponible
; + } + + return ( + +
+ {/* Background Effects */} +
+
+
+ +
+ + + + +
+ + + {currentEvaluation && ( + + )} +
+ + +
+
+ + ); +} diff --git a/components/skill-selector.tsx b/components/skill-selector.tsx new file mode 100644 index 0000000..5faa6a3 --- /dev/null +++ b/components/skill-selector.tsx @@ -0,0 +1,180 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Plus, Search, X } from "lucide-react"; +import { SkillCategory, CategoryEvaluation } from "@/lib/types"; +import { getTechIcon } from "./icons/tech-icons"; + +interface SkillSelectorProps { + categories: SkillCategory[]; + evaluations: CategoryEvaluation[]; + selectedCategory: string; + onAddSkill: (category: string, skillId: string) => void; + onRemoveSkill: (category: string, skillId: string) => void; +} + +export function SkillSelector({ + categories, + evaluations, + selectedCategory, + onAddSkill, + onRemoveSkill, +}: SkillSelectorProps) { + const [isOpen, setIsOpen] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + + const currentCategory = categories.find( + (cat) => cat.category === selectedCategory + ); + const currentEvaluation = evaluations.find( + (evaluation) => evaluation.category === selectedCategory + ); + + if (!currentCategory || !currentEvaluation) return null; + + const selectedSkillIds = currentEvaluation.selectedSkillIds || []; + + const filteredSkills = currentCategory.skills.filter( + (skill) => + skill.name.toLowerCase().includes(searchTerm.toLowerCase()) || + skill.description.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const availableSkills = filteredSkills.filter( + (skill) => !selectedSkillIds.includes(skill.id) + ); + + const selectedSkills = currentCategory.skills.filter((skill) => + selectedSkillIds.includes(skill.id) + ); + + return ( +
+ {/* Selected Skills */} +
+
+

Mes compétences sélectionnées

+ + + + + + + + Ajouter des compétences - {selectedCategory} + + + Choisissez les compétences que vous souhaitez évaluer dans + cette catégorie. + + + +
+ {/* Search */} +
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+ + {/* Available Skills Grid */} +
+
+ {availableSkills.length > 0 ? ( + availableSkills.map((skill) => ( +
+
+
+ {(() => { + const TechIcon = getTechIcon(skill.id); + return ( + + ); + })()} +
+
+
{skill.name}
+

+ {skill.description} +

+
+
+ +
+ )) + ) : ( +
+ {searchTerm + ? "Aucune compétence trouvée" + : "Toutes les compétences ont été ajoutées"} +
+ )} +
+
+
+
+
+
+ + {selectedSkills.length > 0 ? ( +
+ {selectedSkills.map((skill) => ( + + {skill.name} + + + ))} +
+ ) : ( +
+

Aucune compétence sélectionnée

+

+ Cliquez sur "Ajouter une compétence" pour commencer +

+
+ )} +
+
+ ); +} diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx new file mode 100644 index 0000000..4a8cca4 --- /dev/null +++ b/components/ui/accordion.tsx @@ -0,0 +1,66 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDownIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..0863e40 --- /dev/null +++ b/components/ui/alert-dialog.tsx @@ -0,0 +1,157 @@ +"use client" + +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" + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx new file mode 100644 index 0000000..1421354 --- /dev/null +++ b/components/ui/alert.tsx @@ -0,0 +1,66 @@ +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 px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/components/ui/aspect-ratio.tsx b/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..3df3fd0 --- /dev/null +++ b/components/ui/aspect-ratio.tsx @@ -0,0 +1,11 @@ +"use client" + +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +function AspectRatio({ + ...props +}: React.ComponentProps) { + return +} + +export { AspectRatio } diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..71e428b --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..0205413 --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,46 @@ +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 badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..eb88f32 --- /dev/null +++ b/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return