Skip to content

Commit 85694e9

Browse files
authored
Develop (#19)
* feat: add new project "Learn Algo" to portfolio with description and tags * feat: add image and GitHub link to "Learn Algo" project in portfolio * fix: update project status to indicate it is no longer under development * refactor: format portfolio project descriptions and tags for consistency * Tic tac toe (#15) * Develop (#14) * feat: add new project "Learn Algo" to portfolio with description and tags * feat: add image and GitHub link to "Learn Algo" project in portfolio * fix: update project status to indicate it is no longer under development * refactor: format portfolio project descriptions and tags for consistency * tic-tac-toe project added to portfolio * style: enhance body background with geometric patterns and gradients * feat: add BackgroundBeams and CurvedLoop components to enhance portfolio page visuals * feat: add CircularText component for animated circular text display refactor: remove unused PORTFOLIO_INFO import and related bindings in Header component chore: update portfolioData to support multiple avatar images and add hero summary style: enhance index.css with translucent background utilities and panel styles fix: adjust PortfolioPage layout and spacing for better responsiveness refactor: simplify types in portfolio.ts for better clarity and maintainability build: improve Vite config with manual chunking for better bundle optimization * feat: implement CardContainer component for enhanced project card interactions and animations refactor: update ProjectCard to utilize CardContainer for improved structure refactor: adjust ProjectsGrid layout for better card display feat: add MouseEnter context for managing mouse interactions style: simplify background styles in index.css for a cleaner look refactor: streamline PortfolioPage structure for better readability
1 parent 2379c3f commit 85694e9

6 files changed

Lines changed: 218 additions & 50 deletions

File tree

src/components/CardContainer.tsx

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
"use client"
2+
3+
import React, { useEffect, useRef, useState, useMemo } from "react"
4+
import { cn } from "../lib/utils"
5+
import { MouseEnterContext, useMouseEnter } from "./hooks/mouseEnter"
6+
7+
export interface CardContainerProps {
8+
children?: React.ReactNode
9+
className?: string
10+
containerClassName?: string
11+
href?: string
12+
onActivate?: () => void
13+
}
14+
15+
export const CardContainer = ({
16+
children,
17+
className,
18+
containerClassName,
19+
href,
20+
onActivate,
21+
}: CardContainerProps) => {
22+
const containerRef = useRef<HTMLDivElement>(null)
23+
const [isMouseEntered, setIsMouseEntered] = useState(false)
24+
25+
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
26+
if (!containerRef.current) {
27+
return
28+
}
29+
const { left, top, width, height } = containerRef.current.getBoundingClientRect()
30+
const x = (e.clientX - left - width / 2) / 25
31+
const y = (e.clientY - top - height / 2) / 25
32+
containerRef.current.style.transform = `rotateY(${x}deg) rotateX(${y}deg)`
33+
}
34+
35+
const handleMouseEnter = () => {
36+
setIsMouseEntered(true)
37+
}
38+
39+
const handleMouseLeave = () => {
40+
setIsMouseEntered(false)
41+
if (containerRef.current) {
42+
containerRef.current.style.transform = "rotateY(0deg) rotateX(0deg)"
43+
}
44+
}
45+
46+
const handleClick = () => {
47+
if (onActivate) {
48+
onActivate()
49+
return
50+
}
51+
if (href) {
52+
window.open(href, "_blank")
53+
}
54+
}
55+
56+
const contextValue = useMemo(() => [isMouseEntered, setIsMouseEntered] as const, [isMouseEntered])
57+
58+
return (
59+
<MouseEnterContext.Provider value={contextValue}>
60+
<div
61+
className={cn("flex items-center justify-center", containerClassName)}
62+
style={{
63+
perspective: "1000px",
64+
}}
65+
>
66+
<div
67+
className={cn(
68+
"flex items-center justify-center relative transition-all duration-200 ease-linear",
69+
className,
70+
)}
71+
tabIndex={0}
72+
onMouseEnter={handleMouseEnter}
73+
onMouseLeave={handleMouseLeave}
74+
onMouseMove={handleMouseMove}
75+
onClick={handleClick}
76+
onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => {
77+
// Activate on Enter or Space for accessibility
78+
if (e.key === "Enter" || e.key === " ") {
79+
e.preventDefault()
80+
handleClick()
81+
}
82+
}}
83+
ref={containerRef}
84+
style={{
85+
transformStyle: "preserve-3d",
86+
}}
87+
>
88+
{children}
89+
</div>
90+
</div>
91+
</MouseEnterContext.Provider>
92+
)
93+
}
94+
95+
export interface CardBodyProps {
96+
children: React.ReactNode
97+
className?: string
98+
}
99+
100+
export const CardBody = ({ children, className }: CardBodyProps) => {
101+
return (
102+
<div
103+
className={cn(
104+
"h-96 w-96 [transform-style:preserve-3d] [&>*]:[transform-style:preserve-3d]",
105+
className,
106+
)}
107+
>
108+
{children}
109+
</div>
110+
)
111+
}
112+
113+
export type CardItemProps = {
114+
as?: React.ElementType
115+
children: React.ReactNode
116+
className?: string
117+
translateX?: number | string
118+
translateY?: number | string
119+
translateZ?: number | string
120+
rotateX?: number | string
121+
rotateY?: number | string
122+
rotateZ?: number | string
123+
} & Record<string, unknown>
124+
125+
export const CardItem = ({
126+
as: Tag = "div",
127+
children,
128+
className,
129+
translateX = 0,
130+
translateY = 0,
131+
translateZ = 0,
132+
rotateX = 0,
133+
rotateY = 0,
134+
rotateZ = 0,
135+
...rest
136+
}: CardItemProps) => {
137+
const ref = useRef<HTMLElement | null>(null)
138+
const [isMouseEntered] = useMouseEnter()
139+
140+
useEffect(() => {
141+
handleAnimations()
142+
// eslint-disable-next-line react-hooks/exhaustive-deps
143+
}, [isMouseEntered])
144+
145+
const handleAnimations = () => {
146+
if (!ref.current) {
147+
return
148+
}
149+
const fmt = (v: number | string) => (typeof v === "number" ? `${v}px` : String(v ?? "0"))
150+
if (isMouseEntered) {
151+
ref.current.style.transform = `translateX(${fmt(translateX)}) translateY(${fmt(
152+
translateY,
153+
)}) translateZ(${fmt(translateZ)}) rotateX(${fmt(rotateX)}) rotateY(${fmt(rotateY)}) rotateZ(${fmt(
154+
rotateZ,
155+
)})`
156+
} else {
157+
ref.current.style.transform =
158+
"translateX(0px) translateY(0px) translateZ(0px) rotateX(0deg) rotateY(0deg) rotateZ(0deg)"
159+
}
160+
}
161+
162+
return React.createElement(
163+
Tag,
164+
{
165+
ref,
166+
className: cn("w-fit transition duration-200 ease-linear", className),
167+
...rest,
168+
},
169+
children,
170+
)
171+
}

src/components/ProjectCard.tsx

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { Project } from "../types/portfolio";
66
import * as SiIcons from "react-icons/si";
77
import * as FaIcons from "react-icons/fa";
88
import { BsArrowUpRightCircleFill } from "react-icons/bs";
9+
import {CardContainer} from "./CardContainer";
910

1011
export const ProjectCard: React.FC<{
1112
project: Project;
@@ -24,18 +25,26 @@ export const ProjectCard: React.FC<{
2425
const hiddenCount = (project?.tags?.length ?? 0) - VISIBLE_COUNT;
2526

2627
return (
27-
<motion.article
28-
layout
29-
whileHover={{ y: -6 }}
30-
className="p-4 group rounded-2xl bg-[var(--surface)] border border-[var(--border)] shadow-sm hover:shadow-md transition-shadow"
28+
<CardContainer
29+
href={project.href}
30+
onActivate={() => onOpen?.(project)}
31+
containerClassName="w-full h-full"
3132
>
32-
<div className="flex items-start justify-between gap-4">
33-
<div>
33+
<motion.article
34+
layout
35+
whileHover={{ y: -6 }}
36+
className="p-4 group rounded-2xl bg-[var(--surface)] border border-[var(--border)] shadow-sm hover:shadow-md transition-shadow h-full"
37+
>
38+
<div className="flex items-start justify-between gap-4">
39+
<div>
3440
{/* Arrow overlay */}
3541
<button
3642
title="Open project"
3743
type="button"
38-
onClick={() => onOpen?.(project)}
44+
onClick={(e) => {
45+
e.stopPropagation();
46+
onOpen?.(project);
47+
}}
3948
className="absolute top-2 right-2
4049
bg-[var(--surface)]/80 backdrop-blur-sm
4150
rounded-full shadow-md cursor-pointer
@@ -59,7 +68,10 @@ export const ProjectCard: React.FC<{
5968
<button
6069
title="Open project"
6170
type="button"
62-
onClick={() => onOpen?.(project)}
71+
onClick={(e) => {
72+
e.stopPropagation();
73+
onOpen?.(project);
74+
}}
6375
className="font-bold text-xl text-[var(--brand)] cursor-pointer"
6476
>
6577
{project.title}
@@ -130,6 +142,7 @@ export const ProjectCard: React.FC<{
130142
</div>
131143
</div>
132144
</div>
133-
</motion.article>
145+
</motion.article>
146+
</CardContainer>
134147
);
135148
};

src/components/ProjectsGrid.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,12 @@ export const ProjectsGrid: React.FC<{
4242
<div
4343
className={`${
4444
showFilters ? "mt-6" : ""
45-
} grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6`}
45+
} grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 auto-rows-fr`}
4646
>
4747
{visible.map((p) => (
48-
<ProjectCard key={p.id} project={p} onOpen={onOpen} />
48+
<div key={p.id} className="h-full">
49+
<ProjectCard project={p} onOpen={onOpen} />
50+
</div>
4951
))}
5052
</div>
5153
</section>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React, { createContext, useContext } from "react"
2+
3+
export const MouseEnterContext = createContext<
4+
readonly [boolean, React.Dispatch<React.SetStateAction<boolean>>] | undefined
5+
>(undefined)
6+
7+
export const useMouseEnter = () => {
8+
const context = useContext(MouseEnterContext)
9+
if (context === undefined) {
10+
throw new Error("useMouseEnter must be used within a MouseEnterProvider")
11+
}
12+
return context
13+
}
14+
15+
export default useMouseEnter

src/index.css

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -43,41 +43,12 @@ html, body {
4343

4444
/* Professional Geometric Background Pattern */
4545
body {
46+
/* Keep only subtle accent gradients; remove grid/dot patterns */
4647
background-image:
47-
/* Large accent gradient top-right */
4848
radial-gradient(circle at 80% 20%, rgba(124, 58, 237, 0.03) 0%, transparent 50%),
49-
/* Large accent gradient bottom-left */
50-
radial-gradient(circle at 20% 80%, rgba(236, 72, 153, 0.03) 0%, transparent 50%),
51-
/* Dot grid pattern */
52-
radial-gradient(circle at center, var(--border) 1px, transparent 1px),
53-
/* Grid lines - vertical */
54-
repeating-linear-gradient(
55-
90deg,
56-
transparent,
57-
transparent 59px,
58-
var(--border) 59px,
59-
var(--border) 60px
60-
),
61-
/* Grid lines - horizontal */
62-
repeating-linear-gradient(
63-
0deg,
64-
transparent,
65-
transparent 59px,
66-
var(--border) 59px,
67-
var(--border) 60px
68-
);
69-
background-size:
70-
100% 100%,
71-
100% 100%,
72-
20px 20px,
73-
60px 60px,
74-
60px 60px;
75-
background-position:
76-
0 0,
77-
0 0,
78-
0 0,
79-
0 0,
80-
0 0;
49+
radial-gradient(circle at 20% 80%, rgba(236, 72, 153, 0.03) 0%, transparent 50%);
50+
background-size: 100% 100%, 100% 100%;
51+
background-position: 0 0, 0 0;
8152
}
8253

8354
/* small utilities you can reuse (optional) */

src/pages/PortfolioPage.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,7 @@ const PortfolioPage: React.FC = () => {
5858

5959
<main className="max-w-6xl 2xl:max-w-9xl mx-auto px-6 py-15 sm:py-20 lg:py-32 relative z-20">
6060
<section id="projects" className="py-8">
61-
<h2 className="text-2xl font-semibold text-[var(--brand)]">
62-
Projects
63-
</h2>
61+
<h2 className="text-2xl font-semibold text-[var(--brand)]">Projects</h2>
6462
<p className="mb-6 text-sm text-gray-500 dark:text-gray-400 mt-1">
6563
Selected work — click a card for details.
6664
</p>
@@ -79,9 +77,7 @@ const PortfolioPage: React.FC = () => {
7977
</section>
8078

8179
<section id="contact" className="py-8">
82-
<h2 className="text-2xl font-semibold text-[var(--brand)]">
83-
Contact
84-
</h2>
80+
<h2 className="text-2xl font-semibold text-[var(--brand)]">Contact</h2>
8581
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
8682
Tell me about your project, or just say hi.
8783
</p>

0 commit comments

Comments
 (0)