Compare commits
No commits in common. "76bd2f9fb39422d9552eb87f8d0305307f287430" and "3e4a632adc13b331b86e4f4f97f63022febbf110" have entirely different histories.
76bd2f9fb3
...
3e4a632adc
36 changed files with 290 additions and 290 deletions
|
|
@ -2,7 +2,6 @@ import { defineConfig } from "astro/config";
|
|||
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
import tailwind from "@astrojs/tailwind";
|
||||
import { getAlternateUrls } from "./src/utils/page-translations.ts";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
|
|
@ -11,21 +10,13 @@ export default defineConfig({
|
|||
integrations: [
|
||||
tailwind(),
|
||||
sitemap({
|
||||
serialize(item) {
|
||||
const url = new URL(item.url);
|
||||
const pathname = decodeURIComponent(url.pathname).replace(/\/$/, "") || "/";
|
||||
const alternates = getAlternateUrls(pathname);
|
||||
if (alternates) {
|
||||
const fullUrl = (path) =>
|
||||
path === "/" ? `${url.origin}/` : `${url.origin}${path}/`;
|
||||
item.links = [
|
||||
{ lang: "fr", url: fullUrl(alternates.fr) },
|
||||
{ lang: "en", url: fullUrl(alternates.en) },
|
||||
{ lang: "ar", url: fullUrl(alternates.ar) },
|
||||
{ lang: "x-default", url: fullUrl(alternates.fr) },
|
||||
];
|
||||
}
|
||||
return item;
|
||||
i18n: {
|
||||
defaultLocale: "fr",
|
||||
locales: {
|
||||
fr: "fr",
|
||||
en: "en",
|
||||
ar: "ar",
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -2,13 +2,7 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: 'Raleway';
|
||||
src: url('../fonts/Raleway-SemiBold.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
/* Add Your Custom CSS Here */
|
||||
|
||||
.prose img {
|
||||
border-radius: 30px;
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -7,7 +7,7 @@ interface Props {
|
|||
|
||||
const { href, external = false, class: className = '' } = Astro.props;
|
||||
|
||||
const baseClasses = 'text-purple-200 underline decoration-purple-200/40 hover:decoration-white hover:text-white transition-colors';
|
||||
const baseClasses = 'text-indigo-600 dark:text-indigo-400 hover:underline';
|
||||
const classes = `${baseClasses} ${className}`.trim();
|
||||
|
||||
const externalProps = external ? { target: '_blank', rel: 'noopener noreferrer' } : {};
|
||||
|
|
|
|||
|
|
@ -17,10 +17,15 @@ const { title, description, technologies, url, github, featured = false } = Astr
|
|||
? "bg-white/[0.12] border-purple-300/20 hover:bg-white/[0.18] hover:border-purple-300/30"
|
||||
: "bg-white/[0.06] border-white/[0.1] hover:bg-white/[0.12] hover:border-white/[0.2]",
|
||||
]}>
|
||||
<div class="mb-2">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<h3 class="text-lg font-bold text-white">
|
||||
{title}
|
||||
</h3>
|
||||
{featured && (
|
||||
<span class="text-[10px] font-semibold px-2 py-0.5 rounded-full bg-purple-400/20 text-purple-200 border border-purple-300/20">
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-white/60 leading-relaxed flex-1 mb-4">
|
||||
|
|
|
|||
|
|
@ -79,7 +79,6 @@ const recommendationsCollection = defineCollection({
|
|||
avatar: z.string().optional(),
|
||||
url: z.string().url().optional(),
|
||||
date: z.date(),
|
||||
featured: z.boolean().default(false),
|
||||
relationship: z.string().optional(),
|
||||
lang: z.enum(['fr', 'en']).default('fr'),
|
||||
}),
|
||||
|
|
|
|||
11
src/content/projects/dns-surf.ar.md
Normal file
11
src/content/projects/dns-surf.ar.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: "DNS.Surf"
|
||||
description: "أداة استعلام DNS عالمية. استعلام خوادم DNS من مناطق مختلفة."
|
||||
date: 2023-01-01
|
||||
category: "dev"
|
||||
technologies: ["JavaScript"]
|
||||
url: "https://dns.surf"
|
||||
lang: "ar"
|
||||
---
|
||||
|
||||
أداة عبر الإنترنت للتحقق من استعلام DNS لنطاق من مناطق مختلفة حول العالم. مفيدة لتشخيص مشاكل انتشار DNS.
|
||||
11
src/content/projects/dns-surf.en.md
Normal file
11
src/content/projects/dns-surf.en.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: "DNS.Surf"
|
||||
description: "Worldwide DNS resolution tool. Query DNS servers from different regions."
|
||||
date: 2023-01-01
|
||||
category: "dev"
|
||||
technologies: ["JavaScript"]
|
||||
url: "https://dns.surf"
|
||||
lang: "en"
|
||||
---
|
||||
|
||||
Online tool to check DNS resolution of a domain from different regions worldwide. Useful for diagnosing DNS propagation issues.
|
||||
11
src/content/projects/dns-surf.md
Normal file
11
src/content/projects/dns-surf.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: "DNS.Surf"
|
||||
description: "Outil de résolution DNS mondiale. Interroge les serveurs DNS de différentes régions."
|
||||
date: 2023-01-01
|
||||
category: "dev"
|
||||
technologies: ["JavaScript"]
|
||||
url: "https://dns.surf"
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
Outil en ligne permettant de vérifier la résolution DNS d'un domaine depuis différentes régions du monde. Utile pour diagnostiquer les problèmes de propagation DNS.
|
||||
11
src/content/projects/email-ml.ar.md
Normal file
11
src/content/projects/email-ml.ar.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: "Email.ML"
|
||||
description: "خدمة بريد إلكتروني مؤقت بتصميم بسيط."
|
||||
date: 2023-01-01
|
||||
category: "dev"
|
||||
technologies: ["JavaScript"]
|
||||
url: "https://email.ml"
|
||||
lang: "ar"
|
||||
---
|
||||
|
||||
خدمة بريد إلكتروني مؤقت بتصميم أنيق. استقبال رسائل على عنوان يُمكن التخلّص منه، بدون تسجيل.
|
||||
11
src/content/projects/email-ml.en.md
Normal file
11
src/content/projects/email-ml.en.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: "Email.ML"
|
||||
description: "Minimalist temporary email service."
|
||||
date: 2023-01-01
|
||||
category: "dev"
|
||||
technologies: ["JavaScript"]
|
||||
url: "https://email.ml"
|
||||
lang: "en"
|
||||
---
|
||||
|
||||
Temporary email service with a clean design. Receive emails on a disposable address, no signup required.
|
||||
11
src/content/projects/email-ml.md
Normal file
11
src/content/projects/email-ml.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: "Email.ML"
|
||||
description: "Service d'email temporaire minimaliste."
|
||||
date: 2023-01-01
|
||||
category: "dev"
|
||||
technologies: ["JavaScript"]
|
||||
url: "https://email.ml"
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
Service d'email temporaire au design épuré. Permet de recevoir des emails sur une adresse jetable, sans inscription.
|
||||
11
src/content/projects/gobuild.ar.md
Normal file
11
src/content/projects/gobuild.ar.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: "GoBuild"
|
||||
description: "SaaS لنمذجة المباني للمهنيين في قطاع البناء."
|
||||
date: 2020-06-01
|
||||
category: "dev"
|
||||
technologies: ["TypeScript", "React.js", "Elixir", "PostgreSQL", "Docker"]
|
||||
url: "https://www.gobuild.fr"
|
||||
lang: "ar"
|
||||
---
|
||||
|
||||
تطبيق SaaS يتيح لمهنيي البناء نمذجة وتقدير مشاريعهم. طُوّر بصفتي مديرًا تقنيًا من 2020 إلى 2022.
|
||||
11
src/content/projects/gobuild.en.md
Normal file
11
src/content/projects/gobuild.en.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: "GoBuild"
|
||||
description: "SaaS building modeling platform for construction professionals."
|
||||
date: 2020-06-01
|
||||
category: "dev"
|
||||
technologies: ["TypeScript", "React.js", "Elixir", "PostgreSQL", "Docker"]
|
||||
url: "https://www.gobuild.fr"
|
||||
lang: "en"
|
||||
---
|
||||
|
||||
SaaS application enabling construction professionals to model and estimate their building projects. Developed as CTO from 2020 to 2022.
|
||||
11
src/content/projects/gobuild.md
Normal file
11
src/content/projects/gobuild.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: "GoBuild"
|
||||
description: "SaaS de modélisation de bâtiments pour les professionnels du BTP."
|
||||
date: 2020-06-01
|
||||
category: "dev"
|
||||
technologies: ["TypeScript", "React.js", "Elixir", "PostgreSQL", "Docker"]
|
||||
url: "https://www.gobuild.fr"
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
Application SaaS permettant aux professionnels du bâtiment de modéliser et chiffrer leurs projets de construction. Développé en tant que CTO de 2020 à 2022.
|
||||
|
|
@ -6,7 +6,7 @@ category: "dev"
|
|||
technologies: ["TypeScript", "React.js", "Publicodes", "Node.js"]
|
||||
url: "https://mon-entreprise.urssaf.fr/"
|
||||
github: "https://github.com/betagouv/mon-entreprise"
|
||||
featured: false
|
||||
featured: true
|
||||
lang: "ar"
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ category: "dev"
|
|||
technologies: ["TypeScript", "React.js", "Publicodes", "Node.js"]
|
||||
url: "https://mon-entreprise.urssaf.fr/"
|
||||
github: "https://github.com/betagouv/mon-entreprise"
|
||||
featured: false
|
||||
featured: true
|
||||
lang: "en"
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ category: "dev"
|
|||
technologies: ["TypeScript", "React.js", "Publicodes", "Node.js"]
|
||||
url: "https://mon-entreprise.urssaf.fr/"
|
||||
github: "https://github.com/betagouv/mon-entreprise"
|
||||
featured: false
|
||||
featured: true
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
author: "Daniel Gall"
|
||||
authorRole: "Consultant"
|
||||
company: "Taos Conseil"
|
||||
avatar: daniel-gall.jpg
|
||||
url: https://www.linkedin.com/in/daniel-g-385524
|
||||
date: 2011-12-07
|
||||
lang: "fr"
|
||||
|
|
|
|||
|
|
@ -2,10 +2,8 @@
|
|||
author: "Grégoire Lacoste"
|
||||
authorRole: "Chief Product Officer"
|
||||
company: "CertifiCall"
|
||||
avatar: gregoire-lacoste.jpg
|
||||
url: https://www.linkedin.com/in/gregoirelacoste
|
||||
date: 2020-12-08
|
||||
featured: true
|
||||
lang: "fr"
|
||||
---
|
||||
J'ai eu la chance de travailler avec Jalil sur plusieurs projets d'applications react/node ou php, son expérience, sa vision claire et sa pédagogie a toute épreuve en font un partenaire incontournable pour un projet réussi
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
---
|
||||
author: "Guillaume Gendrillon"
|
||||
authorRole: "Lead designer Information Voyageur et signalétique"
|
||||
company: "RATP"
|
||||
avatar: guillaume-gendrillon.webp
|
||||
company: "RATPgroup"
|
||||
url: https://www.linkedin.com/in/guillaumegendrillon
|
||||
date: 2011-12-05
|
||||
lang: "fr"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ company: "DisMoi SAS"
|
|||
avatar: john-samson.png
|
||||
url: https://www.malt.fr/profile/jalilarfaoui
|
||||
date: 2022-06-17
|
||||
featured: true
|
||||
lang: "fr"
|
||||
---
|
||||
Jalil a su trouver la bonne architecture à notre projet à 5 pattes, il est réactif en cas d'urgence, et s'engage au-delà de son rôle de développeur, très apprécié par les autres membres de l'équipe.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ company: "SNCF Connect & Tech"
|
|||
avatar: maxime-boudier.jpg
|
||||
url: https://www.linkedin.com/in/maximeboudier
|
||||
date: 2020-12-12
|
||||
featured: true
|
||||
lang: "fr"
|
||||
---
|
||||
Une des personnes avec qui j'ai préféré travailler. En plus d'être passionné, très bon techniquement et j'en passe.. Jalil est une personne qu'on apprécie pour ses qualités humaines. J'ai beaucoup appris de toi Jalil, sur plusieurs plans, j'espère que nos chemin se re-croiseront.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
author: "Olivier Cornudet"
|
||||
authorRole: "Consultant manager"
|
||||
company: "e-THEMIS"
|
||||
avatar: olivier-cornudet.jpg
|
||||
url: https://www.linkedin.com/in/olivier-cornudet-9a398738
|
||||
date: 2015-02-11
|
||||
lang: "fr"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ authorRole: "Expert Vue.js | Nuxt"
|
|||
avatar: thomas-kientz.jpg
|
||||
url: https://www.malt.fr/profile/jalilarfaoui
|
||||
date: 2022-06-17
|
||||
featured: true
|
||||
lang: "fr"
|
||||
---
|
||||
Jalil est un développeur et mentor hors pair. Le développement logiciel est pour lui un art dont il adore partager sa passion. Je consulte Jalil régulièrement pour avoir son regard expérimenté tant le choix d'une nouvelle techno que pour des reviews de code. C'est un véritable atout à avoir dans son équipe, je le recommande fortement.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
author: "Vadim Toropoff"
|
||||
authorRole: "Dirigeant"
|
||||
company: "Event Finder"
|
||||
avatar: vadim-toropoff.jpg
|
||||
url: https://www.malt.fr/profile/jalilarfaoui
|
||||
date: 2015-10-08
|
||||
lang: "fr"
|
||||
|
|
|
|||
|
|
@ -18,12 +18,16 @@ const experiences = (await getCollection("experiences"))
|
|||
const recentExperiences = experiences.slice(0, 4);
|
||||
|
||||
const projects = (await getCollection("projects"))
|
||||
.filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev" && p.data.featured)
|
||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||
.filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev")
|
||||
.sort((a, b) => {
|
||||
if (a.data.featured !== b.data.featured) return a.data.featured ? -1 : 1;
|
||||
return b.data.date.getTime() - a.data.date.getTime();
|
||||
})
|
||||
.slice(0, 3);
|
||||
|
||||
const recommendations = (await getCollection("recommendations"))
|
||||
.filter((r) => r.data.featured)
|
||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
|
||||
.slice(0, 3);
|
||||
|
||||
const recommendationTexts = recommendations.map((rec) => ({
|
||||
...rec,
|
||||
|
|
@ -46,18 +50,16 @@ function formatMonth(dateStr: string) {
|
|||
>
|
||||
<section dir="rtl" lang="ar" class="relative z-20 max-w-3xl mx-auto my-12 px-7 lg:px-0">
|
||||
<div class="mb-10">
|
||||
<h1 class="text-4xl sm:text-5xl font-semibold text-white font-display tracking-wide">حِرَفيّ البرمجيات</h1>
|
||||
<div class="mt-6 space-y-4 text-lg text-white/60 leading-relaxed max-w-2xl">
|
||||
<p>
|
||||
TDD، Clean Code، Domain-Driven Design: هذه طريقتي في بناء البرمجيات. أرافق الفرق كمطوّر أول، أو قائد تقني، أو مدرب تقني. أدواتي: TypeScript/JavaScript، وكذلك PHP وElixir.
|
||||
</p>
|
||||
<p>
|
||||
ما يميّزني ربّما: أهتمّ بجودة الكود بقدر اهتمامي بما ينتجه. أفضّل البرمجيات الحرّة والأدوات التي تلبّي احتياجات حقيقية. أتساءل أيضًا عن التحيّزات التي ندرجها في الكود، والتي تُديم علاقات اجتماعية تستحقّ المساءلة.
|
||||
</p>
|
||||
<p>
|
||||
أُدرّس البرمجة في <Link href="https://www.univ-jfc.fr/" external>جامعة شامبوليون</Link> وأنشّط مجتمع <Link href="https://www.meetup.com/software-crafters-albi/" external>Software Crafters Albi</Link> منذ 2018.
|
||||
<h1 class="text-4xl sm:text-5xl font-bold text-white">برمجة</h1>
|
||||
<p class="mt-4 text-lg text-white/65 leading-relaxed max-w-2xl">
|
||||
أكثر من 20 سنة في بناء البرمجيات. Craftsmanship، TDD، DDD — وهاجس التحيّزات التي نضعها في الكود دون أن ندري.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="facet-card rounded-2xl bg-white/[0.06] border border-white/[0.1] p-6 mb-10">
|
||||
<p class="text-white/70 leading-relaxed">
|
||||
مطوّر مستقل مقيم في <strong class="text-white">ألبي، فرنسا</strong>، أرافق الفرق كمطوّر أول، أو قائد تقني، أو مدرب تقني. أفضّل البرمجيات الحرّة والأدوات التي تلبي احتياجات حقيقية.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-12">
|
||||
|
|
|
|||
|
|
@ -5,9 +5,6 @@ import RecommendationCard from "../../../components/code/RecommendationCard.astr
|
|||
|
||||
const recommendations = (await getCollection("recommendations"))
|
||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||
|
||||
const leftCol = recommendations.filter((_, i) => i % 2 === 0);
|
||||
const rightCol = recommendations.filter((_, i) => i % 2 === 1);
|
||||
---
|
||||
|
||||
<Layout
|
||||
|
|
@ -27,26 +24,9 @@ const rightCol = recommendations.filter((_, i) => i % 2 === 1);
|
|||
<p class="mt-3 text-white/60 text-lg">ما يقوله الأشخاص الذين عملت معهم.</p>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:grid grid-cols-2 gap-5 items-start">
|
||||
{[leftCol, rightCol].map((col) => (
|
||||
<div class="space-y-5">
|
||||
{col.map((rec) => (
|
||||
<RecommendationCard
|
||||
author={rec.data.author}
|
||||
authorRole={rec.data.authorRole}
|
||||
company={rec.data.company}
|
||||
text={rec.body || ''}
|
||||
date={rec.data.date}
|
||||
avatar={rec.data.avatar}
|
||||
url={rec.data.url}
|
||||
lang={rec.data.lang}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div class="sm:hidden space-y-5">
|
||||
<div class="columns-1 sm:columns-2 gap-5 space-y-5">
|
||||
{recommendations.map((rec) => (
|
||||
<div class="break-inside-avoid">
|
||||
<RecommendationCard
|
||||
author={rec.data.author}
|
||||
authorRole={rec.data.authorRole}
|
||||
|
|
@ -57,6 +37,7 @@ const rightCol = recommendations.filter((_, i) => i % 2 === 1);
|
|||
url={rec.data.url}
|
||||
lang={rec.data.lang}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import Layout from "../../layouts/main.astro";
|
|||
import Link from "../../components/Link.astro";
|
||||
import FeaturedRecommendation from "../../components/code/FeaturedRecommendation.astro";
|
||||
import ProjectCard from "../../components/code/ProjectCard.astro";
|
||||
import SkillBadge from "../../components/code/SkillBadge.astro";
|
||||
import logoTiqa from "../../assets/images/logo-tiqa-blanc.png";
|
||||
import skillsData from "../../data/skills.json";
|
||||
|
||||
const locale = "fr";
|
||||
|
||||
|
|
@ -16,18 +18,24 @@ const experiences = (await getCollection("experiences"))
|
|||
const recentExperiences = experiences.slice(0, 4);
|
||||
|
||||
const projects = (await getCollection("projects"))
|
||||
.filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev" && p.data.featured)
|
||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||
.filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev")
|
||||
.sort((a, b) => {
|
||||
if (a.data.featured !== b.data.featured) return a.data.featured ? -1 : 1;
|
||||
return b.data.date.getTime() - a.data.date.getTime();
|
||||
})
|
||||
.slice(0, 3);
|
||||
|
||||
const recommendations = (await getCollection("recommendations"))
|
||||
.filter((r) => r.data.featured)
|
||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
|
||||
.slice(0, 3);
|
||||
|
||||
const recommendationTexts = recommendations.map((rec) => ({
|
||||
...rec,
|
||||
text: rec.body || '',
|
||||
}));
|
||||
|
||||
const topSkills = skillsData.categories.slice(0, 3);
|
||||
|
||||
function formatMonth(dateStr: string) {
|
||||
const [year, month] = dateStr.split('-');
|
||||
return new Date(parseInt(year), parseInt(month) - 1)
|
||||
|
|
@ -40,45 +48,43 @@ function formatMonth(dateStr: string) {
|
|||
facet="code"
|
||||
description="Parcours professionnel de Jalil Arfaoui : développeur freelance spécialisé en Software Craftsmanship, TDD, DDD. TypeScript, PHP, Elixir."
|
||||
>
|
||||
<section class="relative z-20 max-w-3xl mx-auto my-16 px-7 lg:px-0">
|
||||
<div class="mb-16">
|
||||
<h1 class="text-4xl sm:text-5xl font-semibold text-white font-display tracking-wide">Artisan du logiciel</h1>
|
||||
<div class="mt-6 space-y-4 text-lg text-white/60 leading-relaxed max-w-2xl">
|
||||
<p>
|
||||
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.
|
||||
<section class="relative z-20 max-w-3xl mx-auto my-12 px-7 lg:px-0">
|
||||
<div class="mb-10">
|
||||
<h1 class="text-4xl sm:text-5xl font-bold text-white">Code</h1>
|
||||
<p class="mt-4 text-lg text-white/65 leading-relaxed max-w-2xl">
|
||||
Plus de 20 ans à construire du logiciel. Craftsmanship, TDD, DDD — et une obsession pour les biais qu'on met dans le code sans le savoir.
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-16">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-2xl font-bold text-white ">Parcours</h2>
|
||||
<div class="facet-card rounded-2xl bg-white/[0.06] border border-white/[0.1] p-6 mb-10">
|
||||
<p class="text-white/70 leading-relaxed">
|
||||
Développeur freelance basé à <strong class="text-white">Albi</strong>, j'accompagne les équipes comme développeur senior, tech lead ou coach technique. Je privilégie le logiciel libre et les outils qui répondent à de vrais besoins.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-12">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h2 class="text-2xl font-bold text-white">Parcours</h2>
|
||||
<a href="/code/parcours" class="text-sm text-purple-200 hover:text-white transition-colors">Voir tout →</a>
|
||||
</div>
|
||||
<div class="divide-y divide-white/[0.08]">
|
||||
<div class="space-y-3">
|
||||
{recentExperiences.map((exp) => {
|
||||
const isOngoing = !exp.data.endDate;
|
||||
const start = formatMonth(exp.data.startDate);
|
||||
const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'Présent';
|
||||
return (
|
||||
<div class="py-4 first:pt-0 last:pb-0">
|
||||
<div class="facet-card rounded-xl bg-white/[0.06] border border-white/[0.1] p-4 hover:bg-white/[0.1] transition-colors">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="min-w-0">
|
||||
<p class="font-semibold text-white text-sm">{exp.data.role}</p>
|
||||
<p class="text-sm text-white/45 mt-0.5">
|
||||
<p class="font-semibold text-white text-sm truncate">{exp.data.role}</p>
|
||||
<p class="text-xs text-white/50 mt-0.5">
|
||||
{exp.data.companyUrl ? (
|
||||
<a href={exp.data.companyUrl} target="_blank" rel="noopener noreferrer" class="text-purple-200 hover:text-white transition-colors">{exp.data.company}</a>
|
||||
) : exp.data.company}
|
||||
{exp.data.location && ` · ${exp.data.location}`}
|
||||
</p>
|
||||
</div>
|
||||
<span class:list={["text-sm whitespace-nowrap flex-shrink-0", isOngoing ? "text-purple-200" : "text-white/35"]}>
|
||||
<span class:list={["text-xs whitespace-nowrap flex-shrink-0", isOngoing ? "text-purple-200 font-semibold" : "text-white/40"]}>
|
||||
{start} — {end}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -88,12 +94,12 @@ function formatMonth(dateStr: string) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-16">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-2xl font-bold text-white ">Projets</h2>
|
||||
<div class="mb-12">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h2 class="text-2xl font-bold text-white">Projets</h2>
|
||||
<a href="/code/projets" class="text-sm text-purple-200 hover:text-white transition-colors">Voir tous →</a>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{projects.map((project) => (
|
||||
<ProjectCard
|
||||
title={project.data.title}
|
||||
|
|
@ -108,12 +114,12 @@ function formatMonth(dateStr: string) {
|
|||
</div>
|
||||
|
||||
{recommendationTexts.length > 0 && (
|
||||
<div class="mb-16">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-2xl font-bold text-white ">Recommandations</h2>
|
||||
<div class="mb-12">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h2 class="text-2xl font-bold text-white">Recommandations</h2>
|
||||
<a href="/code/recommandations" class="text-sm text-purple-200 hover:text-white transition-colors">Voir toutes →</a>
|
||||
</div>
|
||||
<div class="space-y-5">
|
||||
<div class="space-y-4">
|
||||
{recommendationTexts.map((rec) => (
|
||||
<FeaturedRecommendation
|
||||
author={rec.data.author}
|
||||
|
|
@ -128,42 +134,60 @@ function formatMonth(dateStr: string) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div class="mb-16">
|
||||
<h2 class="text-2xl font-bold text-white mb-6">Valeurs & Approche</h2>
|
||||
<ul class="space-y-3 text-white/60">
|
||||
<div class="mb-12">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h2 class="text-2xl font-bold text-white">Compétences</h2>
|
||||
<a href="/code/competences" class="text-sm text-purple-200 hover:text-white transition-colors">Voir toutes →</a>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
{topSkills.map((category) => (
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-white/40 uppercase tracking-wider mb-2">{category.name[locale as keyof typeof category.name]}</p>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{category.skills.map((skill) => (
|
||||
<SkillBadge name={skill} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="text-2xl font-bold text-white mb-5">Valeurs & Approche</h2>
|
||||
<div class="facet-card rounded-2xl bg-white/[0.04] border border-white/[0.08] p-6 mb-10">
|
||||
<ul class="space-y-3 text-white/70">
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="w-1 h-1 rounded-full bg-purple-300/60 mt-2.5 flex-shrink-0"></span>
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-purple-400 mt-2 flex-shrink-0"></span>
|
||||
<span>Le mouvement <Link href="http://manifesto.softwarecraftsmanship.org/#/fr-fr" external>Software Craftsmanship</Link></span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="w-1 h-1 rounded-full bg-purple-300/60 mt-2.5 flex-shrink-0"></span>
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-purple-400 mt-2 flex-shrink-0"></span>
|
||||
<span>L'utilité sociale du développeur</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="w-1 h-1 rounded-full bg-purple-300/60 mt-2.5 flex-shrink-0"></span>
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-purple-400 mt-2 flex-shrink-0"></span>
|
||||
<span>Être fier de son travail, mais sans égo</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="w-1 h-1 rounded-full bg-purple-300/60 mt-2.5 flex-shrink-0"></span>
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-purple-400 mt-2 flex-shrink-0"></span>
|
||||
<span>Approche <strong class="text-white">Domain Driven Design</strong></span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="w-1 h-1 rounded-full bg-purple-300/60 mt-2.5 flex-shrink-0"></span>
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-purple-400 mt-2 flex-shrink-0"></span>
|
||||
<span>Organisation <Link href="https://agilemanifesto.org/iso/fr/manifesto.html" external>agile</Link> : itération et amélioration continue</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="mb-16">
|
||||
<h2 class="text-2xl font-bold text-white mb-6">Communauté & Enseignement</h2>
|
||||
<p class="text-white/60 leading-relaxed">
|
||||
<h2 class="text-2xl font-bold text-white mb-5">Communauté & Enseignement</h2>
|
||||
<div class="facet-card rounded-2xl bg-white/[0.04] border border-white/[0.08] p-6 mb-10">
|
||||
<p class="text-white/70 leading-relaxed">
|
||||
J'anime les <Link href="https://www.meetup.com/software-crafters-albi/" external>Software Crafters d'Albi</Link> depuis 2018. Enseignant en génie logiciel à <Link href="https://www.univ-jfc.fr/" external>l'université Champollion</Link> d'Albi depuis 2019.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-16">
|
||||
<h2 class="text-2xl font-bold text-white mb-6">En ligne</h2>
|
||||
<div class="flex flex-wrap gap-x-6 gap-y-2">
|
||||
<h2 class="text-2xl font-bold text-white mb-5">En ligne</h2>
|
||||
<div class="facet-card flex flex-wrap gap-3 mb-12">
|
||||
{[
|
||||
{ label: 'LinkedIn', href: 'https://www.linkedin.com/in/jalil' },
|
||||
{ label: 'Malt', href: 'https://www.malt.fr/profile/jalilarfaoui' },
|
||||
|
|
@ -176,18 +200,20 @@ function formatMonth(dateStr: string) {
|
|||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm text-white/50 hover:text-white transition-colors py-1"
|
||||
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-full bg-white/[0.06] border border-white/[0.1] text-sm text-white/70 hover:bg-white/[0.12] hover:text-white transition-all duration-200"
|
||||
>
|
||||
{link.label}
|
||||
<svg class="w-3 h-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center pt-10 border-t border-white/[0.06]">
|
||||
<div class="text-center pt-8 border-t border-white/[0.08]">
|
||||
<Image src={logoTiqa} alt="Logo Tiqa" class="mx-auto mb-4" width={160} />
|
||||
<p class="text-sm text-white/30">
|
||||
<strong class="text-white/45">SAS Tiqa</strong><br />
|
||||
<p class="text-sm text-white/40">
|
||||
<strong class="text-white/60">SAS Tiqa</strong><br />
|
||||
12, rue Fabre d'Églantine — 81 000 Albi<br />
|
||||
811 917 871 RCS Albi
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -5,9 +5,6 @@ import RecommendationCard from "../../components/code/RecommendationCard.astro";
|
|||
|
||||
const recommendations = (await getCollection("recommendations"))
|
||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||
|
||||
const leftCol = recommendations.filter((_, i) => i % 2 === 0);
|
||||
const rightCol = recommendations.filter((_, i) => i % 2 === 1);
|
||||
---
|
||||
|
||||
<Layout
|
||||
|
|
@ -27,26 +24,9 @@ const rightCol = recommendations.filter((_, i) => i % 2 === 1);
|
|||
<p class="mt-3 text-white/60 text-lg">Ce que disent les gens avec qui j'ai travaillé.</p>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:grid grid-cols-2 gap-5 items-start">
|
||||
{[leftCol, rightCol].map((col) => (
|
||||
<div class="space-y-5">
|
||||
{col.map((rec) => (
|
||||
<RecommendationCard
|
||||
author={rec.data.author}
|
||||
authorRole={rec.data.authorRole}
|
||||
company={rec.data.company}
|
||||
text={rec.body || ''}
|
||||
date={rec.data.date}
|
||||
avatar={rec.data.avatar}
|
||||
url={rec.data.url}
|
||||
lang={rec.data.lang}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div class="sm:hidden space-y-5">
|
||||
<div class="columns-1 sm:columns-2 gap-5 space-y-5">
|
||||
{recommendations.map((rec) => (
|
||||
<div class="break-inside-avoid">
|
||||
<RecommendationCard
|
||||
author={rec.data.author}
|
||||
authorRole={rec.data.authorRole}
|
||||
|
|
@ -57,6 +37,7 @@ const rightCol = recommendations.filter((_, i) => i % 2 === 1);
|
|||
url={rec.data.url}
|
||||
lang={rec.data.lang}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -18,12 +18,16 @@ const experiences = (await getCollection("experiences"))
|
|||
const recentExperiences = experiences.slice(0, 4);
|
||||
|
||||
const projects = (await getCollection("projects"))
|
||||
.filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev" && p.data.featured)
|
||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||
.filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev")
|
||||
.sort((a, b) => {
|
||||
if (a.data.featured !== b.data.featured) return a.data.featured ? -1 : 1;
|
||||
return b.data.date.getTime() - a.data.date.getTime();
|
||||
})
|
||||
.slice(0, 3);
|
||||
|
||||
const recommendations = (await getCollection("recommendations"))
|
||||
.filter((r) => r.data.featured)
|
||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
|
||||
.slice(0, 3);
|
||||
|
||||
const recommendationTexts = recommendations.map((rec) => ({
|
||||
...rec,
|
||||
|
|
@ -46,18 +50,16 @@ function formatMonth(dateStr: string) {
|
|||
>
|
||||
<section class="relative z-20 max-w-3xl mx-auto my-12 px-7 lg:px-0">
|
||||
<div class="mb-10">
|
||||
<h1 class="text-4xl sm:text-5xl font-semibold text-white font-display tracking-wide">Software Craftsman</h1>
|
||||
<div class="mt-6 space-y-4 text-lg text-white/60 leading-relaxed max-w-2xl">
|
||||
<p>
|
||||
TDD, Clean Code, Domain-Driven Design: that's how I build software. I work with teams as a senior developer, tech lead or technical coach. My stack: TypeScript/JavaScript, but also PHP and Elixir.
|
||||
</p>
|
||||
<p>
|
||||
What sets me apart, perhaps: I care as much about code quality as about what it produces. I favor free software and tools that address real needs. I also question the biases we embed in code, perpetuating social dynamics that deserve scrutiny.
|
||||
</p>
|
||||
<p>
|
||||
I teach programming at <Link href="https://www.univ-jfc.fr/" external>Université Champollion</Link> and have been running the <Link href="https://www.meetup.com/software-crafters-albi/" external>Software Crafters Albi</Link> meetup since 2018.
|
||||
<h1 class="text-4xl sm:text-5xl font-bold text-white">Code</h1>
|
||||
<p class="mt-4 text-lg text-white/65 leading-relaxed max-w-2xl">
|
||||
Over 20 years building software. Craftsmanship, TDD, DDD — and an obsession with the biases we unknowingly put into code.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="facet-card rounded-2xl bg-white/[0.06] border border-white/[0.1] p-6 mb-10">
|
||||
<p class="text-white/70 leading-relaxed">
|
||||
Freelance developer based in <strong class="text-white">Albi, France</strong>, I work with teams as a senior developer, tech lead or technical coach. I favor free software and tools that address real needs.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-12">
|
||||
|
|
|
|||
|
|
@ -5,9 +5,6 @@ import RecommendationCard from "../../../components/code/RecommendationCard.astr
|
|||
|
||||
const recommendations = (await getCollection("recommendations"))
|
||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||
|
||||
const leftCol = recommendations.filter((_, i) => i % 2 === 0);
|
||||
const rightCol = recommendations.filter((_, i) => i % 2 === 1);
|
||||
---
|
||||
|
||||
<Layout
|
||||
|
|
@ -27,26 +24,9 @@ const rightCol = recommendations.filter((_, i) => i % 2 === 1);
|
|||
<p class="mt-3 text-white/60 text-lg">What people I've worked with say about me.</p>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:grid grid-cols-2 gap-5 items-start">
|
||||
{[leftCol, rightCol].map((col) => (
|
||||
<div class="space-y-5">
|
||||
{col.map((rec) => (
|
||||
<RecommendationCard
|
||||
author={rec.data.author}
|
||||
authorRole={rec.data.authorRole}
|
||||
company={rec.data.company}
|
||||
text={rec.body || ''}
|
||||
date={rec.data.date}
|
||||
avatar={rec.data.avatar}
|
||||
url={rec.data.url}
|
||||
lang={rec.data.lang}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div class="sm:hidden space-y-5">
|
||||
<div class="columns-1 sm:columns-2 gap-5 space-y-5">
|
||||
{recommendations.map((rec) => (
|
||||
<div class="break-inside-avoid">
|
||||
<RecommendationCard
|
||||
author={rec.data.author}
|
||||
authorRole={rec.data.authorRole}
|
||||
|
|
@ -57,6 +37,7 @@ const rightCol = recommendations.filter((_, i) => i % 2 === 1);
|
|||
url={rec.data.url}
|
||||
lang={rec.data.lang}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { Locale } from "./i18n";
|
||||
import { getPhotoAlbumsPath, getPhotoBlogPath } from "./i18n";
|
||||
|
||||
/**
|
||||
* Groupes de pages traduites.
|
||||
|
|
@ -15,7 +14,6 @@ const translationGroups: Record<Locale, string>[] = [
|
|||
{ fr: "/code/competences", en: "/en/code/skills", ar: "/ar/برمجة/مهارات" },
|
||||
{ fr: "/theatre", en: "/en/acting", ar: "/ar/مسرح" },
|
||||
{ fr: "/photo", en: "/en/photo", ar: "/ar/تصوير" },
|
||||
{ fr: "/photo/blog", en: "/en/photo/blog", ar: "/ar/تصوير/مدونة" },
|
||||
];
|
||||
|
||||
/** Index inversé : pathname → groupe de traduction */
|
||||
|
|
@ -26,59 +24,13 @@ for (const group of translationGroups) {
|
|||
}
|
||||
}
|
||||
|
||||
/** Pattern dynamique pour les routes paramétrées */
|
||||
interface DynamicPattern {
|
||||
regex: RegExp;
|
||||
buildUrls: (match: RegExpMatchArray) => Record<Locale, string>;
|
||||
}
|
||||
|
||||
const dynamicPatterns: DynamicPattern[] = [
|
||||
{
|
||||
// Photo albums: /photo/albums/{cat}, /en/photo/albums/{cat}, /ar/تصوير/ألبومات/{cat}
|
||||
regex: /^(?:\/photo\/albums|\/en\/photo\/albums|\/ar\/تصوير\/ألبومات)\/([^/]+)$/,
|
||||
buildUrls: (match) => {
|
||||
const category = match[1];
|
||||
return {
|
||||
fr: `${getPhotoAlbumsPath("fr")}/${category}`,
|
||||
en: `${getPhotoAlbumsPath("en")}/${category}`,
|
||||
ar: `${getPhotoAlbumsPath("ar")}/${category}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
// Photo blog posts: /photo/blog/{year}/{slug}, /en/photo/blog/{year}/{slug}, /ar/تصوير/مدونة/{year}/{slug}
|
||||
regex: /^(?:\/photo\/blog|\/en\/photo\/blog|\/ar\/تصوير\/مدونة)\/(\d{4})\/([^/]+)$/,
|
||||
buildUrls: (match) => {
|
||||
const [, year, slug] = match;
|
||||
return {
|
||||
fr: `${getPhotoBlogPath("fr")}/${year}/${slug}`,
|
||||
en: `${getPhotoBlogPath("en")}/${year}/${slug}`,
|
||||
ar: `${getPhotoBlogPath("ar")}/${year}/${slug}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function matchDynamicPattern(
|
||||
pathname: string,
|
||||
): Record<Locale, string> | undefined {
|
||||
for (const pattern of dynamicPatterns) {
|
||||
const match = pathname.match(pattern.regex);
|
||||
if (match) {
|
||||
return pattern.buildUrls(match);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les URLs alternatives pour un pathname donné.
|
||||
* Les trailing slashes sont normalisés.
|
||||
* Essaie d'abord le lookup statique, puis le matching dynamique.
|
||||
*/
|
||||
export function getAlternateUrls(
|
||||
pathname: string,
|
||||
): Record<Locale, string> | undefined {
|
||||
const normalized = pathname.replace(/\/$/, "") || "/";
|
||||
return pathIndex.get(normalized) ?? matchDynamicPattern(normalized);
|
||||
return pathIndex.get(normalized);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,7 @@ export default {
|
|||
darkMode: "class",
|
||||
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
display: ['Raleway', 'sans-serif'],
|
||||
},
|
||||
},
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require("@tailwindcss/typography")],
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue