Appearance
🧩 Composants Principaux
Ce document détaille les composants Astro principaux de mon portfolio.
Hero.astro
Responsabilité : Section d'en-tête avec photo, nom, poste, localisation et contacts.
Props
typescript
interface Props {
basics: Basics;
locale: Locale;
}
### Fonctionnalités
---
title: Composants Principaux
description: Présentation des composants principaux du portfolio (Hero, Experience, Skills, etc.).
---
- 📞 **Liens téléphone et email** avec aria-label traduits
### Code Clé
```astro
---
import { t } from "@/utils/i18n";
const { basics, locale } = Astro.props;
const { name, label, image, imagesmall, location, profiles, phone, email } = basics;
const showProfileText = t("sections:hero.showProfile", { lng: locale, name });
---
<img
src={image}
alt={name}
fetchpriority="high"
srcset={`${imagesmall} 128w, ${image} 256w`}
sizes="(max-width: 640px) 128px, 256px"
width="256"
height="256"
/>
<h1>{name}</h1>
<p>{label}</p>
<p>{location.city}, {location.region}</p>
<script>
import { initializeTheme, updateTheme, setupPrintHandlers } from "@/utils/theme";
const select = document.getElementById("themeSwitch");
if (select) {
initializeTheme(select);
setupPrintHandlers(select);
select.addEventListener("change", (e) => {
updateTheme(e.target.value);
});
}
</script>Experience.astro
Responsabilité : Liste des expériences professionnelles avec expand/collapse.
Props
typescript
interface Props {
work: Work[];
locale: Locale;
}Fonctionnalités
- 📅 Dates formatées (startYear - endYear ou "Actuel")
- 🔽 Expand/collapse avec Alpine.js
- 🏷️ Tags de compétences avec icônes SVG
- 🔗 Lien vers le site de l'entreprise
- 📍 Type de lieu (Remote, Hybrid, On-site)
Code Clé
astro
---
const titleText = t("sections:experience.title", { lng: locale });
const nowText = t("sections:experience.now", { lng: locale });
const moreText = t("components:experience.showMore", { lng: locale });
const lessText = t("components:experience.showLess", { lng: locale });
---
{work.map(({ name, position, startDate, endDate, summary, responsibilities, achievements, skills }) => {
const startYear = new Date(startDate).getFullYear();
const endYear = endDate ? new Date(endDate).getFullYear() : nowText;
return (
<article x-data="{ expanded: false }">
<header>
<h3>{position}</h3>
<p>{name} · {startYear} - {endYear}</p>
</header>
<!-- Missions (toujours visibles) -->
<div>
<h4>Missions:</h4>
<ul>
{Array.isArray(summary) ? (
summary.map(item => <li>{item}</li>)
) : (
<li>{summary}</li>
)}
</ul>
</div>
<!-- Contenu expandable -->
<div x-show="expanded" x-collapse>
{responsibilities && (
<div>
<h4>Responsabilités:</h4>
<ul>{responsibilities.map(r => <li>{r}</li>)}</ul>
</div>
)}
{achievements && (
<div>
<h4>Réalisations:</h4>
<ul>{achievements.map(a => <li>{a}</li>)}</ul>
</div>
)}
</div>
<!-- Bouton toggle -->
<button @click="expanded = !expanded">
<span x-text="expanded ? lessText : moreText"></span>
</button>
<!-- Tags compétences -->
{skills && (
<div>
{skills.map(skill => (
<span class="badge">{skill}</span>
))}
</div>
)}
</article>
);
})}Skills.astro
Responsabilité : Afficher les compétences avec niveau et mots-clés dans un tooltip.
Props
typescript
interface Props {
skills: Skills[];
locale: Locale;
}Fonctionnalités
- 🏷️ Badge pour chaque compétence
- 💡 Tooltip au survol (Alpine.js)
- 📊 Niveau affiché dans le tooltip
- 🔤 Mots-clés associés
Code Clé
astro
<ul>
{skills.map(({ name, level, keywords }) => (
<li
x-data="{ open: false }"
@mouseenter="open = true"
@mouseleave="open = false"
class="relative"
>
<span>{name}</span>
<!-- Tooltip -->
<div x-show="open" x-cloak class="tooltip">
<div class="font-semibold">{levelText}: {level}</div>
<ul class="flex flex-wrap gap-1">
{keywords?.map(keyword => (
<li class="badge-small">{keyword}</li>
))}
</ul>
</div>
</li>
))}
</ul>⚠️ Correction accessibilité : Le x-data est directement sur le <li> (pas de <div> wrapper) pour respecter la structure HTML <ul> → <li>.
KeyboardManager.astro
Responsabilité : Gérer la palette de commandes (Cmd+K) avec HotKeyPad.
Props
typescript
interface Props {
profiles: Profiles[];
locale: Locale;
}Fonctionnalités
- ⌨️ Raccourcis clavier (Cmd+K, Ctrl+P, Ctrl+G, etc.)
- 🌐 Actions rapides vers profils sociaux
- 🖨️ Impression rapide
- 🔍 Recherche dans les commandes
Code Clé
astro
---
import { SOCIAL_ICONS_SVG } from "@/constants/social-icons-svg";
const profilesInfo = profiles.map(({ network, url }) => {
const icon = SOCIAL_ICONS_SVG[network];
const firstLetter = network[0].toUpperCase();
return {
id: network,
section: "Social",
title: network,
url,
icon,
hotkey: `ctrl+${firstLetter}`
};
});
---
<div id="hotkeypad"
data-info={JSON.stringify(profilesInfo)}
data-print-text={printText}
data-search-placeholder={searchPlaceholder}>
</div>
<script>
import HotKeyPad from "hotkeypad";
const hotkeypad = new HotKeyPad({
placeholder: document.querySelector("#hotkeypad")?.getAttribute("data-search-placeholder") ?? "Search"
});
const info = hotkeypad.instance.getAttribute("data-info") ?? "[]";
const parsedInfo = JSON.parse(info);
const data = parsedInfo.map(({ url, hotkey, icon, id, section, title }) => {
return {
id,
title,
icon,
hotkey,
section,
handler: () => window.open(url, "_blank")
};
});
const printText = hotkeypad.instance.getAttribute("data-print-text") ?? "Print";
hotkeypad.setCommands([
{
id: "print",
title: printText,
icon: `<svg>...</svg>`,
hotkey: "ctrl+P",
section: "Actions",
handler: () => window.print()
},
...data
]);
// Fix accessibilité des h4 de HotKeyPad
document.querySelectorAll('[data-container] h4').forEach((header) => {
if (header.textContent === 'Actions') {
header.setAttribute('aria-label', header.textContent);
} else if (header.textContent === 'Social') {
header.setAttribute('aria-label', 'Social');
}
});
</script>📝 Note : Les icônes sont en format string car HotKeyPad ne peut pas utiliser de composants Astro.
ThemeSwitch.astro
Responsabilité : Sélecteur de thème (System/Light/Dark).
Props
typescript
interface Props {
locale: Locale;
}Fonctionnalités
- 🌓 3 modes : System, Light, Dark
- 💾 Sauvegarde dans localStorage
- 🔄 Application immédiate sans reload
Code Clé
astro
---
import { t } from "@/utils/i18n";
const labelText = t("common:theme.label", { lng: locale });
const systemText = t("common:theme.system", { lng: locale });
const lightText = t("common:theme.light", { lng: locale });
const darkText = t("common:theme.dark", { lng: locale });
---
<label for="themeSwitch">{labelText}</label>
<select id="themeSwitch">
<option value="system">{systemText}</option>
<option value="light">{lightText}</option>
<option value="dark">{darkText}</option>
</select>
<script>
import { initializeTheme, updateTheme } from "@/utils/theme";
const select = document.getElementById("themeSwitch");
if (select) {
initializeTheme(select);
select.addEventListener("change", (e) => {
updateTheme(e.target.value);
});
}
</script>LocaleSwitch.astro
Responsabilité : Sélecteur de langue (FR/EN).
Props
typescript
interface Props {
locale: Locale;
}Fonctionnalités
- 🌍 Affiche le drapeau de la langue actuelle
- 💾 Sauvegarde dans localStorage
- 🔄 Redirection vers
/fr/ou/en/
Code Clé
astro
---
import FlagFr from "@/icons/FlagFr.astro";
import FlagEn from "@/icons/FlagEn.astro";
const labelText = t("common:locale.label", { lng: locale });
const frText = t("common:locale.fr", { lng: locale });
const enText = t("common:locale.en", { lng: locale });
---
<label for="localeSwitch">
{locale === "fr" ? <FlagFr /> : <FlagEn />}
{labelText}
</label>
<select id="localeSwitch">
<option value="fr" selected={locale === "fr"}>{frText}</option>
<option value="en" selected={locale === "en"}>{enText}</option>
</select>
<script>
import { switchLocale } from "@/utils/locale";
const select = document.getElementById("localeSwitch");
select?.addEventListener("change", function() {
switchLocale(this.value);
});
</script>Dialog.astro
Responsabilité : Modale pour afficher les mentions légales.
Props
typescript
interface Props {
legalInfo: any; // Structure de mentions-fr.json / mentions-en.json
locale: Locale;
}Fonctionnalités
- 📄 Affichage des mentions légales
- ❌ Fermeture par bouton ou click backdrop
- ⌨️ Fermeture avec Escape
- ♿ Focus trap avec Alpine.js
Code Clé
astro
<div x-data="{ open: false }">
<button @click="open = true">
{t("components:dialog.legal", { lng: locale })}
</button>
<div x-show="open" x-cloak class="fixed inset-0 z-50">
<!-- Backdrop -->
<div @click="open = false" class="fixed inset-0 bg-black/50"></div>
<!-- Modal -->
<div class="fixed inset-0 overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-4">
<div @click.away="open = false" @keydown.escape.window="open = false"
class="bg-skin-fill rounded-lg p-6 max-w-2xl w-full">
<h2>{legalInfo.heading}</h2>
{legalInfo.sections.map(section => (
<section>
<h3>{section.title}</h3>
{section.paragraph && <p>{section.paragraph}</p>}
{section.sites && (
<div>
<p>{section.sites.name}</p>
{Object.entries(section.sites.urls).map(([key, url]) => (
<a href={url}>{key}</a>
))}
</div>
)}
</section>
))}
<button @click="open = false">
{t("components:dialog.close", { lng: locale })}
</button>
</div>
</div>
</div>
</div>
</div>Section.astro
Responsabilité : Wrapper générique pour les sections avec titre H2.
Props
typescript
interface Props {
title: string;
className?: string;
}Code
astro
---
const { title, className } = Astro.props;
---
<section class={className}>
<h2 class="text-2xl font-bold mb-4 text-skin-base">
{title}
</h2>
<slot />
</section>Utilisation :
astro
<Section title="Compétences">
<!-- Contenu de la section -->
</Section>Conventions
✅ Toujours passer locale
Tous les composants qui utilisent des traductions reçoivent locale en prop.
✅ Utiliser des interfaces TypeScript
astro
---
interface Props {
basics: Basics;
locale: Locale;
}
const { basics, locale } = Astro.props;
---✅ Traduire tous les textes visibles
astro
const text = t("sections:hero.title", { lng: locale });✅ Utiliser Alpine.js pour l'interactivité
html
<div x-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<div x-show="open">Contenu</div>
</div>