Talk React Academy Conf : redux-saga vs redux-observable avec Moïse Fiscal (FR/EN/AR)
Collection talks avec copresenters dans le schema, dateFormatted calculé, vidéo YouTube embeddée. Composants TalkCard et TalksSection extraits pour éviter la duplication entre les 3 pages code/index.
This commit is contained in:
parent
d97c92a20b
commit
6f749f0790
9 changed files with 151 additions and 7 deletions
68
src/components/code/TalkCard.astro
Normal file
68
src/components/code/TalkCard.astro
Normal file
|
|
@ -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;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="rounded-2xl border bg-white/[0.05] border-white/[0.08] p-6">
|
||||||
|
{(event || dateFormatted || location) && (
|
||||||
|
<div class="mb-2">
|
||||||
|
<span class="text-sm text-purple-200/70">{[event, dateFormatted, location].filter(Boolean).join(' · ')}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h3 class="text-xl font-bold text-white mb-3">{title}</h3>
|
||||||
|
<p class="text-white/60 leading-relaxed mb-3">{description}</p>
|
||||||
|
{copresenters && copresenters.length > 0 && (
|
||||||
|
<p class="text-sm text-white/50 mb-5">
|
||||||
|
{copresenterLabel}{' '}
|
||||||
|
{copresenters.map((cp, i) => (
|
||||||
|
<>
|
||||||
|
{i > 0 && ', '}
|
||||||
|
<a href={cp.url} target="_blank" rel="noopener noreferrer" class="text-purple-200 hover:text-white transition-colors">{cp.name}</a>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tags && tags.length > 0 && (
|
||||||
|
<div class="flex flex-wrap gap-1.5 mb-5">
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<span class="inline-block px-2.5 py-1 text-xs rounded-full bg-purple-400/15 text-purple-200 border border-purple-300/15">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{videoId && (
|
||||||
|
<iframe
|
||||||
|
src={`https://www.youtube-nocookie.com/embed/${videoId}`}
|
||||||
|
title={title}
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowfullscreen
|
||||||
|
loading="lazy"
|
||||||
|
class="w-full aspect-video rounded-xl"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
38
src/components/code/TalksSection.astro
Normal file
38
src/components/code/TalksSection.astro
Normal file
|
|
@ -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 && (
|
||||||
|
<div class="mb-20 -mx-7 px-7 py-8 bg-white/[0.03] rounded-2xl">
|
||||||
|
<h2 class="text-2xl font-bold text-white mb-6">{sectionTitle}</h2>
|
||||||
|
<div class="grid grid-cols-1 gap-5">
|
||||||
|
{talks.map((talk) => (
|
||||||
|
<TalkCard
|
||||||
|
title={talk.data.title}
|
||||||
|
description={talk.data.description}
|
||||||
|
event={talk.data.event}
|
||||||
|
dateFormatted={talk.data.dateFormatted}
|
||||||
|
location={talk.data.location}
|
||||||
|
video={talk.data.video}
|
||||||
|
tags={talk.data.tags}
|
||||||
|
copresenters={talk.data.copresenters}
|
||||||
|
lang={locale}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
@ -87,22 +87,28 @@ const recommendationsCollection = defineCollection({
|
||||||
});
|
});
|
||||||
|
|
||||||
const talksCollection = 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({
|
schema: z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
date: z.date(),
|
date: z.date().optional(),
|
||||||
dateFormatted: z.string(),
|
event: z.string().optional(),
|
||||||
event: z.string(),
|
location: z.string().optional(),
|
||||||
location: z.string(),
|
copresenters: z.array(z.object({
|
||||||
|
name: z.string(),
|
||||||
|
url: z.string().url(),
|
||||||
|
})).optional(),
|
||||||
slides: z.string().url().optional(),
|
slides: z.string().url().optional(),
|
||||||
video: z.string().url().optional(),
|
video: z.string().url().optional(),
|
||||||
image: z.string().optional(),
|
image: z.string().optional(),
|
||||||
imageAlt: z.string().optional(),
|
imageAlt: z.string().optional(),
|
||||||
tags: z.array(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'),
|
lang: z.enum(['fr', 'en', 'ar']).default('fr'),
|
||||||
}),
|
}).transform((data) => ({
|
||||||
|
...data,
|
||||||
|
dateFormatted: data.date ? formatDate(data.date, data.lang) : undefined,
|
||||||
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
const photoBlogPostsCollection = defineCollection({
|
const photoBlogPostsCollection = defineCollection({
|
||||||
|
|
|
||||||
5
src/content/talks/react-academy-conf.ar.md
Normal file
5
src/content/talks/react-academy-conf.ar.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: "Redux-saga مقابل Redux-observable"
|
||||||
|
description: "مقارنة بين طريقتين لإدارة العمليات غير المتزامنة مع Redux: redux-saga و redux-observable."
|
||||||
|
lang: ar
|
||||||
|
---
|
||||||
5
src/content/talks/react-academy-conf.en.md
Normal file
5
src/content/talks/react-academy-conf.en.md
Normal file
|
|
@ -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
|
||||||
|
---
|
||||||
13
src/content/talks/react-academy-conf.md
Normal file
13
src/content/talks/react-academy-conf.md
Normal file
|
|
@ -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
|
||||||
|
---
|
||||||
|
|
@ -12,6 +12,7 @@ import MaltIcon from "../../../components/icons/MaltIcon.astro";
|
||||||
import StackOverflowIcon from "../../../components/icons/StackOverflowIcon.astro";
|
import StackOverflowIcon from "../../../components/icons/StackOverflowIcon.astro";
|
||||||
import GitHubIcon from "../../../components/icons/GitHubIcon.astro";
|
import GitHubIcon from "../../../components/icons/GitHubIcon.astro";
|
||||||
import ForgejoIcon from "../../../components/icons/ForgejoIcon.astro";
|
import ForgejoIcon from "../../../components/icons/ForgejoIcon.astro";
|
||||||
|
import TalksSection from "../../../components/code/TalksSection.astro";
|
||||||
import { getProjectBaseSlug, getProjectsBasePath } from "../../../utils/i18n";
|
import { getProjectBaseSlug, getProjectsBasePath } from "../../../utils/i18n";
|
||||||
import logoTiqa from "../../../assets/images/logo-tiqa-blanc.png";
|
import logoTiqa from "../../../assets/images/logo-tiqa-blanc.png";
|
||||||
|
|
||||||
|
|
@ -175,6 +176,8 @@ function formatMonth(dateStr: string) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<TalksSection locale={locale} />
|
||||||
|
|
||||||
<div class="mb-20">
|
<div class="mb-20">
|
||||||
<h2 class="text-2xl font-bold text-white mb-6">القيم والمنهج</h2>
|
<h2 class="text-2xl font-bold text-white mb-6">القيم والمنهج</h2>
|
||||||
<ul class="space-y-4 text-white/80">
|
<ul class="space-y-4 text-white/80">
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import MaltIcon from "../../components/icons/MaltIcon.astro";
|
||||||
import StackOverflowIcon from "../../components/icons/StackOverflowIcon.astro";
|
import StackOverflowIcon from "../../components/icons/StackOverflowIcon.astro";
|
||||||
import GitHubIcon from "../../components/icons/GitHubIcon.astro";
|
import GitHubIcon from "../../components/icons/GitHubIcon.astro";
|
||||||
import ForgejoIcon from "../../components/icons/ForgejoIcon.astro";
|
import ForgejoIcon from "../../components/icons/ForgejoIcon.astro";
|
||||||
|
import TalksSection from "../../components/code/TalksSection.astro";
|
||||||
import { getProjectBaseSlug, getProjectsBasePath } from "../../utils/i18n";
|
import { getProjectBaseSlug, getProjectsBasePath } from "../../utils/i18n";
|
||||||
import logoTiqa from "../../assets/images/logo-tiqa-blanc.png";
|
import logoTiqa from "../../assets/images/logo-tiqa-blanc.png";
|
||||||
|
|
||||||
|
|
@ -175,6 +176,8 @@ function formatMonth(dateStr: string) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<TalksSection locale={locale} />
|
||||||
|
|
||||||
<div class="mb-20">
|
<div class="mb-20">
|
||||||
<h2 class="text-2xl font-bold text-white mb-6">Valeurs & Approche</h2>
|
<h2 class="text-2xl font-bold text-white mb-6">Valeurs & Approche</h2>
|
||||||
<ul class="space-y-4 text-white/80">
|
<ul class="space-y-4 text-white/80">
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import MaltIcon from "../../../components/icons/MaltIcon.astro";
|
||||||
import StackOverflowIcon from "../../../components/icons/StackOverflowIcon.astro";
|
import StackOverflowIcon from "../../../components/icons/StackOverflowIcon.astro";
|
||||||
import GitHubIcon from "../../../components/icons/GitHubIcon.astro";
|
import GitHubIcon from "../../../components/icons/GitHubIcon.astro";
|
||||||
import ForgejoIcon from "../../../components/icons/ForgejoIcon.astro";
|
import ForgejoIcon from "../../../components/icons/ForgejoIcon.astro";
|
||||||
|
import TalksSection from "../../../components/code/TalksSection.astro";
|
||||||
import { getProjectBaseSlug, getProjectsBasePath } from "../../../utils/i18n";
|
import { getProjectBaseSlug, getProjectsBasePath } from "../../../utils/i18n";
|
||||||
import logoTiqa from "../../../assets/images/logo-tiqa-blanc.png";
|
import logoTiqa from "../../../assets/images/logo-tiqa-blanc.png";
|
||||||
|
|
||||||
|
|
@ -175,6 +176,8 @@ function formatMonth(dateStr: string) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<TalksSection locale={locale} />
|
||||||
|
|
||||||
<div class="mb-20">
|
<div class="mb-20">
|
||||||
<h2 class="text-2xl font-bold text-white mb-6">Values & Approach</h2>
|
<h2 class="text-2xl font-bold text-white mb-6">Values & Approach</h2>
|
||||||
<ul class="space-y-4 text-white/80">
|
<ul class="space-y-4 text-white/80">
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue