feat: Enhance portfolio with SEO meta tags, implement API for experie… (#2)

* feat: Enhance portfolio with SEO meta tags, implement API for experiences and projects, and refactor components for improved structure

* fix: address Copilot review comments on PR #2

- Migrate Grid callers to the System Grid size prop (GridWrapper)
- Render experience icons from exp.type instead of exp.icon
- Show experience date range (dateS-dateE) on desktop and mobile
- Select experience description from the active i18n language
- Restore technology chips from exp.technologies
- Rename uesExperience hook to useExperience
- Normalize getProjets response to the { data, count } shape
- Return ExperienceFull from getExperience
- Type useEditProjet/updateProjet with ProjetEdit
- Fetch project details from the API in Projet page (useProjet)
- Replace ExperienceType enum with const object (erasableSyntaxOnly)
- Sync document lang attribute with the active language
This commit is contained in:
SYLVAIN TROUILH
2026-09-12 20:29:55 +00:00
committed by GitHub
parent bdb9866164
commit cf68d2d2e4
18 changed files with 294 additions and 145 deletions
+3 -1
View File
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="fr">
<head>
<meta charset="UTF-8" />
@@ -11,6 +11,8 @@
href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap"
rel="stylesheet">
<title>Portfolio TROUILH Sylvain</title>
<meta name="description" content="Découvrez le portfolio de Sylvain Trouilh, un développeur passionné. Explorez ses projets, compétences et expériences dans le domaine du développement web. Contactez-le pour collaborer sur des projets passionnants." />
<meta name="keywords" content="Portfolio, Sylvain Trouilh, Développeur, Projets, Compétences, Expériences, Développement Web, Contact" />
</head>
<body>
+9
View File
@@ -4,6 +4,15 @@ server {
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_min_length 256;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_buffers 16 8k;
include /etc/nginx/mime.types;
types {
application/javascript mjs;
+17 -4
View File
@@ -1,16 +1,29 @@
import axios from "axios";
import type { ProjetCreate, Projetfull, Projets, Projetshort } from "../../types/projet";
import type { ProjetCreate, ProjetEdit, Projetfull, Projets, Projetshort } from "../types/projet";
export async function getProjets(limit?: number, page?: number): Promise<Projets> {
const url = import.meta.env.VITE_API_URL + "/projets";
const projets = await axios.get(url, {
const response = await axios.get(url, {
params: {
nb: limit,
page: page,
},
});
return projets.data;
// Normalise la réponse de l'API pour exposer la forme { data, count }.
// Accepte { data, count }, { projets, count } ou un simple tableau.
const body = response.data;
if (body && Array.isArray(body.data)) {
return body as Projets;
}
if (body && Array.isArray(body.projets)) {
return { data: body.projets, count: body.count ?? body.projets.length };
}
return {
data: Array.isArray(body) ? body : [],
count: Array.isArray(body) ? body.length : 0,
};
}
export async function getProjet(id: number): Promise<Projetfull> {
@@ -30,7 +43,7 @@ export async function deleteProjet(id: number): Promise<void> {
await axios.delete(url);
}
export async function updateProjet(projet: Projetshort): Promise<Projetshort> {
export async function updateProjet(projet: ProjetEdit): Promise<Projetfull> {
const url = import.meta.env.VITE_API_URL + "/projet/" + projet.id;
const updatedprojet = await axios.put(url, projet);
return updatedprojet.data;
+26
View File
@@ -0,0 +1,26 @@
import axios from "axios";
import type { Experiencecreate, ExperienceFull, Experiences } from "../types/experience";
export async function getExperiences(): Promise<Experiences> {
const url = import.meta.env.VITE_API_URL + "/experiences";
const experiences = await axios.get(url);
return experiences.data;
}
export async function getExperience(id: number): Promise<ExperienceFull> {
const url = import.meta.env.VITE_API_URL + "/experience/" + id;
const experience = await axios.get(url);
return experience.data;
}
export async function createExperience(experience: Experiencecreate): Promise<Experiencecreate> {
const url = import.meta.env.VITE_API_URL + "/experiences";
const newexperience = await axios.post(url, experience);
return newexperience.data;
}
export async function deleteExperience(id: number): Promise<void> {
const url = import.meta.env.VITE_API_URL + "/experience/" + id;
await axios.delete(url);
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { Grid as MuiGrid } from "@mui/material";
import Grid from "@mui/system/Grid";
export default function GridWrapper(props: any) {
return <MuiGrid {...props} />;
return <Grid {...props} />;
}
+42
View File
@@ -0,0 +1,42 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { createExperience, deleteExperience, getExperience, getExperiences } from '../api/experience';
import type { Experiencecreate } from '../types/experience';
export function useExperiences() {
return useQuery({
queryKey: ['experiences'],
queryFn: () => getExperiences(),
})
}
export function useExperience(id: number | undefined) {
return useQuery({
queryKey: ['experience', id],
queryFn: () => getExperience(id!),
enabled: !!id,
})
}
export default function useCreateExperience() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newExperience: Experiencecreate) => createExperience(newExperience),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['experiences'] });
},
})
}
export function useDeleteExperience() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => deleteExperience(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['experiences'] });
},
})
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import { fetchIpPublic } from '../assets/api/ipPublic';
import { fetchIpPublic } from '../api/ipPublic';
export function useIpPublic() {
+5 -5
View File
@@ -1,7 +1,7 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { createProjet, getProjet, getProjets, updateProjet } from '../assets/api/Projet';
import type { ProjetCreate } from '../types/projet';
import { createProjet, getProjet, getProjets, updateProjet } from '../api/Projet';
import type { ProjetCreate, ProjetEdit } from '../types/projet';
export function useProjets(limit?: number, page?: number) {
@@ -26,17 +26,17 @@ export default function useCreateProjet() {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['projets'] });
},
})
}
export function useEditProjet() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (projet: any) => updateProjet(projet),
mutationFn: (projet: ProjetEdit) => updateProjet(projet),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['projets'] });
},
})
}
+6
View File
@@ -20,4 +20,10 @@ i18n
defaultNS: 'translation'
});
// Synchronise l'attribut lang du document avec la langue active (SEO/accessibilité)
i18n.on('languageChanged', (lng: string) => {
document.documentElement.lang = lng;
});
document.documentElement.lang = i18n.language;
export default i18n;
+1 -1
View File
@@ -25,7 +25,7 @@ export default function AllProjet() {
sx={{ display: "flex", flexDirection: "column", flex: 1, minHeight: 0 }}
>
{projets &&
projets.projets.map((projet) => (
projets.data.map((projet) => (
<Card sx={{ minWidth: 275, mt: 5 }} key={projet.id}>
<CardContent>
<Box
+5 -8
View File
@@ -1,5 +1,5 @@
import { useParams, useNavigate } from "react-router-dom";
// import { useProjet } from "../hook/useProjet";
import { useProjet } from "../hook/useProjet";
import {
Box,
Container,
@@ -15,7 +15,6 @@ import Grid from "../components/GridWrapper";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import GitHubIcon from "@mui/icons-material/GitHub";
import LanguageIcon from "@mui/icons-material/Language";
import { apiprojet } from "../types/projet.ts";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
@@ -23,9 +22,7 @@ export default function Projet() {
const { id } = useParams();
const navigate = useNavigate(); // Permet de gérer le bouton "Retour"
const isLoading = false;
const data = apiprojet.projets.find((p) => p.id === Number(id));
const { data, isLoading } = useProjet(id);
const primaryBlue = "#7ab2cb";
// 1. ÉTAT DE CHARGEMENT PROPRE
@@ -128,7 +125,7 @@ export default function Projet() {
</Box>
<Grid container spacing={4} alignItems="flex-start">
{/* COLONNE GAUCHE (Texte) : Prend 7 colonnes sur 12 */}
<Grid xs={12} md={7} component="div">
<Grid size={{ xs: 12, md: 7 }} component="div">
<Typography
variant="h2"
sx={{
@@ -181,7 +178,7 @@ export default function Projet() {
{/* J'ai utilisé alignItems="stretch" pour que les deux cartes aient la même hauteur naturellement */}
<Grid container spacing={4} alignItems="stretch">
{/* TECHNOLOGIES */}
<Grid xs={12} md={6} component="div">
<Grid size={{ xs: 12, md: 6 }} component="div">
<Paper
elevation={0}
sx={{
@@ -225,7 +222,7 @@ export default function Projet() {
</Grid>
{/* LIENS */}
<Grid xs={12} md={6} component="div">
<Grid size={{ xs: 12, md: 6 }} component="div">
<Paper
elevation={0}
sx={{
+44 -25
View File
@@ -17,11 +17,13 @@ import {
} from "@mui/lab";
import LaptopMacIcon from "@mui/icons-material/LaptopMac";
import SchoolIcon from "@mui/icons-material/School";
import VolunteerActivismIcon from "@mui/icons-material/VolunteerActivism";
import WorkIcon from "@mui/icons-material/Work";
import { useTranslation } from "react-i18next";
import { useExperiences } from "../../hook/useExperience";
// Tes données
const experiences = [
/* const experiences = [
{
title: "Développeur Front-end (stage 4 mois) ",
company: "TotalEnergies",
@@ -55,13 +57,30 @@ const experiences = [
desc: "Apprentissage des bases de l'ingénierie logicielle et gestion de projet.",
},
];
*/
const primaryBlue = "#7ab2cb";
function getExperienceIcon(type: string) {
switch (String(type).toLowerCase()) {
case "job":
return <WorkIcon fontSize="small" />;
case "internship":
return <LaptopMacIcon fontSize="small" />;
case "volunteer":
return <VolunteerActivismIcon fontSize="small" />;
default:
return <SchoolIcon fontSize="small" />;
}
}
export default function ExperienceTimeline() {
const theme = useTheme();
const { t } = useTranslation();
const { t, i18n } = useTranslation();
// Détecte si on est sur mobile pour changer l'affichage
const isMobile = useMediaQuery(theme.breakpoints.down("md"));
const primaryBlue = "#7ab2cb";
const { data: apiExperiences } = useExperiences();
// Sélectionne la description selon la langue active (repli sur le français)
const isEnglish = i18n.language?.toLowerCase().startsWith("en");
return (
<Box sx={{ bgcolor: "#fff" }} id="experience">
@@ -86,9 +105,8 @@ export default function ExperienceTimeline() {
position={isMobile ? "right" : "alternate"}
sx={{ bgcolor: "#fafbfc" }}
>
{experiences.map((exp, index) => (
{apiExperiences?.data.map((exp, index) => (
<TimelineItem key={index}>
{/* LA DATE (s'affiche en face du contenu sur PC) */}
<TimelineOppositeContent
sx={{
m: "auto 0",
@@ -98,10 +116,10 @@ export default function ExperienceTimeline() {
fontSize: "1.1rem",
}}
>
{exp.date}
{exp.dateS}
{exp.dateE ? `${exp.dateE}` : ""}
</TimelineOppositeContent>
{/* LE SÉPARATEUR CENTRAL */}
<TimelineSeparator>
<TimelineConnector sx={{ bgcolor: primaryBlue }} />
<TimelineDot
@@ -111,12 +129,11 @@ export default function ExperienceTimeline() {
boxShadow: "0 0 0 4px rgba(122, 178, 203, 0.2)", // Petit effet de halo autour du point
}}
>
{exp.icon}
{getExperienceIcon(exp.type)}
</TimelineDot>
<TimelineConnector sx={{ bgcolor: primaryBlue }} />
</TimelineSeparator>
{/* LE CONTENU (LA CARTE) */}
<TimelineContent sx={{ py: "12px", px: 2 }}>
<Paper
elevation={3}
@@ -134,7 +151,6 @@ export default function ExperienceTimeline() {
borderTop: `4px solid ${primaryBlue}`, // Petite touche de couleur en haut de la carte
}}
>
{/* Sur mobile, on affiche la date DANS la carte car il n'y a pas de place à côté */}
{isMobile && (
<Typography
variant="caption"
@@ -145,7 +161,8 @@ export default function ExperienceTimeline() {
fontWeight: "bold",
}}
>
{exp.date}
{exp.dateS}
{exp.dateE ? `${exp.dateE}` : ""}
</Typography>
)}
@@ -160,22 +177,24 @@ export default function ExperienceTimeline() {
</Typography>
<Typography variant="body2" color="text.secondary" paragraph>
{exp.desc}
{(isEnglish ? exp.descriptionEn : exp.descriptionFr) ||
exp.descriptionFr}
</Typography>
{/* Chips technologies */}
<Box
sx={{ display: "flex", flexWrap: "wrap", gap: 0.5, mt: 1 }}
>
{exp.technos.map((tech, i) => (
<Chip
key={i}
label={tech}
size="small"
sx={{ bgcolor: "#f0f4f8", color: "#555" }}
/>
))}
</Box>
{exp.technologies && exp.technologies.length > 0 && (
<Box
sx={{ display: "flex", flexWrap: "wrap", gap: 0.5, mt: 1 }}
>
{exp.technologies.map((tech, i) => (
<Chip
key={i}
label={tech}
size="small"
sx={{ bgcolor: "#f0f4f8", color: "#555" }}
/>
))}
</Box>
)}
</Paper>
</TimelineContent>
</TimelineItem>
+20 -23
View File
@@ -1,17 +1,15 @@
import { Box, Stack, Typography } from "@mui/material";
import { Box, Stack, Typography, Button } from "@mui/material";
import { useTranslation } from "react-i18next";
import Card_Projet from "../../components/Card_Projet";
//import { useProjets } from "../../hook/useProjet";
//import { useNavigate } from "react-router-dom";
import { useProjets } from "../../hook/useProjet";
import { useNavigate } from "react-router-dom";
import type { Projetshort } from "../../types/projet";
import { apiprojet } from "../../types/projet.ts";
export default function ProjetSection() {
const { t } = useTranslation();
//const navigate = useNavigate();
// const { data: apiprojet, isLoading } = useProjets(3);
const isLoading = false;
const navigate = useNavigate();
const { data: apiprojet } = useProjets(3);
const projets = Array.isArray(apiprojet?.data) ? apiprojet.data : [];
return (
<Box component="section">
@@ -36,20 +34,19 @@ export default function ProjetSection() {
mb: 4,
}}
>
{!isLoading &&
(Array.isArray(apiprojet?.projets)
? apiprojet.projets.map((projet: Projetshort) => (
<Card_Projet
key={projet.id}
name={projet.name}
shortDescription={projet.shortdescriptionfr}
technologies={projet.technologies ? projet.technologies : []}
id={projet.id}
left={projet.id % 2 === 0}
/>
))
: null)}
{/* <Button
{Array.isArray(projets)
? projets.map((projet: Projetshort) => (
<Card_Projet
key={projet.id}
name={projet.name}
shortDescription={projet.shortdescriptionfr}
technologies={projet.technologies ? projet.technologies : []}
id={projet.id}
left={projet.id % 2 === 0}
/>
))
: null}
<Button
variant="contained"
sx={{ backgroundColor: "#7ab2cb", mb: 4 }}
onClick={() => {
@@ -59,7 +56,7 @@ export default function ProjetSection() {
<Typography sx={{ textDecoration: "none" }}>
{t("SEE_ALL_PROJECTS")}
</Typography>
</Button> */}
</Button>
</Stack>
</Box>
);
+1 -1
View File
@@ -61,7 +61,7 @@ export default function SkillsSection() {
<Stack spacing={4} sx={{ pl: 12 }}>
<Grid container spacing={5} sx={{ mt: 2 }}>
{skillCategories.map((category, index) => (
<Grid xs={12} md={4} key={index} component="div">
<Grid size={{ xs: 12, md: 4 }} key={index} component="div">
<Box>
<Typography
variant="h6"
+34
View File
@@ -0,0 +1,34 @@
import type { arrayApi } from "./types"
export type ExperienceFull = {
id: number,
title: string,
company: string,
descriptionFr: string,
descriptionEn: string,
dateS: string,
dateE: string,
type: string
technologies?: string[];
}
export type Experiencecreate = {
title: string,
company: string,
descriptionFr: string,
descriptionEn: string,
dateS: string,
dateE: string,
type: string
}
export type Experiences = arrayApi<ExperienceFull>;
export const ExperienceType = {
JOB: "job",
INTERNSHIP: "internship",
VOLUNTEER: "volunteer",
OTHER: "other"
} as const;
+72 -74
View File
@@ -3,49 +3,47 @@
// types
export type Projetshort = {
id: number;
name: string;
shortdescriptionfr: string;
technologies?: string[];
id: number;
name: string;
shortdescriptionfr: string;
technologies?: string[];
}
export type Projetfull = {
id: number;
name: string;
shortdescriptionfr: string;
longdescriptionfr: string;
shortdescriptionen: string;
longdescriptionen: string;
linkGithub?: string;
linkWeb?: string;
technologies?: string[];
id: number;
name: string;
shortdescriptionfr: string;
longdescriptionfr: string;
shortdescriptionen: string;
longdescriptionen: string;
linkGithub?: string;
linkWeb?: string;
technologies?: string[];
}
export type Projets = {
projets: Projetshort[];
count: number;
}
export type Projets = arrayApi<Projetshort>;
export type ProjetCreate = {
name: string;
shortdescriptionfr: string;
export type ProjetCreate = {
name: string;
shortdescriptionfr: string;
}
export type ProjetEdit = {
id: number;
name: string;
shortdescriptionfr: string;
longdescriptionfr: string;
shortdescriptionen: string;
longdescriptionen: string;
linkGithub?: string;
linkWeb?: string;
technologies?: string[];
id: number;
name: string;
shortdescriptionfr: string;
longdescriptionfr: string;
shortdescriptionen: string;
longdescriptionen: string;
linkGithub?: string;
linkWeb?: string;
technologies?: string[];
}
import * as yup from "yup";
import type { arrayApi } from "./types";
export const schemaCreateProjet = yup.object().shape({
name: yup.string().required("Le nom du projet est requis"),
@@ -65,51 +63,51 @@ export const schemaEditProjet = yup.object().shape({
export const apiprojet = {
projets: [
{
id: 1,
name: "protfolio",
shortdescriptionfr:
"Un portfolio personnel développé avec React et Material-UI, mettant en avant mes compétences, expériences et projets de manière moderne et responsive. avec une API en Go pour la gestion des données.",
technologies: ["React", "Node.js", "Go", "SQL"],
longdescriptionfr:
"# À propos\nCe projet de **portfolio personnel** a été conçu pour présenter mes compétences, expériences et réalisations de manière professionnelle et attrayante.\n\n## Objectifs\n- Fournir une vitrine claire et moderne de mes compétences techniques.\n- Permettre une navigation fluide et réactive sur tous les appareils.\n- Centraliser les informations de projets via une API dédiée en Go.\n\n## Détails techniques\nL'interface utilisateur est développée avec React et Material-UI pour garantir une expérience cohérente et accessible. L'API backend, écrite en Go, expose des endpoints REST pour gérer les projets et leurs métadonnées. La communication frontback est gérée via axios, avec une stratégie simple de mise en cache pour améliorer les performances.\n\n## Déploiement et maintenance\nLe projet utilise Vite pour des builds rapides et un workflow de développement optimisé. Le code est structuré pour faciliter l'ajout de nouvelles sections et la migration vers une source de données distante (BDD ou CMS). Des scripts de linting et des contrôles basiques sont exécutés avant les builds.\n\n[Voir le dépôt GitHub](https://github.com/sylvaintr/portfolio-react)",
shortdescriptionen:
"A personal portfolio developed with React and Material-UI, showcasing my skills, experiences, and projects in a modern and responsive way. It includes a Go API for data management.",
longdescriptionen:
"This personal portfolio project was designed to showcase my skills, experiences, and achievements in a professional and attractive manner. Developed with React for a dynamic user interface and Material-UI for a modern design, it offers smooth and responsive navigation across all devices. The Go API efficiently manages project data, ensuring optimal performance. This portfolio highlights my key projects, technical skills, and professional background, providing visitors with a comprehensive overview of my expertise in web development.",
linkGithub: "https://github.com/sylvaintr/portfolio-react",
linkWeb: "http://localhost:5173/",
},
{
id: 2,
name: "Poloko Ikastola",
shortdescriptionfr: "Poloko Ikastola est une application de gestion d'une association d'une Ikastola (école basque). Elle permet de gérer des actulités, des événements, faire l'appel des élèves, génére les factures.",
technologies: ["php", "javascript", "laravel", "SQL", "docker"],
longdescriptionfr:
"## Poloko IkastolaPoloko Ikastola est une application de gestion d'une Ikastola (école basque) visant à simplifier les tâches administratives et la communication entre enseignants, administrateurs et parents.\n\n### Fonctionnalités principales\n- Gestion des actualités et calendrier d'événements.\n- Module d'appel \n- Génération et export des factures.\n- Gestion des utilisateurs et des permissions (administrateur, enseignant, parent).\n\n### Architecture & technos\nBackend : PHP + Laravel migrations et gestion des droits.\nFrontend : JavaScript avec templates et composants réutilisables.\nBase de données : SQL (optimisée pour requêtes courantes).\nEnvironnement : Docker Compose pour l'orchestration des services en dev/test.\n\n### Défis & solutions\n- Mise en place d'un modèle de permissions granulaires pour protéger les données élèves.\n- Automatisation des exports de factures .\n\n### Bénéfices\nLe système a réduit la charge administrative et centralisé les informations essentielles, facilitant le quotidien des équipes pédagogiques.",
shortdescriptionen: "Poloko Ikastola is a management application for a Basque Ikastola (school). It allows managing activities, events, student attendance, and generating invoices.",
longdescriptionen:
"Poloko Ikastola is a management application for a Basque Ikastola (school). It allows managing activities, events, student attendance, and generating invoices. The application was developed using PHP with the Laravel framework for the backend, JavaScript for client-side interactions, and SQL for database management. Docker was used to facilitate deployment and environment management. Poloko Ikastola aims to simplify administrative tasks and improve communication within the association.",
linkGithub: "https://github.com/sylvaintr/sae-ikastola-Poloko",
linkWeb: "https://app.polokohiriondoikastola.eus",
},
{
id: 3,
name: "VHS Video Home Share",
shortdescriptionfr:
"VHS est une plateforme web interactive qui transforme le streaming solitaire en une expérience collective. En mélangeant nostalgie et technologie moderne, l'application recrée la convivialité des cinémas et des soirées télé d'autrefois.",
technologies: ["php", "javascript", "twig", "SQL"],
longdescriptionfr:
projets: [
{
id: 1,
name: "protfolio",
shortdescriptionfr:
"Un portfolio personnel développé avec React et Material-UI, mettant en avant mes compétences, expériences et projets de manière moderne et responsive. avec une API en Go pour la gestion des données.",
technologies: ["React", "Node.js", "Go", "SQL"],
longdescriptionfr:
"# À propos\nCe projet de **portfolio personnel** a été conçu pour présenter mes compétences, expériences et réalisations de manière professionnelle et attrayante.\n\n## Objectifs\n- Fournir une vitrine claire et moderne de mes compétences techniques.\n- Permettre une navigation fluide et réactive sur tous les appareils.\n- Centraliser les informations de projets via une API dédiée en Go.\n\n## Détails techniques\nL'interface utilisateur est développée avec React et Material-UI pour garantir une expérience cohérente et accessible. L'API backend, écrite en Go, expose des endpoints REST pour gérer les projets et leurs métadonnées. La communication frontback est gérée via axios, avec une stratégie simple de mise en cache pour améliorer les performances.\n\n## Déploiement et maintenance\nLe projet utilise Vite pour des builds rapides et un workflow de développement optimisé. Le code est structuré pour faciliter l'ajout de nouvelles sections et la migration vers une source de données distante (BDD ou CMS). Des scripts de linting et des contrôles basiques sont exécutés avant les builds.\n\n[Voir le dépôt GitHub](https://github.com/sylvaintr/portfolio-react)",
shortdescriptionen:
"A personal portfolio developed with React and Material-UI, showcasing my skills, experiences, and projects in a modern and responsive way. It includes a Go API for data management.",
longdescriptionen:
"This personal portfolio project was designed to showcase my skills, experiences, and achievements in a professional and attractive manner. Developed with React for a dynamic user interface and Material-UI for a modern design, it offers smooth and responsive navigation across all devices. The Go API efficiently manages project data, ensuring optimal performance. This portfolio highlights my key projects, technical skills, and professional background, providing visitors with a comprehensive overview of my expertise in web development.",
linkGithub: "https://github.com/sylvaintr/portfolio-react",
linkWeb: "https://portefolio.sylvaintrouilh.fr/",
},
{
id: 2,
name: "Poloko Ikastola",
shortdescriptionfr: "Poloko Ikastola est une application de gestion d'une association d'une Ikastola (école basque). Elle permet de gérer des actulités, des événements, faire l'appel des élèves, génére les factures.",
technologies: ["php", "javascript", "laravel", "SQL", "docker"],
longdescriptionfr:
"## Poloko IkastolaPoloko Ikastola est une application de gestion d'une Ikastola (école basque) visant à simplifier les tâches administratives et la communication entre enseignants, administrateurs et parents.\n\n### Fonctionnalités principales\n- Gestion des actualités et calendrier d'événements.\n- Module d'appel \n- Génération et export des factures.\n- Gestion des utilisateurs et des permissions (administrateur, enseignant, parent).\n\n### Architecture & technos\nBackend : PHP + Laravel migrations et gestion des droits.\nFrontend : JavaScript avec templates et composants réutilisables.\nBase de données : SQL (optimisée pour requêtes courantes).\nEnvironnement : Docker Compose pour l'orchestration des services en dev/test.\n\n### Défis & solutions\n- Mise en place d'un modèle de permissions granulaires pour protéger les données élèves.\n- Automatisation des exports de factures .\n\n### Bénéfices\nLe système a réduit la charge administrative et centralisé les informations essentielles, facilitant le quotidien des équipes pédagogiques.",
shortdescriptionen: "Poloko Ikastola is a management application for a Basque Ikastola (school). It allows managing activities, events, student attendance, and generating invoices.",
longdescriptionen:
"Poloko Ikastola is a management application for a Basque Ikastola (school). It allows managing activities, events, student attendance, and generating invoices. The application was developed using PHP with the Laravel framework for the backend, JavaScript for client-side interactions, and SQL for database management. Docker was used to facilitate deployment and environment management. Poloko Ikastola aims to simplify administrative tasks and improve communication within the association.",
linkGithub: "https://github.com/sylvaintr/sae-ikastola-Poloko",
linkWeb: "https://app.polokohiriondoikastola.eus",
},
{
id: 3,
name: "VHS Video Home Share",
shortdescriptionfr:
"VHS est une plateforme web interactive qui transforme le streaming solitaire en une expérience collective. En mélangeant nostalgie et technologie moderne, l'application recrée la convivialité des cinémas et des soirées télé d'autrefois.",
technologies: ["php", "javascript", "twig", "SQL"],
longdescriptionfr:
"### VHS Video Home Share\nVHS Video Home Share est une plateforme de forme sur les film et série avec aussi les avis quiz et watchlist collaborative qui permet à des utilisateurs distants de regarder des vidéos ensemble, en synchronisant la lecture et en proposant des interactions en temps réel.\n\n#### Fonctionnalités détaillées\n- Création et gestion de salles privées ou publiques avec code d'accès.\n- Synchronisation de la lecture (play/pause/seek) pour tous les participants.\n- Chat en temps réel et gestion des invitations.\n- Rôles et permissions (hôte, modérateur, participant).\n\n#### Implémentation technique\nLa solution combine une API classique pour la gestion des ressources et un canal temps réel pour synchroniser les états de lecture entre clients. La persistance est assurée par une base SQL; des optimisations ciblées limitent la latence pour les commandes de contrôle.\n\n#### Perspectives d'amélioration\nAjout d'une authentification robuste, support de multiples sources vidéo, meilleure gestion adaptative de la qualité pour connexions lentes, et intégration d'un système de modération pour les salles publiques.\n\n_Lien du projet :_ https://sylvaintr.alwaysdata.net/vhs",
shortdescriptionen:
shortdescriptionen:
"VHS is an interactive web platform that transforms solitary streaming into a collective experience. By blending nostalgia with modern technology, the application recreates the conviviality of cinemas and old TV nights.",
longdescriptionen:
longdescriptionen:
"VHS Video Home Share is an interactive web platform that transforms solitary streaming into a collective experience. By blending nostalgia with modern technology, the application recreates the conviviality of cinemas and old TV nights. Users can create virtual viewing rooms, invite friends, and synchronize video playback to share entertainment moments",
linkWeb: "https://sylvaintr.alwaysdata.net/sae/SAE3.01/",
linkGithub: "https://github.com/maximeBourciez/VideoHomeShare-Groupe5",
},
],
};
linkWeb: "https://sylvaintr.alwaysdata.net/sae/SAE3.01/",
linkGithub: "https://github.com/maximeBourciez/VideoHomeShare-Groupe5",
},
],
};
+6
View File
@@ -0,0 +1,6 @@
export interface arrayApi<T> {
data: T[];
count: number;
}