add docker files
This commit is contained in:
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
README.md
|
||||||
|
.next
|
||||||
|
.git
|
||||||
54
Dockerfile
Normal file
54
Dockerfile
Normal 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"]
|
||||||
BIN
app/favicon.ico
BIN
app/favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
@@ -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({
|
||||||
|
|||||||
@@ -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's bend
|
Let'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>
|
||||||
|
<h3 style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "2.5rem", letterSpacing: "0.04em", color: "#e2e4e9", lineHeight: 1 }}>
|
||||||
{feat1.name}
|
{feat1.name}
|
||||||
</h3>
|
</h3>
|
||||||
|
<span style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.65rem", color: "#c8a96e", letterSpacing: "0.1em", textTransform: "uppercase" }}>
|
||||||
|
Featured Project
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p style={{ fontFamily: "var(--font-lora), serif", fontSize: "0.82rem", lineHeight: 1.65, color: "#6b7280", marginBottom: "1rem" }}>
|
</div>
|
||||||
|
|
||||||
|
<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,86 +419,93 @@ 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 }}>
|
||||||
|
Arsenal
|
||||||
</h3>
|
</h3>
|
||||||
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.35rem" }}>
|
<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>
|
||||||
|
<div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.6rem", letterSpacing: "0.15em", color: "#6b7280", textTransform: "uppercase", marginBottom: "0.6rem" }}>
|
||||||
Languages
|
Languages
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
|
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
|
||||||
{languages.map((lang) => (
|
{languages.map((lang) => (
|
||||||
<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" }}>
|
<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} {lang.level}
|
{lang.name} <span style={{ color: levelColor(lang.level), opacity: 0.8 }}>{lang.level}</span>
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.56rem", letterSpacing: "0.18em", color: "#4a5060", textTransform: "uppercase", marginBottom: "0.45rem" }}>
|
<div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.6rem", letterSpacing: "0.15em", color: "#6b7280", textTransform: "uppercase", marginBottom: "0.6rem" }}>
|
||||||
Interests
|
Interests
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.35rem" }}>
|
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.4rem" }}>
|
||||||
{interests.map((interest, i) => (
|
{interests.map((interest) => (
|
||||||
<span key={interest} style={{ fontFamily: "var(--font-lora), serif", fontSize: "0.78rem", fontStyle: "italic", color: "#6b7280" }}>
|
<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" }}>
|
||||||
{interest}{i < interests.length - 1 && <span style={{ color: "#1c1f26", marginLeft: "0.35rem" }}>·</span>}
|
{interest}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</BentoCard>
|
</BentoCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── 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>
|
||||||
|
|||||||
@@ -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" }}>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
10
public/favicon.svg
Normal 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&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 |
Reference in New Issue
Block a user