Extraction composant CareerTimeline : suppression de la duplication 3x entre pages FR/EN/AR, traductions centralisées dans i18n.ts
This commit is contained in:
parent
5b887c7926
commit
fca4608beb
5 changed files with 159 additions and 387 deletions
141
src/components/code/CareerTimeline.astro
Normal file
141
src/components/code/CareerTimeline.astro
Normal file
|
|
@ -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<string, { badge: string; border: string; dot: string }> = {
|
||||
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);
|
||||
---
|
||||
|
||||
<div class="mt-10">
|
||||
{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 (
|
||||
<div class="relative ps-10 pb-10 border-s-2 border-white/[0.12] last:pb-0">
|
||||
<div
|
||||
class:list={[
|
||||
"absolute -start-[7px] top-2 w-3 h-3 rounded-full border-2",
|
||||
isOngoing ? colors.dot : "border-white/20 bg-white/10"
|
||||
]}
|
||||
>
|
||||
{isOngoing && <div class="absolute inset-0 rounded-full bg-current opacity-40 animate-ping" />}
|
||||
</div>
|
||||
|
||||
<div class:list={[
|
||||
"code-card exp-card relative rounded-2xl p-6 transition-colors duration-200 border-s-[3px]",
|
||||
isOngoing ? "exp-card--ongoing" : "",
|
||||
colors.border
|
||||
]}>
|
||||
{getLogo(exp.data.logo) ? (
|
||||
<div class="exp-logo absolute top-5 end-5 w-16 h-16 rounded-xl flex items-center justify-center p-2">
|
||||
<Image
|
||||
src={getLogo(exp.data.logo)!}
|
||||
alt={`${logoAltPrefix} ${exp.data.company}`}
|
||||
width={52}
|
||||
height={52}
|
||||
class="rounded-lg object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div class="exp-logo-fallback absolute top-5 end-5 w-16 h-16 rounded-xl flex items-center justify-center">
|
||||
<span class="text-base font-bold">{getInitials(exp.data.company)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="mb-3 flex items-center gap-2 flex-wrap pe-20">
|
||||
<span class:list={["inline-block px-2 py-0.5 text-[11px] font-medium rounded-full border", colors.badge]}>
|
||||
{label}
|
||||
</span>
|
||||
<span class="exp-meta text-xs">
|
||||
{start} — {end}
|
||||
</span>
|
||||
<span class="exp-meta text-xs">· {duration}</span>
|
||||
{exp.data.location && <span class="exp-meta text-xs">· {exp.data.location}</span>}
|
||||
</div>
|
||||
|
||||
<h3 class="text-xl font-bold pe-20">{exp.data.role}</h3>
|
||||
<p class="exp-company text-sm font-medium mt-1 mb-3">
|
||||
{exp.data.companyUrl ? (
|
||||
<a href={exp.data.companyUrl} target="_blank" rel="noopener noreferrer" class="transition-colors">{exp.data.company}</a>
|
||||
) : exp.data.company}
|
||||
</p>
|
||||
|
||||
<div class="exp-content text-sm leading-relaxed mb-4 prose prose-sm max-w-none">
|
||||
<Content />
|
||||
</div>
|
||||
|
||||
{exp.data.technologies && exp.data.technologies.length > 0 && (
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{exp.data.technologies.map((tech: string) => (
|
||||
<span class="exp-tech inline-block px-2.5 py-0.5 text-xs rounded-full border">
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -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<string, string> = {
|
||||
employment: 'وظيفة', freelance: 'مستقل', teaching: 'تدريس',
|
||||
community: 'مجتمع', entrepreneurship: 'ريادة أعمال',
|
||||
};
|
||||
|
||||
const typeColors: Record<string, { badge: string; border: string; dot: string }> = {
|
||||
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";
|
||||
---
|
||||
|
||||
<Layout
|
||||
|
|
@ -70,83 +20,6 @@ function computeDuration(startStr: string, endStr?: string) {
|
|||
<p class="mt-3 text-white/60 text-lg">أكثر من 20 سنة من الخبرة في تطوير البرمجيات.</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-10">
|
||||
{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 (
|
||||
<div class="relative pr-10 pb-10 border-r-2 border-white/[0.12] last:pb-0">
|
||||
<div
|
||||
class:list={[
|
||||
"absolute -right-[7px] top-2 w-3 h-3 rounded-full border-2",
|
||||
isOngoing ? colors.dot : "border-white/20 bg-white/10"
|
||||
]}
|
||||
>
|
||||
{isOngoing && <div class="absolute inset-0 rounded-full bg-current opacity-40 animate-ping" />}
|
||||
</div>
|
||||
|
||||
<div class:list={[
|
||||
"code-card exp-card relative rounded-2xl p-6 transition-colors duration-200 border-s-[3px]",
|
||||
isOngoing ? "exp-card--ongoing" : "",
|
||||
colors.border
|
||||
]}>
|
||||
{getLogo(exp.data.logo) ? (
|
||||
<div class="exp-logo absolute top-5 end-5 w-16 h-16 rounded-xl flex items-center justify-center p-2">
|
||||
<Image
|
||||
src={getLogo(exp.data.logo)!}
|
||||
alt={`شعار ${exp.data.company}`}
|
||||
width={52}
|
||||
height={52}
|
||||
class="rounded-lg object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div class="exp-logo-fallback absolute top-5 end-5 w-16 h-16 rounded-xl flex items-center justify-center">
|
||||
<span class="text-base font-bold">{getInitials(exp.data.company)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="mb-3 flex items-center gap-2 flex-wrap pe-20">
|
||||
<span class:list={["inline-block px-2 py-0.5 text-[11px] font-medium rounded-full border", colors.badge]}>
|
||||
{label}
|
||||
</span>
|
||||
<span class="exp-meta text-xs">
|
||||
{start} — {end}
|
||||
</span>
|
||||
<span class="exp-meta text-xs">· {duration}</span>
|
||||
{exp.data.location && <span class="exp-meta text-xs">· {exp.data.location}</span>}
|
||||
</div>
|
||||
|
||||
<h3 class="text-xl font-bold pe-20">{exp.data.role}</h3>
|
||||
<p class="exp-company text-sm font-medium mt-1 mb-3">
|
||||
{exp.data.companyUrl ? (
|
||||
<a href={exp.data.companyUrl} target="_blank" rel="noopener noreferrer" class="transition-colors">{exp.data.company}</a>
|
||||
) : exp.data.company}
|
||||
</p>
|
||||
|
||||
<div class="exp-content text-sm leading-relaxed mb-4 prose prose-sm max-w-none">
|
||||
<Content />
|
||||
</div>
|
||||
|
||||
{exp.data.technologies && exp.data.technologies.length > 0 && (
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{exp.data.technologies.map((tech: string) => (
|
||||
<span class="exp-tech inline-block px-2.5 py-0.5 text-xs rounded-full border">
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<CareerTimeline locale="ar" />
|
||||
</section>
|
||||
</Layout>
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
||||
employment: 'Emploi', freelance: 'Freelance', teaching: 'Enseignement',
|
||||
community: 'Communauté', entrepreneurship: 'Entrepreneuriat',
|
||||
};
|
||||
|
||||
const typeColors: Record<string, { badge: string; border: string; dot: string }> = {
|
||||
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";
|
||||
---
|
||||
|
||||
<Layout
|
||||
|
|
@ -70,83 +20,6 @@ function computeDuration(startStr: string, endStr?: string) {
|
|||
<p class="mt-3 text-white/60 text-lg">Plus de 20 ans d'expérience en développement logiciel.</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-10">
|
||||
{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 (
|
||||
<div class="relative pl-10 pb-10 border-l-2 border-white/[0.12] last:pb-0">
|
||||
<div
|
||||
class:list={[
|
||||
"absolute -left-[7px] top-2 w-3 h-3 rounded-full border-2",
|
||||
isOngoing ? colors.dot : "border-white/20 bg-white/10"
|
||||
]}
|
||||
>
|
||||
{isOngoing && <div class="absolute inset-0 rounded-full bg-current opacity-40 animate-ping" />}
|
||||
</div>
|
||||
|
||||
<div class:list={[
|
||||
"code-card exp-card relative rounded-2xl p-6 transition-colors duration-200 border-s-[3px]",
|
||||
isOngoing ? "exp-card--ongoing" : "",
|
||||
colors.border
|
||||
]}>
|
||||
{getLogo(exp.data.logo) ? (
|
||||
<div class="exp-logo absolute top-5 end-5 w-16 h-16 rounded-xl flex items-center justify-center p-2">
|
||||
<Image
|
||||
src={getLogo(exp.data.logo)!}
|
||||
alt={`Logo ${exp.data.company}`}
|
||||
width={52}
|
||||
height={52}
|
||||
class="rounded-lg object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div class="exp-logo-fallback absolute top-5 end-5 w-16 h-16 rounded-xl flex items-center justify-center">
|
||||
<span class="text-base font-bold">{getInitials(exp.data.company)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="mb-3 flex items-center gap-2 flex-wrap pe-20">
|
||||
<span class:list={["inline-block px-2 py-0.5 text-[11px] font-medium rounded-full border", colors.badge]}>
|
||||
{label}
|
||||
</span>
|
||||
<span class="exp-meta text-xs">
|
||||
{start} — {end}
|
||||
</span>
|
||||
<span class="exp-meta text-xs">· {duration}</span>
|
||||
{exp.data.location && <span class="exp-meta text-xs">· {exp.data.location}</span>}
|
||||
</div>
|
||||
|
||||
<h3 class="text-xl font-bold pe-20">{exp.data.role}</h3>
|
||||
<p class="exp-company text-sm font-medium mt-1 mb-3">
|
||||
{exp.data.companyUrl ? (
|
||||
<a href={exp.data.companyUrl} target="_blank" rel="noopener noreferrer" class="transition-colors">{exp.data.company}</a>
|
||||
) : exp.data.company}
|
||||
</p>
|
||||
|
||||
<div class="exp-content text-sm leading-relaxed mb-4 prose prose-sm max-w-none">
|
||||
<Content />
|
||||
</div>
|
||||
|
||||
{exp.data.technologies && exp.data.technologies.length > 0 && (
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{exp.data.technologies.map((tech: string) => (
|
||||
<span class="exp-tech inline-block px-2.5 py-0.5 text-xs rounded-full border">
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<CareerTimeline locale="fr" />
|
||||
</section>
|
||||
</Layout>
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
||||
employment: 'Employment', freelance: 'Freelance', teaching: 'Teaching',
|
||||
community: 'Community', entrepreneurship: 'Entrepreneurship',
|
||||
};
|
||||
|
||||
const typeColors: Record<string, { badge: string; border: string; dot: string }> = {
|
||||
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";
|
||||
---
|
||||
|
||||
<Layout
|
||||
|
|
@ -70,83 +20,6 @@ function computeDuration(startStr: string, endStr?: string) {
|
|||
<p class="mt-3 text-white/60 text-lg">Over 20 years of experience in software development.</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-10">
|
||||
{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 (
|
||||
<div class="relative pl-10 pb-10 border-l-2 border-white/[0.12] last:pb-0">
|
||||
<div
|
||||
class:list={[
|
||||
"absolute -left-[7px] top-2 w-3 h-3 rounded-full border-2",
|
||||
isOngoing ? colors.dot : "border-white/20 bg-white/10"
|
||||
]}
|
||||
>
|
||||
{isOngoing && <div class="absolute inset-0 rounded-full bg-current opacity-40 animate-ping" />}
|
||||
</div>
|
||||
|
||||
<div class:list={[
|
||||
"code-card exp-card relative rounded-2xl p-6 transition-colors duration-200 border-s-[3px]",
|
||||
isOngoing ? "exp-card--ongoing" : "",
|
||||
colors.border
|
||||
]}>
|
||||
{getLogo(exp.data.logo) ? (
|
||||
<div class="exp-logo absolute top-5 end-5 w-16 h-16 rounded-xl flex items-center justify-center p-2">
|
||||
<Image
|
||||
src={getLogo(exp.data.logo)!}
|
||||
alt={`${exp.data.company} logo`}
|
||||
width={52}
|
||||
height={52}
|
||||
class="rounded-lg object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div class="exp-logo-fallback absolute top-5 end-5 w-16 h-16 rounded-xl flex items-center justify-center">
|
||||
<span class="text-base font-bold">{getInitials(exp.data.company)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="mb-3 flex items-center gap-2 flex-wrap pe-20">
|
||||
<span class:list={["inline-block px-2 py-0.5 text-[11px] font-medium rounded-full border", colors.badge]}>
|
||||
{label}
|
||||
</span>
|
||||
<span class="exp-meta text-xs">
|
||||
{start} — {end}
|
||||
</span>
|
||||
<span class="exp-meta text-xs">· {duration}</span>
|
||||
{exp.data.location && <span class="exp-meta text-xs">· {exp.data.location}</span>}
|
||||
</div>
|
||||
|
||||
<h3 class="text-xl font-bold pe-20">{exp.data.role}</h3>
|
||||
<p class="exp-company text-sm font-medium mt-1 mb-3">
|
||||
{exp.data.companyUrl ? (
|
||||
<a href={exp.data.companyUrl} target="_blank" rel="noopener noreferrer" class="transition-colors">{exp.data.company}</a>
|
||||
) : exp.data.company}
|
||||
</p>
|
||||
|
||||
<div class="exp-content text-sm leading-relaxed mb-4 prose prose-sm max-w-none">
|
||||
<Content />
|
||||
</div>
|
||||
|
||||
{exp.data.technologies && exp.data.technologies.length > 0 && (
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{exp.data.technologies.map((tech: string) => (
|
||||
<span class="exp-tech inline-block px-2.5 py-0.5 text-xs rounded-full border">
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<CareerTimeline locale="en" />
|
||||
</section>
|
||||
</Layout>
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue