Skip to content

Commit 5226687

Browse files
authored
Merge pull request #45 from ClementBobin/feat/animation
Add animated text components and enhance UI with API updates
2 parents fc2d61c + 63eea4d commit 5226687

43 files changed

Lines changed: 3016 additions & 291 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
RESSOURCE_API_URL=https://mirage-api-ruddy.vercel.app/api
1+
RESSOURCE_API_URL=https://mirage-api.vercel.app
22
NEXT_PUBLIC_RSS_URL=https://clementbobin.github.io/obsidian/index.xml
3-
NEXT_PUBLIC_SITE_URL=https://portfolio-clement.vercel.app/
3+
SITE_URL=https://mirage-api.vercel.app

components.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
},
2424
"registries": {
2525
"@diceui": "https://diceui.com/r/{name}.json",
26-
"@jalco": "https://ui.justinlevine.me/r/{name}.json"
26+
"@jalco": "https://ui.justinlevine.me/r/{name}.json",
27+
"@spell": "https://spell.sh/r/{name}.json"
2728
}
2829
}

components/canvas.tsx

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"use client";
2+
3+
import { useEffect, useRef } from "react";
4+
import { createTimer } from "animejs";
5+
6+
const PARTICLE_COUNT = 300;
7+
8+
interface Particle {
9+
x: number;
10+
y: number;
11+
z: number;
12+
}
13+
14+
function generateParticles(count: number): Particle[] {
15+
const particles: Particle[] = [];
16+
for (let i = 0; i < count; i++) {
17+
const r = Math.random() * 2;
18+
const theta = Math.random() * Math.PI * 2;
19+
const phi = Math.acos(2 * Math.random() - 1);
20+
particles.push({
21+
x: r * Math.sin(phi) * Math.cos(theta),
22+
y: r * Math.sin(phi) * Math.sin(theta),
23+
z: r * Math.cos(phi),
24+
});
25+
}
26+
return particles;
27+
}
28+
29+
const PARTICLES = generateParticles(PARTICLE_COUNT);
30+
31+
32+
/**
33+
* Renders the animated particle sphere background for the hero section.
34+
* Uses a canvas-based animation and runs as a decorative visual layer.
35+
*/
36+
export default function Canvas() {
37+
const canvasRef = useRef<HTMLCanvasElement>(null);
38+
const rotationRef = useRef({ x: 0, y: 0 });
39+
40+
useEffect(() => {
41+
const canvas = canvasRef.current;
42+
if (!canvas) return;
43+
44+
const ctx = canvas.getContext("2d");
45+
if (!ctx) return;
46+
47+
const resize = () => {
48+
canvas.width = canvas.offsetWidth * Math.min(window.devicePixelRatio, 1.5);
49+
canvas.height = canvas.offsetHeight * Math.min(window.devicePixelRatio, 1.5);
50+
};
51+
resize();
52+
const ro = new ResizeObserver(resize);
53+
ro.observe(canvas);
54+
55+
const timer = createTimer({
56+
loop: true,
57+
onUpdate(self) {
58+
const t = self.currentTime / 1000; // seconds
59+
rotationRef.current.x = t * 0.03;
60+
rotationRef.current.y = t * 0.05;
61+
62+
const w = canvas.width;
63+
const h = canvas.height;
64+
const cx = w / 2;
65+
const cy = h / 2;
66+
const scale = Math.min(w, h) * 0.35;
67+
const fov = 3; // perspective divisor
68+
69+
ctx.clearRect(0, 0, w, h);
70+
71+
const rx = rotationRef.current.x;
72+
const ry = rotationRef.current.y;
73+
const cosX = Math.cos(rx), sinX = Math.sin(rx);
74+
const cosY = Math.cos(ry), sinY = Math.sin(ry);
75+
76+
for (const p of PARTICLES) {
77+
// Rotate Y then X
78+
const x1 = p.x * cosY - p.z * sinY;
79+
const z1 = p.x * sinY + p.z * cosY;
80+
const y2 = p.y * cosX - z1 * sinX;
81+
const z2 = p.y * sinX + z1 * cosX;
82+
83+
// Perspective projection
84+
const perspective = fov / (fov + z2);
85+
const sx = cx + x1 * scale * perspective;
86+
const sy = cy + y2 * scale * perspective;
87+
const r = Math.max(0.5, 1.5 * perspective);
88+
const alpha = 0.15 + 0.35 * perspective;
89+
90+
ctx.beginPath();
91+
ctx.arc(sx, sy, r, 0, Math.PI * 2);
92+
ctx.fillStyle = `rgba(196, 146, 42, ${alpha})`;
93+
ctx.fill();
94+
}
95+
},
96+
});
97+
98+
return () => {
99+
timer.cancel();
100+
ro.disconnect();
101+
};
102+
}, []);
103+
104+
return (
105+
<div aria-hidden="true" className="absolute inset-0 -z-10">
106+
<canvas ref={canvasRef} className="w-full h-full" />
107+
</div>
108+
);
109+
}

components/icons/linkedIn.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
"use client"
2+
13
import { IconProps } from "@/lib/types/global";
24

3-
export async function LinkedInIcon(props: IconProps) {
5+
export function LinkedInIcon(props: IconProps) {
46
return (
57
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
68
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />

components/sections/About/About.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getTranslations } from "@/hooks/getTranslations";
44
import SectionHeader from "@/components/ui/SectionHeader";
55
import { Sparkles } from "lucide-react";
66
import AboutInteractive from "./AboutInteractive";
7+
import { RandomizedText } from "@/components/ui/randomized-text";
78

89
interface AboutProps {
910
personal: PortfolioPersonal;
@@ -44,9 +45,9 @@ export default async function About({ personal, portfolio, locale }: AboutProps)
4445
{/* Text content */}
4546
<ScrollReveal direction="right">
4647
<div className="flex flex-col gap-6">
47-
<p className="text-lg leading-relaxed text-foreground/90">
48+
<RandomizedText split="chars" className="text-lg leading-relaxed text-foreground/90" inView>
4849
{t(personal.summary)}
49-
</p>
50+
</RandomizedText>
5051
</div>
5152
</ScrollReveal>
5253
</div>

components/sections/Contact/LinkedInCard.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"use client"
2+
13
import { LinkedInIcon } from "@/components/icons";
24
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
35
import { ExternalLink } from "lucide-react";
@@ -8,7 +10,7 @@ interface LinkedInCardProps {
810
label: string;
911
}
1012

11-
export default async function LinkedInCard({ url, label }: LinkedInCardProps) {
13+
export default function LinkedInCard({ url, label }: LinkedInCardProps) {
1214
return (
1315
<Card className="flex flex-col gap-2.5 rounded-xl p-4">
1416
<CardHeader className="flex items-center gap-2.5">

components/sections/Contributions/ContributionCard.tsx

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { useTranslations } from "@/hooks/useTranslations";
1616
interface ContributionCardProps {
1717
item: ContributionItem;
1818
locale: string;
19-
onOpen: () => void;
19+
onOpen: (item: ContributionItem) => void;
2020
}
2121

2222
export default function ContributionCard({ item, locale, onOpen }: ContributionCardProps) {
@@ -25,21 +25,9 @@ export default function ContributionCard({ item, locale, onOpen }: ContributionC
2525
return (
2626
<Card
2727
className="transition-colors hover:border-accent"
28-
onClick={onOpen}
28+
onClick={() => onOpen(item)}
2929
role="button"
3030
tabIndex={0}
31-
onKeyDown={(event) => {
32-
if (event.target !== event.currentTarget) return;
33-
34-
if (event.key === "Enter") {
35-
onOpen();
36-
}
37-
38-
if (event.key === " ") {
39-
event.preventDefault();
40-
onOpen();
41-
}
42-
}}
4331
>
4432
<CardHeader>
4533
<div className="flex items-start justify-between gap-2">

components/sections/Contributions/ContributionHeatmap.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { use } from "react";
44
import { GitBranch } from "lucide-react";
5-
import { ActivityGraph } from "@/components/activity-graph";
5+
import { ActivityGraph } from "@/components/ui/activity-graph";
66
import { fetchGitHubContributions } from "@/lib/github";
77

88
// Created once when the module loads — survives re-renders and Suspense cycles

components/sections/Contributions/ContributionsSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ async function fetchContributions(): Promise<ContributionItem[]> {
66
if (!apiUrl) return [];
77

88
try {
9-
const res = await fetch(`${apiUrl}/contributions`, {
9+
const res = await fetch(`${apiUrl}/contributions/list`, {
1010
next: { revalidate: 3600 },
1111
});
1212

components/sections/Contributions/ContributionsSectionClient.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useState } from "react";
3+
import { useState, useCallback } from "react";
44
import { ExternalLinkIcon, GitHubIcon } from "@/components/icons"
55
import ScrollReveal from "@/components/ui/ScrollReveal";
66
import ContributionCard from "./ContributionCard";
@@ -25,6 +25,14 @@ export default function ContributionsSectionClient({
2525
const [activeItem, setActiveItem] = useState<ContributionItem | null>(null);
2626
const t = useTranslations(locale, ["portfolio"]);
2727

28+
const handleOpen = useCallback((item: ContributionItem) => {
29+
setActiveItem(item);
30+
}, []);
31+
32+
const handleOpenChange = useCallback((open: boolean) => {
33+
if (!open) setActiveItem(null);
34+
}, []);
35+
2836
return (
2937
<div className="flex flex-col gap-8">
3038
{/* Page header */}
@@ -64,7 +72,7 @@ export default function ContributionsSectionClient({
6472
<ContributionCard
6573
item={item}
6674
locale={locale}
67-
onOpen={() => setActiveItem(item)}
75+
onOpen={handleOpen}
6876
/>
6977
</li>
7078
</ScrollReveal>
@@ -75,7 +83,7 @@ export default function ContributionsSectionClient({
7583
<ContributionModal
7684
item={activeItem}
7785
open={activeItem !== null}
78-
onOpenChange={(open) => { if (!open) setActiveItem(null); }}
86+
onOpenChange={handleOpenChange}
7987
locale={locale}
8088
/>
8189
</div>

0 commit comments

Comments
 (0)