add docker files

This commit is contained in:
achraf
2026-04-03 18:32:22 +02:00
parent 8e62a0fa92
commit 16e7547fcc
9 changed files with 406 additions and 212 deletions

7
.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git

54
Dockerfile Normal file
View File

@@ -0,0 +1,54 @@
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json package-lock.json* ./
RUN npm ci
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
# set hostname to localhost
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -25,6 +25,9 @@ export const metadata: Metadata = {
title: "Achraf Achkari — Technical Lead & Software Engineer", title: "Achraf Achkari — Technical Lead & Software Engineer",
description: description:
"Personal portfolio and project hub. Technical Lead at Kereval, building software with Java, TypeScript, and modern frameworks.", "Personal portfolio and project hub. Technical Lead at Kereval, building software with Java, TypeScript, and modern frameworks.",
icons: {
icon: "/favicon.svg",
},
}; };
export default function RootLayout({ export default function RootLayout({

View File

@@ -1,5 +1,6 @@
"use client"; "use client";
import { useRef, useState } from "react";
import SafeImage from "./SafeImage"; import SafeImage from "./SafeImage";
interface Task { interface Task {
@@ -60,6 +61,9 @@ function BentoCard({
children, children,
accent = false, accent = false,
delay = 0, delay = 0,
className = "",
style = {},
onClick,
}: { }: {
num: string; num: string;
label: string; label: string;
@@ -67,10 +71,33 @@ function BentoCard({
children: React.ReactNode; children: React.ReactNode;
accent?: boolean; accent?: boolean;
delay?: number; delay?: number;
className?: string;
style?: React.CSSProperties;
onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
}) { }) {
const cardRef = useRef<HTMLAnchorElement>(null);
const [mousePosition, setMousePosition] = useState({ x: -1000, y: -1000 });
const [isHovered, setIsHovered] = useState(false);
const handleMouseMove = (e: React.MouseEvent<HTMLAnchorElement>) => {
if (cardRef.current) {
const rect = cardRef.current.getBoundingClientRect();
setMousePosition({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
}
};
return ( return (
<a <a
ref={cardRef}
href={href} href={href}
onClick={onClick}
className={`bento-card ${className}`}
onMouseMove={handleMouseMove}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{ style={{
animationName: "fadeUp", animationName: "fadeUp",
animationDuration: "0.7s", animationDuration: "0.7s",
@@ -80,59 +107,46 @@ function BentoCard({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
padding: "1.75rem", padding: "1.75rem",
border: "1px solid #1c1f26", border: "1px solid rgba(255,255,255,0.05)",
background: accent ? "rgba(200,169,110,0.03)" : "#0e1014", background: accent ? "rgba(200,169,110,0.03)" : "rgba(14,16,20,0.4)",
backdropFilter: "blur(12px)",
textDecoration: "none", textDecoration: "none",
color: "inherit", color: "inherit",
position: "relative", position: "relative",
overflow: "hidden", overflow: "hidden",
cursor: "pointer", cursor: "pointer",
transition: "border-color 0.3s ease, background 0.3s ease, transform 0.3s ease", transition: "border-color 0.4s ease, transform 0.4s ease, background 0.4s ease, box-shadow 0.4s ease",
minHeight: "200px", borderRadius: "16px",
}} minHeight: "220px",
onMouseEnter={(e) => { boxShadow: isHovered ? "0 8px 32px rgba(0,0,0,0.4)" : "0 4px 16px rgba(0,0,0,0.2)",
const el = e.currentTarget as HTMLElement; transform: isHovered ? "translateY(-4px) scale(1.01)" : "translateY(0) scale(1)",
el.style.borderColor = "#6b5730"; borderColor: isHovered ? "rgba(200,169,110,0.3)" : "rgba(255,255,255,0.05)",
el.style.background = accent ? "rgba(200,169,110,0.07)" : "rgba(200,169,110,0.03)"; ...style,
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";
}} }}
> >
{/* Spotlight Effect */}
<div <div
className="card-shine"
style={{ style={{
position: "absolute", position: "absolute",
inset: 0, inset: 0,
background: "linear-gradient(135deg, rgba(200,169,110,0.06) 0%, transparent 50%)", background: `radial-gradient(500px circle at ${mousePosition.x}px ${mousePosition.y}px, rgba(200,169,110,0.12), transparent 40%)`,
opacity: 0, opacity: isHovered ? 1 : 0,
transition: "opacity 0.3s", transition: "opacity 0.4s ease",
pointerEvents: "none", pointerEvents: "none",
zIndex: 0,
}} }}
/> />
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: "auto" }}> <div style={{ position: "relative", zIndex: 1, display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: "auto" }}>
<div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.58rem", letterSpacing: "0.2em", color: "#c8a96e", opacity: 0.7 }}> <div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.58rem", letterSpacing: "0.2em", color: "#c8a96e", opacity: 0.8 }}>
{num} · {label} {num} · {label}
</div> </div>
<div className="card-arrow" style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.75rem", color: "#c8a96e", opacity: 0, transition: "opacity 0.2s" }}> <div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.75rem", color: "#c8a96e", opacity: isHovered ? 1 : 0, transition: "opacity 0.3s ease, transform 0.3s ease", transform: isHovered ? "translate(2px, -2px)" : "translate(0, 0)" }}>
</div> </div>
</div> </div>
<div style={{ marginTop: "1rem", flex: 1 }}>{children}</div> <div style={{ marginTop: "1.25rem", flex: 1, position: "relative", zIndex: 1, display: "flex", flexDirection: "column" }}>{children}</div>
</a> </a>
); );
} }
@@ -152,11 +166,17 @@ export default function Hero({
interests, interests,
location, location,
}: HeroProps) { }: HeroProps) {
const feat1 = projects.find(p => p.name === "FOON") || projects[0];
const feat1 = projects[2]; // FOON const feat2 = projects.find(p => p.name === "Gazelle") || projects[1];
const feat2 = projects[0]; // Gazelle
const topSkills = skills.slice(0, 10); const topSkills = skills.slice(0, 10);
const handleFeatureClick = (projectName: string) => {
const idx = projects.findIndex((p) => p.name === projectName);
if (idx !== -1) {
window.dispatchEvent(new CustomEvent("openProject", { detail: { index: idx } }));
}
};
const levelColor = (level: string) => { const levelColor = (level: string) => {
if (level === "Native") return "#c8a96e"; if (level === "Native") return "#c8a96e";
if (level === "C2") return "#3a7a5a"; if (level === "C2") return "#3a7a5a";
@@ -167,9 +187,47 @@ export default function Hero({
<section <section
id="hero" id="hero"
className="hero-section" className="hero-section"
style={{ minHeight: "100vh", position: "relative" }} style={{ minHeight: "100vh", position: "relative", zIndex: 10 }}
> >
<div style={{ maxWidth: "1200px", margin: "0 auto", position: "relative", zIndex: 3 }}> <style>{`
.creative-bento-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1.25rem;
width: 100%;
position: relative;
}
.bento-span-2x2 { grid-column: span 2; grid-row: span 2; }
.bento-span-2x1 { grid-column: span 2; grid-row: span 1; }
.bento-span-1x1 { grid-column: span 1; grid-row: span 1; }
@media (max-width: 1024px) {
.creative-bento-grid {
grid-template-columns: repeat(2, 1fr);
}
.bento-span-2x2 { grid-column: span 2; grid-row: span 2; }
.bento-span-2x1 { grid-column: span 2; grid-row: span 1; }
.bento-span-1x1 { grid-column: span 1; grid-row: span 1; }
}
@media (max-width: 600px) {
.creative-bento-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
.bento-span-2x2, .bento-span-2x1, .bento-span-1x1 {
grid-column: span 1;
grid-row: auto;
}
}
.glow-text {
text-shadow: 0 0 24px rgba(200, 169, 110, 0.4);
}
`}</style>
<div style={{ maxWidth: "1200px", margin: "0 auto", position: "relative" }}>
{/* ── Headline ─────────────────────────────────────────────────── */} {/* ── Headline ─────────────────────────────────────────────────── */}
<div <div
@@ -181,9 +239,9 @@ export default function Hero({
fontFamily: "var(--font-lora), Georgia, serif", fontFamily: "var(--font-lora), Georgia, serif",
fontStyle: "italic", fontStyle: "italic",
fontWeight: 400, fontWeight: 400,
fontSize: "clamp(2.6rem, 7vw, 7rem)", fontSize: "clamp(3rem, 7vw, 7.5rem)",
lineHeight: 1.05, lineHeight: 1.05,
letterSpacing: "-0.01em", letterSpacing: "-0.02em",
color: "#e2e4e9", color: "#e2e4e9",
margin: 0, margin: 0,
userSelect: "none", userSelect: "none",
@@ -191,7 +249,7 @@ export default function Hero({
> >
Let&apos;s bend Let&apos;s bend
<br /> <br />
<span style={{ color: "#c8a96e" }}>spacetime</span> <span style={{ color: "#c8a96e" }} className="glow-text">spacetime</span>
</h1> </h1>
</div> </div>
@@ -208,7 +266,7 @@ export default function Hero({
<div <div
style={{ style={{
fontFamily: "var(--font-bebas), Impact, sans-serif", fontFamily: "var(--font-bebas), Impact, sans-serif",
fontSize: "clamp(1.6rem, 4vw, 3rem)", fontSize: "clamp(1.8rem, 4.5vw, 3.5rem)",
letterSpacing: "0.04em", letterSpacing: "0.04em",
color: "#6b7280", color: "#6b7280",
lineHeight: 1, lineHeight: 1,
@@ -219,17 +277,14 @@ export default function Hero({
</div> </div>
</div> </div>
<div style={{ maxWidth: "360px" }}> <div style={{ maxWidth: "420px" }}>
<p style={{ fontFamily: "var(--font-lora), serif", fontSize: "0.88rem", lineHeight: 1.75, color: "#6b7280", margin: 0 }}> <p style={{ fontFamily: "var(--font-lora), serif", fontSize: "0.95rem", lineHeight: 1.75, color: "#8b92a5", margin: 0 }}>
{description} {description}
</p> </p>
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem", marginTop: "0.6rem" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.6rem", marginTop: "1rem" }}>
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#4a5060" strokeWidth="2"> <div style={{ width: "8px", height: "8px", borderRadius: "50%", background: "#c8a96e", boxShadow: "0 0 8px #c8a96e" }} />
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" /> <span style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.65rem", letterSpacing: "0.15em", color: "#c8a96e", textTransform: "uppercase" }}>
<circle cx="12" cy="10" r="3" /> Based in {location}
</svg>
<span style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.62rem", letterSpacing: "0.1em", color: "#4a5060" }}>
{location}
</span> </span>
</div> </div>
</div> </div>
@@ -238,36 +293,68 @@ export default function Hero({
{/* ── Divider ───────────────────────────────────────────────── */} {/* ── Divider ───────────────────────────────────────────────── */}
<div <div
className="animate-fade-up delay-3" className="animate-fade-up delay-3"
style={{ height: "1px", background: "#1c1f26", marginBottom: "2rem" }} style={{ height: "1px", background: "linear-gradient(90deg, rgba(200,169,110,0.5) 0%, transparent 100%)", marginBottom: "3rem", opacity: 0.3 }}
/> />
{/* ── 3 × 2 Bento grid ──────────────────────────────────────── */} {/* ── Creative Bento grid ──────────────────────────────────────── */}
<div className="hero-bento-grid"> <div className="creative-bento-grid">
{/* 01 — FOON */} {/* 01 — FOON (Span 2x2: Large Feature) */}
<BentoCard num="01" label="TypeScript SDK" href="#projects" accent delay={500}> <BentoCard
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", marginBottom: "0.75rem" }}> num="01"
label="Showcase"
href="#projects"
accent
delay={400}
className="bento-span-2x2"
onClick={() => handleFeatureClick(feat1.name)}
>
<div style={{ display: "flex", alignItems: "center", gap: "1rem", marginBottom: "1.25rem" }}>
{feat1.logo && ( {feat1.logo && (
<SafeImage src={feat1.logo} alt={feat1.name} width={28} height={28} <div style={{ background: "rgba(255,255,255,0.05)", padding: "12px", borderRadius: "12px", backdropFilter: "blur(4px)", border: "1px solid rgba(255,255,255,0.05)" }}>
style={{ width: "22px", height: "22px", objectFit: "contain", filter: "brightness(0.9)" }} <SafeImage src={feat1.logo} alt={feat1.name} width={40} height={40}
/> style={{ width: "32px", height: "32px", objectFit: "contain", filter: "brightness(1.1)" }}
/>
</div>
)} )}
<h3 style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "1.8rem", letterSpacing: "0.04em", color: "#e2e4e9", lineHeight: 1 }}> <div>
{feat1.name} <h3 style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "2.5rem", letterSpacing: "0.04em", color: "#e2e4e9", lineHeight: 1 }}>
</h3> {feat1.name}
</h3>
<span style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.65rem", color: "#c8a96e", letterSpacing: "0.1em", textTransform: "uppercase" }}>
Featured Project
</span>
</div>
</div> </div>
<p style={{ fontFamily: "var(--font-lora), serif", fontSize: "0.82rem", lineHeight: 1.65, color: "#6b7280", marginBottom: "1rem" }}>
<p style={{ fontFamily: "var(--font-lora), serif", fontSize: "1rem", lineHeight: 1.7, color: "#9ca3af", marginBottom: "1.5rem" }}>
{feat1.description} {feat1.description}
</p> </p>
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.35rem", marginBottom: "0.75rem" }}>
{feat1.tasks.flatMap((t) => t.technologies).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).map((tech) => ( <div style={{ flex: 1, display: "flex", flexDirection: "column", gap: "0.75rem", marginBottom: "1.5rem" }}>
<span key={tech} className="tech-tag">{tech}</span> {feat1.tasks.slice(0, 2).map((task) => (
<div key={task.name} style={{ background: "rgba(0,0,0,0.2)", borderRadius: "8px", padding: "10px 12px", border: "1px solid rgba(255,255,255,0.03)" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "4px" }}>
<span style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.7rem", color: "#e2e4e9" }}>{task.name}</span>
<span className={task.status === "Done" ? "status-done" : "status-progress"} style={{ fontSize: "0.5rem", padding: "2px 4px" }}>{task.status}</span>
</div>
<div style={{ fontFamily: "var(--font-lora), serif", fontSize: "0.8rem", color: "#6b7280", lineHeight: 1.4 }}>
{task.description.length > 80 ? task.description.slice(0, 80) + "..." : task.description}
</div>
</div>
))} ))}
</div> </div>
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.4rem", marginBottom: "1rem" }}>
{feat1.tasks.flatMap((t) => t.technologies).filter((v, i, a) => a.indexOf(v) === i).slice(0, 6).map((tech) => (
<span key={tech} className="tech-tag" style={{ borderRadius: "4px", background: "rgba(200,169,110,0.05)" }}>{tech}</span>
))}
</div>
{feat1.links && ( {feat1.links && (
<div style={{ display: "flex", gap: "0.6rem", flexWrap: "wrap", marginTop: "auto" }}> <div style={{ display: "flex", gap: "1rem", flexWrap: "wrap", marginTop: "auto", borderTop: "1px solid rgba(255,255,255,0.05)", paddingTop: "1rem" }}>
{feat1.links.slice(0, 2).map((l) => ( {feat1.links.map((l) => (
<span key={l.name} style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.6rem", letterSpacing: "0.1em", color: "#c8a96e", textTransform: "uppercase" }}> <span key={l.name} style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.65rem", letterSpacing: "0.1em", color: "#c8a96e", textTransform: "uppercase", display: "flex", alignItems: "center", gap: "4px" }}>
{l.name} {l.name}
</span> </span>
))} ))}
@@ -275,50 +362,56 @@ export default function Hero({
)} )}
</BentoCard> </BentoCard>
{/* 02 — Gazelle */} {/* 02 — Gazelle (Span 2x1) */}
<BentoCard num="02" label="Java · Healthcare" href="#projects" delay={600}> <BentoCard
num="02"
label="Enterprise"
href="#projects"
delay={500}
className="bento-span-2x1"
onClick={() => handleFeatureClick(feat2.name)}
>
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", marginBottom: "0.75rem" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", marginBottom: "0.75rem" }}>
{feat2.logo && ( {feat2.logo && (
<SafeImage src={feat2.logo} alt={feat2.name} width={28} height={28} <SafeImage src={feat2.logo} alt={feat2.name} width={28} height={28}
fallbackLabel={feat2.name.slice(0, 2).toUpperCase()} fallbackLabel={feat2.name.slice(0, 2).toUpperCase()}
style={{ width: "22px", height: "22px", objectFit: "contain", filter: "brightness(0.75) grayscale(0.3)" }} style={{ width: "24px", height: "24px", objectFit: "contain", filter: "brightness(0.9) grayscale(0.2)" }}
/> />
)} )}
<h3 style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "1.8rem", letterSpacing: "0.04em", color: "#e2e4e9", lineHeight: 1 }}> <h3 style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "2rem", letterSpacing: "0.04em", color: "#e2e4e9", lineHeight: 1 }}>
{feat2.name} {feat2.name}
</h3> </h3>
</div> </div>
<p style={{ fontFamily: "var(--font-lora), serif", fontSize: "0.82rem", lineHeight: 1.65, color: "#6b7280", marginBottom: "1rem" }}> <p style={{ fontFamily: "var(--font-lora), serif", fontSize: "0.85rem", lineHeight: 1.65, color: "#8b92a5", marginBottom: "1rem", flex: 1 }}>
{feat2.description} {feat2.description}
</p> </p>
<div style={{ display: "flex", flexDirection: "column", gap: "0.4rem" }}> <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.5rem" }}>
{feat2.tasks.map((task) => ( {feat2.tasks.slice(0, 4).map((task) => (
<div key={task.name} style={{ display: "flex", alignItems: "center", gap: "0.6rem" }}> <div key={task.name} style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
<span style={{ width: "5px", height: "5px", borderRadius: "50%", background: task.status === "Done" ? "#3a7a5a" : "#c8954a", flexShrink: 0 }} /> <span style={{ width: "6px", height: "6px", borderRadius: "50%", background: task.status === "Done" ? "#3a7a5a" : "#c8954a", flexShrink: 0, boxShadow: `0 0 4px ${task.status === "Done" ? "#3a7a5a" : "#c8954a"}` }} />
<span style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.62rem", color: "#6b7280", letterSpacing: "0.04em" }}> <span style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.65rem", color: "#9ca3af", letterSpacing: "0.02em", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
{task.name} {task.name}
</span> </span>
<span className={task.status === "Done" ? "status-done" : "status-progress"}>{task.status}</span>
</div> </div>
))} ))}
</div> </div>
</BentoCard> </BentoCard>
{/* 03 — Experience */} {/* 03 — Experience (Span 1x1) */}
<BentoCard num="03" label="Career" href="#experience" delay={700}> <BentoCard num="03" label="Career" href="#experience" delay={600} className="bento-span-1x1">
<h3 style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "1.8rem", letterSpacing: "0.04em", color: "#e2e4e9", lineHeight: 1, marginBottom: "1rem" }}> <h3 style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "1.8rem", letterSpacing: "0.04em", color: "#e2e4e9", lineHeight: 1, marginBottom: "1rem" }}>
Experience Experience
</h3> </h3>
<div style={{ display: "flex", flexDirection: "column", gap: "0.65rem" }}> <div style={{ display: "flex", flexDirection: "column", gap: "0.8rem", flex: 1 }}>
{experiences.map((exp, i) => ( {experiences.slice(0, 2).map((exp, i) => (
<div key={exp.company + exp.position} style={{ display: "flex", alignItems: "flex-start", gap: "0.75rem", opacity: i === 0 ? 1 : i === 1 ? 0.6 : 0.35 }}> <div key={exp.company + exp.position} style={{ display: "flex", alignItems: "flex-start", gap: "0.75rem", opacity: i === 0 ? 1 : 0.6 }}>
<div style={{ width: "6px", height: "6px", borderRadius: "50%", background: i === 0 ? "#c8a96e" : "#1c1f26", border: i === 0 ? "none" : "1px solid #4a5060", marginTop: "5px", flexShrink: 0 }} /> <div style={{ width: "6px", height: "6px", borderRadius: "50%", background: i === 0 ? "#c8a96e" : "transparent", border: i === 0 ? "none" : "1px solid #4a5060", marginTop: "6px", flexShrink: 0 }} />
<div> <div>
<div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.7rem", color: i === 0 ? "#e2e4e9" : "#6b7280", letterSpacing: "0.04em", lineHeight: 1.3 }}> <div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.7rem", color: i === 0 ? "#e2e4e9" : "#8b92a5", letterSpacing: "0.02em", lineHeight: 1.3 }}>
{exp.position} {exp.position}
</div> </div>
<div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.58rem", letterSpacing: "0.12em", color: i === 0 ? "#c8a96e" : "#4a5060", textTransform: "uppercase", marginTop: "1px" }}> <div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.6rem", letterSpacing: "0.1em", color: i === 0 ? "#c8a96e" : "#4a5060", textTransform: "uppercase", marginTop: "2px" }}>
{exp.company} · {exp.endDate === "current" ? "Present" : exp.endDate.split("-")[2]} {exp.company}
</div> </div>
</div> </div>
</div> </div>
@@ -326,71 +419,78 @@ export default function Hero({
</div> </div>
</BentoCard> </BentoCard>
{/* 04 — Education */} {/* 04 — Education (Span 1x1) */}
<BentoCard num="04" label="Academic" href="#education" delay={800}> <BentoCard num="04" label="Academic" href="#education" delay={700} className="bento-span-1x1">
<h3 style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "1.8rem", letterSpacing: "0.04em", color: "#e2e4e9", lineHeight: 1, marginBottom: "1rem" }}> <h3 style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "1.8rem", letterSpacing: "0.04em", color: "#e2e4e9", lineHeight: 1, marginBottom: "1rem" }}>
Education Education
</h3> </h3>
<div style={{ display: "flex", flexDirection: "column", gap: "0.85rem" }}> <div style={{ display: "flex", flexDirection: "column", gap: "0.85rem", flex: 1 }}>
{education.map((edu, i) => ( {education.slice(0, 2).map((edu, i) => (
<div key={edu.degree} style={{ borderLeft: "2px solid #1c1f26", paddingLeft: "0.75rem" }}> <div key={edu.degree} style={{ borderLeft: i === 0 ? "2px solid #c8a96e" : "2px solid rgba(255,255,255,0.1)", paddingLeft: "0.75rem" }}>
<div style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "0.88rem", letterSpacing: "0.06em", color: i === 0 ? "#e2e4e9" : "#6b7280", lineHeight: 1.3, marginBottom: "0.2rem" }}> <div style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "1rem", letterSpacing: "0.04em", color: i === 0 ? "#e2e4e9" : "#8b92a5", lineHeight: 1.2, marginBottom: "0.2rem" }}>
{edu.degree} {edu.degree}
</div> </div>
<div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.58rem", letterSpacing: "0.1em", color: "#4a5060", textTransform: "uppercase" }}> <div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.6rem", letterSpacing: "0.08em", color: "#4a5060", textTransform: "uppercase" }}>
{edu.school.split(" ").slice(-2).join(" ")} · {edu.endDate} {edu.school.split(" ").slice(-2).join(" ")}
</div> </div>
</div> </div>
))} ))}
</div> </div>
</BentoCard> </BentoCard>
{/* 05 — Skills */} {/* 05 — Skills (Span 2x1) */}
<BentoCard num="05" label="Stack" href="#skills" delay={900}> <BentoCard num="05" label="Stack" href="#skills" delay={800} className="bento-span-2x1">
<h3 style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "1.8rem", letterSpacing: "0.04em", color: "#e2e4e9", lineHeight: 1, marginBottom: "1rem" }}> <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: "1rem" }}>
{skills.length} Skills <h3 style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "2rem", letterSpacing: "0.04em", color: "#e2e4e9", lineHeight: 1, margin: 0 }}>
</h3> Arsenal
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.35rem" }}> </h3>
<span style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.65rem", color: "#c8a96e", letterSpacing: "0.1em" }}>
{skills.length} TECHNOLOGIES
</span>
</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.5rem" }}>
{topSkills.map((skill, i) => ( {topSkills.map((skill, i) => (
<span key={skill} className="tech-tag" style={{ opacity: 1 - i * 0.06, fontSize: i < 3 ? "0.68rem" : "0.6rem" }}> <span key={skill} className="tech-tag" style={{ borderRadius: "6px", padding: "4px 10px", fontSize: "0.7rem", background: i < 3 ? "rgba(200,169,110,0.1)" : "rgba(255,255,255,0.03)", borderColor: i < 3 ? "rgba(200,169,110,0.3)" : "rgba(255,255,255,0.05)", color: i < 3 ? "#e8c887" : "#9ca3af" }}>
{skill} {skill}
</span> </span>
))} ))}
{skills.length > 10 && ( {skills.length > 10 && (
<span className="tech-tag" style={{ color: "#c8a96e", borderColor: "#6b5730" }}> <span className="tech-tag" style={{ borderRadius: "6px", padding: "4px 10px", fontSize: "0.7rem", background: "transparent", borderColor: "dashed 1px rgba(255,255,255,0.1)", color: "#4a5060" }}>
+{skills.length - 10} +{skills.length - 10} more
</span> </span>
)} )}
</div> </div>
</BentoCard> </BentoCard>
{/* 06 — About */} {/* 06 — About (Span 2x1) */}
<BentoCard num="06" label="Profile" href="#contact" accent delay={1000}> <BentoCard num="06" label="Profile" href="#contact" accent delay={900} className="bento-span-2x1">
<h3 style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "1.8rem", letterSpacing: "0.04em", color: "#e2e4e9", lineHeight: 1, marginBottom: "1rem" }}> <h3 style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "2rem", letterSpacing: "0.04em", color: "#e2e4e9", lineHeight: 1, marginBottom: "1.2rem" }}>
About Beyond Code
</h3> </h3>
<div style={{ marginBottom: "0.85rem" }}> <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1.5rem" }}>
<div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.56rem", letterSpacing: "0.18em", color: "#4a5060", textTransform: "uppercase", marginBottom: "0.45rem" }}> <div>
Languages <div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.6rem", letterSpacing: "0.15em", color: "#6b7280", textTransform: "uppercase", marginBottom: "0.6rem" }}>
Languages
</div>
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
{languages.map((lang) => (
<span key={lang.name} style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.65rem", padding: "3px 8px", borderRadius: "4px", background: `linear-gradient(90deg, ${levelColor(lang.level)}22, transparent)`, borderLeft: `2px solid ${levelColor(lang.level)}`, color: "#e2e4e9", letterSpacing: "0.04em" }}>
{lang.name} <span style={{ color: levelColor(lang.level), opacity: 0.8 }}>{lang.level}</span>
</span>
))}
</div>
</div> </div>
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}> <div>
{languages.map((lang) => ( <div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.6rem", letterSpacing: "0.15em", color: "#6b7280", textTransform: "uppercase", marginBottom: "0.6rem" }}>
<span key={lang.name} style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.62rem", padding: "2px 8px", border: `1px solid ${levelColor(lang.level)}`, color: levelColor(lang.level), letterSpacing: "0.06em" }}> Interests
{lang.name} {lang.level} </div>
</span> <div style={{ display: "flex", flexWrap: "wrap", gap: "0.4rem" }}>
))} {interests.map((interest) => (
</div> <span key={interest} style={{ fontFamily: "var(--font-lora), serif", fontSize: "0.85rem", fontStyle: "italic", color: "#8b92a5", background: "rgba(255,255,255,0.02)", padding: "2px 8px", borderRadius: "12px" }}>
</div> {interest}
<div> </span>
<div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.56rem", letterSpacing: "0.18em", color: "#4a5060", textTransform: "uppercase", marginBottom: "0.45rem" }}> ))}
Interests </div>
</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.35rem" }}>
{interests.map((interest, i) => (
<span key={interest} style={{ fontFamily: "var(--font-lora), serif", fontSize: "0.78rem", fontStyle: "italic", color: "#6b7280" }}>
{interest}{i < interests.length - 1 && <span style={{ color: "#1c1f26", marginLeft: "0.35rem" }}>·</span>}
</span>
))}
</div> </div>
</div> </div>
</BentoCard> </BentoCard>
@@ -399,13 +499,13 @@ export default function Hero({
{/* ── Scroll hint ───────────────────────────────────────────── */} {/* ── Scroll hint ───────────────────────────────────────────── */}
<div <div
className="animate-fade-up delay-6" className="animate-fade-up delay-6"
style={{ marginTop: "2rem", display: "flex", alignItems: "center", justifyContent: "center", gap: "0.75rem" }} style={{ marginTop: "3rem", display: "flex", alignItems: "center", justifyContent: "center", gap: "1rem" }}
> >
<div style={{ height: "1px", width: "40px", background: "#1c1f26" }} /> <div style={{ height: "1px", width: "40px", background: "linear-gradient(90deg, transparent, rgba(200,169,110,0.5))" }} />
<span style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.58rem", letterSpacing: "0.2em", color: "#4a5060", textTransform: "uppercase" }}> <span style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.65rem", letterSpacing: "0.2em", color: "#c8a96e", textTransform: "uppercase", opacity: 0.8 }} className="scroll-indicator">
Scroll to explore Scroll to explore
</span> </span>
<div style={{ height: "1px", width: "40px", background: "#1c1f26" }} /> <div style={{ height: "1px", width: "40px", background: "linear-gradient(-90deg, transparent, rgba(200,169,110,0.5))" }} />
</div> </div>
</div> </div>
</section> </section>

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState } from "react"; import { useState, useEffect } from "react";
import SafeImage from "./SafeImage"; import SafeImage from "./SafeImage";
interface Task { interface Task {
@@ -296,6 +296,16 @@ function ProjectRow({
export default function Projects({ projects }: ProjectsProps) { export default function Projects({ projects }: ProjectsProps) {
const [openIndex, setOpenIndex] = useState<number | null>(0); const [openIndex, setOpenIndex] = useState<number | null>(0);
useEffect(() => {
const handleOpenProject = (e: CustomEvent) => {
if (typeof e.detail?.index === "number") {
setOpenIndex(e.detail.index);
}
};
window.addEventListener("openProject", handleOpenProject as EventListener);
return () => window.removeEventListener("openProject", handleOpenProject as EventListener);
}, []);
return ( return (
<section id="projects" style={{ padding: "8rem 0" }}> <section id="projects" style={{ padding: "8rem 0" }}>
<div style={{ maxWidth: "1200px", margin: "0 auto", padding: "0 2rem" }}> <div style={{ maxWidth: "1200px", margin: "0 auto", padding: "0 2rem" }}>

View File

@@ -2,6 +2,69 @@
"name": "Achraf", "name": "Achraf",
"lastName": "Achkari", "lastName": "Achkari",
"projects": [ "projects": [
{
"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": "Gazelle", "name": "Gazelle",
"description": "Gazelle Test Bed", "description": "Gazelle Test Bed",
@@ -103,69 +166,6 @@
} }
] ]
}, },
{
"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", "name": "CLI Portfolio",
"description": "Create a CLI portfolio to display my resume", "description": "Create a CLI portfolio to display my resume",
@@ -256,28 +256,37 @@
"endDate": "2016" "endDate": "2016"
} }
], ],
"certificates": [],
"about": { "about": {
"title": "Techlead Software Engineer", "title": "Techlead Software Engineer",
"FullName": "Achraf Achkari", "FullName": "Achraf Achkari",
"image": "/me.png", "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.", "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": [ "skills": [
"Java", "Java 17/21",
"Jakarta EE", "TypeScript",
"MicroProfile", "Next.js / React",
"Quarkus", "Quarkus",
"Netty", "Jakarta EE",
"Next.js",
"Docker", "Docker",
"TDD", "PostgreSQL",
"Technical Leadership",
"Microservices",
"TDD & Clean Code",
"Node.js (Express)",
"REST APIs (JAX-RS)",
"Tailwind CSS",
"Redis",
"Agile / Scrum",
"Software Architecture",
"Netty",
"MicroProfile",
"Jest",
"Linux & Bash",
"JSON Schema",
"SOAP / XML",
"OCL", "OCL",
"XML", "Engine Dev / Reflection"
"SOAP",
"BASH",
"GraphScan",
"Reflection",
"Typescript",
"Jest"
], ],
"contact": { "contact": {
"email": "achrafachkari@gmail.com", "email": "achrafachkari@gmail.com",

View File

@@ -1,6 +1,7 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: 'standalone',
allowedDevOrigins: process.env.ALLOWED_DEV_ORIGINS allowedDevOrigins: process.env.ALLOWED_DEV_ORIGINS
? process.env.ALLOWED_DEV_ORIGINS.split(",") ? process.env.ALLOWED_DEV_ORIGINS.split(",")
: [], : [],

10
public/favicon.svg Normal file
View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<style>
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&amp;display=swap');
text {
font-family: 'Bebas Neue', Archivo, sans-serif;
}
</style>
<rect x="4" y="4" width="92" height="92" fill="#07080a" stroke="#c8a96e" stroke-width="4" />
<text x="52" y="52" font-size="52" fill="#c8a96e" text-anchor="middle" dominant-baseline="central" letter-spacing="2">AA</text>
</svg>

After

Width:  |  Height:  |  Size: 476 B