init: init repo with existing code

This commit is contained in:
2026-03-30 20:20:21 +06:00
commit d71b4ab17e
100 changed files with 19389 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
# 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?
.vercel
temp_auto_push.bat
temp_interactive_push.bat
+73
View File
@@ -0,0 +1,73 @@
# Welcome to your Lovable project
## Project info
**URL**: https://lovable.dev/projects/REPLACE_WITH_PROJECT_ID
## How can I edit this code?
There are several ways of editing your application.
**Use Lovable**
Simply visit the [Lovable Project](https://lovable.dev/projects/REPLACE_WITH_PROJECT_ID) and start prompting.
Changes made via Lovable will be committed automatically to this repo.
**Use your preferred IDE**
If you want to work locally using your own IDE, you can clone this repo and push changes. Pushed changes will also be reflected in Lovable.
The only requirement is having Node.js & npm installed - [install with nvm](https://github.com/nvm-sh/nvm#installing-and-updating)
Follow these steps:
```sh
# Step 1: Clone the repository using the project's Git URL.
git clone <YOUR_GIT_URL>
# Step 2: Navigate to the project directory.
cd <YOUR_PROJECT_NAME>
# Step 3: Install the necessary dependencies.
npm i
# Step 4: Start the development server with auto-reloading and an instant preview.
npm run dev
```
**Edit a file directly in GitHub**
- Navigate to the desired file(s).
- Click the "Edit" button (pencil icon) at the top right of the file view.
- Make your changes and commit the changes.
**Use GitHub Codespaces**
- Navigate to the main page of your repository.
- Click on the "Code" button (green button) near the top right.
- Select the "Codespaces" tab.
- Click on "New codespace" to launch a new Codespace environment.
- Edit files directly within the Codespace and commit and push your changes once you're done.
## What technologies are used for this project?
This project is built with:
- Vite
- TypeScript
- React
- shadcn-ui
- Tailwind CSS
## How can I deploy this project?
Simply open [Lovable](https://lovable.dev/projects/REPLACE_WITH_PROJECT_ID) and click on Share -> Publish.
## Can I connect a custom domain to my Lovable project?
Yes, you can!
To connect a domain, navigate to Project > Settings > Domains and click Connect Domain.
Read more here: [Setting up a custom domain](https://docs.lovable.dev/features/custom-domain#custom-domain)
Executable
BIN
View File
Binary file not shown.
+20
View 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
View 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",
},
},
);
+83
View File
@@ -0,0 +1,83 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- Mobile Responsive -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Primary SEO -->
<title>TechZaa | AI Powered Tech Solutions & Digital Innovation</title>
<meta
name="description"
content="TechZaa is an AI-powered technology company delivering modern web development, digital transformation, and innovative software solutions for startups and enterprises."
/>
<meta
name="keywords"
content="TechZaa, AI tech solutions, web development, software development, digital transformation, startup technology solutions, AI powered applications"
/>
<meta name="author" content="TechZaa" />
<meta name="robots" content="index, follow" />
<!-- Canonical URL -->
<link rel="canonical" href="https://techzaa.vercel.app" />
<!-- Favicon -->
<link rel="icon" href="/favicon.ico" />
<!-- Theme Color -->
<meta name="theme-color" content="#0f172a" />
<!-- Open Graph (Facebook / LinkedIn) -->
<meta property="og:type" content="website" />
<meta property="og:title" content="TechZaa | AI Powered Tech Solutions" />
<meta
property="og:description"
content="TechZaa helps startups and enterprises transform their ideas into powerful digital products using cutting-edge technology."
/>
<meta property="og:url" content="https://techzaa.vercel.app" />
<meta
property="og:image"
content="https://techzaa.vercel.app/assets/logo-dIE0tUFz.webp"
/>
<meta property="og:site_name" content="TechZaa" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="TechZaa | AI Powered Tech Solutions" />
<meta
name="twitter:description"
content="Innovative AI-powered digital solutions for startups and enterprises."
/>
<meta
name="twitter:image"
content="https://techzaa.vercel.app/assets/logo-dIE0tUFz.webp"
/>
<!-- Structured Data (SEO Boost) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "TechZaa",
"url": "https://techzaa.vercel.app",
"logo": "https://techzaa.vercel.app/assets/logo-dIE0tUFz.webp",
"description": "TechZaa is an AI-powered technology company delivering innovative digital solutions, modern web applications, and scalable software systems.",
"sameAs": [
"https://www.linkedin.com/company/techzaa",
"https://www.facebook.com/share/1BCvfe5Frc/"
]
}
</script>
</head>
<body>
<div id="root"></div>
<!-- Vite Entry -->
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+9764
View File
File diff suppressed because it is too large Load Diff
+94
View File
@@ -0,0 +1,94 @@
{
"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",
"test": "vitest run",
"test:watch": "vitest"
},
"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-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"framer-motion": "^12.26.2",
"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-markdown": "^10.1.0",
"react-resizable-panels": "^2.1.9",
"react-router-dom": "^6.30.1",
"recharts": "^2.15.4",
"remark-gfm": "^4.0.1",
"sonner": "^1.7.4",
"swiper": "^12.1.2",
"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",
"@testing-library/jest-dom": "^6.6.0",
"@testing-library/react": "^16.0.0",
"@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",
"jsdom": "^20.0.3",
"lovable-tagger": "^1.1.13",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0",
"vite": "^5.4.19",
"vitest": "^3.2.4"
}
}
+10
View File
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 980 KiB

+14
View File
@@ -0,0 +1,14 @@
User-agent: Googlebot
Allow: /
User-agent: Bingbot
Allow: /
User-agent: Twitterbot
Allow: /
User-agent: facebookexternalhit
Allow: /
User-agent: *
Allow: /
+42
View File
@@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
+49
View File
@@ -0,0 +1,49 @@
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, useLocation } from "react-router-dom";
import { ThemeProvider } from "@/contexts/ThemeContext";
import { AnimatePresence } from "framer-motion";
import Index from "./pages/Index";
import Projects from "./pages/Projects";
import ProjectDetails from "./pages/ProjectDetails";
import Blog from "./pages/Blog";
import BlogArticle from "./pages/BlogArticle";
import NotFound from "./pages/NotFound";
const queryClient = new QueryClient();
function AnimatedRoutes() {
const location = useLocation();
return (
<AnimatePresence mode="wait">
<Routes location={location} key={location.pathname}>
<Route path="/" element={<Index />} />
<Route path="/projects" element={<Projects />} />
<Route path="/projects/:slug" element={<ProjectDetails />} />
<Route path="/blog" element={<Blog />} />
<Route path="/blog/:slug" element={<BlogArticle />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
</AnimatePresence>
);
}
const App = () => (
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<TooltipProvider>
<Toaster />
<Sonner />
<BrowserRouter>
<AnimatedRoutes />
</BrowserRouter>
</TooltipProvider>
</ThemeProvider>
</QueryClientProvider>
);
export default App;
Binary file not shown.

After

Width:  |  Height:  |  Size: 980 KiB

+158
View File
@@ -0,0 +1,158 @@
import { useRef, useEffect, useState } from 'react';
import { motion, useInView } from 'framer-motion';
import { Zap, Users, Rocket, Award } from 'lucide-react';
const stats = [
{ icon: Zap, value: 10, suffix: '+', label: 'Years Experience' },
{ icon: Rocket, value: 500, suffix: '+', label: 'Projects Completed' },
{ icon: Users, value: 200, suffix: '+', label: 'Happy Clients' },
{ icon: Award, value: 50, suffix: '+', label: 'Awards Won' },
];
function AnimatedCounter({ value, suffix, isInView }: { value: number; suffix: string; isInView: boolean }) {
const [count, setCount] = useState(0);
useEffect(() => {
if (!isInView) return;
let start = 0;
const duration = 2000;
const increment = value / (duration / 16);
const timer = setInterval(() => {
start += increment;
if (start >= value) {
setCount(value);
clearInterval(timer);
} else {
setCount(Math.floor(start));
}
}, 16);
return () => clearInterval(timer);
}, [isInView, value]);
return (
<span className="text-4xl md:text-5xl font-bold text-primary neon-text-glow">
{count}{suffix}
</span>
);
}
export default function AboutSection() {
const ref = useRef<HTMLElement>(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
return (
<section id="about" ref={ref} className="py-24 relative overflow-hidden bg-secondary/30">
{/* Background decoration */}
<div className="absolute inset-0">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] bg-primary/5 rounded-full blur-[200px]" />
</div>
<div className="container mx-auto px-4 relative z-10">
<div className="grid lg:grid-cols-2 gap-16 items-center">
{/* Content */}
<motion.div
initial={{ opacity: 0, x: -30 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.6 }}
>
<span className="inline-block px-4 py-1.5 rounded-full glass text-sm font-medium text-primary mb-4">
About Us
</span>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-6">
About <span className="text-primary neon-text-glow">TechZaa</span>
</h2>
<div className="space-y-4 text-muted-foreground text-lg">
<p>
Founded with a passion for innovation, TechZaa is a leading technology
company dedicated to transforming businesses through cutting-edge
digital solutions.
</p>
<p>
Our team of expert developers, designers, and strategists work
collaboratively to deliver exceptional results that exceed expectations.
We believe in the power of technology to solve complex problems and
create meaningful impact.
</p>
<p>
From startups to enterprises, we've helped hundreds of clients
achieve their digital transformation goals with our innovative
approach and commitment to excellence.
</p>
</div>
{/* Values */}
<div className="mt-8 flex flex-wrap gap-3">
{['Innovation', 'Quality', 'Integrity', 'Excellence'].map((value) => (
<span
key={value}
className="px-4 py-2 rounded-full glass border border-primary/30 text-sm font-medium hover:border-primary hover:neon-glow transition-all cursor-default"
>
{value}
</span>
))}
</div>
</motion.div>
{/* Stats Grid */}
<motion.div
initial={{ opacity: 0, x: 30 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.6, delay: 0.2 }}
className="grid grid-cols-2 gap-6"
>
{stats.map((stat, index) => (
<motion.div
key={stat.label}
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.3 + index * 0.1 }}
whileHover={{ y: -5, scale: 1.02 }}
className="p-6 rounded-2xl glass border border-border/50 hover:border-primary/50 transition-all text-center group"
>
{/* Icon */}
<div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center mx-auto mb-4 group-hover:neon-glow transition-all">
<stat.icon className="w-6 h-6 text-primary" />
</div>
{/* Counter */}
<AnimatedCounter value={stat.value} suffix={stat.suffix} isInView={isInView} />
{/* Label */}
<p className="text-muted-foreground text-sm mt-2">{stat.label}</p>
</motion.div>
))}
</motion.div>
</div>
{/* Tech illustration - abstract shapes */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 0.5 }}
className="mt-16 relative h-20 overflow-hidden"
>
<div className="absolute inset-0 flex items-center justify-center gap-4">
{[...Array(20)].map((_, i) => (
<motion.div
key={i}
className="w-8 h-8 rounded-md bg-gradient-to-br from-primary/20 to-neon-purple/20 border border-primary/20"
animate={{
y: [0, -10, 0],
rotate: [0, 45, 0],
}}
transition={{
duration: 3,
repeat: Infinity,
delay: i * 0.1,
}}
/>
))}
</div>
</motion.div>
</div>
</section>
);
}
+138
View File
@@ -0,0 +1,138 @@
import { motion } from 'framer-motion';
import { Calendar, Clock, ArrowRight, User } from 'lucide-react';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { blogPosts } from '@/data/blogData';
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.6, ease: 'easeOut' as const },
},
};
export default function BlogSection() {
return (
<section id="blog" className="py-24 relative overflow-hidden">
{/* Background elements */}
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-primary/5 to-transparent" />
<div className="container mx-auto px-4 relative z-10">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="text-center mb-16"
>
<span className="inline-block px-4 py-2 rounded-full glass text-primary text-sm font-medium mb-4">
Latest Insights
</span>
<h2 className="text-4xl md:text-5xl font-bold mb-6">
News & <span className="text-primary neon-text-glow">Blog</span>
</h2>
<p className="text-muted-foreground max-w-2xl mx-auto text-lg">
Stay updated with the latest trends, insights, and thought leadership from our team of experts.
</p>
</motion.div>
{/* Blog Grid */}
<motion.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
className="grid md:grid-cols-2 lg:grid-cols-3 gap-8"
>
{blogPosts.map((post) => (
<motion.article
key={post.id}
variants={itemVariants}
className="group glass rounded-2xl overflow-hidden hover:neon-glow transition-all duration-500"
>
{/* Image */}
<div className="relative h-48 overflow-hidden">
<img
src={post.image}
alt={post.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" />
<span className="absolute bottom-4 left-4 px-3 py-1 rounded-full bg-primary/20 text-primary text-xs font-medium backdrop-blur-sm">
{post.category}
</span>
</div>
{/* Content */}
<div className="p-6">
<h3 className="text-xl font-bold mb-3 group-hover:text-primary transition-colors line-clamp-2">
{post.title}
</h3>
<p className="text-muted-foreground text-sm mb-4 line-clamp-2">
{post.excerpt}
</p>
{/* Meta */}
<div className="flex items-center gap-4 text-xs text-muted-foreground mb-4">
<span className="flex items-center gap-1">
<User className="w-3 h-3" />
{post.author.name}
</span>
<span className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
{new Date(post.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</span>
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{post.readTime}
</span>
</div>
{/* Read More */}
<Link
to={`/blog/${post.slug}`}
className="inline-flex items-center gap-2 text-primary font-medium text-sm group/link hover:gap-3 transition-all"
>
Read Article
<ArrowRight className="w-4 h-4" />
</Link>
</div>
</motion.article>
))}
</motion.div>
{/* View All Button */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.4 }}
className="text-center mt-12"
>
<Link to="/blog">
<Button
variant="outline"
className="rounded-full px-8 py-6 glass border-primary/30 hover:neon-glow group"
>
View All Articles
<ArrowRight className="w-4 h-4 ml-2 transition-transform group-hover:translate-x-1" />
</Button>
</Link>
</motion.div>
</div>
</section>
);
}
+211
View File
@@ -0,0 +1,211 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { AnimatePresence, motion } from "framer-motion";
import { CheckCircle, Send, X } from "lucide-react";
import { useState } from "react";
interface ContactModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function ContactModal({ isOpen, onClose }: ContactModalProps) {
const [formData, setFormData] = useState({
name: "",
email: "",
message: "",
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
// Simulate form submission
await new Promise((resolve) => setTimeout(resolve, 1500));
setIsSubmitting(false);
setIsSuccess(true);
// Reset after showing success
setTimeout(() => {
setIsSuccess(false);
setFormData({ name: "", email: "", message: "" });
onClose();
}, 2000);
};
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
setFormData((prev) => ({
...prev,
[e.target.name]: e.target.value,
}));
};
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100] flex items-center justify-center p-4"
>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 bg-background/80 backdrop-blur-sm"
onClick={onClose}
/>
{/* Modal */}
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="relative w-full max-w-md rounded-3xl p-8 border-gradient overflow-hidden"
>
{/* Close button */}
<motion.button
onClick={onClose}
className="absolute top-4 right-4 p-2 rounded-full glass hover:bg-primary/20 transition-colors"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<X className="w-5 h-5" />
</motion.button>
{/* Content */}
<div className="relative z-10">
<AnimatePresence mode="wait">
{isSuccess ? (
<motion.div
key="success"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
className="text-center py-8"
>
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{
type: "spring",
damping: 15,
stiffness: 300,
delay: 0.2,
}}
>
<CheckCircle className="w-20 h-20 text-primary mx-auto mb-4 neon-text-glow" />
</motion.div>
<h3 className="text-2xl font-bold mb-2">Message Sent!</h3>
<p className="text-muted-foreground">
We'll get back to you soon.
</p>
</motion.div>
) : (
<motion.div
key="form"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<h2 className="text-2xl font-bold mb-2">Let's Talk</h2>
<p className="text-muted-foreground mb-6">
Ready to start your project? Tell us about it.
</p>
<form onSubmit={handleSubmit} className="space-y-4">
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<Input
name="name"
placeholder="Your Name"
value={formData.name}
onChange={handleChange}
required
className=" border focus:border-primary focus:neon-glow transition-all"
/>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<Input
name="email"
type="email"
placeholder="Your Email"
value={formData.email}
onChange={handleChange}
required
className=" border focus:border-primary focus:neon-glow transition-all"
/>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<Textarea
name="message"
placeholder="Tell us about your project..."
value={formData.message}
onChange={handleChange}
required
rows={4}
className=" border focus:border-primary focus:neon-glow transition-all resize-none"
/>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
>
<Button
type="submit"
disabled={isSubmitting}
className="w-full bg-primary text-primary-foreground font-semibold py-3 rounded-full neon-glow hover:neon-glow-strong transition-all group"
>
{isSubmitting ? (
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 1,
repeat: Infinity,
ease: "linear",
}}
className="w-5 h-5 border-2 border-primary-foreground/30 border-t-primary-foreground rounded-full"
/>
) : (
<>
Send Message
<Send className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
</>
)}
</Button>
</motion.div>
</form>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}
+291
View File
@@ -0,0 +1,291 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { motion, useInView } from "framer-motion";
import { CheckCircle, Mail, MapPin, Phone, Send } from "lucide-react";
import { useRef, useState } from "react";
const contactInfo = [
{ icon: Mail, label: "Email", value: "techzaa.alpha@gmail.com" },
{ icon: Phone, label: "Phone", value: "+880 19623-21487" },
{ icon: MapPin, label: "Location", value: "Rangpur, Bangladesh" },
];
export default function ContactSection() {
const ref = useRef<HTMLElement>(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
const [formData, setFormData] = useState({
name: "",
email: "",
subject: "",
message: "",
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
// Simulate form submission
await new Promise((resolve) => setTimeout(resolve, 1500));
setIsSubmitting(false);
setIsSuccess(true);
setTimeout(() => {
setIsSuccess(false);
setFormData({ name: "", email: "", subject: "", message: "" });
}, 3000);
};
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
setFormData((prev) => ({
...prev,
[e.target.name]: e.target.value,
}));
};
return (
<section id="contact" ref={ref} className="py-24 relative overflow-hidden">
{/* Background */}
<div className="absolute inset-0 opacity-30">
<div className="absolute bottom-0 left-1/4 w-96 h-96 bg-primary/10 rounded-full blur-[150px]" />
<div className="absolute top-1/4 right-1/4 w-72 h-72 bg-neon-purple/10 rounded-full blur-[120px]" />
</div>
<div className="container mx-auto px-4 relative z-10">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="text-center mb-16"
>
<span className="inline-block px-4 py-1.5 rounded-full glass text-sm font-medium text-primary mb-4">
Let's Connect
</span>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-4">
Get In <span className="text-primary neon-text-glow">Touch</span>
</h2>
<p className="text-muted-foreground max-w-2xl mx-auto text-lg">
Ready to start your next project? We'd love to hear from you. Send
us a message and we'll respond as soon as possible.
</p>
</motion.div>
<div className="grid lg:grid-cols-2 gap-12 max-w-6xl mx-auto">
{/* Contact Info */}
<motion.div
initial={{ opacity: 0, x: -30 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.6 }}
className="space-y-8"
>
<div>
<h3 className="text-2xl font-bold mb-4">Contact Information</h3>
<p className="text-muted-foreground">
Fill out the form and our team will get back to you within 24
hours.
</p>
</div>
{/* Contact cards */}
<div className="space-y-4">
{contactInfo.map((item, index) => (
<motion.div
key={item.label}
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.2 + index * 0.1 }}
whileHover={{ x: 5 }}
className="flex items-center gap-4 p-4 rounded-xl glass border border-border/50 hover:border-primary/50 transition-all group"
>
<div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center group-hover:neon-glow transition-all">
<item.icon className="w-5 h-5 text-primary" />
</div>
<div>
<p className="text-sm text-muted-foreground">
{item.label}
</p>
<p className="font-medium">{item.value}</p>
</div>
</motion.div>
))}
</div>
{/* Decorative illustration */}
<motion.div
initial={{ opacity: 0 }}
animate={isInView ? { opacity: 1 } : {}}
transition={{ duration: 0.8, delay: 0.5 }}
className="hidden lg:block relative h-48"
>
<div className="absolute inset-0 flex items-center justify-center">
<div className="relative">
{[...Array(3)].map((_, i) => (
<motion.div
key={i}
className="absolute w-32 h-32 rounded-full border-2 border-primary/20"
style={{
left: i * 20,
top: i * 10,
}}
animate={{
scale: [1, 1.1, 1],
opacity: [0.3, 0.6, 0.3],
}}
transition={{
duration: 3,
repeat: Infinity,
delay: i * 0.5,
}}
/>
))}
</div>
</div>
</motion.div>
</motion.div>
{/* Contact Form */}
<motion.div
initial={{ opacity: 0, x: 30 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.6, delay: 0.2 }}
className="relative"
>
<div className="p-8 rounded-3xl glass border border-border/50 relative overflow-hidden">
{/* Decorative gradient */}
<div className="absolute -top-20 -right-20 w-40 h-40 rounded-full gradient-primary opacity-20 blur-3xl" />
{isSuccess ? (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
className="text-center py-12"
>
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{
type: "spring",
damping: 15,
stiffness: 300,
delay: 0.2,
}}
>
<CheckCircle className="w-20 h-20 text-primary mx-auto mb-4 neon-text-glow" />
</motion.div>
<h3 className="text-2xl font-bold mb-2">Message Sent!</h3>
<p className="text-muted-foreground">
Thank you for reaching out. We'll get back to you soon.
</p>
</motion.div>
) : (
<form
onSubmit={handleSubmit}
className="space-y-5 relative z-10"
>
<div className="grid sm:grid-cols-2 gap-5">
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ delay: 0.3 }}
>
<Input
name="name"
placeholder="Your Name"
value={formData.name}
onChange={handleChange}
required
className="glass border-border/50 focus:border-primary focus:neon-glow transition-all h-12"
/>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ delay: 0.35 }}
>
<Input
name="email"
type="email"
placeholder="Your Email"
value={formData.email}
onChange={handleChange}
required
className="glass border-border/50 focus:border-primary focus:neon-glow transition-all h-12"
/>
</motion.div>
</div>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ delay: 0.4 }}
>
<Input
name="subject"
placeholder="Subject"
value={formData.subject}
onChange={handleChange}
required
className="glass border-border/50 focus:border-primary focus:neon-glow transition-all h-12"
/>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ delay: 0.45 }}
>
<Textarea
name="message"
placeholder="Your Message"
value={formData.message}
onChange={handleChange}
required
rows={5}
className="glass border-border/50 focus:border-primary focus:neon-glow transition-all resize-none"
/>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ delay: 0.5 }}
>
<Button
type="submit"
disabled={isSubmitting}
className="w-full bg-primary text-primary-foreground font-semibold py-6 rounded-full neon-glow hover:neon-glow-strong transition-all group"
>
{isSubmitting ? (
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 1,
repeat: Infinity,
ease: "linear",
}}
className="w-5 h-5 border-2 border-primary-foreground/30 border-t-primary-foreground rounded-full"
/>
) : (
<>
Send Message
<Send className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" />
</>
)}
</Button>
</motion.div>
</form>
)}
</div>
</motion.div>
</div>
</div>
</section>
);
}
+119
View File
@@ -0,0 +1,119 @@
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { motion, useInView } from "framer-motion";
import { useRef } from "react";
const faqs = [
{
question: "What services does TechZaa offer?",
answer:
"We provide comprehensive technology solutions including web development, mobile app development, AI/ML solutions, and cloud infrastructure services. Our team specializes in creating custom software tailored to your business needs.",
},
{
question: "How long does a typical project take?",
answer:
"Project timelines vary based on complexity and scope. A simple website might take 4-6 weeks, while complex enterprise applications can take 3-6 months. We provide detailed timelines during our initial consultation and keep you updated throughout the development process.",
},
{
question: "What is your development process?",
answer:
"We follow an agile methodology with iterative development cycles. Our process includes: Discovery & Planning, UI/UX Design, Development Sprints, Quality Assurance, Deployment, and Ongoing Support. We maintain transparent communication with regular updates and demos.",
},
{
question: "Do you provide ongoing maintenance and support?",
answer:
"Yes! We offer comprehensive maintenance packages that include bug fixes, security updates, performance optimization, and feature enhancements. Our support team is available to ensure your application runs smoothly 24/7.",
},
{
question: "What technologies do you work with?",
answer:
"Our tech stack includes React, Next.js, Node.js, Python, TypeScript, AWS, Google Cloud, and more. We stay current with industry trends and select the best technologies for each project's specific requirements.",
},
{
question: "How do you handle project pricing?",
answer:
"We offer flexible pricing models including fixed-price projects, time & materials, and dedicated team arrangements. After understanding your requirements, we provide transparent quotes with no hidden costs. Contact us for a free consultation and estimate.",
},
{
question: "Can you work with our existing team?",
answer:
"Absolutely! We frequently collaborate with in-house teams, providing additional expertise and resources. Whether you need staff augmentation, specialized skills, or full project delivery, we adapt to your working style and processes.",
},
{
question: "What industries do you serve?",
answer:
"We've delivered successful projects across fintech, healthcare, e-commerce, education, real estate, and SaaS industries. Our diverse experience allows us to bring cross-industry insights and best practices to every project.",
},
];
export default function FAQSection() {
const ref = useRef<HTMLElement>(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
return (
<section id="faq" ref={ref} className="py-24 relative overflow-hidden">
{/* Background decoration */}
<div className="absolute inset-0 opacity-30">
<div className="absolute top-1/4 right-0 w-96 h-96 bg-primary/10 rounded-full blur-[150px]" />
<div className="absolute bottom-1/4 left-0 w-80 h-80 bg-neon-purple/10 rounded-full blur-[120px]" />
</div>
<div className="container mx-auto px-4 relative z-10">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="text-center mb-16"
>
<span className="inline-block px-4 py-1.5 rounded-full glass text-sm font-medium text-primary mb-4">
Got Questions?
</span>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-4">
Frequently Asked{" "}
<span className="text-primary neon-text-glow">Questions</span>
</h2>
<p className="text-muted-foreground max-w-2xl mx-auto text-lg">
Everything you need to know about working with TechZaa. Can't find
what you're looking for? Reach out to our team.
</p>
</motion.div>
{/* FAQ Accordion */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.2 }}
className="max-w-3xl mx-auto"
>
<Accordion type="single" collapsible className="space-y-4">
{faqs.map((faq, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.4, delay: 0.1 * index }}
>
<AccordionItem
value={`item-${index}`}
className="glass border border-border/50 rounded-xl px-6 overflow-hidden data-[state=open]:border-primary/50 transition-colors"
>
<AccordionTrigger className="text-left hover:no-underline py-5 text-base md:text-lg font-medium hover:text-primary transition-colors">
{faq.question}
</AccordionTrigger>
<AccordionContent className="text-muted-foreground pb-5 text-sm md:text-base leading-relaxed">
{faq.answer}
</AccordionContent>
</AccordionItem>
</motion.div>
))}
</Accordion>
</motion.div>
</div>
</section>
);
}
+209
View File
@@ -0,0 +1,209 @@
import logo from "@/assets/logo.webp";
import { motion } from "framer-motion";
import { Github, Instagram, Linkedin, Mail, Twitter } from "lucide-react";
const quickLinks = [
{ name: "Home", href: "#home" },
{ name: "About", href: "#about" },
{ name: "Services", href: "#services" },
{ name: "Projects", href: "#projects" },
{ name: "Contact", href: "#contact" },
];
const services = [
{ name: "Web Development", href: "#services" },
{ name: "Mobile Apps", href: "#services" },
{ name: "AI Solutions", href: "#services" },
{ name: "Cloud Services", href: "#services" },
];
const socials = [
{
icon: Linkedin,
href: "https://www.linkedin.com/company/techzaa",
label: "LinkedIn",
},
{ icon: Twitter, href: "#", label: "Twitter" },
{ icon: Github, href: "#", label: "GitHub" },
{ icon: Instagram, href: "#", label: "Instagram" },
];
export default function Footer() {
return (
<footer className="relative overflow-hidden bg-secondary/50 pt-20 pb-8">
{/* Animated gradient divider */}
<div className="absolute top-0 left-0 right-0 h-1 gradient-primary-animated" />
{/* Background decoration */}
<div className="absolute inset-0 opacity-20">
<div className="absolute bottom-0 left-0 w-96 h-96 bg-primary/10 rounded-full blur-[150px]" />
<div className="absolute top-0 right-0 w-72 h-72 bg-neon-purple/10 rounded-full blur-[120px]" />
</div>
{/* Circuit pattern background */}
<div className="absolute inset-0 opacity-5">
<svg
className="w-full h-full"
viewBox="0 0 100 100"
preserveAspectRatio="none"
>
<pattern
id="circuit"
x="0"
y="0"
width="20"
height="20"
patternUnits="userSpaceOnUse"
>
<path
d="M 10 0 L 10 5 M 10 15 L 10 20 M 0 10 L 5 10 M 15 10 L 20 10"
stroke="currentColor"
strokeWidth="0.5"
fill="none"
/>
<circle cx="10" cy="10" r="2" fill="currentColor" />
</pattern>
<rect x="0" y="0" width="100" height="100" fill="url(#circuit)" />
</svg>
</div>
<div className="container mx-auto px-4 relative z-10">
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-12 mb-16">
{/* Brand */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="lg:col-span-1"
>
<a href="#home" className="inline-block mb-4">
<img src={logo} alt="TechZaa" className="h-16 w-auto" />
</a>
<p className="text-muted-foreground mb-6">
Transforming ideas into powerful digital solutions. We build the
future with technology.
</p>
{/* Newsletter */}
<div className="flex gap-2">
<div className="relative flex-1">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<input
type="email"
placeholder="Your email"
className="w-full pl-10 pr-4 py-2.5 rounded-full glass border border-border/50 focus:border-primary focus:outline-none transition-colors text-sm"
/>
</div>
<button className="px-4 py-2.5 rounded-full bg-primary text-primary-foreground font-medium text-sm neon-glow hover:neon-glow-strong transition-all">
Subscribe
</button>
</div>
</motion.div>
{/* Quick Links */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.1 }}
>
<h4 className="font-bold text-lg mb-6">Quick Links</h4>
<ul className="space-y-3">
{quickLinks.map((link) => (
<li key={link.name}>
<a
href={link.href}
className="text-muted-foreground hover:text-primary transition-colors relative group"
>
{link.name}
<span className="absolute -bottom-0.5 left-0 w-0 h-px bg-primary transition-all group-hover:w-full" />
</a>
</li>
))}
</ul>
</motion.div>
{/* Services */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.2 }}
>
<h4 className="font-bold text-lg mb-6">Services</h4>
<ul className="space-y-3">
{services.map((service) => (
<li key={service.name}>
<a
href={service.href}
className="text-muted-foreground hover:text-primary transition-colors relative group"
>
{service.name}
<span className="absolute -bottom-0.5 left-0 w-0 h-px bg-primary transition-all group-hover:w-full" />
</a>
</li>
))}
</ul>
</motion.div>
{/* Social & Contact */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.3 }}
>
<h4 className="font-bold text-lg mb-6">Connect With Us</h4>
<div className="flex gap-3 mb-6">
{socials.map((social) => (
<motion.a
key={social.label}
href={social.href}
whileHover={{ scale: 1.1, y: -2 }}
whileTap={{ scale: 0.95 }}
className="w-10 h-10 rounded-full glass border border-border/50 flex items-center justify-center hover:border-primary hover:neon-glow transition-all"
aria-label={social.label}
>
<social.icon className="w-4 h-4" />
</motion.a>
))}
</div>
<p className="text-muted-foreground text-sm">
Rangpur, Bangladesh
<br />
techzaa.alpha@gmail.com
<br />
+880 19623-21487
</p>
</motion.div>
</div>
{/* Bottom bar */}
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ delay: 0.4 }}
className="pt-8 border-t border-border"
>
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
<p className="text-muted-foreground text-sm text-center md:text-left">
© {new Date().getFullYear()} TechZaa. All rights reserved.
</p>
<div className="flex items-center gap-6 text-sm text-muted-foreground">
<a href="#" className="hover:text-primary transition-colors">
Privacy Policy
</a>
<a href="#" className="hover:text-primary transition-colors">
Terms of Service
</a>
<a href="#" className="hover:text-primary transition-colors">
Cookie Policy
</a>
</div>
</div>
</motion.div>
</div>
</footer>
);
}
+187
View File
@@ -0,0 +1,187 @@
import { useEffect, useRef } from 'react';
import { motion } from 'framer-motion';
import { ArrowRight, ChevronDown } from 'lucide-react';
import { Button } from '@/components/ui/button';
export default function HeroSection() {
const containerRef = useRef<HTMLDivElement>(null);
// Parallax effect for floating shapes
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!containerRef.current) return;
const { clientX, clientY } = e;
const { innerWidth, innerHeight } = window;
const x = (clientX / innerWidth - 0.5) * 20;
const y = (clientY / innerHeight - 0.5) * 20;
containerRef.current.style.setProperty('--mouse-x', `${x}px`);
containerRef.current.style.setProperty('--mouse-y', `${y}px`);
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return (
<section
id="home"
ref={containerRef}
className="relative min-h-screen flex items-center justify-center overflow-hidden pt-20"
>
{/* Animated gradient background */}
<div className="absolute inset-0 gradient-primary-animated opacity-20" />
{/* Particle dots */}
<div className="absolute inset-0 overflow-hidden">
{[...Array(50)].map((_, i) => (
<motion.div
key={i}
className="absolute w-1 h-1 rounded-full bg-primary/50"
style={{
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
}}
animate={{
opacity: [0.2, 0.8, 0.2],
scale: [1, 1.5, 1],
}}
transition={{
duration: 3 + Math.random() * 2,
repeat: Infinity,
delay: Math.random() * 2,
}}
/>
))}
</div>
{/* Floating geometric shapes */}
<motion.div
className="absolute top-1/4 left-[10%] w-32 h-32 border-2 border-primary/30 rounded-full"
style={{
transform: 'translate(calc(var(--mouse-x, 0) * -1), calc(var(--mouse-y, 0) * -1))',
}}
animate={{ rotate: 360 }}
transition={{ duration: 20, repeat: Infinity, ease: 'linear' }}
/>
<motion.div
className="absolute bottom-1/4 right-[15%] w-24 h-24 border-2 border-neon-purple/30 rotate-45"
style={{
transform: 'translate(calc(var(--mouse-x, 0) * 0.5), calc(var(--mouse-y, 0) * 0.5)) rotate(45deg)',
}}
animate={{ rotate: [45, 405] }}
transition={{ duration: 25, repeat: Infinity, ease: 'linear' }}
/>
<motion.div
className="absolute top-1/3 right-[25%] w-16 h-16 bg-neon-green/10 rounded-lg"
style={{
transform: 'translate(calc(var(--mouse-x, 0) * 1.5), calc(var(--mouse-y, 0) * 1.5))',
}}
animate={{
y: [-10, 10, -10],
rotate: [0, 180, 360],
}}
transition={{ duration: 8, repeat: Infinity, ease: 'easeInOut' }}
/>
{/* Glowing orbs */}
<div className="absolute top-1/2 left-1/4 w-64 h-64 bg-primary/20 rounded-full blur-[100px] animate-float" />
<div className="absolute bottom-1/3 right-1/4 w-48 h-48 bg-neon-purple/20 rounded-full blur-[80px] animate-float-delayed" />
{/* Content */}
<div className="relative z-10 container mx-auto px-4 text-center">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
{/* Badge */}
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.2 }}
className="inline-flex items-center gap-2 px-4 py-2 rounded-full glass mb-8"
>
<span className="w-2 h-2 rounded-full bg-primary animate-pulse" />
<span className="text-sm font-medium text-muted-foreground">
Innovating the Future
</span>
</motion.div>
{/* Main headline */}
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold mb-6 leading-tight">
<motion.span
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="block"
>
We Build the Future
</motion.span>
<motion.span
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="block text-primary neon-text-glow"
>
with Technology
</motion.span>
</h1>
{/* Subtext */}
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.7 }}
className="text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto mb-10"
>
Transforming ideas into powerful digital solutions. We craft innovative
web applications, mobile experiences, and AI-powered systems that drive
growth and redefine possibilities.
</motion.p>
{/* CTA Buttons */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.9 }}
className="flex flex-col sm:flex-row items-center justify-center gap-4"
>
<Button
size="lg"
className="bg-primary text-primary-foreground font-semibold px-8 py-6 text-lg rounded-full neon-glow hover:neon-glow-strong transition-all group"
>
Get Started
<ArrowRight className="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform" />
</Button>
<Button
size="lg"
variant="outline"
className="font-semibold px-8 py-6 text-lg rounded-full glass border-primary/50 hover:border-primary hover:bg-primary/10 transition-all"
>
View Our Work
</Button>
</motion.div>
</motion.div>
{/* Scroll indicator */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.5 }}
className="absolute -bottom-28 left-1/2 -translate-x-1/2"
>
<motion.a
href="#services"
className="flex flex-col items-center gap-2 text-muted-foreground hover:text-primary transition-colors"
animate={{ y: [0, 8, 0] }}
transition={{ duration: 2, repeat: Infinity }}
>
<span className="text-sm">Scroll to explore</span>
<ChevronDown className="w-5 h-5" />
</motion.a>
</motion.div>
</div>
</section>
);
}
+28
View File
@@ -0,0 +1,28 @@
import { NavLink as RouterNavLink, NavLinkProps } from "react-router-dom";
import { forwardRef } from "react";
import { cn } from "@/lib/utils";
interface NavLinkCompatProps extends Omit<NavLinkProps, "className"> {
className?: string;
activeClassName?: string;
pendingClassName?: string;
}
const NavLink = forwardRef<HTMLAnchorElement, NavLinkCompatProps>(
({ className, activeClassName, pendingClassName, to, ...props }, ref) => {
return (
<RouterNavLink
ref={ref}
to={to}
className={({ isActive, isPending }) =>
cn(className, isActive && activeClassName, isPending && pendingClassName)
}
{...props}
/>
);
},
);
NavLink.displayName = "NavLink";
export { NavLink };
+280
View File
@@ -0,0 +1,280 @@
import logo from "@/assets/logo.webp";
import { Button } from "@/components/ui/button";
import { useTheme } from "@/contexts/ThemeContext";
import { AnimatePresence, motion } from "framer-motion";
import { Menu, Moon, Sun, X } from "lucide-react";
import { useEffect, useState } from "react";
import ContactModal from "./ContactModal";
const navItems = [
{ name: "Home", href: "#home" },
{ name: "Services", href: "#services" },
{ name: "Projects", href: "#projects" },
{ name: "Team", href: "#team" },
{ name: "About", href: "#about" },
{ name: "Contact", href: "#contact" },
];
const accentColors = [
{ name: "blue", color: "bg-neon-blue" },
{ name: "purple", color: "bg-neon-purple" },
{ name: "green", color: "bg-neon-green" },
] as const;
export default function Navbar() {
const [isScrolled, setIsScrolled] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const { mode, accent, setAccent, toggleMode } = useTheme();
// Handle scroll effect
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 20);
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
// Close mobile menu on resize
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= 1024) {
setIsMobileMenuOpen(false);
}
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return (
<>
<motion.nav
initial={{ y: -100 }}
animate={{ y: 0 }}
transition={{ duration: 0.5, ease: "easeOut" }}
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
isScrolled ? "py-2 glass shadow-lg" : "py-4 bg-transparent"
}`}
>
<div className="container mx-auto px-4 flex items-center justify-between">
{/* Logo */}
<motion.a
href="#home"
className="flex items-center gap-2"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<img src={logo} alt="TechZaa" className="h-16 w-auto" />
</motion.a>
{/* Desktop Navigation */}
<div className="hidden lg:flex items-center gap-8">
{/* Nav Links */}
<ul className="flex items-center gap-6">
{navItems.map((item, index) => (
<motion.li
key={item.name}
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
>
<a
href={item.href}
className="relative text-foreground/80 hover:text-foreground transition-colors font-medium group"
>
{item.name}
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full neon-glow" />
</a>
</motion.li>
))}
</ul>
{/* Theme Controls */}
<div className="flex items-center gap-3">
{/* Accent Color Picker */}
<div className="flex items-center gap-1.5 p-1 rounded-full glass">
{accentColors.map((color) => (
<motion.button
key={color.name}
onClick={() => setAccent(color.name)}
className={`w-6 h-6 rounded-full ${color.color} transition-all ${
accent === color.name
? "ring-2 ring-white ring-offset-2 ring-offset-background"
: ""
}`}
whileHover={{ scale: 1.2 }}
whileTap={{ scale: 0.9 }}
aria-label={`Set ${color.name} theme`}
/>
))}
</div>
{/* Dark/Light Mode Toggle */}
<motion.button
onClick={toggleMode}
className="p-2 rounded-full glass hover:neon-glow transition-all"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
aria-label="Toggle theme mode"
>
<AnimatePresence mode="wait">
{mode === "dark" ? (
<motion.div
key="sun"
initial={{ rotate: -90, opacity: 0 }}
animate={{ rotate: 0, opacity: 1 }}
exit={{ rotate: 90, opacity: 0 }}
transition={{ duration: 0.2 }}
>
<Sun className="w-5 h-5 text-primary" />
</motion.div>
) : (
<motion.div
key="moon"
initial={{ rotate: 90, opacity: 0 }}
animate={{ rotate: 0, opacity: 1 }}
exit={{ rotate: -90, opacity: 0 }}
transition={{ duration: 0.2 }}
>
<Moon className="w-5 h-5 text-primary" />
</motion.div>
)}
</AnimatePresence>
</motion.button>
</div>
{/* CTA Button */}
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.5 }}
>
<Button
onClick={() => setIsModalOpen(true)}
className="bg-primary text-primary-foreground font-semibold px-6 py-2 rounded-full animate-pulse-glow hover:scale-105 transition-transform"
>
Let Me Talk
</Button>
</motion.div>
</div>
{/* Mobile Menu Button */}
<motion.button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="lg:hidden p-2 rounded-lg glass"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
aria-label="Toggle mobile menu"
>
<AnimatePresence mode="wait">
{isMobileMenuOpen ? (
<motion.div
key="close"
initial={{ rotate: -90, opacity: 0 }}
animate={{ rotate: 0, opacity: 1 }}
exit={{ rotate: 90, opacity: 0 }}
>
<X className="w-6 h-6" />
</motion.div>
) : (
<motion.div
key="menu"
initial={{ rotate: 90, opacity: 0 }}
animate={{ rotate: 0, opacity: 1 }}
exit={{ rotate: -90, opacity: 0 }}
>
<Menu className="w-6 h-6" />
</motion.div>
)}
</AnimatePresence>
</motion.button>
</div>
{/* Mobile Menu */}
<AnimatePresence>
{isMobileMenuOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
className="lg:hidden glass-strong mt-2 mx-4 rounded-2xl overflow-hidden"
>
<div className="p-6 space-y-6">
{/* Nav Links */}
<ul className="space-y-4">
{navItems.map((item, index) => (
<motion.li
key={item.name}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05 }}
>
<a
href={item.href}
onClick={() => setIsMobileMenuOpen(false)}
className="block text-lg font-medium text-foreground/80 hover:text-primary transition-colors"
>
{item.name}
</a>
</motion.li>
))}
</ul>
{/* Theme Controls */}
<div className="flex items-center justify-between pt-4 border-t border-border">
{/* Accent Colors */}
<div className="flex items-center gap-2">
{accentColors.map((color) => (
<button
key={color.name}
onClick={() => setAccent(color.name)}
className={`w-8 h-8 rounded-full ${color.color} transition-all ${
accent === color.name
? "ring-2 ring-white ring-offset-2 ring-offset-background"
: ""
}`}
aria-label={`Set ${color.name} theme`}
/>
))}
</div>
{/* Mode Toggle */}
<button
onClick={toggleMode}
className="p-2 rounded-full glass"
aria-label="Toggle theme mode"
>
{mode === "dark" ? (
<Sun className="w-5 h-5 text-primary" />
) : (
<Moon className="w-5 h-5 text-primary" />
)}
</button>
</div>
{/* CTA Button */}
<Button
onClick={() => {
setIsMobileMenuOpen(false);
setIsModalOpen(true);
}}
className="w-full bg-primary text-primary-foreground font-semibold py-3 rounded-full neon-glow"
>
Let Me Talk
</Button>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.nav>
{/* Contact Modal */}
<ContactModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
/>
</>
);
}
+42
View File
@@ -0,0 +1,42 @@
import { motion } from 'framer-motion';
import { ReactNode } from 'react';
interface PageTransitionProps {
children: ReactNode;
}
const pageVariants = {
initial: {
opacity: 0,
y: 20,
},
animate: {
opacity: 1,
y: 0,
transition: {
duration: 0.4,
ease: [0.25, 0.46, 0.45, 0.94] as const,
},
},
exit: {
opacity: 0,
y: -20,
transition: {
duration: 0.3,
ease: [0.25, 0.46, 0.45, 0.94] as const,
},
},
};
export default function PageTransition({ children }: PageTransitionProps) {
return (
<motion.div
variants={pageVariants}
initial="initial"
animate="animate"
exit="exit"
>
{children}
</motion.div>
);
}
+143
View File
@@ -0,0 +1,143 @@
import { useRef } from 'react';
import { motion, useInView } from 'framer-motion';
import { ArrowRight, ExternalLink } from 'lucide-react';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
const projects = [
{
title: 'FinTech Dashboard',
category: 'Web Application',
image: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=600&h=400&fit=crop',
color: 'from-neon-blue/80',
},
{
title: 'Health Tracker App',
category: 'Mobile App',
image: 'https://images.unsplash.com/photo-1576091160399-112ba8d25d1f?w=600&h=400&fit=crop',
color: 'from-neon-purple/80',
},
{
title: 'AI Content Platform',
category: 'AI Solution',
image: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=600&h=400&fit=crop',
color: 'from-neon-green/80',
},
{
title: 'E-Commerce Suite',
category: 'Web Application',
image: 'https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?w=600&h=400&fit=crop',
color: 'from-neon-blue/80',
},
{
title: 'Smart IoT Platform',
category: 'Cloud Solution',
image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=600&h=400&fit=crop',
color: 'from-neon-purple/80',
},
{
title: 'Learning Management',
category: 'Web Application',
image: 'https://images.unsplash.com/photo-1501504905252-473c47e087f8?w=600&h=400&fit=crop',
color: 'from-neon-green/80',
},
];
export default function ProjectsSection() {
const ref = useRef<HTMLElement>(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
return (
<section id="projects" ref={ref} className="py-24 relative overflow-hidden bg-secondary/30">
{/* Background pattern */}
<div className="absolute inset-0 opacity-5">
<div className="absolute inset-0" style={{
backgroundImage: `radial-gradient(circle at 1px 1px, currentColor 1px, transparent 0)`,
backgroundSize: '40px 40px',
}} />
</div>
<div className="container mx-auto px-4 relative z-10">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="text-center mb-16"
>
<span className="inline-block px-4 py-1.5 rounded-full glass text-sm font-medium text-primary mb-4">
Our Portfolio
</span>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-4">
Our <span className="text-primary neon-text-glow">Projects</span>
</h2>
<p className="text-muted-foreground max-w-2xl mx-auto text-lg">
Explore our latest work and see how we've helped businesses
transform their digital presence.
</p>
</motion.div>
{/* Projects Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
{projects.map((project, index) => (
<motion.div
key={project.title}
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: index * 0.1 }}
whileHover={{ y: -8 }}
className="group relative rounded-2xl overflow-hidden cursor-pointer"
>
{/* Image */}
<div className="aspect-[4/3] 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>
{/* Overlay */}
<div className={`absolute inset-0 bg-gradient-to-t ${project.color} to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300`} />
{/* Content */}
<div className="absolute inset-0 flex flex-col justify-end p-6">
<div className="transform translate-y-4 opacity-0 group-hover:translate-y-0 group-hover:opacity-100 transition-all duration-300">
<span className="inline-block px-3 py-1 rounded-full bg-background/20 backdrop-blur-sm text-white text-xs font-medium mb-2">
{project.category}
</span>
<h3 className="text-xl font-bold text-white mb-2">{project.title}</h3>
<div className="flex items-center gap-2 text-white/80 text-sm">
<span>View Project</span>
<ExternalLink className="w-4 h-4" />
</div>
</div>
</div>
{/* Border glow effect */}
<div className="absolute inset-0 rounded-2xl border-2 border-transparent group-hover:border-primary/50 group-hover:neon-glow transition-all duration-300 pointer-events-none" />
</motion.div>
))}
</div>
{/* See All Button */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.5 }}
className="text-center"
>
<Link to="/projects">
<Button
size="lg"
className="bg-primary text-primary-foreground font-semibold px-8 py-6 text-lg rounded-full neon-glow hover:neon-glow-strong transition-all group"
>
See All Projects
<ArrowRight className="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform" />
</Button>
</Link>
</motion.div>
</div>
</section>
);
}
+107
View File
@@ -0,0 +1,107 @@
import { useRef } from 'react';
import { motion, useInView } from 'framer-motion';
import { Globe, Smartphone, Brain, Cloud } from 'lucide-react';
const services = [
{
icon: Globe,
title: 'Web Development',
description: 'Custom web applications built with cutting-edge technologies. From stunning landing pages to complex enterprise solutions.',
color: 'neon-blue',
},
{
icon: Smartphone,
title: 'Mobile App Development',
description: 'Native and cross-platform mobile apps that deliver exceptional user experiences on iOS and Android.',
color: 'neon-purple',
},
{
icon: Brain,
title: 'AI Solutions',
description: 'Intelligent automation and machine learning solutions that transform data into actionable insights.',
color: 'neon-green',
},
{
icon: Cloud,
title: 'Cloud Solutions',
description: 'Scalable cloud infrastructure and DevOps practices that ensure your applications run smoothly at any scale.',
color: 'neon-blue',
},
];
export default function ServicesSection() {
const ref = useRef<HTMLElement>(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
return (
<section id="services" ref={ref} className="py-24 relative overflow-hidden">
{/* Background decoration */}
<div className="absolute inset-0 opacity-30">
<div className="absolute top-1/2 left-0 w-96 h-96 bg-primary/10 rounded-full blur-[150px]" />
<div className="absolute bottom-0 right-0 w-80 h-80 bg-neon-purple/10 rounded-full blur-[120px]" />
</div>
<div className="container mx-auto px-4 relative z-10">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="text-center mb-16"
>
<span className="inline-block px-4 py-1.5 rounded-full glass text-sm font-medium text-primary mb-4">
Our Expertise
</span>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-4">
What We <span className="text-primary neon-text-glow">Do</span>
</h2>
<p className="text-muted-foreground max-w-2xl mx-auto text-lg">
We deliver comprehensive technology solutions that empower businesses
to thrive in the digital age.
</p>
</motion.div>
{/* Services Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
{services.map((service, index) => (
<motion.div
key={service.title}
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: index * 0.1 }}
whileHover={{ y: -8, transition: { duration: 0.2 } }}
className="group relative"
>
{/* Card */}
<div className="relative h-full p-6 rounded-2xl glass border border-border/50 hover:border-primary/50 transition-all duration-300 overflow-hidden">
{/* Glow effect on hover */}
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500">
<div className="absolute inset-0 bg-gradient-to-br from-primary/10 to-transparent" />
</div>
{/* Icon */}
<motion.div
whileHover={{ scale: 1.1, rotate: 5 }}
className={`relative w-14 h-14 rounded-xl bg-${service.color}/10 flex items-center justify-center mb-5 group-hover:neon-glow transition-all duration-300`}
>
<service.icon className="w-7 h-7 text-primary" />
</motion.div>
{/* Content */}
<h3 className="text-xl font-bold mb-3 group-hover:text-primary transition-colors">
{service.title}
</h3>
<p className="text-muted-foreground text-sm leading-relaxed">
{service.description}
</p>
{/* Decorative corner */}
<div className="absolute bottom-0 right-0 w-20 h-20 bg-gradient-to-tl from-primary/5 to-transparent rounded-tl-full opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
</motion.div>
))}
</div>
</div>
</section>
);
}
+181
View File
@@ -0,0 +1,181 @@
import { motion, useInView } from "framer-motion";
import { Github, Linkedin, Twitter } from "lucide-react";
import { useRef } from "react";
// Import Swiper React components and modules
import { Autoplay, Pagination } from "swiper/modules";
import { Swiper, SwiperSlide } from "swiper/react";
// Import Swiper styles
import "swiper/css";
import "swiper/css/pagination";
const team = [
// {
// name: "Md Abumahid Islam",
// role: "CEO & Founder",
// image:
// "https://res.cloudinary.com/dnxsk9rgl/image/upload/v1772734622/abumahid_ghax5c.png",
// bio: "Visionary founder building innovative digital technology solutions.",
// github: "https://github.com/abumahid",
// twitter: "https://x.com/Abumahidislam",
// linkedin: "https://www.linkedin.com/in/md-abu-mahid-islam",
// },
{
name: "Rimi Rahman",
role: "Front-end Developer",
image:
"https://res.cloudinary.com/dnxsk9rgl/image/upload/v1772735809/rimi_easbn4.png",
bio: "Technology leader designing scalable and reliable software systems.",
github: "https://github.com/sanjidaRimi023",
twitter: "",
linkedin: "https://www.linkedin.com/in/sanjidarimi023/",
},
{
name: "Utsob Saha",
role: "Social Media Manager",
image:
"https://res.cloudinary.com/dnxsk9rgl/image/upload/v1772733893/utsob_txy0qe.png",
bio: "Driving brand growth through creative social media strategy.",
github: "https://github.com/Utsob1621",
twitter: "",
linkedin: "https://www.linkedin.com/in/utsob-saha-211894350/",
},
{
name: "Yeassin Ali",
role: "Front-end Developer",
image:
"https://res.cloudinary.com/dnxsk9rgl/image/upload/v1772735363/yeassing_pf6ta6.png",
bio: "Frontend specialist creating fast and responsive web interfaces.",
github: "https://github.com/Yeassin7376",
twitter: "",
linkedin: "https://www.linkedin.com/in/yeassin-ali17/",
},
{
name: "Sharafat Hossain",
role: "Front-end Developer",
image:
"https://res.cloudinary.com/dnxsk9rgl/image/upload/v1772736523/sharafat_pydaih.png",
bio: "Building modern web applications with full stack expertise.",
github: "https://github.com/dev-sharafat",
twitter: "",
linkedin: "https://www.linkedin.com/in/sharafathassain23/",
},
{
name: "Ripa Akter Badhon",
role: "HR Manager",
image:
"https://res.cloudinary.com/dnxsk9rgl/image/upload/v1772991488/badhon_ny4nsr.png",
bio: "Supporting team growth and employee wellbeing.",
github: "",
twitter: "",
linkedin: "",
},
];
export default function TeamSection() {
const ref = useRef<HTMLElement>(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
const shouldScroll = team.length >= 5;
return (
<section id="team" ref={ref} className="py-24 relative overflow-hidden">
{/* Background decoration */}
<div className="absolute inset-0 opacity-30">
<div className="absolute top-0 right-1/4 w-72 h-72 bg-neon-purple/10 rounded-full blur-[120px]" />
<div className="absolute bottom-0 left-1/4 w-64 h-64 bg-primary/10 rounded-full blur-[100px]" />
</div>
<div className="container mx-auto px-4 relative z-10">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="text-center mb-16"
>
<span className="inline-block px-4 py-1.5 rounded-full glass text-sm font-medium text-primary mb-4">
The Experts
</span>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-4">
Meet Our <span className="text-primary neon-text-glow">Team</span>
</h2>
<p className="text-muted-foreground max-w-2xl mx-auto text-lg">
A passionate team of innovators, designers, and developers.
</p>
</motion.div>
{/* Team Slider Container */}
<div className="w-full">
<Swiper
direction={"horizontal"}
slidesPerView={1}
spaceBetween={20}
loop={shouldScroll}
autoplay={
shouldScroll
? {
delay: 3000,
disableOnInteraction: false,
}
: false
}
pagination={{ clickable: true }}
modules={[Autoplay, Pagination]}
className="pb-12" // Space for pagination dots
breakpoints={{
640: { slidesPerView: 2 },
1024: { slidesPerView: 3 },
1280: { slidesPerView: 4 },
}}
>
{team.map((member, index) => (
<SwiperSlide key={member.name}>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: index * 0.1 }}
whileHover={{ y: -8 }}
className="group text-center p-4 h-full"
>
<div className="relative mb-6 mx-auto w-48 h-48">
<div className="absolute inset-0 rounded-full bg-gradient-to-r from-primary via-neon-purple to-neon-green opacity-0 group-hover:opacity-100 blur-md transition-opacity duration-500" />
<div className="relative w-full h-full rounded-full overflow-hidden border-4 border-border group-hover:border-primary/50 transition-colors duration-300 ">
<img
src={member.image}
alt={member.name}
className="absolute inset-0 w-full h-full object-cover object-top transition-transform duration-500 group-hover:scale-110"
/>
</div>
<div className="absolute inset-0 rounded-full bg-background/80 backdrop-blur-sm flex items-center justify-center gap-3 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<a href={member.linkedin} target="_blank">
<Linkedin className="w-5 h-5 cursor-pointer hover:text-primary transition-colors" />
</a>
<a href={member.twitter} target="_blank">
<Twitter className="w-5 h-5 cursor-pointer hover:text-primary transition-colors" />
</a>
<a href={member.github} target="_blank">
{" "}
<Github className="w-5 h-5 cursor-pointer hover:text-primary transition-colors" />
</a>
</div>
</div>
<h3 className="text-xl font-bold mb-1 group-hover:text-primary transition-colors">
{member.name}
</h3>
<p className="text-primary font-medium text-sm mb-2">
{member.role}
</p>
<p className="text-muted-foreground text-sm max-w-[250px] mx-auto">
{member.bio}
</p>
</motion.div>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
</section>
);
}
+201
View File
@@ -0,0 +1,201 @@
import { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Star, ChevronLeft, ChevronRight, Quote } from 'lucide-react';
import { Button } from '@/components/ui/button';
import useEmblaCarousel from 'embla-carousel-react';
import Autoplay from 'embla-carousel-autoplay';
const testimonials = [
{
id: 1,
name: 'Jennifer Martinez',
role: 'CEO, InnovateTech',
avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face',
rating: 5,
quote: 'TechZaa transformed our entire digital infrastructure. Their AI solutions increased our operational efficiency by 40%. Absolutely incredible team to work with!',
},
{
id: 2,
name: 'Michael Chen',
role: 'CTO, FutureScale',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face',
rating: 5,
quote: 'The mobile app they developed for us exceeded all expectations. User engagement increased by 200% within the first month. Highly recommend their services.',
},
{
id: 3,
name: 'Sarah Thompson',
role: 'Director of Operations, CloudFirst',
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face',
rating: 5,
quote: 'Their cloud migration expertise saved us countless hours and reduced our infrastructure costs by 35%. Professional, efficient, and always available.',
},
{
id: 4,
name: 'David Rodriguez',
role: 'Founder, StartupLabs',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face',
rating: 5,
quote: 'From concept to launch, TechZaa was with us every step. Their attention to detail and innovative approach made our product stand out in a crowded market.',
},
{
id: 5,
name: 'Emily Watson',
role: 'VP Engineering, DataFlow',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face',
rating: 5,
quote: 'The best tech partner we\'ve ever worked with. Their team understood our vision and delivered a solution that perfectly aligned with our business goals.',
},
];
export default function TestimonialsSection() {
const [emblaRef, emblaApi] = useEmblaCarousel(
{ loop: true, align: 'center' },
[Autoplay({ delay: 5000, stopOnInteraction: false })]
);
const [selectedIndex, setSelectedIndex] = useState(0);
const scrollPrev = useCallback(() => emblaApi?.scrollPrev(), [emblaApi]);
const scrollNext = useCallback(() => emblaApi?.scrollNext(), [emblaApi]);
const onSelect = useCallback(() => {
if (!emblaApi) return;
setSelectedIndex(emblaApi.selectedScrollSnap());
}, [emblaApi]);
useEffect(() => {
if (!emblaApi) return;
onSelect();
emblaApi.on('select', onSelect);
return () => {
emblaApi.off('select', onSelect);
};
}, [emblaApi, onSelect]);
return (
<section id="testimonials" className="py-24 relative overflow-hidden">
{/* Background */}
<div className="absolute inset-0">
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-primary/10 rounded-full blur-3xl" />
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-neon-purple/10 rounded-full blur-3xl" />
</div>
<div className="container mx-auto px-4 relative z-10">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="text-center mb-16"
>
<span className="inline-block px-4 py-2 rounded-full glass text-primary text-sm font-medium mb-4">
Client Stories
</span>
<h2 className="text-4xl md:text-5xl font-bold mb-6">
What Our <span className="text-primary neon-text-glow">Clients Say</span>
</h2>
<p className="text-muted-foreground max-w-2xl mx-auto text-lg">
Don't just take our word for it - hear from the amazing companies we've had the pleasure of working with.
</p>
</motion.div>
{/* Carousel */}
<div className="relative max-w-5xl mx-auto">
{/* Navigation Buttons */}
<Button
variant="outline"
size="icon"
onClick={scrollPrev}
className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-4 z-10 rounded-full glass border-primary/30 hover:neon-glow hidden md:flex"
>
<ChevronLeft className="w-5 h-5" />
</Button>
<Button
variant="outline"
size="icon"
onClick={scrollNext}
className="absolute right-0 top-1/2 -translate-y-1/2 translate-x-4 z-10 rounded-full glass border-primary/30 hover:neon-glow hidden md:flex"
>
<ChevronRight className="w-5 h-5" />
</Button>
{/* Carousel Container */}
<div className="overflow-hidden" ref={emblaRef}>
<div className="flex">
{testimonials.map((testimonial, index) => (
<div
key={testimonial.id}
className="flex-[0_0_100%] min-w-0 px-4 md:flex-[0_0_80%] lg:flex-[0_0_60%]"
>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{
opacity: selectedIndex === index ? 1 : 0.5,
scale: selectedIndex === index ? 1 : 0.9,
}}
transition={{ duration: 0.4 }}
className="glass-strong rounded-3xl p-8 md:p-10 relative"
>
{/* Quote Icon */}
<div className="absolute -top-4 -left-4 w-12 h-12 rounded-full bg-primary flex items-center justify-center neon-glow">
<Quote className="w-6 h-6 text-primary-foreground" />
</div>
{/* Stars */}
<div className="flex gap-1 mb-6">
{[...Array(testimonial.rating)].map((_, i) => (
<Star
key={i}
className="w-5 h-5 fill-primary text-primary"
/>
))}
</div>
{/* Quote */}
<p className="text-lg md:text-xl text-foreground/90 mb-8 leading-relaxed">
"{testimonial.quote}"
</p>
{/* Author */}
<div className="flex items-center gap-4">
<img
src={testimonial.avatar}
alt={testimonial.name}
className="w-14 h-14 rounded-full object-cover border-2 border-primary/50"
/>
<div>
<h4 className="font-bold text-foreground">
{testimonial.name}
</h4>
<p className="text-sm text-muted-foreground">
{testimonial.role}
</p>
</div>
</div>
</motion.div>
</div>
))}
</div>
</div>
{/* Dots Indicator */}
<div className="flex justify-center gap-2 mt-8">
{testimonials.map((_, index) => (
<button
key={index}
onClick={() => emblaApi?.scrollTo(index)}
className={`w-2 h-2 rounded-full transition-all duration-300 ${
selectedIndex === index
? 'w-8 bg-primary neon-glow'
: 'bg-muted-foreground/30 hover:bg-muted-foreground/50'
}`}
aria-label={`Go to slide ${index + 1}`}
/>
))}
</div>
</div>
</div>
</section>
);
}
+52
View File
@@ -0,0 +1,52 @@
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
+104
View File
@@ -0,0 +1,104 @@
import * as React from "react";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
);
AlertDialogHeader.displayName = "AlertDialogHeader";
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
);
AlertDialogFooter.displayName = "AlertDialogFooter";
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
{...props}
/>
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};
+43
View File
@@ -0,0 +1,43 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h5 ref={ref} className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props} />
),
);
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
),
);
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription };
+5
View File
@@ -0,0 +1,5 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
const AspectRatio = AspectRatioPrimitive.Root;
export { AspectRatio };
+38
View File
@@ -0,0 +1,38 @@
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils";
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image ref={ref} className={cn("aspect-square h-full w-full", className)} {...props} />
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback };
+29
View File
@@ -0,0 +1,29 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants };
+90
View File
@@ -0,0 +1,90 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils";
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className,
)}
{...props}
/>
),
);
BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
({ className, ...props }, ref) => (
<li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
),
);
BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a";
return <Comp ref={ref} className={cn("transition-colors hover:text-foreground", className)} {...props} />;
});
BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
),
);
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
<li role="presentation" aria-hidden="true" className={cn("[&>svg]:size-3.5", className)} {...props}>
{children ?? <ChevronRight />}
</li>
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};
+47
View File
@@ -0,0 +1,47 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
},
);
Button.displayName = "Button";
export { Button, buttonVariants };
+54
View File
@@ -0,0 +1,54 @@
import * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DayPicker } from "react-day-picker";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell: "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(buttonVariants({ variant: "ghost" }), "h-9 w-9 p-0 font-normal aria-selected:opacity-100"),
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ..._props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ..._props }) => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
);
}
Calendar.displayName = "Calendar";
export { Calendar };
+43
View File
@@ -0,0 +1,43 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)} {...props} />
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
),
);
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
),
);
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
),
);
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />,
);
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
),
);
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
+224
View File
@@ -0,0 +1,224 @@
import * as React from "react";
import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return context;
}
const Carousel = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & CarouselProps>(
({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }, ref) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins,
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return;
}
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext],
);
React.useEffect(() => {
if (!api || !setApi) {
return;
}
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
if (!api) {
return;
}
onSelect(api);
api.on("reInit", onSelect);
api.on("select", onSelect);
return () => {
api?.off("select", onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
},
);
Carousel.displayName = "Carousel";
const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel();
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn("flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", className)}
{...props}
/>
</div>
);
},
);
CarouselContent.displayName = "CarouselContent";
const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const { orientation } = useCarousel();
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn("min-w-0 shrink-0 grow-0 basis-full", orientation === "horizontal" ? "pl-4" : "pt-4", className)}
{...props}
/>
);
},
);
CarouselItem.displayName = "CarouselItem";
const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
);
},
);
CarouselPrevious.displayName = "CarouselPrevious";
const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
);
},
);
CarouselNext.displayName = "CarouselNext";
export { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };
+303
View File
@@ -0,0 +1,303 @@
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "@/lib/utils";
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> });
};
type ChartContextProps = {
config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
}
return context;
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig;
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className,
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
});
ChartContainer.displayName = "Chart";
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color);
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`,
)
.join("\n"),
}}
/>
);
};
const ChartTooltip = RechartsPrimitive.Tooltip;
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref,
) => {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
}
const [item] = payload;
const key = `${labelKey || item.dataKey || item.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
if (labelFormatter) {
return <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>;
}
if (!value) {
return null;
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);
if (!active || !payload?.length) {
return null;
}
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className,
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center",
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
})}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center",
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">{itemConfig?.label || item.name}</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
},
);
ChartTooltipContent.displayName = "ChartTooltip";
const ChartLegend = RechartsPrimitive.Legend;
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean;
nameKey?: string;
}
>(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
const { config } = useChart();
if (!payload?.length) {
return null;
}
return (
<div
ref={ref}
className={cn("flex items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
className={cn("flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground")}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
});
ChartLegendContent.displayName = "ChartLegend";
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
if (typeof payload !== "object" || payload === null) {
return undefined;
}
const payloadPayload =
"payload" in payload && typeof payload.payload === "object" && payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
}
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
}
export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle };
+26
View File
@@ -0,0 +1,26 @@
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };
+9
View File
@@ -0,0 +1,9 @@
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
+132
View File
@@ -0,0 +1,132 @@
import * as React from "react";
import { type DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import { cn } from "@/lib/utils";
import { Dialog, DialogContent } from "@/components/ui/dialog";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className,
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />);
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className,
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
className,
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};
+178
View File
@@ -0,0 +1,178 @@
import * as React from "react";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const ContextMenu = ContextMenuPrimitive.Root;
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
const ContextMenuGroup = ContextMenuPrimitive.Group;
const ContextMenuPortal = ContextMenuPrimitive.Portal;
const ContextMenuSub = ContextMenuPrimitive.Sub;
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
));
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
));
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
/>
));
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
));
ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
));
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold text-foreground", inset && "pl-8", className)}
{...props}
/>
));
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-border", className)} {...props} />
));
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
};
ContextMenuShortcut.displayName = "ContextMenuShortcut";
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
};
+95
View File
@@ -0,0 +1,95 @@
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
+87
View File
@@ -0,0 +1,87 @@
import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/lib/utils";
const Drawer = ({ shouldScaleBackground = true, ...props }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
);
Drawer.displayName = "Drawer";
const DrawerTrigger = DrawerPrimitive.Trigger;
const DrawerPortal = DrawerPrimitive.Portal;
const DrawerClose = DrawerPrimitive.Close;
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay ref={ref} className={cn("fixed inset-0 z-50 bg-black/80", className)} {...props} />
));
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className,
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
));
DrawerContent.displayName = "DrawerContent";
const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} {...props} />
);
DrawerHeader.displayName = "DrawerHeader";
const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
);
DrawerFooter.displayName = "DrawerFooter";
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
};
+179
View File
@@ -0,0 +1,179 @@
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent focus:bg-accent",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />;
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};
+129
View File
@@ -0,0 +1,129 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from "react-hook-form";
import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label";
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
},
);
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return <Label ref={ref} className={cn(error && "text-destructive", className)} htmlFor={formItemId} {...props} />;
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
);
},
);
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return <p ref={ref} id={formDescriptionId} className={cn("text-sm text-muted-foreground", className)} {...props} />;
},
);
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<p ref={ref} id={formMessageId} className={cn("text-sm font-medium text-destructive", className)} {...props}>
{body}
</p>
);
},
);
FormMessage.displayName = "FormMessage";
export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };
+27
View File
@@ -0,0 +1,27 @@
import * as React from "react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import { cn } from "@/lib/utils";
const HoverCard = HoverCardPrimitive.Root;
const HoverCardTrigger = HoverCardPrimitive.Trigger;
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
export { HoverCard, HoverCardTrigger, HoverCardContent };
+61
View File
@@ -0,0 +1,61 @@
import * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp";
import { Dot } from "lucide-react";
import { cn } from "@/lib/utils";
const InputOTP = React.forwardRef<React.ElementRef<typeof OTPInput>, React.ComponentPropsWithoutRef<typeof OTPInput>>(
({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn("flex items-center gap-2 has-[:disabled]:opacity-50", containerClassName)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
),
);
InputOTP.displayName = "InputOTP";
const InputOTPGroup = React.forwardRef<React.ElementRef<"div">, React.ComponentPropsWithoutRef<"div">>(
({ className, ...props }, ref) => <div ref={ref} className={cn("flex items-center", className)} {...props} />,
);
InputOTPGroup.displayName = "InputOTPGroup";
const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> & { index: number }
>(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
return (
<div
ref={ref}
className={cn(
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-2 ring-ring ring-offset-background",
className,
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink h-4 w-px bg-foreground duration-1000" />
</div>
)}
</div>
);
});
InputOTPSlot.displayName = "InputOTPSlot";
const InputOTPSeparator = React.forwardRef<React.ElementRef<"div">, React.ComponentPropsWithoutRef<"div">>(
({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<Dot />
</div>
),
);
InputOTPSeparator.displayName = "InputOTPSeparator";
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
+22
View File
@@ -0,0 +1,22 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input };
+17
View File
@@ -0,0 +1,17 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70");
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };
+207
View File
@@ -0,0 +1,207 @@
import * as React from "react";
import * as MenubarPrimitive from "@radix-ui/react-menubar";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const MenubarMenu = MenubarPrimitive.Menu;
const MenubarGroup = MenubarPrimitive.Group;
const MenubarPortal = MenubarPrimitive.Portal;
const MenubarSub = MenubarPrimitive.Sub;
const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn("flex h-10 items-center space-x-1 rounded-md border bg-background p-1", className)}
{...props}
/>
));
Menubar.displayName = MenubarPrimitive.Root.displayName;
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
/>
));
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
));
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(({ className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, ref) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</MenubarPrimitive.Portal>
));
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
/>
));
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
));
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
));
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props}
/>
));
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
));
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
};
MenubarShortcut.displayname = "MenubarShortcut";
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
};
+120
View File
@@ -0,0 +1,120 @@
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn("relative z-10 flex max-w-max flex-1 items-center justify-center", className)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn("group flex flex-1 list-none items-center justify-center space-x-1", className)}
{...props}
/>
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
);
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto",
className,
)}
{...props}
/>
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className,
)}
ref={ref}
{...props}
/>
</div>
));
NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className,
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
};
+81
View File
@@ -0,0 +1,81 @@
import * as React from "react";
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils";
import { ButtonProps, buttonVariants } from "@/components/ui/button";
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
);
Pagination.displayName = "Pagination";
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
({ className, ...props }, ref) => (
<ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} />
),
);
PaginationContent.displayName = "PaginationContent";
const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} />
));
PaginationItem.displayName = "PaginationItem";
type PaginationLinkProps = {
isActive?: boolean;
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">;
const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className,
)}
{...props}
/>
);
PaginationLink.displayName = "PaginationLink";
const PaginationPrevious = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink aria-label="Go to previous page" size="default" className={cn("gap-1 pl-2.5", className)} {...props}>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
);
PaginationPrevious.displayName = "PaginationPrevious";
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink aria-label="Go to next page" size="default" className={cn("gap-1 pr-2.5", className)} {...props}>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
);
PaginationNext.displayName = "PaginationNext";
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span aria-hidden className={cn("flex h-9 w-9 items-center justify-center", className)} {...props}>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
);
PaginationEllipsis.displayName = "PaginationEllipsis";
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
};
+29
View File
@@ -0,0 +1,29 @@
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent };
+23
View File
@@ -0,0 +1,23 @@
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@/lib/utils";
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress };
+36
View File
@@ -0,0 +1,36 @@
import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />;
});
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
});
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
export { RadioGroup, RadioGroupItem };
+37
View File
@@ -0,0 +1,37 @@
import { GripVertical } from "lucide-react";
import * as ResizablePrimitive from "react-resizable-panels";
import { cn } from "@/lib/utils";
const ResizablePanelGroup = ({ className, ...props }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn("flex h-full w-full data-[panel-group-direction=vertical]:flex-col", className)}
{...props}
/>
);
const ResizablePanel = ResizablePrimitive.Panel;
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean;
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className,
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
);
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
+38
View File
@@ -0,0 +1,38 @@
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils";
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root ref={ref} className={cn("relative overflow-hidden", className)} {...props}>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };
+143
View File
@@ -0,0 +1,143 @@
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label ref={ref} className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} {...props} />
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};
+20
View File
@@ -0,0 +1,20 @@
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn("shrink-0 bg-border", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", className)}
{...props}
/>
));
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };
+107
View File
@@ -0,0 +1,107 @@
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
},
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>(
({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-secondary hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
),
);
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title ref={ref} className={cn("text-lg font-semibold text-foreground", className)} {...props} />
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetOverlay,
SheetPortal,
SheetTitle,
SheetTrigger,
};
+637
View File
@@ -0,0 +1,637 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { VariantProps, cva } from "class-variance-authority";
import { PanelLeft } from "lucide-react";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Sheet, SheetContent } from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
const SIDEBAR_COOKIE_NAME = "sidebar:state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContext = {
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContext | null>(null);
function useSidebar() {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.");
}
return context;
}
const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
>(({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props }, ref) => {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState);
} else {
_setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open],
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
toggleSidebar();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo<SidebarContext>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
);
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn("group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar", className)}
ref={ref}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
);
});
SidebarProvider.displayName = "SidebarProvider";
const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
}
>(({ side = "left", variant = "sidebar", collapsible = "offcanvas", className, children, ...props }, ref) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") {
return (
<div
className={cn("flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground", className)}
ref={ref}
{...props}
>
{children}
</div>
);
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
);
}
return (
<div
ref={ref}
className="group peer hidden text-sidebar-foreground md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
"relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
)}
/>
<div
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
className,
)}
{...props}
>
<div
data-sidebar="sidebar"
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
{children}
</div>
</div>
</div>
);
});
Sidebar.displayName = "Sidebar";
const SidebarTrigger = React.forwardRef<React.ElementRef<typeof Button>, React.ComponentProps<typeof Button>>(
({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
return (
<Button
ref={ref}
data-sidebar="trigger"
variant="ghost"
size="icon"
className={cn("h-7 w-7", className)}
onClick={(event) => {
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);
},
);
SidebarTrigger.displayName = "SidebarTrigger";
const SidebarRail = React.forwardRef<HTMLButtonElement, React.ComponentProps<"button">>(
({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
return (
<button
ref={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] group-data-[side=left]:-right-4 group-data-[side=right]:left-0 hover:after:bg-sidebar-border sm:flex",
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className,
)}
{...props}
/>
);
},
);
SidebarRail.displayName = "SidebarRail";
const SidebarInset = React.forwardRef<HTMLDivElement, React.ComponentProps<"main">>(({ className, ...props }, ref) => {
return (
<main
ref={ref}
className={cn(
"relative flex min-h-svh flex-1 flex-col bg-background",
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className,
)}
{...props}
/>
);
});
SidebarInset.displayName = "SidebarInset";
const SidebarInput = React.forwardRef<React.ElementRef<typeof Input>, React.ComponentProps<typeof Input>>(
({ className, ...props }, ref) => {
return (
<Input
ref={ref}
data-sidebar="input"
className={cn(
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
className,
)}
{...props}
/>
);
},
);
SidebarInput.displayName = "SidebarInput";
const SidebarHeader = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
return <div ref={ref} data-sidebar="header" className={cn("flex flex-col gap-2 p-2", className)} {...props} />;
});
SidebarHeader.displayName = "SidebarHeader";
const SidebarFooter = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
return <div ref={ref} data-sidebar="footer" className={cn("flex flex-col gap-2 p-2", className)} {...props} />;
});
SidebarFooter.displayName = "SidebarFooter";
const SidebarSeparator = React.forwardRef<React.ElementRef<typeof Separator>, React.ComponentProps<typeof Separator>>(
({ className, ...props }, ref) => {
return (
<Separator
ref={ref}
data-sidebar="separator"
className={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props}
/>
);
},
);
SidebarSeparator.displayName = "SidebarSeparator";
const SidebarContent = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className,
)}
{...props}
/>
);
});
SidebarContent.displayName = "SidebarContent";
const SidebarGroup = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
);
});
SidebarGroup.displayName = "SidebarGroup";
const SidebarGroupLabel = React.forwardRef<HTMLDivElement, React.ComponentProps<"div"> & { asChild?: boolean }>(
({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "div";
return (
<Comp
ref={ref}
data-sidebar="group-label"
className={cn(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className,
)}
{...props}
/>
);
},
);
SidebarGroupLabel.displayName = "SidebarGroupLabel";
const SidebarGroupAction = React.forwardRef<HTMLButtonElement, React.ComponentProps<"button"> & { asChild?: boolean }>(
({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
ref={ref}
data-sidebar="group-action"
className={cn(
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
},
);
SidebarGroupAction.displayName = "SidebarGroupAction";
const SidebarGroupContent = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
({ className, ...props }, ref) => (
<div ref={ref} data-sidebar="group-content" className={cn("w-full text-sm", className)} {...props} />
),
);
SidebarGroupContent.displayName = "SidebarGroupContent";
const SidebarMenu = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(({ className, ...props }, ref) => (
<ul ref={ref} data-sidebar="menu" className={cn("flex w-full min-w-0 flex-col gap-1", className)} {...props} />
));
SidebarMenu.displayName = "SidebarMenu";
const SidebarMenuItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ className, ...props }, ref) => (
<li ref={ref} data-sidebar="menu-item" className={cn("group/menu-item relative", className)} {...props} />
));
SidebarMenuItem.displayName = "SidebarMenuItem";
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
const SidebarMenuButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean;
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>
>(({ asChild = false, isActive = false, variant = "default", size = "default", tooltip, className, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
const { isMobile, state } = useSidebar();
const button = (
<Comp
ref={ref}
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
);
if (!tooltip) {
return button;
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
};
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent side="right" align="center" hidden={state !== "collapsed" || isMobile} {...tooltip} />
</Tooltip>
);
});
SidebarMenuButton.displayName = "SidebarMenuButton";
const SidebarMenuAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean;
showOnHover?: boolean;
}
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
ref={ref}
data-sidebar="menu-action"
className={cn(
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform peer-hover/menu-button:text-sidebar-accent-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
className,
)}
{...props}
/>
);
});
SidebarMenuAction.displayName = "SidebarMenuAction";
const SidebarMenuBadge = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="menu-badge"
className={cn(
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
),
);
SidebarMenuBadge.displayName = "SidebarMenuBadge";
const SidebarMenuSkeleton = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
showIcon?: boolean;
}
>(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
ref={ref}
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />}
<Skeleton
className="h-4 max-w-[--skeleton-width] flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
);
});
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
const SidebarMenuSub = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu-sub"
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
),
);
SidebarMenuSub.displayName = "SidebarMenuSub";
const SidebarMenuSubItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ ...props }, ref) => (
<li ref={ref} {...props} />
));
SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement,
React.ComponentProps<"a"> & {
asChild?: boolean;
size?: "sm" | "md";
isActive?: boolean;
}
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a";
return (
<Comp
ref={ref}
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
});
SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
};
+7
View File
@@ -0,0 +1,7 @@
import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />;
}
export { Skeleton };
+23
View File
@@ -0,0 +1,23 @@
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import { cn } from "@/lib/utils";
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn("relative flex w-full touch-none select-none items-center", className)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider };
+27
View File
@@ -0,0 +1,27 @@
import { useTheme } from "next-themes";
import { Toaster as Sonner, toast } from "sonner";
type ToasterProps = React.ComponentProps<typeof Sonner>;
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
);
};
export { Toaster, toast };
+27
View File
@@ -0,0 +1,27 @@
import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cn } from "@/lib/utils";
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };
+72
View File
@@ -0,0 +1,72 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
</div>
),
);
Table.displayName = "Table";
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />,
);
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
),
);
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tfoot ref={ref} className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)} {...props} />
),
);
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn("border-b transition-colors data-[state=selected]:bg-muted hover:bg-muted/50", className)}
{...props}
/>
),
);
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
),
);
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<td ref={ref} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} {...props} />
),
);
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
({ className, ...props }, ref) => (
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
),
);
TableCaption.displayName = "TableCaption";
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
+53
View File
@@ -0,0 +1,53 @@
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className,
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className,
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };
+21
View File
@@ -0,0 +1,21 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
});
Textarea.displayName = "Textarea";
export { Textarea };
+111
View File
@@ -0,0 +1,111 @@
import * as React from "react";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className,
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive: "destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return <ToastPrimitives.Root ref={ref} className={cn(toastVariants({ variant }), className)} {...props} />;
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors group-[.destructive]:border-muted/40 hover:bg-secondary group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 group-[.destructive]:focus:ring-destructive disabled:pointer-events-none disabled:opacity-50",
className,
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity group-hover:opacity-100 group-[.destructive]:text-red-300 hover:text-foreground group-[.destructive]:hover:text-red-50 focus:opacity-100 focus:outline-none focus:ring-2 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className,
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title ref={ref} className={cn("text-sm font-semibold", className)} {...props} />
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description ref={ref} className={cn("text-sm opacity-90", className)} {...props} />
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
};
+24
View File
@@ -0,0 +1,24 @@
import { useToast } from "@/hooks/use-toast";
import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/toast";
export function Toaster() {
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && <ToastDescription>{description}</ToastDescription>}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}
+49
View File
@@ -0,0 +1,49 @@
import * as React from "react";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import { type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { toggleVariants } from "@/components/ui/toggle";
const ToggleGroupContext = React.createContext<VariantProps<typeof toggleVariants>>({
size: "default",
variant: "default",
});
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root ref={ref} className={cn("flex items-center justify-center gap-1", className)} {...props}>
<ToggleGroupContext.Provider value={{ variant, size }}>{children}</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
));
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className,
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
});
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
export { ToggleGroup, ToggleGroupItem };
+37
View File
@@ -0,0 +1,37 @@
import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3",
sm: "h-9 px-2.5",
lg: "h-11 px-5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root ref={ref} className={cn(toggleVariants({ variant, size, className }))} {...props} />
));
Toggle.displayName = TogglePrimitive.Root.displayName;
export { Toggle, toggleVariants };
+28
View File
@@ -0,0 +1,28 @@
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
+3
View File
@@ -0,0 +1,3 @@
import { useToast, toast } from "@/hooks/use-toast";
export { useToast, toast };
+63
View File
@@ -0,0 +1,63 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
type ThemeMode = 'light' | 'dark';
type AccentTheme = 'blue' | 'purple' | 'green';
interface ThemeContextType {
mode: ThemeMode;
accent: AccentTheme;
setMode: (mode: ThemeMode) => void;
setAccent: (accent: AccentTheme) => void;
toggleMode: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [mode, setModeState] = useState<ThemeMode>('dark');
const [accent, setAccentState] = useState<AccentTheme>('blue');
// Initialize theme from localStorage
useEffect(() => {
const savedMode = localStorage.getItem('techzaa-mode') as ThemeMode;
const savedAccent = localStorage.getItem('techzaa-accent') as AccentTheme;
if (savedMode) setModeState(savedMode);
if (savedAccent) setAccentState(savedAccent);
}, []);
// Apply theme classes to document
useEffect(() => {
const root = document.documentElement;
// Apply dark/light mode
root.classList.remove('light', 'dark');
root.classList.add(mode);
// Apply accent theme
root.classList.remove('theme-blue', 'theme-purple', 'theme-green');
root.classList.add(`theme-${accent}`);
// Save to localStorage
localStorage.setItem('techzaa-mode', mode);
localStorage.setItem('techzaa-accent', accent);
}, [mode, accent]);
const setMode = (newMode: ThemeMode) => setModeState(newMode);
const setAccent = (newAccent: AccentTheme) => setAccentState(newAccent);
const toggleMode = () => setModeState(prev => prev === 'dark' ? 'light' : 'dark');
return (
<ThemeContext.Provider value={{ mode, accent, setMode, setAccent, toggleMode }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
+551
View File
@@ -0,0 +1,551 @@
export interface Author {
name: string;
role: string;
avatar: string;
bio: string;
twitter?: string;
linkedin?: string;
}
export interface BlogPost {
id: number;
slug: string;
title: string;
excerpt: string;
content: string;
author: Author;
date: string;
readTime: string;
category: string;
image: string;
tags: string[];
}
export const authors: Record<string, Author> = {
alex: {
name: 'Alex Chen',
role: 'AI Research Lead',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face',
bio: 'Alex leads our AI research initiatives with 10+ years of experience in machine learning and enterprise software. Previously at Google Brain and OpenAI.',
twitter: 'alexchen',
linkedin: 'alexchen',
},
sarah: {
name: 'Sarah Johnson',
role: 'Cloud Architect',
avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face',
bio: 'Sarah is a certified AWS Solutions Architect with expertise in designing scalable cloud infrastructure. She has helped 50+ enterprises migrate to the cloud.',
twitter: 'sarahjcloud',
linkedin: 'sarahjohnson',
},
mike: {
name: 'Mike Torres',
role: 'Senior Frontend Developer',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face',
bio: 'Mike is passionate about creating beautiful, accessible web experiences. He specializes in React, TypeScript, and modern CSS frameworks.',
twitter: 'miketorres',
linkedin: 'miketorres',
},
emily: {
name: 'Emily Watson',
role: 'Mobile Development Lead',
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face',
bio: 'Emily leads our mobile development team, specializing in cross-platform solutions with React Native and Flutter.',
twitter: 'emilywatson',
linkedin: 'emilywatson',
},
};
export const blogPosts: BlogPost[] = [
{
id: 1,
slug: 'future-of-ai-enterprise-software',
title: 'The Future of AI in Enterprise Software',
excerpt: 'Discover how artificial intelligence is revolutionizing the way businesses operate and make decisions in the digital age.',
content: `
## Introduction
Artificial Intelligence is no longer a futuristic concept—it's here, and it's transforming how enterprises operate. From automating routine tasks to providing deep insights from vast amounts of data, AI is becoming an indispensable tool for businesses of all sizes.
## The Current State of Enterprise AI
Today's enterprise AI solutions go far beyond simple automation. Modern AI systems can:
- **Analyze complex patterns** in customer behavior and market trends
- **Predict outcomes** with unprecedented accuracy
- **Automate decision-making** in real-time scenarios
- **Enhance customer experiences** through personalization
### Key Technologies Driving Change
1. **Large Language Models (LLMs)** - Enabling natural language processing at scale
2. **Computer Vision** - Transforming visual data into actionable insights
3. **Reinforcement Learning** - Optimizing complex business processes
4. **Edge AI** - Bringing intelligence closer to data sources
## Real-World Applications
### Customer Service Automation
AI-powered chatbots and virtual assistants are handling up to 80% of routine customer inquiries, freeing human agents to focus on complex issues that require empathy and nuanced understanding.
### Predictive Analytics
Manufacturing companies are using AI to predict equipment failures before they happen, reducing downtime by up to 50% and saving millions in maintenance costs.
### Supply Chain Optimization
AI algorithms are revolutionizing supply chain management by predicting demand fluctuations, optimizing inventory levels, and identifying potential disruptions before they occur.
## Challenges and Considerations
While the potential of AI is immense, enterprises must navigate several challenges:
- **Data Quality** - AI is only as good as the data it's trained on
- **Integration Complexity** - Legacy systems often require significant updates
- **Skill Gaps** - Finding and retaining AI talent remains challenging
- **Ethical Considerations** - Ensuring AI systems are fair and unbiased
## Looking Ahead
The next five years will see AI become even more deeply embedded in enterprise operations. We predict:
- **Autonomous operations** will become standard in many industries
- **AI-human collaboration** will define new workplace dynamics
- **Personalization at scale** will become the norm, not the exception
## Conclusion
The enterprises that embrace AI today will be the leaders of tomorrow. The question is no longer whether to adopt AI, but how to do so strategically and responsibly.
`,
author: authors.alex,
date: '2024-01-15',
readTime: '8 min read',
category: 'AI & Machine Learning',
image: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=1200&h=600&fit=crop',
tags: ['AI', 'Enterprise', 'Machine Learning', 'Digital Transformation'],
},
{
id: 2,
slug: 'building-scalable-cloud-architecture',
title: 'Building Scalable Cloud Architecture',
excerpt: 'Learn the best practices for designing cloud infrastructure that grows with your business needs.',
content: `
## Why Scalability Matters
In today's digital landscape, the ability to scale your infrastructure quickly and efficiently can be the difference between success and failure. Whether you're handling a sudden surge in traffic or growing steadily over time, your architecture needs to adapt.
## Core Principles of Scalable Architecture
### 1. Design for Failure
Everything fails eventually. The key is to design systems that gracefully handle failures without impacting user experience.
- Implement redundancy at every layer
- Use circuit breakers to prevent cascade failures
- Design for eventual consistency where appropriate
### 2. Embrace Microservices
Breaking your application into smaller, independent services offers numerous benefits:
- **Independent scaling** - Scale only what needs scaling
- **Technology flexibility** - Use the right tool for each job
- **Faster deployments** - Update services without affecting others
- **Team autonomy** - Teams can work independently
### 3. Leverage Managed Services
Cloud providers offer powerful managed services that handle much of the operational burden:
- **Databases** - RDS, Aurora, DynamoDB
- **Caching** - ElastiCache, CloudFront
- **Messaging** - SQS, SNS, EventBridge
- **Compute** - Lambda, ECS, EKS
## Implementation Strategies
### Auto-Scaling Best Practices
Configure your auto-scaling policies to respond to actual demand:
\`\`\`yaml
scaling_policy:
metric: cpu_utilization
target: 70%
scale_up_cooldown: 60s
scale_down_cooldown: 300s
\`\`\`
### Container Orchestration
Kubernetes has become the standard for container orchestration:
- Use horizontal pod autoscaling
- Implement pod disruption budgets
- Leverage node affinity for optimal placement
## Cost Optimization
Scalability shouldn't break the bank:
- Use spot instances for non-critical workloads
- Implement right-sizing based on actual usage
- Take advantage of reserved capacity for predictable loads
## Monitoring and Observability
You can't scale what you can't measure:
- Implement comprehensive logging
- Use distributed tracing
- Set up proactive alerting
## Conclusion
Building scalable cloud architecture requires careful planning and continuous refinement. Start with solid principles, implement incrementally, and always measure the results.
`,
author: authors.sarah,
date: '2024-01-10',
readTime: '10 min read',
category: 'Cloud Solutions',
image: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=1200&h=600&fit=crop',
tags: ['Cloud', 'AWS', 'Architecture', 'DevOps', 'Kubernetes'],
},
{
id: 3,
slug: 'mobile-first-design-principles',
title: 'Mobile-First Design Principles',
excerpt: 'Why mobile-first approach is essential for modern web development and how to implement it effectively.',
content: `
## The Mobile-First Philosophy
Mobile-first design isn't just about making websites work on smaller screens—it's a fundamental shift in how we approach web development. By designing for mobile first, we create experiences that are focused, performant, and accessible.
## Why Mobile-First?
### The Numbers Don't Lie
- **60%+** of web traffic comes from mobile devices
- **Mobile users** are less tolerant of slow, clunky experiences
- **Google prioritizes** mobile-friendly sites in search rankings
### Constraints Drive Innovation
Designing for mobile first forces you to:
- Prioritize content ruthlessly
- Optimize for performance
- Create intuitive, touch-friendly interfaces
## Core Principles
### 1. Content Hierarchy
Every pixel matters on mobile. Ask yourself:
- What's the most important action on this page?
- What content can be hidden or removed?
- How can we streamline the user journey?
### 2. Touch-Friendly Design
Design for fingers, not cursors:
- Minimum touch target size: 44x44 pixels
- Adequate spacing between interactive elements
- Swipe gestures for common actions
### 3. Performance First
Mobile users often have slower connections:
- Optimize images with modern formats (WebP, AVIF)
- Implement lazy loading
- Minimize JavaScript bundle sizes
- Use service workers for offline capability
## Implementation Techniques
### Responsive CSS Strategy
Start with mobile styles, then enhance:
\`\`\`css
/* Mobile first - default styles */
.container {
padding: 1rem;
display: flex;
flex-direction: column;
}
/* Tablet and up */
@media (min-width: 768px) {
.container {
padding: 2rem;
flex-direction: row;
}
}
/* Desktop */
@media (min-width: 1024px) {
.container {
max-width: 1200px;
margin: 0 auto;
}
}
\`\`\`
### Progressive Enhancement
Layer functionality for capable devices:
1. Start with semantic HTML
2. Add CSS for visual styling
3. Enhance with JavaScript for interactivity
## Testing Your Mobile Design
- Use real devices, not just emulators
- Test on various network conditions
- Check accessibility with screen readers
- Validate with real users
## Conclusion
Mobile-first design is no longer optional—it's essential. By embracing constraints and focusing on what matters most, you'll create better experiences for all users.
`,
author: authors.mike,
date: '2024-01-05',
readTime: '7 min read',
category: 'Web Development',
image: 'https://images.unsplash.com/photo-1512941937669-90a1b58e7e9c?w=1200&h=600&fit=crop',
tags: ['Mobile', 'CSS', 'Responsive Design', 'UX', 'Performance'],
},
{
id: 4,
slug: 'cross-platform-mobile-development',
title: 'Cross-Platform Mobile Development in 2024',
excerpt: 'Comparing React Native, Flutter, and other frameworks for building mobile apps that work everywhere.',
content: `
## The Cross-Platform Landscape
Building native apps for both iOS and Android has traditionally meant maintaining two separate codebases. Cross-platform frameworks promise to change that, but which one should you choose?
## Framework Comparison
### React Native
**Pros:**
- Large community and ecosystem
- JavaScript/TypeScript familiarity
- Hot reloading for fast development
- Access to native modules
**Cons:**
- Performance overhead for complex animations
- Debugging can be challenging
- Some native features require bridging
### Flutter
**Pros:**
- Excellent performance with Dart
- Beautiful, customizable widgets
- Strong Google support
- Great for complex UIs
**Cons:**
- Dart learning curve
- Larger app sizes
- Smaller ecosystem than React Native
## Making the Right Choice
Consider these factors:
1. **Team expertise** - What does your team already know?
2. **App complexity** - How demanding are your UI requirements?
3. **Time to market** - How quickly do you need to ship?
4. **Long-term maintenance** - Who will maintain the app?
## Best Practices
### Code Organization
Keep your codebase maintainable:
- Separate business logic from UI
- Use state management solutions
- Write comprehensive tests
### Performance Optimization
- Profile early and often
- Optimize images and assets
- Use lazy loading for heavy components
## Conclusion
There's no one-size-fits-all answer. Evaluate your specific needs, experiment with both frameworks, and choose based on your team's strengths and project requirements.
`,
author: authors.emily,
date: '2024-01-02',
readTime: '6 min read',
category: 'Mobile Development',
image: 'https://images.unsplash.com/photo-1551650975-87deedd944c3?w=1200&h=600&fit=crop',
tags: ['Mobile', 'React Native', 'Flutter', 'Cross-Platform'],
},
{
id: 5,
slug: 'securing-your-cloud-infrastructure',
title: 'Securing Your Cloud Infrastructure',
excerpt: 'Essential security practices every organization should implement to protect their cloud resources.',
content: `
## Cloud Security Fundamentals
Moving to the cloud doesn't mean security becomes someone else's problem. While cloud providers secure the infrastructure, you're responsible for securing what you put on it.
## The Shared Responsibility Model
Understanding who is responsible for what:
- **Cloud Provider**: Physical security, network infrastructure, hypervisor
- **You**: Data, applications, identity management, network configuration
## Essential Security Practices
### 1. Identity and Access Management
- Implement least privilege access
- Use multi-factor authentication everywhere
- Regularly audit and rotate credentials
- Implement role-based access control
### 2. Network Security
- Use private subnets for sensitive resources
- Implement security groups and NACLs
- Enable VPC flow logs
- Use WAF for web applications
### 3. Data Protection
- Encrypt data at rest and in transit
- Implement key management best practices
- Regular backup and disaster recovery testing
- Data classification and handling policies
## Monitoring and Incident Response
- Enable CloudTrail/audit logging
- Set up automated alerting
- Have an incident response plan
- Conduct regular security assessments
## Conclusion
Cloud security is an ongoing process, not a one-time setup. Stay vigilant, keep learning, and always assume you could be targeted.
`,
author: authors.sarah,
date: '2023-12-28',
readTime: '9 min read',
category: 'Cloud Solutions',
image: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=1200&h=600&fit=crop',
tags: ['Security', 'Cloud', 'AWS', 'Best Practices'],
},
{
id: 6,
slug: 'introduction-to-llm-applications',
title: 'Building Applications with Large Language Models',
excerpt: 'A practical guide to integrating LLMs like GPT-4 into your applications for real-world use cases.',
content: `
## The LLM Revolution
Large Language Models have opened up possibilities that seemed like science fiction just a few years ago. From chatbots to code generation, LLMs are transforming how we build software.
## Understanding LLMs
### How They Work
LLMs are trained on vast amounts of text data to predict the next token in a sequence. This simple objective leads to remarkably capable systems that can:
- Generate human-like text
- Answer questions
- Summarize documents
- Write and explain code
- Translate languages
### Limitations to Consider
- **Hallucinations** - LLMs can generate plausible-sounding but incorrect information
- **Context limits** - There's a maximum amount of text they can process
- **Cost** - API calls can add up quickly
- **Latency** - Responses take time to generate
## Practical Applications
### Customer Support
Automate first-line support while maintaining quality:
- Use LLMs to understand customer intent
- Generate helpful responses
- Escalate complex issues to humans
### Content Generation
Speed up content creation:
- Draft marketing copy
- Generate product descriptions
- Create personalized emails
### Code Assistance
Boost developer productivity:
- Code completion and suggestions
- Documentation generation
- Bug explanation and fixes
## Best Practices
### Prompt Engineering
The quality of your prompts directly impacts results:
- Be specific and clear
- Provide context and examples
- Use system prompts to set behavior
### Safety and Guardrails
Protect your users and brand:
- Implement content filtering
- Set up rate limiting
- Monitor for misuse
## Conclusion
LLMs are powerful tools, but they require thoughtful implementation. Start with clear use cases, build incrementally, and always keep the user experience front and center.
`,
author: authors.alex,
date: '2023-12-20',
readTime: '11 min read',
category: 'AI & Machine Learning',
image: 'https://images.unsplash.com/photo-1676299081847-824916de030a?w=1200&h=600&fit=crop',
tags: ['AI', 'LLM', 'GPT', 'Machine Learning', 'NLP'],
},
];
export const getPostBySlug = (slug: string): BlogPost | undefined => {
return blogPosts.find(post => post.slug === slug);
};
export const getRelatedPosts = (currentPost: BlogPost, limit: number = 3): BlogPost[] => {
return blogPosts
.filter(post => post.id !== currentPost.id && post.category === currentPost.category)
.slice(0, limit);
};
+377
View File
@@ -0,0 +1,377 @@
export interface Project {
id: number;
slug: string;
title: string;
description: string;
fullDescription: string;
category: string;
image: string;
gallery: string[];
technologies: string[];
client: string;
year: string;
duration: string;
team: string;
liveUrl: string;
githubUrl: string;
features: string[];
challenges: string[];
results: { label: string; value: string }[];
}
export const projects: Project[] = [
{
id: 1,
slug: 'financeflow-dashboard',
title: 'FinanceFlow Dashboard',
description: 'A comprehensive financial analytics platform with real-time data visualization, predictive analytics, and automated reporting for enterprise clients.',
fullDescription: `FinanceFlow Dashboard is a cutting-edge financial analytics platform designed to transform how enterprises manage and visualize their financial data. Built with scalability and performance in mind, it handles millions of transactions while providing real-time insights.
The platform features an intuitive drag-and-drop interface for creating custom dashboards, advanced machine learning models for predictive analytics, and automated reporting that saves hours of manual work each week.`,
category: 'web',
image: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=1200&h=800&fit=crop',
gallery: [
'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&h=600&fit=crop',
'https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&h=600&fit=crop',
'https://images.unsplash.com/photo-1504868584819-f8e8b4b6d7e3?w=800&h=600&fit=crop',
],
technologies: ['React', 'TypeScript', 'D3.js', 'Node.js', 'PostgreSQL', 'Redis', 'AWS'],
client: 'FinanceFlow Inc.',
year: '2024',
duration: '8 months',
team: '6 developers',
liveUrl: '#',
githubUrl: '#',
features: [
'Real-time data streaming and visualization',
'Custom dashboard builder with drag-and-drop',
'AI-powered predictive analytics',
'Automated PDF and Excel report generation',
'Role-based access control',
'Multi-currency support',
'API integration with major banking systems',
],
challenges: [
'Handling real-time updates for millions of concurrent transactions',
'Ensuring sub-second query performance on large datasets',
'Building a flexible yet intuitive dashboard customization system',
],
results: [
{ label: 'Processing Speed', value: '10x faster' },
{ label: 'Report Generation', value: '95% automated' },
{ label: 'User Adoption', value: '98%' },
{ label: 'Cost Savings', value: '$2M/year' },
],
},
{
id: 2,
slug: 'healthtrack-mobile',
title: 'HealthTrack Mobile',
description: 'Cross-platform health monitoring app with wearable integration, personalized insights, and telehealth features for millions of users.',
fullDescription: `HealthTrack Mobile is a comprehensive health and wellness application that connects users with their health data, healthcare providers, and personalized wellness recommendations.
The app integrates with popular wearables and medical devices to provide a holistic view of the user's health, while AI-powered insights help users make informed decisions about their lifestyle and treatment plans.`,
category: 'mobile',
image: 'https://images.unsplash.com/photo-1576091160399-112ba8d25d1f?w=1200&h=800&fit=crop',
gallery: [
'https://images.unsplash.com/photo-1576091160399-112ba8d25d1f?w=800&h=600&fit=crop',
'https://images.unsplash.com/photo-1559526324-593bc073d938?w=800&h=600&fit=crop',
'https://images.unsplash.com/photo-1505751172876-fa1923c5c528?w=800&h=600&fit=crop',
],
technologies: ['React Native', 'Firebase', 'HealthKit', 'Google Fit', 'TensorFlow Lite', 'Node.js'],
client: 'HealthTrack',
year: '2024',
duration: '12 months',
team: '8 developers',
liveUrl: '#',
githubUrl: '#',
features: [
'Integration with 50+ wearable devices',
'Real-time health metrics tracking',
'AI-powered health insights and recommendations',
'Telehealth video consultations',
'Medication reminders and tracking',
'Emergency SOS with location sharing',
'HIPAA-compliant data storage',
],
challenges: [
'Ensuring HIPAA compliance across all data handling',
'Building reliable real-time sync with diverse wearable APIs',
'Optimizing battery usage while maintaining continuous monitoring',
],
results: [
{ label: 'Active Users', value: '2M+' },
{ label: 'App Store Rating', value: '4.8★' },
{ label: 'Health Goals Met', value: '73%' },
{ label: 'Doctor Consultations', value: '500K+' },
],
},
{
id: 3,
slug: 'smartassist-ai',
title: 'SmartAssist AI',
description: 'Enterprise AI assistant powered by advanced NLP for customer service automation, reducing response time by 80% and improving satisfaction.',
fullDescription: `SmartAssist AI is an enterprise-grade conversational AI platform that transforms customer service operations. Powered by state-of-the-art large language models and custom-trained on client-specific data, it provides human-like support at scale.
The system seamlessly integrates with existing CRM and helpdesk systems, learns from every interaction, and gracefully escalates to human agents when needed.`,
category: 'ai',
image: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=1200&h=800&fit=crop',
gallery: [
'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800&h=600&fit=crop',
'https://images.unsplash.com/photo-1676299081847-824916de030a?w=800&h=600&fit=crop',
'https://images.unsplash.com/photo-1535378917042-10a22c95931a?w=800&h=600&fit=crop',
],
technologies: ['Python', 'GPT-4', 'LangChain', 'FastAPI', 'PostgreSQL', 'Redis', 'Kubernetes'],
client: 'SmartAssist Corp',
year: '2024',
duration: '6 months',
team: '5 developers',
liveUrl: '#',
githubUrl: '#',
features: [
'Multi-language support (40+ languages)',
'Context-aware conversation handling',
'Custom knowledge base integration',
'Sentiment analysis and escalation',
'Real-time analytics dashboard',
'CRM and helpdesk integrations',
'Continuous learning from interactions',
],
challenges: [
'Minimizing hallucinations in customer-facing responses',
'Handling complex multi-turn conversations with context',
'Ensuring consistent brand voice across all interactions',
],
results: [
{ label: 'Response Time', value: '80% faster' },
{ label: 'Resolution Rate', value: '85%' },
{ label: 'Customer Satisfaction', value: '+40%' },
{ label: 'Cost Reduction', value: '60%' },
],
},
{
id: 4,
slug: 'cloudscale-infrastructure',
title: 'CloudScale Infrastructure',
description: 'Multi-region cloud infrastructure with auto-scaling, disaster recovery, and cost optimization for a global e-commerce platform.',
fullDescription: `CloudScale Infrastructure is a comprehensive cloud migration and modernization project that transformed a legacy e-commerce platform into a globally distributed, highly available system.
The new architecture handles Black Friday traffic spikes seamlessly, automatically scales based on demand, and reduced infrastructure costs by 35% while improving performance.`,
category: 'cloud',
image: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=1200&h=800&fit=crop',
gallery: [
'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=800&h=600&fit=crop',
'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=800&h=600&fit=crop',
'https://images.unsplash.com/photo-1544197150-b99a580bb7a8?w=800&h=600&fit=crop',
],
technologies: ['AWS', 'Terraform', 'Kubernetes', 'Docker', 'Prometheus', 'Grafana', 'ArgoCD'],
client: 'GlobalShop',
year: '2023',
duration: '10 months',
team: '7 developers',
liveUrl: '#',
githubUrl: '#',
features: [
'Multi-region active-active deployment',
'Automated scaling based on traffic patterns',
'Zero-downtime deployments',
'Comprehensive disaster recovery',
'Real-time monitoring and alerting',
'Infrastructure as Code with Terraform',
'GitOps-based deployment pipeline',
],
challenges: [
'Migrating live production traffic with zero downtime',
'Handling data consistency across regions',
'Optimizing costs while maintaining performance SLAs',
],
results: [
{ label: 'Uptime', value: '99.99%' },
{ label: 'Cost Savings', value: '35%' },
{ label: 'Deploy Time', value: '5 min' },
{ label: 'Scale Capacity', value: '100x' },
],
},
{
id: 5,
slug: 'edulearn-platform',
title: 'EduLearn Platform',
description: 'Interactive e-learning platform with live streaming, AI-powered tutoring, and gamification elements serving 500K+ students.',
fullDescription: `EduLearn Platform is a next-generation e-learning solution that combines live instruction with AI-powered personalization to create engaging educational experiences.
The platform supports live classes with thousands of concurrent students, provides personalized learning paths, and uses gamification to keep students motivated and engaged.`,
category: 'web',
image: 'https://images.unsplash.com/photo-1501504905252-473c47e087f8?w=1200&h=800&fit=crop',
gallery: [
'https://images.unsplash.com/photo-1501504905252-473c47e087f8?w=800&h=600&fit=crop',
'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=800&h=600&fit=crop',
'https://images.unsplash.com/photo-1434030216411-0b793f4b4173?w=800&h=600&fit=crop',
],
technologies: ['Next.js', 'WebRTC', 'PostgreSQL', 'Redis', 'AWS MediaLive', 'OpenAI'],
client: 'EduLearn',
year: '2023',
duration: '9 months',
team: '10 developers',
liveUrl: '#',
githubUrl: '#',
features: [
'Live streaming with 10K+ concurrent viewers',
'AI-powered personalized learning paths',
'Interactive quizzes and assessments',
'Gamification with badges and leaderboards',
'Progress tracking and analytics',
'Parent dashboard for monitoring',
'Mobile-friendly responsive design',
],
challenges: [
'Scaling live video to thousands of concurrent students',
'Building an effective AI tutoring system',
'Creating engaging gamification without distraction',
],
results: [
{ label: 'Active Students', value: '500K+' },
{ label: 'Course Completion', value: '78%' },
{ label: 'Learning Improvement', value: '+35%' },
{ label: 'Student Satisfaction', value: '4.9★' },
],
},
{
id: 6,
slug: 'foodiego-delivery',
title: 'FoodieGo Delivery',
description: 'On-demand food delivery app with real-time tracking, AI route optimization, and seamless payment integration.',
fullDescription: `FoodieGo Delivery is a feature-rich food delivery platform that connects hungry customers with local restaurants through an intuitive mobile experience.
The app features real-time order tracking, AI-powered delivery route optimization, and a seamless payment experience that has made it one of the top-rated delivery apps in its market.`,
category: 'mobile',
image: 'https://images.unsplash.com/photo-1565299624946-b28f40a0ae38?w=1200&h=800&fit=crop',
gallery: [
'https://images.unsplash.com/photo-1565299624946-b28f40a0ae38?w=800&h=600&fit=crop',
'https://images.unsplash.com/photo-1476224203421-9ac39bcb3327?w=800&h=600&fit=crop',
'https://images.unsplash.com/photo-1504674900247-0877df9cc836?w=800&h=600&fit=crop',
],
technologies: ['Flutter', 'Firebase', 'Google Maps', 'Stripe', 'Node.js', 'MongoDB'],
client: 'FoodieGo',
year: '2023',
duration: '7 months',
team: '6 developers',
liveUrl: '#',
githubUrl: '#',
features: [
'Real-time GPS order tracking',
'AI-optimized delivery routing',
'In-app payment with multiple options',
'Restaurant management dashboard',
'Driver app with navigation',
'Push notifications for order updates',
'Ratings and reviews system',
],
challenges: [
'Building accurate real-time tracking across different GPS conditions',
'Optimizing delivery routes with multiple concurrent orders',
'Handling peak hour traffic at scale',
],
results: [
{ label: 'Daily Orders', value: '50K+' },
{ label: 'Delivery Time', value: '25 min avg' },
{ label: 'Customer Rating', value: '4.7★' },
{ label: 'Driver Earnings', value: '+30%' },
],
},
{
id: 7,
slug: 'visionai-analytics',
title: 'VisionAI Analytics',
description: 'Computer vision platform for retail analytics, providing foot traffic analysis, heatmaps, and customer behavior insights.',
fullDescription: `VisionAI Analytics is a sophisticated computer vision platform that helps retailers understand customer behavior through advanced video analysis.
Using state-of-the-art AI models, the platform provides actionable insights on foot traffic patterns, dwell times, and customer journeys without compromising privacy.`,
category: 'ai',
image: 'https://images.unsplash.com/photo-1555949963-aa79dcee981c?w=1200&h=800&fit=crop',
gallery: [
'https://images.unsplash.com/photo-1555949963-aa79dcee981c?w=800&h=600&fit=crop',
'https://images.unsplash.com/photo-1553877522-43269d4ea984?w=800&h=600&fit=crop',
'https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=800&h=600&fit=crop',
],
technologies: ['PyTorch', 'OpenCV', 'TensorFlow', 'AWS SageMaker', 'FastAPI', 'React'],
client: 'RetailVision',
year: '2023',
duration: '8 months',
team: '5 developers',
liveUrl: '#',
githubUrl: '#',
features: [
'Real-time foot traffic counting',
'Customer journey heatmaps',
'Dwell time analysis',
'Queue management insights',
'Privacy-preserving analytics',
'Multi-store comparison',
'Custom alert configuration',
],
challenges: [
'Achieving high accuracy in crowded environments',
'Processing video streams in real-time at scale',
'Ensuring complete privacy compliance',
],
results: [
{ label: 'Accuracy', value: '98.5%' },
{ label: 'Stores Deployed', value: '200+' },
{ label: 'Sales Increase', value: '+15%' },
{ label: 'Staff Efficiency', value: '+25%' },
],
},
{
id: 8,
slug: 'securecloud-vault',
title: 'SecureCloud Vault',
description: 'Enterprise-grade secure file storage with end-to-end encryption, compliance tools, and advanced access controls.',
fullDescription: `SecureCloud Vault is a zero-trust file storage solution designed for enterprises with the most stringent security requirements.
Built with security-first principles, the platform provides end-to-end encryption, granular access controls, and comprehensive audit logging while maintaining the ease of use that modern teams expect.`,
category: 'cloud',
image: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=1200&h=800&fit=crop',
gallery: [
'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=800&h=600&fit=crop',
'https://images.unsplash.com/photo-1563986768609-322da13575f3?w=800&h=600&fit=crop',
'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?w=800&h=600&fit=crop',
],
technologies: ['Azure', 'Go', 'gRPC', 'HashiCorp Vault', 'PostgreSQL', 'React'],
client: 'SecureCloud',
year: '2023',
duration: '11 months',
team: '8 developers',
liveUrl: '#',
githubUrl: '#',
features: [
'End-to-end encryption (AES-256)',
'Zero-knowledge architecture',
'Granular access controls',
'Comprehensive audit logging',
'GDPR and HIPAA compliance',
'Secure file sharing with expiry',
'Integration with enterprise SSO',
],
challenges: [
'Implementing zero-knowledge encryption without sacrificing usability',
'Meeting compliance requirements across multiple jurisdictions',
'Building a performant system with encryption overhead',
],
results: [
{ label: 'Security Audits', value: '100% passed' },
{ label: 'Enterprise Clients', value: '150+' },
{ label: 'Data Protected', value: '50+ PB' },
{ label: 'Uptime', value: '99.999%' },
],
},
];
export const getProjectBySlug = (slug: string): Project | undefined => {
return projects.find(project => project.slug === slug);
};
export const getRelatedProjects = (currentProject: Project, limit: number = 3): Project[] => {
return projects
.filter(project => project.id !== currentProject.id && project.category === currentProject.category)
.slice(0, limit);
};
+19
View File
@@ -0,0 +1,19 @@
import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
}
+186
View File
@@ -0,0 +1,186 @@
import * as React from "react";
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & {
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const;
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString();
}
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
};
case "DISMISS_TOAST": {
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t,
),
};
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
};
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
},
},
});
return {
id: id,
dismiss,
update,
};
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
export { useToast, toast };
+286
View File
@@ -0,0 +1,286 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* TechZaa Design System
Brand Colors (HSL):
- Neon Blue: 193 100% 43% (#00B4D8)
- Electric Purple: 287 52% 36% (#7B2D8E)
- Neon Green: 146 51% 36% (#2E8B57)
*/
@layer base {
:root {
/* Theme accent (default: neon-blue) */
--neon-blue: 193 100% 43%;
--neon-purple: 287 52% 36%;
--neon-green: 146 51% 36%;
/* Active accent color (changes with theme) */
--accent-neon: var(--neon-blue);
--accent-neon-glow: 193 100% 60%;
--background: 0 0% 100%;
--foreground: 220 20% 10%;
--card: 0 0% 100%;
--card-foreground: 220 20% 10%;
--popover: 0 0% 100%;
--popover-foreground: 220 20% 10%;
--primary: var(--accent-neon);
--primary-foreground: 0 0% 100%;
--secondary: 220 15% 95%;
--secondary-foreground: 220 20% 10%;
--muted: 220 15% 95%;
--muted-foreground: 220 10% 45%;
--accent: 220 15% 96%;
--accent-foreground: 220 20% 10%;
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 100%;
--border: 220 15% 90%;
--input: 220 15% 90%;
--ring: var(--accent-neon);
--radius: 0.75rem;
/* Glassmorphism */
--glass-bg: 0 0% 100% / 0.7;
--glass-border: 0 0% 100% / 0.3;
--glass-blur: 20px;
/* Sidebar */
--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: 220 25% 6%;
--foreground: 0 0% 98%;
--card: 220 25% 8%;
--card-foreground: 0 0% 98%;
--popover: 220 25% 8%;
--popover-foreground: 0 0% 98%;
--primary: var(--accent-neon);
--primary-foreground: 0 0% 100%;
--secondary: 220 20% 15%;
--secondary-foreground: 0 0% 98%;
--muted: 220 20% 15%;
--muted-foreground: 220 10% 60%;
--accent: 220 20% 15%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62% 30%;
--destructive-foreground: 0 0% 98%;
--border: 220 20% 18%;
--input: 220 20% 18%;
--ring: var(--accent-neon);
/* Glassmorphism Dark */
--glass-bg: 220 25% 10% / 0.8;
--glass-border: 0 0% 100% / 0.1;
/* Sidebar Dark */
--sidebar-background: 220 25% 8%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 220 20% 15%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 220 20% 18%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
/* Theme variations */
.theme-blue {
--accent-neon: var(--neon-blue);
--accent-neon-glow: 193 100% 60%;
}
.theme-purple {
--accent-neon: var(--neon-purple);
--accent-neon-glow: 287 52% 55%;
}
.theme-green {
--accent-neon: var(--neon-green);
--accent-neon-glow: 146 51% 50%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground antialiased;
}
html {
scroll-behavior: smooth;
}
}
@layer utilities {
/* Neon glow effects */
.neon-glow {
box-shadow:
0 0 5px hsl(var(--accent-neon) / 0.5),
0 0 20px hsl(var(--accent-neon) / 0.3),
0 0 40px hsl(var(--accent-neon) / 0.2);
}
.neon-glow-strong {
box-shadow:
0 0 10px hsl(var(--accent-neon) / 0.8),
0 0 30px hsl(var(--accent-neon) / 0.5),
0 0 60px hsl(var(--accent-neon) / 0.3);
}
.neon-text-glow {
text-shadow:
0 0 10px hsl(var(--accent-neon) / 0.8),
0 0 20px hsl(var(--accent-neon) / 0.5),
0 0 40px hsl(var(--accent-neon) / 0.3);
}
/* Glassmorphism */
.glass {
background: hsl(var(--glass-bg));
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid hsl(var(--glass-border));
}
.glass-strong {
background: hsl(var(--glass-bg));
backdrop-filter: blur(30px);
-webkit-backdrop-filter: blur(30px);
border: 1px solid hsl(var(--glass-border));
}
/* Gradient backgrounds */
.gradient-primary {
background: linear-gradient(
135deg,
hsl(var(--neon-blue)) 0%,
hsl(var(--neon-purple)) 50%,
hsl(var(--neon-green)) 100%
);
}
.gradient-primary-animated {
background: linear-gradient(
135deg,
hsl(var(--neon-blue)) 0%,
hsl(var(--neon-purple)) 50%,
hsl(var(--neon-green)) 100%
);
background-size: 200% 200%;
animation: gradient-shift 8s ease infinite;
}
@keyframes gradient-shift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
/* Animated border gradient */
.border-gradient {
position: relative;
background: hsl(var(--card));
border-radius: var(--radius);
}
.border-gradient::before {
content: '';
position: absolute;
inset: -2px;
border-radius: calc(var(--radius) + 2px);
background: linear-gradient(
135deg,
hsl(var(--neon-blue)),
hsl(var(--neon-purple)),
hsl(var(--neon-green))
);
z-index: -1;
}
/* Floating animation */
.animate-float {
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
.animate-float-delayed {
animation: float 6s ease-in-out infinite;
animation-delay: 2s;
}
/* Pulse glow */
.animate-pulse-glow {
animation: pulse-glow 2s ease-in-out infinite;
}
@keyframes pulse-glow {
0%, 100% {
box-shadow:
0 0 5px hsl(var(--accent-neon) / 0.5),
0 0 20px hsl(var(--accent-neon) / 0.3);
}
50% {
box-shadow:
0 0 15px hsl(var(--accent-neon) / 0.8),
0 0 40px hsl(var(--accent-neon) / 0.5),
0 0 60px hsl(var(--accent-neon) / 0.3);
}
}
/* Shimmer effect */
.shimmer {
position: relative;
overflow: hidden;
}
.shimmer::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
transparent,
hsl(var(--accent-neon) / 0.1),
transparent
);
transform: translateX(-100%);
animation: shimmer 3s infinite;
}
@keyframes shimmer {
100% { transform: translateX(100%); }
}
}
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+5
View File
@@ -0,0 +1,5 @@
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
createRoot(document.getElementById("root")!).render(<App />);
+200
View File
@@ -0,0 +1,200 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { ArrowLeft, Calendar, Clock, User, Search } from 'lucide-react';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import Navbar from '@/components/Navbar';
import Footer from '@/components/Footer';
import PageTransition from '@/components/PageTransition';
import { blogPosts } from '@/data/blogData';
const categories = ['All', 'AI & Machine Learning', 'Cloud Solutions', 'Web Development', 'Mobile Development'];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.1 },
},
};
const itemVariants = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5, ease: 'easeOut' as const },
},
};
export default function Blog() {
const [activeCategory, setActiveCategory] = useState('All');
const [searchQuery, setSearchQuery] = useState('');
const filteredPosts = blogPosts.filter(post => {
const matchesCategory = activeCategory === 'All' || post.category === activeCategory;
const matchesSearch = post.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
post.excerpt.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
});
return (
<PageTransition>
<div className="min-h-screen bg-background overflow-x-hidden">
<Navbar />
{/* Hero Section */}
<section className="pt-32 pb-16 relative overflow-hidden">
<div className="absolute inset-0">
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-primary/20 rounded-full blur-3xl" />
<div className="absolute bottom-0 right-1/4 w-64 h-64 bg-neon-purple/20 rounded-full blur-3xl" />
</div>
<div className="container mx-auto px-4 relative z-10">
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5 }}
>
<Link to="/">
<Button variant="ghost" className="mb-8 group">
<ArrowLeft className="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" />
Back to Home
</Button>
</Link>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-center mb-12"
>
<h1 className="text-5xl md:text-6xl font-bold mb-6">
Our <span className="text-primary neon-text-glow">Blog</span>
</h1>
<p className="text-muted-foreground max-w-2xl mx-auto text-lg">
Insights, tutorials, and thought leadership from our team of experts.
</p>
</motion.div>
{/* Search */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="max-w-md mx-auto mb-8"
>
<div className="relative">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
<Input
type="text"
placeholder="Search articles..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-12 py-6 rounded-full glass border-primary/30 focus:border-primary"
/>
</div>
</motion.div>
{/* Category Filter */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="flex flex-wrap justify-center gap-3 mb-16"
>
{categories.map((category) => (
<Button
key={category}
variant={activeCategory === category ? 'default' : 'outline'}
onClick={() => setActiveCategory(category)}
className={`rounded-full px-6 transition-all duration-300 ${
activeCategory === category
? 'neon-glow bg-primary text-primary-foreground'
: 'glass border-primary/30 hover:border-primary'
}`}
>
{category}
</Button>
))}
</motion.div>
</div>
</section>
{/* Blog Posts Grid */}
<section className="pb-24">
<div className="container mx-auto px-4">
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="grid md:grid-cols-2 lg:grid-cols-3 gap-8"
>
{filteredPosts.map((post) => (
<motion.article
key={post.id}
variants={itemVariants}
className="group glass rounded-2xl overflow-hidden hover:neon-glow transition-all duration-500"
>
<Link to={`/blog/${post.slug}`}>
<div className="relative h-48 overflow-hidden">
<img
src={post.image}
alt={post.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" />
<span className="absolute bottom-4 left-4 px-3 py-1 rounded-full bg-primary/20 text-primary text-xs font-medium backdrop-blur-sm">
{post.category}
</span>
</div>
<div className="p-6">
<h3 className="text-xl font-bold mb-3 group-hover:text-primary transition-colors line-clamp-2">
{post.title}
</h3>
<p className="text-muted-foreground text-sm mb-4 line-clamp-2">
{post.excerpt}
</p>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<User className="w-3 h-3" />
{post.author.name}
</span>
<span className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
{new Date(post.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</span>
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{post.readTime}
</span>
</div>
</div>
</Link>
</motion.article>
))}
</motion.div>
{filteredPosts.length === 0 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-20"
>
<p className="text-muted-foreground text-lg">
No articles found matching your criteria.
</p>
</motion.div>
)}
</div>
</section>
<Footer />
</div>
</PageTransition>
);
}
+346
View File
@@ -0,0 +1,346 @@
import { useParams, Link, useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import { ArrowLeft, Calendar, Clock, Twitter, Linkedin, Facebook, Link2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import Navbar from '@/components/Navbar';
import Footer from '@/components/Footer';
import PageTransition from '@/components/PageTransition';
import { getPostBySlug, getRelatedPosts } from '@/data/blogData';
export default function BlogArticle() {
const { slug } = useParams<{ slug: string }>();
const navigate = useNavigate();
const post = getPostBySlug(slug || '');
if (!post) {
return (
<PageTransition>
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">Article Not Found</h1>
<p className="text-muted-foreground mb-8">The article you're looking for doesn't exist.</p>
<Button onClick={() => navigate('/blog')}>Back to Blog</Button>
</div>
</div>
</PageTransition>
);
}
const relatedPosts = getRelatedPosts(post);
const shareUrl = window.location.href;
const handleShare = (platform: string) => {
const urls: Record<string, string> = {
twitter: `https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(post.title)}`,
linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}`,
facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`,
};
if (platform === 'copy') {
navigator.clipboard.writeText(shareUrl);
toast.success('Link copied to clipboard!');
} else {
window.open(urls[platform], '_blank', 'width=600,height=400');
}
};
return (
<PageTransition>
<div className="min-h-screen bg-background overflow-x-hidden">
<Navbar />
{/* Hero */}
<section className="pt-32 pb-16 relative overflow-hidden">
<div className="absolute inset-0">
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-primary/20 rounded-full blur-3xl" />
</div>
<div className="container mx-auto px-4 relative z-10 max-w-4xl">
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5 }}
>
<Link to="/blog">
<Button variant="ghost" className="mb-8 group">
<ArrowLeft className="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" />
Back to Blog
</Button>
</Link>
</motion.div>
{/* Category */}
<motion.span
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="inline-block px-4 py-1.5 rounded-full bg-primary/20 text-primary text-sm font-medium mb-6"
>
{post.category}
</motion.span>
{/* Title */}
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-4xl md:text-5xl font-bold mb-6 leading-tight"
>
{post.title}
</motion.h1>
{/* Meta */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="flex flex-wrap items-center gap-6 text-muted-foreground mb-8"
>
<div className="flex items-center gap-3">
<img
src={post.author.avatar}
alt={post.author.name}
className="w-10 h-10 rounded-full object-cover border-2 border-primary/50"
/>
<div>
<p className="font-medium text-foreground">{post.author.name}</p>
<p className="text-sm">{post.author.role}</p>
</div>
</div>
<span className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
{new Date(post.date).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}
</span>
<span className="flex items-center gap-2">
<Clock className="w-4 h-4" />
{post.readTime}
</span>
</motion.div>
{/* Featured Image */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="rounded-2xl overflow-hidden mb-12"
>
<img
src={post.image}
alt={post.title}
className="w-full h-[400px] object-cover"
/>
</motion.div>
</div>
</section>
{/* Content */}
<section className="pb-16">
<div className="container mx-auto px-4 max-w-4xl">
<div className="grid lg:grid-cols-[1fr_200px] gap-12">
{/* Article Content */}
<motion.article
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
className="max-w-none prose-content"
>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
h1: ({ children }) => <h1 className="text-3xl font-bold text-foreground mt-8 mb-4">{children}</h1>,
h2: ({ children }) => <h2 className="text-2xl font-bold text-foreground mt-8 mb-4">{children}</h2>,
h3: ({ children }) => <h3 className="text-xl font-bold text-foreground mt-6 mb-3">{children}</h3>,
h4: ({ children }) => <h4 className="text-lg font-bold text-foreground mt-4 mb-2">{children}</h4>,
p: ({ children }) => <p className="text-muted-foreground leading-relaxed mb-4">{children}</p>,
ul: ({ children }) => <ul className="list-disc list-inside text-muted-foreground space-y-2 mb-4 ml-4">{children}</ul>,
ol: ({ children }) => <ol className="list-decimal list-inside text-muted-foreground space-y-2 mb-4 ml-4">{children}</ol>,
li: ({ children }) => <li className="text-muted-foreground">{children}</li>,
a: ({ href, children }) => <a href={href} className="text-primary hover:underline">{children}</a>,
strong: ({ children }) => <strong className="font-bold text-foreground">{children}</strong>,
em: ({ children }) => <em className="italic">{children}</em>,
blockquote: ({ children }) => (
<blockquote className="border-l-4 border-primary pl-4 italic text-muted-foreground my-4">
{children}
</blockquote>
),
code: ({ className, children }) => {
const isInline = !className;
if (isInline) {
return <code className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">{children}</code>;
}
return (
<code className="block bg-muted border border-border rounded-lg p-4 overflow-x-auto text-sm font-mono my-4">
{children}
</code>
);
},
pre: ({ children }) => <pre className="bg-muted border border-border rounded-lg p-4 overflow-x-auto my-4">{children}</pre>,
hr: () => <hr className="border-border my-8" />,
}}
>
{post.content}
</ReactMarkdown>
</motion.article>
{/* Sidebar */}
<aside className="hidden lg:block">
<div className="sticky top-32 space-y-8">
{/* Share */}
<div className="glass rounded-2xl p-6">
<h4 className="font-bold mb-4 text-sm uppercase tracking-wide text-muted-foreground">Share</h4>
<div className="flex flex-col gap-3">
<Button
variant="outline"
size="sm"
onClick={() => handleShare('twitter')}
className="justify-start gap-2 glass border-primary/30"
>
<Twitter className="w-4 h-4" /> Twitter
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleShare('linkedin')}
className="justify-start gap-2 glass border-primary/30"
>
<Linkedin className="w-4 h-4" /> LinkedIn
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleShare('facebook')}
className="justify-start gap-2 glass border-primary/30"
>
<Facebook className="w-4 h-4" /> Facebook
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleShare('copy')}
className="justify-start gap-2 glass border-primary/30"
>
<Link2 className="w-4 h-4" /> Copy Link
</Button>
</div>
</div>
{/* Tags */}
<div className="glass rounded-2xl p-6">
<h4 className="font-bold mb-4 text-sm uppercase tracking-wide text-muted-foreground">Tags</h4>
<div className="flex flex-wrap gap-2">
{post.tags.map((tag) => (
<span
key={tag}
className="px-3 py-1 rounded-full bg-muted text-muted-foreground text-xs"
>
{tag}
</span>
))}
</div>
</div>
</div>
</aside>
</div>
</div>
</section>
{/* Author Bio */}
<section className="py-16 bg-secondary/30">
<div className="container mx-auto px-4 max-w-4xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="glass rounded-3xl p-8 flex flex-col md:flex-row gap-6 items-center md:items-start"
>
<img
src={post.author.avatar}
alt={post.author.name}
className="w-24 h-24 rounded-full object-cover border-4 border-primary/50"
/>
<div className="text-center md:text-left">
<h3 className="text-2xl font-bold mb-2">{post.author.name}</h3>
<p className="text-primary font-medium mb-4">{post.author.role}</p>
<p className="text-muted-foreground mb-4">{post.author.bio}</p>
<div className="flex gap-3 justify-center md:justify-start">
{post.author.twitter && (
<a
href={`https://twitter.com/${post.author.twitter}`}
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-full glass hover:neon-glow transition-all"
>
<Twitter className="w-5 h-5" />
</a>
)}
{post.author.linkedin && (
<a
href={`https://linkedin.com/in/${post.author.linkedin}`}
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-full glass hover:neon-glow transition-all"
>
<Linkedin className="w-5 h-5" />
</a>
)}
</div>
</div>
</motion.div>
</div>
</section>
{/* Related Articles */}
{relatedPosts.length > 0 && (
<section className="py-24">
<div className="container mx-auto px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<h2 className="text-3xl font-bold">Related Articles</h2>
</motion.div>
<div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
{relatedPosts.map((relatedPost, index) => (
<motion.article
key={relatedPost.id}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1 }}
className="group glass rounded-2xl overflow-hidden hover:neon-glow transition-all duration-500"
>
<Link to={`/blog/${relatedPost.slug}`}>
<div className="relative h-40 overflow-hidden">
<img
src={relatedPost.image}
alt={relatedPost.title}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
/>
</div>
<div className="p-5">
<h3 className="font-bold mb-2 group-hover:text-primary transition-colors line-clamp-2">
{relatedPost.title}
</h3>
<p className="text-sm text-muted-foreground flex items-center gap-2">
<Clock className="w-3 h-3" /> {relatedPost.readTime}
</p>
</div>
</Link>
</motion.article>
))}
</div>
</div>
</section>
)}
<Footer />
</div>
</PageTransition>
);
}
+34
View File
@@ -0,0 +1,34 @@
import Navbar from '@/components/Navbar';
import HeroSection from '@/components/HeroSection';
import ServicesSection from '@/components/ServicesSection';
import ProjectsSection from '@/components/ProjectsSection';
import TeamSection from '@/components/TeamSection';
import AboutSection from '@/components/AboutSection';
import TestimonialsSection from '@/components/TestimonialsSection';
import BlogSection from '@/components/BlogSection';
import FAQSection from '@/components/FAQSection';
import ContactSection from '@/components/ContactSection';
import Footer from '@/components/Footer';
import PageTransition from '@/components/PageTransition';
const Index = () => {
return (
<PageTransition>
<div className="min-h-screen bg-background overflow-x-hidden">
<Navbar />
<HeroSection />
<ServicesSection />
<ProjectsSection />
<TeamSection />
<AboutSection />
<TestimonialsSection />
<BlogSection />
<FAQSection />
<ContactSection />
<Footer />
</div>
</PageTransition>
);
};
export default Index;
+24
View File
@@ -0,0 +1,24 @@
import { useLocation } from "react-router-dom";
import { useEffect } from "react";
const NotFound = () => {
const location = useLocation();
useEffect(() => {
console.error("404 Error: User attempted to access non-existent route:", location.pathname);
}, [location.pathname]);
return (
<div className="flex min-h-screen items-center justify-center bg-muted">
<div className="text-center">
<h1 className="mb-4 text-4xl font-bold">404</h1>
<p className="mb-4 text-xl text-muted-foreground">Oops! Page not found</p>
<a href="/" className="text-primary underline hover:text-primary/90">
Return to Home
</a>
</div>
</div>
);
};
export default NotFound;
+372
View File
@@ -0,0 +1,372 @@
import { useParams, Link, useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import { ArrowLeft, ExternalLink, Github, Calendar, Clock, Users, ChevronRight } from 'lucide-react';
import { Button } from '@/components/ui/button';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import Navbar from '@/components/Navbar';
import Footer from '@/components/Footer';
import PageTransition from '@/components/PageTransition';
import { getProjectBySlug, getRelatedProjects } from '@/data/projectData';
export default function ProjectDetails() {
const { slug } = useParams<{ slug: string }>();
const navigate = useNavigate();
const project = getProjectBySlug(slug || '');
if (!project) {
return (
<PageTransition>
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">Project Not Found</h1>
<p className="text-muted-foreground mb-8">The project you're looking for doesn't exist.</p>
<Button onClick={() => navigate('/projects')}>Back to Projects</Button>
</div>
</div>
</PageTransition>
);
}
const relatedProjects = getRelatedProjects(project);
const categoryLabels: Record<string, string> = {
web: 'Web Development',
mobile: 'Mobile App',
ai: 'AI & Machine Learning',
cloud: 'Cloud Solutions',
};
return (
<PageTransition>
<div className="min-h-screen bg-background overflow-x-hidden">
<Navbar />
{/* Hero */}
<section className="pt-32 pb-16 relative overflow-hidden">
<div className="absolute inset-0">
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-primary/20 rounded-full blur-3xl" />
<div className="absolute bottom-0 right-1/4 w-64 h-64 bg-neon-purple/20 rounded-full blur-3xl" />
</div>
<div className="container mx-auto px-4 relative z-10">
{/* Breadcrumb */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5 }}
className="flex items-center gap-2 text-sm text-muted-foreground mb-8"
>
<Link to="/" className="hover:text-primary transition-colors">Home</Link>
<ChevronRight className="w-4 h-4" />
<Link to="/projects" className="hover:text-primary transition-colors">Projects</Link>
<ChevronRight className="w-4 h-4" />
<span className="text-foreground">{project.title}</span>
</motion.div>
<div className="grid lg:grid-cols-2 gap-12 items-center">
{/* Content */}
<div>
<motion.span
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="inline-block px-4 py-1.5 rounded-full bg-primary/20 text-primary text-sm font-medium mb-6"
>
{categoryLabels[project.category]}
</motion.span>
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-4xl md:text-5xl font-bold mb-6"
>
{project.title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="text-lg text-muted-foreground mb-8"
>
{project.description}
</motion.p>
{/* Meta */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8"
>
<div className="glass rounded-xl p-4 text-center">
<Calendar className="w-5 h-5 mx-auto mb-2 text-primary" />
<p className="text-sm text-muted-foreground">Year</p>
<p className="font-bold">{project.year}</p>
</div>
<div className="glass rounded-xl p-4 text-center">
<Clock className="w-5 h-5 mx-auto mb-2 text-primary" />
<p className="text-sm text-muted-foreground">Duration</p>
<p className="font-bold">{project.duration}</p>
</div>
<div className="glass rounded-xl p-4 text-center">
<Users className="w-5 h-5 mx-auto mb-2 text-primary" />
<p className="text-sm text-muted-foreground">Team</p>
<p className="font-bold">{project.team}</p>
</div>
<div className="glass rounded-xl p-4 text-center">
<p className="text-sm text-muted-foreground mb-1">Client</p>
<p className="font-bold text-sm">{project.client}</p>
</div>
</motion.div>
{/* Actions */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
className="flex gap-4"
>
<Button className="rounded-full neon-glow">
<ExternalLink className="w-4 h-4 mr-2" />
View Live
</Button>
<Button variant="outline" className="rounded-full glass border-primary/30">
<Github className="w-4 h-4 mr-2" />
View Code
</Button>
</motion.div>
</div>
{/* Image */}
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="rounded-3xl overflow-hidden neon-glow"
>
<img
src={project.image}
alt={project.title}
className="w-full h-[400px] object-cover"
/>
</motion.div>
</div>
</div>
</section>
{/* Results */}
<section className="py-16 bg-secondary/30">
<div className="container mx-auto px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<h2 className="text-3xl font-bold">Key Results</h2>
</motion.div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 max-w-4xl mx-auto">
{project.results.map((result, index) => (
<motion.div
key={result.label}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1 }}
className="glass-strong rounded-2xl p-6 text-center"
>
<p className="text-3xl md:text-4xl font-bold text-primary neon-text-glow mb-2">
{result.value}
</p>
<p className="text-muted-foreground text-sm">{result.label}</p>
</motion.div>
))}
</div>
</div>
</section>
{/* Details */}
<section className="py-24">
<div className="container mx-auto px-4">
<div className="grid lg:grid-cols-2 gap-16 max-w-6xl mx-auto">
{/* Full Description */}
<motion.div
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
>
<h3 className="text-2xl font-bold mb-6">About the Project</h3>
<div className="prose-content">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
p: ({ children }) => <p className="text-muted-foreground leading-relaxed mb-4">{children}</p>,
strong: ({ children }) => <strong className="font-bold text-foreground">{children}</strong>,
ul: ({ children }) => <ul className="list-disc list-inside text-muted-foreground space-y-2 mb-4 ml-4">{children}</ul>,
li: ({ children }) => <li className="text-muted-foreground">{children}</li>,
}}
>
{project.fullDescription}
</ReactMarkdown>
</div>
</motion.div>
{/* Features & Tech */}
<div className="space-y-12">
<motion.div
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
>
<h3 className="text-2xl font-bold mb-6">Key Features</h3>
<ul className="space-y-3">
{project.features.map((feature, index) => (
<li key={index} className="flex items-start gap-3">
<span className="w-2 h-2 rounded-full bg-primary mt-2 flex-shrink-0" />
<span className="text-muted-foreground">{feature}</span>
</li>
))}
</ul>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.1 }}
>
<h3 className="text-2xl font-bold mb-6">Technologies Used</h3>
<div className="flex flex-wrap gap-3">
{project.technologies.map((tech) => (
<span
key={tech}
className="px-4 py-2 rounded-full glass text-sm font-medium border border-primary/30"
>
{tech}
</span>
))}
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.2 }}
>
<h3 className="text-2xl font-bold mb-6">Challenges Solved</h3>
<ul className="space-y-3">
{project.challenges.map((challenge, index) => (
<li key={index} className="flex items-start gap-3">
<span className="w-2 h-2 rounded-full bg-neon-purple mt-2 flex-shrink-0" />
<span className="text-muted-foreground">{challenge}</span>
</li>
))}
</ul>
</motion.div>
</div>
</div>
</div>
</section>
{/* Gallery */}
<section className="py-16 bg-secondary/30">
<div className="container mx-auto px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<h2 className="text-3xl font-bold">Project Gallery</h2>
</motion.div>
<div className="grid md:grid-cols-3 gap-6 max-w-5xl mx-auto">
{project.gallery.map((image, index) => (
<motion.div
key={index}
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1 }}
className="rounded-2xl overflow-hidden hover:neon-glow transition-all duration-500"
>
<img
src={image}
alt={`${project.title} screenshot ${index + 1}`}
className="w-full h-48 object-cover hover:scale-105 transition-transform duration-500"
/>
</motion.div>
))}
</div>
</div>
</section>
{/* Related Projects */}
{relatedProjects.length > 0 && (
<section className="py-24">
<div className="container mx-auto px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<h2 className="text-3xl font-bold">Related Projects</h2>
</motion.div>
<div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
{relatedProjects.map((relatedProject, index) => (
<motion.article
key={relatedProject.id}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1 }}
className="group glass rounded-2xl overflow-hidden hover:neon-glow transition-all duration-500"
>
<Link to={`/projects/${relatedProject.slug}`}>
<div className="relative h-40 overflow-hidden">
<img
src={relatedProject.image}
alt={relatedProject.title}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
/>
</div>
<div className="p-5">
<h3 className="font-bold mb-2 group-hover:text-primary transition-colors">
{relatedProject.title}
</h3>
<p className="text-sm text-muted-foreground line-clamp-2">
{relatedProject.description}
</p>
</div>
</Link>
</motion.article>
))}
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mt-12"
>
<Link to="/projects">
<Button variant="outline" className="rounded-full px-8 glass border-primary/30 hover:neon-glow">
View All Projects
</Button>
</Link>
</motion.div>
</div>
</section>
)}
<Footer />
</div>
</PageTransition>
);
}
+225
View File
@@ -0,0 +1,225 @@
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { ArrowLeft, Globe, Smartphone, Brain, Cloud, ExternalLink, Github } from 'lucide-react';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import Navbar from '@/components/Navbar';
import Footer from '@/components/Footer';
import PageTransition from '@/components/PageTransition';
import { projects } from '@/data/projectData';
const categories = [
{ id: 'all', name: 'All Projects', icon: null },
{ id: 'web', name: 'Web', icon: Globe },
{ id: 'mobile', name: 'Mobile', icon: Smartphone },
{ id: 'ai', name: 'AI', icon: Brain },
{ id: 'cloud', name: 'Cloud', icon: Cloud },
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5, ease: 'easeOut' as const },
},
};
export default function Projects() {
const [activeCategory, setActiveCategory] = useState('all');
const filteredProjects = activeCategory === 'all'
? projects
: projects.filter(project => project.category === activeCategory);
return (
<PageTransition>
<div className="min-h-screen bg-background overflow-x-hidden">
<Navbar />
{/* Hero Section */}
<section className="pt-32 pb-16 relative overflow-hidden">
<div className="absolute inset-0">
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-primary/20 rounded-full blur-3xl" />
<div className="absolute bottom-0 right-1/4 w-64 h-64 bg-neon-purple/20 rounded-full blur-3xl" />
</div>
<div className="container mx-auto px-4 relative z-10">
{/* Back Button */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5 }}
>
<Link to="/">
<Button variant="ghost" className="mb-8 group">
<ArrowLeft className="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" />
Back to Home
</Button>
</Link>
</motion.div>
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-center mb-12"
>
<h1 className="text-5xl md:text-6xl font-bold mb-6">
Our <span className="text-primary neon-text-glow">Projects</span>
</h1>
<p className="text-muted-foreground max-w-2xl mx-auto text-lg">
Explore our portfolio of innovative solutions that have transformed businesses across industries.
</p>
</motion.div>
{/* Filter Tabs */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="flex flex-wrap justify-center gap-3 mb-16"
>
{categories.map((category) => {
const Icon = category.icon;
return (
<Button
key={category.id}
variant={activeCategory === category.id ? 'default' : 'outline'}
onClick={() => setActiveCategory(category.id)}
className={`rounded-full px-6 transition-all duration-300 ${
activeCategory === category.id
? 'neon-glow bg-primary text-primary-foreground'
: 'glass border-primary/30 hover:border-primary'
}`}
>
{Icon && <Icon className="w-4 h-4 mr-2" />}
{category.name}
</Button>
);
})}
</motion.div>
</div>
</section>
{/* Projects Grid */}
<section className="pb-24">
<div className="container mx-auto px-4">
<AnimatePresence mode="wait">
<motion.div
key={activeCategory}
variants={containerVariants}
initial="hidden"
animate="visible"
exit="hidden"
className="grid md:grid-cols-2 gap-8"
>
{filteredProjects.map((project) => (
<motion.article
key={project.id}
variants={itemVariants}
layout
className="group glass rounded-3xl overflow-hidden hover:neon-glow transition-all duration-500"
>
{/* Image */}
<div className="relative h-64 overflow-hidden">
<img
src={project.image}
alt={project.title}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/50 to-transparent opacity-60" />
{/* Category Badge */}
<div className="absolute top-4 left-4">
<span className="px-3 py-1 rounded-full bg-primary/20 text-primary text-sm font-medium backdrop-blur-sm border border-primary/30">
{categories.find(c => c.id === project.category)?.name}
</span>
</div>
{/* Quick Links */}
<div className="absolute top-4 right-4 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<a
href={project.liveUrl}
className="p-2 rounded-full glass hover:bg-primary/20 transition-colors"
aria-label="View live project"
>
<ExternalLink className="w-4 h-4" />
</a>
<a
href={project.githubUrl}
className="p-2 rounded-full glass hover:bg-primary/20 transition-colors"
aria-label="View on GitHub"
>
<Github className="w-4 h-4" />
</a>
</div>
</div>
{/* Content */}
<div className="p-8">
{/* Meta */}
<div className="flex items-center gap-4 text-sm text-muted-foreground mb-4">
<span>{project.client}</span>
<span className="w-1 h-1 rounded-full bg-muted-foreground" />
<span>{project.year}</span>
</div>
{/* Title */}
<h3 className="text-2xl font-bold mb-4 group-hover:text-primary transition-colors">
{project.title}
</h3>
{/* Description */}
<p className="text-muted-foreground mb-6 leading-relaxed">
{project.description}
</p>
{/* Technologies */}
<div className="flex flex-wrap gap-2">
{project.technologies.map((tech) => (
<span
key={tech}
className="px-3 py-1 rounded-full bg-muted text-muted-foreground text-xs font-medium"
>
{tech}
</span>
))}
</div>
</div>
</motion.article>
))}
</motion.div>
</AnimatePresence>
{/* Empty State */}
{filteredProjects.length === 0 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-20"
>
<p className="text-muted-foreground text-lg">
No projects found in this category.
</p>
</motion.div>
)}
</div>
</section>
<Footer />
</div>
</PageTransition>
);
}
+7
View File
@@ -0,0 +1,7 @@
import { describe, it, expect } from "vitest";
describe("example", () => {
it("should pass", () => {
expect(true).toBe(true);
});
});
+15
View File
@@ -0,0 +1,15 @@
import "@testing-library/jest-dom";
Object.defineProperty(window, "matchMedia", {
writable: true,
value: (query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => {},
}),
});
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+123
View File
@@ -0,0 +1,123 @@
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))",
},
// TechZaa brand colors
neon: {
blue: "hsl(var(--neon-blue))",
purple: "hsl(var(--neon-purple))",
green: "hsl(var(--neon-green))",
},
},
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" },
},
"fade-in": {
"0%": { opacity: "0", transform: "translateY(10px)" },
"100%": { opacity: "1", transform: "translateY(0)" },
},
"fade-in-up": {
"0%": { opacity: "0", transform: "translateY(20px)" },
"100%": { opacity: "1", transform: "translateY(0)" },
},
"slide-in-right": {
"0%": { transform: "translateX(100%)" },
"100%": { transform: "translateX(0)" },
},
"slide-in-left": {
"0%": { transform: "translateX(-100%)" },
"100%": { transform: "translateX(0)" },
},
"scale-in": {
"0%": { transform: "scale(0.95)", opacity: "0" },
"100%": { transform: "scale(1)", opacity: "1" },
},
"bounce-subtle": {
"0%, 100%": { transform: "translateY(0)" },
"50%": { transform: "translateY(-5px)" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"fade-in": "fade-in 0.5s ease-out",
"fade-in-up": "fade-in-up 0.6s ease-out",
"slide-in-right": "slide-in-right 0.3s ease-out",
"slide-in-left": "slide-in-left 0.3s ease-out",
"scale-in": "scale-in 0.2s ease-out",
"bounce-subtle": "bounce-subtle 2s ease-in-out infinite",
},
fontFamily: {
sans: ["Inter", "system-ui", "sans-serif"],
display: ["Inter", "system-ui", "sans-serif"],
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config;
+31
View File
@@ -0,0 +1,31 @@
{
"compilerOptions": {
"types": ["vitest/globals"],
"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
View 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
View 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"]
}
+13
View File
@@ -0,0 +1,13 @@
import react from "@vitejs/plugin-react-swc";
import path from "path";
import { defineConfig } from "vite";
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
}));
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react-swc";
import path from "path";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./src/test/setup.ts"],
include: ["src/**/*.{test,spec}.{ts,tsx}"],
},
resolve: {
alias: { "@": path.resolve(__dirname, "./src") },
},
});