Skip to content

Commit 91495cd

Browse files
committed
fix(portfolio): update duplicate ID, also refactor to make the items easier to modify
1 parent 9ec4e2d commit 91495cd

2 files changed

Lines changed: 227 additions & 408 deletions

File tree

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
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+
}
38+
39+
export function PortfolioItems({ featuredItem, regularItems }) {
40+
const { t } = useTranslation();
41+
return (
42+
<div className="space-y-6">
43+
{/* Featured Item */}
44+
{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>
122+
)}
123+
124+
{/* Regular Grid */}
125+
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
126+
{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+
);
212+
})}
213+
</div>
214+
</div>
215+
);
216+
}

0 commit comments

Comments
 (0)