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 }
|
||||||
127
app/data/portfolio.ts
Normal file
127
app/data/portfolio.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
export const portfolioData = {
|
||||||
|
profile: {
|
||||||
|
name: "Alex Johnson",
|
||||||
|
title: "Full Stack Developer",
|
||||||
|
bio: "Passionate full-stack developer with 5+ years of experience building scalable web applications. I love creating elegant solutions to complex problems and am always eager to learn new technologies.",
|
||||||
|
email: "alex.johnson@example.com",
|
||||||
|
phone: "+1 (555) 123-4567",
|
||||||
|
location: "San Francisco, CA",
|
||||||
|
githubUrl: "https://github.com/alexjohnson",
|
||||||
|
linkedinUrl: "https://linkedin.com/in/alexjohnson",
|
||||||
|
},
|
||||||
|
experiences: [
|
||||||
|
{
|
||||||
|
_id: "1",
|
||||||
|
company: "TechCorp Inc.",
|
||||||
|
position: "Senior Full Stack Developer",
|
||||||
|
duration: "2022 - Present",
|
||||||
|
description:
|
||||||
|
"Lead development of microservices architecture serving 1M+ users. Mentored junior developers and implemented CI/CD pipelines that reduced deployment time by 60%.",
|
||||||
|
technologies: [
|
||||||
|
"React",
|
||||||
|
"Node.js",
|
||||||
|
"TypeScript",
|
||||||
|
"AWS",
|
||||||
|
"Docker",
|
||||||
|
"PostgreSQL",
|
||||||
|
],
|
||||||
|
current: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "2",
|
||||||
|
company: "StartupXYZ",
|
||||||
|
position: "Full Stack Developer",
|
||||||
|
duration: "2020 - 2022",
|
||||||
|
description:
|
||||||
|
"Built the entire web platform from scratch using modern technologies. Collaborated with designers and product managers to deliver features that increased user engagement by 40%.",
|
||||||
|
technologies: ["Vue.js", "Python", "Django", "Redis", "MySQL"],
|
||||||
|
current: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "3",
|
||||||
|
company: "Digital Agency",
|
||||||
|
position: "Frontend Developer",
|
||||||
|
duration: "2019 - 2020",
|
||||||
|
description:
|
||||||
|
"Developed responsive websites and web applications for various clients. Focused on performance optimization and accessibility standards.",
|
||||||
|
technologies: ["JavaScript", "HTML5", "CSS3", "Sass", "Webpack"],
|
||||||
|
current: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
_id: "1",
|
||||||
|
title: "E-commerce Platform",
|
||||||
|
description:
|
||||||
|
"A full-featured e-commerce platform with real-time inventory management, payment processing, and admin dashboard.",
|
||||||
|
technologies: ["Next.js", "Stripe", "Prisma", "PostgreSQL"],
|
||||||
|
githubUrl: "https://github.com/alexjohnson/ecommerce-platform",
|
||||||
|
liveUrl: "https://ecommerce-demo.example.com",
|
||||||
|
featured: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "2",
|
||||||
|
title: "Task Management App",
|
||||||
|
description:
|
||||||
|
"Collaborative task management application with real-time updates, file sharing, and team communication features.",
|
||||||
|
technologies: ["React", "Socket.io", "Express", "MongoDB"],
|
||||||
|
githubUrl: "https://github.com/alexjohnson/task-manager",
|
||||||
|
liveUrl: "https://taskmanager-demo.example.com",
|
||||||
|
featured: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "3",
|
||||||
|
title: "Weather Dashboard",
|
||||||
|
description:
|
||||||
|
"Interactive weather dashboard with location-based forecasts, historical data visualization, and weather alerts.",
|
||||||
|
technologies: ["Vue.js", "Chart.js", "OpenWeather API"],
|
||||||
|
githubUrl: "https://github.com/alexjohnson/weather-dashboard",
|
||||||
|
liveUrl: "https://weather-demo.example.com",
|
||||||
|
featured: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
skills: [
|
||||||
|
{
|
||||||
|
_id: "1",
|
||||||
|
category: "Frontend",
|
||||||
|
items: [
|
||||||
|
{ name: "React", level: 90 },
|
||||||
|
{ name: "TypeScript", level: 85 },
|
||||||
|
{ name: "Vue.js", level: 80 },
|
||||||
|
{ name: "Next.js", level: 85 },
|
||||||
|
{ name: "Tailwind CSS", level: 90 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "2",
|
||||||
|
category: "Backend",
|
||||||
|
items: [
|
||||||
|
{ name: "Node.js", level: 90 },
|
||||||
|
{ name: "Python", level: 80 },
|
||||||
|
{ name: "Express", level: 85 },
|
||||||
|
{ name: "Django", level: 75 },
|
||||||
|
{ name: "GraphQL", level: 70 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "3",
|
||||||
|
category: "Database",
|
||||||
|
items: [
|
||||||
|
{ name: "PostgreSQL", level: 85 },
|
||||||
|
{ name: "MongoDB", level: 80 },
|
||||||
|
{ name: "Redis", level: 75 },
|
||||||
|
{ name: "MySQL", level: 80 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "4",
|
||||||
|
category: "DevOps",
|
||||||
|
items: [
|
||||||
|
{ name: "AWS", level: 80 },
|
||||||
|
{ name: "Docker", level: 85 },
|
||||||
|
{ name: "CI/CD", level: 80 },
|
||||||
|
{ name: "Kubernetes", level: 70 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
28
app/routes/_index.tsx
Normal file
28
app/routes/_index.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import type { MetaFunction } from "@remix-run/node";
|
||||||
|
import { Navigation } from "@/components/navigation";
|
||||||
|
import { HeroSection } from "@/components/hero-section";
|
||||||
|
import { ExperienceSection } from "@/components/experience-section";
|
||||||
|
import { ProjectsSection } from "@/components/projects-section";
|
||||||
|
import { SkillsSection } from "@/components/skills-section";
|
||||||
|
import { ContactSection } from "@/components/contact-section";
|
||||||
|
import { portfolioData } from "@/data/portfolio";
|
||||||
|
|
||||||
|
export const meta: MetaFunction = () => {
|
||||||
|
return [
|
||||||
|
{ title: `${portfolioData.profile.name} - Portfolio` },
|
||||||
|
{ name: "description", content: portfolioData.profile.bio },
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<Navigation profile={portfolioData.profile} />
|
||||||
|
<HeroSection profile={portfolioData.profile} />
|
||||||
|
<ExperienceSection experiences={portfolioData.experiences} />
|
||||||
|
<ProjectsSection projects={portfolioData.projects} />
|
||||||
|
<SkillsSection skills={portfolioData.skills} />
|
||||||
|
<ContactSection profile={portfolioData.profile} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,10 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/afiqzudinhadi/afiqzudinhadi.github.io#readme",
|
"homepage": "https://github.com/afiqzudinhadi/afiqzudinhadi.github.io#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@remix-run/node": "^2.16.8",
|
"@remix-run/node": "^2.16.8",
|
||||||
"@remix-run/react": "^2.16.8",
|
"@remix-run/react": "^2.16.8",
|
||||||
"@remix-run/serve": "^2.16.8",
|
"@remix-run/serve": "^2.16.8",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue