Adding current files to the repo
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
20
components.json
Normal file
20
components.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "slate",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
}
|
||||||
|
}
|
||||||
26
eslint.config.js
Normal file
26
eslint.config.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import globals from "globals";
|
||||||
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ["dist"] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
"react-hooks": reactHooks,
|
||||||
|
"react-refresh": reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
65
index.html
Normal file
65
index.html
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>OpenXpert Solutions (OXS) - Leading ERP, E-Commerce, RMM & AMC IT Provider in UAE</title>
|
||||||
|
<meta name="description" content="OXS delivers secure ERP systems, high-performance e-commerce sites, RMM monitoring, and AMC IT support across Dubai & UAE. Premium technology for future-ready businesses." />
|
||||||
|
<meta name="author" content="OpenXpert Solutions (OXS)" />
|
||||||
|
<meta name="keywords" content="OpenXpert Solutions, OXS, OXS IT Solutions, OXS UAE, IT solutions company Dubai, ERP software provider UAE, ERP implementation Dubai, Custom e-commerce development, RMM remote monitoring services, AMC annual maintenance contracts, Managed IT support Dubai, IT infrastructure experts, Business automation UAE, Digital transformation Dubai, Software development UAE, Professional website designers, Cybersecurity-focused IT company, Cloud-based ERP systems, Secure enterprise platforms" />
|
||||||
|
<link rel="canonical" href="https://openxpert.solutions" />
|
||||||
|
|
||||||
|
<meta property="og:title" content="OpenXpert Solutions (OXS) - Leading IT Solutions Provider UAE" />
|
||||||
|
<meta property="og:description" content="Transforming businesses through innovative technology. Expert ERP systems, custom e-commerce, RMM services, AMC support, and premium web development across Dubai & UAE." />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
||||||
|
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:site" content="@OpenXpertSolutions" />
|
||||||
|
<meta name="twitter:title" content="OpenXpert Solutions (OXS) - Enterprise IT Solutions UAE" />
|
||||||
|
<meta name="twitter:description" content="Leading provider of enterprise digital solutions, ERP systems, and custom web development in Dubai & UAE." />
|
||||||
|
<meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
||||||
|
|
||||||
|
<!-- Organization Schema -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "OpenXpert Solutions",
|
||||||
|
"alternateName": "OXS",
|
||||||
|
"url": "https://openxpert.solutions",
|
||||||
|
"logo": "https://openxpert.solutions/logo.png",
|
||||||
|
"description": "Leading IT solutions provider in UAE specializing in ERP systems, custom e-commerce development, RMM services, and AMC support.",
|
||||||
|
"address": {
|
||||||
|
"@type": "PostalAddress",
|
||||||
|
"addressCountry": "AE",
|
||||||
|
"addressRegion": "Dubai"
|
||||||
|
},
|
||||||
|
"sameAs": [
|
||||||
|
"https://twitter.com/OpenXpertSolutions",
|
||||||
|
"https://linkedin.com/company/openxpert-solutions"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Website Schema with SearchAction -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebSite",
|
||||||
|
"name": "OpenXpert Solutions",
|
||||||
|
"url": "https://openxpert.solutions",
|
||||||
|
"potentialAction": {
|
||||||
|
"@type": "SearchAction",
|
||||||
|
"target": "https://openxpert.solutions/search?q={search_term_string}",
|
||||||
|
"query-input": "required name=search_term_string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
6766
package-lock.json
generated
Normal file
6766
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
83
package.json
Normal file
83
package.json
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"name": "vite_react_shadcn_ts",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"build:dev": "vite build --mode development",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^3.10.0",
|
||||||
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||||
|
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
|
"@radix-ui/react-collapsible": "^1.1.11",
|
||||||
|
"@radix-ui/react-context-menu": "^2.2.15",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
|
"@radix-ui/react-hover-card": "^1.1.14",
|
||||||
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-menubar": "^1.1.15",
|
||||||
|
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||||
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
|
"@radix-ui/react-radio-group": "^1.3.7",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||||
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
|
"@radix-ui/react-slider": "^1.3.5",
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.12",
|
||||||
|
"@radix-ui/react-toast": "^1.2.14",
|
||||||
|
"@radix-ui/react-toggle": "^1.1.9",
|
||||||
|
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
|
"@tanstack/react-query": "^5.83.0",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
|
"date-fns": "^3.6.0",
|
||||||
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"input-otp": "^1.4.2",
|
||||||
|
"lucide-react": "^0.462.0",
|
||||||
|
"next-themes": "^0.3.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-day-picker": "^8.10.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-hook-form": "^7.61.1",
|
||||||
|
"react-resizable-panels": "^2.1.9",
|
||||||
|
"react-router-dom": "^6.30.1",
|
||||||
|
"recharts": "^2.15.4",
|
||||||
|
"sonner": "^1.7.4",
|
||||||
|
"tailwind-merge": "^2.6.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"vaul": "^0.9.9",
|
||||||
|
"zod": "^3.25.76"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.32.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
|
"@types/node": "^22.16.5",
|
||||||
|
"@types/react": "^18.3.23",
|
||||||
|
"@types/react-dom": "^18.3.7",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.11.0",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"eslint": "^9.32.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
|
"globals": "^15.15.0",
|
||||||
|
"lovable-tagger": "^1.1.11",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"typescript-eslint": "^8.38.0",
|
||||||
|
"vite": "^5.4.19"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
1
public/placeholder.svg
Normal file
1
public/placeholder.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
14
public/robots.txt
Normal file
14
public/robots.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
User-agent: Googlebot
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
User-agent: Bingbot
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
User-agent: Twitterbot
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
User-agent: facebookexternalhit
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
44
src/App.tsx
Normal file
44
src/App.tsx
Normal 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;
|
||||||
BIN
src/assets/openxpert-logo.jpg
Normal file
BIN
src/assets/openxpert-logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
19
src/components/FloatingShape.tsx
Normal file
19
src/components/FloatingShape.tsx
Normal 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
165
src/components/Footer.tsx
Normal 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;
|
||||||
28
src/components/NavLink.tsx
Normal file
28
src/components/NavLink.tsx
Normal 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 };
|
||||||
183
src/components/Navigation.tsx
Normal file
183
src/components/Navigation.tsx
Normal 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;
|
||||||
91
src/components/ParticleField.tsx
Normal file
91
src/components/ParticleField.tsx
Normal 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 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
46
src/components/ServiceCard.tsx
Normal file
46
src/components/ServiceCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
62
src/components/ThemeProvider.tsx
Normal file
62
src/components/ThemeProvider.tsx
Normal 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;
|
||||||
|
};
|
||||||
30
src/components/ThemeToggle.tsx
Normal file
30
src/components/ThemeToggle.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
52
src/components/ui/accordion.tsx
Normal file
52
src/components/ui/accordion.tsx
Normal 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 };
|
||||||
104
src/components/ui/alert-dialog.tsx
Normal file
104
src/components/ui/alert-dialog.tsx
Normal 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,
|
||||||
|
};
|
||||||
43
src/components/ui/alert.tsx
Normal file
43
src/components/ui/alert.tsx
Normal 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 };
|
||||||
5
src/components/ui/aspect-ratio.tsx
Normal file
5
src/components/ui/aspect-ratio.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
|
||||||
|
|
||||||
|
const AspectRatio = AspectRatioPrimitive.Root;
|
||||||
|
|
||||||
|
export { AspectRatio };
|
||||||
38
src/components/ui/avatar.tsx
Normal file
38
src/components/ui/avatar.tsx
Normal 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 };
|
||||||
29
src/components/ui/badge.tsx
Normal file
29
src/components/ui/badge.tsx
Normal 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 };
|
||||||
90
src/components/ui/breadcrumb.tsx
Normal file
90
src/components/ui/breadcrumb.tsx
Normal 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,
|
||||||
|
};
|
||||||
47
src/components/ui/button.tsx
Normal file
47
src/components/ui/button.tsx
Normal 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 };
|
||||||
54
src/components/ui/calendar.tsx
Normal file
54
src/components/ui/calendar.tsx
Normal 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 };
|
||||||
43
src/components/ui/card.tsx
Normal file
43
src/components/ui/card.tsx
Normal 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 };
|
||||||
224
src/components/ui/carousel.tsx
Normal file
224
src/components/ui/carousel.tsx
Normal 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
303
src/components/ui/chart.tsx
Normal 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 };
|
||||||
26
src/components/ui/checkbox.tsx
Normal file
26
src/components/ui/checkbox.tsx
Normal 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 };
|
||||||
9
src/components/ui/collapsible.tsx
Normal file
9
src/components/ui/collapsible.tsx
Normal 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 };
|
||||||
132
src/components/ui/command.tsx
Normal file
132
src/components/ui/command.tsx
Normal 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,
|
||||||
|
};
|
||||||
178
src/components/ui/context-menu.tsx
Normal file
178
src/components/ui/context-menu.tsx
Normal 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,
|
||||||
|
};
|
||||||
95
src/components/ui/dialog.tsx
Normal file
95
src/components/ui/dialog.tsx
Normal 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,
|
||||||
|
};
|
||||||
87
src/components/ui/drawer.tsx
Normal file
87
src/components/ui/drawer.tsx
Normal 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,
|
||||||
|
};
|
||||||
179
src/components/ui/dropdown-menu.tsx
Normal file
179
src/components/ui/dropdown-menu.tsx
Normal 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
129
src/components/ui/form.tsx
Normal 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 };
|
||||||
27
src/components/ui/hover-card.tsx
Normal file
27
src/components/ui/hover-card.tsx
Normal 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 };
|
||||||
61
src/components/ui/input-otp.tsx
Normal file
61
src/components/ui/input-otp.tsx
Normal 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 };
|
||||||
22
src/components/ui/input.tsx
Normal file
22
src/components/ui/input.tsx
Normal 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 };
|
||||||
17
src/components/ui/label.tsx
Normal file
17
src/components/ui/label.tsx
Normal 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 };
|
||||||
207
src/components/ui/menubar.tsx
Normal file
207
src/components/ui/menubar.tsx
Normal 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,
|
||||||
|
};
|
||||||
120
src/components/ui/navigation-menu.tsx
Normal file
120
src/components/ui/navigation-menu.tsx
Normal 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,
|
||||||
|
};
|
||||||
81
src/components/ui/pagination.tsx
Normal file
81
src/components/ui/pagination.tsx
Normal 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,
|
||||||
|
};
|
||||||
29
src/components/ui/popover.tsx
Normal file
29
src/components/ui/popover.tsx
Normal 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 };
|
||||||
23
src/components/ui/progress.tsx
Normal file
23
src/components/ui/progress.tsx
Normal 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 };
|
||||||
36
src/components/ui/radio-group.tsx
Normal file
36
src/components/ui/radio-group.tsx
Normal 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 };
|
||||||
37
src/components/ui/resizable.tsx
Normal file
37
src/components/ui/resizable.tsx
Normal 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 };
|
||||||
38
src/components/ui/scroll-area.tsx
Normal file
38
src/components/ui/scroll-area.tsx
Normal 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 };
|
||||||
143
src/components/ui/select.tsx
Normal file
143
src/components/ui/select.tsx
Normal 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,
|
||||||
|
};
|
||||||
20
src/components/ui/separator.tsx
Normal file
20
src/components/ui/separator.tsx
Normal 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
107
src/components/ui/sheet.tsx
Normal 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,
|
||||||
|
};
|
||||||
637
src/components/ui/sidebar.tsx
Normal file
637
src/components/ui/sidebar.tsx
Normal 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,
|
||||||
|
};
|
||||||
7
src/components/ui/skeleton.tsx
Normal file
7
src/components/ui/skeleton.tsx
Normal 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 };
|
||||||
23
src/components/ui/slider.tsx
Normal file
23
src/components/ui/slider.tsx
Normal 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 };
|
||||||
27
src/components/ui/sonner.tsx
Normal file
27
src/components/ui/sonner.tsx
Normal 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 };
|
||||||
27
src/components/ui/switch.tsx
Normal file
27
src/components/ui/switch.tsx
Normal 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 };
|
||||||
72
src/components/ui/table.tsx
Normal file
72
src/components/ui/table.tsx
Normal 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 };
|
||||||
53
src/components/ui/tabs.tsx
Normal file
53
src/components/ui/tabs.tsx
Normal 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 };
|
||||||
21
src/components/ui/textarea.tsx
Normal file
21
src/components/ui/textarea.tsx
Normal 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
111
src/components/ui/toast.tsx
Normal 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,
|
||||||
|
};
|
||||||
24
src/components/ui/toaster.tsx
Normal file
24
src/components/ui/toaster.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
src/components/ui/toggle-group.tsx
Normal file
49
src/components/ui/toggle-group.tsx
Normal 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 };
|
||||||
37
src/components/ui/toggle.tsx
Normal file
37
src/components/ui/toggle.tsx
Normal 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 };
|
||||||
28
src/components/ui/tooltip.tsx
Normal file
28
src/components/ui/tooltip.tsx
Normal 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 };
|
||||||
3
src/components/ui/use-toast.ts
Normal file
3
src/components/ui/use-toast.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { useToast, toast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
export { useToast, toast };
|
||||||
19
src/hooks/use-mobile.tsx
Normal file
19
src/hooks/use-mobile.tsx
Normal 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
186
src/hooks/use-toast.ts
Normal 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
186
src/index.css
Normal 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
6
src/lib/utils.ts
Normal 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
5
src/main.tsx
Normal 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
97
src/pages/About.tsx
Normal 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
147
src/pages/Contact.tsx
Normal 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
146
src/pages/Index.tsx
Normal 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
24
src/pages/NotFound.tsx
Normal 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
115
src/pages/Portfolio.tsx
Normal 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;
|
||||||
412
src/pages/services/AmcSupport.tsx
Normal file
412
src/pages/services/AmcSupport.tsx
Normal 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;
|
||||||
326
src/pages/services/ECommerce.tsx
Normal file
326
src/pages/services/ECommerce.tsx
Normal 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;
|
||||||
311
src/pages/services/ErpSystems.tsx
Normal file
311
src/pages/services/ErpSystems.tsx
Normal 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;
|
||||||
355
src/pages/services/RmmServices.tsx
Normal file
355
src/pages/services/RmmServices.tsx
Normal 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
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
140
tailwind.config.ts
Normal file
140
tailwind.config.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
darkMode: ["class"],
|
||||||
|
content: ["./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}"],
|
||||||
|
prefix: "",
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
sidebar: {
|
||||||
|
DEFAULT: "hsl(var(--sidebar-background))",
|
||||||
|
foreground: "hsl(var(--sidebar-foreground))",
|
||||||
|
primary: "hsl(var(--sidebar-primary))",
|
||||||
|
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
|
||||||
|
accent: "hsl(var(--sidebar-accent))",
|
||||||
|
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
|
||||||
|
border: "hsl(var(--sidebar-border))",
|
||||||
|
ring: "hsl(var(--sidebar-ring))",
|
||||||
|
},
|
||||||
|
glass: {
|
||||||
|
DEFAULT: "hsl(var(--glass-bg))",
|
||||||
|
border: "hsl(var(--glass-border))",
|
||||||
|
shadow: "hsl(var(--glass-shadow))",
|
||||||
|
},
|
||||||
|
glow: {
|
||||||
|
primary: "hsl(var(--glow-primary))",
|
||||||
|
accent: "hsl(var(--glow-accent))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: {
|
||||||
|
height: "0",
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
height: "var(--radix-accordion-content-height)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: {
|
||||||
|
height: "var(--radix-accordion-content-height)",
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
height: "0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"float": {
|
||||||
|
"0%, 100%": {
|
||||||
|
transform: "translateY(0px) scale(1)",
|
||||||
|
},
|
||||||
|
"50%": {
|
||||||
|
transform: "translateY(-20px) scale(1.02)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"float-delayed": {
|
||||||
|
"0%, 100%": {
|
||||||
|
transform: "translateY(0px) rotate(0deg)",
|
||||||
|
},
|
||||||
|
"50%": {
|
||||||
|
transform: "translateY(-15px) rotate(2deg)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"glow-pulse": {
|
||||||
|
"0%, 100%": {
|
||||||
|
opacity: "0.6",
|
||||||
|
boxShadow: "0 0 20px hsl(var(--glow-accent) / 0.3)",
|
||||||
|
},
|
||||||
|
"50%": {
|
||||||
|
opacity: "1",
|
||||||
|
boxShadow: "0 0 40px hsl(var(--glow-accent) / 0.5)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"slide-in-up": {
|
||||||
|
"0%": {
|
||||||
|
transform: "translateY(30px)",
|
||||||
|
opacity: "0",
|
||||||
|
},
|
||||||
|
"100%": {
|
||||||
|
transform: "translateY(0)",
|
||||||
|
opacity: "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
"float": "float 6s ease-in-out infinite",
|
||||||
|
"float-delayed": "float-delayed 8s ease-in-out infinite",
|
||||||
|
"glow-pulse": "glow-pulse 3s ease-in-out infinite",
|
||||||
|
"slide-in-up": "slide-in-up 0.6s cubic-bezier(0.16, 1, 0.3, 1)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
} satisfies Config;
|
||||||
30
tsconfig.app.json
Normal file
30
tsconfig.app.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": false,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"noFallthroughCasesInSwitch": false,
|
||||||
|
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"strictNullChecks": false
|
||||||
|
}
|
||||||
|
}
|
||||||
22
tsconfig.node.json
Normal file
22
tsconfig.node.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
18
vite.config.ts
Normal file
18
vite.config.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react-swc";
|
||||||
|
import path from "path";
|
||||||
|
import { componentTagger } from "lovable-tagger";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig(({ mode }) => ({
|
||||||
|
server: {
|
||||||
|
host: "::",
|
||||||
|
port: 8080,
|
||||||
|
},
|
||||||
|
plugins: [react(), mode === "development" && componentTagger()].filter(Boolean),
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
Reference in New Issue
Block a user