Commit f63e6cb7 authored by QuanMB's avatar QuanMB

feat(ui): redesign portfolio with custom cv info

parent df20d938
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->
@AGENTS.md
# portfolio
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
This diff is collapsed.
{
"name": "portfolio",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"next": "16.2.9",
"react": "19.2.4",
"react-dom": "19.2.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.3.1",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.9",
"tailwindcss": "^4.3.1",
"typescript": "^5"
}
}
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
\ No newline at end of file
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
\ No newline at end of file
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
\ No newline at end of file
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
\ No newline at end of file
@import "tailwindcss";
@theme {
--color-primary: #3b82f6;
--color-primary-glow: rgba(59, 130, 246, 0.1);
--color-secondary: #8b5cf6;
--color-dark-bg: #0b0f19;
--color-surface: #111827;
--color-surface-border: #1f2937;
--color-surface-hover-border: #374151;
--color-nav-bg: rgba(11, 15, 25, 0.8);
--color-nav-border: rgba(31, 41, 55, 0.55);
--font-outfit: var(--font-outfit), sans-serif;
--font-inter: var(--font-inter), sans-serif;
--animate-fade-in: fadeInUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
--animate-float: float 6s ease-in-out infinite;
--animate-morph: morphing 12s ease-in-out infinite alternate;
--animate-pulse-glow: pulse 1.5s infinite;
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes float {
0% { transform: translateY(0px); }
50% { transform: translateY(-8px); }
100% { transform: translateY(0px); }
}
@keyframes morphing {
0% {
border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%;
}
50% {
border-radius: 50% 50% 33% 67% / 55% 27% 73% 45%;
}
100% {
border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%;
}
}
@keyframes pulse {
0%, 100% {
opacity: 0.25;
}
50% {
opacity: 0.4;
}
}
}
@layer base {
html {
scroll-behavior: smooth;
color-scheme: dark;
}
body {
background-color: var(--color-dark-bg);
color: #f9fafb;
font-family: var(--font-inter), system-ui, -apple-system, sans-serif;
min-height: 100vh;
overflow-x: hidden;
line-height: 1.6;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-outfit), sans-serif;
color: #f9fafb;
letter-spacing: -0.02em;
}
}
/* Minimal design utility cards (Stripe/Linear style) */
@utility glass-panel {
background-color: #111827;
border: 1px solid #1f2937;
border-radius: 18px;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
@utility glass-panel-hover {
&:hover {
border-color: #374151;
transform: translateY(-2px);
box-shadow: 0 12px 30px -10px rgba(0, 0, 0, 0.5);
}
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: var(--color-dark-bg);
}
::-webkit-scrollbar-thumb {
background: #1f2937;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #3b82f6;
}
/* Gradient highlights */
.gradient-text {
background: linear-gradient(135deg, #ffffff 40%, #94a3b8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.gradient-accent-text {
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
import type { Metadata } from "next";
import { Outfit, Inter } from "next/font/google";
import "./globals.css";
const outfit = Outfit({
variable: "--font-outfit",
subsets: ["latin"],
display: "swap",
});
const inter = Inter({
variable: "--font-inter",
subsets: ["latin"],
display: "swap",
});
export const metadata: Metadata = {
title: "Minh Quan | Creative Developer Portfolio",
description: "Creative Fullstack Developer & Frontend Engineer portfolio showcasing modern, interactive web applications.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className={`${outfit.variable} ${inter.variable}`}>
<body>{children}</body>
</html>
);
}
import Navbar from "@/components/Navbar/Navbar";
import Hero from "@/components/Hero/Hero";
import About from "@/components/About/About";
import Projects from "@/components/Projects/Projects";
import Experience from "@/components/Experience/Experience";
import Contact from "@/components/Contact/Contact";
export default function Home() {
return (
<>
{/* Sticky Navbar */}
<Navbar />
{/* Main Sections */}
<main>
{/* Hero Section */}
<Hero />
{/* About Section */}
<About />
{/* Projects Section */}
<Projects />
{/* Experience Section */}
<Experience />
{/* Contact Section */}
<Contact />
</main>
{/* Footer */}
<footer className="border-t border-nav-border bg-nav-bg/90 py-8 px-6 text-center text-sm text-slate-500">
<div className="max-w-[1200px] mx-auto flex justify-between items-center flex-wrap gap-6">
<div>
&copy; {new Date().getFullYear()} Bui Minh Quan. All rights reserved.
</div>
<div className="flex gap-6">
<a href="#home" className="text-slate-500 hover:text-white transition-colors duration-300">Back to top</a>
<span>&bull;</span>
<span>Made with Next.js &amp; Tailwind CSS</span>
</div>
</div>
</footer>
</>
);
}
"use client";
const SKILL_CATEGORIES = [
{
title: "Frontend Skills",
icon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
<polyline points="2 17 12 22 22 17"></polyline>
<polyline points="2 12 12 17 22 12"></polyline>
</svg>
),
skills: ["React.js", "Next.js", "JavaScript", "TypeScript", "HTML", "CSS", "Tailwind CSS"],
},
{
title: "Backend & Databases",
icon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
<line x1="6" y1="6" x2="6.01" y2="6"></line>
<line x1="6" y1="18" x2="6.01" y2="18"></line>
</svg>
),
skills: ["Java", "Spring Boot", "Spring Security", "Spring Data JPA", "Hibernate", "MySQL", "MongoDB"],
},
{
title: "Tools, DevOps & Concepts",
icon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
),
skills: ["Git", "Docker", "Postman", "Swagger", "RESTful APIs", "OOP", "JWT Authentication", "Layered Architecture"],
},
];
export default function About() {
return (
<section id="about" className="py-32 px-6 max-w-[1200px] mx-auto relative">
{/* Header */}
<div className="mb-14">
<span className="text-xs font-semibold uppercase text-primary tracking-widest block">My Profile</span>
<h2 className="text-3xl sm:text-4xl font-extrabold mt-2 text-white tracking-tight">About Me</h2>
</div>
{/* Grid Content */}
<div className="grid grid-cols-1 lg:grid-cols-[1.1fr_0.9fr] gap-16 items-start">
{/* Left Column: Biography & Stats */}
<div className="flex flex-col gap-6">
<p className="text-base leading-relaxed text-slate-400">
I am a final-year Information Technology student at Posts and Telecommunications Institute of Technology (PTIT HCMC), majoring in Software Engineering. My passion lies in Full-Stack Web Development, where I enjoy bridging the gap between secure, scalable backend services and responsive, polished frontend layouts.
</p>
<p className="text-base leading-relaxed text-slate-400">
Through my academic curriculum and dedicated personal projects, I have gained hands-on experience building RESTful APIs using Java and Spring Boot, securing endpoints with Spring Security and JWT, and constructing interactive user interfaces with React and Next.js. I am looking for a Full-Stack Developer Internship to apply my skills, learn from experienced mentors, and contribute to real-world software products.
</p>
{/* Academic Stats Cards */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6 mt-6">
<div className="bg-[#111827] border border-white/5 p-6 rounded-2xl shadow-sm">
<div className="text-4xl font-extrabold text-white font-outfit">3.42</div>
<div className="text-[10px] text-slate-500 font-semibold uppercase tracking-wider mt-2">Cumulative GPA</div>
</div>
<div className="bg-[#111827] border border-white/5 p-6 rounded-2xl shadow-sm">
<div className="text-4xl font-extrabold text-white font-outfit">2027</div>
<div className="text-[10px] text-slate-500 font-semibold uppercase tracking-wider mt-2">Graduation Year</div>
</div>
<div className="bg-[#111827] border border-white/5 p-6 rounded-2xl shadow-sm">
<div className="text-4xl font-extrabold text-white font-outfit">800</div>
<div className="text-[10px] text-slate-500 font-semibold uppercase tracking-wider mt-2">TOEIC English</div>
</div>
</div>
</div>
{/* Right Column: Skills */}
<div className="flex flex-col gap-6">
{SKILL_CATEGORIES.map((category, index) => (
<div key={index} className="bg-[#111827] border border-white/5 p-6 rounded-2xl shadow-sm">
<h3 className="text-base font-bold mb-5 flex items-center gap-2.5 text-white">
<span className="flex items-center text-primary">{category.icon}</span>
{category.title}
</h3>
<div className="flex flex-wrap gap-2">
{category.skills.map((skill, sIdx) => (
<span key={sIdx} className="bg-white/3 border border-white/5 px-3 py-1.5 rounded-lg text-xs sm:text-sm font-medium text-slate-300 transition-all hover:bg-primary/5 hover:border-primary hover:text-white">
{skill}
</span>
))}
</div>
</div>
))}
</div>
</div>
</section>
);
}
This diff is collapsed.
"use client";
interface TimelineItem {
id: string;
role: string;
company: string;
period: string;
description: string;
tag: string;
}
const TIMELINE_DATA: TimelineItem[] = [
{
id: "edu1",
role: "Major in Software Engineering",
company: "Posts and Telecommunications Institute of Technology, Ho Chi Minh City (PTIT HCMC)",
period: "2022 - 2027",
description: "Currently pursuing a Bachelor's degree in Software Engineering. Maintaining a cumulative GPA of 3.42/4.0. Core courses include Object-Oriented Programming (OOP), Data Structures & Algorithms, Software Architecture, and Database Management Systems.",
tag: "Education",
},
{
id: "proj1",
role: "EzBuy Platform Development",
company: "Java Spring Boot E-Commerce Project",
period: "09/2025 - 03/2026",
description: "Engineered a layered architecture backend with Java 21, Spring Boot 3, and MySQL. Secured user sessions using Spring Security and JWT, customized query logic using JPA Specifications, and containerized with Docker.",
tag: "Project",
},
{
id: "proj2",
role: "GreenMart Platform Development",
company: "React & Redux E-Commerce Frontend Project",
period: "02/2025 - 07/2025",
description: "Built a responsive client application using React, Redux, Bootstrap 5, and Axios. Designed an advanced interceptor system for JWT/Google OAuth token refreshes and optimized responsiveness with Optimistic UI.",
tag: "Project",
},
];
export default function Experience() {
return (
<section id="experience" className="py-32 px-6 max-w-[1200px] mx-auto relative">
{/* Header */}
<div className="mb-16 text-center">
<span className="text-xs font-semibold uppercase text-primary tracking-widest block">Timeline</span>
<h2 className="text-3xl sm:text-4xl font-extrabold mt-2 text-white tracking-tight">Education &amp; Projects</h2>
</div>
{/* Timeline Wrapper */}
<div className="relative max-w-[840px] mx-auto py-4">
{/* Central Vertical Line (Git style) */}
<div className="absolute top-0 bottom-0 left-5 md:left-1/2 w-[1px] bg-slate-800 -translate-x-1/2"></div>
{TIMELINE_DATA.map((item, index) => {
const isLeft = index % 2 === 0;
return (
<div
key={item.id}
className={`relative w-full md:w-1/2 py-6 md:px-10 px-8 box-border group ${
isLeft
? "md:left-0 md:text-right md:pr-10 md:pl-4 left-0 text-left pl-10 pr-4"
: "md:left-1/2 md:text-left md:pl-10 md:pr-4 left-0 text-left pl-10 pr-4"
}`}
>
{/* Timeline dot */}
<div
className={`absolute top-9 w-3 h-3 rounded-full bg-dark-bg border-2 border-primary z-10 transition-all duration-350 group-hover:scale-110 group-hover:bg-primary ${
isLeft
? "md:-right-1.5 md:left-auto left-[14px]"
: "md:-left-1.5 left-[14px]"
}`}
></div>
{/* Minimal Card Container */}
<div className="bg-[#111827] border border-white/5 p-7 rounded-2xl shadow-sm hover:border-white/10 transition-colors text-left">
<span className="text-[10px] font-semibold uppercase tracking-wider text-primary mb-3 inline-block bg-primary/10 px-2.5 py-1 rounded-full border border-primary/20">
{item.period}
</span>
<h3 className="text-lg font-bold text-white mb-1 tracking-tight">{item.role}</h3>
<span className="text-xs font-semibold text-slate-400 mb-4 block">{item.company}</span>
<p className="text-sm text-slate-400 leading-relaxed">{item.description}</p>
</div>
</div>
);
})}
</div>
</section>
);
}
"use client";
export default function Hero() {
const handleScrollTo = (e: React.MouseEvent<HTMLAnchorElement>, id: string) => {
e.preventDefault();
const target = document.getElementById(id);
if (target) {
const headerOffset = 80;
const elementPosition = target.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
window.scrollTo({
top: offsetPosition,
behavior: "smooth",
});
}
};
return (
<section id="home" className="min-h-[90vh] grid grid-cols-1 lg:grid-cols-[1.1fr_0.9fr] items-center gap-16 pt-[140px] pb-[80px] px-6 max-w-[1200px] mx-auto relative">
{/* Subtle background glow */}
<div
className="absolute top-[10%] -left-[10%] w-[380px] h-[380px] rounded-full filter blur-[70px] pointer-events-none opacity-20"
style={{ backgroundImage: "radial-gradient(circle, var(--color-primary) 0%, transparent 70%)" }}
></div>
<div
className="absolute bottom-[20%] -right-[10%] w-[450px] h-[450px] rounded-full filter blur-[80px] pointer-events-none opacity-15"
style={{ backgroundImage: "radial-gradient(circle, var(--color-secondary) 0%, transparent 70%)" }}
></div>
{/* Hero Text content */}
<div className="flex flex-col items-start text-left z-10 max-w-[600px] mx-auto lg:mx-0 opacity-0 animate-fade-in">
<div className="text-xs uppercase tracking-widest font-semibold text-primary mb-4 flex items-center gap-2">
<span className="inline-block w-1.5 h-1.5 bg-primary rounded-full shadow-[0_0_6px_var(--color-primary)]"></span>
Aspiring Intern
</div>
<h1 className="text-5xl sm:text-7xl font-extrabold mb-6 leading-[1.05] tracking-tighter text-white">
Hi, I am <span className="bg-gradient-to-r from-white to-slate-300 bg-clip-text text-transparent">Bui Minh Quan</span>
<span className="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent block">Full-Stack Developer</span>
</h1>
<p className="text-base sm:text-lg text-slate-400 mb-8 leading-relaxed max-w-[500px]">
Information Technology student at PTIT HCMC specializing in Software Engineering.
Experienced in building RESTful APIs and modern web applications with Spring Boot, React, and Next.js.
</p>
<div className="flex flex-wrap gap-4 w-full">
<a
href="#projects"
onClick={(e) => handleScrollTo(e, "projects")}
className="bg-primary hover:bg-blue-600 text-white px-7 py-3 rounded-full font-semibold text-[15px] shadow-sm hover:-translate-y-0.5 transition-all duration-200 flex items-center gap-2 cursor-pointer"
>
My Projects
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
</a>
<a
href="#contact"
onClick={(e) => handleScrollTo(e, "contact")}
className="bg-[#111827] hover:bg-[#1f2937] text-white border border-white/5 px-7 py-3 rounded-full font-semibold text-[15px] hover:-translate-y-0.5 transition-all duration-200 cursor-pointer"
>
Contact Info
</a>
</div>
</div>
{/* Hero Visual Column */}
<div className="relative flex justify-center items-center z-5">
<div className="relative w-[300px] h-[300px] sm:w-[380px] sm:h-[380px] flex justify-center items-center">
{/* Subtle morphing background gradient blob */}
<div className="absolute w-[260px] h-[260px] sm:w-[340px] sm:h-[340px] bg-gradient-to-br from-primary/15 to-secondary/5 rounded-full filter blur-[2px] opacity-80 animate-morph"></div>
{/* Floating Dark IDE Code Card */}
<div className="absolute bg-[#111827] border border-[#1f2937] p-5 rounded-2xl shadow-[0_20px_50px_rgba(0,0,0,0.5)] animate-float flex flex-col gap-3.5 w-[230px] sm:w-[270px] left-0 top-8">
<div className="flex items-center justify-between border-b border-white/5 pb-3">
<div className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-red-500/40"></span>
<span className="w-2.5 h-2.5 rounded-full bg-yellow-500/40"></span>
<span className="w-2.5 h-2.5 rounded-full bg-green-500/40"></span>
</div>
<span className="text-[10px] text-slate-500 font-mono">quan.json</span>
</div>
<div className="flex flex-col gap-1.5 font-mono text-[10px] sm:text-[11px] leading-relaxed text-slate-400">
<div>
<span className="text-primary">const</span> <span className="text-white">intern</span> = &#123;
</div>
<div className="pl-4">
name: <span className="text-emerald-400">&quot;Bui Minh Quan&quot;</span>,
</div>
<div className="pl-4">
gpa: <span className="text-secondary">3.42</span>,
</div>
<div className="pl-4">
stack: <span className="text-emerald-400">&quot;Spring Boot &amp; React&quot;</span>
</div>
<div>&#125;;</div>
</div>
</div>
{/* Floating Skill Badge */}
<div className="absolute bg-[#111827] border border-[#1f2937] px-4 py-2 rounded-full text-xs font-semibold text-slate-300 flex items-center gap-2 bottom-8 right-0 sm:right-4 animate-float" style={{ animationDelay: "1s" }}>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-primary"
>
<polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
<polyline points="2 17 12 22 22 17"></polyline>
<polyline points="2 12 12 17 22 12"></polyline>
</svg>
<span>Software Engineering</span>
</div>
</div>
</div>
</section>
);
}
"use client";
import { useState, useEffect } from "react";
const NAV_ITEMS = [
{ id: "home", label: "Home" },
{ id: "about", label: "About" },
{ id: "projects", label: "Projects" },
{ id: "experience", label: "Education" },
{ id: "contact", label: "Contact" },
];
export default function Navbar() {
const [scrolled, setScrolled] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [activeSection, setActiveSection] = useState("home");
useEffect(() => {
const handleScroll = () => {
if (window.scrollY > 30) {
setScrolled(true);
} else {
setScrolled(false);
}
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
useEffect(() => {
const sections = NAV_ITEMS.map((item) => document.getElementById(item.id));
const observerOptions = {
root: null,
rootMargin: "-30% 0px -50% 0px",
threshold: 0,
};
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveSection(entry.target.id);
}
});
}, observerOptions);
sections.forEach((section) => {
if (section) observer.observe(section);
});
return () => {
sections.forEach((section) => {
if (section) observer.unobserve(section);
});
};
}, []);
const handleLinkClick = (e: React.MouseEvent<HTMLAnchorElement>, id: string) => {
e.preventDefault();
setMobileMenuOpen(false);
const target = document.getElementById(id);
if (target) {
const headerOffset = 80;
const elementPosition = target.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
window.scrollTo({
top: offsetPosition,
behavior: "smooth",
});
}
};
return (
<header className={`fixed top-0 left-0 w-full z-50 transition-all duration-300 border-b ${
scrolled
? "py-3 bg-dark-bg/75 backdrop-blur-md border-white/5 shadow-md"
: "py-6 bg-transparent border-transparent"
}`}>
<div className="max-w-[1200px] mx-auto px-6 flex justify-between items-center">
{/* Logo */}
<a href="#home" onClick={(e) => handleLinkClick(e, "home")} className="font-outfit font-bold text-lg text-white flex items-center gap-1.5 tracking-tight">
MinhQuan<span className="inline-block w-1.5 h-1.5 bg-primary rounded-full shadow-[0_0_6px_rgba(59,130,246,0.6)]"></span>
</a>
{/* Desktop Navigation */}
<nav className="hidden md:block">
<ul className="flex items-center gap-8 list-none">
{NAV_ITEMS.map((item) => (
<li key={item.id}>
<a
href={`#${item.id}`}
onClick={(e) => handleLinkClick(e, item.id)}
className={`font-inter text-xs uppercase tracking-wider font-semibold relative py-1 transition-colors duration-200 after:content-[''] after:absolute after:bottom-0 after:left-0 after:w-0 after:h-[1px] after:bg-primary after:transition-all after:duration-250 hover:after:w-full ${
activeSection === item.id
? "text-white after:w-full"
: "text-slate-400 hover:text-white"
}`}
>
{item.label}
</a>
</li>
))}
</ul>
</nav>
{/* CTA Button */}
<div className="hidden md:block">
<a
href="#contact"
onClick={(e) => handleLinkClick(e, "contact")}
className="border border-white/10 text-white px-5 py-2 rounded-full font-medium text-xs tracking-wide hover:border-primary hover:bg-primary/5 transition-all duration-200"
>
Hire Me
</a>
</div>
{/* Mobile Hamburger Toggle */}
<button
className="md:hidden text-slate-400 hover:text-white focus:outline-none z-50 p-1"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Toggle menu"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth="2.5">
{mobileMenuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
{/* Mobile Menu Dropdown */}
<div
className={`fixed top-0 right-0 w-[75%] max-w-[320px] h-screen bg-[#0e1322] border-l border-white/5 p-8 pt-24 flex flex-col gap-6 z-40 transition-transform duration-300 ease-in-out ${
mobileMenuOpen ? "translate-x-0" : "translate-x-full"
}`}
>
<ul className="list-none flex flex-col gap-4">
{NAV_ITEMS.map((item) => (
<li key={item.id}>
<a
href={`#${item.id}`}
onClick={(e) => handleLinkClick(e, item.id)}
className={`font-inter text-sm uppercase tracking-wider font-semibold block py-2 transition-colors ${
activeSection === item.id
? "text-primary"
: "text-slate-400 hover:text-white"
}`}
>
{item.label}
</a>
</li>
))}
</ul>
<a
href="#contact"
onClick={(e) => handleLinkClick(e, "contact")}
className="border border-white/10 text-white py-2.5 rounded-full font-medium text-xs text-center hover:border-primary hover:bg-primary/5 transition-all mt-4"
>
Hire Me
</a>
</div>
</div>
</header>
);
}
"use client";
import { useEffect, ReactNode } from "react";
interface Project {
id: string;
title: string;
description: string;
category: string;
tags: string[];
features: string[];
demoUrl: string;
githubUrl: string;
svgHeader: ReactNode;
}
interface ProjectModalProps {
project: Project | null;
onClose: () => void;
}
export default function ProjectModal({ project, onClose }: ProjectModalProps) {
useEffect(() => {
if (project) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "unset";
}
return () => {
document.body.style.overflow = "unset";
};
}, [project]);
if (!project) return null;
const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) {
onClose();
}
};
return (
<div
className="fixed top-0 left-0 w-full h-full bg-[#05070c]/85 backdrop-blur-sm z-[999] flex justify-center items-center p-5 animate-fade-in"
onClick={handleOverlayClick}
>
<div className="bg-[#111827] border border-[#1f2937] w-full max-w-[760px] max-h-[85vh] overflow-y-auto relative rounded-2xl shadow-2xl">
{/* Close Button */}
<button
className="absolute top-4 right-4 bg-[#111827] border border-white/5 rounded-full w-9 h-9 flex items-center justify-center text-slate-400 hover:text-white transition-all z-10 cursor-pointer hover:scale-105"
onClick={onClose}
aria-label="Close modal"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
{/* Visual Mock Header */}
<div className="h-[180px] sm:h-[260px] w-full relative bg-slate-900 border-b border-white/5 overflow-hidden">
{project.svgHeader}
</div>
{/* Content */}
<div className="p-6 sm:p-10">
<h3 className="text-2xl font-extrabold mb-4 text-white tracking-tight">{project.title}</h3>
<p className="text-sm sm:text-[15px] leading-relaxed text-slate-400 mb-8">{project.description}</p>
{/* Key Features */}
<h4 className="text-base font-bold mb-4 text-white flex items-center gap-2">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" className="text-primary">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
Key Features
</h4>
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-8 list-none pl-0">
{project.features.map((feature, index) => (
<li key={index} className="flex items-start gap-2.5 text-xs sm:text-sm text-slate-300">
<svg className="text-primary flex-shrink-0 mt-0.5" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
{feature}
</li>
))}
</ul>
{/* Technologies */}
<div className="mb-9">
<h4 className="text-base font-bold mb-4 text-white">Technologies Used</h4>
<div className="flex flex-wrap gap-2">
{project.tags.map((tag, index) => (
<span key={index} className="bg-white/3 border border-white/5 text-slate-300 px-3 py-1.5 rounded-lg text-xs font-semibold">
{tag}
</span>
))}
</div>
</div>
{/* Action Footer */}
<div className="flex flex-col sm:flex-row justify-end gap-3.5 border-t border-white/5 pt-6">
<a
href={project.githubUrl}
target="_blank"
rel="noopener noreferrer"
className="px-5 py-2.5 rounded-full font-semibold text-xs tracking-wide flex items-center justify-center gap-2 cursor-pointer transition-all w-full sm:w-auto bg-white/3 border border-white/5 text-white hover:bg-white/8 hover:-translate-y-0.5"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
</svg>
GitHub Code
</a>
{project.demoUrl && (
<a
href={project.demoUrl}
target="_blank"
rel="noopener noreferrer"
className="px-5 py-2.5 rounded-full font-semibold text-xs tracking-wide flex items-center justify-center gap-2 cursor-pointer transition-all w-full sm:w-auto bg-primary text-white hover:bg-blue-600 hover:-translate-y-0.5 shadow-sm"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
Live Project
</a>
)}
</div>
</div>
</div>
</div>
);
}
"use client";
import { useState } from "react";
import ProjectModal from "./ProjectModal";
interface Project {
id: string;
title: string;
description: string;
category: string;
tags: string[];
features: string[];
demoUrl: string;
githubUrl: string;
svgHeader: React.ReactNode;
}
const PROJECTS_DATA: Project[] = [
{
id: "ezbuy",
title: "EzBuy - E-commerce Platform Backend",
description: "A highly scalable and secure e-commerce platform backend constructed following Layered Architecture principles. It supports user accounts, catalog search, shopping cart management, and order checkout pipelines.",
category: "backend",
tags: ["Java 21", "Spring Boot 3", "MySQL", "Spring Security", "JWT", "Spring Data JPA", "Hibernate", "Docker"],
features: [
"Engineered an e-commerce backend platform following Layered Architecture principles",
"Implemented secure authentication and authorization using Spring Security, JWT Access/Refresh Tokens, and Role-Based Access Control (RBAC)",
"Developed dynamic product filtering and search using JPA Specifications (supporting keyword, category, and manufacturer queries)",
"Integrated external Cloudinary cloud API for media storage and image assets handling",
"Utilized MapStruct for DTO mapping and containerized the application stack using Docker and Docker Compose"
],
demoUrl: "",
githubUrl: "https://github.com/BMQuaan/ezbuy-backend",
svgHeader: (
<svg viewBox="0 0 400 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="400" height="200" fill="#0f172a" />
<rect x="25" y="25" width="350" height="150" rx="6" fill="#020617" stroke="#1e293b" />
<circle cx="45" cy="40" r="4" fill="#ef4444" opacity="0.5" />
<circle cx="57" cy="40" r="4" fill="#eab308" opacity="0.5" />
<circle cx="69" cy="40" r="4" fill="#22c55e" opacity="0.5" />
<text x="45" y="70" fill="#475569" fontFamily="monospace" fontSize="10">POST /api/v1/auth/login - 200 OK - 84ms</text>
<text x="45" y="90" fill="#3b82f6" fontFamily="monospace" fontSize="10">[Spring Security] JWT Access/Refresh Token generated</text>
<text x="45" y="110" fill="#475569" fontFamily="monospace" fontSize="10">GET /api/v1/products?category=electronics - 200 OK - 15ms</text>
<text x="45" y="130" fill="#8b5cf6" fontFamily="monospace" fontSize="10">[JPA Specification] Dynamic query compiled successfully</text>
<text x="45" y="150" fill="#3b82f6" fontFamily="monospace" fontSize="10">[Docker] Container ezbuy-backend-db status: RUNNING</text>
</svg>
)
},
{
id: "greenmart",
title: "GreenMart - E-commerce Frontend",
description: "A highly responsive e-commerce web interface. Features secure authentication flows, product catalog browsing, wishlist toggles, checkout forms, and user orders tracker.",
category: "frontend",
tags: ["React", "Redux", "React Router", "Axios", "Bootstrap 5", "Google OAuth", "SweetAlert2"],
features: [
"Developed a fully responsive e-commerce UI supporting product lists, cart additions, and checkout workflows",
"Implemented secure auth flows using JWT, Google OAuth 2.0, and advanced Axios Interceptors with automated token refreshes and request queues",
"Applied Optimistic UI updates for shopping cart and wishlist actions, optimizing user experience",
"Managed global state using Redux to support scalable state management and session persistence across reloads",
"Designed reusable components and responsive grid structures using Bootstrap 5 and custom media queries"
],
demoUrl: "",
githubUrl: "https://github.com/BMQuaan/greenMart",
svgHeader: (
<svg viewBox="0 0 400 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="400" height="200" fill="#090d16" />
<rect x="20" y="20" width="110" height="160" rx="8" fill="rgba(255, 255, 255, 0.01)" stroke="#1e293b" />
<rect x="145" y="20" width="110" height="160" rx="8" fill="rgba(255, 255, 255, 0.01)" stroke="#1e293b" />
<rect x="270" y="20" width="110" height="160" rx="8" fill="rgba(255, 255, 255, 0.01)" stroke="#1e293b" />
<rect x="35" y="30" width="40" height="8" rx="2" fill="#3b82f6" />
<rect x="160" y="30" width="40" height="8" rx="2" fill="#8b5cf6" />
<rect x="285" y="30" width="40" height="8" rx="2" fill="#3b82f6" />
<rect x="30" y="50" width="90" height="35" rx="6" fill="#111827" stroke="#1f2937" />
<rect x="40" y="58" width="50" height="5" rx="2" fill="#475569" />
<circle cx="105" cy="62" r="4" fill="#3b82f6" />
<rect x="155" y="50" width="90" height="45" rx="6" fill="#111827" stroke="#1f2937" />
<rect x="165" y="58" width="60" height="5" rx="2" fill="#475569" />
<rect x="165" y="70" width="30" height="5" rx="2" fill="#8b5cf6" />
<circle cx="230" cy="62" r="4" fill="#8b5cf6" />
<rect x="155" y="105" width="90" height="35" rx="6" fill="#111827" stroke="#1f2937" />
<rect x="165" y="113" width="40" height="5" rx="2" fill="#475569" />
</svg>
)
}
];
export default function Projects() {
const [filter, setFilter] = useState("all");
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
const filteredProjects = PROJECTS_DATA.filter((project) => {
if (filter === "all") return true;
return project.category === filter;
});
return (
<section id="projects" className="py-32 px-6 max-w-[1200px] mx-auto relative">
{/* Header */}
<div className="mb-14 text-center">
<span className="text-xs font-semibold uppercase text-primary tracking-widest block">My Work</span>
<h2 className="text-3xl sm:text-4xl font-extrabold mt-2 text-white tracking-tight">Projects Showcase</h2>
</div>
{/* Filter Tabs */}
<div className="flex justify-center gap-3 mb-12 flex-wrap">
{["all", "frontend", "backend"].map((category) => (
<button
key={category}
onClick={() => setFilter(category)}
className={`px-5 py-2 rounded-full font-inter font-semibold text-xs tracking-wider uppercase transition-all duration-200 cursor-pointer ${
filter === category
? "bg-primary text-white border border-primary shadow-sm"
: "bg-[#111827] border border-white/5 text-slate-400 hover:text-white"
}`}
>
{category}
</button>
))}
</div>
{/* Projects Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-[960px] mx-auto">
{filteredProjects.map((project) => (
<div
key={project.id}
onClick={() => setSelectedProject(project)}
className="bg-[#111827] border border-white/5 flex flex-col h-full overflow-hidden cursor-pointer rounded-2xl transition-all duration-300 hover:border-primary/30 hover:-translate-y-1 hover:shadow-lg group"
>
<div className="h-[200px] w-full overflow-hidden relative bg-slate-900 border-b border-white/5">
<div className="w-full h-full transition-transform duration-350 group-hover:scale-102">
{project.svgHeader}
</div>
</div>
<div className="p-7 flex flex-col flex-1">
<h3 className="text-lg font-bold mb-2.5 text-white tracking-tight">{project.title}</h3>
<p className="text-sm leading-relaxed text-slate-400 mb-6 flex-1">
{project.description}
</p>
<div className="flex flex-wrap gap-2 mb-6">
{project.tags.slice(0, 3).map((tag, idx) => (
<span key={idx} className="bg-white/4 border border-white/5 px-2.5 py-1 rounded-md text-[11px] font-semibold text-primary">
{tag}
</span>
))}
{project.tags.length > 3 && (
<span className="bg-white/4 border border-white/5 px-2.5 py-1 rounded-md text-[11px] font-semibold text-slate-500">+{project.tags.length - 3} more</span>
)}
</div>
<div className="flex justify-between items-center border-t border-white/5 pt-5">
<span className="text-xs font-semibold uppercase tracking-wider text-primary flex items-center gap-1.5 transition-all group-hover:text-white">
Project Details
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<line x1="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
</span>
<div className="flex gap-3" onClick={(e) => e.stopPropagation()}>
<a href={project.githubUrl} target="_blank" rel="noopener noreferrer" className="text-slate-500 hover:text-white transition-colors duration-200" aria-label="Github code">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
</svg>
</a>
</div>
</div>
</div>
</div>
))}
</div>
{/* Modal Dialog */}
<ProjectModal
project={selectedProject}
onClose={() => setSelectedProject(null)}
/>
</section>
);
}
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment