Skip to content

Commit 592a786

Browse files
committed
feat(portfolio): refactor PortfolioItems to use PortfolioItem component for rendering
1 parent 91495cd commit 592a786

2 files changed

Lines changed: 171 additions & 200 deletions

File tree

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import Github from "~/components/icons/Github";
2+
import Next from "~/components/icons/Next";
3+
import { getTechBadgeStyle } from "~/components/portfolio/technologyColors";
4+
5+
function generateId(
6+
category: string,
7+
isFeatured: boolean,
8+
index: number
9+
): string {
10+
const categoryPrefix =
11+
category?.toUpperCase().replace(/-/g, "_") || "PROJECT";
12+
return `ID: ${categoryPrefix}_${isFeatured ? "1" : "0"}x${String(index + 1).padStart(2, "0")}`;
13+
}
14+
15+
const featuredCardClassName =
16+
"group relative flex flex-col overflow-hidden rounded-2xl border border-solid border-surface-container-low bg-surface-container-low transition-all duration-150 ease-out hover:border-primary/60 focus-within:border-primary/60 focus-within:shadow-floating active:scale-[0.995] md:flex-row";
17+
18+
const regularCardClassName =
19+
"group relative flex flex-col overflow-hidden rounded-2xl border border-solid border-surface-container-low bg-surface-container-low transition-all duration-150 ease-out hover:border-primary/60 focus-within:border-primary/60 focus-within:shadow-floating active:scale-[0.995]";
20+
21+
const cardPageLinkClassName =
22+
"absolute inset-0 z-10 rounded-2xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/70 focus-visible:ring-offset-2 focus-visible:ring-offset-background";
23+
24+
const titleClassName = "transition-colors group-hover:text-primary";
25+
26+
const descriptionClassName =
27+
"block text-base leading-relaxed text-on-surface-muted transition-colors group-hover:text-on-surface";
28+
29+
const viewActionClassName =
30+
"rounded bg-primary/10 px-4 py-2 font-semibold text-primary transition-all duration-150 ease-out group-hover:bg-primary/20";
31+
32+
const demoIconClassName =
33+
"rounded p-2 text-on-surface-muted transition-colors duration-150 ease-out group-hover:text-primary";
34+
35+
const githubActionClassName =
36+
"relative z-20 rounded p-2 text-on-surface-muted transition-all duration-150 ease-out hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/70 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-container-high active:scale-[0.95]";
37+
38+
export function PortfolioItem({
39+
item,
40+
variant = "regular", // "featured"
41+
index,
42+
}) {
43+
const isFeatured = variant === "featured";
44+
45+
const id = generateId(item.category || "", isFeatured, index);
46+
const description = item.whatIDid?.[0] || "";
47+
48+
return (
49+
<div className={isFeatured ? featuredCardClassName : regularCardClassName}>
50+
<a
51+
href={`/portfolio/${item.slug}`}
52+
className={cardPageLinkClassName}
53+
aria-label={item.title}
54+
/>
55+
56+
{/* Image */}
57+
<div
58+
className={`relative overflow-hidden bg-surface-container-lowest ${
59+
isFeatured
60+
? "h-80 md:h-auto md:w-2/5 md:flex-shrink-0"
61+
: "h-52 rounded-t-2xl"
62+
}`}
63+
>
64+
<img
65+
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
66+
loading="lazy"
67+
src={item.thumbnail}
68+
alt={item.title}
69+
/>
70+
</div>
71+
72+
{/* Content */}
73+
<div
74+
className={`flex flex-1 flex-col justify-between ${
75+
isFeatured ? "p-6 md:p-8" : ""
76+
}`}
77+
>
78+
{/* Header */}
79+
<div className={isFeatured ? "mb-4" : ""}>
80+
{/* ID badge (both variants, styled slightly differently) */}
81+
<div
82+
className={`flex items-center justify-between gap-2 ${
83+
isFeatured
84+
? "mb-4"
85+
: "px-4 py-3 text-xs font-mono text-on-surface-muted"
86+
}`}
87+
>
88+
<span className="rounded bg-primary/20 px-2 py-1 font-mono text-xs uppercase tracking-wider text-primary">
89+
{id}
90+
</span>
91+
</div>
92+
93+
<div className={isFeatured ? "" : "px-4"}>
94+
<h2
95+
className={`font-headline font-semibold text-on-surface ${
96+
isFeatured ? "mb-3 text-3xl" : "mb-3 text-xl"
97+
}`}
98+
>
99+
<span className={titleClassName}>{item.title}</span>
100+
</h2>
101+
102+
{description && (
103+
<span
104+
className={`${descriptionClassName} ${
105+
isFeatured ? "" : "line-clamp-2 text-sm"
106+
}`}
107+
>
108+
{description}
109+
</span>
110+
)}
111+
</div>
112+
</div>
113+
114+
{/* Technologies */}
115+
{item.technologiesUsed?.length > 0 && (
116+
<div
117+
className={`flex flex-wrap gap-2 mt-4 mb-4 ${!isFeatured ? "px-4" : ""}`}
118+
>
119+
{item.technologiesUsed.map((tech, idx) => (
120+
<span
121+
key={idx}
122+
className="rounded border px-2 py-1 font-mono text-xs uppercase tracking-wider"
123+
style={getTechBadgeStyle(tech.type, idx)}
124+
>
125+
{tech.type}
126+
</span>
127+
))}
128+
</div>
129+
)}
130+
131+
{/* Footer */}
132+
<div
133+
className={`flex items-center gap-3 ${
134+
isFeatured ? "" : "border-t border-outline-variant px-4 py-3"
135+
}`}
136+
>
137+
<span
138+
className={`${viewActionClassName} ${
139+
isFeatured ? "" : "flex-1 px-3 text-center text-sm"
140+
}`}
141+
>
142+
View
143+
</span>
144+
145+
{item.demoUrl && (
146+
<span className={demoIconClassName} aria-hidden="true">
147+
<Next className={isFeatured ? "w-6 h-6" : "w-5 h-5"} />
148+
</span>
149+
)}
150+
151+
{item.githubUrl && (
152+
<a
153+
href={item.githubUrl}
154+
target="_blank"
155+
rel="noopener noreferrer"
156+
className={githubActionClassName}
157+
>
158+
<Github
159+
width={isFeatured ? 24 : 20}
160+
height={isFeatured ? 24 : 20}
161+
/>
162+
</a>
163+
)}
164+
</div>
165+
</div>
166+
</div>
167+
);
168+
}

src/components/app/portfolio/PortfolioItems.tsx

Lines changed: 3 additions & 200 deletions
Original file line numberDiff line numberDiff line change
@@ -1,214 +1,17 @@
1-
import { useTranslation } from "~/i18n/context";
2-
import { getTechBadgeStyle } from "~/components/portfolio/technologyColors";
3-
import Github from "~/components/icons/Github";
4-
import Next from "~/components/icons/Next";
5-
6-
const featuredCardClassName =
7-
"group relative flex flex-col overflow-hidden rounded-2xl border border-solid border-surface-container-low bg-surface-container-low transition-all duration-150 ease-out hover:border-primary/60 focus-within:border-primary/60 focus-within:shadow-floating active:scale-[0.995] md:flex-row";
8-
9-
const regularCardClassName =
10-
"group relative flex flex-col overflow-hidden rounded-2xl border border-solid border-surface-container-low bg-surface-container-low transition-all duration-150 ease-out hover:border-primary/60 focus-within:border-primary/60 focus-within:shadow-floating active:scale-[0.995]";
11-
12-
const cardPageLinkClassName =
13-
"absolute inset-0 z-10 rounded-2xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/70 focus-visible:ring-offset-2 focus-visible:ring-offset-background";
14-
15-
const titleClassName = "transition-colors group-hover:text-primary";
16-
17-
const descriptionClassName =
18-
"block text-base leading-relaxed text-on-surface-muted transition-colors group-hover:text-on-surface";
19-
20-
const viewActionClassName =
21-
"rounded bg-primary/10 px-4 py-2 font-semibold text-primary transition-all duration-150 ease-out group-hover:bg-primary/20";
22-
23-
const demoIconClassName =
24-
"rounded p-2 text-on-surface-muted transition-colors duration-150 ease-out group-hover:text-primary";
25-
26-
const githubActionClassName =
27-
"relative z-20 rounded p-2 text-on-surface-muted transition-all duration-150 ease-out hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/70 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-container-high active:scale-[0.95]";
28-
29-
function generateId(
30-
category: string,
31-
isFeatured: boolean,
32-
index: number
33-
): string {
34-
const categoryPrefix =
35-
category?.toUpperCase().replace(/-/g, "_") || "PROJECT";
36-
return `ID: ${categoryPrefix}_${isFeatured ? "1" : "0"}x${String(index + 1).padStart(2, "0")}`;
37-
}
1+
import { PortfolioItem } from "./PortfolioItem";
382

393
export function PortfolioItems({ featuredItem, regularItems }) {
40-
const { t } = useTranslation();
414
return (
425
<div className="space-y-6">
436
{/* Featured Item */}
447
{featuredItem && (
45-
<div className={featuredCardClassName}>
46-
<a
47-
href={`/portfolio/${featuredItem.slug}`}
48-
className={cardPageLinkClassName}
49-
aria-label={t("portfolio.showcase.viewItem", {
50-
title: featuredItem.title,
51-
})}
52-
/>
53-
{/* Image Container */}
54-
<div className="relative block h-80 w-full overflow-hidden bg-surface-container-lowest md:h-auto md:w-2/5 md:flex-shrink-0">
55-
<img
56-
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
57-
loading="lazy"
58-
src={featuredItem.thumbnail}
59-
alt={featuredItem.title}
60-
/>
61-
</div>
62-
63-
{/* Content */}
64-
<div className="flex flex-1 flex-col justify-between p-6 md:p-8">
65-
{/* Header */}
66-
<div className="mb-4">
67-
<div className="mb-4 flex items-center justify-between gap-2">
68-
<span className="rounded bg-primary/20 px-2 py-1 font-mono text-xs uppercase tracking-wider text-primary">
69-
{generateId(featuredItem.category || "", true, 0)}
70-
</span>
71-
</div>
72-
<h2 className="mb-3 font-headline text-3xl font-semibold text-on-surface">
73-
<span className={titleClassName}>{featuredItem.title}</span>
74-
</h2>
75-
{featuredItem.whatIDid?.[0] && (
76-
<span className={descriptionClassName}>
77-
{featuredItem.whatIDid[0]}
78-
</span>
79-
)}
80-
</div>
81-
82-
{/* Technologies */}
83-
{featuredItem.technologiesUsed &&
84-
featuredItem.technologiesUsed.length > 0 && (
85-
<div className="mb-4 flex flex-wrap gap-2">
86-
{featuredItem.technologiesUsed.map((tech, idx) => (
87-
<span
88-
key={idx}
89-
className="rounded border px-2 py-1 font-mono text-xs uppercase tracking-wider"
90-
style={getTechBadgeStyle(tech.type, idx)}
91-
>
92-
{tech.type}
93-
</span>
94-
))}
95-
</div>
96-
)}
97-
98-
{/* Footer with Links */}
99-
<div className="flex items-center gap-3">
100-
<span className={viewActionClassName}>
101-
{t("portfolio.showcase.viewAction")}
102-
</span>
103-
{featuredItem.demoUrl && (
104-
<span className={demoIconClassName} aria-hidden="true">
105-
<Next className="w-6 h-6" />
106-
</span>
107-
)}
108-
{featuredItem.githubUrl && (
109-
<a
110-
href={featuredItem.githubUrl}
111-
target="_blank"
112-
rel="noopener noreferrer"
113-
className={githubActionClassName}
114-
title={t("portfolio.showcase.viewOnGithub")}
115-
>
116-
<Github width={24} height={24} />
117-
</a>
118-
)}
119-
</div>
120-
</div>
121-
</div>
8+
<PortfolioItem item={featuredItem} variant="featured" index={0} />
1229
)}
12310

12411
{/* Regular Grid */}
12512
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
12613
{regularItems?.map((item, i) => {
127-
const id = generateId(item.category || "", false, i);
128-
const description = item.whatIDid?.[0] || "";
129-
130-
return (
131-
<div key={i} className={regularCardClassName}>
132-
<a
133-
href={`/portfolio/${item.slug}`}
134-
className={cardPageLinkClassName}
135-
aria-label={t("portfolio.showcase.viewItem", {
136-
title: item.title,
137-
})}
138-
/>
139-
{/* Image Container */}
140-
<div className="relative block h-52 w-full overflow-hidden rounded-t-2xl bg-surface-container-lowest">
141-
<img
142-
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
143-
loading="lazy"
144-
src={item.thumbnail}
145-
alt={item.title}
146-
/>
147-
</div>
148-
149-
{/* Header with ID and Status Badge */}
150-
<div className="flex items-center justify-between gap-2 px-4 py-3 text-xs font-mono text-on-surface-muted">
151-
<span className="rounded bg-primary/20 px-2 py-1 uppercase tracking-wider text-primary">
152-
{id}
153-
</span>
154-
</div>
155-
156-
{/* Content */}
157-
<div className="flex flex-1 flex-col gap-3 px-4 py-4">
158-
<h2 className="font-headline text-xl font-semibold text-on-surface">
159-
<span className={titleClassName}>{item.title}</span>
160-
</h2>
161-
162-
{description && (
163-
<span
164-
className={`${descriptionClassName} line-clamp-2 text-sm`}
165-
>
166-
{description}
167-
</span>
168-
)}
169-
170-
{/* Technologies */}
171-
{item.technologiesUsed && item.technologiesUsed.length > 0 && (
172-
<div className="flex flex-wrap gap-2">
173-
{item.technologiesUsed.map((tech, idx) => (
174-
<span
175-
key={idx}
176-
className="rounded border font-mono px-2 py-1 text-xs uppercase tracking-wider"
177-
style={getTechBadgeStyle(tech.type, idx)}
178-
>
179-
{tech.type}
180-
</span>
181-
))}
182-
</div>
183-
)}
184-
</div>
185-
186-
{/* Footer with Links */}
187-
<div className="flex items-center gap-3 border-t border-outline-variant px-4 py-3">
188-
<span
189-
className={`${viewActionClassName} flex-1 px-3 text-center text-sm`}
190-
>
191-
{t("portfolio.showcase.viewAction")}
192-
</span>
193-
{item.demoUrl && (
194-
<span className={demoIconClassName} aria-hidden="true">
195-
<Next className="w-5 h-5" />
196-
</span>
197-
)}
198-
{item.githubUrl && (
199-
<a
200-
href={item.githubUrl}
201-
target="_blank"
202-
rel="noopener noreferrer"
203-
className={githubActionClassName}
204-
title={t("portfolio.showcase.viewOnGithub")}
205-
>
206-
<Github width={20} height={20} />
207-
</a>
208-
)}
209-
</div>
210-
</div>
211-
);
14+
return <PortfolioItem key={i} item={item} index={i} />;
21215
})}
21316
</div>
21417
</div>

0 commit comments

Comments
 (0)