88 lines
No EOL
3.9 KiB
TypeScript
88 lines
No EOL
3.9 KiB
TypeScript
async function getProjects() {
|
|
const baseUrl = process.env.BACKEND_URL;
|
|
if (!baseUrl) {
|
|
throw new Error("Variable d'environnement BACKEND_URL manquante");
|
|
}
|
|
|
|
const res = await fetch(`${baseUrl}/projects/`, {
|
|
cache: "no-store",
|
|
});
|
|
|
|
if(!res.ok){
|
|
throw new Error("Erreur lors de la récupération des projets depuis l'API");
|
|
}
|
|
|
|
return res.json();
|
|
}
|
|
|
|
function toTags(technologies: unknown): string[] {
|
|
if (Array.isArray(technologies)) return technologies.filter(Boolean).map(String);
|
|
if (typeof technologies === 'string') {
|
|
// Split on common separators and the word 'et' (French 'and')
|
|
return technologies
|
|
.split(/,|;|\||\//g)
|
|
.flatMap(part => part.split(/\bet\b/i))
|
|
.flatMap(part => part.split(/\s{2,}/))
|
|
.map(s => s.trim())
|
|
.filter(s => s.length > 0);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function normalizeImageUrl(img: unknown): string | null {
|
|
if (typeof img !== 'string' || img.trim() === '') return null;
|
|
const val = img.trim();
|
|
// If already absolute (http/https or data URL), return as is
|
|
if (/^(https?:)?\/\//i.test(val) || /^data:/i.test(val)) return val;
|
|
// Otherwise, try to use as-is; backend might serve it relatively
|
|
return val;
|
|
}
|
|
|
|
export default async function ProjectsPage() {
|
|
const projects = await getProjects();
|
|
|
|
return(
|
|
<section id="projets" className="section">
|
|
<div className="container">
|
|
<h2 className="section-title">Mes projets</h2>
|
|
<ul className="projects-list">
|
|
{projects.map((p: any) => {
|
|
const tags = toTags(p.technologies);
|
|
const href = p.link || p.url || '#';
|
|
const imgUrl = normalizeImageUrl(p.image);
|
|
return (
|
|
<li key={p.id} className="project-card">
|
|
<a href={href} target="_blank" rel="noopener noreferrer" className="project-card__link">
|
|
<div className="project-card__header" aria-hidden="true">
|
|
{p.image ? (
|
|
<div className="project-card__image" style={{ backgroundImage: `url(${p.image || ''})` }} />
|
|
) : (
|
|
<div className="project-card__image project-card__image--placeholder">
|
|
<span>{(p.name || '').slice(0, 1).toUpperCase()}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="project-card__body">
|
|
<h3>{p.name}</h3>
|
|
<p>{p.description}</p>
|
|
</div>
|
|
<div className="project-card__footer">
|
|
<div className="tech-badges">
|
|
{tags.length > 0 ? (
|
|
tags.map((t: string, idx: number) => (
|
|
<span key={idx} className="badge badge--mono">{t}</span>
|
|
))
|
|
) : (
|
|
p.technologies ? <span className="badge badge--mono">{String(p.technologies)}</span> : null
|
|
)}
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
);
|
|
} |