jalil.arfaoui.net/src/pages/code/parcours.astro

228 lines
11 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
import { getCollection, render } from "astro:content";
import { Image } from "astro:assets";
import Layout from "../../layouts/main.astro";
const logos = import.meta.glob<{ default: ImageMetadata }>('../../assets/images/companies/*.png', { eager: true });
function getLogo(filename?: string) {
if (!filename) return null;
const key = `../../assets/images/companies/${filename}`;
return logos[key]?.default ?? null;
}
function getInitials(company: string) {
return company.split(/[\s-]+/).map(w => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase();
}
const locale = "fr";
const experiences = (await getCollection("experiences"))
.filter((e) => e.data.lang === locale && !e.data.draft)
.sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1));
const typeLabels: Record<string, string> = {
employment: 'Emploi', freelance: 'Freelance', teaching: 'Enseignement',
community: 'Communauté', entrepreneurship: 'Entrepreneuriat',
};
const typeColors: Record<string, { badge: string; border: string; dot: string }> = {
employment: { badge: 'type-badge-blue', border: 'type-border-blue', dot: 'bg-blue-400 border-blue-400/50' },
freelance: { badge: 'type-badge-amber', border: 'type-border-amber', dot: 'bg-amber-400 border-amber-400/50' },
teaching: { badge: 'type-badge-emerald', border: 'type-border-emerald', dot: 'bg-emerald-400 border-emerald-400/50' },
community: { badge: 'type-badge-pink', border: 'type-border-pink', dot: 'bg-pink-400 border-pink-400/50' },
entrepreneurship: { badge: 'type-badge-purple', border: 'type-border-purple', dot: 'bg-purple-400 border-purple-400/50' },
};
function formatMonth(dateStr: string) {
const [year, month] = dateStr.split('-');
return new Date(parseInt(year), parseInt(month) - 1)
.toLocaleDateString('fr-FR', { year: 'numeric', month: 'short' });
}
function computeDuration(startStr: string, endStr?: string) {
const [sy, sm] = startStr.split('-').map(Number);
const end = endStr ? endStr.split('-').map(Number) : [new Date().getFullYear(), new Date().getMonth() + 1];
const totalMonths = (end[0] - sy) * 12 + (end[1] - sm);
const years = Math.floor(totalMonths / 12);
const months = totalMonths % 12;
if (years === 0) return `${months} mois`;
if (months === 0) return `${years} an${years > 1 ? 's' : ''}`;
return `${years} an${years > 1 ? 's' : ''} ${months} mois`;
}
---
<Layout
title="Parcours - Jalil Arfaoui"
facet="code"
description="Parcours professionnel de Jalil Arfaoui : plus de 20 ans d'expérience en développement logiciel, de développeur junior à lead developer et CTO."
>
<section class="relative z-20 max-w-3xl mx-auto my-12 px-7 lg:px-0">
<div class="mb-10">
<a href="/code" class="inline-flex items-center gap-1.5 text-sm text-white/50 hover:text-white/80 transition-colors mb-6 group">
<svg class="w-4 h-4 group-hover:-translate-x-0.5 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
Code
</a>
<h1 class="text-3xl sm:text-4xl font-bold text-white">Parcours</h1>
<p class="mt-3 text-white/60 text-lg">Plus de 20 ans d'expérience en développement logiciel.</p>
</div>
<div class="mt-10">
{experiences.map(async (exp) => {
const { Content } = await render(exp);
const isOngoing = !exp.data.endDate;
const start = formatMonth(exp.data.startDate);
const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'Présent';
const duration = computeDuration(exp.data.startDate, exp.data.endDate);
const colors = typeColors[exp.data.type] || typeColors.employment;
const label = typeLabels[exp.data.type] || exp.data.type;
return (
<div class="relative pl-10 pb-10 border-l-2 border-white/[0.12] last:pb-0">
<div
class:list={[
"absolute -left-[7px] top-2 w-3 h-3 rounded-full border-2",
isOngoing ? colors.dot : "border-white/20 bg-white/10"
]}
>
{isOngoing && <div class="absolute inset-0 rounded-full bg-current opacity-40 animate-ping" />}
</div>
<div class:list={[
"exp-card relative rounded-2xl p-6 transition-colors duration-200 border-l-[3px]",
isOngoing ? "exp-card--ongoing" : "",
colors.border
]}>
{getLogo(exp.data.logo) ? (
<div class="exp-logo absolute top-5 right-5 w-16 h-16 rounded-xl flex items-center justify-center p-2">
<Image
src={getLogo(exp.data.logo)!}
alt={`Logo ${exp.data.company}`}
width={52}
height={52}
class="rounded-lg object-contain"
/>
</div>
) : (
<div class="exp-logo-fallback absolute top-5 right-5 w-16 h-16 rounded-xl flex items-center justify-center">
<span class="text-base font-bold">{getInitials(exp.data.company)}</span>
</div>
)}
<div class="mb-3 flex items-center gap-2 flex-wrap pr-20">
<span class:list={["inline-block px-2 py-0.5 text-[11px] font-medium rounded-full border", colors.badge]}>
{label}
</span>
<span class="exp-meta text-xs">
{start} — {end}
</span>
<span class="exp-meta text-xs">· {duration}</span>
{exp.data.location && <span class="exp-meta text-xs">· {exp.data.location}</span>}
</div>
<h3 class="text-xl font-bold pr-20">{exp.data.role}</h3>
<p class="exp-company text-sm font-medium mt-1 mb-3">
{exp.data.companyUrl ? (
<a href={exp.data.companyUrl} target="_blank" rel="noopener noreferrer" class="transition-colors">{exp.data.company}</a>
) : exp.data.company}
</p>
<div class="exp-content text-sm leading-relaxed mb-4 prose prose-sm max-w-none">
<Content />
</div>
{exp.data.technologies && exp.data.technologies.length > 0 && (
<div class="flex flex-wrap gap-1.5">
{exp.data.technologies.map((tech: string) => (
<span class="exp-tech inline-block px-2.5 py-0.5 text-xs rounded-full border">
{tech}
</span>
))}
</div>
)}
</div>
</div>
);
})}
</div>
</section>
</Layout>
<style>
/* ── Light mode (default): white cards on purple gradient ── */
.exp-card {
background: white;
border: 1px solid rgba(0, 0, 0, 0.06);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.exp-card--ongoing { box-shadow: 0 0 0 1px rgba(168, 85, 247, 0.25), 0 1px 3px rgba(0, 0, 0, 0.08); }
.exp-card h3 { color: #111827 !important; }
.exp-card .exp-meta { color: #9ca3af; }
.exp-card .exp-company { color: #6b21a8; }
.exp-card .exp-company a { color: #6b21a8 !important; text-decoration: none !important; }
.exp-card .exp-company a:hover { color: #581c87 !important; }
.exp-card .exp-content,
.exp-card .exp-content p { color: #4b5563 !important; }
.exp-card .exp-content a { color: #6b21a8 !important; text-decoration-color: rgba(107, 33, 168, 0.3) !important; }
.exp-card .exp-content a:hover { color: #581c87 !important; }
.exp-card .exp-tech { background: #f3f4f6; color: #6b7280; border-color: #e5e7eb; }
.exp-logo { background: #f9fafb; border: 1px solid #e5e7eb; }
.exp-logo-fallback { background: #f3f4f6; border: 1px solid #e5e7eb; }
.exp-logo-fallback span { color: #9ca3af; }
/* Type badges light */
.type-badge-blue { background: #eff6ff; color: #1d4ed8; border-color: #bfdbfe; }
.type-badge-amber { background: #fffbeb; color: #b45309; border-color: #fde68a; }
.type-badge-emerald { background: #ecfdf5; color: #047857; border-color: #a7f3d0; }
.type-badge-pink { background: #fdf2f8; color: #be185d; border-color: #fbcfe8; }
.type-badge-purple { background: #faf5ff; color: #7e22ce; border-color: #e9d5ff; }
/* Type borders light */
.type-border-blue { border-left-color: #3b82f6 !important; }
.type-border-amber { border-left-color: #f59e0b !important; }
.type-border-emerald { border-left-color: #10b981 !important; }
.type-border-pink { border-left-color: #ec4899 !important; }
.type-border-purple { border-left-color: #a855f7 !important; }
/* ── Dark mode: translucent cards ── */
:global(.dark) .exp-card {
background: rgba(255, 255, 255, 0.03);
border-color: rgba(255, 255, 255, 0.08);
box-shadow: none;
}
:global(.dark) .exp-card:hover { background: rgba(255, 255, 255, 0.06); }
:global(.dark) .exp-card--ongoing {
background: rgba(255, 255, 255, 0.07);
border-color: rgba(255, 255, 255, 0.12);
box-shadow: none;
}
:global(.dark) .exp-card h3 { color: white !important; }
:global(.dark) .exp-card .exp-meta { color: rgba(255, 255, 255, 0.3); }
:global(.dark) .exp-card .exp-company { color: #d4b5ff; }
:global(.dark) .exp-card .exp-company a { color: #d4b5ff !important; }
:global(.dark) .exp-card .exp-company a:hover { color: white !important; }
:global(.dark) .exp-card .exp-content,
:global(.dark) .exp-card .exp-content p { color: rgba(255, 255, 255, 0.6) !important; }
:global(.dark) .exp-card .exp-content a { color: #d4b5ff !important; }
:global(.dark) .exp-card .exp-content a:hover { color: white !important; }
:global(.dark) .exp-card .exp-tech { background: rgba(255, 255, 255, 0.06); color: rgba(255, 255, 255, 0.45); border-color: rgba(255, 255, 255, 0.06); }
:global(.dark) .exp-logo { background: white; border-color: rgba(255, 255, 255, 0.2); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); }
:global(.dark) .exp-logo-fallback { background: rgba(255, 255, 255, 0.08); border-color: rgba(255, 255, 255, 0.1); }
:global(.dark) .exp-logo-fallback span { color: rgba(255, 255, 255, 0.4); }
/* Type badges dark */
:global(.dark) .type-badge-blue { background: rgba(96, 165, 250, 0.15); color: #93c5fd; border-color: rgba(96, 165, 250, 0.2); }
:global(.dark) .type-badge-amber { background: rgba(251, 191, 36, 0.15); color: #fcd34d; border-color: rgba(251, 191, 36, 0.2); }
:global(.dark) .type-badge-emerald { background: rgba(52, 211, 153, 0.15); color: #6ee7b7; border-color: rgba(52, 211, 153, 0.2); }
:global(.dark) .type-badge-pink { background: rgba(244, 114, 182, 0.15); color: #f9a8d4; border-color: rgba(244, 114, 182, 0.2); }
:global(.dark) .type-badge-purple { background: rgba(168, 85, 247, 0.15); color: #d4b5ff; border-color: rgba(168, 85, 247, 0.2); }
/* Type borders dark */
:global(.dark) .type-border-blue { border-left-color: rgba(96, 165, 250, 0.4) !important; }
:global(.dark) .type-border-amber { border-left-color: rgba(251, 191, 36, 0.4) !important; }
:global(.dark) .type-border-emerald { border-left-color: rgba(52, 211, 153, 0.4) !important; }
:global(.dark) .type-border-pink { border-left-color: rgba(244, 114, 182, 0.4) !important; }
:global(.dark) .type-border-purple { border-left-color: rgba(168, 85, 247, 0.4) !important; }
</style>