diff --git a/src/components/code/TalkCard.astro b/src/components/code/TalkCard.astro
new file mode 100644
index 0000000..94210ca
--- /dev/null
+++ b/src/components/code/TalkCard.astro
@@ -0,0 +1,68 @@
+---
+import type { Locale } from '../../utils/i18n';
+
+interface Props {
+ title: string;
+ description: string;
+ event?: string;
+ dateFormatted?: string;
+ location?: string;
+ video?: string;
+ tags?: string[];
+ copresenters?: { name: string; url: string }[];
+ lang?: Locale;
+}
+
+const { title, description, event, dateFormatted, location, video, tags, copresenters, lang = 'fr' } = Astro.props;
+
+const copresenterLabel = { fr: 'Co-présenté avec', en: 'Co-presented with', ar: 'محاضرة مشتركة مع' }[lang];
+
+function getYouTubeId(url: string): string | null {
+ const match = url.match(/(?:youtu\.be\/|youtube\.com\/watch\?v=)([^&]+)/);
+ return match?.[1] ?? null;
+}
+
+const videoId = video ? getYouTubeId(video) : null;
+---
+
+
+ {(event || dateFormatted || location) && (
+
+ {[event, dateFormatted, location].filter(Boolean).join(' · ')}
+
+ )}
+
{title}
+
{description}
+ {copresenters && copresenters.length > 0 && (
+
+ {copresenterLabel}{' '}
+ {copresenters.map((cp, i) => (
+ <>
+ {i > 0 && ', '}
+ {cp.name}
+ >
+ ))}
+
+ )}
+
+ {tags && tags.length > 0 && (
+
+ {tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ )}
+
+ {videoId && (
+
+ )}
+
diff --git a/src/components/code/TalksSection.astro b/src/components/code/TalksSection.astro
new file mode 100644
index 0000000..3cf2248
--- /dev/null
+++ b/src/components/code/TalksSection.astro
@@ -0,0 +1,38 @@
+---
+import { getLocalizedCollection } from "../../utils/content-i18n";
+import TalkCard from "./TalkCard.astro";
+import type { Locale } from "../../utils/i18n";
+
+interface Props {
+ locale: Locale;
+}
+
+const { locale } = Astro.props;
+
+const talks = (await getLocalizedCollection("talks", locale))
+ .filter((t) => !t.data.draft)
+ .sort((a, b) => b.data.date!.getTime() - a.data.date!.getTime());
+
+const sectionTitle = { fr: 'Talks', en: 'Talks', ar: 'محاضرات' }[locale];
+---
+
+{talks.length > 0 && (
+
+
{sectionTitle}
+
+ {talks.map((talk) => (
+
+ ))}
+
+
+)}
diff --git a/src/content.config.ts b/src/content.config.ts
index cdd0a23..418bbc1 100644
--- a/src/content.config.ts
+++ b/src/content.config.ts
@@ -87,22 +87,28 @@ const recommendationsCollection = defineCollection({
});
const talksCollection = defineCollection({
- loader: glob({ pattern: "**/*.md", base: "./src/content/talks" }),
+ loader: glob({ pattern: "**/*.md", base: "./src/content/talks", ...stripExtension('md') }),
schema: z.object({
title: z.string(),
description: z.string(),
- date: z.date(),
- dateFormatted: z.string(),
- event: z.string(),
- location: z.string(),
+ date: z.date().optional(),
+ event: z.string().optional(),
+ location: z.string().optional(),
+ copresenters: z.array(z.object({
+ name: z.string(),
+ url: z.string().url(),
+ })).optional(),
slides: z.string().url().optional(),
video: z.string().url().optional(),
image: z.string().optional(),
imageAlt: z.string().optional(),
tags: z.array(z.string()).optional(),
- draft: z.boolean().default(false),
+ draft: z.boolean().optional(),
lang: z.enum(['fr', 'en', 'ar']).default('fr'),
- }),
+ }).transform((data) => ({
+ ...data,
+ dateFormatted: data.date ? formatDate(data.date, data.lang) : undefined,
+ })),
});
const photoBlogPostsCollection = defineCollection({
diff --git a/src/content/talks/react-academy-conf.ar.md b/src/content/talks/react-academy-conf.ar.md
new file mode 100644
index 0000000..d21ddcf
--- /dev/null
+++ b/src/content/talks/react-academy-conf.ar.md
@@ -0,0 +1,5 @@
+---
+title: "Redux-saga مقابل Redux-observable"
+description: "مقارنة بين طريقتين لإدارة العمليات غير المتزامنة مع Redux: redux-saga و redux-observable."
+lang: ar
+---
diff --git a/src/content/talks/react-academy-conf.en.md b/src/content/talks/react-academy-conf.en.md
new file mode 100644
index 0000000..e8d10a1
--- /dev/null
+++ b/src/content/talks/react-academy-conf.en.md
@@ -0,0 +1,5 @@
+---
+title: "Redux-saga vs Redux-observable"
+description: "Comparison of two approaches to handling asynchronous operations with Redux: redux-saga and redux-observable."
+lang: en
+---
diff --git a/src/content/talks/react-academy-conf.md b/src/content/talks/react-academy-conf.md
new file mode 100644
index 0000000..a279299
--- /dev/null
+++ b/src/content/talks/react-academy-conf.md
@@ -0,0 +1,13 @@
+---
+title: "Redux-saga vs Redux-observable"
+description: "Comparaison de deux approches pour gérer l'asynchronisme avec Redux : redux-saga et redux-observable."
+date: 2018-11-01
+event: "React Academy Conf"
+location: "Paris, École 42"
+video: "https://www.youtube.com/watch?v=u9wULmOQ7sU"
+copresenters:
+ - name: "Moïse Fiscal"
+ url: "https://www.linkedin.com/in/mo%C3%AFse-fiscal-35634622/"
+tags: ["Redux", "RxJS", "redux-observable", "redux-saga", "React"]
+lang: fr
+---
diff --git a/src/pages/ar/برمجة/index.astro b/src/pages/ar/برمجة/index.astro
index 087eb3e..1671b72 100644
--- a/src/pages/ar/برمجة/index.astro
+++ b/src/pages/ar/برمجة/index.astro
@@ -12,6 +12,7 @@ import MaltIcon from "../../../components/icons/MaltIcon.astro";
import StackOverflowIcon from "../../../components/icons/StackOverflowIcon.astro";
import GitHubIcon from "../../../components/icons/GitHubIcon.astro";
import ForgejoIcon from "../../../components/icons/ForgejoIcon.astro";
+import TalksSection from "../../../components/code/TalksSection.astro";
import { getProjectBaseSlug, getProjectsBasePath } from "../../../utils/i18n";
import logoTiqa from "../../../assets/images/logo-tiqa-blanc.png";
@@ -175,6 +176,8 @@ function formatMonth(dateStr: string) {
)}
+
+
القيم والمنهج
diff --git a/src/pages/code/index.astro b/src/pages/code/index.astro
index 927d916..336ecfa 100644
--- a/src/pages/code/index.astro
+++ b/src/pages/code/index.astro
@@ -12,6 +12,7 @@ import MaltIcon from "../../components/icons/MaltIcon.astro";
import StackOverflowIcon from "../../components/icons/StackOverflowIcon.astro";
import GitHubIcon from "../../components/icons/GitHubIcon.astro";
import ForgejoIcon from "../../components/icons/ForgejoIcon.astro";
+import TalksSection from "../../components/code/TalksSection.astro";
import { getProjectBaseSlug, getProjectsBasePath } from "../../utils/i18n";
import logoTiqa from "../../assets/images/logo-tiqa-blanc.png";
@@ -175,6 +176,8 @@ function formatMonth(dateStr: string) {
)}
+
+
Valeurs & Approche
diff --git a/src/pages/en/code/index.astro b/src/pages/en/code/index.astro
index 081f7d8..f980992 100644
--- a/src/pages/en/code/index.astro
+++ b/src/pages/en/code/index.astro
@@ -12,6 +12,7 @@ import MaltIcon from "../../../components/icons/MaltIcon.astro";
import StackOverflowIcon from "../../../components/icons/StackOverflowIcon.astro";
import GitHubIcon from "../../../components/icons/GitHubIcon.astro";
import ForgejoIcon from "../../../components/icons/ForgejoIcon.astro";
+import TalksSection from "../../../components/code/TalksSection.astro";
import { getProjectBaseSlug, getProjectsBasePath } from "../../../utils/i18n";
import logoTiqa from "../../../assets/images/logo-tiqa-blanc.png";
@@ -175,6 +176,8 @@ function formatMonth(dateStr: string) {
)}
+
+