diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..5e14b5b
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,13 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(npx tsc:*)",
+ "Bash(npm run:*)",
+ "Bash(cp /c/Users/User/Documents/dev/cli-portfolio/public/CLIPortfolioLogo.png /c/Users/User/Documents/dev/achraf-portfolio/public/)",
+ "Bash(cp /c/Users/User/Documents/dev/cli-portfolio/public/typesonlogo-light.png /c/Users/User/Documents/dev/achraf-portfolio/public/)",
+ "Bash(cp /c/Users/User/Documents/dev/cli-portfolio/public/foon-logo.svg /c/Users/User/Documents/dev/achraf-portfolio/public/)",
+ "Bash(cp /c/Users/User/Documents/dev/cli-portfolio/public/me.png /c/Users/User/Documents/dev/achraf-portfolio/public/)",
+ "Bash(cp /c/Users/User/Documents/dev/cli-portfolio/public/ensa.png /c/Users/User/Documents/dev/achraf-portfolio/public/)"
+ ]
+ }
+}
diff --git a/app/globals.css b/app/globals.css
index a2dc41e..8fd3c99 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,26 +1,259 @@
@import "tailwindcss";
-:root {
- --background: #ffffff;
- --foreground: #171717;
-}
-
@theme inline {
- --color-background: var(--background);
- --color-foreground: var(--foreground);
- --font-sans: var(--font-geist-sans);
- --font-mono: var(--font-geist-mono);
+ --color-bg: #07080a;
+ --color-surface: #0e1014;
+ --color-border: #1c1f26;
+ --color-border-dim: #141619;
+ --color-text: #e2e4e9;
+ --color-muted: #4a5060;
+ --color-accent: #c8a96e;
+ --color-accent-bright: #e8c887;
+ --color-accent-dim: #6b5730;
+ --color-tag-bg: #131720;
+ --color-done: #3a7a5a;
+ --color-progress: #6b5730;
}
-@media (prefers-color-scheme: dark) {
- :root {
- --background: #0a0a0a;
- --foreground: #ededed;
- }
+* { box-sizing: border-box; }
+
+/* All page sections sit above the fixed GridCanvas (z-index 0) */
+main > section { position: relative; z-index: 5; }
+
+html {
+ scroll-behavior: smooth;
+ background: #07080a;
}
body {
- background: var(--background);
- color: var(--foreground);
- font-family: Arial, Helvetica, sans-serif;
+ background: #07080a;
+ color: #e2e4e9;
+ font-family: var(--font-lora), Georgia, serif;
+ overflow-x: hidden;
+}
+
+body::before {
+ content: "";
+ position: fixed;
+ inset: 0;
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.04'/%3E%3C/svg%3E");
+ pointer-events: none;
+ z-index: 1;
+ opacity: 0.35;
+}
+
+::-webkit-scrollbar { width: 4px; }
+::-webkit-scrollbar-track { background: #07080a; }
+::-webkit-scrollbar-thumb { background: #1c1f26; border-radius: 2px; }
+
+@keyframes fadeUp {
+ from { opacity: 0; transform: translateY(32px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+@keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+@keyframes slideRight {
+ from { transform: scaleX(0); }
+ to { transform: scaleX(1); }
+}
+@keyframes pulse-glow {
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(200, 169, 110, 0); }
+ 50% { box-shadow: 0 0 20px 4px rgba(200, 169, 110, 0.12); }
+}
+@keyframes blink {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0; }
+}
+@keyframes scroll-bounce {
+ 0%, 100% { transform: translateY(0); }
+ 50% { transform: translateY(6px); }
+}
+
+.animate-fade-up {
+ animation: fadeUp 0.75s ease both;
+}
+
+.delay-1 { animation-delay: 0.15s; }
+.delay-2 { animation-delay: 0.3s; }
+.delay-3 { animation-delay: 0.45s; }
+.delay-4 { animation-delay: 0.6s; }
+.delay-5 { animation-delay: 0.75s; }
+.delay-6 { animation-delay: 0.9s; }
+
+.reveal {
+ opacity: 0;
+ transform: translateY(24px);
+ transition: opacity 0.7s ease, transform 0.7s ease;
+}
+.reveal.visible {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+/* grid-bg: static CSS grid removed — replaced by GridCanvas (canvas-based distortion) */
+.grid-bg {}
+
+.project-card {
+ background: #0e1014;
+ border: 1px solid #1c1f26;
+ transition: border-color 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease;
+ position: relative;
+ overflow: hidden;
+}
+.project-card::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(135deg, rgba(200,169,110,0.05) 0%, transparent 60%);
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ pointer-events: none;
+}
+.project-card:hover {
+ border-color: #6b5730;
+ transform: translateY(-3px);
+ box-shadow: 0 16px 48px rgba(0,0,0,0.5);
+}
+.project-card:hover::before { opacity: 1; }
+
+.tech-tag {
+ font-family: var(--font-jetbrains), monospace;
+ font-size: 0.62rem;
+ letter-spacing: 0.08em;
+ padding: 2px 8px;
+ border: 1px solid #1c1f26;
+ color: #4a5060;
+ background: #131720;
+ display: inline-block;
+ transition: border-color 0.2s, color 0.2s;
+}
+.tech-tag:hover {
+ border-color: #6b5730;
+ color: #c8a96e;
+}
+
+.nav-link {
+ font-family: var(--font-jetbrains), monospace;
+ font-size: 0.7rem;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: #4a5060;
+ transition: color 0.2s;
+ position: relative;
+}
+.nav-link::after {
+ content: '';
+ position: absolute;
+ bottom: -2px;
+ left: 0;
+ width: 0;
+ height: 1px;
+ background: #c8a96e;
+ transition: width 0.25s ease;
+}
+.nav-link:hover { color: #c8a96e; }
+.nav-link:hover::after { width: 100%; }
+
+.status-done {
+ font-family: var(--font-jetbrains), monospace;
+ font-size: 0.58rem;
+ letter-spacing: 0.1em;
+ padding: 1px 6px;
+ border: 1px solid #3a7a5a;
+ color: #3a7a5a;
+ text-transform: uppercase;
+}
+.status-progress {
+ font-family: var(--font-jetbrains), monospace;
+ font-size: 0.58rem;
+ letter-spacing: 0.1em;
+ padding: 1px 6px;
+ border: 1px solid #6b5730;
+ color: #c8954a;
+ text-transform: uppercase;
+}
+
+.section-label {
+ font-family: var(--font-jetbrains), monospace;
+ font-size: 0.62rem;
+ letter-spacing: 0.22em;
+ text-transform: uppercase;
+ color: #c8a96e;
+}
+
+.cursor::after {
+ content: '_';
+ animation: blink 1s step-end infinite;
+ color: #c8a96e;
+}
+
+.scroll-indicator {
+ animation: scroll-bounce 1.8s ease-in-out infinite;
+}
+
+.accent-bar {
+ display: block;
+ width: 32px;
+ height: 2px;
+ background: #c8a96e;
+ transform-origin: left;
+}
+
+/* ── Responsive bento grid ───────────────────────────────────── */
+.hero-bento-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1px;
+ background: #1c1f26;
+ border: 1px solid #1c1f26;
+}
+@media (max-width: 1023px) {
+ .hero-bento-grid { grid-template-columns: repeat(2, 1fr); }
+}
+@media (max-width: 599px) {
+ .hero-bento-grid { grid-template-columns: 1fr; }
+}
+
+/* ── Hero header responsive ──────────────────────────────────── */
+.hero-header-strip {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 1.5rem;
+ margin-bottom: 2.5rem;
+}
+@media (max-width: 599px) {
+ .hero-header-strip { flex-direction: column; align-items: flex-start; }
+}
+
+/* ── Hero section padding ────────────────────────────────────── */
+.hero-section {
+ padding-top: 88px;
+ padding-bottom: 4rem;
+ padding-left: 2rem;
+ padding-right: 2rem;
+}
+@media (max-width: 599px) {
+ .hero-section {
+ padding-top: 76px;
+ padding-left: 1.25rem;
+ padding-right: 1.25rem;
+ }
+}
+
+/* ── Headline cursor blink ───────────────────────────────────── */
+@keyframes cursor-blink {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0; }
+}
+.headline-cursor {
+ display: inline-block;
+ width: 3px;
+ background: #c8a96e;
+ margin-left: 4px;
+ animation: cursor-blink 1.1s step-end infinite;
+ vertical-align: baseline;
}
diff --git a/app/layout.tsx b/app/layout.tsx
index 976eb90..ff90441 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,33 +1,44 @@
import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
+import { Bebas_Neue, JetBrains_Mono, Lora } from "next/font/google";
import "./globals.css";
+import GridCanvas from "@/components/GridCanvas";
-const geistSans = Geist({
- variable: "--font-geist-sans",
+const bebas = Bebas_Neue({
+ weight: "400",
+ variable: "--font-bebas",
subsets: ["latin"],
});
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
+const jetbrains = JetBrains_Mono({
+ variable: "--font-jetbrains",
subsets: ["latin"],
+ weight: ["400", "500"],
+});
+
+const lora = Lora({
+ variable: "--font-lora",
+ subsets: ["latin"],
+ weight: ["400", "500"],
});
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "Achraf Achkari — Technical Lead & Software Engineer",
+ description:
+ "Personal portfolio and project hub. Technical Lead at Kereval, building software with Java, TypeScript, and modern frameworks.",
};
export default function RootLayout({
children,
-}: Readonly<{
- children: React.ReactNode;
-}>) {
+}: Readonly<{ children: React.ReactNode }>) {
return (
-
{children}
+
+
+ {children}
+
);
}
diff --git a/app/page.tsx b/app/page.tsx
index 3f36f7c..a9db107 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,65 +1,46 @@
-import Image from "next/image";
+import portfolioData from "@/data/portfolio.json";
+import Navigation from "@/components/Navigation";
+import Hero from "@/components/Hero";
+import Projects from "@/components/Projects";
+import Experience from "@/components/Experience";
+import Education from "@/components/Education";
+import Skills from "@/components/Skills";
+import Contact from "@/components/Contact";
export default function Home() {
+ const { about, projects, experiences, education } = portfolioData;
+
return (
-
-
-
+
+
+
+
+
+
+
+
-
-
- To get started, edit the page.tsx file.
-
-
- Looking for a starting point or more instructions? Head over to{" "}
-
- Templates
- {" "}
- or the{" "}
-
- Learning
- {" "}
- center.
-
-
-
-
+ >
);
}
diff --git a/components/Contact.tsx b/components/Contact.tsx
new file mode 100644
index 0000000..b689847
--- /dev/null
+++ b/components/Contact.tsx
@@ -0,0 +1,300 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+import SafeImage from "./SafeImage";
+
+interface ContactInfo {
+ email: string;
+ phone: string;
+ linkedin: string;
+ github: string;
+ Address: string;
+}
+
+interface ContactProps {
+ contact: ContactInfo;
+ fullName: string;
+ image?: string;
+}
+
+export default function Contact({ contact, fullName, image }: ContactProps) {
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const el = ref.current;
+ if (!el) return;
+ const obs = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) { el.classList.add("visible"); obs.disconnect(); }
+ },
+ { threshold: 0.1 }
+ );
+ obs.observe(el);
+ return () => obs.disconnect();
+ }, []);
+
+ const links = [
+ {
+ label: "Email",
+ value: contact.email,
+ href: `mailto:${contact.email}`,
+ icon: (
+
+ ),
+ },
+ {
+ label: "Phone",
+ value: contact.phone,
+ href: `tel:${contact.phone}`,
+ icon: (
+
+ ),
+ },
+ {
+ label: "LinkedIn",
+ value: "achraf-achkari",
+ href: contact.linkedin,
+ icon: (
+
+ ),
+ },
+ {
+ label: "GitHub",
+ value: "achachraf",
+ href: contact.github,
+ icon: (
+
+ ),
+ },
+ ];
+
+ return (
+
+ );
+}
diff --git a/components/Education.tsx b/components/Education.tsx
new file mode 100644
index 0000000..dcca859
--- /dev/null
+++ b/components/Education.tsx
@@ -0,0 +1,170 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+import SafeImage from "./SafeImage";
+
+interface EducationItem {
+ degree: string;
+ school: string;
+ startDate: string;
+ endDate: string;
+ logo?: string;
+}
+
+interface EducationProps {
+ education: EducationItem[];
+}
+
+function EduCard({ item, index }: { item: EducationItem; index: number }) {
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const el = ref.current;
+ if (!el) return;
+ const obs = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) {
+ setTimeout(() => el.classList.add("visible"), index * 100);
+ obs.disconnect();
+ }
+ },
+ { threshold: 0.1 }
+ );
+ obs.observe(el);
+ return () => obs.disconnect();
+ }, [index]);
+
+ return (
+ (e.currentTarget.style.borderColor = "#6b5730")}
+ onMouseLeave={(e) => (e.currentTarget.style.borderColor = "#1c1f26")}
+ >
+ {/* Corner accent */}
+
+
+
+ {item.startDate} — {item.endDate}
+
+
+ {item.logo && (
+
+
+
+ )}
+
+
+ {item.degree}
+
+
+ {item.school}
+
+
+ );
+}
+
+export default function Education({ education }: EducationProps) {
+ const headingRef = useRef(null);
+
+ useEffect(() => {
+ const el = headingRef.current;
+ if (!el) return;
+ const obs = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) { el.classList.add("visible"); obs.disconnect(); }
+ },
+ { threshold: 0.1 }
+ );
+ obs.observe(el);
+ return () => obs.disconnect();
+ }, []);
+
+ return (
+
+
+
+
Academic Background
+
+ Education
+
+
+
+
+
+ {education.map((item, i) => (
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/components/Experience.tsx b/components/Experience.tsx
new file mode 100644
index 0000000..38cb730
--- /dev/null
+++ b/components/Experience.tsx
@@ -0,0 +1,195 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+import SafeImage from "./SafeImage";
+
+interface Experience {
+ name: string;
+ company: string;
+ position: string;
+ description: string;
+ startDate: string;
+ endDate: string;
+ logo?: string;
+}
+
+interface ExperienceProps {
+ experiences: Experience[];
+}
+
+function formatDate(d: string) {
+ if (d === "current") return "Present";
+ const [, m, y] = d.split("-");
+ const months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
+ return `${months[parseInt(m) - 1]} ${y}`;
+}
+
+function ExperienceItem({ exp, index }: { exp: Experience; index: number }) {
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const el = ref.current;
+ if (!el) return;
+ const obs = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) {
+ setTimeout(() => el.classList.add("visible"), index * 100);
+ obs.disconnect();
+ }
+ },
+ { threshold: 0.1 }
+ );
+ obs.observe(el);
+ return () => obs.disconnect();
+ }, [index]);
+
+ const isCurrent = exp.endDate === "current";
+
+ return (
+
+ {/* Date */}
+
+
+ {isCurrent && (
+
+ ● NOW
+
+ )}
+ {formatDate(exp.startDate)}
+
—
+ {formatDate(exp.endDate)}
+
+
+
+ {/* Line with dot */}
+
+
+ {/* Content */}
+
+
+ {exp.logo && (
+
+ )}
+
+ {exp.company}
+
+
+
+
+ {exp.position}
+
+
+ {exp.description}
+
+
+
+ );
+}
+
+export default function Experience({ experiences }: ExperienceProps) {
+ const headingRef = useRef(null);
+
+ useEffect(() => {
+ const el = headingRef.current;
+ if (!el) return;
+ const obs = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) { el.classList.add("visible"); obs.disconnect(); }
+ },
+ { threshold: 0.1 }
+ );
+ obs.observe(el);
+ return () => obs.disconnect();
+ }, []);
+
+ return (
+
+
+
+
Career Path
+
+ Experience
+
+
+
+
+
+ {experiences.map((exp, i) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/components/GridCanvas.tsx b/components/GridCanvas.tsx
new file mode 100644
index 0000000..1287c06
--- /dev/null
+++ b/components/GridCanvas.tsx
@@ -0,0 +1,181 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+
+/**
+ * Full-page fixed canvas that draws a distorted grid background.
+ * The grid warps toward the mouse cursor like a gravitational lens /
+ * spacetime distortion — grid lines curve inward, intersections glow,
+ * and a soft mass-glow follows the cursor.
+ */
+export default function GridCanvas() {
+ const canvasRef = useRef(null);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const ctx = canvas.getContext("2d");
+ if (!ctx) return;
+
+ let raf: number;
+
+ // Smoothed mouse position (lerped toward target)
+ let mx = -3000;
+ let my = -3000;
+ // Raw target from events
+ let tx = -3000;
+ let ty = -3000;
+
+ const GRID = 64; // px between grid lines
+ const SIGMA = 190; // radius of distortion effect (px)
+ const STRENGTH = 52; // max pixel pull toward cursor
+
+ // ── resize ───────────────────────────────────────────────────────────────
+ const resize = () => {
+ canvas.width = window.innerWidth;
+ canvas.height = window.innerHeight;
+ };
+ resize();
+ window.addEventListener("resize", resize);
+
+ // ── mouse tracking (global so it works across the whole page) ────────────
+ const onMove = (e: MouseEvent) => { tx = e.clientX; ty = e.clientY; };
+ const onLeave = () => { tx = -3000; ty = -3000; };
+ window.addEventListener("mousemove", onMove);
+ document.documentElement.addEventListener("mouseleave", onLeave);
+
+ // ── displacement: pull a point (px,py) toward the cursor ─────────────────
+ const warp = (px: number, py: number): [number, number] => {
+ const dx = px - mx;
+ const dy = py - my;
+ const d2 = dx * dx + dy * dy;
+ if (d2 < 1) return [px, py];
+ const d = Math.sqrt(d2);
+ const f = STRENGTH * Math.exp(-d2 / (2 * SIGMA * SIGMA));
+ return [px - (dx / d) * f, py - (dy / d) * f];
+ };
+
+ // ── main draw loop ────────────────────────────────────────────────────────
+ const draw = () => {
+ // Smooth-lerp mouse position (feels weighty / spacetime-like)
+ mx += (tx - mx) * 0.065;
+ my += (ty - my) * 0.065;
+
+ const W = canvas.width;
+ const H = canvas.height;
+ const active = mx > -2000;
+
+ ctx.clearRect(0, 0, W, H);
+
+ // ── horizontal grid lines ─────────────────────────────────────────────
+ for (let gy = 0; gy <= H + GRID; gy += GRID) {
+ const lineDist = active ? Math.abs(gy - my) : 9999;
+ const lift = active ? Math.max(0, 1 - lineDist / (SIGMA * 1.4)) : 0;
+ ctx.beginPath();
+ let first = true;
+ for (let x = 0; x <= W; x += 5) {
+ const [wx, wy] = active ? warp(x, gy) : [x, gy];
+ first ? ctx.moveTo(wx, wy) : ctx.lineTo(wx, wy);
+ first = false;
+ }
+ ctx.strokeStyle = `rgba(200,169,110,${0.048 + lift * 0.12})`;
+ ctx.lineWidth = 0.5 + lift * 0.55;
+ ctx.stroke();
+ }
+
+ // ── vertical grid lines ───────────────────────────────────────────────
+ for (let gx = 0; gx <= W + GRID; gx += GRID) {
+ const lineDist = active ? Math.abs(gx - mx) : 9999;
+ const lift = active ? Math.max(0, 1 - lineDist / (SIGMA * 1.4)) : 0;
+ ctx.beginPath();
+ let first = true;
+ for (let y = 0; y <= H; y += 5) {
+ const [wx, wy] = active ? warp(gx, y) : [gx, y];
+ first ? ctx.moveTo(wx, wy) : ctx.lineTo(wx, wy);
+ first = false;
+ }
+ ctx.strokeStyle = `rgba(200,169,110,${0.048 + lift * 0.12})`;
+ ctx.lineWidth = 0.5 + lift * 0.55;
+ ctx.stroke();
+ }
+
+ // ── intersection dots (lensed stars) ─────────────────────────────────
+ if (active) {
+ const cullR2 = (SIGMA * 2.2) * (SIGMA * 2.2);
+ for (let gx = 0; gx <= W + GRID; gx += GRID) {
+ for (let gy = 0; gy <= H + GRID; gy += GRID) {
+ const dx = gx - mx;
+ const dy = gy - my;
+ const d2 = dx * dx + dy * dy;
+ if (d2 > cullR2) continue;
+
+ const [wx, wy] = warp(gx, gy);
+ const intensity = Math.exp(-d2 / (2 * SIGMA * SIGMA * 0.35));
+
+ // outer halo
+ ctx.beginPath();
+ ctx.arc(wx, wy, 1.5 + intensity * 2, 0, Math.PI * 2);
+ ctx.fillStyle = `rgba(200,169,110,${0.1 + intensity * 0.5})`;
+ ctx.fill();
+
+ // bright core for very close intersections
+ if (intensity > 0.4) {
+ ctx.beginPath();
+ ctx.arc(wx, wy, 0.8, 0, Math.PI * 2);
+ ctx.fillStyle = `rgba(232,200,135,${intensity * 0.7})`;
+ ctx.fill();
+ }
+ }
+ }
+ }
+
+ // ── gravitational mass glow at cursor ─────────────────────────────────
+ if (active) {
+ // wide soft halo
+ const halo = ctx.createRadialGradient(mx, my, 0, mx, my, SIGMA * 0.85);
+ halo.addColorStop(0, "rgba(200,169,110,0.07)");
+ halo.addColorStop(0.5, "rgba(200,169,110,0.025)");
+ halo.addColorStop(1, "rgba(200,169,110,0)");
+ ctx.fillStyle = halo;
+ ctx.beginPath();
+ ctx.arc(mx, my, SIGMA * 0.85, 0, Math.PI * 2);
+ ctx.fill();
+
+ // tight bright core (the "mass")
+ const core = ctx.createRadialGradient(mx, my, 0, mx, my, 22);
+ core.addColorStop(0, "rgba(240,210,150,0.22)");
+ core.addColorStop(1, "rgba(200,169,110,0)");
+ ctx.fillStyle = core;
+ ctx.beginPath();
+ ctx.arc(mx, my, 22, 0, Math.PI * 2);
+ ctx.fill();
+ }
+
+ raf = requestAnimationFrame(draw);
+ };
+
+ draw();
+
+ return () => {
+ cancelAnimationFrame(raf);
+ window.removeEventListener("resize", resize);
+ window.removeEventListener("mousemove", onMove);
+ document.documentElement.removeEventListener("mouseleave", onLeave);
+ };
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/components/Hero.tsx b/components/Hero.tsx
new file mode 100644
index 0000000..4995f17
--- /dev/null
+++ b/components/Hero.tsx
@@ -0,0 +1,413 @@
+"use client";
+
+import SafeImage from "./SafeImage";
+
+interface Task {
+ name: string;
+ description: string;
+ status: string;
+ technologies: string[];
+}
+
+interface ProjectLink {
+ name: string;
+ url: string;
+}
+
+interface Project {
+ name: string;
+ description: string;
+ logo?: string;
+ links?: ProjectLink[];
+ tasks: Task[];
+}
+
+interface Experience {
+ company: string;
+ position: string;
+ startDate: string;
+ endDate: string;
+ logo?: string;
+}
+
+interface Education {
+ degree: string;
+ school: string;
+ startDate: string;
+ endDate: string;
+}
+
+interface HeroProps {
+ name: string;
+ lastName: string;
+ title: string;
+ description: string;
+ skills: string[];
+ projects: Project[];
+ experiences: Experience[];
+ education: Education[];
+ languages: { name: string; level: string }[];
+ interests: string[];
+ location: string;
+}
+
+// ─── BentoCard ───────────────────────────────────────────────────────────────
+
+function BentoCard({
+ num,
+ label,
+ href,
+ children,
+ accent = false,
+ delay = 0,
+}: {
+ num: string;
+ label: string;
+ href: string;
+ children: React.ReactNode;
+ accent?: boolean;
+ delay?: number;
+}) {
+ return (
+ {
+ const el = e.currentTarget as HTMLElement;
+ el.style.borderColor = "#6b5730";
+ el.style.background = accent ? "rgba(200,169,110,0.07)" : "rgba(200,169,110,0.03)";
+ el.style.transform = "translateY(-2px)";
+ const arrow = el.querySelector(".card-arrow") as HTMLElement | null;
+ if (arrow) arrow.style.opacity = "1";
+ const shine = el.querySelector(".card-shine") as HTMLElement | null;
+ if (shine) shine.style.opacity = "1";
+ }}
+ onMouseLeave={(e) => {
+ const el = e.currentTarget as HTMLElement;
+ el.style.borderColor = "#1c1f26";
+ el.style.background = accent ? "rgba(200,169,110,0.03)" : "#0e1014";
+ el.style.transform = "translateY(0)";
+ const arrow = el.querySelector(".card-arrow") as HTMLElement | null;
+ if (arrow) arrow.style.opacity = "0";
+ const shine = el.querySelector(".card-shine") as HTMLElement | null;
+ if (shine) shine.style.opacity = "0";
+ }}
+ >
+
+
+
+
+ {num} · {label}
+
+
+ ↗
+
+
+
+ {children}
+
+ );
+}
+
+// ─── Hero ─────────────────────────────────────────────────────────────────────
+
+export default function Hero({
+ name,
+ lastName,
+ title,
+ description,
+ skills,
+ projects,
+ experiences,
+ education,
+ languages,
+ interests,
+ location,
+}: HeroProps) {
+
+ const feat1 = projects[2]; // FOON
+ const feat2 = projects[0]; // Gazelle
+ const topSkills = skills.slice(0, 10);
+
+ const levelColor = (level: string) => {
+ if (level === "Native") return "#c8a96e";
+ if (level === "C2") return "#3a7a5a";
+ return "#4a6050";
+ };
+
+ return (
+
+
+
+ {/* ── Headline ─────────────────────────────────────────────────── */}
+
+
+ Let's bend
+
+ spacetime
+
+
+
+ {/* ── Name + description strip ──────────────────────────────── */}
+
+
+
+
+ {title}
+
+
+ {name}{" "}
+ {lastName}
+
+
+
+
+
+ {description}
+
+
+
+
+ {location}
+
+
+
+
+
+ {/* ── Divider ───────────────────────────────────────────────── */}
+
+
+ {/* ── 3 × 2 Bento grid ──────────────────────────────────────── */}
+
+
+ {/* 01 — FOON */}
+
+
+ {feat1.logo && (
+
+ )}
+
+ {feat1.name}
+
+
+
+ {feat1.description}
+
+
+ {feat1.tasks.flatMap((t) => t.technologies).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).map((tech) => (
+ {tech}
+ ))}
+
+ {feat1.links && (
+
+ {feat1.links.slice(0, 2).map((l) => (
+
+ ↗ {l.name}
+
+ ))}
+
+ )}
+
+
+ {/* 02 — Gazelle */}
+
+
+ {feat2.logo && (
+
+ )}
+
+ {feat2.name}
+
+
+
+ {feat2.description}
+
+
+ {feat2.tasks.map((task) => (
+
+
+
+ {task.name}
+
+ {task.status}
+
+ ))}
+
+
+
+ {/* 03 — Experience */}
+
+
+ Experience
+
+
+ {experiences.map((exp, i) => (
+
+
+
+
+ {exp.position}
+
+
+ {exp.company} · {exp.endDate === "current" ? "Present" : exp.endDate.split("-")[2]}
+
+
+
+ ))}
+
+
+
+ {/* 04 — Education */}
+
+
+ Education
+
+
+ {education.map((edu, i) => (
+
+
+ {edu.degree}
+
+
+ {edu.school.split(" ").slice(-2).join(" ")} · {edu.endDate}
+
+
+ ))}
+
+
+
+ {/* 05 — Skills */}
+
+
+ {skills.length} Skills
+
+
+ {topSkills.map((skill, i) => (
+
+ {skill}
+
+ ))}
+ {skills.length > 10 && (
+
+ +{skills.length - 10}
+
+ )}
+
+
+
+ {/* 06 — About */}
+
+
+ About
+
+
+
+ Languages
+
+
+ {languages.map((lang) => (
+
+ {lang.name} {lang.level}
+
+ ))}
+
+
+
+
+ Interests
+
+
+ {interests.map((interest, i) => (
+
+ {interest}{i < interests.length - 1 && ·}
+
+ ))}
+
+
+
+
+
+ {/* ── Scroll hint ───────────────────────────────────────────── */}
+
+
+
+ Scroll to explore
+
+
+
+
+
+ );
+}
diff --git a/components/Navigation.tsx b/components/Navigation.tsx
new file mode 100644
index 0000000..b4f1aa3
--- /dev/null
+++ b/components/Navigation.tsx
@@ -0,0 +1,219 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+const links = [
+ { label: "Projects", href: "#projects" },
+ { label: "Experience", href: "#experience" },
+ { label: "Education", href: "#education" },
+ { label: "Skills", href: "#skills" },
+ { label: "Contact", href: "#contact" },
+];
+
+export default function Navigation() {
+ const [scrolled, setScrolled] = useState(false);
+ const [open, setOpen] = useState(false);
+ const [active, setActive] = useState("");
+
+ useEffect(() => {
+ const onScroll = () => setScrolled(window.scrollY > 60);
+ window.addEventListener("scroll", onScroll, { passive: true });
+ return () => window.removeEventListener("scroll", onScroll);
+ }, []);
+
+ return (
+
+
+
+ {/* Hover underline via CSS */}
+
+
+ {/* Mobile menu */}
+
+
+ {links.map((l, i) => (
+
setOpen(false)}
+ style={{
+ fontFamily: "var(--font-jetbrains), monospace",
+ fontSize: "0.78rem",
+ letterSpacing: "0.16em",
+ textTransform: "uppercase",
+ textDecoration: "none",
+ padding: "1rem 0",
+ color: "#4a5060",
+ borderBottom: i < links.length - 1 ? "1px solid #1c1f26" : "none",
+ transition: "color 0.2s",
+ }}
+ onMouseEnter={(e) => (e.currentTarget.style.color = "#c8a96e")}
+ onMouseLeave={(e) => (e.currentTarget.style.color = "#4a5060")}
+ >
+
+ {String(i + 1).padStart(2, "0")}
+
+ {l.label}
+
+ ))}
+
+
+
+ );
+}
diff --git a/components/Projects.tsx b/components/Projects.tsx
new file mode 100644
index 0000000..d2806c2
--- /dev/null
+++ b/components/Projects.tsx
@@ -0,0 +1,322 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import SafeImage from "./SafeImage";
+
+interface Task {
+ name: string;
+ description: string;
+ status: string;
+ technologies: string[];
+}
+
+interface ProjectLink {
+ name: string;
+ url: string;
+}
+
+interface Project {
+ name: string;
+ description: string;
+ logo?: string;
+ links?: ProjectLink[];
+ tasks: Task[];
+}
+
+interface ProjectsProps {
+ projects: Project[];
+}
+
+function ProjectCard({ project, index }: { project: Project; index: number }) {
+ const [expanded, setExpanded] = useState(false);
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const el = ref.current;
+ if (!el) return;
+ const obs = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) {
+ setTimeout(() => el.classList.add("visible"), index * 80);
+ obs.disconnect();
+ }
+ },
+ { threshold: 0.1 }
+ );
+ obs.observe(el);
+ return () => obs.disconnect();
+ }, [index]);
+
+ const allTechs = Array.from(
+ new Set(project.tasks.flatMap((t) => t.technologies))
+ );
+ const doneCount = project.tasks.filter((t) => t.status === "Done").length;
+
+ return (
+
+ {/* Header */}
+
+
+ {project.logo && (
+
+ )}
+
+
+ {String(index + 1).padStart(2, "0")}
+
+
+ {project.name}
+
+
+
+
+
+
+
+ {/* Description */}
+
+ {project.description}
+
+
+ {/* Tech tags */}
+
+ {allTechs.slice(0, 8).map((tech) => (
+ {tech}
+ ))}
+ {allTechs.length > 8 && (
+
+ +{allTechs.length - 8}
+
+ )}
+
+
+ {/* Footer */}
+
+
+ {doneCount}/{project.tasks.length} tasks complete
+
+
+
+
+
+ {/* Tasks */}
+ {expanded && (
+
+
+ {project.tasks.map((task) => (
+
+
+
+
+
+ {task.name}
+
+
+ {task.status}
+
+
+
+ {task.description}
+
+
+ {task.technologies.map((tech) => (
+
+ {tech}
+
+ ))}
+
+
+
+ ))}
+
+
+ )}
+
+ );
+}
+
+export default function Projects({ projects }: ProjectsProps) {
+ const headingRef = useRef(null);
+
+ useEffect(() => {
+ const el = headingRef.current;
+ if (!el) return;
+ const obs = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) {
+ el.classList.add("visible");
+ obs.disconnect();
+ }
+ },
+ { threshold: 0.1 }
+ );
+ obs.observe(el);
+ return () => obs.disconnect();
+ }, []);
+
+ return (
+
+
+ {/* Section header */}
+
+
+ Selected Work
+
+
+
+ Projects
+
+
+ {String(projects.length).padStart(2, "0")} total
+
+
+
+
+
+ {/* Grid */}
+
+ {projects.map((project, i) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/components/SafeImage.tsx b/components/SafeImage.tsx
new file mode 100644
index 0000000..450c89b
--- /dev/null
+++ b/components/SafeImage.tsx
@@ -0,0 +1,62 @@
+"use client";
+
+import { useState } from "react";
+
+interface SafeImageProps {
+ src: string;
+ alt: string;
+ width?: number;
+ height?: number;
+ style?: React.CSSProperties;
+ fallbackLabel?: string; // initials or short label shown when image fails
+ containerStyle?: React.CSSProperties;
+}
+
+export default function SafeImage({
+ src,
+ alt,
+ width,
+ height,
+ style,
+ fallbackLabel,
+ containerStyle,
+}: SafeImageProps) {
+ const [errored, setErrored] = useState(false);
+
+ if (errored) {
+ if (!fallbackLabel) return null;
+ return (
+
+ {fallbackLabel}
+
+ );
+ }
+
+ return (
+ // eslint-disable-next-line @next/next/no-img-element
+
setErrored(true)}
+ />
+ );
+}
diff --git a/components/Skills.tsx b/components/Skills.tsx
new file mode 100644
index 0000000..863642f
--- /dev/null
+++ b/components/Skills.tsx
@@ -0,0 +1,227 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+
+interface SkillsProps {
+ skills: string[];
+ languages: { name: string; level: string }[];
+ interests: string[];
+}
+
+export default function Skills({ skills, languages, interests }: SkillsProps) {
+ const headingRef = useRef(null);
+ const skillsRef = useRef(null);
+
+ useEffect(() => {
+ const els = [headingRef.current, skillsRef.current].filter(Boolean);
+ const obs = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ entry.target.classList.add("visible");
+ }
+ });
+ },
+ { threshold: 0.1 }
+ );
+ els.forEach((el) => el && obs.observe(el));
+ return () => obs.disconnect();
+ }, []);
+
+ // Give skills a pseudo-random "weight" based on their index for visual variety
+ const weights = [1.3, 1, 1.5, 1, 1.2, 0.9, 1.4, 1, 1.1, 0.95, 1.3, 1, 1.2, 0.9, 1.4, 1, 1.1, 1.3];
+
+ const levelColor = (level: string) => {
+ if (level === "Native") return "#c8a96e";
+ if (level === "C2") return "#3a7a5a";
+ if (level === "C1") return "#6b7a50";
+ return "#4a5060";
+ };
+
+ return (
+
+
+
+
Expertise
+
+ Skills & Profile
+
+
+
+
+
+ {/* Tech skills */}
+
+
+ Technical Stack
+
+
+ {skills.map((skill, i) => (
+ {
+ const el = e.currentTarget as HTMLElement;
+ el.style.borderColor = "#c8a96e";
+ el.style.color = "#c8a96e";
+ el.style.background = "rgba(200,169,110,0.05)";
+ }}
+ onMouseLeave={(e) => {
+ const el = e.currentTarget as HTMLElement;
+ el.style.borderColor = "#1c1f26";
+ el.style.color = "#6b7280";
+ el.style.background = "#0e1014";
+ }}
+ >
+ {skill}
+
+ ))}
+
+
+
+ {/* Languages + Interests stacked */}
+
+ {/* Languages */}
+
+
+ Languages
+
+
+ {languages.map((lang) => (
+
+
+ {lang.name}
+
+
+ {lang.level}
+
+
+ ))}
+
+
+
+ {/* Interests */}
+
+
+ Interests
+
+
+ {interests.map((interest) => (
+ {
+ const el = e.currentTarget as HTMLElement;
+ el.style.borderColor = "#6b5730";
+ el.style.color = "#c8a96e";
+ }}
+ onMouseLeave={(e) => {
+ const el = e.currentTarget as HTMLElement;
+ el.style.borderColor = "#1c1f26";
+ el.style.color = "#6b7280";
+ }}
+ >
+ {interest}
+
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/data/portfolio.json b/data/portfolio.json
new file mode 100644
index 0000000..65817e5
--- /dev/null
+++ b/data/portfolio.json
@@ -0,0 +1,311 @@
+{
+ "name":"Achraf",
+ "lastName":"Achkari",
+ "projects":[
+ {
+ "name":"Gazelle",
+ "description":"Gazelle Test Bed",
+ "logo": "https://gazelle.ihe.net/files/LOGO_0.png",
+ "links":[
+ {
+ "name":"Deployed",
+ "url":"https://gazelle.ihe.net/"
+ },
+ {
+ "name":"Github",
+ "url":"https://gitlab.inria.fr/gazelle/"
+ }
+ ],
+ "tasks":[
+ {
+ "name":"Gazelle Proxy",
+ "description":"Coordinate the renovation the Core Gazelle Proxy to support new protocols and security standards",
+ "status":"Done",
+ "technologies":[
+ "Java 17/21",
+ "Jakarta EE",
+ "Quarkus",
+ "Netty",
+ "Next.js",
+ "Docker"
+ ]
+ },
+ {
+ "name":"Gazelle Test Management",
+ "description":"Develop a new test management module to orchestrate and track testing campaigns across systems",
+ "status":"In Progress",
+ "technologies":[
+ "Java 17",
+ "Jakarta EE",
+ "Quarkus",
+ "PostgreSQL"
+ ]
+ },
+ {
+ "name":"Validation Service API",
+ "description":"Create a new API to unify all the validation services consumed by Gazelle",
+ "status":"Done",
+ "technologies":[
+ "Java 17",
+ "MicroProfile",
+ "JAX-RS",
+ "TDD"
+ ]
+ },
+ {
+ "name":"Gazelle Objects Checker",
+ "description":"Renovate the validator generator engine to recent JDK and enhance the validation rules",
+ "status":"Done",
+ "technologies":[
+ "Java",
+ "OCL",
+ "XML",
+ "SOAP",
+ "BASH"
+ ]
+ }
+ ]
+ },
+ {
+ "name":"Typeson",
+ "description":"Polymorphic JSON serialization and deserialization library",
+ "logo": "/typesonlogo-light.png",
+ "links":[
+ {
+ "name":"Github",
+ "url":"https://github.com/achachraf/Typeson"
+ },
+ {
+ "name":"Maven",
+ "url":"https://mvnrepository.com/artifact/io.github.achachraf/typeson/1.0.1"
+ }
+ ],
+ "tasks":[
+ {
+ "name":"JSON Object Mapper",
+ "description":"Develop a new JSON object mapper to support polymorphic serialization and deserialization",
+ "status":"Done",
+ "technologies":[
+ "Java 17",
+ "GraphScan",
+ "Reflection",
+ "TDD"
+ ]
+ },
+ {
+ "name":"Integration with Jackson",
+ "description":"Integrate Typeson with Jackson to support polymorphic serialization and deserialization",
+ "status":"Done",
+ "technologies":[
+ "Java 17",
+ "Jackson"
+ ]
+ }
+ ]
+ },
+ {
+ "name":"FOON",
+ "description":"TypeScript SDK for semantic JSON transformation using LLMs — turn messy JSON into your schema, safely",
+ "logo": "/foon-logo.svg",
+ "links":[
+ {
+ "name":"Website",
+ "url":"https://foon.ink"
+ },
+ {
+ "name":"Github",
+ "url":"https://github.com/achachraf/foon-sdk"
+ },
+ {
+ "name":"NPM",
+ "url":"https://www.npmjs.com/package/foon-sdk"
+ }
+ ],
+ "tasks":[
+ {
+ "name":"Mapping Engine",
+ "description":"Core engine that uses LLM providers to generate semantic mapping plans, then deterministically executes them against a JSON Schema with confidence scoring",
+ "status":"Done",
+ "technologies":[
+ "TypeScript",
+ "AJV",
+ "JSONPath",
+ "LRU Cache"
+ ]
+ },
+ {
+ "name":"Security Layer",
+ "description":"Input validation, prompt injection protection, sensitive field redaction, and prototype pollution detection",
+ "status":"Done",
+ "technologies":[
+ "TypeScript",
+ "Schema Validation"
+ ]
+ },
+ {
+ "name":"Express Middleware",
+ "description":"Drop-in Express integration with dual-route transformation and configurable error handling",
+ "status":"Done",
+ "technologies":[
+ "TypeScript",
+ "Express",
+ "REST"
+ ]
+ },
+ {
+ "name":"Marketing Site & Docs",
+ "description":"Interactive demo website with live transformation showcase, MDX documentation, and waitlist backend",
+ "status":"Done",
+ "technologies":[
+ "Next.js",
+ "React 19",
+ "Tailwind CSS",
+ "MDX",
+ "Upstash Redis"
+ ]
+ }
+ ]
+ },
+ {
+ "name":"CLI Portfolio",
+ "description":"Create a CLI portfolio to display my resume",
+ "logo": "/CLIPortfolioLogo.png",
+ "tasks":[
+ {
+ "name":"Main CLI",
+ "description":"Create the main CLI with basic commands handling",
+ "status":"Done",
+ "technologies":[
+ "Next.js",
+ "SSR",
+ "Typescript"
+ ]
+ },
+ {
+ "name":"Enhanced terminal",
+ "description":"Enhance the terminal to support graphical content",
+ "status":"Done",
+ "technologies":[
+ "Next.js",
+ "Next API",
+ "SSR",
+ "Typescript",
+ "Jest"
+ ]
+ }
+ ]
+ }
+ ],
+ "experiences": [
+ {
+ "name":"techlead-kereval",
+ "company":"Kereval",
+ "position":"TechLead",
+ "description":"Coordinate the technical team to deliver the best quality software",
+ "startDate":"01-01-2023",
+ "endDate":"current",
+ "logo": "https://www.kereval.com/wp-content/uploads/2021/06/Logo_Kereval_WR.png"
+ },
+ {
+ "name":"software-engineer-kereval",
+ "company":"Kereval",
+ "position":"Software Engineer",
+ "description":"Technical Referent for the Gazelle Test Bed tools",
+ "startDate":"01-10-2021",
+ "endDate":"31-12-2022",
+ "logo": "https://www.kereval.com/wp-content/uploads/2021/06/Logo_Kereval_WR.png"
+ },
+ {
+ "name":"intern-kereval",
+ "company":"Kereval",
+ "position":"Intern",
+ "description":"Developed and improved the Gazelle Test Bed tools",
+ "startDate":"06-04-2021",
+ "endDate":"31-09-2021",
+ "logo": "https://www.kereval.com/wp-content/uploads/2021/06/Logo_Kereval_WR.png"
+ },
+ {
+ "name":"intern-veolia",
+ "company":"Veolia",
+ "position":"Intern",
+ "description":"Developed Ticketing System for the company's employees",
+ "startDate":"01-07-2019",
+ "endDate":"01-08-2019",
+ "logo": "/veolia_logo.png"
+ }
+ ],
+ "education":[
+ {
+ "degree":"Master Degree in Computer Science",
+ "school":"Université de Bretagne Occidentale",
+ "startDate":"2016",
+ "endDate":"2020",
+ "logo": "/ensa.png"
+ },
+ {
+ "degree":"Engineer Degree in Computer Science",
+ "school":"Ecole Nationale des Sciences Appliquées de Tanger",
+ "startDate":"2016",
+ "endDate":"2020",
+ "logo": "/ensa.png"
+ },
+ {
+ "degree":"Baccalauréat",
+ "school":"Lycée Ibn Batouta",
+ "startDate":"2015",
+ "endDate":"2016"
+ }
+ ],
+ "about":{
+ "title":"Software Engineer",
+ "FullName":"Achraf Achkari",
+ "image": "/me.png",
+ "description":"I am a Technial Lead at Kereval. I have a strong experience in software development and I am passionate about new technologies. I am always looking for new challenges and I am eager to learn new things.",
+ "skills":[
+ "Java",
+ "Jakarta EE",
+ "MicroProfile",
+ "Quarkus",
+ "Netty",
+ "Next.js",
+ "Docker",
+ "TDD",
+ "OCL",
+ "XML",
+ "SOAP",
+ "BASH",
+ "GraphScan",
+ "Reflection",
+ "Typescript",
+ "Jest"
+ ],
+ "contact":{
+ "email":"achrafachkari@gmail.com",
+ "phone":"+33 7 82 92 98 79",
+ "linkedin":"https://www.linkedin.com/in/achraf-achkari/",
+ "github":"https://github.com/achachraf/",
+ "Address":"Toulouse, France"
+ },
+ "languages":[
+ {
+ "name":"French",
+ "level":"C1"
+ },
+ {
+ "name":"English",
+ "level":"C2"
+ },
+ {
+ "name":"Arabic",
+ "level":"Native"
+ }
+ ],
+ "interests":[
+ "Mathematics",
+ "Piano",
+ "Writing",
+ "Football",
+ "Puzzles"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/public/CLIPortfolioLogo.png b/public/CLIPortfolioLogo.png
new file mode 100644
index 0000000..62ffde6
Binary files /dev/null and b/public/CLIPortfolioLogo.png differ
diff --git a/public/ensa.png b/public/ensa.png
new file mode 100644
index 0000000..6fee09d
Binary files /dev/null and b/public/ensa.png differ
diff --git a/public/foon-logo.svg b/public/foon-logo.svg
new file mode 100644
index 0000000..30db245
--- /dev/null
+++ b/public/foon-logo.svg
@@ -0,0 +1,9 @@
+
diff --git a/public/me.png b/public/me.png
new file mode 100644
index 0000000..e8d74f7
Binary files /dev/null and b/public/me.png differ
diff --git a/public/typesonlogo-light.png b/public/typesonlogo-light.png
new file mode 100644
index 0000000..cf4b7a9
Binary files /dev/null and b/public/typesonlogo-light.png differ
diff --git a/public/veolia_logo.png b/public/veolia_logo.png
new file mode 100644
index 0000000..e2b61d9
Binary files /dev/null and b/public/veolia_logo.png differ