Adding current files to the repo

This commit is contained in:
Adam Kagdi
2025-12-10 17:15:45 +04:00
parent 2f0d1a16de
commit ae41548799
90 changed files with 14189 additions and 0 deletions

44
src/App.tsx Normal file
View File

@@ -0,0 +1,44 @@
import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { ThemeProvider } from "@/components/ThemeProvider";
import Index from "./pages/Index";
import About from "./pages/About";
import Portfolio from "./pages/Portfolio";
import Contact from "./pages/Contact";
import ErpSystems from "./pages/services/ErpSystems";
import ECommerce from "./pages/services/ECommerce";
import RmmServices from "./pages/services/RmmServices";
import AmcSupport from "./pages/services/AmcSupport";
import NotFound from "./pages/NotFound";
const queryClient = new QueryClient();
const App = () => (
<QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="light" storageKey="openxpert-ui-theme">
<TooltipProvider>
<Toaster />
<Sonner />
<BrowserRouter>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/about" element={<About />} />
<Route path="/portfolio" element={<Portfolio />} />
<Route path="/contact" element={<Contact />} />
<Route path="/services/erp" element={<ErpSystems />} />
<Route path="/services/ecommerce" element={<ECommerce />} />
<Route path="/services/rmm" element={<RmmServices />} />
<Route path="/services/amc" element={<AmcSupport />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</TooltipProvider>
</ThemeProvider>
</QueryClientProvider>
);
export default App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -0,0 +1,19 @@
import { cn } from "@/lib/utils";
interface FloatingShapeProps {
className?: string;
delay?: number;
}
export const FloatingShape = ({ className, delay = 0 }: FloatingShapeProps) => {
return (
<div
className={cn(
"absolute rounded-full blur-3xl opacity-20 pointer-events-none",
delay % 2 === 0 ? "animate-float" : "animate-float-delayed",
className
)}
style={{ animationDelay: `${delay}s` }}
/>
);
};

165
src/components/Footer.tsx Normal file
View File

@@ -0,0 +1,165 @@
import { Link } from "react-router-dom";
import { Mail, Phone, MapPin, Linkedin, Twitter, Github } from "lucide-react";
import openxpertLogo from "@/assets/openxpert-logo.jpg";
const Footer = () => {
return (
<footer className="glass-strong border-t border-glass-border mt-32">
<div className="container mx-auto px-6 py-16">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12">
{/* Brand Column */}
<div>
<div className="flex items-center gap-3 mb-6">
<img
src={openxpertLogo}
alt="OpenXpert Solutions"
className="h-12 w-auto object-contain"
/>
</div>
<p className="text-sm text-muted-foreground leading-relaxed mb-6">
Empowering businesses with secure, future-ready technology solutions.
</p>
<div className="flex gap-3">
<a
href="#"
className="w-10 h-10 rounded-full glass-card flex items-center justify-center hover:glow-effect transition-all"
>
<Linkedin className="w-4 h-4 text-primary" />
</a>
<a
href="#"
className="w-10 h-10 rounded-full glass-card flex items-center justify-center hover:glow-effect transition-all"
>
<Twitter className="w-4 h-4 text-primary" />
</a>
<a
href="#"
className="w-10 h-10 rounded-full glass-card flex items-center justify-center hover:glow-effect transition-all"
>
<Github className="w-4 h-4 text-primary" />
</a>
</div>
</div>
{/* Services Column */}
<div>
<h4 className="font-semibold text-foreground mb-4">Services</h4>
<ul className="space-y-3">
<li>
<Link
to="/services/erp"
className="text-sm text-muted-foreground hover:text-primary transition-colors"
>
ERP Systems
</Link>
</li>
<li>
<Link
to="/services/ecommerce"
className="text-sm text-muted-foreground hover:text-primary transition-colors"
>
E-Commerce Solutions
</Link>
</li>
<li>
<Link
to="/services/rmm"
className="text-sm text-muted-foreground hover:text-primary transition-colors"
>
RMM Services
</Link>
</li>
<li>
<Link
to="/services/amc"
className="text-sm text-muted-foreground hover:text-primary transition-colors"
>
AMC Support
</Link>
</li>
</ul>
</div>
{/* Company Column */}
<div>
<h4 className="font-semibold text-foreground mb-4">Company</h4>
<ul className="space-y-3">
<li>
<Link
to="/about"
className="text-sm text-muted-foreground hover:text-primary transition-colors"
>
About Us
</Link>
</li>
<li>
<Link
to="/portfolio"
className="text-sm text-muted-foreground hover:text-primary transition-colors"
>
Portfolio
</Link>
</li>
<li>
<Link
to="/contact"
className="text-sm text-muted-foreground hover:text-primary transition-colors"
>
Contact Us
</Link>
</li>
</ul>
</div>
{/* Contact Column */}
<div>
<h4 className="font-semibold text-foreground mb-4">Contact</h4>
<ul className="space-y-3">
<li className="flex items-start gap-2">
<Mail className="w-4 h-4 text-primary mt-0.5" />
<span className="text-sm text-muted-foreground">
info@openxpert.com
</span>
</li>
<li className="flex items-start gap-2">
<Phone className="w-4 h-4 text-primary mt-0.5" />
<span className="text-sm text-muted-foreground">
+971 XXX XXXX
</span>
</li>
<li className="flex items-start gap-2">
<MapPin className="w-4 h-4 text-primary mt-0.5" />
<span className="text-sm text-muted-foreground">
Dubai, UAE
</span>
</li>
</ul>
</div>
</div>
{/* Bottom Bar */}
<div className="border-t border-glass-border mt-12 pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
<p className="text-xs text-muted-foreground">
© {new Date().getFullYear()} OpenXpert Solutions. All rights reserved.
</p>
<div className="flex gap-6">
<Link
to="/privacy"
className="text-xs text-muted-foreground hover:text-primary transition-colors"
>
Privacy Policy
</Link>
<Link
to="/terms"
className="text-xs text-muted-foreground hover:text-primary transition-colors"
>
Terms of Service
</Link>
</div>
</div>
</div>
</footer>
);
};
export default Footer;

View File

@@ -0,0 +1,28 @@
import { NavLink as RouterNavLink, NavLinkProps } from "react-router-dom";
import { forwardRef } from "react";
import { cn } from "@/lib/utils";
interface NavLinkCompatProps extends Omit<NavLinkProps, "className"> {
className?: string;
activeClassName?: string;
pendingClassName?: string;
}
const NavLink = forwardRef<HTMLAnchorElement, NavLinkCompatProps>(
({ className, activeClassName, pendingClassName, to, ...props }, ref) => {
return (
<RouterNavLink
ref={ref}
to={to}
className={({ isActive, isPending }) =>
cn(className, isActive && activeClassName, isPending && pendingClassName)
}
{...props}
/>
);
},
);
NavLink.displayName = "NavLink";
export { NavLink };

View File

@@ -0,0 +1,183 @@
import { useState } from "react";
import { Link, useLocation } from "react-router-dom";
import { ChevronDown, Menu, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ThemeToggle } from "@/components/ThemeToggle";
import openxpertLogo from "@/assets/openxpert-logo.jpg";
const Navigation = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isServicesOpen, setIsServicesOpen] = useState(false);
const location = useLocation();
const services = [
{ name: "ERP Systems", path: "/services/erp" },
{ name: "Custom E-Commerce", path: "/services/ecommerce" },
{ name: "RMM Services", path: "/services/rmm" },
{ name: "AMC Support", path: "/services/amc" },
{ name: "Community Projects", path: "/services/community" },
{ name: "Web Development", path: "/services/web" },
];
const isActive = (path: string) => location.pathname === path;
return (
<nav className="fixed top-0 left-0 right-0 z-50 glass-strong border-b border-glass-border">
<div className="container mx-auto px-6 py-4">
<div className="flex items-center justify-between">
{/* Logo */}
<Link to="/" className="flex items-center gap-3 group">
<div className="relative">
<img
src={openxpertLogo}
alt="OpenXpert Solutions"
className="h-10 w-auto object-contain transition-transform duration-300 group-hover:scale-105"
/>
<div className="absolute inset-0 blur-xl opacity-0 group-hover:opacity-20 bg-primary/30 -z-10 transition-opacity duration-300" />
</div>
<span className="text-lg font-semibold text-foreground hidden md:block">
OpenXpert Solutions
</span>
</Link>
{/* Desktop Navigation */}
<div className="hidden lg:flex items-center gap-8">
<Link
to="/"
className={`nav-link ${isActive("/") ? "text-primary font-semibold" : "text-muted-foreground"}`}
>
Home
</Link>
<Link
to="/about"
className={`nav-link ${isActive("/about") ? "text-primary font-semibold" : "text-muted-foreground"}`}
>
About Us
</Link>
{/* Services Dropdown */}
<div
className="relative"
onMouseEnter={() => setIsServicesOpen(true)}
onMouseLeave={() => setIsServicesOpen(false)}
>
<button className="nav-link text-muted-foreground flex items-center gap-1">
Services
<ChevronDown className={`w-4 h-4 transition-transform duration-200 ${isServicesOpen ? "rotate-180" : ""}`} />
</button>
{isServicesOpen && (
<div className="absolute top-full left-0 mt-2 w-64 glass-strong rounded-2xl overflow-hidden shadow-lg animate-slide-in-up">
{services.map((service) => (
<Link
key={service.path}
to={service.path}
className="block px-6 py-3 text-sm text-muted-foreground hover:text-primary hover:bg-accent/50 transition-colors"
>
{service.name}
</Link>
))}
</div>
)}
</div>
<Link
to="/portfolio"
className={`nav-link ${isActive("/portfolio") ? "text-primary font-semibold" : "text-muted-foreground"}`}
>
Portfolio
</Link>
<Link
to="/contact"
className={`nav-link ${isActive("/contact") ? "text-primary font-semibold" : "text-muted-foreground"}`}
>
Contact
</Link>
</div>
{/* Right Side Actions */}
<div className="flex items-center gap-4">
<ThemeToggle />
<Button className="hidden lg:inline-flex glass-card hover:glow-effect">
Book a Consultation
</Button>
{/* Mobile Menu Toggle */}
<button
className="lg:hidden p-2 text-foreground"
onClick={() => setIsMenuOpen(!isMenuOpen)}
>
{isMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
</div>
</div>
{/* Mobile Menu */}
{isMenuOpen && (
<div className="lg:hidden mt-6 glass-card rounded-2xl p-6 animate-slide-in-up">
<div className="flex flex-col gap-4">
<Link
to="/"
className="nav-link text-muted-foreground"
onClick={() => setIsMenuOpen(false)}
>
Home
</Link>
<Link
to="/about"
className="nav-link text-muted-foreground"
onClick={() => setIsMenuOpen(false)}
>
About Us
</Link>
<div>
<button
className="nav-link text-muted-foreground flex items-center gap-1 w-full justify-between"
onClick={() => setIsServicesOpen(!isServicesOpen)}
>
Services
<ChevronDown className={`w-4 h-4 transition-transform ${isServicesOpen ? "rotate-180" : ""}`} />
</button>
{isServicesOpen && (
<div className="ml-4 mt-2 flex flex-col gap-2">
{services.map((service) => (
<Link
key={service.path}
to={service.path}
className="text-sm text-muted-foreground hover:text-primary"
onClick={() => setIsMenuOpen(false)}
>
{service.name}
</Link>
))}
</div>
)}
</div>
<Link
to="/portfolio"
className="nav-link text-muted-foreground"
onClick={() => setIsMenuOpen(false)}
>
Portfolio
</Link>
<Link
to="/contact"
className="nav-link text-muted-foreground"
onClick={() => setIsMenuOpen(false)}
>
Contact
</Link>
<Button className="glass-card hover:glow-effect w-full mt-4">
Book a Consultation
</Button>
</div>
</div>
)}
</div>
</nav>
);
};
export default Navigation;

View File

@@ -0,0 +1,91 @@
import { useEffect, useRef } from "react";
interface Particle {
x: number;
y: number;
size: number;
speedX: number;
speedY: number;
opacity: number;
}
export const ParticleField = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let animationFrameId: number;
let particles: Particle[] = [];
const resizeCanvas = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
const createParticles = () => {
particles = [];
const particleCount = Math.floor((canvas.width * canvas.height) / 15000);
for (let i = 0; i < particleCount; i++) {
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
size: Math.random() * 3 + 1,
speedX: (Math.random() - 0.5) * 0.5,
speedY: (Math.random() - 0.5) * 0.5,
opacity: Math.random() * 0.5 + 0.2
});
}
};
const drawParticles = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach((particle) => {
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
ctx.fillStyle = `hsla(205, 100%, 88%, ${particle.opacity})`;
ctx.fill();
// Update position
particle.x += particle.speedX;
particle.y += particle.speedY;
// Wrap around edges
if (particle.x < 0) particle.x = canvas.width;
if (particle.x > canvas.width) particle.x = 0;
if (particle.y < 0) particle.y = canvas.height;
if (particle.y > canvas.height) particle.y = 0;
});
animationFrameId = requestAnimationFrame(drawParticles);
};
resizeCanvas();
createParticles();
drawParticles();
window.addEventListener("resize", () => {
resizeCanvas();
createParticles();
});
return () => {
cancelAnimationFrame(animationFrameId);
window.removeEventListener("resize", resizeCanvas);
};
}, []);
return (
<canvas
ref={canvasRef}
className="absolute inset-0 pointer-events-none"
style={{ opacity: 0.6 }}
/>
);
};

View File

@@ -0,0 +1,46 @@
import { LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
interface ServiceCardProps {
icon: LucideIcon;
title: string;
description: string;
delay?: number;
}
export const ServiceCard = ({ icon: Icon, title, description, delay = 0 }: ServiceCardProps) => {
return (
<div
className={cn(
"group relative glass-card rounded-3xl p-8 transition-all duration-500 hover:scale-105",
"animate-slide-in-up"
)}
style={{ animationDelay: `${delay}ms` }}
>
{/* Glow effect on hover */}
<div className="absolute inset-0 rounded-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-500 glow-effect -z-10" />
{/* Icon container with gradient */}
<div className="mb-6 relative">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-primary/10 to-accent/20 flex items-center justify-center backdrop-blur-sm border border-primary/10">
<Icon className="w-8 h-8 text-primary" />
</div>
{/* Floating decorative element */}
<div className="absolute -top-1 -right-1 w-3 h-3 rounded-full bg-accent/50 animate-glow-pulse" />
</div>
{/* Content */}
<h3 className="text-xl font-semibold mb-3 text-foreground tracking-tight">
{title}
</h3>
<p className="text-muted-foreground leading-relaxed text-sm">
{description}
</p>
{/* Shimmer effect */}
<div className="absolute top-0 left-0 w-full h-full rounded-3xl overflow-hidden pointer-events-none">
<div className="absolute top-0 left-[-100%] w-full h-full bg-gradient-to-r from-transparent via-primary/5 to-transparent group-hover:left-[100%] transition-all duration-1000" />
</div>
</div>
);
};

View File

@@ -0,0 +1,62 @@
import { createContext, useContext, useEffect, useState } from "react";
type Theme = "dark" | "light";
type ThemeProviderProps = {
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
};
type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
const initialState: ThemeProviderState = {
theme: "light",
setTheme: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
export function ThemeProvider({
children,
defaultTheme = "light",
storageKey = "openxpert-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
);
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
root.classList.add(theme);
}, [theme]);
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider");
return context;
};

View File

@@ -0,0 +1,30 @@
import { Moon, Sun } from "lucide-react";
import { useTheme } from "@/components/ThemeProvider";
export const ThemeToggle = () => {
const { theme, setTheme } = useTheme();
return (
<button
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
className="relative w-14 h-7 rounded-full glass-card transition-all duration-300 hover:glow-effect group"
aria-label="Toggle theme"
>
{/* Toggle Pill */}
<div
className={`absolute top-1 ${
theme === "dark" ? "right-1" : "left-1"
} w-5 h-5 rounded-full bg-gradient-to-br from-primary to-accent transition-all duration-300 flex items-center justify-center shadow-lg`}
>
{theme === "dark" ? (
<Moon className="w-3 h-3 text-primary-foreground" />
) : (
<Sun className="w-3 h-3 text-primary-foreground" />
)}
</div>
{/* Background Glow */}
<div className="absolute inset-0 rounded-full opacity-0 group-hover:opacity-100 glow-effect transition-opacity duration-300 -z-10" />
</button>
);
};

View File

@@ -0,0 +1,52 @@
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<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@@ -0,0 +1,104 @@
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<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
);
AlertDialogHeader.displayName = "AlertDialogHeader";
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
);
AlertDialogFooter.displayName = "AlertDialogFooter";
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
{...props}
/>
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};

View File

@@ -0,0 +1,43 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h5 ref={ref} className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props} />
),
);
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
),
);
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription };

View File

@@ -0,0 +1,5 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
const AspectRatio = AspectRatioPrimitive.Root;
export { AspectRatio };

View File

@@ -0,0 +1,38 @@
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils";
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image ref={ref} className={cn("aspect-square h-full w-full", className)} {...props} />
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback };

View File

@@ -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<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants };

View File

@@ -0,0 +1,90 @@
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) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className,
)}
{...props}
/>
),
);
BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
({ className, ...props }, ref) => (
<li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
),
);
BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a";
return <Comp ref={ref} className={cn("transition-colors hover:text-foreground", className)} {...props} />;
});
BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
),
);
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
<li role="presentation" aria-hidden="true" className={cn("[&>svg]:size-3.5", className)} {...props}>
{children ?? <ChevronRight />}
</li>
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};

View File

@@ -0,0 +1,47 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
},
);
Button.displayName = "Button";
export { Button, buttonVariants };

View File

@@ -0,0 +1,54 @@
import * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DayPicker } from "react-day-picker";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell: "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(buttonVariants({ variant: "ghost" }), "h-9 w-9 p-0 font-normal aria-selected:opacity-100"),
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ..._props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ..._props }) => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
);
}
Calendar.displayName = "Calendar";
export { Calendar };

View File

@@ -0,0 +1,43 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)} {...props} />
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
),
);
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
),
);
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
),
);
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />,
);
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
),
);
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };

View File

@@ -0,0 +1,224 @@
import * as React from "react";
import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return context;
}
const Carousel = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & CarouselProps>(
({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }, ref) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins,
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return;
}
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext],
);
React.useEffect(() => {
if (!api || !setApi) {
return;
}
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
if (!api) {
return;
}
onSelect(api);
api.on("reInit", onSelect);
api.on("select", onSelect);
return () => {
api?.off("select", onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
},
);
Carousel.displayName = "Carousel";
const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel();
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn("flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", className)}
{...props}
/>
</div>
);
},
);
CarouselContent.displayName = "CarouselContent";
const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const { orientation } = useCarousel();
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn("min-w-0 shrink-0 grow-0 basis-full", orientation === "horizontal" ? "pl-4" : "pt-4", className)}
{...props}
/>
);
},
);
CarouselItem.displayName = "CarouselItem";
const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
);
},
);
CarouselPrevious.displayName = "CarouselPrevious";
const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
);
},
);
CarouselNext.displayName = "CarouselNext";
export { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };

303
src/components/ui/chart.tsx Normal file
View File

@@ -0,0 +1,303 @@
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "@/lib/utils";
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> });
};
type ChartContextProps = {
config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
}
return context;
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig;
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className,
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
});
ChartContainer.displayName = "Chart";
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color);
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`,
)
.join("\n"),
}}
/>
);
};
const ChartTooltip = RechartsPrimitive.Tooltip;
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref,
) => {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
}
const [item] = payload;
const key = `${labelKey || item.dataKey || item.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
if (labelFormatter) {
return <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>;
}
if (!value) {
return null;
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);
if (!active || !payload?.length) {
return null;
}
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className,
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center",
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
})}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center",
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">{itemConfig?.label || item.name}</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
},
);
ChartTooltipContent.displayName = "ChartTooltip";
const ChartLegend = RechartsPrimitive.Legend;
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean;
nameKey?: string;
}
>(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
const { config } = useChart();
if (!payload?.length) {
return null;
}
return (
<div
ref={ref}
className={cn("flex items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
className={cn("flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground")}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
});
ChartLegendContent.displayName = "ChartLegend";
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
if (typeof payload !== "object" || payload === null) {
return undefined;
}
const payloadPayload =
"payload" in payload && typeof payload.payload === "object" && payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
}
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
}
export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle };

View File

@@ -0,0 +1,26 @@
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };

View File

@@ -0,0 +1,9 @@
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View File

@@ -0,0 +1,132 @@
import * as React from "react";
import { type DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import { cn } from "@/lib/utils";
import { Dialog, DialogContent } from "@/components/ui/dialog";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className,
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />);
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className,
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
className,
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};

View File

@@ -0,0 +1,178 @@
import * as React from "react";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const ContextMenu = ContextMenuPrimitive.Root;
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
const ContextMenuGroup = ContextMenuPrimitive.Group;
const ContextMenuPortal = ContextMenuPrimitive.Portal;
const ContextMenuSub = ContextMenuPrimitive.Sub;
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
));
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
));
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
/>
));
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
));
ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
));
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold text-foreground", inset && "pl-8", className)}
{...props}
/>
));
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-border", className)} {...props} />
));
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
};
ContextMenuShortcut.displayName = "ContextMenuShortcut";
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
};

View File

@@ -0,0 +1,95 @@
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};

View File

@@ -0,0 +1,87 @@
import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/lib/utils";
const Drawer = ({ shouldScaleBackground = true, ...props }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
);
Drawer.displayName = "Drawer";
const DrawerTrigger = DrawerPrimitive.Trigger;
const DrawerPortal = DrawerPrimitive.Portal;
const DrawerClose = DrawerPrimitive.Close;
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay ref={ref} className={cn("fixed inset-0 z-50 bg-black/80", className)} {...props} />
));
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className,
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
));
DrawerContent.displayName = "DrawerContent";
const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} {...props} />
);
DrawerHeader.displayName = "DrawerHeader";
const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
);
DrawerFooter.displayName = "DrawerFooter";
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
};

View File

@@ -0,0 +1,179 @@
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent focus:bg-accent",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />;
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};

129
src/components/ui/form.tsx Normal file
View File

@@ -0,0 +1,129 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from "react-hook-form";
import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label";
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
},
);
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return <Label ref={ref} className={cn(error && "text-destructive", className)} htmlFor={formItemId} {...props} />;
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
);
},
);
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return <p ref={ref} id={formDescriptionId} className={cn("text-sm text-muted-foreground", className)} {...props} />;
},
);
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<p ref={ref} id={formMessageId} className={cn("text-sm font-medium text-destructive", className)} {...props}>
{body}
</p>
);
},
);
FormMessage.displayName = "FormMessage";
export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };

View File

@@ -0,0 +1,27 @@
import * as React from "react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import { cn } from "@/lib/utils";
const HoverCard = HoverCardPrimitive.Root;
const HoverCardTrigger = HoverCardPrimitive.Trigger;
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
export { HoverCard, HoverCardTrigger, HoverCardContent };

View File

@@ -0,0 +1,61 @@
import * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp";
import { Dot } from "lucide-react";
import { cn } from "@/lib/utils";
const InputOTP = React.forwardRef<React.ElementRef<typeof OTPInput>, React.ComponentPropsWithoutRef<typeof OTPInput>>(
({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn("flex items-center gap-2 has-[:disabled]:opacity-50", containerClassName)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
),
);
InputOTP.displayName = "InputOTP";
const InputOTPGroup = React.forwardRef<React.ElementRef<"div">, React.ComponentPropsWithoutRef<"div">>(
({ className, ...props }, ref) => <div ref={ref} className={cn("flex items-center", className)} {...props} />,
);
InputOTPGroup.displayName = "InputOTPGroup";
const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> & { index: number }
>(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
return (
<div
ref={ref}
className={cn(
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-2 ring-ring ring-offset-background",
className,
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink h-4 w-px bg-foreground duration-1000" />
</div>
)}
</div>
);
});
InputOTPSlot.displayName = "InputOTPSlot";
const InputOTPSeparator = React.forwardRef<React.ElementRef<"div">, React.ComponentPropsWithoutRef<"div">>(
({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<Dot />
</div>
),
);
InputOTPSeparator.displayName = "InputOTPSeparator";
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

View File

@@ -0,0 +1,22 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input };

View File

@@ -0,0 +1,17 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70");
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };

View File

@@ -0,0 +1,207 @@
import * as React from "react";
import * as MenubarPrimitive from "@radix-ui/react-menubar";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const MenubarMenu = MenubarPrimitive.Menu;
const MenubarGroup = MenubarPrimitive.Group;
const MenubarPortal = MenubarPrimitive.Portal;
const MenubarSub = MenubarPrimitive.Sub;
const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn("flex h-10 items-center space-x-1 rounded-md border bg-background p-1", className)}
{...props}
/>
));
Menubar.displayName = MenubarPrimitive.Root.displayName;
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
/>
));
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
));
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(({ className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, ref) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</MenubarPrimitive.Portal>
));
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
/>
));
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
));
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
));
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props}
/>
));
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
));
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
};
MenubarShortcut.displayname = "MenubarShortcut";
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
};

View File

@@ -0,0 +1,120 @@
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn("relative z-10 flex max-w-max flex-1 items-center justify-center", className)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn("group flex flex-1 list-none items-center justify-center space-x-1", className)}
{...props}
/>
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
);
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto",
className,
)}
{...props}
/>
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className,
)}
ref={ref}
{...props}
/>
</div>
));
NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className,
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
};

View File

@@ -0,0 +1,81 @@
import * as React from "react";
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils";
import { ButtonProps, buttonVariants } from "@/components/ui/button";
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
);
Pagination.displayName = "Pagination";
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
({ className, ...props }, ref) => (
<ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} />
),
);
PaginationContent.displayName = "PaginationContent";
const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} />
));
PaginationItem.displayName = "PaginationItem";
type PaginationLinkProps = {
isActive?: boolean;
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">;
const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className,
)}
{...props}
/>
);
PaginationLink.displayName = "PaginationLink";
const PaginationPrevious = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink aria-label="Go to previous page" size="default" className={cn("gap-1 pl-2.5", className)} {...props}>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
);
PaginationPrevious.displayName = "PaginationPrevious";
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink aria-label="Go to next page" size="default" className={cn("gap-1 pr-2.5", className)} {...props}>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
);
PaginationNext.displayName = "PaginationNext";
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span aria-hidden className={cn("flex h-9 w-9 items-center justify-center", className)} {...props}>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
);
PaginationEllipsis.displayName = "PaginationEllipsis";
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
};

View File

@@ -0,0 +1,29 @@
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent };

View File

@@ -0,0 +1,23 @@
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@/lib/utils";
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress };

View File

@@ -0,0 +1,36 @@
import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />;
});
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
});
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
export { RadioGroup, RadioGroupItem };

View File

@@ -0,0 +1,37 @@
import { GripVertical } from "lucide-react";
import * as ResizablePrimitive from "react-resizable-panels";
import { cn } from "@/lib/utils";
const ResizablePanelGroup = ({ className, ...props }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn("flex h-full w-full data-[panel-group-direction=vertical]:flex-col", className)}
{...props}
/>
);
const ResizablePanel = ResizablePrimitive.Panel;
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean;
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className,
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
);
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };

View File

@@ -0,0 +1,38 @@
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils";
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root ref={ref} className={cn("relative overflow-hidden", className)} {...props}>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };

View File

@@ -0,0 +1,143 @@
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label ref={ref} className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} {...props} />
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};

View File

@@ -0,0 +1,20 @@
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn("shrink-0 bg-border", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", className)}
{...props}
/>
));
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };

107
src/components/ui/sheet.tsx Normal file
View File

@@ -0,0 +1,107 @@
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
},
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>(
({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-secondary hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
),
);
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title ref={ref} className={cn("text-lg font-semibold text-foreground", className)} {...props} />
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetOverlay,
SheetPortal,
SheetTitle,
SheetTrigger,
};

View File

@@ -0,0 +1,637 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { VariantProps, cva } from "class-variance-authority";
import { PanelLeft } from "lucide-react";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Sheet, SheetContent } from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
const SIDEBAR_COOKIE_NAME = "sidebar:state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContext = {
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContext | null>(null);
function useSidebar() {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.");
}
return context;
}
const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
>(({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props }, ref) => {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState);
} else {
_setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open],
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
toggleSidebar();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo<SidebarContext>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
);
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn("group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar", className)}
ref={ref}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
);
});
SidebarProvider.displayName = "SidebarProvider";
const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
}
>(({ side = "left", variant = "sidebar", collapsible = "offcanvas", className, children, ...props }, ref) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") {
return (
<div
className={cn("flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground", className)}
ref={ref}
{...props}
>
{children}
</div>
);
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
);
}
return (
<div
ref={ref}
className="group peer hidden text-sidebar-foreground md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
"relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
)}
/>
<div
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
className,
)}
{...props}
>
<div
data-sidebar="sidebar"
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
{children}
</div>
</div>
</div>
);
});
Sidebar.displayName = "Sidebar";
const SidebarTrigger = React.forwardRef<React.ElementRef<typeof Button>, React.ComponentProps<typeof Button>>(
({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
return (
<Button
ref={ref}
data-sidebar="trigger"
variant="ghost"
size="icon"
className={cn("h-7 w-7", className)}
onClick={(event) => {
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);
},
);
SidebarTrigger.displayName = "SidebarTrigger";
const SidebarRail = React.forwardRef<HTMLButtonElement, React.ComponentProps<"button">>(
({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
return (
<button
ref={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] group-data-[side=left]:-right-4 group-data-[side=right]:left-0 hover:after:bg-sidebar-border sm:flex",
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className,
)}
{...props}
/>
);
},
);
SidebarRail.displayName = "SidebarRail";
const SidebarInset = React.forwardRef<HTMLDivElement, React.ComponentProps<"main">>(({ className, ...props }, ref) => {
return (
<main
ref={ref}
className={cn(
"relative flex min-h-svh flex-1 flex-col bg-background",
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className,
)}
{...props}
/>
);
});
SidebarInset.displayName = "SidebarInset";
const SidebarInput = React.forwardRef<React.ElementRef<typeof Input>, React.ComponentProps<typeof Input>>(
({ className, ...props }, ref) => {
return (
<Input
ref={ref}
data-sidebar="input"
className={cn(
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
className,
)}
{...props}
/>
);
},
);
SidebarInput.displayName = "SidebarInput";
const SidebarHeader = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
return <div ref={ref} data-sidebar="header" className={cn("flex flex-col gap-2 p-2", className)} {...props} />;
});
SidebarHeader.displayName = "SidebarHeader";
const SidebarFooter = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
return <div ref={ref} data-sidebar="footer" className={cn("flex flex-col gap-2 p-2", className)} {...props} />;
});
SidebarFooter.displayName = "SidebarFooter";
const SidebarSeparator = React.forwardRef<React.ElementRef<typeof Separator>, React.ComponentProps<typeof Separator>>(
({ className, ...props }, ref) => {
return (
<Separator
ref={ref}
data-sidebar="separator"
className={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props}
/>
);
},
);
SidebarSeparator.displayName = "SidebarSeparator";
const SidebarContent = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className,
)}
{...props}
/>
);
});
SidebarContent.displayName = "SidebarContent";
const SidebarGroup = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
);
});
SidebarGroup.displayName = "SidebarGroup";
const SidebarGroupLabel = React.forwardRef<HTMLDivElement, React.ComponentProps<"div"> & { asChild?: boolean }>(
({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "div";
return (
<Comp
ref={ref}
data-sidebar="group-label"
className={cn(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className,
)}
{...props}
/>
);
},
);
SidebarGroupLabel.displayName = "SidebarGroupLabel";
const SidebarGroupAction = React.forwardRef<HTMLButtonElement, React.ComponentProps<"button"> & { asChild?: boolean }>(
({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
ref={ref}
data-sidebar="group-action"
className={cn(
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
},
);
SidebarGroupAction.displayName = "SidebarGroupAction";
const SidebarGroupContent = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
({ className, ...props }, ref) => (
<div ref={ref} data-sidebar="group-content" className={cn("w-full text-sm", className)} {...props} />
),
);
SidebarGroupContent.displayName = "SidebarGroupContent";
const SidebarMenu = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(({ className, ...props }, ref) => (
<ul ref={ref} data-sidebar="menu" className={cn("flex w-full min-w-0 flex-col gap-1", className)} {...props} />
));
SidebarMenu.displayName = "SidebarMenu";
const SidebarMenuItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ className, ...props }, ref) => (
<li ref={ref} data-sidebar="menu-item" className={cn("group/menu-item relative", className)} {...props} />
));
SidebarMenuItem.displayName = "SidebarMenuItem";
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
const SidebarMenuButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean;
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>
>(({ asChild = false, isActive = false, variant = "default", size = "default", tooltip, className, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
const { isMobile, state } = useSidebar();
const button = (
<Comp
ref={ref}
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
);
if (!tooltip) {
return button;
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
};
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent side="right" align="center" hidden={state !== "collapsed" || isMobile} {...tooltip} />
</Tooltip>
);
});
SidebarMenuButton.displayName = "SidebarMenuButton";
const SidebarMenuAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean;
showOnHover?: boolean;
}
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
ref={ref}
data-sidebar="menu-action"
className={cn(
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform peer-hover/menu-button:text-sidebar-accent-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
className,
)}
{...props}
/>
);
});
SidebarMenuAction.displayName = "SidebarMenuAction";
const SidebarMenuBadge = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="menu-badge"
className={cn(
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
),
);
SidebarMenuBadge.displayName = "SidebarMenuBadge";
const SidebarMenuSkeleton = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
showIcon?: boolean;
}
>(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
ref={ref}
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />}
<Skeleton
className="h-4 max-w-[--skeleton-width] flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
);
});
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
const SidebarMenuSub = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu-sub"
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
),
);
SidebarMenuSub.displayName = "SidebarMenuSub";
const SidebarMenuSubItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ ...props }, ref) => (
<li ref={ref} {...props} />
));
SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement,
React.ComponentProps<"a"> & {
asChild?: boolean;
size?: "sm" | "md";
isActive?: boolean;
}
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a";
return (
<Comp
ref={ref}
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
});
SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
};

View File

@@ -0,0 +1,7 @@
import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />;
}
export { Skeleton };

View File

@@ -0,0 +1,23 @@
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import { cn } from "@/lib/utils";
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn("relative flex w-full touch-none select-none items-center", className)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider };

View File

@@ -0,0 +1,27 @@
import { useTheme } from "next-themes";
import { Toaster as Sonner, toast } from "sonner";
type ToasterProps = React.ComponentProps<typeof Sonner>;
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
);
};
export { Toaster, toast };

View File

@@ -0,0 +1,27 @@
import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cn } from "@/lib/utils";
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };

View File

@@ -0,0 +1,72 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
</div>
),
);
Table.displayName = "Table";
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />,
);
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
),
);
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tfoot ref={ref} className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)} {...props} />
),
);
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn("border-b transition-colors data-[state=selected]:bg-muted hover:bg-muted/50", className)}
{...props}
/>
),
);
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
),
);
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<td ref={ref} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} {...props} />
),
);
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
({ className, ...props }, ref) => (
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
),
);
TableCaption.displayName = "TableCaption";
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };

View File

@@ -0,0 +1,53 @@
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className,
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className,
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@@ -0,0 +1,21 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
});
Textarea.displayName = "Textarea";
export { Textarea };

111
src/components/ui/toast.tsx Normal file
View File

@@ -0,0 +1,111 @@
import * as React from "react";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className,
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive: "destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return <ToastPrimitives.Root ref={ref} className={cn(toastVariants({ variant }), className)} {...props} />;
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors group-[.destructive]:border-muted/40 hover:bg-secondary group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 group-[.destructive]:focus:ring-destructive disabled:pointer-events-none disabled:opacity-50",
className,
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity group-hover:opacity-100 group-[.destructive]:text-red-300 hover:text-foreground group-[.destructive]:hover:text-red-50 focus:opacity-100 focus:outline-none focus:ring-2 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className,
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title ref={ref} className={cn("text-sm font-semibold", className)} {...props} />
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description ref={ref} className={cn("text-sm opacity-90", className)} {...props} />
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
};

View File

@@ -0,0 +1,24 @@
import { useToast } from "@/hooks/use-toast";
import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/toast";
export function Toaster() {
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && <ToastDescription>{description}</ToastDescription>}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}

View File

@@ -0,0 +1,49 @@
import * as React from "react";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import { type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { toggleVariants } from "@/components/ui/toggle";
const ToggleGroupContext = React.createContext<VariantProps<typeof toggleVariants>>({
size: "default",
variant: "default",
});
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root ref={ref} className={cn("flex items-center justify-center gap-1", className)} {...props}>
<ToggleGroupContext.Provider value={{ variant, size }}>{children}</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
));
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className,
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
});
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
export { ToggleGroup, ToggleGroupItem };

View File

@@ -0,0 +1,37 @@
import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3",
sm: "h-9 px-2.5",
lg: "h-11 px-5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root ref={ref} className={cn(toggleVariants({ variant, size, className }))} {...props} />
));
Toggle.displayName = TogglePrimitive.Root.displayName;
export { Toggle, toggleVariants };

View File

@@ -0,0 +1,28 @@
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@@ -0,0 +1,3 @@
import { useToast, toast } from "@/hooks/use-toast";
export { useToast, toast };

19
src/hooks/use-mobile.tsx Normal file
View File

@@ -0,0 +1,19 @@
import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
}

186
src/hooks/use-toast.ts Normal file
View File

@@ -0,0 +1,186 @@
import * as React from "react";
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & {
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const;
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString();
}
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
};
case "DISMISS_TOAST": {
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t,
),
};
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
};
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
},
},
});
return {
id: id,
dismiss,
update,
};
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
export { useToast, toast };

186
src/index.css Normal file
View File

@@ -0,0 +1,186 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Definition of the design system. All colors, gradients, fonts, etc should be defined here.
All colors MUST be HSL.
*/
@layer base {
:root {
/* OpenXpert brand colors - deep navy blue and light blue tones */
--background: 210 100% 99%;
--foreground: 205 100% 18%;
--card: 210 60% 98%;
--card-foreground: 205 100% 18%;
--popover: 210 60% 98%;
--popover-foreground: 205 100% 18%;
/* Deep navy blue from logo */
--primary: 205 100% 18%;
--primary-foreground: 210 100% 99%;
/* Light blue glass tones */
--secondary: 210 50% 95%;
--secondary-foreground: 205 100% 18%;
--muted: 210 40% 94%;
--muted-foreground: 205 30% 45%;
/* Accent blue for highlights */
--accent: 210 100% 88%;
--accent-foreground: 205 100% 18%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 100% 99%;
--border: 210 30% 90%;
--input: 210 30% 92%;
--ring: 205 100% 18%;
--radius: 1.25rem;
/* Glass effect variables */
--glass-bg: 210 60% 98%;
--glass-border: 210 40% 92%;
--glass-shadow: 205 20% 70%;
/* Gradient variables */
--gradient-start: 210 100% 99%;
--gradient-mid: 210 60% 97%;
--gradient-end: 210 50% 95%;
/* Glow effects */
--glow-primary: 205 100% 18%;
--glow-accent: 210 100% 88%;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 205 50% 8%;
--foreground: 210 100% 98%;
--card: 205 40% 12%;
--card-foreground: 210 100% 98%;
--popover: 205 40% 12%;
--popover-foreground: 210 100% 98%;
--primary: 210 100% 88%;
--primary-foreground: 205 100% 18%;
--secondary: 205 30% 20%;
--secondary-foreground: 210 100% 98%;
--muted: 205 30% 20%;
--muted-foreground: 210 40% 70%;
--accent: 210 80% 70%;
--accent-foreground: 205 100% 18%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 100% 98%;
--border: 205 30% 25%;
--input: 205 30% 25%;
--ring: 210 100% 88%;
--glass-bg: 205 40% 12%;
--glass-border: 205 30% 25%;
--glass-shadow: 205 20% 10%;
--gradient-start: 205 50% 8%;
--gradient-mid: 205 40% 12%;
--gradient-end: 205 35% 15%;
--glow-primary: 210 100% 88%;
--glow-accent: 210 80% 70%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
/* Custom glassmorphism utilities */
@layer utilities {
.glass-card {
background: linear-gradient(
135deg,
hsl(var(--glass-bg) / 0.7) 0%,
hsl(var(--glass-bg) / 0.5) 100%
);
backdrop-filter: blur(20px) saturate(180%);
border: 1px solid hsl(var(--glass-border) / 0.3);
box-shadow:
0 8px 32px hsl(var(--glass-shadow) / 0.1),
0 2px 8px hsl(var(--glass-shadow) / 0.05),
inset 0 1px 0 hsl(var(--glass-border) / 0.2);
}
.glass-strong {
background: linear-gradient(
135deg,
hsl(var(--glass-bg) / 0.9) 0%,
hsl(var(--glass-bg) / 0.8) 100%
);
backdrop-filter: blur(30px) saturate(200%);
border: 1px solid hsl(var(--glass-border) / 0.4);
box-shadow:
0 12px 48px hsl(var(--glass-shadow) / 0.15),
0 4px 12px hsl(var(--glass-shadow) / 0.08),
inset 0 1px 0 hsl(var(--glass-border) / 0.3);
}
.glow-effect {
box-shadow:
0 0 20px hsl(var(--glow-accent) / 0.3),
0 0 40px hsl(var(--glow-accent) / 0.15),
0 0 60px hsl(var(--glow-accent) / 0.08);
}
.gradient-bg {
background: linear-gradient(
135deg,
hsl(var(--gradient-start)) 0%,
hsl(var(--gradient-mid)) 50%,
hsl(var(--gradient-end)) 100%
);
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground antialiased;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
transition: background-color 0.3s ease, color 0.3s ease;
}
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
letter-spacing: -0.02em;
}
.nav-link {
@apply transition-colors duration-200 hover:text-primary;
}
}

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

5
src/main.tsx Normal file
View File

@@ -0,0 +1,5 @@
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
createRoot(document.getElementById("root")!).render(<App />);

97
src/pages/About.tsx Normal file
View File

@@ -0,0 +1,97 @@
import Navigation from "@/components/Navigation";
import Footer from "@/components/Footer";
import { Target, Eye, Award } from "lucide-react";
const About = () => {
return (
<div className="min-h-screen gradient-bg">
<Navigation />
<main className="pt-32 pb-20">
<div className="container mx-auto px-6">
{/* Header */}
<div className="text-center mb-20 animate-slide-in-up">
<h1 className="text-5xl md:text-6xl font-bold text-foreground mb-6 tracking-tight">
About
<span className="block mt-2 bg-gradient-to-r from-primary via-primary/80 to-accent bg-clip-text text-transparent">
OpenXpert Solutions
</span>
</h1>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
Pioneering technology solutions with precision, innovation, and unwavering commitment to excellence.
</p>
</div>
{/* Mission, Vision, Promise */}
<div className="grid md:grid-cols-3 gap-8 mb-20">
<div className="glass-card rounded-3xl p-8 animate-slide-in-up" style={{ animationDelay: "100ms" }}>
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-primary/10 to-accent/20 flex items-center justify-center mb-6">
<Target className="w-8 h-8 text-primary" />
</div>
<h3 className="text-2xl font-semibold text-foreground mb-4">Our Mission</h3>
<p className="text-muted-foreground leading-relaxed">
To empower businesses with innovative, secure, and scalable technology solutions that drive growth and digital transformation.
</p>
</div>
<div className="glass-card rounded-3xl p-8 animate-slide-in-up" style={{ animationDelay: "200ms" }}>
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-primary/10 to-accent/20 flex items-center justify-center mb-6">
<Eye className="w-8 h-8 text-primary" />
</div>
<h3 className="text-2xl font-semibold text-foreground mb-4">Our Vision</h3>
<p className="text-muted-foreground leading-relaxed">
To be the leading provider of enterprise technology solutions, recognized for excellence, innovation, and client success.
</p>
</div>
<div className="glass-card rounded-3xl p-8 animate-slide-in-up" style={{ animationDelay: "300ms" }}>
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-primary/10 to-accent/20 flex items-center justify-center mb-6">
<Award className="w-8 h-8 text-primary" />
</div>
<h3 className="text-2xl font-semibold text-foreground mb-4">Our Promise</h3>
<p className="text-muted-foreground leading-relaxed">
Deliver exceptional value through cutting-edge solutions, unwavering support, and partnerships built on trust and integrity.
</p>
</div>
</div>
{/* Story Section */}
<div className="glass-card rounded-3xl p-12 mb-20 animate-slide-in-up" style={{ animationDelay: "400ms" }}>
<h2 className="text-3xl font-bold text-foreground mb-6">Our Story</h2>
<div className="space-y-4 text-muted-foreground leading-relaxed">
<p>
Founded with a vision to bridge the gap between business needs and technology solutions, OpenXpert Solutions has grown into a trusted partner for enterprises seeking digital transformation.
</p>
<p>
Our team of experts brings decades of combined experience in enterprise systems, web development, and IT infrastructure management. We pride ourselves on delivering solutions that are not just technically sound, but also aligned with our clients' strategic objectives.
</p>
<p>
From ERP implementations to custom e-commerce platforms, from 24/7 monitoring to community-driven open-source projects, we approach every engagement with the same commitment to excellence and innovation.
</p>
</div>
</div>
{/* Values */}
<div className="text-center">
<h2 className="text-3xl font-bold text-foreground mb-12">Our Values</h2>
<div className="flex flex-wrap justify-center gap-4">
{["Security", "Innovation", "Integrity", "Excellence", "Collaboration", "Agility"].map((value, index) => (
<div
key={value}
className="glass-card px-8 py-4 rounded-full animate-slide-in-up"
style={{ animationDelay: `${500 + index * 100}ms` }}
>
<span className="text-foreground font-medium">{value}</span>
</div>
))}
</div>
</div>
</div>
</main>
<Footer />
</div>
);
};
export default About;

147
src/pages/Contact.tsx Normal file
View File

@@ -0,0 +1,147 @@
import Navigation from "@/components/Navigation";
import Footer from "@/components/Footer";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Mail, Phone, MapPin, Send } from "lucide-react";
const Contact = () => {
return (
<div className="min-h-screen gradient-bg">
<Navigation />
<main className="pt-32 pb-20">
<div className="container mx-auto px-6">
{/* Header */}
<div className="text-center mb-20 animate-slide-in-up">
<h1 className="text-5xl md:text-6xl font-bold text-foreground mb-6 tracking-tight">
Get In
<span className="block mt-2 bg-gradient-to-r from-primary via-primary/80 to-accent bg-clip-text text-transparent">
Touch
</span>
</h1>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
Let's discuss how we can help transform your business with innovative technology solutions.
</p>
</div>
<div className="grid lg:grid-cols-2 gap-12 max-w-6xl mx-auto">
{/* Contact Form */}
<div className="glass-card rounded-3xl p-8 animate-slide-in-up">
<h2 className="text-2xl font-semibold text-foreground mb-6">Send Us a Message</h2>
<form className="space-y-6">
<div className="grid md:grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-foreground mb-2 block">
First Name
</label>
<Input placeholder="John" className="glass-card" />
</div>
<div>
<label className="text-sm font-medium text-foreground mb-2 block">
Last Name
</label>
<Input placeholder="Doe" className="glass-card" />
</div>
</div>
<div>
<label className="text-sm font-medium text-foreground mb-2 block">
Email
</label>
<Input type="email" placeholder="john@example.com" className="glass-card" />
</div>
<div>
<label className="text-sm font-medium text-foreground mb-2 block">
Phone
</label>
<Input type="tel" placeholder="+971 XXX XXXX" className="glass-card" />
</div>
<div>
<label className="text-sm font-medium text-foreground mb-2 block">
Message
</label>
<Textarea
placeholder="Tell us about your project..."
className="glass-card min-h-32"
/>
</div>
<Button className="w-full glass-card hover:glow-effect">
<Send className="w-4 h-4 mr-2" />
Send Message
</Button>
</form>
</div>
{/* Contact Info */}
<div className="space-y-8">
<div className="glass-card rounded-3xl p-8 animate-slide-in-up" style={{ animationDelay: "100ms" }}>
<h2 className="text-2xl font-semibold text-foreground mb-6">Contact Information</h2>
<div className="space-y-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-primary/10 to-accent/20 flex items-center justify-center flex-shrink-0">
<Mail className="w-5 h-5 text-primary" />
</div>
<div>
<h3 className="font-semibold text-foreground mb-1">Email</h3>
<p className="text-muted-foreground">info@openxpert.com</p>
<p className="text-muted-foreground">support@openxpert.com</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-primary/10 to-accent/20 flex items-center justify-center flex-shrink-0">
<Phone className="w-5 h-5 text-primary" />
</div>
<div>
<h3 className="font-semibold text-foreground mb-1">Phone</h3>
<p className="text-muted-foreground">+971 XXX XXXX</p>
<p className="text-muted-foreground">+971 XXX XXXX</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-primary/10 to-accent/20 flex items-center justify-center flex-shrink-0">
<MapPin className="w-5 h-5 text-primary" />
</div>
<div>
<h3 className="font-semibold text-foreground mb-1">Office</h3>
<p className="text-muted-foreground">
Dubai, United Arab Emirates
</p>
</div>
</div>
</div>
</div>
<div className="glass-card rounded-3xl p-8 animate-slide-in-up" style={{ animationDelay: "200ms" }}>
<h3 className="text-xl font-semibold text-foreground mb-4">Business Hours</h3>
<div className="space-y-2 text-muted-foreground">
<div className="flex justify-between">
<span>Monday - Friday</span>
<span className="font-medium">9:00 AM - 6:00 PM</span>
</div>
<div className="flex justify-between">
<span>Saturday</span>
<span className="font-medium">10:00 AM - 4:00 PM</span>
</div>
<div className="flex justify-between">
<span>Sunday</span>
<span className="font-medium">Closed</span>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<Footer />
</div>
);
};
export default Contact;

146
src/pages/Index.tsx Normal file
View File

@@ -0,0 +1,146 @@
import { Server, ShoppingCart, Shield, Wrench, Users, Globe, ArrowRight, Sparkles } from "lucide-react";
import { ServiceCard } from "@/components/ServiceCard";
import { FloatingShape } from "@/components/FloatingShape";
import { ParticleField } from "@/components/ParticleField";
import { Button } from "@/components/ui/button";
import Navigation from "@/components/Navigation";
import Footer from "@/components/Footer";
import { Link } from "react-router-dom";
const Index = () => {
const services = [{
icon: Server,
title: "ERP Systems",
description: "Enterprise Resource Planning solutions tailored to streamline your business operations with cutting-edge technology.",
link: "/services/erp"
}, {
icon: ShoppingCart,
title: "Custom E-Commerce",
description: "Bespoke e-commerce platforms designed to deliver exceptional user experiences and drive conversions.",
link: "/services/ecommerce"
}, {
icon: Shield,
title: "RMM Services",
description: "Remote Monitoring and Management services ensuring your IT infrastructure operates at peak performance.",
link: "/services/rmm"
}, {
icon: Wrench,
title: "AMC Support",
description: "Comprehensive Annual Maintenance Contracts providing reliable, ongoing technical support and system optimization.",
link: "/services/amc"
}, {
icon: Users,
title: "Community Projects",
description: "Innovative technology initiatives that empower communities and create lasting social impact.",
link: "/services/community"
}, {
icon: Globe,
title: "Web Development",
description: "Premium web solutions combining elegant design with powerful functionality for modern digital experiences.",
link: "/services/web"
}];
return <div className="min-h-screen gradient-bg relative overflow-hidden">
<Navigation />
{/* Particle field background */}
<div className="fixed inset-0 z-0">
<ParticleField />
</div>
{/* Floating decorative shapes */}
<FloatingShape className="top-20 left-10 w-96 h-96 bg-accent/30" delay={0} />
<FloatingShape className="top-40 right-20 w-80 h-80 bg-primary/10" delay={2} />
<FloatingShape className="bottom-20 left-1/3 w-72 h-72 bg-accent/20" delay={4} />
{/* Light streaks */}
<div className="fixed top-0 left-0 w-full h-2 bg-gradient-to-r from-transparent via-primary/30 to-transparent animate-[slide-in-right_3s_ease-in-out_infinite]" />
<div className="fixed top-20 left-0 w-full h-1 bg-gradient-to-r from-transparent via-accent/20 to-transparent animate-[slide-in-right_4s_ease-in-out_infinite]" style={{
animationDelay: "1s"
}} />
{/* Main content */}
<div className="relative z-10 container mx-auto px-6 pt-32 pb-20">
{/* Hero Section */}
<header className="text-center mb-32 animate-slide-in-up relative">
{/* Floating sparkle effects */}
<Sparkles className="absolute top-0 left-1/4 w-8 h-8 text-primary/30 animate-glow-pulse" style={{
animationDelay: "0s"
}} />
<Sparkles className="absolute top-10 right-1/3 w-6 h-6 text-accent/40 animate-glow-pulse" style={{
animationDelay: "1s"
}} />
<Sparkles className="absolute bottom-10 left-1/3 w-7 h-7 text-primary/20 animate-glow-pulse" style={{
animationDelay: "2s"
}} />
<div className="inline-block mb-8 glass-card px-4 py-2 rounded-full animate-scale-in">
<span className="text-sm text-primary font-medium">Trusted by 100+ Enterprise Clients</span>
</div>
<h1 className="text-6xl md:text-7xl lg:text-8xl font-bold text-foreground mb-8 tracking-tight leading-none">
Precision. Innovation.
<span className="block mt-3 bg-gradient-to-r from-primary via-primary/80 to-accent bg-clip-text text-transparent animate-glow-pulse">
Open Solutions.
</span>
</h1>
<p className="text-xl md:text-2xl text-muted-foreground max-w-3xl mx-auto leading-relaxed mb-12">
Empowering businesses with secure, future-ready technology that drives growth and digital transformation.
</p>
{/* CTA Buttons */}
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
<Link to="/services/erp">
<Button size="lg" className="glass-card hover:glow-effect px-8 py-6 text-lg group relative overflow-hidden bg-[#00375d]">
<span className="relative z-10 flex items-center">
Explore Services
<ArrowRight className="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform" />
</span>
<div className="absolute inset-0 bg-gradient-to-r from-primary/20 to-accent/20 translate-x-[-100%] group-hover:translate-x-0 transition-transform duration-500" />
</Button>
</Link>
<Link to="/contact">
<Button size="lg" variant="outline" className="glass-card hover:glow-effect px-8 py-6 text-lg group">
<span className="flex items-center">
Talk to an Expert
</span>
</Button>
</Link>
</div>
</header>
{/* Services grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto">
{services.map((service, index) => <Link key={service.title} to={service.link}>
<ServiceCard icon={service.icon} title={service.title} description={service.description} delay={index * 100} />
</Link>)}
</div>
{/* Trust Badges */}
<div className="mt-32 text-center animate-slide-in-up" style={{
animationDelay: "600ms"
}}>
<div className="glass-card inline-block px-12 py-6 rounded-full">
<p className="text-sm text-muted-foreground font-medium flex items-center gap-6">
<span className="flex items-center gap-2">
<Shield className="w-4 h-4 text-primary" />
Secure
</span>
<span className="text-border"></span>
<span className="flex items-center gap-2">
<Globe className="w-4 h-4 text-primary" />
Innovative
</span>
<span className="text-border"></span>
<span className="flex items-center gap-2">
<Server className="w-4 h-4 text-primary" />
Trusted
</span>
</p>
</div>
</div>
</div>
<Footer />
</div>;
};
export default Index;

24
src/pages/NotFound.tsx Normal file
View File

@@ -0,0 +1,24 @@
import { useLocation } from "react-router-dom";
import { useEffect } from "react";
const NotFound = () => {
const location = useLocation();
useEffect(() => {
console.error("404 Error: User attempted to access non-existent route:", location.pathname);
}, [location.pathname]);
return (
<div className="flex min-h-screen items-center justify-center bg-muted">
<div className="text-center">
<h1 className="mb-4 text-4xl font-bold">404</h1>
<p className="mb-4 text-xl text-muted-foreground">Oops! Page not found</p>
<a href="/" className="text-primary underline hover:text-primary/90">
Return to Home
</a>
</div>
</div>
);
};
export default NotFound;

115
src/pages/Portfolio.tsx Normal file
View File

@@ -0,0 +1,115 @@
import Navigation from "@/components/Navigation";
import Footer from "@/components/Footer";
import { ExternalLink } from "lucide-react";
const Portfolio = () => {
const projects = [
{
title: "Enterprise ERP Solution",
category: "ERP Systems",
description: "Comprehensive resource planning system for manufacturing enterprise",
image: "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&q=80",
},
{
title: "E-Commerce Platform",
category: "E-Commerce",
description: "High-performance online retail platform with advanced features",
image: "https://images.unsplash.com/photo-1557821552-17105176677c?w=800&q=80",
},
{
title: "IT Infrastructure Monitoring",
category: "RMM Services",
description: "24/7 monitoring solution for distributed enterprise infrastructure",
image: "https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=800&q=80",
},
{
title: "Corporate Website Redesign",
category: "Web Development",
description: "Modern, responsive website with enhanced user experience",
image: "https://images.unsplash.com/photo-1487017159836-4e23ece2e4cf?w=800&q=80",
},
{
title: "Community Tech Initiative",
category: "Community Projects",
description: "Open-source platform connecting local tech communities",
image: "https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800&q=80",
},
{
title: "Managed Support System",
category: "AMC Support",
description: "Comprehensive maintenance and support infrastructure",
image: "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&q=80",
},
];
return (
<div className="min-h-screen gradient-bg">
<Navigation />
<main className="pt-32 pb-20">
<div className="container mx-auto px-6">
{/* Header */}
<div className="text-center mb-20 animate-slide-in-up">
<h1 className="text-5xl md:text-6xl font-bold text-foreground mb-6 tracking-tight">
Our
<span className="block mt-2 bg-gradient-to-r from-primary via-primary/80 to-accent bg-clip-text text-transparent">
Portfolio
</span>
</h1>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
Explore our latest projects and success stories across diverse industries and technologies.
</p>
</div>
{/* Projects Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{projects.map((project, index) => (
<div
key={project.title}
className="group glass-card rounded-3xl overflow-hidden hover:scale-105 transition-all duration-500 animate-slide-in-up"
style={{ animationDelay: `${index * 100}ms` }}
>
{/* Image */}
<div className="relative h-56 overflow-hidden">
<img
src={project.image}
alt={project.title}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-gradient-to-t from-background/80 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
{/* Hover Icon */}
<div className="absolute top-4 right-4 w-10 h-10 rounded-full glass-strong flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<ExternalLink className="w-5 h-5 text-primary" />
</div>
</div>
{/* Content */}
<div className="p-6">
<span className="text-xs font-medium text-primary uppercase tracking-wider">
{project.category}
</span>
<h3 className="text-xl font-semibold text-foreground mt-2 mb-3">
{project.title}
</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
{project.description}
</p>
</div>
{/* Shimmer Effect */}
<div className="absolute top-0 left-0 w-full h-full rounded-3xl overflow-hidden pointer-events-none">
<div className="absolute top-0 left-[-100%] w-full h-full bg-gradient-to-r from-transparent via-primary/5 to-transparent group-hover:left-[100%] transition-all duration-1000" />
</div>
</div>
))}
</div>
</div>
</main>
<Footer />
</div>
);
};
export default Portfolio;

View File

@@ -0,0 +1,412 @@
import { useState } from "react";
import { Wrench, Clock, Phone, Shield, TrendingUp, Users, CheckCircle, Zap, HeadphonesIcon, ArrowRight, Check, Star } from "lucide-react";
import Navigation from "@/components/Navigation";
import Footer from "@/components/Footer";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Link } from "react-router-dom";
const AmcSupport = () => {
const [selectedTier, setSelectedTier] = useState("standard");
const [showSuccess, setShowSuccess] = useState(false);
const features = [
{
icon: Clock,
title: "Rapid Response",
description: "Get help when you need it most",
detail: "4-hour response time"
},
{
icon: Phone,
title: "24/7 Support",
description: "Round-the-clock technical assistance",
detail: "Always available"
},
{
icon: Wrench,
title: "Preventive Maintenance",
description: "Regular checkups to prevent issues",
detail: "Monthly inspections"
},
{
icon: Shield,
title: "Priority Service",
description: "Jump to the front of the queue",
detail: "VIP treatment"
},
{
icon: Users,
title: "Dedicated Team",
description: "Your own technical experts",
detail: "Consistent support"
},
{
icon: Zap,
title: "Emergency Response",
description: "Critical issue resolution",
detail: "1-hour for emergencies"
}
];
const tiers = [
{
id: "basic",
name: "Basic Care",
price: "AED 1,500/month",
responseTime: "8 hours",
coverage: "Business hours (9am-6pm)",
features: [
"Email and phone support",
"Quarterly maintenance",
"Remote assistance",
"Basic troubleshooting",
"System health reports"
]
},
{
id: "standard",
name: "Standard Care",
price: "AED 3,500/month",
responseTime: "4 hours",
coverage: "Extended hours (7am-10pm)",
popular: true,
features: [
"Everything in Basic",
"Monthly on-site visits",
"Priority phone support",
"Preventive maintenance",
"Hardware diagnostics",
"Software updates",
"Performance optimization"
]
},
{
id: "premium",
name: "Premium Care",
price: "AED 7,500/month",
responseTime: "1 hour",
coverage: "24/7 availability",
features: [
"Everything in Standard",
"Dedicated account manager",
"Weekly system checks",
"Emergency on-site support",
"Unlimited support tickets",
"Custom SLA agreements",
"Spare parts included"
]
}
];
const benefits = [
{
title: "Reduced Costs",
value: "40%",
description: "Lower than ad-hoc IT support"
},
{
title: "System Uptime",
value: "99.5%",
description: "Keep your business running"
},
{
title: "Issue Prevention",
value: "80%",
description: "Problems caught early"
},
{
title: "Response Speed",
value: "5x",
description: "Faster than without AMC"
}
];
const testimonials = [
{
company: "TechCorp Middle East",
role: "IT Manager",
quote: "The AMC support has been invaluable. We've had zero critical downtime in 18 months.",
rating: 5
},
{
company: "Dubai Retail Group",
role: "Operations Director",
quote: "Response times are incredible. Issues are resolved before they impact our business.",
rating: 5
},
{
company: "Emirates Finance",
role: "CTO",
quote: "Best investment we made. The preventive maintenance alone saved us thousands.",
rating: 5
}
];
const responseGuarantees = [
{ tier: "Basic", response: "8 hours", resolution: "48 hours", onsite: "5 business days" },
{ tier: "Standard", response: "4 hours", resolution: "24 hours", onsite: "2 business days" },
{ tier: "Premium", response: "1 hour", resolution: "8 hours", onsite: "4 hours" }
];
return (
<div className="min-h-screen gradient-bg">
<Navigation />
{/* Hero Section */}
<section className="relative container mx-auto px-6 pt-32 pb-20">
<div className="text-center mb-16 animate-slide-in-up">
<h1 className="text-5xl md:text-6xl font-bold text-foreground mb-6">
AMC Support Services
</h1>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto mb-4">
Comprehensive IT support that keeps you running.
</p>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
Annual Maintenance Contracts designed to prevent problems before they happen.
</p>
</div>
{/* Animated Support Visual */}
<div className="glass-strong rounded-3xl p-8 max-w-5xl mx-auto mb-20 animate-slide-in-up" style={{ animationDelay: "200ms" }}>
<div className="aspect-video glass-card rounded-2xl flex items-center justify-center relative overflow-hidden">
<div className="relative">
<HeadphonesIcon className="w-24 h-24 text-primary/30 animate-glow-pulse" />
<div className="absolute -top-4 -right-4">
<div className="glass-card px-3 py-1 rounded-full text-xs text-primary font-bold animate-scale-in">
Available Now
</div>
</div>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6">
{[
{ label: "Avg Response", value: "2.5 hrs" },
{ label: "Tickets Resolved", value: "98%" },
{ label: "Client Satisfaction", value: "4.9/5" },
{ label: "Uptime Guarantee", value: "99.5%" }
].map((stat, i) => (
<div key={i} className="glass-card p-4 rounded-2xl text-center animate-scale-in" style={{ animationDelay: `${400 + i * 100}ms` }}>
<div className="text-2xl font-bold text-primary mb-1">{stat.value}</div>
<div className="text-sm text-muted-foreground">{stat.label}</div>
</div>
))}
</div>
</div>
</section>
{/* Features Grid */}
<section className="container mx-auto px-6 py-20">
<h2 className="text-4xl font-bold text-center text-foreground mb-4">
Complete IT Care & Support
</h2>
<p className="text-center text-muted-foreground mb-16 max-w-2xl mx-auto">
Everything you need to keep your IT infrastructure healthy
</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-7xl mx-auto">
{features.map((feature, index) => (
<Card
key={index}
className="group glass-card p-8 rounded-3xl transition-all duration-500 hover:scale-105 hover:glow-effect cursor-pointer"
style={{ animationDelay: `${index * 50}ms` }}
>
<div className="mb-6">
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-primary/10 to-accent/20 flex items-center justify-center">
<feature.icon className="w-7 h-7 text-primary" />
</div>
</div>
<h3 className="text-xl font-semibold text-foreground mb-2">{feature.title}</h3>
<p className="text-sm text-muted-foreground mb-4">{feature.description}</p>
<div className="text-xs text-primary font-bold">{feature.detail}</div>
</Card>
))}
</div>
</section>
{/* Response Time Guarantees */}
<section className="container mx-auto px-6 py-20">
<h2 className="text-4xl font-bold text-center text-foreground mb-4">
Our Response Time Guarantees
</h2>
<p className="text-center text-muted-foreground mb-16 max-w-2xl mx-auto">
Clear SLAs with guaranteed response times
</p>
<div className="glass-strong rounded-3xl p-8 max-w-5xl mx-auto overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-glass-border">
<th className="text-left py-4 px-4 text-foreground font-semibold">Tier</th>
<th className="text-center py-4 px-4 text-foreground font-semibold">Response Time</th>
<th className="text-center py-4 px-4 text-foreground font-semibold">Resolution Target</th>
<th className="text-center py-4 px-4 text-foreground font-semibold">On-Site Visit</th>
</tr>
</thead>
<tbody>
{responseGuarantees.map((guarantee, index) => (
<tr
key={index}
className={`border-b border-glass-border/50 hover:bg-accent/10 transition-colors ${
guarantee.tier === "Standard" ? "bg-primary/5" : ""
}`}
>
<td className="py-4 px-4 text-foreground font-medium">{guarantee.tier}</td>
<td className="text-center py-4 px-4">
<div className="inline-flex items-center gap-2 glass-card px-4 py-2 rounded-full">
<Clock className="w-4 h-4 text-primary" />
<span className="text-primary font-bold">{guarantee.response}</span>
</div>
</td>
<td className="text-center py-4 px-4 text-muted-foreground">{guarantee.resolution}</td>
<td className="text-center py-4 px-4 text-muted-foreground">{guarantee.onsite}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
{/* Benefits Stats */}
<section className="container mx-auto px-6 py-20">
<h2 className="text-4xl font-bold text-center text-foreground mb-16">
Why Choose AMC Support?
</h2>
<div className="grid md:grid-cols-4 gap-6 max-w-6xl mx-auto">
{benefits.map((benefit, index) => (
<Card
key={index}
className="glass-card p-8 rounded-3xl text-center transition-all duration-500 hover:scale-105 hover:glow-effect"
>
<div className="text-5xl font-bold text-primary mb-2 animate-glow-pulse">
{benefit.value}
</div>
<h3 className="text-lg font-semibold text-foreground mb-2">{benefit.title}</h3>
<p className="text-sm text-muted-foreground">{benefit.description}</p>
</Card>
))}
</div>
</section>
{/* Pricing Tiers */}
<section className="container mx-auto px-6 py-20">
<h2 className="text-4xl font-bold text-center text-foreground mb-4">
Choose Your Support Plan
</h2>
<p className="text-center text-muted-foreground mb-16 max-w-2xl mx-auto">
Flexible contracts with no hidden fees
</p>
<div className="grid md:grid-cols-3 gap-6 max-w-6xl mx-auto">
{tiers.map((tier, index) => (
<Card
key={index}
className={`glass-card p-8 rounded-3xl transition-all duration-500 hover:scale-105 cursor-pointer ${
tier.popular ? "ring-2 ring-primary glow-effect" : ""
} ${selectedTier === tier.id ? "ring-2 ring-primary" : ""}`}
onClick={() => setSelectedTier(tier.id)}
>
{tier.popular && (
<div className="inline-block px-4 py-1 rounded-full bg-primary text-primary-foreground text-xs font-bold mb-4">
Most Popular
</div>
)}
<h3 className="text-2xl font-bold text-foreground mb-2">{tier.name}</h3>
<div className="text-3xl font-bold text-primary mb-2">{tier.price}</div>
<div className="space-y-2 mb-6">
<div className="flex items-center gap-2 text-sm">
<Clock className="w-4 h-4 text-primary" />
<span className="text-muted-foreground">{tier.responseTime} response</span>
</div>
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-primary" />
<span className="text-muted-foreground">{tier.coverage}</span>
</div>
</div>
<ul className="space-y-3 mb-8">
{tier.features.map((feature, i) => (
<li key={i} className="flex items-start gap-2 text-sm text-muted-foreground">
<Check className="w-4 h-4 text-primary mt-0.5 flex-shrink-0" />
<span>{feature}</span>
</li>
))}
</ul>
<Button className="w-full glass-card hover:glow-effect">
Select Plan
</Button>
</Card>
))}
</div>
</section>
{/* Testimonials */}
<section className="container mx-auto px-6 py-20">
<h2 className="text-4xl font-bold text-center text-foreground mb-16">
What Our Clients Say
</h2>
<div className="grid md:grid-cols-3 gap-6 max-w-6xl mx-auto">
{testimonials.map((testimonial, index) => (
<Card
key={index}
className="glass-card p-8 rounded-3xl transition-all duration-500 hover:scale-105 hover:glow-effect"
>
<div className="flex gap-1 mb-4">
{[...Array(testimonial.rating)].map((_, i) => (
<Star key={i} className="w-4 h-4 fill-primary text-primary" />
))}
</div>
<p className="text-muted-foreground mb-6 italic">"{testimonial.quote}"</p>
<div>
<div className="font-semibold text-foreground">{testimonial.company}</div>
<div className="text-sm text-muted-foreground">{testimonial.role}</div>
</div>
</Card>
))}
</div>
</section>
{/* CTA Section */}
<section className="container mx-auto px-6 py-20">
<div className="glass-strong rounded-3xl p-12 max-w-4xl mx-auto text-center">
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4">
Ready to Get Protected?
</h2>
<p className="text-lg text-muted-foreground mb-8 max-w-2xl mx-auto">
Start your AMC contract today and enjoy peace of mind
</p>
{!showSuccess ? (
<Button
size="lg"
className="glass-card hover:glow-effect px-8 py-6 text-lg group"
onClick={() => setShowSuccess(true)}
>
<Wrench className="mr-2 w-5 h-5" />
Get Your Quote
<ArrowRight className="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform" />
</Button>
) : (
<div className="glass-card p-8 rounded-2xl animate-scale-in">
<Check className="w-16 h-16 text-primary mx-auto mb-4 animate-glow-pulse" />
<h3 className="text-xl font-semibold text-foreground mb-2">Request Received!</h3>
<p className="text-muted-foreground mb-6">
Our support team will contact you within 24 hours with a customized quote.
</p>
<Link to="/contact">
<Button variant="outline" className="glass-card">
Contact Us Directly
</Button>
</Link>
</div>
)}
</div>
</section>
<Footer />
</div>
);
};
export default AmcSupport;

View File

@@ -0,0 +1,326 @@
import { useState } from "react";
import { ShoppingCart, Zap, CreditCard, Package, TrendingUp, Smartphone, Search, Shield, ArrowRight, Check, Play } from "lucide-react";
import Navigation from "@/components/Navigation";
import Footer from "@/components/Footer";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Link } from "react-router-dom";
const ECommerce = () => {
const [selectedTheme, setSelectedTheme] = useState("modern");
const [showSuccess, setShowSuccess] = useState(false);
const features = [
{
icon: Zap,
title: "Lightning Fast",
description: "Optimized for speed with 95+ PageSpeed scores",
stat: "<1s load time"
},
{
icon: Smartphone,
title: "Mobile-First Design",
description: "Responsive layouts that convert on any device",
stat: "60% mobile traffic"
},
{
icon: CreditCard,
title: "Payment Integration",
description: "Multiple payment gateways, secure checkout",
stat: "20+ gateways"
},
{
icon: Search,
title: "SEO Optimized",
description: "Built for search engines from the ground up",
stat: "Top 3 rankings"
},
{
icon: Package,
title: "Inventory Sync",
description: "Real-time stock management and alerts",
stat: "99.9% accuracy"
},
{
icon: Shield,
title: "Secure & Compliant",
description: "PCI-DSS compliant, SSL encrypted",
stat: "Bank-level security"
}
];
const themes = [
{ id: "modern", name: "Modern", color: "from-blue-500 to-purple-500" },
{ id: "minimal", name: "Minimal", color: "from-gray-800 to-gray-600" },
{ id: "vibrant", name: "Vibrant", color: "from-pink-500 to-orange-500" }
];
const packages = [
{
name: "Starter Store",
price: "Starting at AED 15,000",
features: [
"Up to 100 products",
"1 payment gateway",
"Mobile responsive",
"Basic SEO setup",
"Contact form",
"3 months support"
]
},
{
name: "Business Store",
price: "Starting at AED 30,000",
popular: true,
features: [
"Unlimited products",
"Multiple payment gateways",
"Advanced SEO",
"Analytics dashboard",
"Cart recovery",
"1 year support",
"Custom features"
]
},
{
name: "Enterprise",
price: "Custom pricing",
features: [
"Everything in Business",
"Multi-vendor support",
"Custom integrations",
"Dedicated manager",
"Priority support",
"Advanced analytics",
"Scalable infrastructure"
]
}
];
const conversionFeatures = [
{ label: "One-Page Checkout", increase: "35% higher conversion" },
{ label: "Smart Product Filters", increase: "40% more engagement" },
{ label: "Cart Abandonment Recovery", increase: "25% recovered sales" },
{ label: "Wishlist & Compare", increase: "50% return visitors" }
];
return (
<div className="min-h-screen gradient-bg">
<Navigation />
{/* Hero Section */}
<section className="relative container mx-auto px-6 pt-32 pb-20">
<div className="text-center mb-16 animate-slide-in-up">
<h1 className="text-5xl md:text-6xl font-bold text-foreground mb-6">
Custom E-Commerce Solutions
</h1>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto mb-4">
Beautiful stores that convert visitors into customers.
</p>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
Fast, secure, and built to sell. From concept to launch, we create online stores that drive revenue.
</p>
</div>
{/* Animated Store Preview */}
<div className="glass-strong rounded-3xl p-8 max-w-5xl mx-auto mb-20 animate-slide-in-up" style={{ animationDelay: "200ms" }}>
<div className="aspect-video glass-card rounded-2xl flex items-center justify-center relative overflow-hidden">
<ShoppingCart className="w-24 h-24 text-primary/30 animate-glow-pulse" />
<div className="absolute top-4 right-4">
<div className="glass-card px-4 py-2 rounded-full text-sm text-primary font-medium animate-scale-in">
Live Demo Available
</div>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6">
{[
{ label: "Products", value: "10K+" },
{ label: "Orders/day", value: "500+" },
{ label: "Conversion", value: "3.8%" },
{ label: "Avg. Order", value: "AED 450" }
].map((stat, i) => (
<div key={i} className="glass-card p-4 rounded-2xl text-center animate-scale-in" style={{ animationDelay: `${400 + i * 100}ms` }}>
<div className="text-2xl font-bold text-primary mb-1">{stat.value}</div>
<div className="text-sm text-muted-foreground">{stat.label}</div>
</div>
))}
</div>
</div>
</section>
{/* Features Grid */}
<section className="container mx-auto px-6 py-20">
<h2 className="text-4xl font-bold text-center text-foreground mb-4">
Everything You Need to Sell Online
</h2>
<p className="text-center text-muted-foreground mb-16 max-w-2xl mx-auto">
Powerful features that drive sales and delight customers
</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-7xl mx-auto">
{features.map((feature, index) => (
<Card
key={index}
className="group glass-card p-8 rounded-3xl transition-all duration-500 hover:scale-105 hover:glow-effect cursor-pointer"
style={{ animationDelay: `${index * 50}ms` }}
>
<div className="mb-6">
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-primary/10 to-accent/20 flex items-center justify-center">
<feature.icon className="w-7 h-7 text-primary" />
</div>
</div>
<h3 className="text-xl font-semibold text-foreground mb-2">{feature.title}</h3>
<p className="text-sm text-muted-foreground mb-4">{feature.description}</p>
<div className="text-xs text-primary font-bold">{feature.stat}</div>
</Card>
))}
</div>
</section>
{/* Theme Playground */}
<section className="container mx-auto px-6 py-20">
<div className="glass-strong rounded-3xl p-8 md:p-12 max-w-5xl mx-auto">
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4 text-center">
Live Theme Customizer
</h2>
<p className="text-center text-muted-foreground mb-12">
See your store come to life with real-time theme previews
</p>
<div className="grid md:grid-cols-3 gap-4 mb-8">
{themes.map((theme) => (
<button
key={theme.id}
onClick={() => setSelectedTheme(theme.id)}
className={`glass-card p-6 rounded-2xl transition-all duration-300 ${
selectedTheme === theme.id ? "ring-2 ring-primary glow-effect" : ""
}`}
>
<div className={`h-20 rounded-xl bg-gradient-to-r ${theme.color} mb-4`} />
<div className="text-foreground font-medium">{theme.name}</div>
</button>
))}
</div>
<div className="glass-card p-8 rounded-2xl">
<div className="text-center">
<Play className="w-12 h-12 text-primary mx-auto mb-4 animate-glow-pulse" />
<p className="text-muted-foreground">
Selected Theme: <span className="text-primary font-semibold">{themes.find(t => t.id === selectedTheme)?.name}</span>
</p>
</div>
</div>
</div>
</section>
{/* Conversion Features */}
<section className="container mx-auto px-6 py-20">
<h2 className="text-4xl font-bold text-center text-foreground mb-4">
Conversion-Optimized Features
</h2>
<p className="text-center text-muted-foreground mb-16 max-w-2xl mx-auto">
Every feature is designed to increase sales
</p>
<div className="max-w-4xl mx-auto space-y-4">
{conversionFeatures.map((feature, index) => (
<div
key={index}
className="glass-card p-6 rounded-2xl flex items-center justify-between hover:glow-effect transition-all duration-300 animate-slide-in-up"
style={{ animationDelay: `${index * 100}ms` }}
>
<div className="flex items-center gap-4">
<Check className="w-6 h-6 text-primary" />
<span className="text-lg text-foreground font-medium">{feature.label}</span>
</div>
<div className="text-primary font-bold">{feature.increase}</div>
</div>
))}
</div>
</section>
{/* Pricing Packages */}
<section className="container mx-auto px-6 py-20">
<h2 className="text-4xl font-bold text-center text-foreground mb-4">
Choose Your Package
</h2>
<p className="text-center text-muted-foreground mb-16 max-w-2xl mx-auto">
Transparent pricing with no hidden fees
</p>
<div className="grid md:grid-cols-3 gap-6 max-w-6xl mx-auto">
{packages.map((pkg, index) => (
<Card
key={index}
className={`glass-card p-8 rounded-3xl transition-all duration-500 hover:scale-105 ${
pkg.popular ? "ring-2 ring-primary glow-effect" : ""
}`}
>
{pkg.popular && (
<div className="inline-block px-4 py-1 rounded-full bg-primary text-primary-foreground text-xs font-bold mb-4">
Most Popular
</div>
)}
<h3 className="text-2xl font-bold text-foreground mb-2">{pkg.name}</h3>
<div className="text-3xl font-bold text-primary mb-6">{pkg.price}</div>
<ul className="space-y-3 mb-8">
{pkg.features.map((feature, i) => (
<li key={i} className="flex items-start gap-2 text-sm text-muted-foreground">
<Check className="w-4 h-4 text-primary mt-0.5 flex-shrink-0" />
<span>{feature}</span>
</li>
))}
</ul>
<Button className="w-full glass-card hover:glow-effect">
Get Started
</Button>
</Card>
))}
</div>
</section>
{/* CTA Section */}
<section className="container mx-auto px-6 py-20">
<div className="glass-strong rounded-3xl p-12 max-w-4xl mx-auto text-center">
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4">
Ready to Launch Your Store?
</h2>
<p className="text-lg text-muted-foreground mb-8 max-w-2xl mx-auto">
Let's build an e-commerce experience that your customers will love
</p>
{!showSuccess ? (
<Button
size="lg"
className="glass-card hover:glow-effect px-8 py-6 text-lg group"
onClick={() => setShowSuccess(true)}
>
<ShoppingCart className="mr-2 w-5 h-5" />
Start Your Project
<ArrowRight className="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform" />
</Button>
) : (
<div className="glass-card p-8 rounded-2xl animate-scale-in">
<Check className="w-16 h-16 text-primary mx-auto mb-4 animate-glow-pulse" />
<h3 className="text-xl font-semibold text-foreground mb-2">Request Received!</h3>
<p className="text-muted-foreground mb-6">
Our e-commerce specialists will contact you within 24 hours.
</p>
<Link to="/contact">
<Button variant="outline" className="glass-card">
Contact Us Directly
</Button>
</Link>
</div>
)}
</div>
</section>
<Footer />
</div>
);
};
export default ECommerce;

View File

@@ -0,0 +1,311 @@
import { useState } from "react";
import { Server, Database, Users, BarChart3, Package, DollarSign, Factory, FileText, ArrowRight, Check, X, Calendar } from "lucide-react";
import Navigation from "@/components/Navigation";
import Footer from "@/components/Footer";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Link } from "react-router-dom";
const ErpSystems = () => {
const [employees, setEmployees] = useState(50);
const [departments, setDepartments] = useState(5);
const [showCalendar, setShowCalendar] = useState(false);
const calculateROI = () => {
const savings = employees * 150 * 12;
const timesSaved = employees * 2.5;
const productivity = 35;
return { savings, timesSaved, productivity };
};
const roi = calculateROI();
const modules = [
{
icon: Package,
title: "Inventory Management",
description: "Real-time stock tracking, automated reordering, multi-warehouse support",
benefits: "Reduce stock-outs by 85%"
},
{
icon: Users,
title: "Human Resources",
description: "Payroll, attendance, leave management, performance tracking",
benefits: "Save 20 hours/week on HR tasks"
},
{
icon: DollarSign,
title: "Finance & Accounting",
description: "General ledger, accounts payable/receivable, financial reporting",
benefits: "Close books 10x faster"
},
{
icon: BarChart3,
title: "CRM & Sales",
description: "Lead tracking, opportunity management, sales forecasting",
benefits: "Increase conversions by 40%"
},
{
icon: Package,
title: "Purchase Management",
description: "Vendor management, purchase orders, procurement workflows",
benefits: "Reduce purchasing costs by 15%"
},
{
icon: Factory,
title: "Manufacturing",
description: "Production planning, bill of materials, shop floor control",
benefits: "Optimize production by 30%"
},
{
icon: FileText,
title: "Reporting & Analytics",
description: "Real-time dashboards, custom reports, predictive analytics",
benefits: "Make data-driven decisions"
},
{
icon: Database,
title: "Data Integration",
description: "Connect all your systems, APIs, automated data sync",
benefits: "Eliminate manual data entry"
}
];
const features = [
{ name: "Cloud-Based Architecture", us: true, them: false },
{ name: "Real-Time Analytics", us: true, them: true },
{ name: "Mobile Access", us: true, them: false },
{ name: "Customizable Workflows", us: true, them: true },
{ name: "24/7 Support", us: true, them: false },
{ name: "Automated Backups", us: true, them: true },
{ name: "Multi-Currency Support", us: true, them: false },
{ name: "API Integration", us: true, them: false },
{ name: "Free Updates", us: true, them: false },
];
return (
<div className="min-h-screen gradient-bg">
<Navigation />
{/* Hero Section with Animated Dashboard */}
<section className="relative container mx-auto px-6 pt-32 pb-20">
<div className="text-center mb-16 animate-slide-in-up">
<h1 className="text-5xl md:text-6xl font-bold text-foreground mb-6">
ERP Systems
</h1>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto mb-4">
Powerful systems made wonderfully simple.
</p>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
Transform your business with intelligent automation, real-time insights, and seamless integration.
</p>
</div>
{/* Animated Dashboard Preview */}
<div className="glass-strong rounded-3xl p-8 max-w-5xl mx-auto mb-20 animate-slide-in-up" style={{ animationDelay: "200ms" }}>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
{[
{ label: "Revenue", value: "$2.4M", trend: "+12%" },
{ label: "Orders", value: "1,847", trend: "+8%" },
{ label: "Inventory", value: "94%", trend: "+3%" },
{ label: "Efficiency", value: "87%", trend: "+15%" }
].map((stat, i) => (
<div key={i} className="glass-card p-4 rounded-2xl animate-scale-in" style={{ animationDelay: `${400 + i * 100}ms` }}>
<div className="text-sm text-muted-foreground mb-1">{stat.label}</div>
<div className="text-2xl font-bold text-foreground mb-1">{stat.value}</div>
<div className="text-xs text-primary">{stat.trend}</div>
</div>
))}
</div>
<div className="h-48 glass-card rounded-2xl flex items-center justify-center">
<BarChart3 className="w-16 h-16 text-primary/30 animate-glow-pulse" />
</div>
</div>
</section>
{/* Modules Section */}
<section className="container mx-auto px-6 py-20">
<h2 className="text-4xl font-bold text-center text-foreground mb-4">
Comprehensive ERP Modules
</h2>
<p className="text-center text-muted-foreground mb-16 max-w-2xl mx-auto">
Everything you need to run your business, all in one place
</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 max-w-7xl mx-auto">
{modules.map((module, index) => (
<Card
key={index}
className="group glass-card p-6 rounded-3xl transition-all duration-500 hover:scale-105 hover:glow-effect cursor-pointer"
style={{ animationDelay: `${index * 50}ms` }}
>
<div className="mb-4">
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-primary/10 to-accent/20 flex items-center justify-center">
<module.icon className="w-7 h-7 text-primary" />
</div>
</div>
<h3 className="text-lg font-semibold text-foreground mb-2">{module.title}</h3>
<p className="text-sm text-muted-foreground mb-3">{module.description}</p>
<div className="text-xs text-primary font-medium">{module.benefits}</div>
</Card>
))}
</div>
</section>
{/* ROI Calculator */}
<section className="container mx-auto px-6 py-20">
<div className="glass-strong rounded-3xl p-8 md:p-12 max-w-5xl mx-auto">
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4 text-center">
Calculate Your ROI
</h2>
<p className="text-center text-muted-foreground mb-12">
See how much you could save with our ERP system
</p>
<div className="grid md:grid-cols-2 gap-8 mb-12">
<div>
<Label htmlFor="employees" className="text-foreground mb-2 block">
Number of Employees
</Label>
<Input
id="employees"
type="number"
value={employees}
onChange={(e) => setEmployees(Number(e.target.value))}
className="glass-card border-glass-border"
/>
</div>
<div>
<Label htmlFor="departments" className="text-foreground mb-2 block">
Number of Departments
</Label>
<Input
id="departments"
type="number"
value={departments}
onChange={(e) => setDepartments(Number(e.target.value))}
className="glass-card border-glass-border"
/>
</div>
</div>
<div className="grid md:grid-cols-3 gap-6">
<div className="glass-card p-6 rounded-2xl text-center">
<div className="text-sm text-muted-foreground mb-2">Estimated Yearly Savings</div>
<div className="text-3xl font-bold text-primary animate-glow-pulse">
${roi.savings.toLocaleString()}
</div>
</div>
<div className="glass-card p-6 rounded-2xl text-center">
<div className="text-sm text-muted-foreground mb-2">Time Saved (hours/month)</div>
<div className="text-3xl font-bold text-primary animate-glow-pulse">
{roi.timesSaved.toLocaleString()}
</div>
</div>
<div className="glass-card p-6 rounded-2xl text-center">
<div className="text-sm text-muted-foreground mb-2">Productivity Increase</div>
<div className="text-3xl font-bold text-primary animate-glow-pulse">
{roi.productivity}%
</div>
</div>
</div>
<div className="mt-8 text-center">
<Button className="glass-card hover:glow-effect">
Export to PDF
</Button>
</div>
</div>
</section>
{/* Features Comparison */}
<section className="container mx-auto px-6 py-20">
<h2 className="text-4xl font-bold text-center text-foreground mb-4">
Why OpenXpert ERP?
</h2>
<p className="text-center text-muted-foreground mb-16 max-w-2xl mx-auto">
See how we compare to other solutions
</p>
<div className="glass-strong rounded-3xl p-8 max-w-4xl mx-auto overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-glass-border">
<th className="text-left py-4 px-4 text-foreground font-semibold">Feature</th>
<th className="text-center py-4 px-4 text-primary font-semibold">OpenXpert (OXS)</th>
<th className="text-center py-4 px-4 text-muted-foreground font-semibold">Others</th>
</tr>
</thead>
<tbody>
{features.map((feature, index) => (
<tr
key={index}
className="border-b border-glass-border/50 hover:bg-accent/10 transition-colors"
>
<td className="py-4 px-4 text-foreground">{feature.name}</td>
<td className="text-center py-4 px-4">
{feature.us ? (
<Check className="w-6 h-6 text-primary mx-auto animate-scale-in" />
) : (
<X className="w-6 h-6 text-muted-foreground/30 mx-auto" />
)}
</td>
<td className="text-center py-4 px-4">
{feature.them ? (
<Check className="w-6 h-6 text-muted-foreground/50 mx-auto" />
) : (
<X className="w-6 h-6 text-muted-foreground/30 mx-auto" />
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
{/* Demo CTA */}
<section className="container mx-auto px-6 py-20">
<div className="glass-strong rounded-3xl p-12 max-w-4xl mx-auto text-center">
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4">
Ready to Transform Your Business?
</h2>
<p className="text-lg text-muted-foreground mb-8 max-w-2xl mx-auto">
Schedule a personalized demo and see how our ERP system can revolutionize your operations.
</p>
{!showCalendar ? (
<Button
size="lg"
className="glass-card hover:glow-effect px-8 py-6 text-lg group"
onClick={() => setShowCalendar(true)}
>
<Calendar className="mr-2 w-5 h-5" />
Schedule ERP Demo
<ArrowRight className="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform" />
</Button>
) : (
<div className="glass-card p-8 rounded-2xl animate-scale-in">
<Check className="w-16 h-16 text-primary mx-auto mb-4 animate-glow-pulse" />
<h3 className="text-xl font-semibold text-foreground mb-2">Request Received!</h3>
<p className="text-muted-foreground mb-6">
Our team will contact you within 24 hours to schedule your personalized demo.
</p>
<Link to="/contact">
<Button variant="outline" className="glass-card">
Contact Us Directly
</Button>
</Link>
</div>
)}
</div>
</section>
<Footer />
</div>
);
};
export default ErpSystems;

View File

@@ -0,0 +1,355 @@
import { useState } from "react";
import { Monitor, Shield, Bell, TrendingUp, Clock, Wifi, HardDrive, Cpu, Activity, AlertTriangle, CheckCircle, ArrowRight, Check } from "lucide-react";
import Navigation from "@/components/Navigation";
import Footer from "@/components/Footer";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Link } from "react-router-dom";
const RmmServices = () => {
const [showDemo, setShowDemo] = useState(false);
const [simulationActive, setSimulationActive] = useState(false);
const features = [
{
icon: Monitor,
title: "24/7 Monitoring",
description: "Continuous surveillance of all your systems",
benefit: "99.9% uptime"
},
{
icon: Bell,
title: "Instant Alerts",
description: "Real-time notifications for any issues",
benefit: "<5min response"
},
{
icon: Shield,
title: "Threat Protection",
description: "Proactive security monitoring and patching",
benefit: "Zero-day protection"
},
{
icon: HardDrive,
title: "Automated Backups",
description: "Daily backups with instant recovery options",
benefit: "15-min recovery"
},
{
icon: Cpu,
title: "Performance Optimization",
description: "Keep systems running at peak efficiency",
benefit: "30% faster"
},
{
icon: TrendingUp,
title: "Predictive Analytics",
description: "AI-powered insights to prevent issues",
benefit: "85% issue prevention"
}
];
const monitoringMetrics = [
{ icon: Activity, label: "CPU Usage", value: "42%", status: "healthy" },
{ icon: HardDrive, label: "Disk Space", value: "68%", status: "healthy" },
{ icon: Wifi, label: "Network", value: "125 Mbps", status: "healthy" },
{ icon: Clock, label: "Uptime", value: "99.98%", status: "excellent" }
];
const tiers = [
{
name: "Essential",
price: "AED 2,500/month",
devices: "Up to 25 devices",
features: [
"24/7 system monitoring",
"Automated alerts",
"Monthly reports",
"Email support",
"Basic patching"
]
},
{
name: "Professional",
price: "AED 5,000/month",
devices: "Up to 100 devices",
popular: true,
features: [
"Everything in Essential",
"Real-time dashboards",
"Automated patching",
"Priority phone support",
"Backup management",
"Performance optimization"
]
},
{
name: "Enterprise",
price: "Custom pricing",
devices: "Unlimited devices",
features: [
"Everything in Professional",
"Dedicated account manager",
"Custom integrations",
"Advanced analytics",
"SLA guarantees",
"On-site support available"
]
}
];
const benefits = [
{
title: "Reduced Downtime",
stat: "95%",
description: "Issues detected and resolved before impact"
},
{
title: "Cost Savings",
stat: "60%",
description: "Reduction in emergency IT costs"
},
{
title: "Faster Resolution",
stat: "10x",
description: "Problems fixed before users notice"
},
{
title: "Security Updates",
stat: "100%",
description: "All systems always patched and protected"
}
];
return (
<div className="min-h-screen gradient-bg">
<Navigation />
{/* Hero Section */}
<section className="relative container mx-auto px-6 pt-32 pb-20">
<div className="text-center mb-16 animate-slide-in-up">
<h1 className="text-5xl md:text-6xl font-bold text-foreground mb-6">
RMM Services
</h1>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto mb-4">
Never worry about IT infrastructure again.
</p>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
24/7 remote monitoring and management that keeps your business running smoothly.
</p>
</div>
{/* Animated Network Map */}
<div className="glass-strong rounded-3xl p-8 max-w-5xl mx-auto mb-20 animate-slide-in-up" style={{ animationDelay: "200ms" }}>
<div className="aspect-video glass-card rounded-2xl flex items-center justify-center relative overflow-hidden">
<div className="relative">
<Monitor className="w-24 h-24 text-primary/30 animate-glow-pulse" />
<div className="absolute -top-8 -left-8">
<div className="w-3 h-3 rounded-full bg-primary animate-ping" />
</div>
<div className="absolute -top-8 -right-8">
<div className="w-3 h-3 rounded-full bg-primary animate-ping" style={{ animationDelay: "0.5s" }} />
</div>
<div className="absolute -bottom-8 -left-8">
<div className="w-3 h-3 rounded-full bg-primary animate-ping" style={{ animationDelay: "1s" }} />
</div>
<div className="absolute -bottom-8 -right-8">
<div className="w-3 h-3 rounded-full bg-primary animate-ping" style={{ animationDelay: "1.5s" }} />
</div>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6">
{monitoringMetrics.map((metric, i) => (
<div key={i} className="glass-card p-4 rounded-2xl animate-scale-in" style={{ animationDelay: `${400 + i * 100}ms` }}>
<metric.icon className="w-6 h-6 text-primary mb-2" />
<div className="text-sm text-muted-foreground mb-1">{metric.label}</div>
<div className="text-xl font-bold text-foreground">{metric.value}</div>
</div>
))}
</div>
</div>
</section>
{/* Features Grid */}
<section className="container mx-auto px-6 py-20">
<h2 className="text-4xl font-bold text-center text-foreground mb-4">
Comprehensive Monitoring & Management
</h2>
<p className="text-center text-muted-foreground mb-16 max-w-2xl mx-auto">
Protect your infrastructure with enterprise-grade monitoring
</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-7xl mx-auto">
{features.map((feature, index) => (
<Card
key={index}
className="group glass-card p-8 rounded-3xl transition-all duration-500 hover:scale-105 hover:glow-effect cursor-pointer"
style={{ animationDelay: `${index * 50}ms` }}
>
<div className="mb-6">
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-primary/10 to-accent/20 flex items-center justify-center">
<feature.icon className="w-7 h-7 text-primary" />
</div>
</div>
<h3 className="text-xl font-semibold text-foreground mb-2">{feature.title}</h3>
<p className="text-sm text-muted-foreground mb-4">{feature.description}</p>
<div className="text-xs text-primary font-bold">{feature.benefit}</div>
</Card>
))}
</div>
</section>
{/* Live Status Simulation */}
<section className="container mx-auto px-6 py-20">
<div className="glass-strong rounded-3xl p-8 md:p-12 max-w-5xl mx-auto">
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4 text-center">
Live Dashboard Preview
</h2>
<p className="text-center text-muted-foreground mb-12">
See how our monitoring system keeps your infrastructure healthy
</p>
{!simulationActive ? (
<div className="text-center">
<Button
size="lg"
className="glass-card hover:glow-effect px-8 py-6 text-lg"
onClick={() => setSimulationActive(true)}
>
<Activity className="mr-2 w-5 h-5" />
Start Live Simulation
</Button>
</div>
) : (
<div className="space-y-4">
{[
{ type: "success", message: "All systems operational", icon: CheckCircle },
{ type: "info", message: "Backup completed successfully", icon: HardDrive },
{ type: "warning", message: "High CPU usage detected - optimizing", icon: AlertTriangle },
{ type: "success", message: "Security patch applied", icon: Shield }
].map((alert, index) => (
<div
key={index}
className="glass-card p-4 rounded-2xl flex items-center gap-4 animate-slide-in-up"
style={{ animationDelay: `${index * 200}ms` }}
>
<alert.icon className={`w-6 h-6 ${
alert.type === "success" ? "text-green-500" :
alert.type === "warning" ? "text-yellow-500" :
"text-primary"
}`} />
<span className="text-foreground">{alert.message}</span>
<span className="ml-auto text-xs text-muted-foreground">Just now</span>
</div>
))}
</div>
)}
</div>
</section>
{/* Benefits Stats */}
<section className="container mx-auto px-6 py-20">
<h2 className="text-4xl font-bold text-center text-foreground mb-16">
Measurable Business Impact
</h2>
<div className="grid md:grid-cols-4 gap-6 max-w-6xl mx-auto">
{benefits.map((benefit, index) => (
<Card
key={index}
className="glass-card p-8 rounded-3xl text-center transition-all duration-500 hover:scale-105 hover:glow-effect"
>
<div className="text-5xl font-bold text-primary mb-2 animate-glow-pulse">
{benefit.stat}
</div>
<h3 className="text-lg font-semibold text-foreground mb-2">{benefit.title}</h3>
<p className="text-sm text-muted-foreground">{benefit.description}</p>
</Card>
))}
</div>
</section>
{/* Pricing Tiers */}
<section className="container mx-auto px-6 py-20">
<h2 className="text-4xl font-bold text-center text-foreground mb-4">
Choose Your Monitoring Plan
</h2>
<p className="text-center text-muted-foreground mb-16 max-w-2xl mx-auto">
Flexible plans that scale with your business
</p>
<div className="grid md:grid-cols-3 gap-6 max-w-6xl mx-auto">
{tiers.map((tier, index) => (
<Card
key={index}
className={`glass-card p-8 rounded-3xl transition-all duration-500 hover:scale-105 ${
tier.popular ? "ring-2 ring-primary glow-effect" : ""
}`}
>
{tier.popular && (
<div className="inline-block px-4 py-1 rounded-full bg-primary text-primary-foreground text-xs font-bold mb-4">
Most Popular
</div>
)}
<h3 className="text-2xl font-bold text-foreground mb-2">{tier.name}</h3>
<div className="text-3xl font-bold text-primary mb-2">{tier.price}</div>
<div className="text-sm text-muted-foreground mb-6">{tier.devices}</div>
<ul className="space-y-3 mb-8">
{tier.features.map((feature, i) => (
<li key={i} className="flex items-start gap-2 text-sm text-muted-foreground">
<Check className="w-4 h-4 text-primary mt-0.5 flex-shrink-0" />
<span>{feature}</span>
</li>
))}
</ul>
<Button className="w-full glass-card hover:glow-effect">
Get Started
</Button>
</Card>
))}
</div>
</section>
{/* CTA Section */}
<section className="container mx-auto px-6 py-20">
<div className="glass-strong rounded-3xl p-12 max-w-4xl mx-auto text-center">
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4">
Ready to Protect Your Infrastructure?
</h2>
<p className="text-lg text-muted-foreground mb-8 max-w-2xl mx-auto">
Start monitoring your systems today with a free 30-day trial
</p>
{!showDemo ? (
<Button
size="lg"
className="glass-card hover:glow-effect px-8 py-6 text-lg group"
onClick={() => setShowDemo(true)}
>
<Monitor className="mr-2 w-5 h-5" />
Start Free Trial
<ArrowRight className="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform" />
</Button>
) : (
<div className="glass-card p-8 rounded-2xl animate-scale-in">
<Check className="w-16 h-16 text-primary mx-auto mb-4 animate-glow-pulse" />
<h3 className="text-xl font-semibold text-foreground mb-2">Request Received!</h3>
<p className="text-muted-foreground mb-6">
Our RMM specialists will set up your trial within 24 hours.
</p>
<Link to="/contact">
<Button variant="outline" className="glass-card">
Contact Us Directly
</Button>
</Link>
</div>
)}
</div>
</section>
<Footer />
</div>
);
};
export default RmmServices;

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />