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({
|
||||
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({
|
||||
|
|
|
|||
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 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) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<TalksSection locale={locale} />
|
||||
|
||||
<div class="mb-20">
|
||||
<h2 class="text-2xl font-bold text-white mb-6">القيم والمنهج</h2>
|
||||
<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 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) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<TalksSection locale={locale} />
|
||||
|
||||
<div class="mb-20">
|
||||
<h2 class="text-2xl font-bold text-white mb-6">Valeurs & Approche</h2>
|
||||
<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 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) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<TalksSection locale={locale} />
|
||||
|
||||
<div class="mb-20">
|
||||
<h2 class="text-2xl font-bold text-white mb-6">Values & Approach</h2>
|
||||
<ul class="space-y-4 text-white/80">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue