diff --git a/.replit b/.replit index 82bac8f..d9cc22f 100644 --- a/.replit +++ b/.replit @@ -18,3 +18,15 @@ expertMode = true [postMerge] path = "scripts/post-merge.sh" timeoutMs = 20000 + +[[ports]] +localPort = 8080 +externalPort = 8080 + +[[ports]] +localPort = 8081 +externalPort = 80 + +[[ports]] +localPort = 23924 +externalPort = 3000 diff --git a/artifacts/joachim-portfolio/.replit-artifact/artifact.toml b/artifacts/joachim-portfolio/.replit-artifact/artifact.toml new file mode 100644 index 0000000..4803696 --- /dev/null +++ b/artifacts/joachim-portfolio/.replit-artifact/artifact.toml @@ -0,0 +1,31 @@ +kind = "web" +previewPath = "/" +title = "Joachim Hummel – Portfolio" +version = "1.0.0" +id = "artifacts/joachim-portfolio" +router = "path" + +[[integratedSkills]] +name = "react-vite" +version = "1.0.0" + +[[services]] +name = "web" +paths = [ "/" ] +localPort = 23924 + +[services.development] +run = "pnpm --filter @workspace/joachim-portfolio run dev" + +[services.production] +build = [ "pnpm", "--filter", "@workspace/joachim-portfolio", "run", "build" ] +publicDir = "artifacts/joachim-portfolio/dist/public" +serve = "static" + +[[services.production.rewrites]] +from = "/*" +to = "/index.html" + +[services.env] +PORT = "23924" +BASE_PATH = "/" diff --git a/artifacts/joachim-portfolio/components.json b/artifacts/joachim-portfolio/components.json new file mode 100644 index 0000000..3ff62cf --- /dev/null +++ b/artifacts/joachim-portfolio/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} \ No newline at end of file diff --git a/artifacts/joachim-portfolio/index.html b/artifacts/joachim-portfolio/index.html new file mode 100644 index 0000000..3968508 --- /dev/null +++ b/artifacts/joachim-portfolio/index.html @@ -0,0 +1,24 @@ + + + + + + Joachim Hummel – Portfolio + + + + + + + + + + + + + + +
+ + + diff --git a/artifacts/joachim-portfolio/package.json b/artifacts/joachim-portfolio/package.json new file mode 100644 index 0000000..5792331 --- /dev/null +++ b/artifacts/joachim-portfolio/package.json @@ -0,0 +1,77 @@ +{ + "name": "@workspace/joachim-portfolio", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --config vite.config.ts --host 0.0.0.0", + "build": "vite build --config vite.config.ts", + "serve": "vite preview --config vite.config.ts --host 0.0.0.0", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "devDependencies": { + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-accordion": "^1.2.4", + "@radix-ui/react-alert-dialog": "^1.1.7", + "@radix-ui/react-aspect-ratio": "^1.1.3", + "@radix-ui/react-avatar": "^1.1.4", + "@radix-ui/react-checkbox": "^1.1.5", + "@radix-ui/react-collapsible": "^1.1.4", + "@radix-ui/react-context-menu": "^2.2.7", + "@radix-ui/react-dialog": "^1.1.7", + "@radix-ui/react-dropdown-menu": "^2.1.7", + "@radix-ui/react-hover-card": "^1.1.7", + "@radix-ui/react-label": "^2.1.3", + "@radix-ui/react-menubar": "^1.1.7", + "@radix-ui/react-navigation-menu": "^1.2.6", + "@radix-ui/react-popover": "^1.1.7", + "@radix-ui/react-progress": "^1.1.3", + "@radix-ui/react-radio-group": "^1.2.4", + "@radix-ui/react-scroll-area": "^1.2.4", + "@radix-ui/react-select": "^2.1.7", + "@radix-ui/react-separator": "^1.1.3", + "@radix-ui/react-slider": "^1.2.4", + "@radix-ui/react-slot": "^1.2.0", + "@radix-ui/react-switch": "^1.1.4", + "@radix-ui/react-tabs": "^1.1.4", + "@radix-ui/react-toast": "^1.2.7", + "@radix-ui/react-toggle": "^1.1.3", + "@radix-ui/react-toggle-group": "^1.1.3", + "@radix-ui/react-tooltip": "^1.2.0", + "@replit/vite-plugin-cartographer": "catalog:", + "@replit/vite-plugin-dev-banner": "catalog:", + "@replit/vite-plugin-runtime-error-modal": "catalog:", + "@tailwindcss/typography": "^0.5.15", + "@tailwindcss/vite": "catalog:", + "@tanstack/react-query": "catalog:", + "@types/node": "catalog:", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@vitejs/plugin-react": "catalog:", + "@workspace/api-client-react": "workspace:*", + "class-variance-authority": "catalog:", + "clsx": "catalog:", + "cmdk": "^1.1.1", + "date-fns": "^3.6.0", + "embla-carousel-react": "^8.6.0", + "framer-motion": "catalog:", + "input-otp": "^1.4.2", + "lucide-react": "catalog:", + "next-themes": "^0.4.6", + "react": "catalog:", + "react-day-picker": "^9.11.1", + "react-dom": "catalog:", + "react-hook-form": "^7.55.0", + "react-icons": "^5.4.0", + "react-resizable-panels": "^2.1.7", + "recharts": "^2.15.2", + "sonner": "^2.0.7", + "tailwind-merge": "catalog:", + "tailwindcss": "catalog:", + "tw-animate-css": "^1.4.0", + "vaul": "^1.1.2", + "vite": "catalog:", + "wouter": "^3.3.5", + "zod": "catalog:" + } +} diff --git a/artifacts/joachim-portfolio/public/favicon.svg b/artifacts/joachim-portfolio/public/favicon.svg new file mode 100644 index 0000000..4373d3c --- /dev/null +++ b/artifacts/joachim-portfolio/public/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/artifacts/joachim-portfolio/public/opengraph.jpg b/artifacts/joachim-portfolio/public/opengraph.jpg new file mode 100644 index 0000000..8b36111 Binary files /dev/null and b/artifacts/joachim-portfolio/public/opengraph.jpg differ diff --git a/artifacts/joachim-portfolio/public/robots.txt b/artifacts/joachim-portfolio/public/robots.txt new file mode 100644 index 0000000..c2a49f4 --- /dev/null +++ b/artifacts/joachim-portfolio/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / diff --git a/artifacts/joachim-portfolio/src/App.tsx b/artifacts/joachim-portfolio/src/App.tsx new file mode 100644 index 0000000..681037e --- /dev/null +++ b/artifacts/joachim-portfolio/src/App.tsx @@ -0,0 +1,32 @@ +import { Switch, Route, Router as WouterRouter } from "wouter"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { Toaster } from "@/components/ui/toaster"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import Home from "@/pages/home"; +import NotFound from "@/pages/not-found"; + +const queryClient = new QueryClient(); + +function Router() { + return ( + + + + + ); +} + +function App() { + return ( + + + + + + + + + ); +} + +export default App; diff --git a/artifacts/joachim-portfolio/src/components/bio.tsx b/artifacts/joachim-portfolio/src/components/bio.tsx new file mode 100644 index 0000000..dcf817f --- /dev/null +++ b/artifacts/joachim-portfolio/src/components/bio.tsx @@ -0,0 +1,31 @@ +import { motion } from "framer-motion"; + +export function Bio() { + return ( +
+ +

+ 04. Über Mich +

+ +
+

+ Joachim Hummel ist Senior IT-Consultant, IT-Systems Engineer und technischer Umsetzer mit rund 30 Jahren Erfahrung im IT-Betrieb. +

+

+ Seine Schwerpunkte liegen auf Linux, Docker, On-Premise-Infrastrukturen, DSGVO-konformem Selfhosting, Mailservern, Monitoring, Automatisierung und KI-gestützter Softwareentwicklung. +

+

+ Mit Vibe-Coding entwickelt er aus Ideen lauffähige Anwendungen, Automatisierungen und SaaS-Projekte. +

+
+
+
+ ); +} diff --git a/artifacts/joachim-portfolio/src/components/competencies.tsx b/artifacts/joachim-portfolio/src/components/competencies.tsx new file mode 100644 index 0000000..c236ba9 --- /dev/null +++ b/artifacts/joachim-portfolio/src/components/competencies.tsx @@ -0,0 +1,81 @@ +import { motion } from "framer-motion"; +import { Server, Box, ShieldCheck, Activity, Cpu } from "lucide-react"; +import { SiDocker, SiLinux, SiGrafana, SiNginx, SiUbuntu } from "react-icons/si"; + +const skills = [ + { + icon: Server, + title: "Linux, Serverbetrieb & Infrastruktur", + desc: "Ubuntu, Debian, Proxmox, Systemd, Shell-Scripting, TLS, Reverse Proxy" + }, + { + icon: Box, + title: "Docker & Container-Betrieb", + desc: "Docker Compose, Netzwerke, Volumes, Persistenz, Hetzner/Contabo" + }, + { + icon: ShieldCheck, + title: "On-Premise & DSGVO Selfhosting", + desc: "iRedMail/Mailcow, Nextcloud, SOGo, Authentik/OIDC, Zoraxy/Nginx" + }, + { + icon: Activity, + title: "Monitoring", + desc: "Grafana, Prometheus, Loki, Alloy" + }, + { + icon: Cpu, + title: "KI, Automatisierung & Vibe-Coding", + desc: "Claude Code, ChatGPT, n8n, RAG-Systeme, Pinecone" + } +]; + +export function Competencies() { + return ( +
+ +

+ 01. Kernkompetenzen +

+

+ Über 30 Jahre Erfahrung im IT-Betrieb, destilliert in hochspezialisierte Fähigkeiten. +

+ +
+ {skills.map((skill, index) => ( + +
+ +
+ +

{skill.title}

+

+ {skill.desc} +

+
+ ))} +
+ +
+ + + + + +
+
+
+ ); +} diff --git a/artifacts/joachim-portfolio/src/components/contact.tsx b/artifacts/joachim-portfolio/src/components/contact.tsx new file mode 100644 index 0000000..1c8fe18 --- /dev/null +++ b/artifacts/joachim-portfolio/src/components/contact.tsx @@ -0,0 +1,42 @@ +import { motion } from "framer-motion"; +import { Mail, Github, Linkedin, Terminal } from "lucide-react"; + +export function Contact() { + return ( +
+ +
+ + + +

Bereit für das nächste Projekt?

+

+ Lassen Sie uns darüber sprechen, wie wir Ihre Infrastruktur modernisieren oder Ihre nächste Anwendung bauen können. +

+ + + + Kontakt aufnehmen + + +
+ + + + + + +
+ +
+ ); +} diff --git a/artifacts/joachim-portfolio/src/components/hero.tsx b/artifacts/joachim-portfolio/src/components/hero.tsx new file mode 100644 index 0000000..7ce49c2 --- /dev/null +++ b/artifacts/joachim-portfolio/src/components/hero.tsx @@ -0,0 +1,61 @@ +import { motion } from "framer-motion"; +import { Terminal, ChevronDown } from "lucide-react"; + +export function Hero() { + return ( +
+ +
+ + System.init() +
+ +

+ Joachim + Hummel +

+ +
+ Senior IT-Consultant · IT-Systems Engineer · Vibe-Coder +
+ +

+ Zwei Welten, eine Lösung: Tiefe, praxisbewährte Infrastruktur-Expertise + trifft auf moderne KI-gestützte Softwareentwicklung. Ich baue Systeme, die + in Produktion laufen – präzise, sicher und zukunftsfähig. +

+ +
+ + Kontakt aufnehmen + + + Projekte ansehen + +
+
+ + + + + + +
+ ); +} diff --git a/artifacts/joachim-portfolio/src/components/navbar.tsx b/artifacts/joachim-portfolio/src/components/navbar.tsx new file mode 100644 index 0000000..0ffcd65 --- /dev/null +++ b/artifacts/joachim-portfolio/src/components/navbar.tsx @@ -0,0 +1,56 @@ +import { useState, useEffect } from "react"; +import { motion, useScroll, useMotionValueEvent } from "framer-motion"; + +export function Navbar() { + const [isScrolled, setIsScrolled] = useState(false); + const { scrollY } = useScroll(); + + useMotionValueEvent(scrollY, "change", (latest) => { + setIsScrolled(latest > 50); + }); + + const navLinks = [ + { name: "Kompetenzen", href: "#competencies" }, + { name: "3 Welten", href: "#strengths" }, + { name: "Projekte", href: "#projects" }, + { name: "Kontakt", href: "#contact" }, + ]; + + return ( + +
+ + JH. + + + + + + Kontakt + +
+
+ ); +} diff --git a/artifacts/joachim-portfolio/src/components/projects.tsx b/artifacts/joachim-portfolio/src/components/projects.tsx new file mode 100644 index 0000000..58bc3a0 --- /dev/null +++ b/artifacts/joachim-portfolio/src/components/projects.tsx @@ -0,0 +1,82 @@ +import { motion } from "framer-motion"; +import { FolderGit2, ArrowRight } from "lucide-react"; + +const projects = [ + { + title: "SafeDocs Portal", + desc: "Sichere Upload-Plattform mit AES-256-GCM-Verschlüsselung, JWT-Auth, Passwort-Reset, Rate Limiting, Docker-Deployment", + tags: ["Docker", "Security", "AES-256"] + }, + { + title: "zensend.email", + desc: "Newsletter & E-Mail-Marketing SaaS: Double-Opt-In, DSGVO, SPF/DKIM/DMARC, Zahlungsmodelle, Onboarding", + tags: ["SaaS", "Mail", "DSGVO"] + }, + { + title: "KI-Automation mit n8n", + desc: "Automatisierte Bildgenerierung, Lead-Prozesse, KI-gestützte Bildprüfung, API/Webhook-Workflows", + tags: ["n8n", "AI", "API"] + }, + { + title: "Eigene KI- & RAG-Systeme", + desc: "PDF-Verarbeitung, Vektorisierung, Pinecone, eigene Wissensdatenbanken", + tags: ["RAG", "Pinecone", "LLMs"] + }, + { + title: "On-Premise Monitoring", + desc: "Grafana, Prometheus, Loki, Alloy Stacks auf Docker-Basis", + tags: ["Grafana", "Prometheus", "Docker"] + }, + { + title: "Mailserver & Groupware", + desc: "iRedMail, Mailcow, SOGo, DNS, SPF, DKIM, DMARC, Zustellbarkeit", + tags: ["Mailcow", "DNS", "Security"] + } +]; + +export function Projects() { + return ( +
+ +

+ 03. Ausgewählte Projekte +

+

+ Von der sicheren Infrastruktur bis zur modernen SaaS-Lösung – in der Praxis bewährt. +

+ +
+ {projects.map((project, index) => ( + +
+ + +
+

{project.title}

+

{project.desc}

+
+ {project.tags.map((tag, tIndex) => ( + + {tag} + + ))} +
+
+ ))} +
+
+
+ ); +} diff --git a/artifacts/joachim-portfolio/src/components/strengths.tsx b/artifacts/joachim-portfolio/src/components/strengths.tsx new file mode 100644 index 0000000..5774f2a --- /dev/null +++ b/artifacts/joachim-portfolio/src/components/strengths.tsx @@ -0,0 +1,64 @@ +import { motion } from "framer-motion"; +import { TerminalSquare, BrainCircuit, Wrench } from "lucide-react"; + +const strengths = [ + { + icon: TerminalSquare, + title: "Klassischer IT-Betrieb", + desc: "Server, Netzwerke, Logs, Zertifikate, Mailserver aus 30 Jahren Praxis. Fundiertes Wissen, das die Basis für jedes zuverlässige System bildet." + }, + { + icon: BrainCircuit, + title: "KI-gestützte Entwicklung", + desc: "Vibe-Coding mit Claude Code & ChatGPT. Der direkte Weg von der Idee zur lauffähigen Anwendung, unterstützt durch modernste AI-Tools." + }, + { + icon: Wrench, + title: "Praxisnahe Umsetzung", + desc: "Baut, testet, betreibt, dokumentiert und verbessert reale Systeme. Kein Elfenbeinturm-Engineering, sondern Lösungen, die in Produktion funktionieren." + } +]; + +export function Strengths() { + return ( +
+ +

+ 02. 3 Welten +

+

+ Die besondere Stärke liegt in der Kombination aus klassischem Betrieb, moderner KI-Entwicklung und handfester Umsetzung. +

+ +
+ {strengths.map((item, index) => ( + +
+ +
+

{item.title}

+

+ {item.desc} +

+ + {/* Decorative line */} +
+ + ))} +
+
+
+ ); +} diff --git a/artifacts/joachim-portfolio/src/components/ui/accordion.tsx b/artifacts/joachim-portfolio/src/components/ui/accordion.tsx new file mode 100644 index 0000000..e1797c9 --- /dev/null +++ b/artifacts/joachim-portfolio/src/components/ui/accordion.tsx @@ -0,0 +1,55 @@ +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/artifacts/joachim-portfolio/src/components/ui/alert-dialog.tsx b/artifacts/joachim-portfolio/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..fa2b442 --- /dev/null +++ b/artifacts/joachim-portfolio/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/artifacts/joachim-portfolio/src/components/ui/alert.tsx b/artifacts/joachim-portfolio/src/components/ui/alert.tsx new file mode 100644 index 0000000..5afd41d --- /dev/null +++ b/artifacts/joachim-portfolio/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 px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + 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/artifacts/joachim-portfolio/src/components/ui/aspect-ratio.tsx b/artifacts/joachim-portfolio/src/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..c4abbf3 --- /dev/null +++ b/artifacts/joachim-portfolio/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/artifacts/joachim-portfolio/src/components/ui/avatar.tsx b/artifacts/joachim-portfolio/src/components/ui/avatar.tsx new file mode 100644 index 0000000..51e507b --- /dev/null +++ b/artifacts/joachim-portfolio/src/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +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/artifacts/joachim-portfolio/src/components/ui/badge.tsx b/artifacts/joachim-portfolio/src/components/ui/badge.tsx new file mode 100644 index 0000000..3f03665 --- /dev/null +++ b/artifacts/joachim-portfolio/src/components/ui/badge.tsx @@ -0,0 +1,43 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + // @replit + // Whitespace-nowrap: Badges should never wrap. + "whitespace-nowrap inline-flex items-center rounded-md 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" + + " hover-elevate ", + { + variants: { + variant: { + default: + // @replit shadow-xs instead of shadow, no hover because we use hover-elevate + "border-transparent bg-primary text-primary-foreground shadow-xs", + secondary: + // @replit no hover because we use hover-elevate + "border-transparent bg-secondary text-secondary-foreground", + destructive: + // @replit shadow-xs instead of shadow, no hover because we use hover-elevate + "border-transparent bg-destructive text-destructive-foreground shadow-xs", + // @replit shadow-xs" - use badge outline variable + outline: "text-foreground border [border-color:var(--badge-outline)]", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/artifacts/joachim-portfolio/src/components/ui/breadcrumb.tsx b/artifacts/joachim-portfolio/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..60e6c96 --- /dev/null +++ b/artifacts/joachim-portfolio/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) =>