diff --git a/app/components/contact-section.tsx b/app/components/contact-section.tsx new file mode 100644 index 0000000..1142eed --- /dev/null +++ b/app/components/contact-section.tsx @@ -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 ( +
+
+

Get In Touch

+ + + Let's Work Together + + I'm always interested in new opportunities and exciting projects. + Feel free to reach out if you'd like to collaborate! + + + +
+
+ + +
+ + {profile.phone && ( +
+ + +
+ )} + +
+ +
+

Location

+

{profile.location}

+
+
+
+ +
+ {profile.githubUrl && ( + + )} + {profile.linkedinUrl && ( + + )} + +
+
+
+
+
+ ); +} diff --git a/app/components/experience-section.tsx b/app/components/experience-section.tsx new file mode 100644 index 0000000..430c5cd --- /dev/null +++ b/app/components/experience-section.tsx @@ -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 ( +
+
+

Experience

+
+ {experiences.map((exp, index) => ( +
+ + +
+
+ {exp.company} + + {exp.position} + +

+ {exp.duration} +

+
+ {exp.current && ( + + Current + + )} +
+
+ +

+ {exp.description} +

+
+ {exp.technologies.map((tech) => ( + + {tech} + + ))} +
+
+
+ {index < experiences.length - 1 && } +
+ ))} +
+
+
+ ); +} diff --git a/app/components/hero-section.tsx b/app/components/hero-section.tsx new file mode 100644 index 0000000..32bd508 --- /dev/null +++ b/app/components/hero-section.tsx @@ -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 ( +
+
+
+
+ + + {profile.name + .split(" ") + .map((n) => n[0]) + .join("")} + + +

+ {profile.name} +

+

+ {profile.title} +

+

+ {profile.bio} +

+
+ +
+ {profile.githubUrl && ( + + )} + {profile.linkedinUrl && ( + + )} + +
+ +
+
+ + {profile.location} +
+
+ + {profile.email} +
+ {profile.phone && ( +
+ + {profile.phone} +
+ )} +
+
+
+
+ ); +} diff --git a/app/components/navigation.tsx b/app/components/navigation.tsx new file mode 100644 index 0000000..c29ec34 --- /dev/null +++ b/app/components/navigation.tsx @@ -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 ( + + ); +} diff --git a/app/components/projects-section.tsx b/app/components/projects-section.tsx new file mode 100644 index 0000000..0d25903 --- /dev/null +++ b/app/components/projects-section.tsx @@ -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 ( +
+
+

Projects

+ + {featuredProjects.length > 0 && ( +
+

+ + Featured Projects +

+
+ {featuredProjects.map((project) => ( + + + + {project.title} + Featured + + + {project.description} + + + +
+ {project.technologies.map((tech) => ( + + {tech} + + ))} +
+
+ {project.githubUrl && ( + + )} + {project.liveUrl && ( + + )} +
+
+
+ ))} +
+
+ )} + + {otherProjects.length > 0 && ( +
+

Other Projects

+
+ {otherProjects.map((project) => ( + + + {project.title} + + {project.description} + + + +
+ {project.technologies.slice(0, 3).map((tech) => ( + + {tech} + + ))} + {project.technologies.length > 3 && ( + + +{project.technologies.length - 3} + + )} +
+
+ {project.githubUrl && ( + + )} + {project.liveUrl && ( + + )} +
+
+
+ ))} +
+
+ )} +
+
+ ); +} diff --git a/app/components/skills-section.tsx b/app/components/skills-section.tsx new file mode 100644 index 0000000..1fec020 --- /dev/null +++ b/app/components/skills-section.tsx @@ -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 ( +
+
+

Skills

+
+ {skills.map((skillCategory) => ( + + + + {skillCategory.category} + + + + {skillCategory.items.map((skill) => ( +
+
+ {skill.name} + + {skill.level}% + +
+ +
+ ))} +
+
+ ))} +
+
+
+ ); +} diff --git a/app/components/ui/avatar.tsx b/app/components/ui/avatar.tsx new file mode 100644 index 0000000..706f177 --- /dev/null +++ b/app/components/ui/avatar.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/app/components/ui/badge.tsx b/app/components/ui/badge.tsx new file mode 100644 index 0000000..5e2b7ac --- /dev/null +++ b/app/components/ui/badge.tsx @@ -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, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/app/components/ui/button.tsx b/app/components/ui/button.tsx new file mode 100644 index 0000000..ef511cf --- /dev/null +++ b/app/components/ui/button.tsx @@ -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, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/app/components/ui/card.tsx b/app/components/ui/card.tsx new file mode 100644 index 0000000..874ce09 --- /dev/null +++ b/app/components/ui/card.tsx @@ -0,0 +1,76 @@ +import * as React from "react" + +import { cn } from "~/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/app/components/ui/progress.tsx b/app/components/ui/progress.tsx new file mode 100644 index 0000000..ba7cb0b --- /dev/null +++ b/app/components/ui/progress.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/app/components/ui/separator.tsx b/app/components/ui/separator.tsx new file mode 100644 index 0000000..464677f --- /dev/null +++ b/app/components/ui/separator.tsx @@ -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, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } diff --git a/app/data/portfolio.ts b/app/data/portfolio.ts new file mode 100644 index 0000000..da64ff2 --- /dev/null +++ b/app/data/portfolio.ts @@ -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 }, + ], + }, + ], +}; diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx new file mode 100644 index 0000000..0dbd6b7 --- /dev/null +++ b/app/routes/_index.tsx @@ -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 ( +
+ + + + + + +
+ ); +} diff --git a/package.json b/package.json index 400d26e..99b17ec 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,10 @@ }, "homepage": "https://github.com/afiqzudinhadi/afiqzudinhadi.github.io#readme", "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/react": "^2.16.8", "@remix-run/serve": "^2.16.8",