|
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"; |
38 | 2 |
|
39 | 3 | export function PortfolioItems({ featuredItem, regularItems }) { |
40 | | - const { t } = useTranslation(); |
41 | 4 | return ( |
42 | 5 | <div className="space-y-6"> |
43 | 6 | {/* Featured Item */} |
44 | 7 | {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} /> |
122 | 9 | )} |
123 | 10 |
|
124 | 11 | {/* Regular Grid */} |
125 | 12 | <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"> |
126 | 13 | {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} />; |
212 | 15 | })} |
213 | 16 | </div> |
214 | 17 | </div> |
|
0 commit comments