Compare commits
4 commits
194dec0fbe
...
5b887c7926
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b887c7926 | |||
| 8728594347 | |||
| 9871f189e3 | |||
| 4e1bba4fb4 |
4847
pnpm-lock.yaml
generated
BIN
src/assets/images/companies/araymond.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src/assets/images/companies/champollion.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/assets/images/companies/dismoi.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/images/companies/libeo.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src/assets/images/companies/obat.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/images/companies/urssaf.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/images/companies/veepee.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -59,6 +59,7 @@ const experiencesCollection = defineCollection({
|
||||||
role: z.string(),
|
role: z.string(),
|
||||||
company: z.string(),
|
company: z.string(),
|
||||||
companyUrl: z.string().url().optional(),
|
companyUrl: z.string().url().optional(),
|
||||||
|
logo: z.string().optional(),
|
||||||
location: z.string().optional(),
|
location: z.string().optional(),
|
||||||
startDate: z.string(),
|
startDate: z.string(),
|
||||||
endDate: z.string().optional(),
|
endDate: z.string().optional(),
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "مهندس معماري للواجهات"
|
role: "مهندس معماري للواجهات"
|
||||||
company: "ARaymond"
|
company: "ARaymond"
|
||||||
companyUrl: "https://www.araymond.com/"
|
companyUrl: "https://www.araymond.com/"
|
||||||
|
logo: "araymond.png"
|
||||||
location: "غرونوبل"
|
location: "غرونوبل"
|
||||||
startDate: "2022-01"
|
startDate: "2022-01"
|
||||||
endDate: "2023-01"
|
endDate: "2023-01"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "Frontend Architect"
|
role: "Frontend Architect"
|
||||||
company: "ARaymond"
|
company: "ARaymond"
|
||||||
companyUrl: "https://www.araymond.com/"
|
companyUrl: "https://www.araymond.com/"
|
||||||
|
logo: "araymond.png"
|
||||||
location: "Grenoble"
|
location: "Grenoble"
|
||||||
startDate: "2022-01"
|
startDate: "2022-01"
|
||||||
endDate: "2023-01"
|
endDate: "2023-01"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "Architecte frontend"
|
role: "Architecte frontend"
|
||||||
company: "ARaymond"
|
company: "ARaymond"
|
||||||
companyUrl: "https://www.araymond.com/"
|
companyUrl: "https://www.araymond.com/"
|
||||||
|
logo: "araymond.png"
|
||||||
location: "Grenoble"
|
location: "Grenoble"
|
||||||
startDate: "2022-01"
|
startDate: "2022-01"
|
||||||
endDate: "2023-01"
|
endDate: "2023-01"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "أستاذ هندسة البرمجيات"
|
role: "أستاذ هندسة البرمجيات"
|
||||||
company: "جامعة شامبوليون"
|
company: "جامعة شامبوليون"
|
||||||
companyUrl: "https://www.univ-jfc.fr/"
|
companyUrl: "https://www.univ-jfc.fr/"
|
||||||
|
logo: "champollion.png"
|
||||||
location: "ألبي"
|
location: "ألبي"
|
||||||
startDate: "2019-09"
|
startDate: "2019-09"
|
||||||
technologies: ["TypeScript", "JavaScript", "Node.js", "TDD", "Clean Code"]
|
technologies: ["TypeScript", "JavaScript", "Node.js", "TDD", "Clean Code"]
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "Software Engineering Professor"
|
role: "Software Engineering Professor"
|
||||||
company: "Université Champollion"
|
company: "Université Champollion"
|
||||||
companyUrl: "https://www.univ-jfc.fr/"
|
companyUrl: "https://www.univ-jfc.fr/"
|
||||||
|
logo: "champollion.png"
|
||||||
location: "Albi"
|
location: "Albi"
|
||||||
startDate: "2019-09"
|
startDate: "2019-09"
|
||||||
technologies: ["TypeScript", "JavaScript", "Node.js", "TDD", "Clean Code"]
|
technologies: ["TypeScript", "JavaScript", "Node.js", "TDD", "Clean Code"]
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "Enseignant en génie logiciel"
|
role: "Enseignant en génie logiciel"
|
||||||
company: "Université Champollion"
|
company: "Université Champollion"
|
||||||
companyUrl: "https://www.univ-jfc.fr/"
|
companyUrl: "https://www.univ-jfc.fr/"
|
||||||
|
logo: "champollion.png"
|
||||||
location: "Albi"
|
location: "Albi"
|
||||||
startDate: "2019-09"
|
startDate: "2019-09"
|
||||||
technologies: ["TypeScript", "JavaScript", "Node.js", "TDD", "Clean Code"]
|
technologies: ["TypeScript", "JavaScript", "Node.js", "TDD", "Clean Code"]
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "حرفي برمجيات / مؤسس مشارك"
|
role: "حرفي برمجيات / مؤسس مشارك"
|
||||||
company: "DisMoi"
|
company: "DisMoi"
|
||||||
companyUrl: "https://github.com/dis-moi"
|
companyUrl: "https://github.com/dis-moi"
|
||||||
|
logo: "dismoi.png"
|
||||||
location: "عن بُعد"
|
location: "عن بُعد"
|
||||||
startDate: "2019-01"
|
startDate: "2019-01"
|
||||||
endDate: "2021-06"
|
endDate: "2021-06"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "Software Craftsman / Cofounder"
|
role: "Software Craftsman / Cofounder"
|
||||||
company: "DisMoi"
|
company: "DisMoi"
|
||||||
companyUrl: "https://github.com/dis-moi"
|
companyUrl: "https://github.com/dis-moi"
|
||||||
|
logo: "dismoi.png"
|
||||||
location: "Remote"
|
location: "Remote"
|
||||||
startDate: "2019-01"
|
startDate: "2019-01"
|
||||||
endDate: "2021-06"
|
endDate: "2021-06"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "Software Craftsman / Cofondateur"
|
role: "Software Craftsman / Cofondateur"
|
||||||
company: "DisMoi"
|
company: "DisMoi"
|
||||||
companyUrl: "https://github.com/dis-moi"
|
companyUrl: "https://github.com/dis-moi"
|
||||||
|
logo: "dismoi.png"
|
||||||
location: "Remote"
|
location: "Remote"
|
||||||
startDate: "2019-01"
|
startDate: "2019-01"
|
||||||
endDate: "2021-06"
|
endDate: "2021-06"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "مهندس برمجيات fullstack"
|
role: "مهندس برمجيات fullstack"
|
||||||
company: "Libeo"
|
company: "Libeo"
|
||||||
companyUrl: "https://www.libeo.io/"
|
companyUrl: "https://www.libeo.io/"
|
||||||
|
logo: "libeo.png"
|
||||||
location: "عن بُعد"
|
location: "عن بُعد"
|
||||||
startDate: "2021-01"
|
startDate: "2021-01"
|
||||||
endDate: "2021-06"
|
endDate: "2021-06"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "Fullstack Software Engineer"
|
role: "Fullstack Software Engineer"
|
||||||
company: "Libeo"
|
company: "Libeo"
|
||||||
companyUrl: "https://www.libeo.io/"
|
companyUrl: "https://www.libeo.io/"
|
||||||
|
logo: "libeo.png"
|
||||||
location: "Remote"
|
location: "Remote"
|
||||||
startDate: "2021-01"
|
startDate: "2021-01"
|
||||||
endDate: "2021-06"
|
endDate: "2021-06"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "Ingénieur logiciel fullstack"
|
role: "Ingénieur logiciel fullstack"
|
||||||
company: "Libeo"
|
company: "Libeo"
|
||||||
companyUrl: "https://www.libeo.io/"
|
companyUrl: "https://www.libeo.io/"
|
||||||
|
logo: "libeo.png"
|
||||||
location: "Remote"
|
location: "Remote"
|
||||||
startDate: "2021-01"
|
startDate: "2021-01"
|
||||||
endDate: "2021-06"
|
endDate: "2021-06"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "مهندس برمجيات أول"
|
role: "مهندس برمجيات أول"
|
||||||
company: "Obat"
|
company: "Obat"
|
||||||
companyUrl: "https://www.obat.fr/"
|
companyUrl: "https://www.obat.fr/"
|
||||||
|
logo: "obat.png"
|
||||||
location: "عن بُعد"
|
location: "عن بُعد"
|
||||||
startDate: "2023-02"
|
startDate: "2023-02"
|
||||||
endDate: "2024-01"
|
endDate: "2024-01"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "Senior Software Engineer"
|
role: "Senior Software Engineer"
|
||||||
company: "Obat"
|
company: "Obat"
|
||||||
companyUrl: "https://www.obat.fr/"
|
companyUrl: "https://www.obat.fr/"
|
||||||
|
logo: "obat.png"
|
||||||
location: "Remote"
|
location: "Remote"
|
||||||
startDate: "2023-02"
|
startDate: "2023-02"
|
||||||
endDate: "2024-01"
|
endDate: "2024-01"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "Ingénieur logiciel senior"
|
role: "Ingénieur logiciel senior"
|
||||||
company: "Obat"
|
company: "Obat"
|
||||||
companyUrl: "https://www.obat.fr/"
|
companyUrl: "https://www.obat.fr/"
|
||||||
|
logo: "obat.png"
|
||||||
location: "Remote"
|
location: "Remote"
|
||||||
startDate: "2023-02"
|
startDate: "2023-02"
|
||||||
endDate: "2024-01"
|
endDate: "2024-01"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "مطوّر رئيسي"
|
role: "مطوّر رئيسي"
|
||||||
company: "Urssaf Caisse nationale"
|
company: "Urssaf Caisse nationale"
|
||||||
companyUrl: "https://www.urssaf.fr/"
|
companyUrl: "https://www.urssaf.fr/"
|
||||||
|
logo: "urssaf.png"
|
||||||
location: "عن بُعد / باريس"
|
location: "عن بُعد / باريس"
|
||||||
startDate: "2024-02"
|
startDate: "2024-02"
|
||||||
technologies: ["TypeScript", "React.js", "Publicodes", "Node.js", "GitHub"]
|
technologies: ["TypeScript", "React.js", "Publicodes", "Node.js", "GitHub"]
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "Lead Developer"
|
role: "Lead Developer"
|
||||||
company: "Urssaf Caisse nationale"
|
company: "Urssaf Caisse nationale"
|
||||||
companyUrl: "https://www.urssaf.fr/"
|
companyUrl: "https://www.urssaf.fr/"
|
||||||
|
logo: "urssaf.png"
|
||||||
location: "Remote / Paris"
|
location: "Remote / Paris"
|
||||||
startDate: "2024-02"
|
startDate: "2024-02"
|
||||||
technologies: ["TypeScript", "React.js", "Publicodes", "Node.js", "GitHub"]
|
technologies: ["TypeScript", "React.js", "Publicodes", "Node.js", "GitHub"]
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "Lead Developer"
|
role: "Lead Developer"
|
||||||
company: "Urssaf Caisse nationale"
|
company: "Urssaf Caisse nationale"
|
||||||
companyUrl: "https://www.urssaf.fr/"
|
companyUrl: "https://www.urssaf.fr/"
|
||||||
|
logo: "urssaf.png"
|
||||||
location: "Remote / Paris"
|
location: "Remote / Paris"
|
||||||
startDate: "2024-02"
|
startDate: "2024-02"
|
||||||
technologies: ["TypeScript", "React.js", "Publicodes", "Node.js", "GitHub"]
|
technologies: ["TypeScript", "React.js", "Publicodes", "Node.js", "GitHub"]
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "مطوّر واجهات رئيسي Travel"
|
role: "مطوّر واجهات رئيسي Travel"
|
||||||
company: "Veepee"
|
company: "Veepee"
|
||||||
companyUrl: "https://www.veepee.com/"
|
companyUrl: "https://www.veepee.com/"
|
||||||
|
logo: "veepee.png"
|
||||||
location: "باريس"
|
location: "باريس"
|
||||||
startDate: "2016-02"
|
startDate: "2016-02"
|
||||||
endDate: "2018-01"
|
endDate: "2018-01"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "Travel Front Lead Developer"
|
role: "Travel Front Lead Developer"
|
||||||
company: "Veepee"
|
company: "Veepee"
|
||||||
companyUrl: "https://www.veepee.com/"
|
companyUrl: "https://www.veepee.com/"
|
||||||
|
logo: "veepee.png"
|
||||||
location: "Paris"
|
location: "Paris"
|
||||||
startDate: "2016-02"
|
startDate: "2016-02"
|
||||||
endDate: "2018-01"
|
endDate: "2018-01"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "Travel Front Lead Developer"
|
role: "Travel Front Lead Developer"
|
||||||
company: "Veepee"
|
company: "Veepee"
|
||||||
companyUrl: "https://www.veepee.com/"
|
companyUrl: "https://www.veepee.com/"
|
||||||
|
logo: "veepee.png"
|
||||||
location: "Paris"
|
location: "Paris"
|
||||||
startDate: "2016-02"
|
startDate: "2016-02"
|
||||||
endDate: "2018-01"
|
endDate: "2018-01"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "قائد تقني Travel"
|
role: "قائد تقني Travel"
|
||||||
company: "Veepee"
|
company: "Veepee"
|
||||||
companyUrl: "https://www.veepee.com/"
|
companyUrl: "https://www.veepee.com/"
|
||||||
|
logo: "veepee.png"
|
||||||
location: "باريس"
|
location: "باريس"
|
||||||
startDate: "2018-01"
|
startDate: "2018-01"
|
||||||
endDate: "2019-06"
|
endDate: "2019-06"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "Travel Tech Lead"
|
role: "Travel Tech Lead"
|
||||||
company: "Veepee"
|
company: "Veepee"
|
||||||
companyUrl: "https://www.veepee.com/"
|
companyUrl: "https://www.veepee.com/"
|
||||||
|
logo: "veepee.png"
|
||||||
location: "Paris"
|
location: "Paris"
|
||||||
startDate: "2018-01"
|
startDate: "2018-01"
|
||||||
endDate: "2019-06"
|
endDate: "2019-06"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
role: "Travel Tech Lead"
|
role: "Travel Tech Lead"
|
||||||
company: "Veepee"
|
company: "Veepee"
|
||||||
companyUrl: "https://www.veepee.com/"
|
companyUrl: "https://www.veepee.com/"
|
||||||
|
logo: "veepee.png"
|
||||||
location: "Paris"
|
location: "Paris"
|
||||||
startDate: "2018-01"
|
startDate: "2018-01"
|
||||||
endDate: "2019-06"
|
endDate: "2019-06"
|
||||||
|
|
|
||||||
|
|
@ -55,25 +55,33 @@ const locale = pathname.startsWith("/en")
|
||||||
color: rgba(255, 255, 255, 0.85) !important;
|
color: rgba(255, 255, 255, 0.85) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Page-level text overrides for the purple gradient.
|
||||||
|
* Any component that manages its own colors should add
|
||||||
|
* the `.code-card` class — it will be excluded automatically.
|
||||||
|
*/
|
||||||
[data-facet="code"] > section h1,
|
[data-facet="code"] > section h1,
|
||||||
[data-facet="code"] > section h2,
|
[data-facet="code"] > section h2 {
|
||||||
[data-facet="code"] > section h3,
|
|
||||||
[data-facet="code"] > section strong {
|
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-facet="code"] > section a:not(.facet-card a) {
|
[data-facet="code"] > section h3:not(.code-card h3),
|
||||||
|
[data-facet="code"] > section strong:not(.code-card strong) {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-facet="code"] > section a:not(.code-card a) {
|
||||||
color: #d4b5ff !important;
|
color: #d4b5ff !important;
|
||||||
text-decoration-color: rgba(212, 181, 255, 0.3) !important;
|
text-decoration-color: rgba(212, 181, 255, 0.3) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-facet="code"] > section a:not(.facet-card a):hover {
|
[data-facet="code"] > section a:not(.code-card a):hover {
|
||||||
color: white !important;
|
color: white !important;
|
||||||
text-decoration-color: white !important;
|
text-decoration-color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-facet="code"] > section > :not(.facet-card) p,
|
[data-facet="code"] > section p:not(.code-card p),
|
||||||
[data-facet="code"] > section > :not(.facet-card) li {
|
[data-facet="code"] > section li:not(.code-card li) {
|
||||||
color: rgba(255, 255, 255, 0.75) !important;
|
color: rgba(255, 255, 255, 0.75) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,20 @@
|
||||||
---
|
---
|
||||||
import { getCollection, render } from "astro:content";
|
import { getCollection, render } from "astro:content";
|
||||||
|
import { Image } from "astro:assets";
|
||||||
import Layout from "../../../layouts/main.astro";
|
import Layout from "../../../layouts/main.astro";
|
||||||
|
import "../../../styles/exp-card.css";
|
||||||
|
|
||||||
|
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 = "ar";
|
const locale = "ar";
|
||||||
|
|
||||||
|
|
@ -8,9 +22,17 @@ const experiences = (await getCollection("experiences"))
|
||||||
.filter((e) => e.data.lang === locale && !e.data.draft)
|
.filter((e) => e.data.lang === locale && !e.data.draft)
|
||||||
.sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1));
|
.sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1));
|
||||||
|
|
||||||
const typeIcons: Record<string, string> = {
|
const typeLabels: Record<string, string> = {
|
||||||
employment: '🏢', freelance: '💼', teaching: '🎓',
|
employment: 'وظيفة', freelance: 'مستقل', teaching: 'تدريس',
|
||||||
community: '🤝', entrepreneurship: '🚀',
|
community: 'مجتمع', entrepreneurship: 'ريادة أعمال',
|
||||||
|
};
|
||||||
|
|
||||||
|
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) {
|
function formatMonth(dateStr: string) {
|
||||||
|
|
@ -18,6 +40,17 @@ function formatMonth(dateStr: string) {
|
||||||
return new Date(parseInt(year), parseInt(month) - 1)
|
return new Date(parseInt(year), parseInt(month) - 1)
|
||||||
.toLocaleDateString('ar-SA', { year: 'numeric', month: 'short' });
|
.toLocaleDateString('ar-SA', { 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} أشهر`;
|
||||||
|
if (months === 0) return `${years} سنة`;
|
||||||
|
return `${years} سنة ${months} أشهر`;
|
||||||
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout
|
||||||
|
|
@ -43,43 +76,68 @@ function formatMonth(dateStr: string) {
|
||||||
const isOngoing = !exp.data.endDate;
|
const isOngoing = !exp.data.endDate;
|
||||||
const start = formatMonth(exp.data.startDate);
|
const start = formatMonth(exp.data.startDate);
|
||||||
const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'الحاضر';
|
const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'الحاضر';
|
||||||
|
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 (
|
return (
|
||||||
<div class="relative pr-8 pb-10 border-r-2 border-white/[0.1] last:pb-0">
|
<div class="relative pr-10 pb-10 border-r-2 border-white/[0.12] last:pb-0">
|
||||||
<div
|
<div
|
||||||
class:list={[
|
class:list={[
|
||||||
"absolute -right-[9px] top-1 w-4 h-4 rounded-full border-2",
|
"absolute -right-[7px] top-2 w-3 h-3 rounded-full border-2",
|
||||||
isOngoing
|
isOngoing ? colors.dot : "border-white/20 bg-white/10"
|
||||||
? "border-purple-300 bg-purple-400"
|
|
||||||
: "border-white/20 bg-white/10"
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{isOngoing && <div class="absolute inset-0.5 rounded-full bg-purple-300 animate-pulse" />}
|
{isOngoing && <div class="absolute inset-0 rounded-full bg-current opacity-40 animate-ping" />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="facet-card rounded-2xl bg-white/[0.04] border border-white/[0.08] p-5 hover:bg-white/[0.08] transition-colors duration-200">
|
<div class:list={[
|
||||||
<div class="mb-2 flex items-center gap-2 flex-wrap">
|
"code-card exp-card relative rounded-2xl p-6 transition-colors duration-200 border-s-[3px]",
|
||||||
<span class="text-xs text-white/40">
|
isOngoing ? "exp-card--ongoing" : "",
|
||||||
{typeIcons[exp.data.type] || '💼'} {start} — {end}
|
colors.border
|
||||||
|
]}>
|
||||||
|
{getLogo(exp.data.logo) ? (
|
||||||
|
<div class="exp-logo absolute top-5 end-5 w-16 h-16 rounded-xl flex items-center justify-center p-2">
|
||||||
|
<Image
|
||||||
|
src={getLogo(exp.data.logo)!}
|
||||||
|
alt={`شعار ${exp.data.company}`}
|
||||||
|
width={52}
|
||||||
|
height={52}
|
||||||
|
class="rounded-lg object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div class="exp-logo-fallback absolute top-5 end-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 pe-20">
|
||||||
|
<span class:list={["inline-block px-2 py-0.5 text-[11px] font-medium rounded-full border", colors.badge]}>
|
||||||
|
{label}
|
||||||
</span>
|
</span>
|
||||||
{exp.data.location && <span class="text-xs text-white/30">· {exp.data.location}</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>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-lg font-bold text-white">{exp.data.role}</h3>
|
<h3 class="text-xl font-bold pe-20">{exp.data.role}</h3>
|
||||||
<p class="text-sm font-medium text-purple-200 mb-3">
|
<p class="exp-company text-sm font-medium mt-1 mb-3">
|
||||||
{exp.data.companyUrl ? (
|
{exp.data.companyUrl ? (
|
||||||
<a href={exp.data.companyUrl} target="_blank" rel="noopener noreferrer" class="hover:text-white transition-colors">{exp.data.company}</a>
|
<a href={exp.data.companyUrl} target="_blank" rel="noopener noreferrer" class="transition-colors">{exp.data.company}</a>
|
||||||
) : exp.data.company}
|
) : exp.data.company}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="text-sm text-white/60 leading-relaxed mb-3 prose prose-sm prose-invert max-w-none [&_p]:text-white/60 [&_a]:text-purple-200 [&_a:hover]:text-white">
|
<div class="exp-content text-sm leading-relaxed mb-4 prose prose-sm max-w-none">
|
||||||
<Content />
|
<Content />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{exp.data.technologies && exp.data.technologies.length > 0 && (
|
{exp.data.technologies && exp.data.technologies.length > 0 && (
|
||||||
<div class="flex flex-wrap gap-1.5">
|
<div class="flex flex-wrap gap-1.5">
|
||||||
{exp.data.technologies.map((tech: string) => (
|
{exp.data.technologies.map((tech: string) => (
|
||||||
<span class="inline-block px-2 py-0.5 text-xs rounded-full bg-white/[0.06] text-white/50 border border-white/[0.06]">
|
<span class="exp-tech inline-block px-2.5 py-0.5 text-xs rounded-full border">
|
||||||
{tech}
|
{tech}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -53,16 +53,16 @@ function formatMonth(dateStr: string) {
|
||||||
<span class="inline-block px-3 py-1 text-sm text-purple-200/80 bg-white/[0.06] border border-white/[0.08] rounded-full mb-5">Développeur freelance · Albi</span>
|
<span class="inline-block px-3 py-1 text-sm text-purple-200/80 bg-white/[0.06] border border-white/[0.08] rounded-full mb-5">Développeur freelance · Albi</span>
|
||||||
<h1 class="text-5xl sm:text-6xl font-semibold text-white font-display tracking-wide">Artisan du logiciel</h1>
|
<h1 class="text-5xl sm:text-6xl font-semibold text-white font-display tracking-wide">Artisan du logiciel</h1>
|
||||||
<div class="mt-6 mb-6 w-16 h-0.5 bg-gradient-to-r from-purple-400 to-transparent rounded-full"></div>
|
<div class="mt-6 mb-6 w-16 h-0.5 bg-gradient-to-r from-purple-400 to-transparent rounded-full"></div>
|
||||||
<div class="space-y-4 text-lg text-white/60 leading-relaxed max-w-2xl">
|
<p class="text-lg text-white/60 leading-relaxed max-w-2xl mb-8">
|
||||||
<p>
|
J'accompagne les équipes comme développeur senior, tech lead ou coach technique. Ce qui me distingue peut-être : je m'intéresse autant à la qualité du code qu'à ce qu'il produit. Je privilégie le logiciel libre et les outils qui répondent à de vrais besoins.
|
||||||
TDD, Clean Code, Domain-Driven Design : c'est ma façon de construire du logiciel. J'accompagne les équipes comme développeur senior, tech lead ou coach technique. Ma stack : TypeScript/JavaScript, mais aussi PHP et Elixir.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Ce qui me distingue peut-être : je m'intéresse autant à la qualité du code qu'à ce qu'il produit. Je privilégie le logiciel libre et les outils qui répondent à de vrais besoins. Je m'interroge aussi sur les biais que nous inscrivons dans le code, et qui perpétuent des rapports sociaux qu'on doit questionner.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
J'enseigne la programmation à <Link href="https://www.univ-jfc.fr/" external>l'université Champollion</Link> et j'anime les <Link href="https://www.meetup.com/software-crafters-albi/" external>Software Crafters d'Albi</Link> depuis 2018.
|
|
||||||
</p>
|
</p>
|
||||||
|
<div class="flex flex-wrap gap-2 mb-8">
|
||||||
|
<span class="px-3 py-1.5 text-sm font-medium rounded-full bg-purple-400/15 text-purple-200 border border-purple-300/20">TypeScript</span>
|
||||||
|
<span class="px-3 py-1.5 text-sm font-medium rounded-full bg-purple-400/15 text-purple-200 border border-purple-300/20">PHP</span>
|
||||||
|
<span class="px-3 py-1.5 text-sm font-medium rounded-full bg-purple-400/15 text-purple-200 border border-purple-300/20">Elixir</span>
|
||||||
|
<span class="px-3 py-1.5 text-sm font-medium rounded-full bg-white/[0.06] text-white/50 border border-white/[0.08]">TDD</span>
|
||||||
|
<span class="px-3 py-1.5 text-sm font-medium rounded-full bg-white/[0.06] text-white/50 border border-white/[0.08]">DDD</span>
|
||||||
|
<span class="px-3 py-1.5 text-sm font-medium rounded-full bg-white/[0.06] text-white/50 border border-white/[0.08]">Clean Code</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,20 @@
|
||||||
---
|
---
|
||||||
import { getCollection, render } from "astro:content";
|
import { getCollection, render } from "astro:content";
|
||||||
|
import { Image } from "astro:assets";
|
||||||
import Layout from "../../layouts/main.astro";
|
import Layout from "../../layouts/main.astro";
|
||||||
|
import "../../styles/exp-card.css";
|
||||||
|
|
||||||
|
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 locale = "fr";
|
||||||
|
|
||||||
|
|
@ -8,9 +22,17 @@ const experiences = (await getCollection("experiences"))
|
||||||
.filter((e) => e.data.lang === locale && !e.data.draft)
|
.filter((e) => e.data.lang === locale && !e.data.draft)
|
||||||
.sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1));
|
.sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1));
|
||||||
|
|
||||||
const typeIcons: Record<string, string> = {
|
const typeLabels: Record<string, string> = {
|
||||||
employment: '🏢', freelance: '💼', teaching: '🎓',
|
employment: 'Emploi', freelance: 'Freelance', teaching: 'Enseignement',
|
||||||
community: '🤝', entrepreneurship: '🚀',
|
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) {
|
function formatMonth(dateStr: string) {
|
||||||
|
|
@ -18,6 +40,17 @@ function formatMonth(dateStr: string) {
|
||||||
return new Date(parseInt(year), parseInt(month) - 1)
|
return new Date(parseInt(year), parseInt(month) - 1)
|
||||||
.toLocaleDateString('fr-FR', { year: 'numeric', month: 'short' });
|
.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
|
<Layout
|
||||||
|
|
@ -43,43 +76,68 @@ function formatMonth(dateStr: string) {
|
||||||
const isOngoing = !exp.data.endDate;
|
const isOngoing = !exp.data.endDate;
|
||||||
const start = formatMonth(exp.data.startDate);
|
const start = formatMonth(exp.data.startDate);
|
||||||
const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'Présent';
|
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 (
|
return (
|
||||||
<div class="relative pl-8 pb-10 border-l-2 border-white/[0.1] last:pb-0">
|
<div class="relative pl-10 pb-10 border-l-2 border-white/[0.12] last:pb-0">
|
||||||
<div
|
<div
|
||||||
class:list={[
|
class:list={[
|
||||||
"absolute -left-[9px] top-1 w-4 h-4 rounded-full border-2",
|
"absolute -left-[7px] top-2 w-3 h-3 rounded-full border-2",
|
||||||
isOngoing
|
isOngoing ? colors.dot : "border-white/20 bg-white/10"
|
||||||
? "border-purple-300 bg-purple-400"
|
|
||||||
: "border-white/20 bg-white/10"
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{isOngoing && <div class="absolute inset-0.5 rounded-full bg-purple-300 animate-pulse" />}
|
{isOngoing && <div class="absolute inset-0 rounded-full bg-current opacity-40 animate-ping" />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="facet-card rounded-2xl bg-white/[0.04] border border-white/[0.08] p-5 hover:bg-white/[0.08] transition-colors duration-200">
|
<div class:list={[
|
||||||
<div class="mb-2 flex items-center gap-2 flex-wrap">
|
"code-card exp-card relative rounded-2xl p-6 transition-colors duration-200 border-s-[3px]",
|
||||||
<span class="text-xs text-white/40">
|
isOngoing ? "exp-card--ongoing" : "",
|
||||||
{typeIcons[exp.data.type] || '💼'} {start} — {end}
|
colors.border
|
||||||
|
]}>
|
||||||
|
{getLogo(exp.data.logo) ? (
|
||||||
|
<div class="exp-logo absolute top-5 end-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 end-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 pe-20">
|
||||||
|
<span class:list={["inline-block px-2 py-0.5 text-[11px] font-medium rounded-full border", colors.badge]}>
|
||||||
|
{label}
|
||||||
</span>
|
</span>
|
||||||
{exp.data.location && <span class="text-xs text-white/30">· {exp.data.location}</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>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-lg font-bold text-white">{exp.data.role}</h3>
|
<h3 class="text-xl font-bold pe-20">{exp.data.role}</h3>
|
||||||
<p class="text-sm font-medium text-purple-200 mb-3">
|
<p class="exp-company text-sm font-medium mt-1 mb-3">
|
||||||
{exp.data.companyUrl ? (
|
{exp.data.companyUrl ? (
|
||||||
<a href={exp.data.companyUrl} target="_blank" rel="noopener noreferrer" class="hover:text-white transition-colors">{exp.data.company}</a>
|
<a href={exp.data.companyUrl} target="_blank" rel="noopener noreferrer" class="transition-colors">{exp.data.company}</a>
|
||||||
) : exp.data.company}
|
) : exp.data.company}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="text-sm text-white/60 leading-relaxed mb-3 prose prose-sm prose-invert max-w-none [&_p]:text-white/60 [&_a]:text-purple-200 [&_a:hover]:text-white">
|
<div class="exp-content text-sm leading-relaxed mb-4 prose prose-sm max-w-none">
|
||||||
<Content />
|
<Content />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{exp.data.technologies && exp.data.technologies.length > 0 && (
|
{exp.data.technologies && exp.data.technologies.length > 0 && (
|
||||||
<div class="flex flex-wrap gap-1.5">
|
<div class="flex flex-wrap gap-1.5">
|
||||||
{exp.data.technologies.map((tech: string) => (
|
{exp.data.technologies.map((tech: string) => (
|
||||||
<span class="inline-block px-2 py-0.5 text-xs rounded-full bg-white/[0.06] text-white/50 border border-white/[0.06]">
|
<span class="exp-tech inline-block px-2.5 py-0.5 text-xs rounded-full border">
|
||||||
{tech}
|
{tech}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,20 @@
|
||||||
---
|
---
|
||||||
import { getCollection, render } from "astro:content";
|
import { getCollection, render } from "astro:content";
|
||||||
|
import { Image } from "astro:assets";
|
||||||
import Layout from "../../../layouts/main.astro";
|
import Layout from "../../../layouts/main.astro";
|
||||||
|
import "../../../styles/exp-card.css";
|
||||||
|
|
||||||
|
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 = "en";
|
const locale = "en";
|
||||||
|
|
||||||
|
|
@ -8,9 +22,17 @@ const experiences = (await getCollection("experiences"))
|
||||||
.filter((e) => e.data.lang === locale && !e.data.draft)
|
.filter((e) => e.data.lang === locale && !e.data.draft)
|
||||||
.sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1));
|
.sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1));
|
||||||
|
|
||||||
const typeIcons: Record<string, string> = {
|
const typeLabels: Record<string, string> = {
|
||||||
employment: '🏢', freelance: '💼', teaching: '🎓',
|
employment: 'Employment', freelance: 'Freelance', teaching: 'Teaching',
|
||||||
community: '🤝', entrepreneurship: '🚀',
|
community: 'Community', entrepreneurship: 'Entrepreneurship',
|
||||||
|
};
|
||||||
|
|
||||||
|
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) {
|
function formatMonth(dateStr: string) {
|
||||||
|
|
@ -18,6 +40,17 @@ function formatMonth(dateStr: string) {
|
||||||
return new Date(parseInt(year), parseInt(month) - 1)
|
return new Date(parseInt(year), parseInt(month) - 1)
|
||||||
.toLocaleDateString('en-US', { year: 'numeric', month: 'short' });
|
.toLocaleDateString('en-US', { 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} mo`;
|
||||||
|
if (months === 0) return `${years} yr${years > 1 ? 's' : ''}`;
|
||||||
|
return `${years} yr${years > 1 ? 's' : ''} ${months} mo`;
|
||||||
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout
|
||||||
|
|
@ -43,43 +76,68 @@ function formatMonth(dateStr: string) {
|
||||||
const isOngoing = !exp.data.endDate;
|
const isOngoing = !exp.data.endDate;
|
||||||
const start = formatMonth(exp.data.startDate);
|
const start = formatMonth(exp.data.startDate);
|
||||||
const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'Present';
|
const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'Present';
|
||||||
|
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 (
|
return (
|
||||||
<div class="relative pl-8 pb-10 border-l-2 border-white/[0.1] last:pb-0">
|
<div class="relative pl-10 pb-10 border-l-2 border-white/[0.12] last:pb-0">
|
||||||
<div
|
<div
|
||||||
class:list={[
|
class:list={[
|
||||||
"absolute -left-[9px] top-1 w-4 h-4 rounded-full border-2",
|
"absolute -left-[7px] top-2 w-3 h-3 rounded-full border-2",
|
||||||
isOngoing
|
isOngoing ? colors.dot : "border-white/20 bg-white/10"
|
||||||
? "border-purple-300 bg-purple-400"
|
|
||||||
: "border-white/20 bg-white/10"
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{isOngoing && <div class="absolute inset-0.5 rounded-full bg-purple-300 animate-pulse" />}
|
{isOngoing && <div class="absolute inset-0 rounded-full bg-current opacity-40 animate-ping" />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="facet-card rounded-2xl bg-white/[0.04] border border-white/[0.08] p-5 hover:bg-white/[0.08] transition-colors duration-200">
|
<div class:list={[
|
||||||
<div class="mb-2 flex items-center gap-2 flex-wrap">
|
"code-card exp-card relative rounded-2xl p-6 transition-colors duration-200 border-s-[3px]",
|
||||||
<span class="text-xs text-white/40">
|
isOngoing ? "exp-card--ongoing" : "",
|
||||||
{typeIcons[exp.data.type] || '💼'} {start} — {end}
|
colors.border
|
||||||
|
]}>
|
||||||
|
{getLogo(exp.data.logo) ? (
|
||||||
|
<div class="exp-logo absolute top-5 end-5 w-16 h-16 rounded-xl flex items-center justify-center p-2">
|
||||||
|
<Image
|
||||||
|
src={getLogo(exp.data.logo)!}
|
||||||
|
alt={`${exp.data.company} logo`}
|
||||||
|
width={52}
|
||||||
|
height={52}
|
||||||
|
class="rounded-lg object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div class="exp-logo-fallback absolute top-5 end-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 pe-20">
|
||||||
|
<span class:list={["inline-block px-2 py-0.5 text-[11px] font-medium rounded-full border", colors.badge]}>
|
||||||
|
{label}
|
||||||
</span>
|
</span>
|
||||||
{exp.data.location && <span class="text-xs text-white/30">· {exp.data.location}</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>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-lg font-bold text-white">{exp.data.role}</h3>
|
<h3 class="text-xl font-bold pe-20">{exp.data.role}</h3>
|
||||||
<p class="text-sm font-medium text-purple-200 mb-3">
|
<p class="exp-company text-sm font-medium mt-1 mb-3">
|
||||||
{exp.data.companyUrl ? (
|
{exp.data.companyUrl ? (
|
||||||
<a href={exp.data.companyUrl} target="_blank" rel="noopener noreferrer" class="hover:text-white transition-colors">{exp.data.company}</a>
|
<a href={exp.data.companyUrl} target="_blank" rel="noopener noreferrer" class="transition-colors">{exp.data.company}</a>
|
||||||
) : exp.data.company}
|
) : exp.data.company}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="text-sm text-white/60 leading-relaxed mb-3 prose prose-sm prose-invert max-w-none [&_p]:text-white/60 [&_a]:text-purple-200 [&_a:hover]:text-white">
|
<div class="exp-content text-sm leading-relaxed mb-4 prose prose-sm max-w-none">
|
||||||
<Content />
|
<Content />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{exp.data.technologies && exp.data.technologies.length > 0 && (
|
{exp.data.technologies && exp.data.technologies.length > 0 && (
|
||||||
<div class="flex flex-wrap gap-1.5">
|
<div class="flex flex-wrap gap-1.5">
|
||||||
{exp.data.technologies.map((tech: string) => (
|
{exp.data.technologies.map((tech: string) => (
|
||||||
<span class="inline-block px-2 py-0.5 text-xs rounded-full bg-white/[0.06] text-white/50 border border-white/[0.06]">
|
<span class="exp-tech inline-block px-2.5 py-0.5 text-xs rounded-full border">
|
||||||
{tech}
|
{tech}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
144
src/styles/exp-card.css
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
/*
|
||||||
|
* Experience card styles — shared by FR, EN, AR career pages.
|
||||||
|
* Cards use the `.code-card` class to opt out of the layout's
|
||||||
|
* white-on-purple overrides (see main.astro).
|
||||||
|
*
|
||||||
|
* Light mode: white cards on purple gradient background.
|
||||||
|
* Dark mode: translucent cards blending into the gradient.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ────────────────────────────────────────────
|
||||||
|
Card container
|
||||||
|
──────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .exp-card {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border-color: rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .exp-card:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .exp-card--ongoing {
|
||||||
|
background: rgba(255, 255, 255, 0.07);
|
||||||
|
border-color: rgba(255, 255, 255, 0.12);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ────────────────────────────────────────────
|
||||||
|
Typography
|
||||||
|
──────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.exp-card h3 { color: #111827; }
|
||||||
|
.exp-card .exp-meta { color: #9ca3af; }
|
||||||
|
.exp-card .exp-company { color: #6b21a8; }
|
||||||
|
.exp-card .exp-company a { color: #6b21a8; text-decoration: none; }
|
||||||
|
.exp-card .exp-company a:hover { color: #581c87; }
|
||||||
|
.exp-card .exp-content,
|
||||||
|
.exp-card .exp-content p { color: #4b5563; }
|
||||||
|
.exp-card .exp-content a { color: #6b21a8; text-decoration-color: rgba(107, 33, 168, 0.3); }
|
||||||
|
.exp-card .exp-content a:hover { color: #581c87; }
|
||||||
|
|
||||||
|
.dark .exp-card h3 { color: white; }
|
||||||
|
.dark .exp-card .exp-meta { color: rgba(255, 255, 255, 0.3); }
|
||||||
|
.dark .exp-card .exp-company { color: #d4b5ff; }
|
||||||
|
.dark .exp-card .exp-company a { color: #d4b5ff; }
|
||||||
|
.dark .exp-card .exp-company a:hover { color: white; }
|
||||||
|
.dark .exp-card .exp-content,
|
||||||
|
.dark .exp-card .exp-content p { color: rgba(255, 255, 255, 0.6); }
|
||||||
|
.dark .exp-card .exp-content a { color: #d4b5ff; }
|
||||||
|
.dark .exp-card .exp-content a:hover { color: white; }
|
||||||
|
|
||||||
|
/* ────────────────────────────────────────────
|
||||||
|
Tech badges
|
||||||
|
──────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.exp-card .exp-tech {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #6b7280;
|
||||||
|
border-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ────────────────────────────────────────────
|
||||||
|
Logo container
|
||||||
|
──────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.exp-logo {
|
||||||
|
background: #f9fafb;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exp-logo-fallback {
|
||||||
|
background: #f3f4f6;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exp-logo-fallback span {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .exp-logo {
|
||||||
|
background: white;
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .exp-logo-fallback {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .exp-logo-fallback span {
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ────────────────────────────────────────────
|
||||||
|
Type badges
|
||||||
|
──────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.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; }
|
||||||
|
|
||||||
|
.dark .type-badge-blue { background: rgba(96, 165, 250, 0.15); color: #93c5fd; border-color: rgba(96, 165, 250, 0.2); }
|
||||||
|
.dark .type-badge-amber { background: rgba(251, 191, 36, 0.15); color: #fcd34d; border-color: rgba(251, 191, 36, 0.2); }
|
||||||
|
.dark .type-badge-emerald { background: rgba(52, 211, 153, 0.15); color: #6ee7b7; border-color: rgba(52, 211, 153, 0.2); }
|
||||||
|
.dark .type-badge-pink { background: rgba(244, 114, 182, 0.15); color: #f9a8d4; border-color: rgba(244, 114, 182, 0.2); }
|
||||||
|
.dark .type-badge-purple { background: rgba(168, 85, 247, 0.15); color: #d4b5ff; border-color: rgba(168, 85, 247, 0.2); }
|
||||||
|
|
||||||
|
/* ────────────────────────────────────────────
|
||||||
|
Type border (left for LTR, right for RTL)
|
||||||
|
──────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.type-border-blue { border-inline-start-color: #3b82f6; }
|
||||||
|
.type-border-amber { border-inline-start-color: #f59e0b; }
|
||||||
|
.type-border-emerald { border-inline-start-color: #10b981; }
|
||||||
|
.type-border-pink { border-inline-start-color: #ec4899; }
|
||||||
|
.type-border-purple { border-inline-start-color: #a855f7; }
|
||||||
|
|
||||||
|
.dark .type-border-blue { border-inline-start-color: rgba(96, 165, 250, 0.4); }
|
||||||
|
.dark .type-border-amber { border-inline-start-color: rgba(251, 191, 36, 0.4); }
|
||||||
|
.dark .type-border-emerald { border-inline-start-color: rgba(52, 211, 153, 0.4); }
|
||||||
|
.dark .type-border-pink { border-inline-start-color: rgba(244, 114, 182, 0.4); }
|
||||||
|
.dark .type-border-purple { border-inline-start-color: rgba(168, 85, 247, 0.4); }
|
||||||