diff --git a/src/components/code/CareerTimeline.astro b/src/components/code/CareerTimeline.astro new file mode 100644 index 0000000..871f75b --- /dev/null +++ b/src/components/code/CareerTimeline.astro @@ -0,0 +1,141 @@ +--- +import { getCollection, render } from "astro:content"; +import { Image } from "astro:assets"; +import { t, getDateLocale, type Locale } from "../../utils/i18n"; +import "../../styles/exp-card.css"; + +interface Props { + locale: Locale; +} + +const { locale } = Astro.props; + +const logos = import.meta.glob<{ default: ImageMetadata }>('../../assets/images/companies/*.png', { eager: true }); + +function getLogo(filename?: string) { + if (!filename) return null; + const key = `../../assets/images/companies/${filename}`; + return logos[key]?.default ?? null; +} + +function getInitials(company: string) { + return company.split(/[\s-]+/).map(w => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase(); +} + +const typeColors: Record = { + employment: { badge: 'type-badge-blue', border: 'type-border-blue', dot: 'bg-blue-400 border-blue-400/50' }, + freelance: { badge: 'type-badge-amber', border: 'type-border-amber', dot: 'bg-amber-400 border-amber-400/50' }, + teaching: { badge: 'type-badge-emerald', border: 'type-border-emerald', dot: 'bg-emerald-400 border-emerald-400/50' }, + community: { badge: 'type-badge-pink', border: 'type-border-pink', dot: 'bg-pink-400 border-pink-400/50' }, + entrepreneurship: { badge: 'type-badge-purple', border: 'type-border-purple', dot: 'bg-purple-400 border-purple-400/50' }, +}; + +function formatMonth(dateStr: string) { + const [year, month] = dateStr.split('-'); + return new Date(parseInt(year), parseInt(month) - 1) + .toLocaleDateString(getDateLocale(locale), { year: 'numeric', month: 'short' }); +} + +function formatDuration(years: number, months: number) { + const yearLabel = years > 1 ? t('career', 'durationYears', locale) : t('career', 'durationYear', locale); + const monthLabel = t('career', 'durationMonth', locale); + if (years === 0) return `${months} ${monthLabel}`; + if (months === 0) return `${years} ${yearLabel}`; + return `${years} ${yearLabel} ${months} ${monthLabel}`; +} + +function computeDuration(startStr: string, endStr?: string) { + const [sy, sm] = startStr.split('-').map(Number); + const end = endStr ? endStr.split('-').map(Number) : [new Date().getFullYear(), new Date().getMonth() + 1]; + const totalMonths = Math.max(1, (end[0] - sy) * 12 + (end[1] - sm)); + const years = Math.floor(totalMonths / 12); + const months = totalMonths % 12; + return formatDuration(years, months); +} + +const experiences = (await getCollection("experiences")) + .filter((e) => e.data.lang === locale && !e.data.draft) + .sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1)); + +const presentLabel = t('career', 'present', locale); +const logoAltPrefix = t('career', 'logoAlt', locale); +--- + +
+ {experiences.map(async (exp) => { + const { Content } = await render(exp); + const isOngoing = !exp.data.endDate; + const start = formatMonth(exp.data.startDate); + const end = exp.data.endDate ? formatMonth(exp.data.endDate) : presentLabel; + const duration = computeDuration(exp.data.startDate, exp.data.endDate); + const colors = typeColors[exp.data.type] || typeColors.employment; + const label = t('career', exp.data.type, locale); + + return ( +
+
+ {isOngoing &&
} +
+ +
+ {getLogo(exp.data.logo) ? ( + + ) : ( +
+ {getInitials(exp.data.company)} +
+ )} + +
+ + {label} + + + {start} — {end} + + · {duration} + {exp.data.location && · {exp.data.location}} +
+ +

{exp.data.role}

+

+ {exp.data.companyUrl ? ( + {exp.data.company} + ) : exp.data.company} +

+ +
+ +
+ + {exp.data.technologies && exp.data.technologies.length > 0 && ( +
+ {exp.data.technologies.map((tech: string) => ( + + {tech} + + ))} +
+ )} +
+
+ ); + })} +
diff --git a/src/pages/ar/برمجة/مسار.astro b/src/pages/ar/برمجة/مسار.astro index 9922699..22dd508 100644 --- a/src/pages/ar/برمجة/مسار.astro +++ b/src/pages/ar/برمجة/مسار.astro @@ -1,56 +1,6 @@ --- -import { getCollection, render } from "astro:content"; -import { Image } from "astro:assets"; import Layout from "../../../layouts/main.astro"; -import "../../../styles/exp-card.css"; - -const logos = import.meta.glob<{ default: ImageMetadata }>('../../../assets/images/companies/*.png', { eager: true }); - -function getLogo(filename?: string) { - if (!filename) return null; - const key = `../../../assets/images/companies/${filename}`; - return logos[key]?.default ?? null; -} - -function getInitials(company: string) { - return company.split(/[\s-]+/).map(w => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase(); -} - -const locale = "ar"; - -const experiences = (await getCollection("experiences")) - .filter((e) => e.data.lang === locale && !e.data.draft) - .sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1)); - -const typeLabels: Record = { - employment: 'وظيفة', freelance: 'مستقل', teaching: 'تدريس', - community: 'مجتمع', entrepreneurship: 'ريادة أعمال', -}; - -const typeColors: Record = { - employment: { badge: 'type-badge-blue', border: 'type-border-blue', dot: 'bg-blue-400 border-blue-400/50' }, - freelance: { badge: 'type-badge-amber', border: 'type-border-amber', dot: 'bg-amber-400 border-amber-400/50' }, - teaching: { badge: 'type-badge-emerald', border: 'type-border-emerald', dot: 'bg-emerald-400 border-emerald-400/50' }, - community: { badge: 'type-badge-pink', border: 'type-border-pink', dot: 'bg-pink-400 border-pink-400/50' }, - entrepreneurship: { badge: 'type-badge-purple', border: 'type-border-purple', dot: 'bg-purple-400 border-purple-400/50' }, -}; - -function formatMonth(dateStr: string) { - const [year, month] = dateStr.split('-'); - return new Date(parseInt(year), parseInt(month) - 1) - .toLocaleDateString('ar-SA', { year: 'numeric', month: 'short' }); -} - -function computeDuration(startStr: string, endStr?: string) { - const [sy, sm] = startStr.split('-').map(Number); - const end = endStr ? endStr.split('-').map(Number) : [new Date().getFullYear(), new Date().getMonth() + 1]; - const totalMonths = (end[0] - sy) * 12 + (end[1] - sm); - const years = Math.floor(totalMonths / 12); - const months = totalMonths % 12; - if (years === 0) return `${months} أشهر`; - if (months === 0) return `${years} سنة`; - return `${years} سنة ${months} أشهر`; -} +import CareerTimeline from "../../../components/code/CareerTimeline.astro"; --- أكثر من 20 سنة من الخبرة في تطوير البرمجيات.

-
- {experiences.map(async (exp) => { - const { Content } = await render(exp); - const isOngoing = !exp.data.endDate; - const start = formatMonth(exp.data.startDate); - const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'الحاضر'; - const duration = computeDuration(exp.data.startDate, exp.data.endDate); - const colors = typeColors[exp.data.type] || typeColors.employment; - const label = typeLabels[exp.data.type] || exp.data.type; - - return ( -
-
- {isOngoing &&
} -
- -
- {getLogo(exp.data.logo) ? ( - - ) : ( -
- {getInitials(exp.data.company)} -
- )} - -
- - {label} - - - {start} — {end} - - · {duration} - {exp.data.location && · {exp.data.location}} -
- -

{exp.data.role}

-

- {exp.data.companyUrl ? ( - {exp.data.company} - ) : exp.data.company} -

- -
- -
- - {exp.data.technologies && exp.data.technologies.length > 0 && ( -
- {exp.data.technologies.map((tech: string) => ( - - {tech} - - ))} -
- )} -
-
- ); - })} -
+ diff --git a/src/pages/code/parcours.astro b/src/pages/code/parcours.astro index dc2531e..83c223b 100644 --- a/src/pages/code/parcours.astro +++ b/src/pages/code/parcours.astro @@ -1,56 +1,6 @@ --- -import { getCollection, render } from "astro:content"; -import { Image } from "astro:assets"; import Layout from "../../layouts/main.astro"; -import "../../styles/exp-card.css"; - -const logos = import.meta.glob<{ default: ImageMetadata }>('../../assets/images/companies/*.png', { eager: true }); - -function getLogo(filename?: string) { - if (!filename) return null; - const key = `../../assets/images/companies/${filename}`; - return logos[key]?.default ?? null; -} - -function getInitials(company: string) { - return company.split(/[\s-]+/).map(w => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase(); -} - -const locale = "fr"; - -const experiences = (await getCollection("experiences")) - .filter((e) => e.data.lang === locale && !e.data.draft) - .sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1)); - -const typeLabels: Record = { - employment: 'Emploi', freelance: 'Freelance', teaching: 'Enseignement', - community: 'Communauté', entrepreneurship: 'Entrepreneuriat', -}; - -const typeColors: Record = { - employment: { badge: 'type-badge-blue', border: 'type-border-blue', dot: 'bg-blue-400 border-blue-400/50' }, - freelance: { badge: 'type-badge-amber', border: 'type-border-amber', dot: 'bg-amber-400 border-amber-400/50' }, - teaching: { badge: 'type-badge-emerald', border: 'type-border-emerald', dot: 'bg-emerald-400 border-emerald-400/50' }, - community: { badge: 'type-badge-pink', border: 'type-border-pink', dot: 'bg-pink-400 border-pink-400/50' }, - entrepreneurship: { badge: 'type-badge-purple', border: 'type-border-purple', dot: 'bg-purple-400 border-purple-400/50' }, -}; - -function formatMonth(dateStr: string) { - const [year, month] = dateStr.split('-'); - return new Date(parseInt(year), parseInt(month) - 1) - .toLocaleDateString('fr-FR', { year: 'numeric', month: 'short' }); -} - -function computeDuration(startStr: string, endStr?: string) { - const [sy, sm] = startStr.split('-').map(Number); - const end = endStr ? endStr.split('-').map(Number) : [new Date().getFullYear(), new Date().getMonth() + 1]; - const totalMonths = (end[0] - sy) * 12 + (end[1] - sm); - const years = Math.floor(totalMonths / 12); - const months = totalMonths % 12; - if (years === 0) return `${months} mois`; - if (months === 0) return `${years} an${years > 1 ? 's' : ''}`; - return `${years} an${years > 1 ? 's' : ''} ${months} mois`; -} +import CareerTimeline from "../../components/code/CareerTimeline.astro"; --- Plus de 20 ans d'expérience en développement logiciel.

-
- {experiences.map(async (exp) => { - const { Content } = await render(exp); - const isOngoing = !exp.data.endDate; - const start = formatMonth(exp.data.startDate); - const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'Présent'; - const duration = computeDuration(exp.data.startDate, exp.data.endDate); - const colors = typeColors[exp.data.type] || typeColors.employment; - const label = typeLabels[exp.data.type] || exp.data.type; - - return ( -
-
- {isOngoing &&
} -
- -
- {getLogo(exp.data.logo) ? ( - - ) : ( -
- {getInitials(exp.data.company)} -
- )} - -
- - {label} - - - {start} — {end} - - · {duration} - {exp.data.location && · {exp.data.location}} -
- -

{exp.data.role}

-

- {exp.data.companyUrl ? ( - {exp.data.company} - ) : exp.data.company} -

- -
- -
- - {exp.data.technologies && exp.data.technologies.length > 0 && ( -
- {exp.data.technologies.map((tech: string) => ( - - {tech} - - ))} -
- )} -
-
- ); - })} -
+ diff --git a/src/pages/en/code/career.astro b/src/pages/en/code/career.astro index c1d0e63..667a4da 100644 --- a/src/pages/en/code/career.astro +++ b/src/pages/en/code/career.astro @@ -1,56 +1,6 @@ --- -import { getCollection, render } from "astro:content"; -import { Image } from "astro:assets"; import Layout from "../../../layouts/main.astro"; -import "../../../styles/exp-card.css"; - -const logos = import.meta.glob<{ default: ImageMetadata }>('../../../assets/images/companies/*.png', { eager: true }); - -function getLogo(filename?: string) { - if (!filename) return null; - const key = `../../../assets/images/companies/${filename}`; - return logos[key]?.default ?? null; -} - -function getInitials(company: string) { - return company.split(/[\s-]+/).map(w => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase(); -} - -const locale = "en"; - -const experiences = (await getCollection("experiences")) - .filter((e) => e.data.lang === locale && !e.data.draft) - .sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1)); - -const typeLabels: Record = { - employment: 'Employment', freelance: 'Freelance', teaching: 'Teaching', - community: 'Community', entrepreneurship: 'Entrepreneurship', -}; - -const typeColors: Record = { - employment: { badge: 'type-badge-blue', border: 'type-border-blue', dot: 'bg-blue-400 border-blue-400/50' }, - freelance: { badge: 'type-badge-amber', border: 'type-border-amber', dot: 'bg-amber-400 border-amber-400/50' }, - teaching: { badge: 'type-badge-emerald', border: 'type-border-emerald', dot: 'bg-emerald-400 border-emerald-400/50' }, - community: { badge: 'type-badge-pink', border: 'type-border-pink', dot: 'bg-pink-400 border-pink-400/50' }, - entrepreneurship: { badge: 'type-badge-purple', border: 'type-border-purple', dot: 'bg-purple-400 border-purple-400/50' }, -}; - -function formatMonth(dateStr: string) { - const [year, month] = dateStr.split('-'); - return new Date(parseInt(year), parseInt(month) - 1) - .toLocaleDateString('en-US', { year: 'numeric', month: 'short' }); -} - -function computeDuration(startStr: string, endStr?: string) { - const [sy, sm] = startStr.split('-').map(Number); - const end = endStr ? endStr.split('-').map(Number) : [new Date().getFullYear(), new Date().getMonth() + 1]; - const totalMonths = (end[0] - sy) * 12 + (end[1] - sm); - const years = Math.floor(totalMonths / 12); - const months = totalMonths % 12; - if (years === 0) return `${months} mo`; - if (months === 0) return `${years} yr${years > 1 ? 's' : ''}`; - return `${years} yr${years > 1 ? 's' : ''} ${months} mo`; -} +import CareerTimeline from "../../../components/code/CareerTimeline.astro"; --- Over 20 years of experience in software development.

-
- {experiences.map(async (exp) => { - const { Content } = await render(exp); - const isOngoing = !exp.data.endDate; - const start = formatMonth(exp.data.startDate); - const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'Present'; - const duration = computeDuration(exp.data.startDate, exp.data.endDate); - const colors = typeColors[exp.data.type] || typeColors.employment; - const label = typeLabels[exp.data.type] || exp.data.type; - - return ( -
-
- {isOngoing &&
} -
- -
- {getLogo(exp.data.logo) ? ( - - ) : ( -
- {getInitials(exp.data.company)} -
- )} - -
- - {label} - - - {start} — {end} - - · {duration} - {exp.data.location && · {exp.data.location}} -
- -

{exp.data.role}

-

- {exp.data.companyUrl ? ( - {exp.data.company} - ) : exp.data.company} -

- -
- -
- - {exp.data.technologies && exp.data.technologies.length > 0 && ( -
- {exp.data.technologies.map((tech: string) => ( - - {tech} - - ))} -
- )} -
-
- ); - })} -
+ diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts index 9de4640..3532022 100644 --- a/src/utils/i18n.ts +++ b/src/utils/i18n.ts @@ -93,6 +93,18 @@ export const translations = { nextImage: { fr: 'Image suivante', en: 'Next image', ar: 'الصورة التالية' }, goToImage: { fr: "Aller à l'image", en: 'Go to image', ar: 'انتقل إلى الصورة' }, }, + career: { + present: { fr: 'Présent', en: 'Present', ar: 'الحاضر' }, + logoAlt: { fr: 'Logo', en: 'Logo', ar: 'شعار' }, + employment: { fr: 'Emploi', en: 'Employment', ar: 'وظيفة' }, + freelance: { fr: 'Freelance', en: 'Freelance', ar: 'مستقل' }, + teaching: { fr: 'Enseignement', en: 'Teaching', ar: 'تدريس' }, + community: { fr: 'Communauté', en: 'Community', ar: 'مجتمع' }, + entrepreneurship: { fr: 'Entrepreneuriat', en: 'Entrepreneurship', ar: 'ريادة أعمال' }, + durationYear: { fr: 'an', en: 'yr', ar: 'سنة' }, + durationYears: { fr: 'ans', en: 'yrs', ar: 'سنة' }, + durationMonth: { fr: 'mois', en: 'mo', ar: 'أشهر' }, + }, projects: { visitSite: { fr: 'Voir le site', en: 'Visit site', ar: 'زيارة الموقع' }, viewOnGithub: { fr: 'Voir sur GitHub', en: 'View on GitHub', ar: 'عرض على GitHub' },