Pages de détail individuelles pour les projets (FR, EN, AR)
Les cartes projet sont désormais cliquables et mènent vers une page de détail qui affiche le contenu markdown riche (historique, architecture, impact) avec les technologies, liens externes et hreflang.
This commit is contained in:
parent
6f13800716
commit
578c6733db
13 changed files with 257 additions and 55 deletions
|
|
@ -1,18 +1,23 @@
|
|||
---
|
||||
import { t, type Locale } from '../../utils/i18n';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
technologies?: string[];
|
||||
url?: string;
|
||||
github?: string;
|
||||
featured?: boolean;
|
||||
detailUrl: string;
|
||||
lang?: Locale;
|
||||
}
|
||||
|
||||
const { title, description, technologies, url, github, featured = false } = Astro.props;
|
||||
const { title, description, technologies, featured = false, detailUrl, lang = 'fr' } = Astro.props;
|
||||
|
||||
const isRtl = lang === 'ar';
|
||||
const readMoreLabel = t('projects', 'readMore', lang);
|
||||
---
|
||||
|
||||
<div class:list={[
|
||||
"facet-card rounded-2xl border p-6 flex flex-col h-full transition-all duration-300 hover:scale-[1.02]",
|
||||
<a href={detailUrl} class:list={[
|
||||
"facet-card rounded-2xl border p-6 flex flex-col h-full transition-all duration-300 hover:scale-[1.02] no-underline",
|
||||
featured
|
||||
? "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]",
|
||||
|
|
@ -37,16 +42,11 @@ const { title, description, technologies, url, github, featured = false } = Astr
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div class="flex gap-4">
|
||||
{url && (
|
||||
<a href={url} target="_blank" rel="noopener noreferrer" class="text-sm font-medium text-purple-200 hover:text-white transition-colors">
|
||||
Voir le site →
|
||||
</a>
|
||||
<span class="text-sm font-medium text-purple-200 group-hover:text-white transition-colors">
|
||||
{isRtl ? (
|
||||
<>{readMoreLabel} ←</>
|
||||
) : (
|
||||
<>{readMoreLabel} →</>
|
||||
)}
|
||||
{github && (
|
||||
<a href={github} target="_blank" rel="noopener noreferrer" class="text-sm font-medium text-white/50 hover:text-white transition-colors">
|
||||
GitHub →
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</a>
|
||||
|
|
|
|||
104
src/components/code/ProjectDetailContent.astro
Normal file
104
src/components/code/ProjectDetailContent.astro
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
---
|
||||
import Layout from '../../layouts/main.astro';
|
||||
import { render } from 'astro:content';
|
||||
import { t, getProjectsBasePath, getDateLocale, type Locale } from '../../utils/i18n';
|
||||
|
||||
interface Props {
|
||||
project: any;
|
||||
lang: Locale;
|
||||
}
|
||||
|
||||
const { project, lang } = Astro.props;
|
||||
const { Content } = await render(project);
|
||||
|
||||
const projectsPath = getProjectsBasePath(lang);
|
||||
const isRtl = lang === 'ar';
|
||||
const year = project.data.date.getFullYear();
|
||||
---
|
||||
|
||||
<Layout
|
||||
title={`${project.data.title} - Jalil Arfaoui`}
|
||||
facet="code"
|
||||
description={project.data.description}
|
||||
>
|
||||
<section
|
||||
dir={isRtl ? 'rtl' : undefined}
|
||||
lang={isRtl ? 'ar' : undefined}
|
||||
class="relative z-20 max-w-3xl mx-auto my-12 px-7 lg:px-0"
|
||||
>
|
||||
<a
|
||||
href={projectsPath}
|
||||
class="inline-flex items-center gap-1.5 text-sm text-white/50 hover:text-white/80 transition-colors mb-8 group"
|
||||
>
|
||||
{isRtl ? (
|
||||
<>
|
||||
{t('projects', 'backToProjects', lang)}
|
||||
<svg class="w-4 h-4 group-hover:translate-x-0.5 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg class="w-4 h-4 group-hover:-translate-x-0.5 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
{t('projects', 'backToProjects', lang)}
|
||||
</>
|
||||
)}
|
||||
</a>
|
||||
|
||||
<header class="mb-10">
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-white mb-3">{project.data.title}</h1>
|
||||
<p class="text-lg text-white/60 leading-relaxed mb-5">{project.data.description}</p>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-x-5 gap-y-2 text-sm text-white/45 mb-5">
|
||||
<span>{t('projects', 'startedIn', lang)} {year}</span>
|
||||
</div>
|
||||
|
||||
{project.data.technologies && project.data.technologies.length > 0 && (
|
||||
<div class="flex flex-wrap gap-1.5 mb-5">
|
||||
{project.data.technologies.map((tech: string) => (
|
||||
<span class="inline-block px-2.5 py-1 text-xs rounded-full bg-white/[0.08] text-white/60 border border-white/[0.08]">
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="flex flex-wrap gap-4">
|
||||
{project.data.url && (
|
||||
<a
|
||||
href={project.data.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-1.5 text-sm font-medium text-purple-200 hover:text-white transition-colors"
|
||||
>
|
||||
{t('projects', 'visitSite', lang)}
|
||||
<svg class="w-3.5 h-3.5" 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>
|
||||
)}
|
||||
{project.data.github && (
|
||||
<a
|
||||
href={project.data.github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-1.5 text-sm font-medium text-white/50 hover:text-white transition-colors"
|
||||
>
|
||||
{t('projects', 'viewOnGithub', lang)}
|
||||
<svg class="w-3.5 h-3.5" 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>
|
||||
</header>
|
||||
|
||||
{project.body && (
|
||||
<div class="prose prose-invert prose-purple max-w-none">
|
||||
<Content />
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</Layout>
|
||||
|
|
@ -6,10 +6,12 @@ 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 { getProjectBaseSlug, getProjectsBasePath } from "../../../utils/i18n";
|
||||
import logoTiqa from "../../../assets/images/logo-tiqa-blanc.png";
|
||||
import skillsData from "../../../data/skills.json";
|
||||
|
||||
const locale = "ar";
|
||||
const projectsBasePath = getProjectsBasePath(locale);
|
||||
|
||||
const experiences = (await getCollection("experiences"))
|
||||
.filter((e) => e.data.lang === locale && !e.data.draft)
|
||||
|
|
@ -103,9 +105,9 @@ function formatMonth(dateStr: string) {
|
|||
title={project.data.title}
|
||||
description={project.data.description}
|
||||
technologies={project.data.technologies}
|
||||
url={project.data.url}
|
||||
github={project.data.github}
|
||||
featured={project.data.featured}
|
||||
detailUrl={`${projectsBasePath}/${getProjectBaseSlug(project.id)}`}
|
||||
lang={locale}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
import { getCollection } from "astro:content";
|
||||
import Layout from "../../../layouts/main.astro";
|
||||
import ProjectCard from "../../../components/code/ProjectCard.astro";
|
||||
import { getProjectBaseSlug, getProjectsBasePath } from "../../../utils/i18n";
|
||||
|
||||
const locale = "ar";
|
||||
const projectsBasePath = getProjectsBasePath(locale);
|
||||
|
||||
const projects = (await getCollection("projects"))
|
||||
.filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev")
|
||||
|
|
@ -36,9 +38,9 @@ const projects = (await getCollection("projects"))
|
|||
title={project.data.title}
|
||||
description={project.data.description}
|
||||
technologies={project.data.technologies}
|
||||
url={project.data.url}
|
||||
github={project.data.github}
|
||||
featured={project.data.featured}
|
||||
detailUrl={`${projectsBasePath}/${getProjectBaseSlug(project.id)}`}
|
||||
lang={locale}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
18
src/pages/ar/برمجة/مشاريع/[slug].astro
Normal file
18
src/pages/ar/برمجة/مشاريع/[slug].astro
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
import ProjectDetailContent from '../../../../components/code/ProjectDetailContent.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getProjectBaseSlug } from '../../../../utils/i18n';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const allProjects = await getCollection('projects');
|
||||
const arProjects = allProjects.filter(p => p.data.lang === 'ar' && !p.data.draft && p.data.category === 'dev');
|
||||
return arProjects.map(project => ({
|
||||
params: { slug: getProjectBaseSlug(project.id) },
|
||||
props: { project },
|
||||
}));
|
||||
}
|
||||
|
||||
const { project } = Astro.props;
|
||||
---
|
||||
|
||||
<ProjectDetailContent project={project} lang="ar" />
|
||||
|
|
@ -5,9 +5,11 @@ 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 { getProjectBaseSlug, getProjectsBasePath } from "../../utils/i18n";
|
||||
import logoTiqa from "../../assets/images/logo-tiqa-blanc.png";
|
||||
|
||||
const locale = "fr";
|
||||
const projectsBasePath = getProjectsBasePath(locale);
|
||||
|
||||
const experiences = (await getCollection("experiences"))
|
||||
.filter((e) => e.data.lang === locale && !e.data.draft)
|
||||
|
|
@ -99,9 +101,9 @@ function formatMonth(dateStr: string) {
|
|||
title={project.data.title}
|
||||
description={project.data.description}
|
||||
technologies={project.data.technologies}
|
||||
url={project.data.url}
|
||||
github={project.data.github}
|
||||
featured={project.data.featured}
|
||||
detailUrl={`${projectsBasePath}/${getProjectBaseSlug(project.id)}`}
|
||||
lang={locale}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
import { getCollection } from "astro:content";
|
||||
import Layout from "../../layouts/main.astro";
|
||||
import ProjectCard from "../../components/code/ProjectCard.astro";
|
||||
import { getProjectBaseSlug, getProjectsBasePath } from "../../utils/i18n";
|
||||
|
||||
const locale = "fr";
|
||||
const projectsBasePath = getProjectsBasePath(locale);
|
||||
|
||||
const projects = (await getCollection("projects"))
|
||||
.filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev")
|
||||
|
|
@ -36,9 +38,9 @@ const projects = (await getCollection("projects"))
|
|||
title={project.data.title}
|
||||
description={project.data.description}
|
||||
technologies={project.data.technologies}
|
||||
url={project.data.url}
|
||||
github={project.data.github}
|
||||
featured={project.data.featured}
|
||||
detailUrl={`${projectsBasePath}/${getProjectBaseSlug(project.id)}`}
|
||||
lang={locale}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
18
src/pages/code/projets/[slug].astro
Normal file
18
src/pages/code/projets/[slug].astro
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
import ProjectDetailContent from '../../../components/code/ProjectDetailContent.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getProjectBaseSlug } from '../../../utils/i18n';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const allProjects = await getCollection('projects');
|
||||
const frProjects = allProjects.filter(p => p.data.lang === 'fr' && !p.data.draft && p.data.category === 'dev');
|
||||
return frProjects.map(project => ({
|
||||
params: { slug: getProjectBaseSlug(project.id) },
|
||||
props: { project },
|
||||
}));
|
||||
}
|
||||
|
||||
const { project } = Astro.props;
|
||||
---
|
||||
|
||||
<ProjectDetailContent project={project} lang="fr" />
|
||||
|
|
@ -6,10 +6,12 @@ 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 { getProjectBaseSlug, getProjectsBasePath } from "../../../utils/i18n";
|
||||
import logoTiqa from "../../../assets/images/logo-tiqa-blanc.png";
|
||||
import skillsData from "../../../data/skills.json";
|
||||
|
||||
const locale = "en";
|
||||
const projectsBasePath = getProjectsBasePath(locale);
|
||||
|
||||
const experiences = (await getCollection("experiences"))
|
||||
.filter((e) => e.data.lang === locale && !e.data.draft)
|
||||
|
|
@ -103,9 +105,9 @@ function formatMonth(dateStr: string) {
|
|||
title={project.data.title}
|
||||
description={project.data.description}
|
||||
technologies={project.data.technologies}
|
||||
url={project.data.url}
|
||||
github={project.data.github}
|
||||
featured={project.data.featured}
|
||||
detailUrl={`${projectsBasePath}/${getProjectBaseSlug(project.id)}`}
|
||||
lang={locale}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
import { getCollection } from "astro:content";
|
||||
import Layout from "../../../layouts/main.astro";
|
||||
import ProjectCard from "../../../components/code/ProjectCard.astro";
|
||||
import { getProjectBaseSlug, getProjectsBasePath } from "../../../utils/i18n";
|
||||
|
||||
const locale = "en";
|
||||
const projectsBasePath = getProjectsBasePath(locale);
|
||||
|
||||
const projects = (await getCollection("projects"))
|
||||
.filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev")
|
||||
|
|
@ -36,9 +38,9 @@ const projects = (await getCollection("projects"))
|
|||
title={project.data.title}
|
||||
description={project.data.description}
|
||||
technologies={project.data.technologies}
|
||||
url={project.data.url}
|
||||
github={project.data.github}
|
||||
featured={project.data.featured}
|
||||
detailUrl={`${projectsBasePath}/${getProjectBaseSlug(project.id)}`}
|
||||
lang={locale}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
18
src/pages/en/code/projects/[slug].astro
Normal file
18
src/pages/en/code/projects/[slug].astro
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
import ProjectDetailContent from '../../../../components/code/ProjectDetailContent.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getProjectBaseSlug } from '../../../../utils/i18n';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const allProjects = await getCollection('projects');
|
||||
const enProjects = allProjects.filter(p => p.data.lang === 'en' && !p.data.draft && p.data.category === 'dev');
|
||||
return enProjects.map(project => ({
|
||||
params: { slug: getProjectBaseSlug(project.id) },
|
||||
props: { project },
|
||||
}));
|
||||
}
|
||||
|
||||
const { project } = Astro.props;
|
||||
---
|
||||
|
||||
<ProjectDetailContent project={project} lang="en" />
|
||||
|
|
@ -93,6 +93,14 @@ export const translations = {
|
|||
nextImage: { fr: 'Image suivante', en: 'Next image', ar: 'الصورة التالية' },
|
||||
goToImage: { fr: "Aller à l'image", en: 'Go to image', ar: 'انتقل إلى الصورة' },
|
||||
},
|
||||
projects: {
|
||||
visitSite: { fr: 'Voir le site', en: 'Visit site', ar: 'زيارة الموقع' },
|
||||
viewOnGithub: { fr: 'Voir sur GitHub', en: 'View on GitHub', ar: 'عرض على GitHub' },
|
||||
backToProjects: { fr: 'Projets', en: 'Projects', ar: 'المشاريع' },
|
||||
technologies: { fr: 'Technologies', en: 'Technologies', ar: 'التقنيات' },
|
||||
startedIn: { fr: 'Démarré en', en: 'Started in', ar: 'بدأ في' },
|
||||
readMore: { fr: 'Lire plus', en: 'Read more', ar: 'اقرأ المزيد' },
|
||||
},
|
||||
pages: {
|
||||
home: {
|
||||
title: { fr: 'Jalil Arfaoui', en: 'Jalil Arfaoui', ar: 'جليل عرفاوي' },
|
||||
|
|
@ -216,3 +224,15 @@ export function getHomePath(locale: Locale): string {
|
|||
export function getPostBaseSlug(postId: string): string {
|
||||
return postId.replace(/^\d{4}\//, '').replace(/\.(en|ar)(\.md)?$/, '');
|
||||
}
|
||||
|
||||
/** Slug de base d'un projet depuis son id (ex: "debats.en" → "debats") */
|
||||
export function getProjectBaseSlug(projectId: string): string {
|
||||
return projectId.replace(/\.(en|ar)$/, '');
|
||||
}
|
||||
|
||||
/** Chemin de base des projets selon la langue */
|
||||
export function getProjectsBasePath(locale: Locale): string {
|
||||
if (locale === 'ar') return '/ar/برمجة/مشاريع';
|
||||
if (locale === 'en') return '/en/code/projects';
|
||||
return '/code/projets';
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Locale } from "./i18n";
|
||||
import { getPhotoAlbumsPath, getPhotoBlogPath } from "./i18n";
|
||||
import { getPhotoAlbumsPath, getPhotoBlogPath, getProjectsBasePath } from "./i18n";
|
||||
|
||||
/**
|
||||
* Groupes de pages traduites.
|
||||
|
|
@ -57,6 +57,18 @@ const dynamicPatterns: DynamicPattern[] = [
|
|||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
// Projects: /code/projets/{slug}, /en/code/projects/{slug}, /ar/برمجة/مشاريع/{slug}
|
||||
regex: /^(?:\/code\/projets|\/en\/code\/projects|\/ar\/برمجة\/مشاريع)\/([^/]+)$/,
|
||||
buildUrls: (match) => {
|
||||
const slug = match[1];
|
||||
return {
|
||||
fr: `${getProjectsBasePath("fr")}/${slug}`,
|
||||
en: `${getProjectsBasePath("en")}/${slug}`,
|
||||
ar: `${getProjectsBasePath("ar")}/${slug}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function matchDynamicPattern(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue