Skip to content

Commit 92c3763

Browse files
authored
feat: add Philosophy section with cards and translations
- Implemented Philosophy section with dynamic cards displaying philosophy items. - Created PhilosophyCard component to render individual philosophy items. - Updated Projects section to include subtitle and improved layout. - Enhanced Skills section with new layout and card components for skill sections. - Refactored Strength section to include example modal for detailed view. - Removed ValueCards section as it was redundant. - Updated Vision section to use new VisionCard component for better structure. - Modified SectionHeader component to support subtitle and children. - Updated localization files to include new keys for various sections. - Expanded portfolio API types to accommodate new structure for Philosophy and Vision sections.
1 parent e33211d commit 92c3763

26 files changed

Lines changed: 1385 additions & 692 deletions
Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { Suspense } from "react";
22
import type { Metadata } from "next";
3-
import ScrollReveal from "@/components/ui/ScrollReveal";
4-
import { ContributionsSection, ContributionsSkeleton } from "@/components/sections/Contributions/ContributionsSection";
5-
import { getTranslations } from "@/hooks/useTranslation";
3+
import { ContributionsSection, ContributionsSkeleton } from "@/components/sections/Contributions";
64
import type { PageParams } from "@/types/global";
5+
import { getTranslations } from "@/hooks/useTranslation";
76

87
export async function generateMetadata({ params }: PageParams): Promise<Metadata> {
98
const { locale } = await params;
@@ -16,26 +15,16 @@ export async function generateMetadata({ params }: PageParams): Promise<Metadata
1615

1716
export default async function ContributionsPage({ params }: PageParams) {
1817
const { locale } = await params;
19-
const t = await getTranslations(locale, ["pages"]);
2018

2119
return (
22-
<>
23-
<main className="mx-auto w-full max-w-2xl px-6 py-24">
24-
<ScrollReveal>
25-
<div className="mb-12">
26-
<h1 className="text-4xl font-bold tracking-tight mb-3">
27-
{t("contributions.title")}
28-
</h1>
29-
<p className="leading-relaxed text-muted-foreground">
30-
{t("contributions.description")}
31-
</p>
32-
</div>
33-
</ScrollReveal>
34-
35-
<Suspense fallback={<ContributionsSkeleton />}>
36-
<ContributionsSection locale={locale} />
37-
</Suspense>
38-
</main>
39-
</>
20+
<main className="mx-auto w-full max-w-3xl px-6 py-24">
21+
<Suspense fallback={<ContributionsSkeleton />}>
22+
<ContributionsSection
23+
locale={locale}
24+
githubUsername="@clementbobin"
25+
githubUrl="https://github.com/clementbobin"
26+
/>
27+
</Suspense>
28+
</main>
4029
);
4130
}

app/[locale]/page.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import About from "@/components/sections/About/About";
66
import Skills from "@/components/sections/Skills/Skills";
77
import Experience from "@/components/sections/Experience/Experience";
88
import Projects from "@/components/sections/Projects/Projects";
9-
import ValueCards from "@/components/sections/ValueCards/ValueCards";
9+
import Philosophy from "@/components/sections/Philosophy/Philosophy";
1010
import Vision from "@/components/sections/Vision/Vision";
1111
import Hobbies from "@/components/sections/Hobbies/Hobbies";
1212
import Strength from "@/components/sections/Strength/Strength";
@@ -16,7 +16,7 @@ export default async function PortfolioPage({ params }: PageParams) {
1616
const { locale } = await params;
1717
const {
1818
personal, contact, skills, experiences,
19-
education, projects, valueCards,
19+
education, projects, philosophy,
2020
vision, hobbies, strength,
2121
} = await fetchPortfolioData();
2222

@@ -34,8 +34,8 @@ export default async function PortfolioPage({ params }: PageParams) {
3434
{education.length > 0 && <Experience experiences={experiences} education={education} locale={locale} />}
3535
{projects.length > 0 && <Projects projects={projects} locale={locale} />}
3636
{strength && strength.strengths.length > 0 && <Strength strength={strength} locale={locale} />}
37-
{valueCards && valueCards.length > 0 && <ValueCards valueCards={valueCards} locale={locale} />}
38-
{vision && vision.items.length > 0 && <Vision vision={vision} locale={locale} />}
37+
{philosophy && philosophy.cards.length > 0 && <Philosophy philosophy={philosophy} locale={locale} />}
38+
{vision && vision.length > 0 && <Vision vision={vision} locale={locale} />}
3939
{hobbies.length > 0 && <Hobbies hobbies={hobbies} locale={locale} />}
4040
</Suspense>
4141
</main>

components/sections/About/About.tsx

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import ScrollReveal from "@/components/ui/ScrollReveal";
33
import type { PortfolioPersonal } from "@/types/portfolio-api";
44
import { getTranslations } from "@/hooks/useTranslation";
55
import { AspectRatio } from "@/components/ui/aspect-ratio";
6+
import { SectionHeader } from "@/components/ui/SectionHeader";
7+
import { Sparkles } from "lucide-react";
68

79
interface AboutProps {
810
personal: PortfolioPersonal;
@@ -17,52 +19,60 @@ interface AboutProps {
1719
* @param locale - Current locale
1820
*/
1921
export default async function About({ personal, locale }: AboutProps) {
20-
const t = await getTranslations(locale);
22+
const t = await getTranslations(locale, ["portfolio"]);
2123

2224
return (
2325
<section
2426
id="about"
2527
className="mx-auto w-full max-w-5xl px-6 py-24"
2628
aria-label={t("section.about")}
2729
>
28-
29-
<div className="grid gap-12 md:grid-cols-2 md:items-center">
30-
{/* Photo */}
31-
<ScrollReveal direction="left">
32-
<div className="relative mx-auto w-64 md:w-80">
33-
<div className="absolute -inset-2 rounded-2xl bg-linear-to-br from-accent/30 to-primary/20 blur-xl" />
34-
<div className="relative overflow-hidden rounded-2xl border border-border bg-card shadow-xl">
35-
{personal.photo ? (
36-
<AspectRatio ratio={4 / 4} className="w-full">
37-
<Image
38-
src={personal.photo}
39-
alt={personal.name}
40-
width={320}
41-
height={320}
42-
priority
43-
className="object-cover"
44-
/>
45-
</AspectRatio>
46-
) : (
47-
<div className="flex h-80 items-center justify-center bg-secondary text-8xl">
48-
{personal.name.charAt(0)}
49-
</div>
50-
)}
30+
<SectionHeader
31+
eyebrow={
32+
<>
33+
<Sparkles aria-hidden width={16} height={16} />
34+
{t("about.badge")}
35+
</>
36+
}
37+
>
38+
<div className="grid gap-12 md:grid-cols-2 md:items-center">
39+
{/* Photo */}
40+
<ScrollReveal direction="left">
41+
<div className="relative mx-auto w-64 md:w-80">
42+
<div className="absolute -inset-2 rounded-2xl bg-linear-to-br from-accent/30 to-primary/20 blur-xl" />
43+
<div className="relative overflow-hidden rounded-2xl border border-border bg-card shadow-xl">
44+
{personal.photo ? (
45+
<AspectRatio ratio={4 / 4} className="w-full">
46+
<Image
47+
src={personal.photo}
48+
alt={personal.name}
49+
width={320}
50+
height={320}
51+
priority
52+
className="object-cover"
53+
/>
54+
</AspectRatio>
55+
) : (
56+
<div className="flex h-80 items-center justify-center bg-secondary text-8xl">
57+
{personal.name.charAt(0)}
58+
</div>
59+
)}
60+
</div>
61+
{/* Decorative corner accent */}
62+
<div className="absolute -bottom-3 -right-3 h-12 w-12 rounded-full bg-accent/20 border border-accent/40" />
5163
</div>
52-
{/* Decorative corner accent */}
53-
<div className="absolute -bottom-3 -right-3 h-12 w-12 rounded-full bg-accent/20 border border-accent/40" />
54-
</div>
55-
</ScrollReveal>
64+
</ScrollReveal>
5665

57-
{/* Text content */}
58-
<ScrollReveal direction="right">
59-
<div className="flex flex-col gap-6">
60-
<p className="text-lg leading-relaxed text-foreground/90">
61-
{t(personal.summary)}
62-
</p>
63-
</div>
64-
</ScrollReveal>
65-
</div>
66+
{/* Text content */}
67+
<ScrollReveal direction="right">
68+
<div className="flex flex-col gap-6">
69+
<p className="text-lg leading-relaxed text-foreground/90">
70+
{t(personal.summary)}
71+
</p>
72+
</div>
73+
</ScrollReveal>
74+
</div>
75+
</SectionHeader>
6676
</section>
6777
);
6878
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { GitBranch, Lock } from "lucide-react";
2+
import { Badge } from "@/components/ui/badge";
3+
import { ExternalLinkIcon } from "@/components/icons/externalLink"
4+
import {
5+
Card,
6+
CardContent,
7+
CardFooter,
8+
CardHeader,
9+
CardTitle,
10+
CardDescription,
11+
} from "@/components/ui/card";
12+
import { Status, StatusIndicator, StatusLabel } from "@/components/ui/status";
13+
import type { ContributionItem } from "@/types/contribution";
14+
import { useTranslations } from "@/hooks/useTranslation";
15+
16+
interface ContributionCardProps {
17+
item: ContributionItem;
18+
locale: string;
19+
onOpen: () => void;
20+
}
21+
22+
export function ContributionCard({ item, locale, onOpen }: ContributionCardProps) {
23+
const t = useTranslations(locale, ["portfolio"]);
24+
25+
return (
26+
<Card className="transition-colors hover:border-accent" onClick={onOpen}>
27+
<CardHeader>
28+
<div className="flex items-start justify-between gap-2">
29+
<div className="flex items-center gap-2">
30+
<GitBranch size={15} className="shrink-0 text-emerald-400" aria-hidden />
31+
<CardTitle className="font-mono text-base font-bold text-white">
32+
{item.title}
33+
</CardTitle>
34+
</div>
35+
36+
<div className="flex shrink-0 items-center gap-2 pt-0.5">
37+
<Status variant={item.private ? "warning" : "success"}>
38+
<StatusIndicator />
39+
<StatusLabel>{t(item.status)}</StatusLabel>
40+
</Status>
41+
{item.private && (
42+
<span title="Private" className="text-white/30">
43+
<Lock size={13} />
44+
</span>
45+
)}
46+
{item.href && (
47+
<a
48+
href={item.href}
49+
target="_blank"
50+
rel="noopener noreferrer"
51+
className="text-white/30 transition-colors hover:text-white/70"
52+
onClick={(e) => e.stopPropagation()}
53+
aria-label={`Open ${item.title} on GitHub`}
54+
>
55+
<ExternalLinkIcon scale={1.2} />
56+
</a>
57+
)}
58+
</div>
59+
</div>
60+
61+
<CardDescription className="text-xs font-medium uppercase tracking-wide text-white/40">
62+
{t(item.subtitle)}
63+
</CardDescription>
64+
</CardHeader>
65+
66+
<CardContent className="flex flex-col gap-3 pt-4">
67+
<p className="text-sm leading-relaxed text-white/50 line-clamp-3">
68+
{t(item.description)}
69+
</p>
70+
71+
{item.tags.length > 0 && (
72+
<div className="flex flex-wrap gap-1.5">
73+
{item.tags.map((tag) => (
74+
<Badge
75+
key={tag}
76+
variant="outline"
77+
className="border-white/10 bg-white/5 font-mono text-[11px] text-white/50"
78+
>
79+
{tag}
80+
</Badge>
81+
))}
82+
</div>
83+
)}
84+
</CardContent>
85+
86+
<CardFooter>
87+
<button
88+
className="font-mono text-xs font-semibold text-[#7c3aed] transition-colors hover:text-[#a78bfa]"
89+
>
90+
Read Case Study →
91+
</button>
92+
</CardFooter>
93+
</Card>
94+
);
95+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { GitBranch } from "lucide-react";
2+
3+
// Deterministic pseudo-random commit counts for visual demo.
4+
// Swap `data` for a real GitHub contributions API payload when available.
5+
function generateHeatmapData(weeks: number, days: number): number[] {
6+
return Array.from({ length: weeks * days }, (_, i) => {
7+
const seed = (i * 1103515245 + 12345) & 0x7fffffff;
8+
const rand = (seed % 100) / 100;
9+
if (rand > 0.6) return 0;
10+
if (rand > 0.4) return Math.floor(rand * 4) + 1;
11+
if (rand > 0.2) return Math.floor(rand * 8) + 3;
12+
return Math.floor(rand * 15) + 6;
13+
});
14+
}
15+
16+
function getColor(count: number, max: number): string {
17+
if (count === 0) return "bg-[#161b22]";
18+
const intensity = count / max;
19+
if (intensity < 0.25) return "bg-[#0e4429]";
20+
if (intensity < 0.5) return "bg-[#006d32]";
21+
if (intensity < 0.75) return "bg-[#26a641]";
22+
return "bg-[#39d353]";
23+
}
24+
25+
const LEGEND_COLORS = [
26+
"bg-[#161b22]",
27+
"bg-[#0e4429]",
28+
"bg-[#006d32]",
29+
"bg-[#26a641]",
30+
"bg-[#39d353]",
31+
];
32+
33+
interface ContributionHeatmapProps {
34+
weeks?: number;
35+
githubUrl?: string;
36+
}
37+
38+
export function ContributionHeatmap({
39+
weeks = 53,
40+
githubUrl = "https://github.com",
41+
}: ContributionHeatmapProps) {
42+
const days = 7;
43+
const data = generateHeatmapData(weeks, days);
44+
const max = Math.max(...data);
45+
46+
return (
47+
<div className="rounded-xl border p-5">
48+
<div className="mb-4 flex items-center justify-between">
49+
<span className="flex items-center gap-2 font-mono text-xs font-semibold uppercase tracking-widest text-emerald-400">
50+
<GitBranch size={12} aria-hidden />
51+
Continuous Commit Activity (Past Year)
52+
</span>
53+
<span className="font-mono text-xs text-white/30">{githubUrl.replace("https://", "")}</span>
54+
</div>
55+
56+
<div className="overflow-x-auto">
57+
<div
58+
role="img"
59+
aria-label="GitHub contribution heatmap for the past year"
60+
className="grid gap-0.75"
61+
style={{
62+
gridTemplateColumns: `repeat(${weeks}, minmax(10px, 1fr))`,
63+
gridTemplateRows: `repeat(${days}, 10px)`,
64+
gridAutoFlow: "column",
65+
}}
66+
>
67+
{data.map((count, i) => (
68+
<div
69+
key={i}
70+
title={`${count} contribution${count !== 1 ? "s" : ""}`}
71+
className={`rounded-xs ${getColor(count, max)}`}
72+
/>
73+
))}
74+
</div>
75+
</div>
76+
77+
<div className="mt-3 flex items-center justify-between">
78+
<a
79+
href={githubUrl}
80+
target="_blank"
81+
rel="noopener noreferrer"
82+
className="font-mono text-xs text-white/30 transition-colors hover:text-white/60"
83+
>
84+
Learn more on GitHub
85+
</a>
86+
<div className="flex items-center gap-1.5">
87+
<span className="font-mono text-xs text-white/30">Less</span>
88+
{LEGEND_COLORS.map((c, i) => (
89+
<div key={i} className={`size-2.5 rounded-xs ${c}`} />
90+
))}
91+
<span className="font-mono text-xs text-white/30">More</span>
92+
</div>
93+
</div>
94+
</div>
95+
);
96+
}

0 commit comments

Comments
 (0)