feat: template from chef
This commit is contained in:
parent
7335ea2581
commit
64feeaa877
15 changed files with 1001 additions and 0 deletions
111
app/components/contact-section.tsx
Normal file
111
app/components/contact-section.tsx
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Mail, Phone, MapPin, Github, Linkedin } from "lucide-react";
|
||||
|
||||
interface ContactSectionProps {
|
||||
profile: {
|
||||
name: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
location: string;
|
||||
githubUrl?: string;
|
||||
linkedinUrl?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function ContactSection({ profile }: ContactSectionProps) {
|
||||
return (
|
||||
<section id="contact" className="py-20 bg-muted/30">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 className="text-4xl font-bold text-center mb-16">Get In Touch</h2>
|
||||
<Card className="max-w-2xl mx-auto">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-2xl">Let's Work Together</CardTitle>
|
||||
<CardDescription className="text-lg">
|
||||
I'm always interested in new opportunities and exciting projects.
|
||||
Feel free to reach out if you'd like to collaborate!
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center space-x-3 p-3 rounded-lg bg-muted/50">
|
||||
<Mail className="h-5 w-5 text-primary" />
|
||||
<div>
|
||||
<p className="font-medium">Email</p>
|
||||
<a
|
||||
href={`mailto:${profile.email}`}
|
||||
className="text-muted-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
{profile.email}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{profile.phone && (
|
||||
<div className="flex items-center space-x-3 p-3 rounded-lg bg-muted/50">
|
||||
<Phone className="h-5 w-5 text-primary" />
|
||||
<div>
|
||||
<p className="font-medium">Phone</p>
|
||||
<a
|
||||
href={`tel:${profile.phone}`}
|
||||
className="text-muted-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
{profile.phone}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center space-x-3 p-3 rounded-lg bg-muted/50">
|
||||
<MapPin className="h-5 w-5 text-primary" />
|
||||
<div>
|
||||
<p className="font-medium">Location</p>
|
||||
<p className="text-muted-foreground">{profile.location}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center space-x-4 pt-6">
|
||||
{profile.githubUrl && (
|
||||
<Button variant="outline" size="lg" asChild>
|
||||
<a
|
||||
href={profile.githubUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Github className="mr-2 h-5 w-5" />
|
||||
GitHub
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
{profile.linkedinUrl && (
|
||||
<Button variant="outline" size="lg" asChild>
|
||||
<a
|
||||
href={profile.linkedinUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Linkedin className="mr-2 h-5 w-5" />
|
||||
LinkedIn
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
<Button size="lg" asChild>
|
||||
<a href={`mailto:${profile.email}`}>
|
||||
<Mail className="mr-2 h-5 w-5" />
|
||||
Send Email
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
72
app/components/experience-section.tsx
Normal file
72
app/components/experience-section.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
interface Experience {
|
||||
_id: string;
|
||||
company: string;
|
||||
position: string;
|
||||
duration: string;
|
||||
description: string;
|
||||
technologies: string[];
|
||||
current: boolean;
|
||||
}
|
||||
|
||||
interface ExperienceSectionProps {
|
||||
experiences: Experience[];
|
||||
}
|
||||
|
||||
export function ExperienceSection({ experiences }: ExperienceSectionProps) {
|
||||
return (
|
||||
<section id="experience" className="py-20">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 className="text-4xl font-bold text-center mb-16">Experience</h2>
|
||||
<div className="space-y-8">
|
||||
{experiences.map((exp, index) => (
|
||||
<div key={exp._id}>
|
||||
<Card className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader>
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<CardTitle className="text-xl">{exp.company}</CardTitle>
|
||||
<CardDescription className="text-lg font-medium text-primary">
|
||||
{exp.position}
|
||||
</CardDescription>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{exp.duration}
|
||||
</p>
|
||||
</div>
|
||||
{exp.current && (
|
||||
<Badge variant="secondary" className="w-fit">
|
||||
Current
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-4 leading-relaxed">
|
||||
{exp.description}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{exp.technologies.map((tech) => (
|
||||
<Badge key={tech} variant="outline">
|
||||
{tech}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{index < experiences.length - 1 && <Separator className="my-8" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
107
app/components/hero-section.tsx
Normal file
107
app/components/hero-section.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Github, Linkedin, Mail, MapPin, Phone } from "lucide-react";
|
||||
|
||||
interface HeroSectionProps {
|
||||
profile: {
|
||||
name: string;
|
||||
title: string;
|
||||
bio: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
location: string;
|
||||
githubUrl?: string;
|
||||
linkedinUrl?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function HeroSection({ profile }: HeroSectionProps) {
|
||||
const scrollToSection = (sectionId: string) => {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
id="about"
|
||||
className="pt-16 min-h-screen flex items-center bg-gradient-to-br from-primary/5 to-secondary/5"
|
||||
>
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||
<div className="text-center">
|
||||
<div className="mb-8">
|
||||
<Avatar className="w-32 h-32 mx-auto mb-6">
|
||||
<AvatarFallback className="text-4xl font-bold bg-gradient-to-br from-primary to-primary/80 text-primary-foreground">
|
||||
{profile.name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<h1 className="text-5xl md:text-6xl font-bold mb-4">
|
||||
{profile.name}
|
||||
</h1>
|
||||
<h2 className="text-2xl md:text-3xl text-primary font-medium mb-6">
|
||||
{profile.title}
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-3xl mx-auto leading-relaxed">
|
||||
{profile.bio}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-4 mb-8">
|
||||
{profile.githubUrl && (
|
||||
<Button asChild>
|
||||
<a
|
||||
href={profile.githubUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Github className="mr-2 h-4 w-4" />
|
||||
GitHub
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
{profile.linkedinUrl && (
|
||||
<Button asChild variant="outline">
|
||||
<a
|
||||
href={profile.linkedinUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Linkedin className="mr-2 h-4 w-4" />
|
||||
LinkedIn
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => scrollToSection("contact")}
|
||||
variant="secondary"
|
||||
>
|
||||
<Mail className="mr-2 h-4 w-4" />
|
||||
Get In Touch
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-6 text-muted-foreground">
|
||||
<div className="flex items-center">
|
||||
<MapPin className="mr-2 h-4 w-4" />
|
||||
{profile.location}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Mail className="mr-2 h-4 w-4" />
|
||||
{profile.email}
|
||||
</div>
|
||||
{profile.phone && (
|
||||
<div className="flex items-center">
|
||||
<Phone className="mr-2 h-4 w-4" />
|
||||
{profile.phone}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
66
app/components/navigation.tsx
Normal file
66
app/components/navigation.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface NavigationProps {
|
||||
profile: {
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function Navigation({ profile }: NavigationProps) {
|
||||
const [activeSection, setActiveSection] = useState("about");
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const sections = ["about", "experience", "projects", "skills", "contact"];
|
||||
const scrollPosition = window.scrollY + 100;
|
||||
|
||||
for (const section of sections) {
|
||||
const element = document.getElementById(section);
|
||||
if (element) {
|
||||
const offsetTop = element.offsetTop;
|
||||
const offsetBottom = offsetTop + element.offsetHeight;
|
||||
|
||||
if (scrollPosition >= offsetTop && scrollPosition < offsetBottom) {
|
||||
setActiveSection(section);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
const scrollToSection = (sectionId: string) => {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="fixed top-0 left-0 right-0 z-50 bg-background/80 backdrop-blur-md border-b">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
<div className="font-bold text-xl">{profile.name}</div>
|
||||
<div className="hidden md:flex space-x-2">
|
||||
{["about", "experience", "projects", "skills", "contact"].map(
|
||||
(section) => (
|
||||
<Button
|
||||
key={section}
|
||||
variant={activeSection === section ? "default" : "ghost"}
|
||||
onClick={() => scrollToSection(section)}
|
||||
className="capitalize"
|
||||
>
|
||||
{section}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
160
app/components/projects-section.tsx
Normal file
160
app/components/projects-section.tsx
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Github, ExternalLink, Star } from "lucide-react";
|
||||
|
||||
interface Project {
|
||||
_id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
technologies: string[];
|
||||
githubUrl?: string;
|
||||
liveUrl?: string;
|
||||
featured: boolean;
|
||||
}
|
||||
|
||||
interface ProjectsSectionProps {
|
||||
projects: Project[];
|
||||
}
|
||||
|
||||
export function ProjectsSection({ projects }: ProjectsSectionProps) {
|
||||
const featuredProjects = projects.filter((p) => p.featured);
|
||||
const otherProjects = projects.filter((p) => !p.featured);
|
||||
|
||||
return (
|
||||
<section id="projects" className="py-20 bg-muted/30">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 className="text-4xl font-bold text-center mb-16">Projects</h2>
|
||||
|
||||
{featuredProjects.length > 0 && (
|
||||
<div className="mb-16">
|
||||
<h3 className="text-2xl font-semibold mb-8 flex items-center">
|
||||
<Star className="mr-2 h-6 w-6 text-yellow-500" />
|
||||
Featured Projects
|
||||
</h3>
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
{featuredProjects.map((project) => (
|
||||
<Card
|
||||
key={project._id}
|
||||
className="hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
{project.title}
|
||||
<Badge variant="secondary">Featured</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription className="leading-relaxed">
|
||||
{project.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{project.technologies.map((tech) => (
|
||||
<Badge key={tech} variant="outline">
|
||||
{tech}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{project.githubUrl && (
|
||||
<Button size="sm" variant="outline" asChild>
|
||||
<a
|
||||
href={project.githubUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Github className="mr-2 h-4 w-4" />
|
||||
Code
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
{project.liveUrl && (
|
||||
<Button size="sm" asChild>
|
||||
<a
|
||||
href={project.liveUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
Live Demo
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{otherProjects.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold mb-8">Other Projects</h3>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{otherProjects.map((project) => (
|
||||
<Card
|
||||
key={project._id}
|
||||
className="hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{project.title}</CardTitle>
|
||||
<CardDescription className="text-sm leading-relaxed">
|
||||
{project.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-1 mb-4">
|
||||
{project.technologies.slice(0, 3).map((tech) => (
|
||||
<Badge key={tech} variant="outline" className="text-xs">
|
||||
{tech}
|
||||
</Badge>
|
||||
))}
|
||||
{project.technologies.length > 3 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{project.technologies.length - 3}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{project.githubUrl && (
|
||||
<Button size="sm" variant="outline" asChild>
|
||||
<a
|
||||
href={project.githubUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Github className="mr-1 h-3 w-3" />
|
||||
Code
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
{project.liveUrl && (
|
||||
<Button size="sm" asChild>
|
||||
<a
|
||||
href={project.liveUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ExternalLink className="mr-1 h-3 w-3" />
|
||||
Demo
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
54
app/components/skills-section.tsx
Normal file
54
app/components/skills-section.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
|
||||
interface Skill {
|
||||
name: string;
|
||||
level: number;
|
||||
}
|
||||
|
||||
interface SkillCategory {
|
||||
_id: string;
|
||||
category: string;
|
||||
items: Skill[];
|
||||
}
|
||||
|
||||
interface SkillsSectionProps {
|
||||
skills: SkillCategory[];
|
||||
}
|
||||
|
||||
export function SkillsSection({ skills }: SkillsSectionProps) {
|
||||
return (
|
||||
<section id="skills" className="py-20">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 className="text-4xl font-bold text-center mb-16">Skills</h2>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{skills.map((skillCategory) => (
|
||||
<Card
|
||||
key={skillCategory._id}
|
||||
className="hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl text-center">
|
||||
{skillCategory.category}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{skillCategory.items.map((skill) => (
|
||||
<div key={skill.name} className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium">{skill.name}</span>
|
||||
<span className="text-muted-foreground">
|
||||
{skill.level}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={skill.level} className="h-2" />
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
48
app/components/ui/avatar.tsx
Normal file
48
app/components/ui/avatar.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
36
app/components/ui/badge.tsx
Normal file
36
app/components/ui/badge.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
57
app/components/ui/button.tsx
Normal file
57
app/components/ui/button.tsx
Normal 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 "~/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm 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:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
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, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
76
app/components/ui/card.tsx
Normal file
76
app/components/ui/card.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
26
app/components/ui/progress.tsx
Normal file
26
app/components/ui/progress.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
))
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||
|
||||
export { Progress }
|
||||
29
app/components/ui/separator.tsx
Normal file
29
app/components/ui/separator.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
Loading…
Add table
Add a link
Reference in a new issue