Add french and english language and toggle

This commit is contained in:
2024-06-23 21:40:20 +02:00
parent 6166e49922
commit 6593374836
11 changed files with 164 additions and 21 deletions

61
package-lock.json generated
View File

@@ -12,9 +12,11 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"aos": "^2.3.4",
"i18next": "^23.11.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-github-btn": "^1.4.0",
"react-i18next": "^14.1.2",
"react-icons": "^4.11.0",
"react-scripts": "^5.0.1",
"sass": "^1.69.4",
@@ -8915,6 +8917,14 @@
"node": ">=12"
}
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/html-webpack-plugin": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz",
@@ -9058,6 +9068,28 @@
"node": ">=10.17.0"
}
},
"node_modules/i18next": {
"version": "23.11.5",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.5.tgz",
"integrity": "sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -14484,6 +14516,27 @@
"react": ">=16.3.0"
}
},
"node_modules/react-i18next": {
"version": "14.1.2",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.2.tgz",
"integrity": "sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"html-parse-stringify": "^3.0.1"
},
"peerDependencies": {
"i18next": ">= 23.2.3",
"react": ">= 16.8.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/react-icons": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz",
@@ -16884,6 +16937,14 @@
"node": ">= 0.8"
}
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/w3c-hr-time": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",

View File

@@ -7,9 +7,11 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"aos": "^2.3.4",
"i18next": "^23.11.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-github-btn": "^1.4.0",
"react-i18next": "^14.1.2",
"react-icons": "^4.11.0",
"react-scripts": "^5.0.1",
"sass": "^1.69.4",

View File

@@ -9,6 +9,8 @@ import Footer from "./components/Footer.tsx";
import CV from "./components/CV.tsx";
import Menu from "./components/Menu.tsx";
import data from "./assets/DATA.ts";
import { useTranslation } from "react-i18next";
import i18n from './i18n.js';
function App() {
const [theme, setTheme] = useState("light");
@@ -28,22 +30,34 @@ function App() {
document.documentElement.classList.toggle("dark");
}
const { i18n } = useTranslation();
const toggleLanguage = () => {
i18n.changeLanguage(i18n.language === "fr" ? "en" : "fr");
}
return (
<div className="min-h-screen py-10 px-3 sm:px-5 bg-gray-100 dark:bg-gray-900">
<Menu />
<div data-aos="face-down" data-aos-duration="800">
<div data-aos="face-down" data-aos-duration="800" id="top">
<Card name={data.name} title={data.title} social={data.social} />
</div>
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="about"/>
<div data-aos="fade-up" data-aos-duration="800" data-aos-delay="400">
<About title={data.about.title} description={data.about.description} />
<About />
<Skills skills={data.skills} />
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="projects" />
<Project projects={data.projects} />
<hr className="text-gray-800 dark:text-gray-200 mt-4" id="cv" />
<CV />
<Footer />
</div>
<button onClick={toggleTheme} className="fixed bottom-5 right-5 p-2 rounded-full bg-gray-100 dark:bg-gray-900 shadow-md">
{theme === "light" ? <FaMoon size={20} className="text-gray-800" /> : <FaSun size={20} className="text-gray-200" />}
</button>
<button onClick={toggleLanguage} className="fixed bottom-5 right-16 p-2 rounded-full bg-gray-100 dark:bg-gray-900 shadow-md text-gray-800 dark:text-gray-200">
{i18n.language === "fr" ? "EN" : "FR"}
</button>
</div>
);
}

View File

@@ -71,7 +71,7 @@ const Felix = {
link: "https://github.com/modelec"
},
{
title: "Mercury Cloud",
title: "MercuryCloud",
description: "Projet d'herbergeur de serveur de jeu et VPS. Poste de support technique, administrateur des service VPS, Game et web.",
tags: ["Linux", "Virtualisation", "CPanel", "Plesk", "WHMCS"],
link: "https://mercurycloud.fr/"

View File

@@ -1,11 +1,13 @@
// @ts-ignore
import React from 'react';
import {useTranslation} from "react-i18next";
function About({ref, title, description}) {
function About() {
const { t } = useTranslation();
return(
<div className="max-w-4xl mx-auto mt-16" id="about">
<p className="text-2xl md:text-4xl font-bold text-center text-gray-800 dark:text-gray-200">{title}</p>
<p className="text-base text-left md:text-center text-gray-600 leading-relaxed mt-4 dark:text-gray-400">{description}</p>
<div className="max-w-4xl mx-auto mt-16">
<p className="text-2xl md:text-4xl font-bold text-center text-gray-800 dark:text-gray-200">{t('about.title')}</p>
<p className="text-base text-left md:text-center text-gray-600 leading-relaxed mt-4 dark:text-gray-400">{t('about.description')}</p>
</div>
);
}

View File

@@ -1,11 +1,13 @@
// @ts-ignore
import React from 'react';
import { useTranslation } from 'react-i18next';
function CV(){
const { t } = useTranslation();
return (
<div className="max-w-4xl mx-auto mt-16 flex justify-center items-center flex-col gap-1" id="cv">
<p className="text-2xl md:text-4xl font-bold text-center text-gray-800 dark:text-gray-200">Mon CV</p>
<iframe src="/CV%20Félix%20MARQUET.pdf" className="w-full md:w-3/4 h-64 md:h-96 lg:h-screen" title="CV Félix MARQUET"></iframe>
<div className="max-w-4xl mx-auto mt-16 flex justify-center items-center flex-col gap-1 mb-4">
<p className="text-2xl md:text-4xl font-bold text-center text-gray-800 dark:text-gray-200">{t('cv.title')}</p>
<iframe src="/CV%20Félix%20MARQUET.pdf" className="w-full md:w-3/4 h-64 md:h-96 lg:h-screen mt-3" title="CV Félix MARQUET"></iframe>
</div>
);
}

View File

@@ -1,10 +1,12 @@
// @ts-ignore
import React from 'react';
import { FaGithub, FaTwitter, FaLinkedin, FaRegEnvelope } from 'react-icons/fa';
import {useTranslation} from "react-i18next";
function Card({name, title, social: {github, twitter, linkedin, mail}}){
function Card({name, social: {github, twitter, linkedin, mail}}){
//Get avatar from gravatar using email
const profile = `https://1.gravatar.com/avatar/4d43af207280d1d23e2a2905577c7b6167723fec2d33f946cc86f114c1a85b8d?size=256`;
const { t } = useTranslation();
return (
<div className="w-full" id="top">
<div className="flex flex-col items-center justify-center max-w-xs mx-auto bg-white shadow-xl rounded-xl p-5 dark:bg-gray-800">
@@ -17,7 +19,7 @@ function Card({name, title, social: {github, twitter, linkedin, mail}}){
</div>
<div className="text-center mt-5">
<p className="text-xl sm:text-2xl text-gray-900 dark:text-white">{name}</p>
<p className="text-xs sm:text-base text-gray-600 pt-2 pb-4 px-5 w-auto inline-block border-b-2 dark:text-gray-200"> {title} </p>
<p className="text-xs sm:text-base text-gray-600 pt-2 pb-4 px-5 w-auto inline-block border-b-2 dark:text-gray-200"> {t('card.title')} </p>
<div className="flex align-center justify-center mt-4">
<a
className="text-xl m-1 p-1 sm:m-2 sm:p-2 text-gray-800 hover:bg-gray-800 rounded-full hover:text-white transition-colors duration-300 dark:text-gray-200 dark:hover:bg-gray-200 dark:hover:text-gray-800 dark:hover:bg-opacity-20"

View File

@@ -1,14 +1,16 @@
//@ts-ignore
import React from 'react';
import {useTranslation} from "react-i18next";
function Menu() {
const { t } = useTranslation();
return (
<nav className="fixed top-0 w-full bg-gray-100 dark:bg-gray-900 z-50 shadow-md shadow-gray-800 dark:shadow-gray-200">
<ul className="flex justify-around">
<li><a href="#top" className="text-gray-800 dark:text-gray-200">Félix MARQUET</a></li>
<li><a href="#about" className="text-gray-800 dark:text-gray-200">A propos de moi</a></li>
<li><a href="#projects" className="text-gray-800 dark:text-gray-200">Mes projets</a></li>
<li><a href="#cv" className="text-gray-800 dark:text-gray-200">Mon CV</a></li>
<li><a href="#about" className="text-gray-800 dark:text-gray-200">{t('nav.about')}</a></li>
<li><a href="#projects" className="text-gray-800 dark:text-gray-200">{t('nav.projects')}</a></li>
<li><a href="#cv" className="text-gray-800 dark:text-gray-200">{t('nav.cv')}</a></li>
</ul>
</nav>
)

View File

@@ -2,11 +2,13 @@
import React from 'react';
// @ts-ignore
import ProjectCard from "./ProjectCard.tsx";
import {useTranslation} from "react-i18next";
function Projects({projects}) {
const { t } = useTranslation();
return(
<div id="projects">
<h1 className="mt-8 text-2xl md:text-4xl text-center font-extrabold dark:text-gray-200">Mes projets</h1>
<div>
<h1 className="mt-8 text-2xl md:text-4xl text-center font-extrabold dark:text-gray-200">{t('projects.title')}</h1>
<div className="flex-row flex-wrap flex justify-center gap-4">
{projects.map((project) => (
<ProjectCard project={project} />

View File

@@ -1,8 +1,12 @@
// @ts-ignore
import React from 'react';
import { FaExternalLinkAlt} from "react-icons/fa";
import GitHubButton from 'react-github-btn';
import {useTranslation} from "react-i18next";
const projectCard = ({ project: { title, description, tags, link} }) => {
const ProjectCard = ({ project }) => {
const { t } = useTranslation();
const { title, link, tags } = project;
return (
<div className="group w-full sm:w-5/12 m-4 mx-auto p-6 rounded-xl border-2 border-gray-300 dark:border-gray-700">
<a href={link}>
@@ -12,13 +16,13 @@ const projectCard = ({ project: { title, description, tags, link} }) => {
</h1>
</a>
<hr className="my-4" />
<p className="dark:text-gray-300">{description}</p>
<p className="dark:text-gray-300">{t(`projects.${project.title}.description`)}</p>
<div className="mt-4 mb-8 flex flex-wrap justify-center items-center gap-2">
{tags.map((tag) => (
<div className="px-4 py-1 border-2 rounded-full dark:text-gray-300">{tag}</div>
))}
</div>
{title !== "Mercury Cloud" && (
{title !== "MercuryCloud" && (
<div className="w-full text-center">
<GitHubButton href={link} data-color-scheme="no-preference: light; light: light; dark: dark;" data-icon="octicon-star" data-size="large" data-show-count="true" aria-label="Star ntkme/github-buttons on GitHub">Star</GitHubButton>
{" "}
@@ -29,4 +33,4 @@ const projectCard = ({ project: { title, description, tags, link} }) => {
);
};
export default projectCard;
export default ProjectCard;

52
src/i18n.js Normal file
View File

@@ -0,0 +1,52 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
i18n
.use(initReactI18next)
.init({
resources: {
fr: {
translation: {
"about.title": "A propos de moi",
"about.description": "Je suis étudiant en 2e année à l'ISEN Nantes. Je suis passionné par l'informatique. J'ai appris à coder en autodidacte et je suis actuellement en train d'apprendre le C++ et le PHP. Je suis également passionné par l'électronique et le hardware. Je possède un homelab composé de 2 serveur, un DELL T320 et un DELL T330 les 2 sous proxmox.",
"card.title": "Etudiant en 2e année a l'ISEN Nantes",
"projects.title": "Mes projets",
"projects.SansDomaineFixe.xyz.description": "Site web offrant des enregistrements DNS gratuits",
"projects.Front end starter.description": "Mon starter personnel pour projet front end",
"projects.Project C - ISEN CIR 1.description": "Projet de fin de 1ere année à l'ISEN Nantes",
"projects.Projet robot.description": "Projet de robot avec le club Modelec ISEN pour la coupe de france de robotique (Developpement et déploiment sur Raspberry Pi)",
"projects.MercuryCloud.description": "Projet d'herbergeur de serveur de jeu et VPS. Poste de support technique, administrateur des service VPS, Game et web.",
"projects.Projet C++ - ISEN CIR 2.description": "Projet de fin de 4e semestre à l'ISEN Nantes. Création d'un jeu de type Tower Defense en C++ avec la librairie QT6.",
"cv.title": "Mon CV",
"nav.about": "A propos de moi",
"nav.projects": "Mes projets",
"nav.cv": "Mon CV",
},
},
en: {
translation: {
"cv.title": "My CV",
"about.title": "About me",
"about.description": "I am a second year student at ISEN Nantes. I am passionate about computer science. I learned to code on my own and I am currently learning C++ and PHP. I am also passionate about electronics and hardware. I have a homelab composed of 2 servers, a DELL T320 and a DELL T330 both under proxmox.",
"card.title": "Second year student at ISEN Nantes",
"projects.title": "My projects",
"projects.SansDomaineFixe.xyz.description": "Website offering free DNS records",
"projects.Front end starter.description": "My personal starter for front end projects",
"projects.Project C - ISEN CIR 1.description": "End of 1st year project at ISEN Nantes",
"projects.Projet robot.description": "Robot project with the Modelec ISEN club for the French robotics cup (Development and deployment on Raspberry Pi)",
"projects.MercuryCloud.description": "Game server and VPS hosting project. Technical support position, administrator of VPS, Game and web services.",
"projects.Projet C++ - ISEN CIR 2.description": "End of 4th semester project at ISEN Nantes. Creation of a Tower Defense type game in C++ with the QT6 library.",
"nav.about": "About me",
"nav.projects": "My projects",
"nav.cv": "My CV",
},
},
},
lng: navigator.language.startsWith('fr') ? 'fr' : 'en',
fallbackLng: "en",
interpolation: {
escapeValue: false
},
});
export default i18n;