first commit

This commit is contained in:
priyanshuvish
2025-07-11 16:54:37 +05:30
parent 15e50f1dec
commit 8a5bb95a0e
221 changed files with 109527 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
import { motion } from "framer-motion";
interface AnimatedGradientTextProps {
text: string;
className?: string;
}
export const AnimatedGradientText = ({ text, className = "" }: AnimatedGradientTextProps) => {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className={`inline-block ${className}`}
>
<span className="bg-gradient-to-r from-[#E5195E] via-purple-400 to-[#E5195E] bg-clip-text text-transparent animate-pulse">
{text}
</span>
</motion.div>
);
};

View File

@@ -0,0 +1,139 @@
import React from "react";
import { motion } from "framer-motion";
import { ImageWithFallback } from "./figma/ImageWithFallback";
// import successMetricsImage from 'figma:asset/619c58bb9b76889672d43420adc0dd6ef9ef21f6.png';
const successMetricsImage =
"https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=400&h=300&fit=crop&auto=format";
const AppSuccessMetrics = () => {
const metrics = [
{
value: "75+",
label: "App Developed",
description: "Successful mobile applications delivered",
},
{
value: "25+",
label: "App Deployed",
description: "Live applications in production",
},
{
value: "3M+",
label: "App downloads",
description: "Total downloads across all platforms",
},
];
return (
<section className="py-20 lg:py-32 bg-black relative overflow-hidden">
<div className="container mx-auto px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-4xl lg:text-5xl font-semibold text-white mb-6 leading-tight">
Proven Success in{" "}
<span className="text-accent">Mobile Innovation</span>
</h2>
<p className="text-xl text-gray-300 max-w-3xl mx-auto leading-relaxed">
Our portfolio speaks for itself from concept to launch, we
deliver exceptional mobile experiences that users love and
businesses rely on.
</p>
</motion.div>
{/* Main Visual Section */}
<div className="relative">
{/* iPhone Mockups Display */}
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.2 }}
viewport={{ once: true }}
className="flex justify-center mb-16"
>
<div className="relative max-w-4xl w-full">
<ImageWithFallback
src={successMetricsImage}
alt="Three iPhone mockups showcasing different mobile applications with success metrics"
className="w-full h-auto object-contain"
/>
</div>
</motion.div>
{/* Performance Statistics */}
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
viewport={{ once: true }}
className="grid grid-cols-1 md:grid-cols-3 gap-12 max-w-4xl mx-auto"
>
{metrics.map((metric, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{
duration: 0.6,
delay: 0.6 + index * 0.1,
}}
viewport={{ once: true }}
className="text-center group"
>
{/* Large Metric Number */}
<div className="mb-4">
<span className="text-6xl lg:text-7xl font-bold text-white group-hover:text-accent transition-colors duration-300">
{metric.value}
</span>
</div>
{/* Metric Label */}
<h3 className="text-lg lg:text-xl font-semibold text-gray-300 mb-2">
{metric.label}
</h3>
{/* Metric Description */}
<p className="text-sm text-gray-400 leading-relaxed">
{metric.description}
</p>
</motion.div>
))}
</motion.div>
</div>
{/* Supporting Content */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.8 }}
viewport={{ once: true }}
className="text-center mt-16"
>
<p className="text-gray-400 max-w-2xl mx-auto leading-relaxed">
Every project we deliver combines cutting-edge technology with
user-centered design, resulting in mobile applications that not
only meet but exceed expectations across industries and platforms.
</p>
</motion.div>
</div>
</div>
{/* Background Decorative Elements */}
<div className="absolute inset-0 pointer-events-none overflow-hidden">
{/* Subtle gradient orbs for depth */}
<div className="absolute top-20 left-10 w-80 h-80 bg-accent/5 rounded-full blur-3xl"></div>
<div className="absolute bottom-20 right-10 w-80 h-80 bg-blue-500/5 rounded-full blur-3xl"></div>
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-purple-500/3 rounded-full blur-3xl"></div>
</div>
</section>
);
};
export { AppSuccessMetrics };

View File

@@ -0,0 +1,292 @@
import React, { useState, useEffect, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { Card, CardContent } from "./ui/card";
import { Button } from "./ui/button";
import { Star } from "lucide-react";
import { ImageWithFallback } from "./figma/ImageWithFallback";
// High-quality Clutch logo placeholder
const clutchLogo = "https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=120&h=60&fit=crop&auto=format";
const testimonials = [
{
id: 1,
name: "Sarah Chen",
position: "CTO",
company: "FinTech Innovations",
image: "https://images.unsplash.com/photo-1494790108755-2616b332c5cd?w=150&h=150&fit=crop&auto=format",
rating: 5,
text: "WDI transformed our legacy banking system into a modern, scalable platform. Their expertise in financial technology is unmatched.",
projectType: "Banking Platform Modernization"
},
{
id: 2,
name: "Michael Rodriguez",
position: "Founder & CEO",
company: "HealthTech Solutions",
image: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&auto=format",
rating: 5,
text: "The mobile health app WDI developed has revolutionized patient care delivery. Exceptional attention to healthcare compliance and user experience.",
projectType: "Healthcare Mobile App"
},
{
id: 3,
name: "Emily Watson",
position: "VP of Digital Strategy",
company: "RetailMax Corp",
image: "https://images.unsplash.com/photo-1580489944761-15a19d654956?w=150&h=150&fit=crop&auto=format",
rating: 5,
text: "Our e-commerce platform's performance improved by 300% after WDI's optimization. Their technical expertise is outstanding.",
projectType: "E-commerce Platform Enhancement"
},
{
id: 4,
name: "James Thompson",
position: "Chief Innovation Officer",
company: "EduTech Pioneers",
image: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&auto=format",
rating: 5,
text: "WDI's AI-powered learning platform has transformed how our students engage with content. Incredible innovation and execution.",
projectType: "AI-Powered EdTech Platform"
},
{
id: 5,
name: "Lisa Park",
position: "Operations Director",
company: "LogiFlow Systems",
image: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&auto=format",
rating: 5,
text: "The supply chain management system WDI built has streamlined our operations and reduced costs by 40%. Highly recommended.",
projectType: "Supply Chain Management System"
}
];
export const CarouselTestimonials = () => {
const [currentIndex, setCurrentIndex] = useState(0);
const [isAutoPlaying, setIsAutoPlaying] = useState(true);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
// Auto-play functionality
useEffect(() => {
if (isAutoPlaying) {
intervalRef.current = setInterval(() => {
setCurrentIndex((prevIndex) =>
prevIndex === testimonials.length - 1 ? 0 : prevIndex + 1
);
}, 5000);
}
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [isAutoPlaying]);
const goToPrevious = () => {
setIsAutoPlaying(false);
setCurrentIndex(currentIndex === 0 ? testimonials.length - 1 : currentIndex - 1);
};
const goToNext = () => {
setIsAutoPlaying(false);
setCurrentIndex(currentIndex === testimonials.length - 1 ? 0 : currentIndex + 1);
};
const goToSlide = (index: number) => {
setIsAutoPlaying(false);
setCurrentIndex(index);
};
// Resume auto-play after user interaction
useEffect(() => {
if (!isAutoPlaying) {
const resumeTimer = setTimeout(() => {
setIsAutoPlaying(true);
}, 10000); // Resume after 10 seconds
return () => clearTimeout(resumeTimer);
}
}, [isAutoPlaying]);
return (
<section className="py-32 bg-background">
<div className="container mx-auto px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-20"
>
<h2 className="text-4xl lg:text-5xl font-semibold text-foreground mb-6">
What Our <span className="text-accent">Clients Say</span>
</h2>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto leading-relaxed">
Hear from industry leaders who have transformed their businesses with our innovative solutions.
</p>
</motion.div>
<div className="relative max-w-6xl mx-auto">
{/* Main Testimonial Display */}
<div className="relative overflow-hidden rounded-3xl">
<AnimatePresence mode="wait">
<motion.div
key={currentIndex}
initial={{ opacity: 0, x: 100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -100 }}
transition={{ duration: 0.5, ease: "easeInOut" }}
>
<Card className="bg-card/50 backdrop-blur-md border-white/10 shadow-2xl">
<CardContent className="p-12">
<div className="grid lg:grid-cols-3 gap-12 items-center">
{/* Client Photo and Info */}
<div className="lg:col-span-1 text-center lg:text-left">
<div className="relative mb-8">
<div className="w-32 h-32 mx-auto lg:mx-0 rounded-full overflow-hidden border-4 border-accent/20">
<ImageWithFallback
src={testimonials[currentIndex].image}
alt={testimonials[currentIndex].name}
className="w-full h-full object-cover"
/>
</div>
{/* Rating Stars */}
<div className="flex justify-center lg:justify-start gap-1 mt-6">
{[...Array(testimonials[currentIndex].rating)].map((_, i) => (
<Star key={i} className="w-5 h-5 fill-accent text-accent" />
))}
</div>
</div>
<div className="space-y-2">
<h3 className="text-2xl font-semibold text-foreground">
{testimonials[currentIndex].name}
</h3>
<p className="text-accent font-medium">
{testimonials[currentIndex].position}
</p>
<p className="text-muted-foreground">
{testimonials[currentIndex].company}
</p>
<div className="pt-4">
<span className="inline-block px-4 py-2 bg-accent/10 text-accent text-sm rounded-full border border-accent/20">
{testimonials[currentIndex].projectType}
</span>
</div>
</div>
</div>
{/* Testimonial Content */}
<div className="lg:col-span-2">
<div className="relative">
{/* Quote Icon */}
<div className="absolute -top-4 -left-4 text-6xl text-accent/20 font-serif">"</div>
<blockquote className="text-2xl lg:text-3xl text-foreground leading-relaxed font-medium pl-8">
{testimonials[currentIndex].text}
</blockquote>
{/* Clutch Logo */}
<div className="flex items-center justify-end mt-8">
<div className="text-sm text-muted-foreground mr-4">
Verified Review on
</div>
<ImageWithFallback
src={clutchLogo}
alt="Clutch"
className="h-8 w-auto opacity-70 hover:opacity-100 transition-opacity duration-300"
/>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
</AnimatePresence>
</div>
{/* Navigation Controls */}
<div className="flex items-center justify-center mt-12 gap-8">
{/* Previous Button */}
<Button
variant="outline"
size="lg"
onClick={goToPrevious}
className="w-14 h-14 rounded-full border-white/20 hover:border-accent/50 hover:bg-accent/10 transition-all duration-300"
>
<ChevronLeft className="w-6 h-6" />
</Button>
{/* Dots Indicator */}
<div className="flex gap-3">
{testimonials.map((_, index) => (
<button
key={index}
onClick={() => goToSlide(index)}
className={`w-3 h-3 rounded-full transition-all duration-300 ${
index === currentIndex
? 'bg-accent scale-125'
: 'bg-white/30 hover:bg-white/50'
}`}
aria-label={`Go to testimonial ${index + 1}`}
/>
))}
</div>
{/* Next Button */}
<Button
variant="outline"
size="lg"
onClick={goToNext}
className="w-14 h-14 rounded-full border-white/20 hover:border-accent/50 hover:bg-accent/10 transition-all duration-300"
>
<ChevronRight className="w-6 h-6" />
</Button>
</div>
{/* Auto-play Indicator */}
<div className="text-center mt-6">
<button
onClick={() => setIsAutoPlaying(!isAutoPlaying)}
className="text-sm text-muted-foreground hover:text-foreground transition-colors duration-300"
>
{isAutoPlaying ? '⏸ Pause Auto-play' : '▶ Resume Auto-play'}
</button>
</div>
</div>
{/* Additional Social Proof */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.3 }}
viewport={{ once: true }}
className="text-center mt-20 pt-16 border-t border-white/10"
>
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 max-w-4xl mx-auto">
<div className="space-y-2">
<div className="text-3xl font-bold text-accent">98%</div>
<div className="text-sm text-muted-foreground">Client Satisfaction</div>
</div>
<div className="space-y-2">
<div className="text-3xl font-bold text-accent">150+</div>
<div className="text-sm text-muted-foreground">Projects Delivered</div>
</div>
<div className="space-y-2">
<div className="text-3xl font-bold text-accent">5.0</div>
<div className="text-sm text-muted-foreground">Average Rating</div>
</div>
<div className="space-y-2">
<div className="text-3xl font-bold text-accent">24/7</div>
<div className="text-sm text-muted-foreground">Support Available</div>
</div>
</div>
</motion.div>
</div>
</section>
);
};

View File

@@ -0,0 +1,274 @@
import React from "react";
import { motion } from "framer-motion";
import { Card, CardContent } from "./ui/card";
import { Button } from "./ui/button";
import { Badge } from "./ui/badge";
import { ArrowRight, ExternalLink, TrendingUp, Users, Clock, Star } from "lucide-react";
import { ImageWithFallback } from "./figma/ImageWithFallback";
import { navigateTo } from "../App";
// High-quality project images
const regroupImage = "https://images.unsplash.com/photo-1551650975-87deedd944c3?w=600&h=400&fit=crop&auto=format";
const seezunImage = "https://images.unsplash.com/photo-1512941937669-90a1b58e7e9c?w=600&h=400&fit=crop&auto=format";
const wokaAwardImage = "https://images.unsplash.com/photo-1517077304055-6e89abbf09b0?w=600&h=400&fit=crop&auto=format";
const caseStudies = [
{
id: 1,
title: "Regroup",
subtitle: "Social Networking Revolution",
description: "A comprehensive social platform that connects communities worldwide with advanced messaging, group management, and content sharing capabilities.",
image: regroupImage,
category: "Social Platform",
client: "Regroup Technologies",
duration: "8 months",
teamSize: "12 developers",
technologies: ["React Native", "Node.js", "MongoDB", "WebRTC", "AWS"],
results: [
{ metric: "User Engagement", value: "+240%" },
{ metric: "Active Communities", value: "50K+" },
{ metric: "Daily Messages", value: "2.5M+" }
],
awards: ["Best Social App 2023", "Innovation Award"],
link: "/projects/regroup",
featured: true
},
{
id: 2,
title: "Seezun",
subtitle: "Next-Gen E-commerce Platform",
description: "Revolutionary e-commerce solution with AI-powered recommendations, seamless checkout, and integrated inventory management for modern retailers.",
image: seezunImage,
category: "E-commerce",
client: "Seezun Retail",
duration: "6 months",
teamSize: "8 developers",
technologies: ["React", "Python", "PostgreSQL", "Redis", "Stripe"],
results: [
{ metric: "Conversion Rate", value: "+180%" },
{ metric: "Page Load Speed", value: "2.1s" },
{ metric: "Customer Satisfaction", value: "4.9/5" }
],
awards: ["E-commerce Excellence Award"],
link: "/projects/seezun",
featured: true
},
{
id: 3,
title: "Woka",
subtitle: "Award-Winning Fitness App",
description: "Comprehensive fitness and wellness platform with personalized workout plans, nutrition tracking, and community features that won multiple industry awards.",
image: wokaAwardImage,
category: "Health & Fitness",
client: "Woka Wellness",
duration: "10 months",
teamSize: "15 developers",
technologies: ["Flutter", "Firebase", "TensorFlow", "Apple HealthKit", "Google Fit"],
results: [
{ metric: "User Retention", value: "+320%" },
{ metric: "Workout Completions", value: "1M+" },
{ metric: "App Store Rating", value: "4.8/5" }
],
awards: ["App of the Year 2023", "Health Innovation Award", "User Choice Award"],
link: "/projects/woka",
featured: true
}
];
export const CaseStudyHighlight = () => {
return (
<section className="py-32 bg-background">
<div className="container mx-auto px-6 lg:px-8">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-20"
>
<Badge variant="outline" className="mb-6 border-accent/20 text-accent">
Featured Work
</Badge>
<h2 className="text-4xl lg:text-5xl font-semibold text-foreground mb-6">
Success Stories That <span className="text-accent">Define Excellence</span>
</h2>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto leading-relaxed">
Explore our award-winning projects that have transformed businesses and delighted millions of users worldwide.
</p>
</motion.div>
{/* Featured Case Studies Grid */}
<div className="grid lg:grid-cols-3 gap-8 mb-16">
{caseStudies.map((study, index) => (
<motion.div
key={study.id}
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: index * 0.2 }}
viewport={{ once: true }}
whileHover={{ y: -8, scale: 1.02 }}
className="group cursor-pointer"
onClick={() => navigateTo(study.link)}
>
<Card className="bg-card/50 backdrop-blur-md border-white/10 hover:border-accent/30 transition-all duration-500 shadow-lg hover:shadow-2xl hover:shadow-accent/10 rounded-2xl overflow-hidden h-full">
<CardContent className="p-0 flex flex-col h-full">
{/* Image Header */}
<div className="relative overflow-hidden">
<ImageWithFallback
src={study.image}
alt={study.title}
className="w-full h-64 object-cover transition-transform duration-500 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
{/* Category Badge */}
<div className="absolute top-4 left-4">
<Badge className="bg-accent/90 text-white border-0">
{study.category}
</Badge>
</div>
{/* Awards */}
{study.awards.length > 0 && (
<div className="absolute top-4 right-4">
<div className="bg-amber-500/90 text-white px-3 py-1 rounded-full text-xs font-medium flex items-center gap-1">
<Star className="w-3 h-3 fill-current" />
Award Winner
</div>
</div>
)}
{/* Project Title Overlay */}
<div className="absolute bottom-4 left-4 right-4">
<h3 className="text-2xl font-bold text-white mb-1">
{study.title}
</h3>
<p className="text-white/80 text-sm">
{study.subtitle}
</p>
</div>
</div>
{/* Content */}
<div className="p-8 flex-1 flex flex-col">
<p className="text-muted-foreground leading-relaxed mb-6 flex-1">
{study.description}
</p>
{/* Key Metrics */}
<div className="grid grid-cols-3 gap-4 mb-6 p-4 bg-accent/5 rounded-lg border border-accent/10">
{study.results.slice(0, 3).map((result, idx) => (
<div key={idx} className="text-center">
<div className="text-lg font-bold text-accent">
{result.value}
</div>
<div className="text-xs text-muted-foreground">
{result.metric}
</div>
</div>
))}
</div>
{/* Technologies */}
<div className="mb-6">
<p className="text-sm font-medium text-foreground mb-2">Technologies:</p>
<div className="flex flex-wrap gap-2">
{study.technologies.slice(0, 3).map((tech) => (
<Badge key={tech} variant="secondary" className="text-xs bg-muted/50">
{tech}
</Badge>
))}
{study.technologies.length > 3 && (
<Badge variant="secondary" className="text-xs bg-muted/50">
+{study.technologies.length - 3} more
</Badge>
)}
</div>
</div>
{/* Project Details */}
<div className="grid grid-cols-2 gap-4 mb-6 text-sm">
<div className="flex items-center gap-2 text-muted-foreground">
<Clock className="w-4 h-4" />
{study.duration}
</div>
<div className="flex items-center gap-2 text-muted-foreground">
<Users className="w-4 h-4" />
{study.teamSize}
</div>
</div>
{/* Awards List */}
{study.awards.length > 0 && (
<div className="mb-6">
<p className="text-sm font-medium text-foreground mb-2">Awards:</p>
<div className="space-y-1">
{study.awards.slice(0, 2).map((award, idx) => (
<div key={idx} className="flex items-center gap-2 text-sm text-amber-600">
<Star className="w-3 h-3 fill-current" />
{award}
</div>
))}
</div>
</div>
)}
{/* CTA Button */}
<Button
variant="ghost"
className="w-full justify-between text-accent hover:text-accent hover:bg-accent/10 group-hover:translate-x-1 transition-all duration-300 mt-auto"
onClick={(e) => {
e.stopPropagation();
navigateTo(study.link);
}}
>
<span>View Case Study</span>
<ArrowRight className="w-4 h-4" />
</Button>
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
{/* Call-to-Action */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.6 }}
viewport={{ once: true }}
className="text-center"
>
<div className="bg-gradient-to-r from-accent/10 via-accent/5 to-accent/10 rounded-2xl p-8 border border-accent/20">
<h3 className="text-2xl font-semibold text-foreground mb-4">
Ready to Create Your Success Story?
</h3>
<p className="text-muted-foreground mb-6 max-w-2xl mx-auto">
Join the ranks of industry leaders who have transformed their businesses with our innovative solutions.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button
size="lg"
className="bg-accent hover:bg-accent/90 text-white"
onClick={() => navigateTo("/case-studies")}
>
View All Case Studies
<ExternalLink className="w-4 h-4 ml-2" />
</Button>
<Button
size="lg"
variant="outline"
onClick={() => navigateTo("/start-a-project")}
>
Start Your Project
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
</div>
</div>
</motion.div>
</div>
</section>
);
};

246
components/ClientLogos.tsx Normal file
View File

@@ -0,0 +1,246 @@
import { motion } from "framer-motion";
import { GridPattern } from "./GridPattern";
const companyLogos = [
{ name: "TechFlow Solutions", logo: null, width: "140" },
{ name: "DataSync Pro", logo: null, width: "120" },
{ name: "CloudNova Systems", logo: null, width: "140" },
{ name: "AMOZ", logo: null, width: "90" },
{ name: "SimpliTend", logo: null, width: "120" },
{ name: "Seezun", logo: null, width: "100" },
{ name: "TradersCircuit", logo: null, width: "140" },
{ name: "FreeU", logo: null, width: "90" },
{ name: "Amble", logo: null, width: "100" },
{ name: "Lean In World", logo: null, width: "130" },
{ name: "WOKA", logo: null, width: "90" },
{ name: "SSA", logo: null, width: "80" },
{ name: "Dorf Ketal", logo: null, width: "120" },
{ name: "Agromate", logo: null, width: "120" },
{ name: "Regroup", logo: null, width: "110" },
{ name: "CAD IT Solutions", logo: null, width: "150" },
{ name: "Tanami Capital", logo: null, width: "140" },
{ name: "SuperMoney Advisor", logo: null, width: "170" },
{ name: "Prosperty Platform", logo: null, width: "160" },
{ name: "Moving Cargo", logo: null, width: "130" },
{ name: "GSF Mobile", logo: null, width: "120" },
{ name: "Farm Feeder", logo: null, width: "120" },
{ name: "Melbourne City Card", logo: null, width: "170" },
{ name: "ByteForge Labs", logo: null, width: "130" },
{ name: "CodeCraft Studio", logo: null, width: "140" },
{ name: "DevStream Tech", logo: null, width: "130" },
{ name: "NextGen Solutions", logo: null, width: "150" },
{ name: "ProdPush Platform", logo: null, width: "140" },
{ name: "ScaleUp Ventures", logo: null, width: "140" },
{ name: "AlphaVision Labs", logo: null, width: "140" },
{ name: "CloudSync Systems", logo: null, width: "140" },
{ name: "TechNova Group", logo: null, width: "130" },
{ name: "DataFlow Pro", logo: null, width: "120" },
{ name: "InnovateLab", logo: null, width: "120" }
];
const countryFlags = [
{
name: "United States",
alt: "United States flag icon",
flagSvg: (
<svg viewBox="0 0 24 18" className="w-8 h-6">
<rect width="24" height="18" fill="#B22234"/>
<rect width="24" height="1.38" y="1.38" fill="white"/>
<rect width="24" height="1.38" y="4.15" fill="white"/>
<rect width="24" height="1.38" y="6.92" fill="white"/>
<rect width="24" height="1.38" y="9.69" fill="white"/>
<rect width="24" height="1.38" y="12.46" fill="white"/>
<rect width="24" height="1.38" y="15.23" fill="white"/>
<rect width="9.6" height="9.69" fill="#3C3B6E"/>
</svg>
)
},
{
name: "United Kingdom",
alt: "United Kingdom flag icon",
flagSvg: (
<svg viewBox="0 0 24 18" className="w-8 h-6">
<rect width="24" height="18" fill="#012169"/>
<path d="M0 0L24 18M24 0L0 18" stroke="white" strokeWidth="2"/>
<path d="M0 0L24 18M24 0L0 18" stroke="#C8102E" strokeWidth="1"/>
<path d="M12 0V18M0 9H24" stroke="white" strokeWidth="3"/>
<path d="M12 0V18M0 9H24" stroke="#C8102E" strokeWidth="2"/>
</svg>
)
},
{
name: "India",
alt: "India flag icon",
flagSvg: (
<svg viewBox="0 0 24 18" className="w-8 h-6">
<rect width="24" height="6" fill="#FF9933"/>
<rect width="24" height="6" y="6" fill="white"/>
<rect width="24" height="6" y="12" fill="#138808"/>
<circle cx="12" cy="9" r="2" fill="none" stroke="#000080" strokeWidth="0.3"/>
<g transform="translate(12,9)">
{Array.from({length: 24}, (_, i) => (
<line key={i} x1="0" y1="-1.5" x2="0" y2="-1.8" stroke="#000080" strokeWidth="0.1" transform={`rotate(${i * 15})`}/>
))}
</g>
</svg>
)
},
{
name: "Canada",
alt: "Canada flag icon",
flagSvg: (
<svg viewBox="0 0 24 18" className="w-8 h-6">
<rect width="6" height="18" fill="#FF0000"/>
<rect width="12" height="18" x="6" fill="white"/>
<rect width="6" height="18" x="18" fill="#FF0000"/>
<path d="M12 4L13 7H16L13.5 9L14.5 12L12 10L9.5 12L10.5 9L8 7H11L12 4Z" fill="#FF0000"/>
</svg>
)
},
{
name: "Australia",
alt: "Australia flag icon",
flagSvg: (
<svg viewBox="0 0 24 18" className="w-8 h-6">
<rect width="24" height="18" fill="#012169"/>
<g transform="scale(0.5)">
<rect width="24" height="9" fill="#012169"/>
<path d="M0 0L24 18M24 0L0 18" stroke="white" strokeWidth="2"/>
<path d="M0 0L24 18M24 0L0 18" stroke="#C8102E" strokeWidth="1"/>
<path d="M12 0V18M0 9H24" stroke="white" strokeWidth="3"/>
<path d="M12 0V18M0 9H24" stroke="#C8102E" strokeWidth="2"/>
</g>
<g fill="white">
<circle cx="18" cy="6" r="0.5"/>
<circle cx="20" cy="8" r="0.3"/>
<circle cx="19" cy="10" r="0.4"/>
<circle cx="21" cy="12" r="0.3"/>
<circle cx="18" cy="14" r="0.5"/>
</g>
</svg>
)
}
];
const ProjectImageCircles = () => (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.3 }}
viewport={{ once: true }}
className="flex justify-center items-center mb-12"
>
<div className="flex items-center">
{countryFlags.map((flag, index) => (
<motion.div
key={index}
initial={{ opacity: 0, scale: 0 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.5,
delay: 0.5 + (index * 0.1),
type: "spring",
stiffness: 200
}}
viewport={{ once: true }}
whileHover={{
scale: 1.1,
zIndex: 10,
transition: { duration: 0.2 }
}}
className="relative w-16 h-16 cursor-pointer group"
style={{
marginLeft: index > 0 ? '-8px' : '0',
zIndex: countryFlags.length - index
}}
>
<div className="absolute inset-0 w-16 h-16 rounded-full bg-white/10 backdrop-blur-sm border-2 border-white/20 group-hover:border-accent/50 group-hover:bg-white/15 transition-all duration-300 shadow-lg group-hover:shadow-xl flex items-center justify-center overflow-hidden">
<div
className="w-10 h-8 flex items-center justify-center transform group-hover:scale-110 transition-transform duration-300"
role="img"
aria-label={flag.alt}
>
{flag.flagSvg}
</div>
</div>
{/* Subtle glow effect */}
<div className="absolute inset-0 w-16 h-16 rounded-full bg-gradient-to-br from-accent/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
{/* Tooltip */}
<div className="absolute -top-12 left-1/2 transform -translate-x-1/2 bg-[#0E0E0E] text-white text-xs px-3 py-1.5 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg border border-white/10 z-50">
<div className="text-center">
<div className="font-medium text-accent">{flag.name}</div>
</div>
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-3 border-r-3 border-t-3 border-transparent border-t-[#0E0E0E]"></div>
</div>
</motion.div>
))}
</div>
</motion.div>
);
const LogoCard = ({ name, width }: { name: string; width: string }) => (
<div
className="flex items-center justify-center h-16 bg-white/8 rounded-xl border border-white/10 hover:scale-105 hover:bg-white/12 hover:border-accent/20 transition-all duration-300 px-6 shadow-lg backdrop-blur-sm group"
style={{ minWidth: `${width}px` }}
>
<span className="text-white/80 font-medium text-sm text-center leading-tight group-hover:text-white/95 transition-colors duration-300">
{name}
</span>
</div>
);
const MarqueeRow = ({ logos }: { logos: typeof companyLogos }) => (
<motion.div
animate={{
x: [0, -3200],
}}
transition={{
x: {
repeat: Infinity,
repeatType: "loop",
duration: 120,
ease: "linear",
},
}}
className="flex gap-6 items-center"
style={{
width: "fit-content",
}}
>
{[...logos, ...logos].map((company, index) => (
<LogoCard key={`${company.name}-${index}`} name={company.name} width={company.width} />
))}
</motion.div>
);
export const ClientLogos = () => {
return (
<section className="relative py-20 bg-[#121212] border-y border-white/5 overflow-hidden">
<GridPattern strokeDasharray="4 2" />
<div className="relative z-10 container mx-auto px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-8"
>
<h2 className="text-3xl lg:text-4xl font-semibold text-white mb-6">
Trusted by Founders and CTOs Across 15+ Countries
</h2>
</motion.div>
{/* Project Image Circles */}
<ProjectImageCircles />
{/* Company Logos Ticker */}
<div className="overflow-hidden">
<MarqueeRow logos={companyLogos} />
</div>
</div>
</section>
);
};

View File

@@ -0,0 +1,205 @@
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, Cookie, Shield, Settings } from 'lucide-react';
import { Button } from './ui/button';
export const CookieConsent = () => {
const [isVisible, setIsVisible] = useState(false);
const [showSettings, setShowSettings] = useState(false);
useEffect(() => {
// Check if user has already made a choice
const consent = localStorage.getItem('cookieConsent');
if (!consent) {
// Show banner after a short delay
const timer = setTimeout(() => {
setIsVisible(true);
}, 1000);
return () => clearTimeout(timer);
}
}, []);
const handleAcceptAll = () => {
localStorage.setItem('cookieConsent', 'accepted');
localStorage.setItem('cookiePreferences', JSON.stringify({
necessary: true,
analytics: true,
marketing: true,
functional: true
}));
setIsVisible(false);
};
const handleDeclineAll = () => {
localStorage.setItem('cookieConsent', 'declined');
localStorage.setItem('cookiePreferences', JSON.stringify({
necessary: true,
analytics: false,
marketing: false,
functional: false
}));
setIsVisible(false);
};
const handleSavePreferences = () => {
localStorage.setItem('cookieConsent', 'customized');
// In a real app, you would save the specific preferences here
setIsVisible(false);
setShowSettings(false);
};
const handleClose = () => {
setIsVisible(false);
};
if (!isVisible) return null;
return (
<AnimatePresence>
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="fixed bottom-0 left-0 right-0 z-50 bg-black border-t border-white/10 shadow-2xl"
>
<div className="container mx-auto px-4 lg:px-8">
{!showSettings ? (
// Main Cookie Consent Banner
<div className="py-4 lg:py-6">
<div className="flex flex-col lg:flex-row items-start lg:items-center gap-4 lg:gap-6">
{/* Icon and Message */}
<div className="flex items-start gap-3 flex-1">
<div className="w-8 h-8 bg-[#E5195E]/20 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
<Cookie className="w-4 h-4 text-[#E5195E]" />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-white font-semibold text-sm mb-1">
We use cookies to enhance your experience
</h3>
<p className="text-[#CCCCCC] text-sm leading-relaxed">
We use cookies to analyze site performance, deliver personalized content, and improve your browsing experience.
By clicking "Accept All", you consent to our use of cookies.{' '}
<a href="/privacy" className="text-[#E5195E] hover:text-[#E5195E]/80 underline">
Learn more
</a>
</p>
</div>
</div>
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-3 w-full lg:w-auto">
<Button
variant="outline"
size="sm"
className="border-white/20 text-white hover:bg-white/10 hover:border-[#E5195E]/50 hover:text-white transition-all duration-300"
onClick={() => setShowSettings(true)}
>
<Settings className="w-4 h-4 mr-2" />
Customize
</Button>
<Button
variant="outline"
size="sm"
className="border-white/20 text-white hover:bg-white/10 hover:border-white/30 hover:text-white transition-all duration-300"
onClick={handleDeclineAll}
>
Decline All
</Button>
<Button
size="sm"
className="bg-[#E5195E] hover:bg-[#E5195E]/90 text-white transition-all duration-300"
onClick={handleAcceptAll}
>
Accept All
</Button>
</div>
{/* Close Button */}
<button
onClick={handleClose}
className="absolute top-4 right-4 lg:relative lg:top-auto lg:right-auto w-8 h-8 flex items-center justify-center text-[#CCCCCC] hover:text-white hover:bg-white/10 rounded-lg transition-all duration-300"
aria-label="Close cookie banner"
>
<X className="w-4 h-4" />
</button>
</div>
</div>
) : (
// Cookie Settings Panel
<div className="py-6">
<div className="max-w-2xl mx-auto">
<div className="flex items-center gap-3 mb-6">
<div className="w-8 h-8 bg-[#E5195E]/20 rounded-lg flex items-center justify-center">
<Shield className="w-4 h-4 text-[#E5195E]" />
</div>
<h3 className="text-white font-semibold text-lg">Cookie Preferences</h3>
</div>
<div className="space-y-4 mb-6">
{/* Necessary Cookies */}
<div className="bg-white/5 rounded-lg p-4 border border-white/10">
<div className="flex items-center justify-between mb-2">
<h4 className="text-white font-medium">Necessary Cookies</h4>
<div className="w-12 h-6 bg-[#E5195E] rounded-full flex items-center justify-end px-1">
<div className="w-4 h-4 bg-white rounded-full"></div>
</div>
</div>
<p className="text-[#CCCCCC] text-sm">
These cookies are essential for the website to function properly. They cannot be disabled.
</p>
</div>
{/* Analytics Cookies */}
<div className="bg-white/5 rounded-lg p-4 border border-white/10">
<div className="flex items-center justify-between mb-2">
<h4 className="text-white font-medium">Analytics Cookies</h4>
<div className="w-12 h-6 bg-white/20 rounded-full flex items-center justify-start px-1">
<div className="w-4 h-4 bg-white rounded-full"></div>
</div>
</div>
<p className="text-[#CCCCCC] text-sm">
Help us understand how visitors interact with our website by collecting anonymous information.
</p>
</div>
{/* Marketing Cookies */}
<div className="bg-white/5 rounded-lg p-4 border border-white/10">
<div className="flex items-center justify-between mb-2">
<h4 className="text-white font-medium">Marketing Cookies</h4>
<div className="w-12 h-6 bg-white/20 rounded-full flex items-center justify-start px-1">
<div className="w-4 h-4 bg-white rounded-full"></div>
</div>
</div>
<p className="text-[#CCCCCC] text-sm">
Used to track visitors across websites to display relevant advertisements.
</p>
</div>
</div>
{/* Settings Action Buttons */}
<div className="flex flex-col sm:flex-row gap-3 justify-end">
<Button
variant="outline"
size="sm"
className="border-white/20 text-white hover:bg-white/10 hover:border-white/30 hover:text-white transition-all duration-300"
onClick={() => setShowSettings(false)}
>
Back
</Button>
<Button
size="sm"
className="bg-[#E5195E] hover:bg-[#E5195E]/90 text-white transition-all duration-300"
onClick={handleSavePreferences}
>
Save Preferences
</Button>
</div>
</div>
</div>
)}
</div>
</motion.div>
</AnimatePresence>
);
};

192
components/CountryFlags.tsx Normal file
View File

@@ -0,0 +1,192 @@
import { motion } from "framer-motion";
const foundersAndCTOs = [
{
name: "SimpliTend",
title: "HealthTech Platform",
country: "India",
code: "IN",
flagEmoji: "🇮🇳",
projectType: "Care Management"
},
{
name: "Seezun",
title: "Fashion Marketplace",
country: "United Kingdom",
code: "GB",
flagEmoji: "🇬🇧",
projectType: "P2P Platform"
},
{
name: "AMOZ",
title: "E-commerce Platform",
country: "United States",
code: "US",
flagEmoji: "🇺🇸",
projectType: "Digital Commerce"
},
{
name: "TradersCircuit",
title: "Trading Platform",
country: "United Arab Emirates",
code: "AE",
flagEmoji: "🇦🇪",
projectType: "FinTech"
},
{
name: "FreeU",
title: "Social Platform",
country: "Australia",
code: "AU",
flagEmoji: "🇦🇺",
projectType: "Community"
},
{
name: "Dorf Ketal",
title: "Manufacturing Tech",
country: "Germany",
code: "DE",
flagEmoji: "🇩🇪",
projectType: "Industrial IoT"
},
{
name: "WOKA",
title: "Learning Platform",
country: "Singapore",
code: "SG",
flagEmoji: "🇸🇬",
projectType: "EdTech"
},
{
name: "Regroup",
title: "Sports Networking",
country: "Canada",
code: "CA",
flagEmoji: "🇨🇦",
projectType: "Social Sports"
},
{
name: "Tanami Capital",
title: "Wealth Management",
country: "Brazil",
code: "BR",
flagEmoji: "🇧🇷",
projectType: "FinTech"
},
{
name: "SSA",
title: "Networking Platform",
country: "Japan",
code: "JP",
flagEmoji: "🇯🇵",
projectType: "Professional Network"
},
{
name: "Amble",
title: "Travel Platform",
country: "France",
code: "FR",
flagEmoji: "🇫🇷",
projectType: "Travel Tech"
},
{
name: "Agromate",
title: "AgriTech Solution",
country: "Netherlands",
code: "NL",
flagEmoji: "🇳🇱",
projectType: "Agriculture"
},
{
name: "Moving Cargo",
title: "Logistics Platform",
country: "Sweden",
code: "SE",
flagEmoji: "🇸🇪",
projectType: "Supply Chain"
},
{
name: "Farm Feeder",
title: "Agricultural Tech",
country: "New Zealand",
code: "NZ",
flagEmoji: "🇳🇿",
projectType: "AgriTech"
},
{
name: "Melbourne City Card",
title: "City Services Platform",
country: "South Korea",
code: "KR",
flagEmoji: "🇰🇷",
projectType: "Civic Tech"
}
];
export const CountryFlags = () => {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.3 }}
viewport={{ once: true }}
className="mb-12"
>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-6 max-w-7xl mx-auto">
{foundersAndCTOs.map((project, index) => (
<motion.div
key={project.code + index}
initial={{ opacity: 0, scale: 0 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.5,
delay: 0.5 + (index * 0.1),
type: "spring",
stiffness: 200
}}
viewport={{ once: true }}
whileHover={{
scale: 1.05,
transition: { duration: 0.2 }
}}
className="group cursor-pointer relative"
>
<div className="text-center">
{/* Flag Icon */}
<div className="flex justify-center mb-3">
<div className="w-14 h-14 rounded-full bg-white/10 backdrop-blur-sm border border-white/20 flex items-center justify-center group-hover:bg-white/20 group-hover:border-accent/30 transition-all duration-300 shadow-lg group-hover:shadow-xl">
<span className="text-2xl" role="img" aria-label={`${project.country} flag`}>
{project.flagEmoji}
</span>
</div>
</div>
{/* Project Badge */}
<div className="bg-white/8 rounded-lg border border-white/10 px-3 py-2.5 group-hover:bg-white/12 group-hover:border-accent/30 transition-all duration-300 shadow-sm backdrop-blur-sm min-h-[60px] flex flex-col justify-center">
<div className="text-white/90 font-medium text-sm leading-tight mb-1 group-hover:text-white transition-colors duration-300">
{project.name}
</div>
<div className="text-white/60 text-xs leading-tight">
{project.projectType}
</div>
</div>
{/* Enhanced Tooltip */}
<div className="absolute -top-24 left-1/2 transform -translate-x-1/2 bg-[#0E0E0E] text-white text-xs px-4 py-3 rounded-xl opacity-0 group-hover:opacity-100 transition-all duration-300 whitespace-nowrap pointer-events-none shadow-xl border border-white/20 z-50 backdrop-blur-md">
<div className="text-center">
<div className="font-semibold text-accent mb-1">{project.name}</div>
<div className="text-white/80 mb-1">{project.title}</div>
<div className="text-white/60 flex items-center gap-1 justify-center">
<span>{project.flagEmoji}</span>
<span>{project.country}</span>
</div>
</div>
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-[#0E0E0E]"></div>
</div>
</div>
</motion.div>
))}
</div>
</motion.div>
);
};

View File

@@ -0,0 +1,160 @@
import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
interface CustomReCaptchaProps {
siteKey: string;
onVerify: (token: string) => void;
onExpired?: () => void;
onError?: () => void;
className?: string;
}
export interface ReCaptchaRef {
reset: () => void;
execute: () => void;
}
declare global {
interface Window {
grecaptcha: any;
}
}
const CustomReCaptcha = forwardRef<ReCaptchaRef, CustomReCaptchaProps>(({
siteKey,
onVerify,
onExpired,
onError,
className = ""
}, ref) => {
const captchaRef = useRef<HTMLDivElement>(null);
const widgetId = useRef<number | null>(null);
const isLoadedRef = useRef(false);
useImperativeHandle(ref, () => ({
reset: () => {
if (window.grecaptcha && widgetId.current !== null) {
window.grecaptcha.reset(widgetId.current);
}
},
execute: () => {
if (window.grecaptcha && widgetId.current !== null) {
window.grecaptcha.execute(widgetId.current);
}
}
}));
const loadReCaptcha = () => {
if (window.grecaptcha) {
renderReCaptcha();
return;
}
// Load reCAPTCHA script
const script = document.createElement('script');
script.src = 'https://www.google.com/recaptcha/api.js?onload=onReCaptchaLoad&render=explicit';
script.async = true;
script.defer = true;
// Set up callback for when script loads
(window as any).onReCaptchaLoad = () => {
renderReCaptcha();
};
document.head.appendChild(script);
};
const renderReCaptcha = () => {
if (!captchaRef.current || isLoadedRef.current) return;
try {
widgetId.current = window.grecaptcha.render(captchaRef.current, {
sitekey: siteKey,
callback: onVerify,
'expired-callback': onExpired,
'error-callback': onError,
theme: 'dark',
size: 'normal'
});
isLoadedRef.current = true;
} catch (error) {
console.error('Error rendering reCAPTCHA:', error);
if (onError) onError();
}
};
useEffect(() => {
loadReCaptcha();
return () => {
// Cleanup
if (window.grecaptcha && widgetId.current !== null) {
try {
window.grecaptcha.reset(widgetId.current);
} catch (error) {
// Ignore cleanup errors
}
}
};
}, []);
// Add styles to document head instead of using styled-jsx
useEffect(() => {
const styleId = 'custom-recaptcha-styles';
// Check if styles are already added
if (document.getElementById(styleId)) {
return;
}
const styleElement = document.createElement('style');
styleElement.id = styleId;
styleElement.textContent = `
.grecaptcha-badge {
visibility: hidden;
}
iframe[src*="recaptcha"] {
border-radius: 8px !important;
overflow: hidden;
}
.g-recaptcha {
transform: scale(1);
transform-origin: center;
}
.g-recaptcha > div {
border-radius: 8px !important;
overflow: hidden;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
`;
document.head.appendChild(styleElement);
// Cleanup function to remove styles when component unmounts
return () => {
const existingStyle = document.getElementById(styleId);
if (existingStyle) {
document.head.removeChild(existingStyle);
}
};
}, []);
return (
<div className={`flex justify-center ${className}`}>
<div
className="bg-gray-800/30 border border-gray-600/50 rounded-xl p-6 shadow-lg backdrop-blur-sm"
style={{
'--recaptcha-border-radius': '12px'
} as React.CSSProperties}
>
<div ref={captchaRef} />
</div>
</div>
);
});
CustomReCaptcha.displayName = 'CustomReCaptcha';
export default CustomReCaptcha;

79
components/FAQSection.tsx Normal file
View File

@@ -0,0 +1,79 @@
import React from "react";
import { motion } from "framer-motion";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./ui/accordion";
import { ChevronDown } from "lucide-react";
interface FAQ {
question: string;
answer: string;
}
interface FAQSectionProps {
title?: string;
subtitle?: string;
faqs: FAQ[];
}
export const FAQSection: React.FC<FAQSectionProps> = ({
title = "Frequently Asked Questions",
subtitle,
faqs
}) => {
return (
<section className="py-20 bg-black">
<div className="container mx-auto px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-4xl lg:text-5xl font-semibold text-white mb-6">
{title}
</h2>
{subtitle && (
<p className="text-lg text-gray-400 max-w-2xl mx-auto">
{subtitle}
</p>
)}
</motion.div>
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
viewport={{ once: true }}
className="max-w-4xl mx-auto"
>
<Accordion type="single" collapsible className="w-full space-y-4">
{faqs.map((faq, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
viewport={{ once: true }}
>
<AccordionItem
value={`item-${index}`}
className="border-none bg-slate-800/40 rounded-xl overflow-hidden"
>
<AccordionTrigger
className="text-left text-white hover:text-white hover:no-underline px-6 py-6 text-lg font-medium [&[data-state=open]>svg]:rotate-180"
>
<span className="flex-1 text-left">{faq.question}</span>
<ChevronDown className="h-5 w-5 shrink-0 text-white transition-transform duration-200" />
</AccordionTrigger>
<AccordionContent className="text-gray-300 px-6 pb-6 pt-0 text-base leading-relaxed">
{faq.answer}
</AccordionContent>
</AccordionItem>
</motion.div>
))}
</Accordion>
</motion.div>
</div>
</section>
);
};

View File

@@ -0,0 +1,497 @@
import React from "react";
import { motion } from "framer-motion";
import { Button } from "./ui/button";
import { Badge } from "./ui/badge";
import { Card, CardContent } from "./ui/card";
import { ImageWithFallback } from "./figma/ImageWithFallback";
import { navigateTo } from "../App";
import {
ArrowRight,
TrendingUp,
Users,
Zap,
Eye,
ShoppingCart,
Heart,
Star,
Clock,
Target,
Smartphone,
BarChart3,
Settings,
Network,
Search,
Calendar,
PlayCircle,
PartyPopper,
PieChart
} from "lucide-react";
const FeaturedCaseStudies = () => {
const caseStudies = [
{
id: 1,
title: "SimplyTend",
client: "Simply Tend",
description: "SimpliTend is a mobile-first care management platform that connects patients and caregivers through real-time alerts, scheduling, and safety tools—delivered via dual apps and an admin dashboard.",
keyAchievement: {
number: "95%",
metric: "Care Coordination Efficiency",
icon: Heart
},
visual: "https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=600&h=400&fit=crop&auto=format",
tags: ["Mobile App", "Care Management", "Real-Time Alerts", "Scheduling"],
gradient: "from-blue-500/20 to-cyan-500/20",
accentColor: "blue",
featured: true
},
{
id: 2,
title: "Seezun",
client: "Seezun",
description: "Seezun is a trend-driven P2P fashion marketplace enabling users to rent, buy, sell, or lend South Asian ethnicwear via mobile and web platforms.",
keyAchievement: {
number: "85%",
metric: "Brand Recognition",
icon: TrendingUp
},
visual: "https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=600&h=400&fit=crop&auto=format",
tags: ["Marketplace", "P2P", "Fashion", "Mobile & Web"],
gradient: "from-purple-500/20 to-pink-500/20",
accentColor: "purple",
featured: false
},
{
id: 3,
title: "WOKA",
client: "WOKA Creations Pvt. Ltd",
description: "WDI transformed WOKA's hybrid app into a high-performance native Android and iOS platform featuring seamless streaming, deep analytics, and robust 120-hour monthly support.",
keyAchievement: {
number: "300%",
metric: "User Retention",
icon: Users
},
visual: "https://images.unsplash.com/photo-1503676260728-1c00da094a0b?w=600&h=400&fit=crop&auto=format",
tags: ["Native App", "Streaming", "Analytics", "Support"],
gradient: "from-green-500/20 to-emerald-500/20",
accentColor: "green",
featured: false
}
];
const moreSuccessStories = [
{
id: 4,
title: "TradersCircuit",
client: "TradersCircuit",
description: "TradersCircuit empowers India's millennials and Gen Z with smarter investments through seamless investment experience and ultra-personalized financial planning.",
keyAchievement: {
number: "300%",
metric: "User Growth",
icon: TrendingUp
},
visual: "https://images.unsplash.com/photo-1611974789855-9c2a0a7236a3?w=600&h=400&fit=crop&auto=format",
tags: ["FinTech", "Trading Platform", "Indian Market", "Mobile App"],
gradient: "from-green-500/20 to-emerald-500/20",
accentColor: "green",
featured: false
},
{
id: 5,
title: "RanOutOf",
client: "Global Ease Solutions",
description: "WDI developed a smart grocery planning app with barcode scanning, voice commands, reminders, and a web-based admin CMS for Global Ease Solutions.",
keyAchievement: {
number: "75%",
metric: "Shopping Efficiency",
icon: ShoppingCart
},
visual: "https://images.unsplash.com/photo-1542838132-92c53300491e?w=600&h=400&fit=crop&auto=format",
tags: ["Mobile App", "Barcode Scanning", "Voice AI", "Grocery Tech"],
gradient: "from-green-500/20 to-emerald-500/20",
accentColor: "green",
featured: false
},
{
id: 6,
title: "Prosperty",
client: "Prosperty Ltd",
description: "Break the barrier of real estate investing. With Prosperty, you can invest in portions of properties, making portfolio diversification smarter and more accessible.",
keyAchievement: {
number: "300%",
metric: "Portfolio Options",
icon: PieChart
},
visual: "https://images.unsplash.com/photo-1560518883-ce09059eeffa?w=600&h=400&fit=crop&auto=format",
tags: ["Real Estate", "Investment", "FinTech", "Portfolio"],
gradient: "from-blue-500/20 to-cyan-500/20",
accentColor: "blue",
featured: false
},
{
id: 7,
title: "GoodTimes",
client: "GoodTimes Ltd",
description: "From casual hangouts to special celebrations, Good Times makes browsing and booking a breeze, so you never miss out.",
keyAchievement: {
number: "250%",
metric: "Event Discovery",
icon: PartyPopper
},
visual: "https://images.unsplash.com/photo-1492684223066-81342ee5ff30?w=600&h=400&fit=crop&auto=format",
tags: ["Events", "Booking", "Lifestyle", "Mobile App"],
gradient: "from-purple-500/20 to-pink-500/20",
accentColor: "purple",
featured: false
}
];
return (
<section className="py-20 bg-black">
<div className="container mx-auto px-6 lg:px-8">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="mb-6"
>
<div className="inline-block p-[2px] rounded-full bg-gradient-to-r from-accent via-blue-500 to-purple-500">
<div className="bg-black rounded-full px-6 py-3 flex items-center gap-2">
<Star className="w-5 h-5 text-accent" />
<span className="text-white text-base font-medium">Featured Work</span>
</div>
</div>
</motion.div>
<h2 className="text-4xl lg:text-5xl font-semibold text-white mb-6">
Featured Success Stories
</h2>
<p className="text-xl text-gray-300 max-w-3xl mx-auto leading-relaxed">
Discover how we've helped companies across industries achieve remarkable results with our innovative development solutions.
</p>
</motion.div>
{/* Case Studies Grid - Consistent Dimensions */}
<div className="grid lg:grid-cols-3 gap-8 items-stretch">
{caseStudies.map((study, index) => {
const AchievementIcon = study.keyAchievement.icon;
return (
<motion.div
key={study.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.2 }}
viewport={{ once: true }}
className="group h-full"
>
<Card
className="bg-gray-900/50 backdrop-blur-md border-gray-800 hover:border-accent/30 transition-all duration-500 shadow-lg hover:shadow-2xl rounded-2xl overflow-hidden h-full group-hover:scale-[1.02] transform flex flex-col cursor-pointer"
onClick={() => {
if (study.title === 'Seezun') {
navigateTo('/projects/seezun');
} else if (study.title === 'WOKA') {
navigateTo('/projects/woka');
} else if (study.title === 'Tanami') {
navigateTo('/projects/tanami');
} else {
navigateTo('/case-studies');
}
}}
>
<CardContent className="p-0 flex flex-col h-full min-h-[600px]">
{/* Visual Section - Fixed Height */}
<div className="relative overflow-hidden">
<div className="relative h-64 w-full">
<ImageWithFallback
src={study.visual}
alt={study.title}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
/>
{/* Overlay with gradient */}
<div className={`absolute inset-0 bg-gradient-to-t ${study.gradient} opacity-20 group-hover:opacity-40 transition-opacity duration-500`} />
{/* Featured Badge */}
{study.featured && (
<div className="absolute top-4 left-4">
<Badge className="bg-accent text-white shadow-lg">
<Star className="w-3 h-3 mr-1" />
Featured
</Badge>
</div>
)}
{/* Key Achievement - Overlaid on Visual */}
<div className="absolute bottom-4 left-4 right-4">
<motion.div
whileHover={{ scale: 1.05 }}
className="bg-black/80 backdrop-blur-md rounded-xl p-4 border border-white/10"
>
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-lg bg-gradient-to-r ${
study.accentColor === 'blue' ? 'from-blue-500 to-cyan-500' :
study.accentColor === 'green' ? 'from-green-500 to-emerald-500' :
study.accentColor === 'purple' ? 'from-purple-500 to-pink-500' :
study.accentColor === 'cyan' ? 'from-cyan-500 to-blue-500' :
study.accentColor === 'orange' ? 'from-orange-500 to-red-500' :
'from-emerald-500 to-teal-500'
} flex items-center justify-center flex-shrink-0`}>
<AchievementIcon className="w-5 h-5 text-white" />
</div>
<div className="min-w-0 flex-1">
<div className="text-2xl font-bold text-white">{study.keyAchievement.number}</div>
<div className="text-sm text-gray-300 leading-tight">{study.keyAchievement.metric}</div>
</div>
</div>
</motion.div>
</div>
</div>
</div>
{/* Content Section - Flexible Height with Consistent Spacing */}
<div className="p-6 flex-1 flex flex-col justify-between min-h-[336px]">
<div className="flex-1">
{/* Project Title - Consistent Height */}
<div className="mb-4 min-h-[60px] flex items-start">
<h3 className="text-xl font-semibold text-white leading-tight group-hover:text-accent transition-colors duration-300 line-clamp-2">
{study.title}
</h3>
</div>
{/* Client & Description - Consistent Height */}
<div className="mb-6 min-h-[100px]">
<div className="text-accent font-medium text-sm mb-2">{study.client}</div>
<p className="text-gray-300 text-sm leading-relaxed line-clamp-4">
{study.description}
</p>
</div>
{/* Tags - Consistent Height */}
<div className="mb-6 min-h-[32px]">
<div className="flex flex-wrap gap-2">
{study.tags.map((tag) => (
<Badge
key={tag}
variant="secondary"
className="text-xs bg-gray-800/50 text-gray-300 border-gray-700 hover:bg-gray-700/50 transition-colors"
>
{tag}
</Badge>
))}
</div>
</div>
</div>
{/* CTA Button - Fixed at Bottom */}
<div className="mt-auto">
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
<Button
className="w-full bg-gradient-to-r from-accent to-accent/80 hover:from-accent/90 hover:to-accent/70 text-white font-semibold py-3 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 group h-12"
onClick={(e) => {
e.stopPropagation();
if (study.title === 'Seezun') {
navigateTo('/projects/seezun');
} else if (study.title === 'WOKA') {
navigateTo('/projects/woka');
} else if (study.title === 'Tanami') {
navigateTo('/projects/tanami');
} else {
navigateTo('/case-studies');
}
}}
>
<span>View Full Case Study</span>
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform duration-300" />
</Button>
</motion.div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
);
})}
</div>
{/* More Success Stories Section */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
viewport={{ once: true }}
className="mt-20 mb-16"
>
<h3 className="text-3xl lg:text-4xl font-semibold text-white mb-12 text-center">
More Success Stories
</h3>
<div className="grid lg:grid-cols-3 gap-8 items-stretch">
{moreSuccessStories.map((story, index) => {
const AchievementIcon = story.keyAchievement.icon;
return (
<motion.div
key={story.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
className="group h-full"
>
<Card
className="bg-gray-900/50 backdrop-blur-md border-gray-800 hover:border-accent/30 transition-all duration-500 shadow-lg hover:shadow-2xl rounded-2xl overflow-hidden h-full group-hover:scale-[1.02] transform flex flex-col cursor-pointer"
onClick={() => {
if (story.title === 'TradersCircuit') {
navigateTo('/projects/traderscircuit');
} else if (story.title === 'GoodTimes') {
navigateTo('/projects/goodtimes');
} else if (story.title === 'Prosperty') {
navigateTo('/projects/prosperty');
} else if (story.title === 'RanOutOf') {
navigateTo('/projects/ranoutof');
} else {
navigateTo('/case-studies');
}
}}
>
<CardContent className="p-0 flex flex-col h-full min-h-[600px]">
{/* Visual Section - Fixed Height */}
<div className="relative overflow-hidden">
<div className="relative h-64 w-full">
<ImageWithFallback
src={story.visual}
alt={`${story.title} - ${story.client}`}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
/>
{/* Overlay with gradient */}
<div className={`absolute inset-0 bg-gradient-to-t ${story.gradient} opacity-20 group-hover:opacity-40 transition-opacity duration-500`} />
{/* Key Achievement - Overlaid on Visual */}
<div className="absolute bottom-4 left-4 right-4">
<motion.div
whileHover={{ scale: 1.05 }}
className="bg-black/80 backdrop-blur-md rounded-xl p-4 border border-white/10"
>
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-lg bg-gradient-to-r ${
story.accentColor === 'blue' ? 'from-blue-500 to-cyan-500' :
story.accentColor === 'green' ? 'from-green-500 to-emerald-500' :
story.accentColor === 'purple' ? 'from-purple-500 to-pink-500' :
story.accentColor === 'cyan' ? 'from-cyan-500 to-blue-500' :
story.accentColor === 'orange' ? 'from-orange-500 to-red-500' :
'from-emerald-500 to-teal-500'
} flex items-center justify-center flex-shrink-0`}>
<AchievementIcon className="w-5 h-5 text-white" />
</div>
<div className="min-w-0 flex-1">
<div className="text-2xl font-bold text-white">{story.keyAchievement.number}</div>
<div className="text-sm text-gray-300 leading-tight">{story.keyAchievement.metric}</div>
</div>
</div>
</motion.div>
</div>
</div>
</div>
{/* Content Section - Flexible Height with Consistent Spacing */}
<div className="p-6 flex-1 flex flex-col justify-between min-h-[336px]">
<div className="flex-1">
{/* Project Title - Consistent Height */}
<div className="mb-4 min-h-[60px] flex items-start">
<h4 className="text-xl font-semibold text-white leading-tight group-hover:text-accent transition-colors duration-300 line-clamp-2">
{story.title}
</h4>
</div>
{/* Client & Description - Consistent Height */}
<div className="mb-6 min-h-[100px]">
<div className="text-accent font-medium text-sm mb-2">{story.client}</div>
<p className="text-gray-300 text-sm leading-relaxed line-clamp-4">
{story.description}
</p>
</div>
{/* Tags - Consistent Height */}
<div className="mb-6 min-h-[32px]">
<div className="flex flex-wrap gap-2">
{story.tags.map((tag) => (
<Badge
key={tag}
variant="secondary"
className="text-xs bg-gray-800/50 text-gray-300 border-gray-700 hover:bg-gray-700/50 transition-colors"
>
{tag}
</Badge>
))}
</div>
</div>
</div>
{/* CTA Button - Fixed at Bottom */}
<div className="mt-auto">
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
<Button
className="w-full bg-gradient-to-r from-accent to-accent/80 hover:from-accent/90 hover:to-accent/70 text-white font-semibold py-3 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 group h-12"
onClick={(e) => {
e.stopPropagation();
if (story.title === 'TradersCircuit') {
navigateTo('/projects/traderscircuit');
} else if (story.title === 'GoodTimes') {
navigateTo('/projects/goodtimes');
} else if (story.title === 'Prosperty') {
navigateTo('/projects/prosperty');
} else if (story.title === 'RanOutOf') {
navigateTo('/projects/ranoutof');
} else {
navigateTo('/case-studies');
}
}}
>
<span>View Full Case Study</span>
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform duration-300" />
</Button>
</motion.div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
);
})}
</div>
</motion.div>
{/* Bottom CTA */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.6 }}
viewport={{ once: true }}
className="text-center mt-16"
>
<Button
variant="outline"
size="lg"
className="border-gray-600 text-gray-300 hover:bg-gray-800 hover:text-white hover:border-accent/50 transition-all duration-300"
>
<Eye className="w-5 h-5 mr-2" />
View All Case Studies
</Button>
</motion.div>
</div>
</section>
);
};
export default FeaturedCaseStudies;

426
components/Footer.tsx Normal file
View File

@@ -0,0 +1,426 @@
import { motion } from "framer-motion";
import {
Mail,
Phone,
MapPin,
Linkedin,
Twitter,
Github,
Youtube,
} from "lucide-react";
import { GridPattern } from "./GridPattern";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import BlackLogo14 from "../imports/BlackLogo14";
import { navigateTo } from "../App";
import { useState } from "react";
const footerNavigation = {
Explore: [
{ label: "Home", url: "/home" },
{ label: "Services", url: "/services" },
{ label: "Solutions", url: "/solutions" },
{ label: "Industries", url: "/industries" },
{ label: "Company", url: "/company" },
{ label: "Contact", url: "/contact" },
],
Resources: [
{ label: "Articles", url: "/resources/blog" },
{ label: "Case Studies", url: "/case-studies" },
{
label: "Client Testimonials",
url: "/resources/client-testimonials",
},
{
label: "Whitepapers & Insights",
url: "/resources/whitepapers-insights",
},
{ label: "FAQ", url: "/resources/faqs" },
],
Services: [
{
label: "Mobile App Development",
url: "/services/mobile-app-development",
},
{
label: "Web & Cloud Solutions",
url: "/services/web-cloud-solutions",
},
{
label: "Software Engineering",
url: "/services/software-engineering",
},
{
label: "Design & Experience",
url: "/services/design-experience",
},
],
"AI & ML": [
{
label: "Artificial Intelligence Services",
url: "/ai/artificial-intelligence-services",
},
{
label: "Machine Learning Solutions",
url: "/ai/machine-learning-solutions",
},
],
Solutions: [
{
label: "Digital Product Development",
url: "/digital-product-development",
},
{
label: "MVP & Startup Launch Packages",
url: "/mvp-startup-launch",
},
{
label: "Legacy System Rebuilds",
url: "/legacy-system-rebuilds",
},
{
label: "Dedicated Development Centers",
url: "/dedicated-development-centers",
},
{
label: "Business Process Automation",
url: "/business-process-automation",
},
{
label: "Compliance-Ready Systems",
url: "/compliance-ready-systems",
},
],
};
const socialLinks = [
{
name: "LinkedIn",
icon: Linkedin,
url: "https://linkedin.com/company/wdi",
},
{
name: "Twitter",
icon: Twitter,
url: "https://twitter.com/wdi_dev",
},
{
name: "GitHub",
icon: Github,
url: "https://github.com/wdi",
},
{
name: "YouTube",
icon: Youtube,
url: "https://youtube.com/wdi",
},
];
const contactInfo = [
{
icon: Mail,
label: "ideas@wdipl.com",
url: "mailto:ideas@wdipl.com",
},
{
icon: Phone,
label: "(+91) 7700900039",
url: "tel:+917700900039",
},
{
icon: MapPin,
label:
"614, Palm Spring Centre, Link Road, Malad (West), Mumbai - 400064. India.",
url: "#",
},
];
const FooterSection = ({
title,
links,
delay = 0,
}: {
title: string;
links: Array<{ label: string; url: string }>;
delay?: number;
}) => (
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay }}
viewport={{ once: true }}
className="space-y-4"
>
<h4 className="font-semibold text-white text-lg">
{title}
</h4>
<ul className="space-y-3">
{links.map((link) => (
<li key={link.label}>
<a
href="#"
onClick={(e) => {
e.preventDefault();
navigateTo(link.url);
}}
className="text-[#CCCCCC] hover:text-white transition-colors duration-200 text-sm block py-1 hover:translate-x-1 transform cursor-pointer"
>
{link.label}
</a>
</li>
))}
</ul>
</motion.div>
);
// Newsletter subscription component
const NewsletterSection = () => {
const [email, setEmail] = useState("");
const [isSubscribed, setIsSubscribed] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubscribe = async (e: React.FormEvent) => {
e.preventDefault();
if (!email) return;
setIsSubmitting(true);
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000));
setIsSubscribed(true);
setIsSubmitting(false);
setEmail("");
// Reset success message after 3 seconds
setTimeout(() => setIsSubscribed(false), 3000);
};
return (
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.5 }}
viewport={{ once: true }}
className="border-t border-white/10"
>
<div className="container mx-auto px-6 lg:px-8 py-16">
<div className="max-w-4xl mx-auto text-center">
<h3 className="text-2xl lg:text-3xl font-semibold text-white mb-4">
Never Miss an Update
</h3>
<p className="text-[#CCCCCC] text-lg mb-8 max-w-2xl mx-auto">
Get the latest insights on digital product
development, AI trends, and startup success stories
delivered to your inbox.
</p>
{isSubscribed ? (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="bg-green-500/10 border border-green-500/20 rounded-lg p-6 max-w-md mx-auto"
>
<div className="flex items-center justify-center gap-2 text-green-400">
<Mail className="w-5 h-5" />
<span className="font-medium">
Successfully subscribed!
</span>
</div>
<p className="text-green-300 text-sm mt-2">
Welcome to our community of innovators.
</p>
</motion.div>
) : (
<form
onSubmit={handleSubscribe}
className="max-w-md mx-auto"
>
<div className="flex gap-3">
<Input
type="email"
placeholder="Enter your email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="flex-1 bg-white/5 border-white/10 text-white placeholder:text-[#CCCCCC] focus:border-[#E5195E] focus:ring-[#E5195E]/20"
required
/>
<Button
type="submit"
disabled={isSubmitting}
className="bg-[#E5195E] hover:bg-[#E5195E]/90 text-white px-6 shrink-0 disabled:opacity-50"
>
{isSubmitting
? "Subscribing..."
: "Subscribe"}
</Button>
</div>
<p className="text-[#CCCCCC] text-xs mt-3">
No spam, unsubscribe at any time. We respect
your privacy.
</p>
</form>
)}
</div>
</div>
</motion.div>
);
};
export const Footer = () => {
return (
<footer className="relative bg-[#0E0E0E] border-t border-white/10 overflow-hidden">
<GridPattern strokeDasharray="4 2" />
<div className="relative z-10">
{/* Main Footer Content */}
<div className="container mx-auto px-6 lg:px-8 py-16">
<div className="grid lg:grid-cols-7 gap-12">
{/* Company Info */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="lg:col-span-2 space-y-6"
>
<div className="flex items-center">
<div className="w-12 h-12">
<BlackLogo14 />
</div>
</div>
<p className="text-[#CCCCCC] leading-relaxed max-w-md">
Web Development Institute - Transforming ideas
into scalable digital products. 25+ years of
industry expertise, serving founders and CTOs
across 15+ countries.
</p>
{/* India Office Contact Information */}
<div className="space-y-4">
<h5 className="font-semibold text-white text-sm tracking-wide uppercase">
India Office
</h5>
<div className="space-y-3">
{contactInfo.map((contact) => {
const Icon = contact.icon;
return (
<a
key={contact.label}
href={contact.url}
className="flex items-start gap-3 text-[#CCCCCC] hover:text-white transition-colors duration-200"
>
<Icon className="w-4 h-4 text-[#E5195E] mt-0.5 flex-shrink-0" />
<span className="text-sm leading-relaxed">
{contact.label}
</span>
</a>
);
})}
</div>
</div>
{/* Social Links */}
<div className="flex gap-4 pt-4">
{socialLinks.map((social) => {
const Icon = social.icon;
return (
<a
key={social.name}
href={social.url}
className="w-10 h-10 bg-white/5 rounded-lg flex items-center justify-center text-[#CCCCCC] hover:text-white hover:bg-[#E5195E]/20 transition-all duration-200"
aria-label={social.name}
>
<Icon className="w-4 h-4" />
</a>
);
})}
</div>
</motion.div>
{/* Navigation Sections */}
<FooterSection
title="Explore"
links={footerNavigation.Explore}
delay={0.1}
/>
<FooterSection
title="Services"
links={footerNavigation.Services}
delay={0.2}
/>
<FooterSection
title="AI & ML"
links={footerNavigation["AI & ML"]}
delay={0.3}
/>
<FooterSection
title="Solutions"
links={footerNavigation.Solutions}
delay={0.4}
/>
<FooterSection
title="Resources"
links={footerNavigation.Resources}
delay={0.5}
/>
</div>
</div>
{/* Newsletter Subscription Section */}
<NewsletterSection />
{/* Bottom Bar */}
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.6 }}
viewport={{ once: true }}
className="border-t border-white/10"
>
<div className="container mx-auto px-6 lg:px-8 py-8">
<div className="flex flex-col lg:flex-row justify-between items-center gap-6">
<div className="text-[#CCCCCC] text-sm text-center lg:text-left">
© 2024 Web Development Institute. All rights
reserved.
</div>
<div className="flex flex-wrap gap-6 text-sm">
<a
href="/privacy"
className="text-[#CCCCCC] hover:text-white transition-colors"
>
Privacy Policy
</a>
<a
href="/terms"
className="text-[#CCCCCC] hover:text-white transition-colors"
>
Terms of Service
</a>
<a
href="/cookies"
className="text-[#CCCCCC] hover:text-white transition-colors"
>
Cookie Policy
</a>
<a
href="/sitemap"
className="text-[#CCCCCC] hover:text-white transition-colors"
>
Sitemap
</a>
</div>
<div className="text-[#CCCCCC] text-sm text-center lg:text-right">
Engineered by WDI because someone had to do it
right. 💻
</div>
</div>
</div>
</motion.div>
</div>
</footer>
);
};

View File

@@ -0,0 +1,26 @@
export const GridPattern = ({ strokeDasharray = "4 2" }: { strokeDasharray?: string }) => {
return (
<svg
className="absolute inset-0 h-full w-full opacity-20"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<pattern
id="grid"
width="40"
height="40"
patternUnits="userSpaceOnUse"
>
<path
d="M 40 0 L 0 0 0 40"
fill="none"
stroke="rgba(255,255,255,0.3)"
strokeWidth="1"
strokeDasharray={strokeDasharray}
/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
</svg>
);
};

83
components/HeroBanner.tsx Normal file
View File

@@ -0,0 +1,83 @@
import { Button } from "./ui/button";
import { GridPattern } from "./GridPattern";
import { ChevronRight } from "lucide-react";
import { navigateTo } from "../App";
interface HeroBannerProps {
category?: string;
title: string;
description: string;
primaryCTA: {
text: string;
href: string;
};
secondaryCTA?: {
text: string;
href: string;
};
}
export function HeroBanner({
category,
title,
description,
primaryCTA,
secondaryCTA
}: HeroBannerProps) {
return (
<section className="relative py-20 lg:py-32 bg-[#0E0E0E] overflow-hidden">
<GridPattern />
<div className="container mx-auto px-6 lg:px-8 relative z-10">
<div className="max-w-4xl mx-auto text-center">
{/* Category Badge */}
{category && (
<div className="inline-flex items-center rounded-full px-4 py-2 mb-8 bg-[#E5195E]/10 border border-[#E5195E]/20">
<span className="text-[#E5195E] text-sm font-medium">
{category}
</span>
</div>
)}
{/* Main Title */}
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-semibold tracking-tight text-white mb-6">
{title}
</h1>
{/* Description */}
<p className="text-lg lg:text-xl text-gray-400 mb-10 max-w-3xl mx-auto leading-relaxed">
{description}
</p>
{/* CTAs */}
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button
size="lg"
className="bg-gradient-to-r from-[#E5195E] to-[#C41653] hover:from-[#C41653] hover:to-[#A31348] text-white font-semibold px-8 py-4 h-auto text-base"
onClick={() => navigateTo(primaryCTA.href)}
>
{primaryCTA.text}
</Button>
{secondaryCTA && (
<Button
variant="secondary"
size="lg"
className="bg-white/5 hover:bg-white/10 text-white border-white/20 hover:border-white/30 font-medium px-8 py-4 h-auto text-base"
onClick={() => navigateTo(secondaryCTA.href)}
>
{secondaryCTA.text}
<ChevronRight className="w-4 h-4 ml-2" />
</Button>
)}
</div>
</div>
{/* Decorative Elements */}
<div className="absolute top-1/4 left-1/4 w-32 h-32 bg-gradient-to-r from-[#E5195E]/10 to-purple-500/10 rounded-full blur-3xl animate-pulse opacity-60"></div>
<div className="absolute top-3/4 right-1/4 w-24 h-24 bg-gradient-to-r from-blue-500/10 to-cyan-500/10 rounded-full blur-2xl animate-pulse delay-1000 opacity-60"></div>
<div className="absolute bottom-1/4 left-1/3 w-20 h-20 bg-gradient-to-r from-green-500/10 to-emerald-500/10 rounded-full blur-2xl animate-pulse delay-2000 opacity-60"></div>
</div>
</section>
);
}

View File

@@ -0,0 +1,90 @@
import { Button } from "./ui/button";
import { GridPattern } from "./GridPattern";
import { SplineFallback } from "./SplineFallback";
import { Calendar, Briefcase } from "lucide-react";
import { navigateTo } from "../App";
export function HeroSection() {
return (
<section id="hero" className="relative lg:min-h-[85vh] flex items-center pt-20">
<GridPattern />
<div className="container mx-auto px-6 lg:px-8">
<div className="flex flex-col-reverse lg:flex-row items-center gap-12 w-full py-24 relative z-10">
<div className="w-full lg:w-1/2">
{/* Animated Badge */}
<div className="group relative inline-flex items-center rounded-full px-4 py-1.5 shadow-[inset_0_-8px_10px_#8fdfff1f] transition-shadow duration-500 ease-out hover:shadow-[inset_0_-5px_10px_#8fdfff3f] mb-6">
<span
className="absolute inset-0 block h-full w-full animate-gradient rounded-[inherit] bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:300%_100%] p-[1px]"
style={{
WebkitMask: "linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0)",
WebkitMaskComposite: "destination-out",
maskComposite: "subtract",
}}
/>
<span className="relative z-10 flex items-center text-sm font-medium">
🎉
<span aria-hidden="true" className="mx-2 h-4 w-px shrink-0 bg-neutral-500" />
<span className="bg-clip-text text-transparent bg-[linear-gradient(90deg,#ffaa40_0%,#9c40ff_50%,#ffaa40_100%)] bg-[length:200%_100%] animate-[gradientMove_6s_ease_infinite]">
25+ Years Of Industry Expertise
</span>
<svg
className="ml-1 w-4 h-4 stroke-neutral-500 transition-transform duration-300 group-hover:translate-x-0.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</span>
</div>
<h1 className="text-4xl sm:text-5xl md:text-6xl font-semibold tracking-tight text-white max-w-3xl">
Architecting Digital Success for Startups & Enterprises
</h1>
<p className="mt-6 max-w-2xl text-lg text-gray-400">
We design and build secure, AI-powered apps and software tailored for scale, speed, and user engagement.
</p>
<div className="mt-10 flex flex-col sm:flex-row gap-3">
<Button size="lg" className="whitespace-nowrap" onClick={() => navigateTo('/contact')}>
<Calendar className="w-4 h-4" />
Book a Free Consultation
</Button>
<Button variant="secondary" size="lg" className="whitespace-nowrap" onClick={() => navigateTo('/services')}>
<Briefcase className="w-4 h-4" />
Explore Services
</Button>
</div>
</div>
<div className="w-full lg:w-1/2 h-[320px] md:h-[480px] lg:h-[560px] shrink-0 relative">
{/* Animated Background Elements */}
<div className="absolute inset-0 overflow-hidden rounded-xl">
<div className="absolute top-1/4 left-1/4 w-32 h-32 bg-gradient-to-r from-[#E5195E]/20 to-purple-500/20 rounded-full blur-3xl animate-pulse"></div>
<div className="absolute top-3/4 right-1/4 w-24 h-24 bg-gradient-to-r from-blue-500/20 to-cyan-500/20 rounded-full blur-2xl animate-pulse delay-1000"></div>
<div className="absolute bottom-1/4 left-1/3 w-20 h-20 bg-gradient-to-r from-green-500/20 to-emerald-500/20 rounded-full blur-2xl animate-pulse delay-2000"></div>
</div>
{/* Interactive 3D-like Animation */}
<div className="relative w-full h-full rounded-xl overflow-hidden border border-gray-800/50 bg-gradient-to-br from-gray-900/50 to-gray-800/30 backdrop-blur-sm">
<div className="absolute inset-0 bg-gradient-to-br from-[#E5195E]/5 to-purple-500/5 rounded-xl"></div>
<div className="relative z-10 w-full h-full">
<SplineFallback />
</div>
</div>
</div>
</div>
{/* Floating Elements */}
<div className="absolute bottom-10 left-1/2 transform -translate-x-1/2 animate-bounce">
<svg className="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
</svg>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,95 @@
import { motion } from "framer-motion";
import {
CreditCard,
Heart,
ShoppingCart,
GraduationCap,
Truck,
Video,
Building,
Plane,
Factory,
Wheat,
Gamepad2,
Cloud
} from "lucide-react";
import { GridPattern } from "./GridPattern";
const industries = [
// First row
{ name: "FinTech", icon: CreditCard },
{ name: "HealthTech", icon: Heart },
{ name: "eCommerce", icon: ShoppingCart },
{ name: "EdTech", icon: GraduationCap },
// Second row
{ name: "Logistics", icon: Truck },
{ name: "Media & OTT", icon: Video },
{ name: "Real Estate", icon: Building },
{ name: "Travel", icon: Plane },
// Third row (we'll make it 3x4 instead to fit all 12)
{ name: "Manufacturing", icon: Factory },
{ name: "AgriTech", icon: Wheat },
{ name: "Gaming", icon: Gamepad2 },
{ name: "SaaS", icon: Cloud }
];
const IndustryCard = ({ industry, index }: {
industry: { name: string; icon: any };
index: number;
}) => {
const Icon = industry.icon;
return (
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
whileHover={{ y: -5, scale: 1.02 }}
className="group p-6 rounded-xl bg-white/5 backdrop-blur-sm border border-white/10 hover:border-[#E5195E]/50 transition-all duration-300 cursor-pointer text-center"
>
<div className="w-16 h-16 mx-auto mb-4 rounded-xl bg-[#E5195E]/10 flex items-center justify-center group-hover:scale-110 group-hover:bg-[#E5195E]/20 transition-all duration-300">
<Icon className="w-8 h-8 text-[#E5195E]" />
</div>
<h3 className="text-white font-medium text-lg group-hover:text-[#E5195E] transition-colors duration-300">
{industry.name}
</h3>
</motion.div>
);
};
export const HorizontalTagScroller = () => {
return (
<section className="relative py-20 bg-[#0E0E0E] overflow-hidden">
<GridPattern strokeDasharray="4 2" />
<div className="relative z-10 container mx-auto px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl lg:text-4xl font-semibold text-white mb-4">
Tailored Solutions for Your Industry
</h2>
<p className="text-[#CCCCCC] text-lg max-w-2xl mx-auto">
We serve diverse industries with specialized expertise and domain knowledge
</p>
</motion.div>
{/* 4x3 Grid for larger screens, 2x6 for tablets, 1x12 for mobile */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 max-w-7xl mx-auto">
{industries.map((industry, index) => (
<IndustryCard
key={industry.name}
industry={industry}
index={index}
/>
))}
</div>
</div>
</section>
);
};

214
components/InlineCTA.tsx Normal file
View File

@@ -0,0 +1,214 @@
import { motion } from "framer-motion";
import { Lightbulb, Clock } from "lucide-react";
import { Button } from "./ui/button";
import { GridPattern } from "./GridPattern";
import { navigateTo } from "../App";
export const InlineCTA = () => {
return (
<section className="relative py-20 overflow-hidden">
<GridPattern strokeDasharray="4 2" />
<div className="relative z-10 container mx-auto px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="max-w-4xl mx-auto text-center"
>
<div className="mb-8">
<motion.div
className="w-20 h-20 mx-auto mb-6 bg-white/10 backdrop-blur-sm rounded-full border border-white/20 flex items-center justify-center relative"
whileHover={{ scale: 1.05 }}
transition={{ duration: 0.3 }}
>
{/* Animated glow effect */}
<motion.div
className="absolute inset-0 rounded-full bg-accent/20"
animate={{
scale: [1, 1.2, 1],
opacity: [0.3, 0.6, 0.3],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
/>
{/* Pulsing ring */}
<motion.div
className="absolute inset-0 rounded-full border border-accent/30"
animate={{
scale: [1, 1.1, 1],
opacity: [0.5, 0.8, 0.5],
}}
transition={{
duration: 1.5,
repeat: Infinity,
ease: "easeInOut",
delay: 0.5
}}
/>
{/* Main icon with subtle animation */}
<motion.div
animate={{
rotate: [0, 5, -5, 0],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut"
}}
>
<Lightbulb className="w-10 h-10 text-accent relative z-10" />
</motion.div>
{/* Sparkle effects */}
<motion.div
className="absolute -top-1 -right-1 w-2 h-2 bg-accent rounded-full"
animate={{
scale: [0, 1.2, 0],
opacity: [0, 1, 0],
}}
transition={{
duration: 1.8,
repeat: Infinity,
ease: "easeInOut",
delay: 0.3
}}
/>
<motion.div
className="absolute -bottom-1 -left-1 w-1.5 h-1.5 bg-accent rounded-full"
animate={{
scale: [0, 1, 0],
opacity: [0, 0.8, 0],
}}
transition={{
duration: 2.2,
repeat: Infinity,
ease: "easeInOut",
delay: 0.8
}}
/>
<motion.div
className="absolute top-2 -left-2 w-1 h-1 bg-accent rounded-full"
animate={{
scale: [0, 1.5, 0],
opacity: [0, 0.6, 0],
}}
transition={{
duration: 2.5,
repeat: Infinity,
ease: "easeInOut",
delay: 1.2
}}
/>
</motion.div>
<motion.h2
className="text-3xl lg:text-5xl font-semibold text-foreground mb-6"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
viewport={{ once: true }}
>
Have an Idea? Let's Talk.
</motion.h2>
<motion.p
className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
viewport={{ once: true }}
>
Get clarity, timelines, and answers within 24 hours.
</motion.p>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
viewport={{ once: true }}
className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-8"
>
<motion.div
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Button
size="lg"
className="bg-accent hover:bg-accent/90 text-accent-foreground px-8 py-4 text-lg border-0 rounded-lg shadow-lg hover:shadow-xl transition-all duration-300"
onClick={() => navigateTo('/contact')}
>
<motion.div
animate={{
rotate: [0, 10, -10, 0],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
className="mr-2"
>
<Lightbulb className="w-5 h-5" />
</motion.div>
Request a Proposal
</Button>
</motion.div>
<div className="flex items-center gap-2 text-muted-foreground">
<Clock className="w-4 h-4" />
<span className="text-sm">24-hour response guarantee</span>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 0.6 }}
viewport={{ once: true }}
className="grid grid-cols-3 gap-8 max-w-2xl mx-auto text-center"
>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.7 }}
viewport={{ once: true }}
>
<div className="text-2xl font-bold text-foreground">15min</div>
<div className="text-sm text-muted-foreground">Quick Discovery Call</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.8 }}
viewport={{ once: true }}
>
<div className="text-2xl font-bold text-foreground">24hrs</div>
<div className="text-sm text-muted-foreground">Detailed Proposal</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.9 }}
viewport={{ once: true }}
>
<div className="text-2xl font-bold text-foreground">48hrs</div>
<div className="text-sm text-muted-foreground">Project Kickoff</div>
</motion.div>
</motion.div>
</motion.div>
</div>
</section>
);
};

843
components/Navigation.tsx Normal file
View File

@@ -0,0 +1,843 @@
import { useState, useRef, useEffect, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
ChevronDown,
Menu,
X,
Code,
Smartphone,
Globe,
Palette,
Brain,
Users,
Building2,
Monitor,
ShoppingCart,
Server,
Wrench,
Lightbulb,
Database,
Eye,
MessageSquare,
Target,
BarChart3,
Zap,
Rocket,
Shield,
Cog,
HeartHandshake,
GraduationCap,
Stethoscope,
ShoppingBag,
Truck,
Gamepad2,
Factory,
DollarSign,
Home,
BookOpen,
Users2,
Code2,
Laptop,
Paintbrush,
Bot,
RefreshCw,
Info,
Clock,
Award,
Briefcase,
Heart,
Newspaper,
FileText,
Star,
HelpCircle,
Mail,
FileCheck,
Phone,
MapPin,
Headphones,
UserPlus,
Apple,
GitMerge,
Gauge,
Chrome,
Watch,
Cloud,
CloudCog,
Link,
PenTool,
MousePointer2,
TestTube,
PlayCircle,
Search,
Workflow,
Sparkles,
Wand2,
Activity,
TrendingUp,
Settings,
ArrowRight,
Calculator,
Calendar,
FileEdit
} from "lucide-react";
import { Button } from "./ui/button";
import BlackLogo14 from "../imports/BlackLogo14";
import { navigateTo } from "../App";
const navigationData = {
main_navigation: [
"Services",
"AI & ML",
"Solutions",
"Industries",
"Hire Talent",
"Company",
"Resources"
],
services: [
{
category: "Mobile App Development",
icon: Smartphone,
href: "/services/mobile-app-development",
sub_services: [
{ name: "iOS App Development", href: "/services/ios-app-development" },
{ name: "Android App Development", href: "/services/android-app-development" },
{ name: "Cross-Platform App Development", href: "/services/cross-platform-app-development" },
{ name: "Native App Development", href: "/services/native-app-development" },
{ name: "Progressive Web Apps (PWAs)", href: "/services/pwa-development" },
{ name: "App Development for Wearables & Devices", href: "/services/wearable-device-development" }
]
},
{
category: "Web & Cloud Solutions",
icon: Globe,
href: "/web-cloud",
sub_services: [
{ name: "Custom Web Application Development", href: "/services/custom-web-app-development" },
{ name: "SaaS Product Engineering", href: "/services/saas-product-engineering" },
{ name: "eCommerce Platforms", href: "/services/ecommerce-platforms" },
{ name: "Admin Panels & Dashboards", href: "/services/admin-panels-dashboards" },
{ name: "API & Backend Development", href: "/services/api-backend-development" }
]
},
{
category: "Software Engineering",
icon: Code2,
href: "/software-engineering",
sub_services: [
{ name: "Enterprise Software Solutions", href: "/services/enterprise-software-solutions" },
{ name: "System Architecture & DevOps", href: "/services/system-architecture-devops" },
{ name: "Third-Party Integrations", href: "/services/third-party-integrations" },
{ name: "Product Modernization", href: "/services/product-modernization" }
]
},
{
category: "Design & Experience",
icon: Paintbrush,
href: "/design-experience",
sub_services: [
{ name: "UI/UX Design", href: "/services/ui-ux-design" },
{ name: "Clickable Prototypes", href: "/services/clickable-prototypes" },
{ name: "Design Thinking Workshops", href: "/services/design-thinking-workshops" },
{ name: "User Research & Testing", href: "/services/user-research-testing" }
]
}
],
ai_data_intelligence: [
{
category: "Artificial Intelligence Services",
icon: Bot,
href: "/artificial-intelligence",
sub_services: [
{ name: "AI Strategy & Consulting", href: "/services/ai-strategy-consulting" },
{ name: "AI-Powered Automation & Workflows", href: "/services/ai-automation-workflows" },
{ name: "AI Integration into Digital Products", href: "/services/ai-integration-digital-products" },
{ name: "Gen AI Integration into Digital Products", href: "/services/gen-ai-integration-digital-products" },
{ name: "AI Chatbots & Virtual Assistants", href: "/services/ai-chatbots-virtual-assistants" },
{ name: "AI Model Deployment & Maintenance", href: "/services/ai-model-deployment-mlops" }
]
},
{
category: "Machine Learning Solutions",
icon: Brain,
href: "/machine-learning",
sub_services: [
{ name: "Custom ML Model Development", href: "/services/custom-ml-model-development" },
{ name: "Predictive Analytics & Forecasting", href: "/services/predictive-analytics-forecasting" },
{ name: "Computer Vision Applications", href: "/services/computer-vision-applications" },
{ name: "NLP & Text Analytics", href: "/services/nlp-text-analytics" },
{ name: "Recommendation Engines", href: "/services/recommendation-engines" }
]
}
],
solutions: [
{ text: "Digital Product Development", icon: Rocket, href: "/solutions/digital-product-development" },
{ text: "MVP & Startup Launch Packages", icon: Zap, href: "/solutions/mvp-startup-launch-packages" },
{ text: "Legacy System Rebuilds", icon: RefreshCw, href: "/solutions/legacy-system-rebuilds" },
{ text: "Dedicated Offshore Development Centers (ODC)", icon: Building2, href: "/solutions/dedicated-offshore-odc" },
{ text: "Business Process Automation", icon: Cog, href: "/solutions/business-process-automation" },
{ text: "Compliance-Ready Systems (HIPAA, GDPR, etc.)", icon: Shield, href: "/solutions/compliance-ready-systems" }
],
industries: [
{
group: "Financial Services",
icon: DollarSign,
items: [
{ name: "FinTech & Banking Apps", href: "/industries/fintech-banking-apps" },
{ name: "WealthTech Platforms", href: "/industries/financial-services/wealthtech-platforms" },
{ name: "Real Estate Tech", href: "/industries/financial-services/real-estate-tech" }
]
},
{
group: "Healthcare & Wellness",
icon: Stethoscope,
items: [
{ name: "HealthTech Applications", href: "/industries/healthcare/healthtech-applications" },
{ name: "Medical Compliance Solutions", href: "/industries/healthcare/medical-compliance-solutions" },
{ name: "Fitness & Wellness Platforms", href: "/industries/healthcare/fitness-wellness-platforms" }
]
},
{
group: "Learning & Education",
icon: GraduationCap,
items: [
{ name: "EdTech Platforms", href: "/industries/education/edtech-platforms" },
{ name: "Virtual Classrooms & LMS", href: "/industries/education/virtual-classrooms-lms" },
{ name: "Microlearning Apps", href: "/industries/education/microlearning-apps" }
]
},
{
group: "Commerce & Consumer",
icon: ShoppingBag,
items: [
{ name: "eCommerce & Marketplaces", href: "/industries/commerce/ecommerce-marketplaces" },
{ name: "Food Ordering & Delivery", href: "/industries/commerce/food-ordering-delivery" },
{ name: "Travel & Booking Systems", href: "/industries/commerce/travel-booking-systems" },
{ name: "Event & Ticketing Solutions", href: "/industries/commerce/event-ticketing-solutions" }
]
},
{
group: "Media & Community",
icon: Gamepad2,
items: [
{ name: "OTT & Streaming Apps", href: "/industries/media/ott-streaming-apps" },
{ name: "Social Platforms & Networks", href: "/industries/media/social-platforms-networks" },
{ name: "Sports & Fan Engagement", href: "/industries/media/sports-fan-engagement" }
]
},
{
group: "Mobility & Logistics",
icon: Truck,
items: [
{ name: "Transportation Apps", href: "/industries/mobility/transportation-apps" },
{ name: "On-Demand Services", href: "/industries/mobility/on-demand-services" },
{ name: "Supply Chain & Fleet Management", href: "/industries/mobility/supply-chain-fleet-management" }
]
},
{
group: "Industrial & Emerging Tech",
icon: Factory,
items: [
{ name: "Manufacturing Automation", href: "/industries/industrial/manufacturing-automation" },
{ name: "AgriTech Platforms", href: "/industries/industrial/agritech-platforms" },
{ name: "Oil & Gas Monitoring Systems", href: "/industries/industrial/oil-gas-monitoring-systems" }
]
}
],
hire_talent: [
{ text: "Hire Mobile App Developers", icon: Smartphone, href: "/hire-talent/mobile-app-developers" },
{ text: "Hire Full Stack Developers", icon: Code, href: "/hire-talent/full-stack-developers" },
{ text: "Hire Frontend Developers", icon: Monitor, href: "/hire-talent/frontend-developers" },
{ text: "Hire Backend Developers", icon: Database, href: "/hire-talent/backend-developers" },
{ text: "Hire UI/UX Designers", icon: Palette, href: "/hire-talent/ui-ux-designers" },
{ text: "Hire QA Engineers", icon: TestTube, href: "/hire-talent/qa-engineers" },
{ text: "Dedicated Development Teams", icon: Users, href: "/dedicated-development-teams" },
{ text: "Engagement Models", icon: Settings, href: "/engagement-models" },
{ text: "Team Augmentation Services", icon: Zap, href: "/team-augmentation-services" }
],
company: [
{ text: "About WDI", icon: Info, href: "/company/about-wdi" },
{ text: "Our History", icon: Clock, href: "/company/our-history" },
{ text: "Leadership Team", icon: Users2, href: "/company/leadership-team" },
{ text: "Awards & Certifications", icon: Award, href: "/company/awards-certifications" },
{ text: "Careers", icon: Briefcase, href: "/company/careers" },
{ text: "Culture & Values", icon: Heart, href: "/company/culture-values" },
{ text: "Press & Media", icon: Newspaper, href: "/company/press-media" }
],
resources: [
{ text: "Articles", icon: BookOpen, href: "/resources/blog" },
{ text: "Case Studies", icon: FileText, href: "/case-studies" },
{ text: "Client Testimonials", icon: Star, href: "/resources/client-testimonials" },
{ text: "Whitepapers & Insights", icon: FileCheck, href: "/resources/whitepapers-insights" },
{ text: "FAQs", icon: HelpCircle, href: "/resources/faqs" }
],
contact: [
{ text: "Contact Form", icon: Mail, href: "/contact" },
{ text: "Request a Proposal", icon: FileCheck, href: "/contact/request-a-proposal" },
{ text: "Schedule a Discovery Call", icon: Phone, href: "/contact/schedule-a-discovery-call" },
{ text: "Office Locations", icon: MapPin, href: "/contact/office-locations" },
{ text: "Client Support", icon: Headphones, href: "/contact/client-support" },
{ text: "Send your CV to HR", icon: UserPlus, href: "/contact/send-your-cv" }
]
};
// CTA configurations for each mega menu type - UPDATED ALL TO LINK TO START A PROJECT PAGE
const megaMenuCTAs = {
Services: {
title: "Development Quote",
subtitle: "Get a custom quote for your project",
buttonText: "Get Started",
href: "/start-a-project",
icon: Calculator
},
"AI & ML": {
title: "AI Strategy Session",
subtitle: "Discover AI opportunities for your business",
buttonText: "Book Session",
href: "/start-a-project",
icon: Bot
},
Solutions: {
title: "Solution Consultation",
subtitle: "Find the perfect solution for your business",
buttonText: "Consult Now",
href: "/start-a-project",
icon: Lightbulb
},
Industries: {
title: "Industry Expertise",
subtitle: "Learn how we transform your industry",
buttonText: "Explore",
href: "/start-a-project",
icon: Building2
},
"Hire Talent": {
title: "Team Assessment",
subtitle: "Get matched with the right talent",
buttonText: "Start Hiring",
href: "/start-a-project",
icon: Users
},
Company: {
title: "Schedule a Call",
subtitle: "Let's discuss your project requirements",
buttonText: "Book Call",
href: "/start-a-project",
icon: Calendar
},
Resources: {
title: "Free Consultation",
subtitle: "Get expert insights for your project",
buttonText: "Get Started",
href: "/start-a-project",
icon: FileEdit
},
Contact: {
title: "Start Your Project",
subtitle: "Ready to bring your idea to life?",
buttonText: "Get Started",
href: "/start-a-project",
icon: Rocket
}
};
// Horizontal CTA Component matching reference design
const MegaMenuCTA = ({ type }: { type: string }) => {
const cta = megaMenuCTAs[type as keyof typeof megaMenuCTAs];
if (!cta) return null;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.1 }}
className="mt-8 p-6 bg-gradient-to-r from-gray-900/60 to-gray-800/60 backdrop-blur-sm border border-gray-700/30 rounded-2xl"
>
<div className="flex items-center justify-between">
<div className="flex-1">
<h3 className="text-white font-semibold text-lg mb-1">{cta.title}</h3>
<p className="text-gray-400 text-sm leading-relaxed">{cta.subtitle}</p>
</div>
<div className="ml-6">
<Button
className="bg-gradient-to-r from-[#E5195E] to-[#C41653] hover:from-[#C41653] hover:to-[#A31348] text-white font-medium text-sm px-6 py-3 h-auto rounded-xl shadow-lg hover:shadow-xl transition-all duration-200 group"
onClick={() => navigateTo(cta.href)}
>
{cta.buttonText}
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
</Button>
</div>
</div>
</motion.div>
);
};
interface MegaMenuProps {
isOpen: boolean;
onClose: () => void;
onCancelClose: () => void;
type: string;
timeoutRef?: React.MutableRefObject<NodeJS.Timeout | undefined>;
}
const MegaMenu = ({ isOpen, onClose, onCancelClose, type, timeoutRef }: MegaMenuProps) => {
if (!isOpen) return null;
const navigate = (path: string) => {
navigateTo(path);
onClose();
};
const renderServicesMenu = () => (
<div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8">
{navigationData.services.map((service, index) => {
const Icon = service.icon;
return (
<div key={service.category} className="space-y-4">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-xl bg-[#E5195E]/20 flex items-center justify-center">
<Icon className="w-5 h-5 text-[#E5195E]" />
</div>
<h4
className="font-semibold text-white text-sm cursor-pointer hover:text-[#E5195E] transition-colors"
onClick={() => service.href && navigate(service.href)}
>
{service.category}
</h4>
</div>
<ul className="space-y-3">
{service.sub_services.map((subService) => (
<li key={subService.name}>
<a
href="#"
className="text-[#CCCCCC] hover:text-white text-sm transition-colors duration-200 block py-1 hover:translate-x-1 transform"
onClick={(e) => {
e.preventDefault();
if (subService.href) {
navigate(subService.href);
}
}}
>
{subService.name}
</a>
</li>
))}
</ul>
</div>
);
})}
</div>
<MegaMenuCTA type="Services" />
</div>
);
const renderAIMenu = () => (
<div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{navigationData.ai_data_intelligence.map((category) => {
const Icon = category.icon;
return (
<div key={category.category} className="space-y-4">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-xl bg-[#E5195E]/20 flex items-center justify-center">
<Icon className="w-5 h-5 text-[#E5195E]" />
</div>
<h4
className="font-semibold text-white text-lg cursor-pointer hover:text-[#E5195E] transition-colors"
onClick={() => category.href && navigate(category.href)}
>
{category.category}
</h4>
</div>
<ul className="space-y-3">
{category.sub_services.map((service) => (
<li key={service.name}>
<a
href="#"
className="text-[#CCCCCC] hover:text-white text-sm transition-colors duration-200 block py-1 hover:translate-x-1 transform"
onClick={(e) => {
e.preventDefault();
if (service.href) {
navigate(service.href);
}
}}
>
{service.name}
</a>
</li>
))}
</ul>
</div>
);
})}
</div>
<MegaMenuCTA type="AI & ML" />
</div>
);
const renderSolutionsMenu = () => (
<div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{navigationData.solutions.map((solution) => {
const Icon = solution.icon;
return (
<a
key={solution.text}
href="#"
className="flex items-center gap-4 text-[#CCCCCC] hover:text-white transition-all duration-200 p-4 rounded-lg hover:bg-white/5 group"
onClick={(e) => {
e.preventDefault();
if (solution.href) {
navigate(solution.href);
}
}}
>
<div className="w-8 h-8 rounded-lg bg-[#E5195E]/20 flex items-center justify-center group-hover:scale-110 transition-transform">
<Icon className="w-4 h-4 text-[#E5195E]" />
</div>
<span className="font-medium">{solution.text}</span>
</a>
);
})}
</div>
<MegaMenuCTA type="Solutions" />
</div>
);
const renderIndustriesMenu = () => (
<div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8">
{navigationData.industries.map((industry) => {
const Icon = industry.icon;
return (
<div key={industry.group} className="space-y-4">
<div className="flex items-center gap-3 mb-4">
<div className="w-8 h-8 rounded-lg bg-[#E5195E]/20 flex items-center justify-center">
<Icon className="w-4 h-4 text-[#E5195E]" />
</div>
<h4 className="font-semibold text-white text-sm border-b border-white/10 pb-2">
{industry.group}
</h4>
</div>
<ul className="space-y-2">
{industry.items.map((item) => (
<li key={item.name}>
<a
href="#"
className="text-[#CCCCCC] hover:text-white text-sm transition-colors duration-200 block py-1 hover:translate-x-1 transform"
onClick={(e) => {
e.preventDefault();
if (item.href) {
navigate(item.href);
}
}}
>
{item.name}
</a>
</li>
))}
</ul>
</div>
);
})}
</div>
<MegaMenuCTA type="Industries" />
</div>
);
const renderGenericMenu = (items: any[], menuType: string) => (
<div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{items.map((item) => {
const Icon = item.icon;
return (
<a
key={item.text}
href="#"
className="flex items-center gap-4 text-[#CCCCCC] hover:text-white transition-all duration-200 p-4 rounded-lg hover:bg-white/5 group"
onClick={(e) => {
e.preventDefault();
if (item.href) {
navigate(item.href);
}
}}
>
<div className="w-8 h-8 rounded-lg bg-[#E5195E]/20 flex items-center justify-center group-hover:scale-110 transition-transform">
<Icon className="w-4 h-4 text-[#E5195E]" />
</div>
<span className="font-medium">{item.text}</span>
</a>
);
})}
</div>
<MegaMenuCTA type={menuType} />
</div>
);
const getMenuContent = () => {
switch (type) {
case 'Services':
return renderServicesMenu();
case 'AI & ML':
return renderAIMenu();
case 'Solutions':
return renderSolutionsMenu();
case 'Industries':
return renderIndustriesMenu();
case 'Hire Talent':
return renderGenericMenu(navigationData.hire_talent, 'Hire Talent');
case 'Company':
return renderGenericMenu(navigationData.company, 'Company');
case 'Resources':
return renderGenericMenu(navigationData.resources, 'Resources');
case 'Contact':
return renderGenericMenu(navigationData.contact, 'Contact');
default:
return null;
}
};
return (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
className="absolute top-full left-0 w-full bg-[#121212] backdrop-blur-xl border-t border-white/10 shadow-xl z-50 nav-mega-menu"
style={{ minHeight: '400px' }}
onMouseEnter={onCancelClose}
onMouseLeave={onClose}
>
<div className="absolute inset-0 bg-gradient-to-b from-[#121212] to-[#0E0E0E] opacity-95" />
<div
className="absolute inset-0 opacity-5"
style={{
backgroundImage: `radial-gradient(circle at 1px 1px, rgba(255,255,255,0.1) 1px, transparent 0)`,
backgroundSize: '20px 20px'
}}
/>
<div className="relative z-10 container mx-auto px-6 lg:px-8 py-12">
{getMenuContent()}
</div>
</motion.div>
);
};
export const Navigation = () => {
const [activeMenu, setActiveMenu] = useState<string | null>(null);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout>();
const navRef = useRef<HTMLElement>(null);
const navigate = (path: string) => {
navigateTo(path);
};
const openMenu = useCallback((item: string) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
if (['Services', 'AI & ML', 'Solutions', 'Industries', 'Hire Talent', 'Company', 'Resources'].includes(item)) {
setActiveMenu(item);
}
}, []);
const closeMenu = useCallback(() => {
timeoutRef.current = setTimeout(() => {
setActiveMenu(null);
}, 200);
}, []);
const cancelClose = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
}, []);
const handleNavItemMouseEnter = useCallback((item: string) => {
cancelClose();
openMenu(item);
}, [cancelClose, openMenu]);
const handleNavItemMouseLeave = useCallback(() => {
closeMenu();
}, [closeMenu]);
const handleNavMouseLeave = useCallback((e: React.MouseEvent) => {
const relatedTarget = e.relatedTarget as Element;
if (!navRef.current?.contains(relatedTarget)) {
closeMenu();
}
}, [closeMenu]);
const handleNavMouseEnter = useCallback(() => {
cancelClose();
}, [cancelClose]);
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
const hasDropdown = (item: string) => {
return ['Services', 'AI & ML', 'Solutions', 'Industries', 'Hire Talent', 'Company', 'Resources'].includes(item);
};
// Function to get main category page route for navigation items
const getMainCategoryRoute = (item: string) => {
switch (item) {
case 'Services':
return '/services';
case 'Company':
return '/company';
case 'Resources':
return '/resources';
case 'Contact':
return '/contact';
case 'Hire Talent':
return '/hire-talent';
case 'AI & ML':
return '/artificial-intelligence';
case 'Solutions':
return '/solutions/digital-product-development'; // Default to first solution
case 'Industries':
return '/industries/fintech-banking-apps'; // Default to first industry
default:
return null;
}
};
return (
<nav
ref={navRef}
className="fixed top-0 left-0 right-0 z-50 bg-[#0E0E0E]/90 backdrop-blur-lg border-b border-white/10"
onMouseLeave={handleNavMouseLeave}
onMouseEnter={handleNavMouseEnter}
>
<div className="container mx-auto px-6 lg:px-8">
<div className="flex items-center justify-between h-20">
<div className="flex items-center">
<a
href="#"
className="flex items-center"
onClick={(e) => {
e.preventDefault();
navigate('/');
}}
>
<div className="w-10 h-10">
<BlackLogo14 />
</div>
</a>
</div>
<div className="hidden lg:flex items-center space-x-6 xl:space-x-8">
{navigationData.main_navigation.map((item) => (
<div
key={item}
className="relative nav-dropdown-trigger"
onMouseEnter={() => handleNavItemMouseEnter(item)}
onMouseLeave={handleNavItemMouseLeave}
>
<a
href={`#${item.toLowerCase().replace(/\s+/g, '-')}`}
className="flex items-center gap-1 text-[#CCCCCC] hover:text-white transition-colors duration-200 py-2 font-medium text-sm xl:text-base whitespace-nowrap"
onClick={(e) => {
e.preventDefault();
const route = getMainCategoryRoute(item);
if (route) {
navigate(route);
}
}}
>
{item}
{hasDropdown(item) && (
<ChevronDown className={`w-4 h-4 transition-transform duration-200 ${activeMenu === item ? 'rotate-180' : ''}`} />
)}
</a>
</div>
))}
</div>
<div className="flex items-center space-x-4">
<Button
onClick={() => navigate('/start-a-project')}
className="hidden lg:flex"
>
Get Started
</Button>
{/* Mobile Menu Button */}
<button
className="lg:hidden text-[#CCCCCC] hover:text-white transition-colors"
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
>
{isMobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
</div>
</div>
</div>
{/* Mega Menu */}
<AnimatePresence>
{activeMenu && (
<MegaMenu
isOpen={true}
onClose={closeMenu}
onCancelClose={cancelClose}
type={activeMenu}
timeoutRef={timeoutRef}
/>
)}
</AnimatePresence>
{/* Mobile Menu */}
<AnimatePresence>
{isMobileMenuOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
className="lg:hidden bg-[#121212] border-t border-white/10"
>
<div className="container mx-auto px-6 py-6">
<div className="space-y-4">
{navigationData.main_navigation.map((item) => (
<a
key={item}
href="#"
className="block text-[#CCCCCC] hover:text-white transition-colors py-2 font-medium"
onClick={(e) => {
e.preventDefault();
const route = getMainCategoryRoute(item);
if (route) {
navigate(route);
setIsMobileMenuOpen(false);
}
}}
>
{item}
</a>
))}
<Button
onClick={() => {
navigate('/start-a-project');
setIsMobileMenuOpen(false);
}}
className="w-full mt-4"
>
Get Started
</Button>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</nav>
);
};

View File

@@ -0,0 +1,280 @@
import { motion } from "framer-motion";
import { useRef } from "react";
import { ArrowRight, FileText, Users, CheckCircle, Rocket, Search, Code, Palette, Monitor } from "lucide-react";
import { Button } from "./ui/button";
import { Badge } from "./ui/badge";
const steps = [
{
id: "step-1",
title: "1. Define Scope",
description: "We begin by gathering all project inputs, defining clear goals, creating technical documentation, and aligning on expectations.",
visual: {
type: "icon_or_doc_mockup",
style: "minimal_ui"
}
},
{
id: "step-2",
title: "2. Design UI/UX",
description: "Our designers craft user-centric interfaces in Figma with clickable flows, visual systems, and detailed wireframes for all screens.",
tags: [
{ label: "Wireframes", color: "#6366F1" },
{ label: "Prototyping", color: "#10B981" },
{ label: "UI System", color: "#F59E0B" }
]
},
{
id: "step-3",
title: "3. Develop with Agile Sprints",
description: "We use Agile sprints to turn designs into production-ready code, with continuous integration and weekly builds.",
tags: [
{ label: "Frontend", color: "#3B82F6" },
{ label: "Backend", color: "#0EA5E9" },
{ label: "APIs", color: "#8B5CF6" }
]
},
{
id: "step-4",
title: "4. Test, Launch & Scale",
description: "After QA and UAT, we help launch confidently. Then we monitor, iterate, and scale your product to grow with your users.",
chat_simulation: [
{ from: "You", text: "Are we ready to go live?" },
{ from: "Team", text: "Yes! Final tests passed 🚀" }
]
}
];
// Chat simulation component - Compact version
const ChatSimulation = ({ messages }: { messages: Array<{ from: string; text: string }> }) => {
return (
<div className="space-y-2 p-3 bg-background rounded-lg border border-border">
{messages.map((message, index) => (
<motion.div
key={index}
initial={{ opacity: 0, x: message.from === "You" ? -20 : 20 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: index * 0.3 }}
viewport={{ once: true }}
className={`flex ${message.from === "You" ? "justify-start" : "justify-end"}`}
>
<div
className={`max-w-[80%] px-3 py-1.5 rounded-lg ${
message.from === "You"
? "bg-muted border border-border text-foreground"
: "bg-accent text-accent-foreground"
}`}
>
<div className="text-xs font-medium mb-1 opacity-70">{message.from}</div>
<div className="text-xs">{message.text}</div>
</div>
</motion.div>
))}
</div>
);
};
// Document mockup component - Compact version
const DocumentMockup = () => {
return (
<div className="relative">
<div className="w-full bg-background rounded-lg border border-border p-4">
<div className="space-y-3">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FileText className="w-4 h-4 text-accent" />
<span className="text-xs font-medium text-foreground">Project Scope</span>
</div>
<div className="text-xs text-muted-foreground">Draft v1.2</div>
</div>
{/* Document sections */}
<div className="space-y-2">
<motion.div
initial={{ opacity: 0, width: 0 }}
whileInView={{ opacity: 1, width: "100%" }}
transition={{ duration: 0.8, delay: 0.2 }}
viewport={{ once: true }}
className="space-y-1.5"
>
<div className="h-1.5 bg-muted rounded w-3/4"></div>
<div className="h-1.5 bg-muted/60 rounded w-full"></div>
<div className="h-1.5 bg-muted/60 rounded w-5/6"></div>
</motion.div>
<div className="flex gap-2">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 0.4 }}
viewport={{ once: true }}
className="flex items-center gap-1 px-2 py-1 bg-muted rounded text-xs"
>
<CheckCircle className="w-3 h-3 text-green-500" />
<span className="text-muted-foreground">Requirements</span>
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 0.6 }}
viewport={{ once: true }}
className="flex items-center gap-1 px-2 py-1 bg-muted rounded text-xs"
>
<Search className="w-3 h-3 text-blue-400" />
<span className="text-muted-foreground">Research</span>
</motion.div>
</div>
</div>
</div>
</div>
</div>
);
};
// Process step card component
const ProcessCard = ({ step, index }: { step: typeof steps[0]; index: number }) => {
const cardRef = useRef(null);
const renderContent = () => {
if (step.visual?.type === "icon_or_doc_mockup") {
return <DocumentMockup />;
}
if (step.chat_simulation) {
return <ChatSimulation messages={step.chat_simulation} />;
}
if (step.tags) {
return (
<div className="flex flex-wrap gap-2">
{step.tags.map((tag, tagIndex) => (
<motion.div
key={tagIndex}
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4, delay: tagIndex * 0.1 }}
viewport={{ once: true }}
>
<Badge
className="text-white border-0 px-3 py-1 text-xs font-medium rounded-lg"
style={{ backgroundColor: tag.color }}
>
{tag.label}
</Badge>
</motion.div>
))}
</div>
);
}
return null;
};
return (
<motion.div
ref={cardRef}
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: index * 0.15 }}
viewport={{ once: true, margin: "-50px" }}
className="bg-card rounded-lg border border-border hover:border-border/80 transition-all duration-300 overflow-hidden group hover:shadow-lg"
>
<div className="p-6 space-y-4">
{/* Header */}
<div className="space-y-3">
<motion.h3
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.15 + 0.2 }}
viewport={{ once: true }}
className="text-foreground leading-tight text-lg"
>
{step.title}
</motion.h3>
<motion.p
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.15 + 0.3 }}
viewport={{ once: true }}
className="text-muted-foreground leading-relaxed text-base"
>
{step.description}
</motion.p>
</div>
{/* Content */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.15 + 0.4 }}
viewport={{ once: true }}
>
{renderContent()}
</motion.div>
</div>
</motion.div>
);
};
export const ProcessSection = () => {
const titleRef = useRef(null);
return (
<section className="relative overflow-hidden py-20 bg-background">
<div className="container mx-auto px-6 lg:px-8">
{/* Title Section */}
<div ref={titleRef} className="text-center mb-16">
<motion.h2
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-4xl lg:text-5xl font-semibold text-foreground mb-4"
>
How We Turn an Idea into a{" "}
<span className="text-accent">Scalable Product</span>
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
viewport={{ once: true }}
className="text-muted-foreground text-xl max-w-2xl mx-auto"
>
Our proven process transforms your vision into reality through strategic planning,
thoughtful design, and expert engineeringevery step of the way.
</motion.p>
</div>
{/* Cards Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-6xl mx-auto">
{steps.map((step, index) => (
<ProcessCard key={step.id} step={step} index={index} />
))}
</div>
{/* Bottom CTA */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.8 }}
viewport={{ once: true }}
className="text-center mt-16"
>
<Button
size="lg"
className="bg-accent hover:bg-accent/90 text-accent-foreground border-0 rounded-lg px-8 py-4 group text-lg"
>
<span className="flex items-center gap-2">
Start Your Project Today
<ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
</span>
</Button>
</motion.div>
</div>
</section>
);
};

View File

@@ -0,0 +1,169 @@
import { motion } from "framer-motion";
import { ArrowRight, Calendar, Clock } from "lucide-react";
import { Button } from "./ui/button";
import { ImageWithFallback } from "./figma/ImageWithFallback";
import { navigateTo } from "../App";
const resources = [
{
title: "UX review presentations",
excerpt: "How do you create compelling presentations that wow clients, and actually close projects and deals? Here are the key insights that will elevate your game.",
readTime: "8 min read",
date: "Dec 15, 2024",
image: "https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop&auto=format",
author: {
name: "Olivia Rhye",
avatar: "https://images.unsplash.com/photo-1494790108755-2616b612b47c?w=150&h=150&fit=crop&crop=face&auto=format"
},
category: "Design",
slug: "ux-review-presentations"
},
{
title: "Migrating to Linear 101",
excerpt: "Linear helps streamline software projects, sprints, tasks, and bug tracking. Here's how to get started and make the most of it.",
readTime: "6 min read",
date: "Dec 10, 2024",
image: "https://images.unsplash.com/photo-1551434678-e076c223a692?w=800&h=400&fit=crop&auto=format",
author: {
name: "Phoenix Baker",
avatar: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face&auto=format"
},
category: "Software Engineering",
slug: "migrating-to-linear-101"
},
{
title: "Building your API Stack",
excerpt: "The rise of RESTful APIs has been met by a rise in tools for creating, testing, and managing them. Here are the best practices for API development.",
readTime: "12 min read",
date: "Dec 5, 2024",
image: "https://images.unsplash.com/photo-1555949963-ff9fe0c870eb?w=800&h=400&fit=crop&auto=format",
author: {
name: "Lana Steiner",
avatar: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face&auto=format"
},
category: "Software Engineering",
slug: "building-your-api-stack"
}
];
const ResourceCard = ({ resource, index }: { resource: typeof resources[0]; index: number }) => {
return (
<motion.article
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.2 }}
viewport={{ once: true }}
className="group bg-card rounded-lg border border-border overflow-hidden hover:border-border/80 transition-all duration-300 hover:shadow-lg cursor-pointer"
onClick={() => navigateTo(`/insights/${resource.slug}`)}
>
{/* Image */}
<div className="aspect-[16/9] overflow-hidden relative">
<ImageWithFallback
src={resource.image}
alt={resource.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
{/* Capsule Tag */}
<div className="absolute top-4 left-4">
<span className="px-3 py-1.5 bg-background/90 backdrop-blur-sm text-foreground text-xs font-medium rounded-full border border-border/50">
{resource.category}
</span>
</div>
</div>
{/* Content */}
<div className="p-6 space-y-4">
{/* Date and Read Time */}
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
{resource.date}
</div>
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{resource.readTime}
</div>
</div>
{/* Title */}
<h3 className="text-foreground font-medium leading-tight group-hover:text-accent transition-colors">
{resource.title}
</h3>
{/* Excerpt */}
<p className="text-muted-foreground text-sm leading-relaxed">
{resource.excerpt}
</p>
{/* Author */}
<div className="flex items-center gap-3 pt-2 border-t border-border">
<ImageWithFallback
src={resource.author.avatar}
alt={resource.author.name}
className="w-10 h-10 rounded-full object-cover"
/>
<div className="flex-1">
<div className="font-medium text-foreground text-sm">
{resource.author.name}
</div>
</div>
<Button
variant="ghost"
size="sm"
className="text-accent hover:text-accent-foreground hover:bg-accent/10 p-2 h-auto group-hover:translate-x-1 transition-transform"
onClick={(e) => {
e.stopPropagation();
navigateTo(`/insights/${resource.slug}`);
}}
>
<ArrowRight className="w-4 h-4" />
</Button>
</div>
</div>
</motion.article>
);
};
export const ResourceCards = () => {
return (
<section className="relative py-20 overflow-hidden">
<div className="container mx-auto px-6 lg:px-8">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl lg:text-4xl font-semibold text-foreground mb-4">
Insights for Founders & Product Leaders
</h2>
<p className="text-muted-foreground text-lg max-w-2xl mx-auto">
Learn from our experience building 200+ digital products. Practical insights, real case studies, and actionable strategies.
</p>
</motion.div>
{/* Resource Cards Grid */}
<div className="grid lg:grid-cols-3 gap-8 mb-12 max-w-7xl mx-auto">
{resources.map((resource, index) => (
<ResourceCard key={resource.title} resource={resource} index={index} />
))}
</div>
{/* CTA */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.8 }}
viewport={{ once: true }}
className="text-center"
>
<Button className="bg-accent hover:bg-accent/90 text-accent-foreground border-0 rounded-lg px-6 py-3" onClick={() => navigateTo('/resources')}>
View All Resources <ArrowRight className="w-4 h-4 ml-2" />
</Button>
</motion.div>
</div>
</section>
);
};

View File

@@ -0,0 +1,420 @@
import { motion, useScroll, useTransform, useSpring } from "framer-motion";
import { useRef, useEffect, useState } from "react";
import { FileText, Figma, Code, Rocket, ArrowRight, Smartphone, Monitor, Palette, Database, Cloud, CheckCircle } from "lucide-react";
import { Button } from "./ui/button";
import { GridPattern } from "./GridPattern";
const steps = [
{
id: "discovery",
title: "Define the Scope",
subtext: "We begin by understanding your vision, identifying key problems, and drafting a well-defined scope with clear goals and deliverables.",
icon: FileText,
visual: "workspace_parallax",
color: "from-blue-500/20 to-cyan-500/20"
},
{
id: "design",
title: "Designing the Experience",
subtext: "Our designers craft intuitive and stunning user interfaces in Figma, turning requirements into clickable, user-first prototypes.",
icon: Figma,
visual: "figma_canvas_animation",
color: "from-purple-500/20 to-pink-500/20"
},
{
id: "development",
title: "Engineering the Solution",
subtext: "We bring designs to life with clean, scalable code—choosing the right tech stack to ensure performance and longevity.",
icon: Code,
visual: "code_editor_animation",
color: "from-green-500/20 to-emerald-500/20"
},
{
id: "launch",
title: "Launch & Beyond",
subtext: "We ship your product with confidence—ensuring QA, deployment, and post-launch support to keep it growing.",
icon: Rocket,
visual: "launch_animation",
color: "from-orange-500/20 to-red-500/20"
}
];
const WorkspaceVisual = ({ inView }: { inView: boolean }) => {
return (
<div className="relative w-full h-64 overflow-hidden rounded-lg bg-gradient-to-br from-blue-500/10 to-cyan-500/10 p-6">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={inView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
transition={{ duration: 0.6 }}
className="space-y-4"
>
{/* Floating Documents */}
<motion.div
animate={inView ? { x: [-10, 10, -10], y: [-5, 5, -5] } : {}}
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }}
className="absolute top-4 left-4 w-12 h-16 bg-white/10 rounded shadow-lg flex items-center justify-center"
>
<FileText className="w-6 h-6 text-blue-400" />
</motion.div>
{/* Sticky Notes */}
<motion.div
animate={inView ? { rotate: [-2, 2, -2] } : {}}
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
className="absolute top-16 right-8 w-16 h-16 bg-yellow-400/20 rounded-sm p-2"
>
<div className="w-full h-1 bg-yellow-400/40 rounded mb-1"></div>
<div className="w-3/4 h-1 bg-yellow-400/30 rounded mb-1"></div>
<div className="w-1/2 h-1 bg-yellow-400/20 rounded"></div>
</motion.div>
{/* Pointer Highlighting */}
<motion.div
animate={inView ? { x: [0, 100, 0] } : {}}
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut", delay: 1 }}
className="absolute bottom-8 left-8 w-2 h-2 bg-accent rounded-full"
>
<div className="absolute -top-1 -left-1 w-4 h-4 border-2 border-accent rounded-full animate-pulse"></div>
</motion.div>
</motion.div>
</div>
);
};
const FigmaCanvasVisual = ({ inView }: { inView: boolean }) => {
return (
<div className="relative w-full h-64 overflow-hidden rounded-lg bg-gradient-to-br from-purple-500/10 to-pink-500/10 p-6">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={inView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.9 }}
transition={{ duration: 0.8 }}
className="space-y-4"
>
{/* UI Frames */}
<motion.div
animate={inView ? { scale: [1, 1.05, 1] } : {}}
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
className="absolute top-4 left-4 w-20 h-32 bg-white/10 rounded-lg border border-purple-400/30 p-2"
>
<div className="w-full h-4 bg-purple-400/20 rounded mb-2"></div>
<div className="space-y-1">
<div className="w-full h-2 bg-purple-400/15 rounded"></div>
<div className="w-3/4 h-2 bg-purple-400/10 rounded"></div>
</div>
<Smartphone className="absolute bottom-2 right-2 w-4 h-4 text-purple-400/50" />
</motion.div>
<motion.div
animate={inView ? { scale: [1, 1.03, 1] } : {}}
transition={{ duration: 2.5, repeat: Infinity, ease: "easeInOut", delay: 0.5 }}
className="absolute top-4 right-4 w-24 h-16 bg-white/10 rounded-lg border border-pink-400/30 p-2"
>
<div className="w-full h-3 bg-pink-400/20 rounded mb-1"></div>
<div className="w-2/3 h-2 bg-pink-400/15 rounded"></div>
<Monitor className="absolute bottom-1 right-1 w-3 h-3 text-pink-400/50" />
</motion.div>
{/* Design Tools */}
<motion.div
animate={inView ? { rotate: [0, 360] } : {}}
transition={{ duration: 8, repeat: Infinity, ease: "linear" }}
className="absolute bottom-8 left-8"
>
<Palette className="w-8 h-8 text-purple-400" />
</motion.div>
</motion.div>
</div>
);
};
const CodeEditorVisual = ({ inView }: { inView: boolean }) => {
const [typedText, setTypedText] = useState("");
const codeSnippet = "const app = () => {\n return <div>Hello World</div>\n}";
useEffect(() => {
if (inView) {
let i = 0;
const interval = setInterval(() => {
if (i < codeSnippet.length) {
setTypedText(codeSnippet.slice(0, i + 1));
i++;
} else {
clearInterval(interval);
}
}, 100);
return () => clearInterval(interval);
} else {
setTypedText("");
}
}, [inView]);
return (
<div className="relative w-full h-64 overflow-hidden rounded-lg bg-gradient-to-br from-green-500/10 to-emerald-500/10 p-6">
<motion.div
initial={{ opacity: 0 }}
animate={inView ? { opacity: 1 } : { opacity: 0 }}
transition={{ duration: 0.5 }}
className="bg-gray-900/50 rounded-lg p-4 h-full"
>
{/* Code Editor Interface */}
<div className="flex items-center gap-2 mb-4">
<div className="w-3 h-3 bg-red-400 rounded-full"></div>
<div className="w-3 h-3 bg-yellow-400 rounded-full"></div>
<div className="w-3 h-3 bg-green-400 rounded-full"></div>
</div>
{/* Typed Code */}
<pre className="text-green-400 text-sm font-mono">
{typedText}
<motion.span
animate={{ opacity: [1, 0, 1] }}
transition={{ duration: 1, repeat: Infinity }}
className="bg-green-400 w-2 h-4 inline-block ml-1"
/>
</pre>
{/* Tech Stack Icons */}
<div className="absolute bottom-4 right-4 flex gap-2">
<motion.div
animate={inView ? { y: [-2, 2, -2] } : {}}
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
>
<Database className="w-6 h-6 text-green-400" />
</motion.div>
<motion.div
animate={inView ? { y: [2, -2, 2] } : {}}
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut", delay: 0.5 }}
>
<Cloud className="w-6 h-6 text-emerald-400" />
</motion.div>
</div>
</motion.div>
</div>
);
};
const LaunchVisual = ({ inView }: { inView: boolean }) => {
return (
<div className="relative w-full h-64 overflow-hidden rounded-lg bg-gradient-to-br from-orange-500/10 to-red-500/10 p-6">
<motion.div
initial={{ opacity: 0 }}
animate={inView ? { opacity: 1 } : { opacity: 0 }}
transition={{ duration: 0.6 }}
className="relative h-full"
>
{/* Rocket Animation */}
<motion.div
animate={inView ? { y: [-20, -40, -20], x: [0, 10, 0] } : {}}
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
className="absolute bottom-8 left-1/2 transform -translate-x-1/2"
>
<Rocket className="w-12 h-12 text-orange-400" />
<motion.div
animate={inView ? { scale: [0.8, 1.2, 0.8], opacity: [0.3, 0.7, 0.3] } : {}}
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 w-8 h-8 bg-orange-400/30 rounded-full blur-sm"
/>
</motion.div>
{/* Analytics Dashboard */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={inView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
transition={{ duration: 0.8, delay: 0.5 }}
className="absolute top-4 right-4 w-24 h-16 bg-white/10 rounded p-2"
>
<div className="flex items-end justify-between h-full">
<motion.div
animate={inView ? { height: ["40%", "60%", "40%"] } : {}}
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
className="w-2 bg-orange-400/60 rounded-t"
/>
<motion.div
animate={inView ? { height: ["60%", "80%", "60%"] } : {}}
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut", delay: 0.3 }}
className="w-2 bg-red-400/60 rounded-t"
/>
<motion.div
animate={inView ? { height: ["30%", "70%", "30%"] } : {}}
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut", delay: 0.6 }}
className="w-2 bg-yellow-400/60 rounded-t"
/>
</div>
</motion.div>
{/* Success Indicators */}
<motion.div
animate={inView ? { scale: [0, 1, 0] } : {}}
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut", delay: 1 }}
className="absolute top-8 left-8"
>
<CheckCircle className="w-8 h-8 text-green-400" />
</motion.div>
</motion.div>
</div>
);
};
const ProcessStep = ({ step, index, inView }: { step: typeof steps[0]; index: number; inView: boolean }) => {
const Icon = step.icon;
const renderVisual = () => {
switch (step.visual) {
case "workspace_parallax":
return <WorkspaceVisual inView={inView} />;
case "figma_canvas_animation":
return <FigmaCanvasVisual inView={inView} />;
case "code_editor_animation":
return <CodeEditorVisual inView={inView} />;
case "launch_animation":
return <LaunchVisual inView={inView} />;
default:
return null;
}
};
const isEven = index % 2 === 0;
return (
<motion.div
initial={{ opacity: 0, y: 100 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: index * 0.2 }}
viewport={{ once: true, margin: "-100px" }}
className={`grid lg:grid-cols-2 gap-12 items-center ${!isEven ? "lg:flex-row-reverse" : ""}`}
>
{/* Content */}
<div className={`space-y-6 ${!isEven ? "lg:order-2" : ""}`}>
<div className="flex items-center gap-4">
<motion.div
whileHover={{ scale: 1.1, rotate: 5 }}
className="w-16 h-16 bg-accent/10 rounded-lg flex items-center justify-center"
>
<Icon className="w-8 h-8 text-accent" />
</motion.div>
<div className="text-sm text-accent font-medium">
Step {index + 1}
</div>
</div>
<motion.h3
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="text-3xl lg:text-4xl font-semibold text-foreground"
>
{step.title}
</motion.h3>
<motion.p
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
className="text-muted-foreground text-lg leading-relaxed"
>
{step.subtext}
</motion.p>
</div>
{/* Visual */}
<motion.div
initial={{ opacity: 0, x: isEven ? 50 : -50 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.3 }}
className={`${!isEven ? "lg:order-1" : ""}`}
>
{renderVisual()}
</motion.div>
</motion.div>
);
};
export const ScrollParallaxProcess = () => {
const containerRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start end", "end start"]
});
const springScrollProgress = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001
});
const y1 = useTransform(springScrollProgress, [0, 1], [0, -100]);
const y2 = useTransform(springScrollProgress, [0, 1], [0, -200]);
const y3 = useTransform(springScrollProgress, [0, 1], [0, -50]);
return (
<section ref={containerRef} className="relative py-20 overflow-hidden">
<GridPattern strokeDasharray="4 2" />
{/* Parallax Background Elements */}
<motion.div
style={{ y: y1 }}
className="absolute top-20 left-10 w-32 h-32 bg-accent/5 rounded-full blur-3xl"
/>
<motion.div
style={{ y: y2 }}
className="absolute bottom-20 right-10 w-48 h-48 bg-blue-500/5 rounded-full blur-3xl"
/>
<motion.div
style={{ y: y3 }}
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-64 h-64 bg-purple-500/5 rounded-full blur-3xl"
/>
<div className="relative z-10 container mx-auto px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-20"
>
<h2 className="text-4xl lg:text-5xl font-semibold text-foreground mb-6">
How We Turn an Idea into a{" "}
<span className="text-accent">Scalable Product</span>
</h2>
<p className="text-muted-foreground text-xl max-w-3xl mx-auto leading-relaxed">
Our proven process transforms your vision into reality through strategic planning,
thoughtful design, and expert engineeringevery step of the way.
</p>
</motion.div>
<div className="space-y-32">
{steps.map((step, index) => (
<ProcessStep
key={step.id}
step={step}
index={index}
inView={true}
/>
))}
</div>
{/* Final CTA */}
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5 }}
viewport={{ once: true }}
className="text-center mt-20"
>
<Button
size="lg"
className="bg-accent hover:bg-accent/90 text-accent-foreground px-8 py-4 rounded-lg group"
>
Let's Build Yours
<motion.div
animate={{ x: [0, 5, 0] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
>
<ArrowRight className="w-5 h-5 ml-2" />
</motion.div>
</Button>
</motion.div>
</div>
</section>
);
};

135
components/ServicesGrid.tsx Normal file
View File

@@ -0,0 +1,135 @@
import { motion } from "framer-motion";
import { Smartphone, Globe, Palette, Brain, RefreshCw, Users } from "lucide-react";
import { GridPattern } from "./GridPattern";
import { navigateTo } from "../App";
const services = [
{
title: "Mobile App Development",
icon: Smartphone,
description: "Native and cross-platform mobile solutions",
href: "/services/mobile-app-development"
},
{
title: "Web & SaaS Engineering",
icon: Globe,
description: "Scalable web applications and SaaS platforms"
},
{
title: "UI/UX & Prototyping",
icon: Palette,
description: "User-centered design and interactive prototypes"
},
{
title: "AI & Data Intelligence",
icon: Brain,
description: "Machine learning and data-driven solutions"
},
{
title: "Product Modernization",
icon: RefreshCw,
description: "Legacy system upgrades and optimization"
},
{
title: "Dedicated Development Teams",
icon: Users,
description: "Extended teams and staff augmentation"
},
];
const ServiceCard = ({ service, index }: { service: typeof services[0]; index: number }) => {
const Icon = service.icon;
const handleClick = () => {
if (service.href) {
navigateTo(service.href);
}
};
return (
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
whileHover={{
scale: 1.05,
y: -8
}}
className={`group relative p-8 rounded-2xl bg-white/5 backdrop-blur-sm border border-white/10 hover:border-[#E5195E] transition-all duration-500 overflow-hidden ${
service.href ? 'cursor-pointer' : 'cursor-default'
}`}
onClick={handleClick}
>
<div className="relative z-10">
<div className="mb-6">
{/* Icon container with glassmorphism effect */}
<div className="relative w-16 h-16 rounded-xl bg-gradient-to-br from-[#E5195E]/20 to-white/10 flex items-center justify-center mb-6 group-hover:scale-110 transition-all duration-500 backdrop-blur-sm border border-white/10">
{/* Icon glow effect */}
<div className="absolute inset-0 rounded-xl bg-[#E5195E]/20 blur-xl opacity-0 group-hover:opacity-60 transition-opacity duration-500" />
<Icon className="relative w-8 h-8 text-[#E5195E] transition-colors duration-300" />
</div>
<h3 className="text-xl font-semibold text-white mb-3 group-hover:text-white transition-colors duration-300">
{service.title}
</h3>
<p className="text-[#CCCCCC] text-sm leading-relaxed group-hover:text-white/90 transition-colors duration-300">
{service.description}
</p>
</div>
{/* Arrow indicator - only show for clickable services */}
{service.href && (
<div className="flex items-center justify-end opacity-0 group-hover:opacity-100 transition-all duration-300 transform translate-x-4 group-hover:translate-x-0">
<div className="w-8 h-8 rounded-full bg-[#E5195E]/20 backdrop-blur-sm flex items-center justify-center border border-[#E5195E]/30">
<svg
className="w-4 h-4 text-[#E5195E]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</div>
</div>
)}
</div>
{/* Hover overlay for clickable services */}
{service.href && (
<div className="absolute inset-0 bg-gradient-to-r from-[#E5195E]/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
)}
</motion.div>
);
};
export const ServicesGrid = () => {
return (
<section className="relative py-20 bg-[#0E0E0E] overflow-hidden">
<GridPattern strokeDasharray="4 2" />
<div className="relative z-10 container mx-auto px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl lg:text-4xl font-semibold text-white mb-4">
What We Do
</h2>
<p className="text-[#CCCCCC] text-lg max-w-2xl mx-auto">
We deliver comprehensive digital solutions that transform ideas into powerful, scalable applications
</p>
</motion.div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{services.map((service, index) => (
<ServiceCard key={service.title} service={service} index={index} />
))}
</div>
</div>
</section>
);
};

View File

@@ -0,0 +1,125 @@
export function ServicesSection() {
const services = [
{
title: "Mobile App Development",
description: "Native & cross-platform apps with pixel-perfect UIs and seamless user experiences.",
icon: (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"
/>
),
gradient: "from-[#E5195E]/20 to-purple-500/20"
},
{
title: "Web Platforms",
description: "Scalable, secure, and SEO-optimized web applications built for performance.",
icon: (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
/>
),
gradient: "from-[#E5195E]/20 to-blue-500/20"
},
{
title: "AI & ML Solutions",
description: "Intelligent features powered by cutting-edge algorithms and machine learning.",
icon: (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
/>
),
gradient: "from-[#E5195E]/20 to-cyan-500/20"
},
{
title: "DevOps & Cloud",
description: "CI/CD pipelines and managed cloud infrastructure for seamless deployment.",
icon: (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"
/>
),
gradient: "from-[#E5195E]/20 to-orange-500/20"
},
{
title: "Product Design",
description: "Human-centered UX with delightful micro-interactions and intuitive interfaces.",
icon: (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zM21 5a2 2 0 00-2-2h-4a2 2 0 00-2 2v12a4 4 0 004 4h4a2 2 0 002-2V5z"
/>
),
gradient: "from-[#E5195E]/20 to-pink-500/20"
},
{
title: "Security & Compliance",
description: "Pen-testing, auditing, and regulatory alignment for enterprise-grade security.",
icon: (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
),
gradient: "from-[#E5195E]/20 to-green-500/20"
}
];
return (
<section id="services" className="border-t border-gray-800">
<div className="container mx-auto px-6 lg:px-8 py-24">
<div className="text-center">
<h2 className="text-3xl sm:text-4xl font-semibold tracking-tight text-white">What We Do</h2>
<p className="mt-4 text-gray-400 max-w-2xl mx-auto">
End-to-end solutions for every stage of your product lifecycle.
</p>
</div>
{/* Services Grid - Wider container and larger cards */}
<div className="mt-16 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-8xl mx-auto">
{services.map((service, index) => (
<div key={index} className="group relative p-10 border border-gray-800/50 hover:border-gray-700/70 transition-all duration-300 rounded-[10px] backdrop-blur-sm hover:backdrop-blur-md">
<div className="absolute inset-0 bg-gradient-to-br from-gray-900/20 via-gray-800/10 to-gray-900/20 rounded-[10px] opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
{/* Glassmorphism Icon */}
<div className={`relative mb-8 w-20 h-20 rounded-2xl bg-gradient-to-br from-white/10 to-white/5 backdrop-blur-xl border border-white/20 flex items-center justify-center group-hover:scale-110 transition-transform duration-300`}>
<div className={`absolute inset-0 bg-gradient-to-br ${service.gradient} rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300`}></div>
<svg
className="w-10 h-10 text-white/90 relative z-10"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
{service.icon}
</svg>
</div>
<h3 className="text-xl font-semibold text-white tracking-tight mb-4">
<span className="text-[#E5195E]">{service.title.split(' ')[0]}</span>
{service.title.split(' ').slice(1).join(' ') && ` ${service.title.split(' ').slice(1).join(' ')}`}
</h3>
<p className="text-gray-400 leading-relaxed">
{service.description}
</p>
</div>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,99 @@
export function SplineFallback() {
return (
<div className="w-full h-full bg-gradient-to-br from-gray-900/90 to-gray-800/90 backdrop-blur-sm rounded-xl flex items-center justify-center overflow-hidden relative">
{/* Animated background gradient */}
<div className="absolute inset-0 bg-gradient-to-br from-[#E5195E]/10 to-purple-500/10 animate-pulse"></div>
{/* Grid pattern overlay */}
<div className="absolute inset-0 opacity-10">
<svg className="w-full h-full" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="white" strokeWidth="1"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
</svg>
</div>
{/* Main 3D scene */}
<div className="relative z-10 w-full h-full flex items-center justify-center">
{/* Central 3D composition */}
<div className="relative">
{/* Main floating cube with nested elements */}
<div className="w-40 h-40 relative transform-gpu">
{/* Outer cube */}
<div className="absolute inset-0 bg-gradient-to-br from-[#E5195E]/30 to-purple-500/30 rounded-3xl transform rotate-12 animate-[float_4s_ease-in-out_infinite] backdrop-blur-sm border border-white/20 shadow-2xl">
{/* Middle cube */}
<div className="absolute inset-6 bg-gradient-to-br from-blue-500/40 to-cyan-500/40 rounded-2xl transform -rotate-12 animate-[float_3s_ease-in-out_infinite_0.5s] backdrop-blur-sm border border-white/10">
{/* Inner cube */}
<div className="absolute inset-6 bg-gradient-to-br from-green-500/50 to-emerald-500/50 rounded-xl transform rotate-6 animate-[float_2s_ease-in-out_infinite_1s] backdrop-blur-sm border border-white/5">
{/* Core element */}
<div className="absolute inset-4 bg-gradient-to-br from-orange-500/60 to-yellow-500/60 rounded-lg animate-pulse"></div>
</div>
</div>
</div>
</div>
{/* Orbiting satellites */}
<div className="absolute top-0 left-0 w-full h-full">
{/* Satellite 1 */}
<div className="absolute -top-8 -right-8 w-8 h-8 bg-gradient-to-r from-[#E5195E] to-pink-500 rounded-xl transform rotate-45 animate-[orbit_8s_linear_infinite] shadow-lg">
<div className="absolute inset-1 bg-white/20 rounded-lg animate-pulse"></div>
</div>
{/* Satellite 2 */}
<div className="absolute -bottom-6 -left-8 w-6 h-6 bg-gradient-to-r from-blue-500 to-cyan-400 rounded-lg transform -rotate-12 animate-[orbit_6s_linear_infinite_reverse] shadow-lg">
<div className="absolute inset-1 bg-white/30 rounded-sm animate-pulse delay-500"></div>
</div>
{/* Satellite 3 */}
<div className="absolute top-1/2 -left-16 w-5 h-5 bg-gradient-to-r from-green-400 to-emerald-400 rounded-full animate-[orbit_10s_linear_infinite] shadow-lg">
<div className="absolute inset-1 bg-white/40 rounded-full animate-pulse delay-1000"></div>
</div>
{/* Satellite 4 */}
<div className="absolute top-1/4 -right-14 w-7 h-7 bg-gradient-to-r from-purple-500 to-violet-400 rounded-2xl transform rotate-30 animate-[orbit_7s_linear_infinite_reverse] shadow-lg">
<div className="absolute inset-1.5 bg-white/25 rounded-xl animate-pulse delay-1500"></div>
</div>
</div>
</div>
{/* Floating particles */}
<div className="absolute inset-0 pointer-events-none">
{/* Particle 1 */}
<div className="absolute top-1/6 left-1/5 w-2 h-2 bg-white/60 rounded-full animate-[float_3s_ease-in-out_infinite] shadow-sm"></div>
{/* Particle 2 */}
<div className="absolute top-2/3 left-4/5 w-1.5 h-1.5 bg-[#E5195E]/80 rounded-full animate-[float_4s_ease-in-out_infinite_0.8s] shadow-sm"></div>
{/* Particle 3 */}
<div className="absolute top-1/2 left-1/8 w-1 h-1 bg-blue-400/70 rounded-full animate-[float_2.5s_ease-in-out_infinite_1.2s] shadow-sm"></div>
{/* Particle 4 */}
<div className="absolute top-1/4 right-1/6 w-1.5 h-1.5 bg-green-400/60 rounded-full animate-[float_3.5s_ease-in-out_infinite_1.8s] shadow-sm"></div>
{/* Particle 5 */}
<div className="absolute bottom-1/4 left-1/3 w-1 h-1 bg-purple-400/50 rounded-full animate-[float_4.5s_ease-in-out_infinite_2.2s] shadow-sm"></div>
</div>
{/* Light rays */}
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-1/2 left-1/2 w-1 h-32 bg-gradient-to-t from-transparent via-white/10 to-transparent transform -translate-x-1/2 -translate-y-1/2 rotate-45 animate-[rotate_20s_linear_infinite]"></div>
<div className="absolute top-1/2 left-1/2 w-1 h-24 bg-gradient-to-t from-transparent via-[#E5195E]/10 to-transparent transform -translate-x-1/2 -translate-y-1/2 rotate-135 animate-[rotate_15s_linear_infinite_reverse]"></div>
</div>
</div>
{/* Status indicator */}
<div className="absolute bottom-6 left-6 flex items-center gap-2">
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
<span className="text-xs text-white/70 font-medium">Interactive 3D Experience</span>
</div>
{/* Tech badge */}
<div className="absolute top-6 right-6 px-3 py-1 bg-black/40 backdrop-blur-sm rounded-full border border-white/10">
<span className="text-xs text-white/80 font-medium">AI Powered</span>
</div>
</div>
);
}

View File

@@ -0,0 +1,7 @@
// This file is no longer needed as we're using the official Spline component
// from '@splinetool/react-spline/next' directly in the HeroSection component.
// Keeping this file empty to avoid any import errors until references are cleaned up.
export function SplineViewer() {
return null;
}

View File

@@ -0,0 +1,118 @@
import { motion } from "framer-motion";
import { Phone, Clock, Zap, Calendar, MessageSquare } from "lucide-react";
import { Button } from "./ui/button";
import { GridPattern } from "./GridPattern";
import { navigateTo } from "../App";
export const SplitCallToAction = () => {
return (
<section className="relative py-20 overflow-hidden">
<GridPattern strokeDasharray="4 2" />
<div className="relative z-10 container mx-auto px-6 lg:px-8">
<div className="grid lg:grid-cols-2 gap-16 items-center max-w-6xl mx-auto">
{/* Left Content */}
<motion.div
initial={{ opacity: 0, x: -50 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="space-y-8"
>
<div>
<h2 className="text-2xl md:text-3xl lg:text-5xl font-semibold text-foreground mb-6 whitespace-nowrap">
Ready to Build with WDI?
</h2>
<p className="text-xl text-muted-foreground leading-relaxed mb-8">
Schedule a no-commitment discovery call with our consulting team. Let's discuss your vision and create a roadmap to success.
</p>
</div>
<div className="space-y-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-white/10 backdrop-blur-sm rounded-lg border border-white/20 flex items-center justify-center">
<MessageSquare className="w-6 h-6 text-accent" />
</div>
<div>
<div className="text-foreground font-medium">Free Consultation</div>
<div className="text-muted-foreground text-sm">No sales pitch, just honest advice</div>
</div>
</div>
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-white/10 backdrop-blur-sm rounded-lg border border-white/20 flex items-center justify-center">
<Clock className="w-6 h-6 text-accent" />
</div>
<div>
<div className="text-foreground font-medium">Flexible Scheduling</div>
<div className="text-muted-foreground text-sm">Available across all time zones</div>
</div>
</div>
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-white/10 backdrop-blur-sm rounded-lg border border-white/20 flex items-center justify-center">
<Zap className="w-6 h-6 text-accent" />
</div>
<div>
<div className="text-foreground font-medium">Quick Response</div>
<div className="text-muted-foreground text-sm">We'll get back to you within 2 hours</div>
</div>
</div>
</div>
</motion.div>
{/* Right CTA */}
<motion.div
initial={{ opacity: 0, x: 50 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
viewport={{ once: true }}
className="relative"
>
<div className="bg-card/50 backdrop-blur-sm rounded-lg p-8 border border-border">
<div className="text-center mb-8">
<div className="w-20 h-20 mx-auto mb-6 bg-white/10 backdrop-blur-sm rounded-full border border-white/20 flex items-center justify-center">
<Calendar className="w-10 h-10 text-accent" />
</div>
<h3 className="text-2xl font-semibold text-foreground mb-2">
Book a Discovery Call
</h3>
<p className="text-muted-foreground mb-8">
Let's discuss your project and explore how we can help you succeed.
</p>
</div>
<div className="space-y-8">
<Button
size="lg"
className="w-full bg-accent hover:bg-accent/90 text-accent-foreground py-4 text-lg border-0 rounded-lg"
onClick={() => navigateTo('/contact')}
>
<Phone className="w-5 h-5 mr-2" />
Schedule Free Call
</Button>
</div>
<div className="mt-8 pt-8 border-t border-border">
<div className="grid grid-cols-3 gap-4 text-center text-sm">
<div>
<div className="font-semibold text-foreground">200+</div>
<div className="text-muted-foreground">Projects</div>
</div>
<div>
<div className="font-semibold text-foreground">25+</div>
<div className="text-muted-foreground">Years</div>
</div>
<div>
<div className="font-semibold text-foreground">15+</div>
<div className="text-muted-foreground">Countries</div>
</div>
</div>
</div>
</div>
</motion.div>
</div>
</div>
</section>
);
};

View File

@@ -0,0 +1,94 @@
import { motion } from "framer-motion";
import { FileText, Palette, Code, Rocket } from "lucide-react";
import { GridPattern } from "./GridPattern";
const steps = [
{
title: "Define Scope",
description: "We analyze your requirements and create a detailed project roadmap with clear timelines.",
icon: FileText,
color: "from-blue-500 to-cyan-500"
},
{
title: "Design UI/UX",
description: "Our designers create intuitive, user-centered interfaces that align with your brand.",
icon: Palette,
color: "from-purple-500 to-pink-500"
},
{
title: "Develop with Agile Sprints",
description: "We build your product using agile methodology with regular updates and feedback loops.",
icon: Code,
color: "from-green-500 to-emerald-500"
},
{
title: "Test, Launch & Scale",
description: "Comprehensive testing, smooth deployment, and ongoing support for continuous growth.",
icon: Rocket,
color: "from-[#E5195E] to-orange-500"
}
];
const StepCard = ({ step, index }: { step: typeof steps[0]; index: number }) => {
const Icon = step.icon;
return (
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.2 }}
viewport={{ once: true }}
className="relative group"
>
{/* Connection Line */}
{index < steps.length - 1 && (
<div className="hidden lg:block absolute top-16 left-full w-full h-0.5 bg-gradient-to-r from-white/20 to-transparent z-0" />
)}
<div className="relative z-10 text-center">
<div className="mb-6">
<div className={`w-20 h-20 mx-auto rounded-2xl bg-gradient-to-br ${step.color} flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300`}>
<Icon className="w-10 h-10 text-white" />
</div>
<div className="w-8 h-8 mx-auto rounded-full bg-[#E5195E] flex items-center justify-center text-white font-bold text-sm">
{index + 1}
</div>
</div>
<h3 className="text-xl font-semibold text-white mb-4">{step.title}</h3>
<p className="text-[#CCCCCC] leading-relaxed max-w-sm mx-auto">{step.description}</p>
</div>
</motion.div>
);
};
export const StepsIllustrated = () => {
return (
<section className="relative py-20 bg-[#121212] overflow-hidden">
<GridPattern strokeDasharray="4 2" />
<div className="relative z-10 container mx-auto px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl lg:text-4xl font-semibold text-white mb-4">
How We Turn Your Idea Into a Scalable Product
</h2>
<p className="text-[#CCCCCC] text-lg max-w-2xl mx-auto">
Our proven 4-step process ensures your project is delivered on time, on budget, and exceeds expectations.
</p>
</motion.div>
<div className="grid lg:grid-cols-4 gap-12 max-w-6xl mx-auto">
{steps.map((step, index) => (
<StepCard key={step.title} step={step} index={index} />
))}
</div>
</div>
</section>
);
};

View File

@@ -0,0 +1,72 @@
import { motion } from "framer-motion";
import { Wrench, Shield, Zap } from "lucide-react";
import { GridPattern } from "./GridPattern";
const features = [
{
icon: Wrench,
title: "24+ Years of Product Engineering",
description: "Deep expertise in building scalable, production-ready solutions"
},
{
icon: Shield,
title: "100% Project Ownership & IP Transfer",
description: "Complete intellectual property rights and full project ownership"
},
{
icon: Zap,
title: "Agile, Transparent, and Outcome-Driven",
description: "Fast delivery with clear communication and measurable results"
},
];
const FeatureCard = ({ feature, index }: { feature: typeof features[0]; index: number }) => {
const Icon = feature.icon;
return (
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.2 }}
viewport={{ once: true }}
whileHover={{ y: -5 }}
className="text-center group"
>
<div className="mb-6">
<div className="w-20 h-20 mx-auto rounded-2xl bg-[#E5195E]/10 flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300">
<Icon className="w-10 h-10 text-[#E5195E]" />
</div>
<h3 className="text-xl font-semibold text-white mb-4">{feature.title}</h3>
<p className="text-[#CCCCCC] leading-relaxed max-w-sm mx-auto">{feature.description}</p>
</div>
</motion.div>
);
};
export const WhyChooseWDI = () => {
return (
<section className="relative py-20 bg-[#0E0E0E] overflow-hidden">
<GridPattern strokeDasharray="4 2" />
<div className="relative z-10 container mx-auto px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl lg:text-4xl font-semibold text-white mb-4">
Why Leading Startups Choose WDI
</h2>
</motion.div>
<div className="grid md:grid-cols-3 gap-12 max-w-6xl mx-auto">
{features.map((feature, index) => (
<FeatureCard key={feature.title} feature={feature} index={index} />
))}
</div>
</div>
</section>
);
};

View File

@@ -0,0 +1,27 @@
import React, { useState } from 'react'
const ERROR_IMG_SRC =
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg=='
export function ImageWithFallback(props: React.ImgHTMLAttributes<HTMLImageElement>) {
const [didError, setDidError] = useState(false)
const handleError = () => {
setDidError(true)
}
const { src, alt, style, className, ...rest } = props
return didError ? (
<div
className={`inline-block bg-gray-100 text-center align-middle ${className ?? ''}`}
style={style}
>
<div className="flex items-center justify-center w-full h-full">
<img src={ERROR_IMG_SRC} alt="Error loading image" {...rest} data-original-url={src} />
</div>
</div>
) : (
<img src={src} alt={alt} className={className} style={style} {...rest} onError={handleError} />
)
}

View File

@@ -0,0 +1,55 @@
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import { cn } from "./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}
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@@ -0,0 +1,157 @@
"use client";
import * as React from "react";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { cn } from "./utils";
import { buttonVariants } from "./button";
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
);
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
);
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className,
)}
{...props}
/>
</AlertDialogPortal>
);
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
)}
{...props}
/>
);
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
);
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
);
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
);
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};

66
components/ui/alert.tsx Normal file
View File

@@ -0,0 +1,66 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "./utils";
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
},
);
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
);
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className,
)}
{...props}
/>
);
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className,
)}
{...props}
/>
);
}
export { Alert, AlertTitle, AlertDescription };

View File

@@ -0,0 +1,11 @@
"use client";
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
function AspectRatio({
...props
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
}
export { AspectRatio };

53
components/ui/avatar.tsx Normal file
View File

@@ -0,0 +1,53 @@
"use client";
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "./utils";
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-10 shrink-0 overflow-hidden rounded-full",
className,
)}
{...props}
/>
);
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
);
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className,
)}
{...props}
/>
);
}
export { Avatar, AvatarImage, AvatarFallback };

46
components/ui/badge.tsx Normal file
View File

@@ -0,0 +1,46 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "./utils";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span";
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
);
}
export { Badge, badgeVariants };

View File

@@ -0,0 +1,41 @@
import { cn } from "./utils";
interface BorderBeamProps {
className?: string;
size?: number;
duration?: number;
borderWidth?: number;
colorFrom?: string;
colorTo?: string;
delay?: number;
}
export const BorderBeam = ({
className,
size = 200,
duration = 8,
borderWidth = 2,
colorFrom = "#E5195E",
colorTo = "#ffffff",
delay = 0,
}: BorderBeamProps) => {
return (
<div
className={cn(
"pointer-events-none absolute inset-0 rounded-[inherit]",
className
)}
>
<div
className="absolute inset-0 rounded-[inherit] opacity-75"
style={{
background: `conic-gradient(from 0deg, transparent, ${colorFrom}, ${colorTo}, transparent)`,
WebkitMask: `radial-gradient(farthest-side at center, transparent calc(100% - ${borderWidth}px), white calc(100% - ${borderWidth}px))`,
mask: `radial-gradient(farthest-side at center, transparent calc(100% - ${borderWidth}px), white calc(100% - ${borderWidth}px))`,
animation: `border-beam ${duration}s linear infinite`,
animationDelay: `${delay}s`,
}}
/>
</div>
);
};

View File

@@ -0,0 +1,109 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "./utils";
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className,
)}
{...props}
/>
);
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
);
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "a";
return (
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
);
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
);
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
);
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};

57
components/ui/button.tsx Normal file
View File

@@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "./utils"
const buttonVariants = cva(
"btn-elevation inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "btn-primary-wdi",
destructive: "btn-destructive-wdi",
outline: "btn-outline-wdi",
secondary: "btn-secondary-wdi",
ghost: "btn-ghost-wdi",
link: "btn-link-wdi",
},
size: {
default: "h-10 px-4 py-2 text-sm",
sm: "btn-sm h-8 rounded-md px-3 text-xs",
lg: "btn-lg h-11 rounded-md px-8",
xl: "btn-xl h-12 rounded-md px-10",
icon: "h-9 w-9",
},
},
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, children, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
>
{children}
</Comp>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,75 @@
"use client";
import * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DayPicker } from "react-day-picker";
import { cn } from "./utils";
import { buttonVariants } from "./button";
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: React.ComponentProps<typeof DayPicker>) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row gap-2",
month: "flex flex-col gap-4",
caption: "flex justify-center pt-1 relative items-center w-full",
caption_label: "text-sm font-medium",
nav: "flex items-center gap-1",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"size-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-x-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md",
),
day: cn(
buttonVariants({ variant: "ghost" }),
"size-8 p-0 font-normal aria-selected:opacity-100",
),
day_range_start:
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
day_range_end:
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
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 aria-selected:text-muted-foreground",
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: ({ className, ...props }) => (
<ChevronLeft className={cn("size-4", className)} {...props} />
),
IconRight: ({ className, ...props }) => (
<ChevronRight className={cn("size-4", className)} {...props} />
),
}}
{...props}
/>
);
}
export { Calendar };

92
components/ui/card.tsx Normal file
View File

@@ -0,0 +1,92 @@
import * as React from "react";
import { cn } from "./utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border",
className,
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 pt-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className,
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<h4
data-slot="card-title"
className={cn("leading-none", className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<p
data-slot="card-description"
className={cn("text-muted-foreground", className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6 [&:last-child]:pb-6", className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 pb-6 [.border-t]:pt-6", className)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};

241
components/ui/carousel.tsx Normal file
View File

@@ -0,0 +1,241 @@
"use client";
import * as React from "react";
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { cn } from "./utils";
import { Button } from "./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;
}
function Carousel({
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
}: React.ComponentProps<"div"> & CarouselProps) {
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
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
}
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel();
return (
<div
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className,
)}
{...props}
/>
</div>
);
}
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel();
return (
<div
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className,
)}
{...props}
/>
);
}
function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft />
<span className="sr-only">Previous slide</span>
</Button>
);
}
function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight />
<span className="sr-only">Next slide</span>
</Button>
);
}
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
};

353
components/ui/chart.tsx Normal file
View File

@@ -0,0 +1,353 @@
"use client";
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "./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;
}
function ChartContainer({
id,
className,
children,
config,
...props
}: React.ComponentProps<"div"> & {
config: ChartConfig;
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"];
}) {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{ config }}>
<div
data-slot="chart"
data-chart={chartId}
className={cn(
"[&_.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-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 flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className,
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
}
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;
function ChartTooltipContent({
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
}) {
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
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border 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(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
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="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
}
const ChartLegend = RechartsPrimitive.Legend;
function ChartLegendContent({
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
}: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean;
nameKey?: string;
}) {
const { config } = useChart();
if (!payload?.length) {
return null;
}
return (
<div
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(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
}
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string,
) {
if (typeof payload !== "object" || payload === null) {
return undefined;
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string;
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config];
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
};

View File

@@ -0,0 +1,32 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { CheckIcon } from "lucide-react";
import { cn } from "./utils";
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border bg-input-background dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}
export { Checkbox };

View File

@@ -0,0 +1,33 @@
"use client";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
}
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
);
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
);
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

177
components/ui/command.tsx Normal file
View File

@@ -0,0 +1,177 @@
"use client";
import * as React from "react";
import { Command as CommandPrimitive } from "cmdk";
import { SearchIcon } from "lucide-react";
import { cn } from "./utils";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "./dialog";
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className,
)}
{...props}
/>
);
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string;
description?: string;
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[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>
);
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
);
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className,
)}
{...props}
/>
);
}
function CommandEmpty({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
);
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className,
)}
{...props}
/>
);
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
);
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};

View File

@@ -0,0 +1,252 @@
"use client";
import * as React from "react";
import * as ContextMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "./utils";
function ContextMenu({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
}
function ContextMenuTrigger({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
);
}
function ContextMenuGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
);
}
function ContextMenuPortal({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
);
}
function ContextMenuSub({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
}
function ContextMenuRadioGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
);
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubTrigger>
);
}
function ContextMenuSubContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return (
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={cn(
"bg-popover 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 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
/>
);
}
function ContextMenuContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={cn(
"bg-popover 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 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
);
}
function ContextMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function ContextMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
);
}
function ContextMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
);
}
function ContextMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.Label
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function ContextMenuSeparator({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function ContextMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
};

135
components/ui/dialog.tsx Normal file
View File

@@ -0,0 +1,135 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import { cn } from "./utils";
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function DialogContent({
className,
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
)}
{...props}
/>
);
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
);
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};

132
components/ui/drawer.tsx Normal file
View File

@@ -0,0 +1,132 @@
"use client";
import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "./utils";
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
className,
)}
{...props}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
);
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
);
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
};

View File

@@ -0,0 +1,257 @@
"use client";
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "./utils";
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
);
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
);
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover 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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
);
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
);
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover 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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};

168
components/ui/form.tsx Normal file
View File

@@ -0,0 +1,168 @@
"use client";
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form";
import { cn } from "./utils";
import { Label } from "./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 } = useFormContext();
const formState = useFormState({ name: fieldContext.name });
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,
);
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
);
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField();
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
);
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField();
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message ?? "") : props.children;
if (!body) {
return null;
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
{...props}
>
{body}
</p>
);
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
};

View File

@@ -0,0 +1,44 @@
"use client";
import * as React from "react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import { cn } from "./utils";
function HoverCard({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
}
function HoverCardTrigger({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return (
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
);
}
function HoverCardContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
return (
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
<HoverCardPrimitive.Content
data-slot="hover-card-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover 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 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className,
)}
{...props}
/>
</HoverCardPrimitive.Portal>
);
}
export { HoverCard, HoverCardTrigger, HoverCardContent };

View File

@@ -0,0 +1,77 @@
"use client";
import * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp";
import { MinusIcon } from "lucide-react";
import { cn } from "./utils";
function InputOTP({
className,
containerClassName,
...props
}: React.ComponentProps<typeof OTPInput> & {
containerClassName?: string;
}) {
return (
<OTPInput
data-slot="input-otp"
containerClassName={cn(
"flex items-center gap-2 has-disabled:opacity-50",
containerClassName,
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
);
}
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-otp-group"
className={cn("flex items-center gap-1", className)}
{...props}
/>
);
}
function InputOTPSlot({
index,
className,
...props
}: React.ComponentProps<"div"> & {
index: number;
}) {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
return (
<div
data-slot="input-otp-slot"
data-active={isActive}
className={cn(
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm bg-input-background transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
className,
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
</div>
)}
</div>
);
}
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="input-otp-separator" role="separator" {...props}>
<MinusIcon />
</div>
);
}
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

21
components/ui/input.tsx Normal file
View File

@@ -0,0 +1,21 @@
import * as React from "react";
import { cn } from "./utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base bg-input-background transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
)}
{...props}
/>
);
}
export { Input };

24
components/ui/label.tsx Normal file
View File

@@ -0,0 +1,24 @@
"use client";
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cn } from "./utils";
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className,
)}
{...props}
/>
);
}
export { Label };

276
components/ui/menubar.tsx Normal file
View File

@@ -0,0 +1,276 @@
"use client";
import * as React from "react";
import * as MenubarPrimitive from "@radix-ui/react-menubar";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "./utils";
function Menubar({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
return (
<MenubarPrimitive.Root
data-slot="menubar"
className={cn(
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
className,
)}
{...props}
/>
);
}
function MenubarMenu({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />;
}
function MenubarGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />;
}
function MenubarPortal({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />;
}
function MenubarRadioGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return (
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
);
}
function MenubarTrigger({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
return (
<MenubarPrimitive.Trigger
data-slot="menubar-trigger"
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
className,
)}
{...props}
/>
);
}
function MenubarContent({
className,
align = "start",
alignOffset = -4,
sideOffset = 8,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
return (
<MenubarPortal>
<MenubarPrimitive.Content
data-slot="menubar-content"
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground 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 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
className,
)}
{...props}
/>
</MenubarPortal>
);
}
function MenubarItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<MenubarPrimitive.Item
data-slot="menubar-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function MenubarCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
return (
<MenubarPrimitive.CheckboxItem
data-slot="menubar-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
);
}
function MenubarRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
return (
<MenubarPrimitive.RadioItem
data-slot="menubar-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
);
}
function MenubarLabel({
className,
inset,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
inset?: boolean;
}) {
return (
<MenubarPrimitive.Label
data-slot="menubar-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function MenubarSeparator({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
return (
<MenubarPrimitive.Separator
data-slot="menubar-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function MenubarShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="menubar-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
function MenubarSub({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
}
function MenubarSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<MenubarPrimitive.SubTrigger
data-slot="menubar-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
);
}
function MenubarSubContent({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
return (
<MenubarPrimitive.SubContent
data-slot="menubar-sub-content"
className={cn(
"bg-popover 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 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
/>
);
}
export {
Menubar,
MenubarPortal,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarGroup,
MenubarSeparator,
MenubarLabel,
MenubarItem,
MenubarShortcut,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSub,
MenubarSubTrigger,
MenubarSubContent,
};

View File

@@ -0,0 +1,168 @@
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDownIcon } from "lucide-react";
import { cn } from "./utils";
function NavigationMenu({
className,
children,
viewport = true,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
viewport?: boolean;
}) {
return (
<NavigationMenuPrimitive.Root
data-slot="navigation-menu"
data-viewport={viewport}
className={cn(
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
className,
)}
{...props}
>
{children}
{viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root>
);
}
function NavigationMenuList({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
return (
<NavigationMenuPrimitive.List
data-slot="navigation-menu-list"
className={cn(
"group flex flex-1 list-none items-center justify-center gap-1",
className,
)}
{...props}
/>
);
}
function NavigationMenuItem({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
return (
<NavigationMenuPrimitive.Item
data-slot="navigation-menu-item"
className={cn("relative", className)}
{...props}
/>
);
}
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1",
);
function NavigationMenuTrigger({
className,
children,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
return (
<NavigationMenuPrimitive.Trigger
data-slot="navigation-menu-trigger"
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDownIcon
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
);
}
function NavigationMenuContent({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
return (
<NavigationMenuPrimitive.Content
data-slot="navigation-menu-content"
className={cn(
"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 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
className,
)}
{...props}
/>
);
}
function NavigationMenuViewport({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
return (
<div
className={cn(
"absolute top-full left-0 isolate z-50 flex justify-center",
)}
>
<NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport"
className={cn(
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
className,
)}
{...props}
/>
</div>
);
}
function NavigationMenuLink({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
return (
<NavigationMenuPrimitive.Link
data-slot="navigation-menu-link"
className={cn(
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function NavigationMenuIndicator({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
return (
<NavigationMenuPrimitive.Indicator
data-slot="navigation-menu-indicator"
className={cn(
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
className,
)}
{...props}
>
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
</NavigationMenuPrimitive.Indicator>
);
}
export {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
navigationMenuTriggerStyle,
};

View File

@@ -0,0 +1,127 @@
import * as React from "react";
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react";
import { cn } from "./utils";
import { Button, buttonVariants } from "./button";
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
<nav
role="navigation"
aria-label="pagination"
data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
);
}
function PaginationContent({
className,
...props
}: React.ComponentProps<"ul">) {
return (
<ul
data-slot="pagination-content"
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
);
}
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} />;
}
type PaginationLinkProps = {
isActive?: boolean;
} & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a">;
function PaginationLink({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
return (
<a
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className,
)}
{...props}
/>
);
}
function PaginationPrevious({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
{...props}
>
<ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink>
);
}
function PaginationNext({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
{...props}
>
<span className="hidden sm:block">Next</span>
<ChevronRightIcon />
</PaginationLink>
);
}
function PaginationEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden
data-slot="pagination-ellipsis"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
);
}
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
};

48
components/ui/popover.tsx Normal file
View File

@@ -0,0 +1,48 @@
"use client";
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "./utils";
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover 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 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
);
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

View File

@@ -0,0 +1,31 @@
"use client";
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "./utils";
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
);
}
export { Progress };

View File

@@ -0,0 +1,45 @@
"use client";
import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { CircleIcon } from "lucide-react";
import { cn } from "./utils";
function RadioGroup({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
);
}
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
}
export { RadioGroup, RadioGroupItem };

View File

@@ -0,0 +1,56 @@
"use client";
import * as React from "react";
import { GripVerticalIcon } from "lucide-react";
import * as ResizablePrimitive from "react-resizable-panels";
import { cn } from "./utils";
function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
return (
<ResizablePrimitive.PanelGroup
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className,
)}
{...props}
/>
);
}
function ResizablePanel({
...props
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
}
function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean;
}) {
return (
<ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden 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 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className,
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
);
}
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };

View File

@@ -0,0 +1,58 @@
"use client";
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "./utils";
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
);
}
export { ScrollArea, ScrollBar };

189
components/ui/select.tsx Normal file
View File

@@ -0,0 +1,189 @@
"use client";
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import {
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "lucide-react";
import { cn } from "./utils";
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default";
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-full items-center justify-between gap-2 rounded-md border bg-input-background px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover 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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
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)] scroll-my-1",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
);
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};

View File

@@ -0,0 +1,27 @@
"use client";
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "./utils";
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator-root"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className,
)}
{...props}
/>
);
}
export { Separator };

139
components/ui/sheet.tsx Normal file
View File

@@ -0,0 +1,139 @@
"use client";
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import { cn } from "./utils";
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left";
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className,
)}
{...props}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
);
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
);
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};

View File

@@ -0,0 +1,29 @@
import React from "react";
import { cn } from "./utils";
interface ShimmerButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
className?: string;
}
const ShimmerButton = React.forwardRef<HTMLButtonElement, ShimmerButtonProps>(
({ className, children, ...props }, ref) => {
return (
<button
ref={ref}
className={cn(
"relative inline-flex items-center justify-center gap-2 rounded-md bg-accent px-6 py-3 text-sm font-medium text-accent-foreground transition-all duration-300 hover:bg-accent/90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent whitespace-nowrap overflow-hidden",
"before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent",
className
)}
{...props}
>
{children}
</button>
);
}
);
ShimmerButton.displayName = "ShimmerButton";
export { ShimmerButton };

726
components/ui/sidebar.tsx Normal file
View File

@@ -0,0 +1,726 @@
"use client";
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { VariantProps, cva } from "class-variance-authority";
import { PanelLeftIcon } from "lucide-react";
import { useIsMobile } from "./use-mobile";
import { cn } from "./utils";
import { Button } from "./button";
import { Input } from "./input";
import { Separator } from "./separator";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "./sheet";
import { Skeleton } from "./skeleton";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./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 SidebarContextProps = {
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
function useSidebar() {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.");
}
return context;
}
function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}) {
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<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
);
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
data-slot="sidebar-wrapper"
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
className,
)}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
);
}
function Sidebar({
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
}: React.ComponentProps<"div"> & {
side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") {
return (
<div
data-slot="sidebar"
className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
className,
)}
{...props}
>
{children}
</div>
);
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
);
}
return (
<div
className="group peer text-sidebar-foreground hidden md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
data-slot="sidebar"
>
{/* This is what handles the sidebar gap on desktop */}
<div
data-slot="sidebar-gap"
className={cn(
"relative 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)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
)}
/>
<div
data-slot="sidebar-container"
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)+(--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"
data-slot="sidebar-inner"
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
>
{children}
</div>
</div>
</div>
);
}
function SidebarTrigger({
className,
onClick,
...props
}: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar();
return (
<Button
data-sidebar="trigger"
data-slot="sidebar-trigger"
variant="ghost"
size="icon"
className={cn("size-7", className)}
onClick={(event) => {
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);
}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar();
return (
<button
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className,
)}
{...props}
/>
);
}
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
return (
<main
data-slot="sidebar-inset"
className={cn(
"bg-background relative flex w-full flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className,
)}
{...props}
/>
);
}
function SidebarInput({
className,
...props
}: React.ComponentProps<typeof Input>) {
return (
<Input
data-slot="sidebar-input"
data-sidebar="input"
className={cn("bg-background h-8 w-full shadow-none", className)}
{...props}
/>
);
}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-header"
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
);
}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-footer"
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
);
}
function SidebarSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="sidebar-separator"
data-sidebar="separator"
className={cn("bg-sidebar-border mx-2 w-auto", className)}
{...props}
/>
);
}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-content"
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}
/>
);
}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group"
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
);
}
function SidebarGroupLabel({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div";
return (
<Comp
data-slot="sidebar-group-label"
data-sidebar="group-label"
className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] 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}
/>
);
}
function SidebarGroupAction({
className,
asChild = false,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="sidebar-group-action"
data-sidebar="group-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
}
function SidebarGroupContent({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
);
}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu"
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
);
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-item"
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
);
}
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden 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",
},
},
);
function SidebarMenuButton({
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean;
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot : "button";
const { isMobile, state } = useSidebar();
const button = (
<Comp
data-slot="sidebar-menu-button"
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>
);
}
function SidebarMenuAction({
className,
asChild = false,
showOnHover = false,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean;
showOnHover?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="sidebar-menu-action"
data-sidebar="menu-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after: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 &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className,
)}
{...props}
/>
);
}
function SidebarMenuBadge({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
className={cn(
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
"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}
/>
);
}
function SidebarMenuSkeleton({
className,
showIcon = false,
...props
}: React.ComponentProps<"div"> & {
showIcon?: boolean;
}) {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
data-slot="sidebar-menu-skeleton"
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>
);
}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu-sub"
data-sidebar="menu-sub"
className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
}
function SidebarMenuSubItem({
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-sub-item"
data-sidebar="menu-sub-item"
className={cn("group/menu-sub-item relative", className)}
{...props}
/>
);
}
function SidebarMenuSubButton({
asChild = false,
size = "md",
isActive = false,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean;
size?: "sm" | "md";
isActive?: boolean;
}) {
const Comp = asChild ? Slot : "a";
return (
<Comp
data-slot="sidebar-menu-sub-button"
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"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}
/>
);
}
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
};

View File

@@ -0,0 +1,13 @@
import { cn } from "./utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
/>
);
}
export { Skeleton };

63
components/ui/slider.tsx Normal file
View File

@@ -0,0 +1,63 @@
"use client";
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import { cn } from "./utils";
function Slider({
className,
defaultValue,
value,
min = 0,
max = 100,
...props
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
const _values = React.useMemo(
() =>
Array.isArray(value)
? value
: Array.isArray(defaultValue)
? defaultValue
: [min, max],
[value, defaultValue, min, max],
);
return (
<SliderPrimitive.Root
data-slot="slider"
defaultValue={defaultValue}
value={value}
min={min}
max={max}
className={cn(
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
className,
)}
{...props}
>
<SliderPrimitive.Track
data-slot="slider-track"
className={cn(
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-4 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
)}
>
<SliderPrimitive.Range
data-slot="slider-range"
className={cn(
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
)}
/>
</SliderPrimitive.Track>
{Array.from({ length: _values.length }, (_, index) => (
<SliderPrimitive.Thumb
data-slot="slider-thumb"
key={index}
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
/>
))}
</SliderPrimitive.Root>
);
}
export { Slider };

25
components/ui/sonner.tsx Normal file
View File

@@ -0,0 +1,25 @@
"use client";
import { useTheme } from "next-themes";
import { Toaster as Sonner, ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
);
};
export { Toaster };

31
components/ui/switch.tsx Normal file
View File

@@ -0,0 +1,31 @@
"use client";
import * as React from "react";
import * as SwitchPrimitive from "@radix-ui/react-switch";
import { cn } from "./utils";
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-switch-background focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-card dark:data-[state=unchecked]:bg-card-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitive.Root>
);
}
export { Switch };

116
components/ui/table.tsx Normal file
View File

@@ -0,0 +1,116 @@
"use client";
import * as React from "react";
import { cn } from "./utils";
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
);
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
);
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
);
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className,
)}
{...props}
/>
);
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className,
)}
{...props}
/>
);
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}
/>
);
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}
/>
);
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
);
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};

66
components/ui/tabs.tsx Normal file
View File

@@ -0,0 +1,66 @@
"use client";
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "./utils";
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
);
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-xl p-[3px] flex",
className,
)}
{...props}
/>
);
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-card dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-xl border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@@ -0,0 +1,18 @@
import * as React from "react";
import { cn } from "./utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"resize-none border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-input-background px-3 py-2 text-base transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
{...props}
/>
);
}
export { Textarea };

View File

@@ -0,0 +1,73 @@
"use client";
import * as React from "react";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import { type VariantProps } from "class-variance-authority";
import { cn } from "./utils";
import { toggleVariants } from "./toggle";
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
});
function ToggleGroup({
className,
variant,
size,
children,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<ToggleGroupPrimitive.Root
data-slot="toggle-group"
data-variant={variant}
data-size={size}
className={cn(
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
className,
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
);
}
function ToggleGroupItem({
className,
children,
variant,
size,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
className,
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
}
export { ToggleGroup, ToggleGroupItem };

47
components/ui/toggle.tsx Normal file
View File

@@ -0,0 +1,47 @@
"use client";
import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "./utils";
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-2 min-w-9",
sm: "h-8 px-1.5 min-w-8",
lg: "h-10 px-2.5 min-w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Toggle({
className,
variant,
size,
...props
}: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Toggle, toggleVariants };

61
components/ui/tooltip.tsx Normal file
View File

@@ -0,0 +1,61 @@
"use client";
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "./utils";
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
);
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
);
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground 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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@@ -0,0 +1,21 @@
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;
}

6
components/ui/utils.ts Normal file
View File

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