From ad385c2bd98515426320460b49577d07062a9c0a Mon Sep 17 00:00:00 2001 From: luantran Date: Mon, 22 Dec 2025 23:21:40 -0500 Subject: [PATCH 1/2] made the whole website responsive (navbar and every section) --- src/App.jsx | 54 ++++++++----- src/components/AboutMe.jsx | 26 +++--- src/components/Education.jsx | 66 ++++++++-------- src/components/Experience.jsx | 40 +++++----- src/components/Navbar.jsx | 131 ++++++++++++++++++------------- src/components/Projects.jsx | 2 +- src/components/Skills.jsx | 37 +++++---- src/components/data/education.js | 7 ++ 8 files changed, 208 insertions(+), 155 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index c08da90..14f2df7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,27 +1,39 @@ -import AboutMe from "./components/AboutMe.jsx"; -import Education from "./components/Education.jsx"; -import Experience from "./components/Experience.jsx"; -import Skills from "./components/Skills.jsx"; -import Projects from "./components/Projects.jsx"; -import Navbar from "./components/Navbar.jsx"; - +import Navbar from './components/Navbar' +import AboutMe from './components/AboutMe' +import Skills from './components/Skills' +import Education from './components/Education' +import Experience from './components/Experience' +import Projects from './components/Projects' function App() { + return ( + <> + {/* Mobile: no grid, Tablet+: grid with sidebar column */} + {/* Using minmax(0, 1fr) for content column to allow proper shrinking */} +
+ + {/* Sidebar Column - only exists on desktop */} + + + {/* Main Content Column */} +
+ {/* Mobile navbar (shows outside grid) */} +
+ +
+ + + + + + +
-return ( - <> -
- -
- - - - -
-
- -) + + ) } -export default App +export default App \ No newline at end of file diff --git a/src/components/AboutMe.jsx b/src/components/AboutMe.jsx index 30e70ef..bd2cbb1 100644 --- a/src/components/AboutMe.jsx +++ b/src/components/AboutMe.jsx @@ -8,28 +8,28 @@ import SectionArrow from "./SectionArrow.jsx"; function AboutMe() { return ( -
+
-
-
+
+
-

Luan Tran

-

+

Luan Tran

+

I'm a Master's student in Applied Computer Science with industry experience in automation and web development at Broadsign, Ericsson, and Matrox. My projects include deploying automated CI/CD workflows, multimodal medical imaging applications, and automated language proficiency assessment systems.

-

+

I'm currently researching LLM-based agents for linguistic education and seeking opportunities in software development and machine learning.

-
+
@@ -37,18 +37,18 @@ function AboutMe() { - + Download CV
-
+
Luan Tran profile picture
diff --git a/src/components/Education.jsx b/src/components/Education.jsx index 722e1b3..1bc8953 100644 --- a/src/components/Education.jsx +++ b/src/components/Education.jsx @@ -1,49 +1,50 @@ import SectionArrow from "./SectionArrow.jsx"; import {educationData} from "./data/education.js" -const EducationItem = ({ data }) => { +const EducationItem = ({ data, index }) => { const { university, logo, degree, date, description, gpa } = data return ( -
+
0 ? 'my-[5px] sm:-mt-10 md:-mt-12 lg:-mt-14 xl:-mt-16' : 'my-[5px] sm:my-[3px]'} flex w-full sm:w-1/2 justify-start sm:justify-end sm:pr-[22px] sm:odd:justify-start sm:odd:self-end sm:odd:pl-[22px] sm:odd:pr-0 md:pr-[30px] md:odd:pl-[30px]`}>
+ sm:group-odd:after:left-[-7.5px] + sm:group-odd:after:right-auto + sm:group-odd:after:border-l + sm:group-odd:after:border-t + sm:group-odd:after:border-r-0 + sm:group-odd:after:border-b-0"> - {/* University Logo */} + {/* University Logo - responsive sizing */} {`${university} {/* Content */} -
-

{university}

-

{degree}

- +
+

{university}

+

{degree}

+
- {/* Timeline dot */} - + {/* Timeline dot - hidden on mobile, visible on tablet+ */} +
) @@ -53,13 +54,14 @@ function Education() { return (
-
-

+
+

Education

-
+ {/* Timeline - hidden on mobile via opacity, visible on tablet+ */} +
{educationData.map((data, idx) => ( - + ))}
diff --git a/src/components/Experience.jsx b/src/components/Experience.jsx index 2f15419..f759052 100644 --- a/src/components/Experience.jsx +++ b/src/components/Experience.jsx @@ -4,29 +4,32 @@ import {experienceData} from "./data/experience.js"; const ExperienceItem = ({ data }) => { return ( -
-
+
+
-
-
-
- {data.hash} -

{data.title}

-

{data.company} -

+
+
+
+ {data.hash} +

{data.title}

+

+ + {data.company} + +

- {data.period} + {data.period}
-
+
{data.achievements.map((achievementPoint, index) => ( -
+
+{achievementPoint}
))}
-
+
{data.tech.map((techPoint, index) => ( - {techPoint} + {techPoint} ))}
@@ -37,14 +40,15 @@ const ExperienceItem = ({ data }) => { function Experience() { return (
-
-
-

+
+
+

Work Experience

-
+
{experienceData.map((experience) => ( diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 31a1e96..4e337b3 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -1,8 +1,9 @@ import { useState, useEffect } from 'react' -import { FaUser, FaLightbulb, FaGraduationCap, FaBriefcase, FaFolderOpen } from 'react-icons/fa' +import { FaUser, FaLightbulb, FaGraduationCap, FaBriefcase, FaFolderOpen, FaBars, FaTimes } from 'react-icons/fa' function Navbar() { const [activeSection, setActiveSection] = useState('about') + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false) useEffect(() => { const handleScroll = () => { @@ -24,61 +25,81 @@ function Navbar() { return () => window.removeEventListener('scroll', handleScroll) }, []) + const handleNavClick = () => { + setIsMobileMenuOpen(false) + } + + const navItems = [ + { id: 'about', icon: FaUser, label: 'About' }, + { id: 'skills', icon: FaLightbulb, label: 'Skills' }, + { id: 'education', icon: FaGraduationCap, label: 'Education' }, + { id: 'experience', icon: FaBriefcase, label: 'Experience' }, + { id: 'projects', icon: FaFolderOpen, label: 'Projects' } + ] + return ( - + <> + {/* Mobile Top Bar (visible on screens smaller than md) */} +
+
+ Portfolio + +
+ + {/* Mobile Dropdown Menu */} + {isMobileMenuOpen && ( +
+
    + {navItems.map(({ id, icon: Icon, label }) => ( +
  • + + + {label} + +
  • + ))} +
+
+ )} +
+ + {/* Desktop Side Navigation - fixed positioning within the sidebar column */} +
+ +
+ ) } diff --git a/src/components/Projects.jsx b/src/components/Projects.jsx index 8372f70..8949ae2 100644 --- a/src/components/Projects.jsx +++ b/src/components/Projects.jsx @@ -130,7 +130,7 @@ function Projects() { return (
-

+

Projects

diff --git a/src/components/Skills.jsx b/src/components/Skills.jsx index 5156231..4ff1e1c 100644 --- a/src/components/Skills.jsx +++ b/src/components/Skills.jsx @@ -6,27 +6,34 @@ function Skills() { const renderDomainCard = (domain) => { const MainIcon = iconMap[domain.icon]; return ( -
-
- {/* Column 1: Domain Name with Icon (25%) */} -
- -

+
+ {/* Mobile: Stack vertically, Tablet+: Side by side */} +
+ + {/* Domain Name with Icon - keep horizontal on mobile, vertical on desktop */} +
+ {/* ONLY DOMAIN ICON resized: Mobile: 3xl, Small: 4xl, Tablet: 3xl, Desktop: 5xl */} + + {/* ONLY DOMAIN NAME resized: Mobile: base, Small: lg, Tablet: base, Desktop: xl */} +

{domain.name}

- {/* Column 3: Related Skills Icons (50%) */} -
+ {/* Related Skills Icons - KEEP ORIGINAL PROGRESSIVE SIZING */} +
{domain.relatedSkills.map((skill) => { const SkillIcon = iconMap[skill.icon]; return (
-
- +
+ {/* Skills keep progressive sizing: 2xl -> 3xl -> 4xl */} +
-

{skill.displayName}

+

+ {skill.displayName} +

); })} @@ -41,13 +48,13 @@ function Skills() { return (
-
-

+
+

Skills

-
-
+
+
{domains.map((domain) => renderDomainCard(domain))}
diff --git a/src/components/data/education.js b/src/components/data/education.js index 97dc0ff..a37eb8c 100644 --- a/src/components/data/education.js +++ b/src/components/data/education.js @@ -1,6 +1,7 @@ // Import logos import mcgillLogo from '../../assets/images/mcgill_logo.png' import concordiaLogo from '../../assets/images/concordia_logo.png' +import cimfLogo from '../../assets/images/cimf_logo.png' export const educationData = [ { @@ -14,5 +15,11 @@ export const educationData = [ date: '2014 - 2019', logo: mcgillLogo, degree: 'Bachelors\'s of Computer Engineering' + }, + { + university: 'Collège Marie de France', + date: '2012-2014', + logo: cimfLogo, + degree: 'French Baccalaureate - Highest Honors' } ] \ No newline at end of file From 6409b33f715b709ab1bf3bc9985a1ebad9e14eac Mon Sep 17 00:00:00 2001 From: luantran Date: Tue, 23 Dec 2025 00:17:59 -0500 Subject: [PATCH 2/2] made the website bilingual --- src/App.jsx | 44 ++--- src/assets/images/cimf_logo.png | Bin 0 -> 14172 bytes src/components/AboutMe.jsx | 24 +-- src/components/Education.jsx | 54 +++++-- src/components/Experience.jsx | 35 ++-- src/components/LanguageSwitcher.jsx | 59 +++++++ src/components/Navbar.jsx | 67 +++++--- src/components/Projects.jsx | 89 +++++----- src/components/Skills.jsx | 27 ++-- src/components/data/about.js | 26 +++ src/components/data/education.js | 30 +++- src/components/data/experience.js | 111 ++++++++----- src/components/data/skills.js | 242 ++++++++-------------------- src/contexts/LanguageContext.js | 3 + src/contexts/LanguageProvider.jsx | 31 ++++ src/contexts/useLanguage.js | 10 ++ src/utils/translationHelpers.js | 39 +++++ 17 files changed, 533 insertions(+), 358 deletions(-) create mode 100644 src/assets/images/cimf_logo.png create mode 100644 src/components/LanguageSwitcher.jsx create mode 100644 src/components/data/about.js create mode 100644 src/contexts/LanguageContext.js create mode 100644 src/contexts/LanguageProvider.jsx create mode 100644 src/contexts/useLanguage.js create mode 100644 src/utils/translationHelpers.js diff --git a/src/App.jsx b/src/App.jsx index 14f2df7..27717b1 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,35 +4,39 @@ import Skills from './components/Skills' import Education from './components/Education' import Experience from './components/Experience' import Projects from './components/Projects' +import {LanguageProvider} from "./contexts/LanguageProvider.jsx"; function App() { return ( <> - {/* Mobile: no grid, Tablet+: grid with sidebar column */} - {/* Using minmax(0, 1fr) for content column to allow proper shrinking */} -
+ + {/* Mobile: no grid, Tablet+: grid with sidebar column */} + {/* Using minmax(0, 1fr) for content column to allow proper shrinking */} +
- {/* Sidebar Column - only exists on desktop */} - - - {/* Main Content Column */} -
- {/* Mobile navbar (shows outside grid) */} -
+ {/* Sidebar Column - only exists on desktop */} +
+ + + {/* Main Content Column */} +
+ {/* Mobile navbar (shows outside grid) */} +
+ +
- - - - - -
+ + + + + +
-
+
+ + ) } diff --git a/src/assets/images/cimf_logo.png b/src/assets/images/cimf_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3c73f4584f068d7b02007022993c36c4128af33b GIT binary patch literal 14172 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 C{#;K0 literal 0 HcmV?d00001 diff --git a/src/components/AboutMe.jsx b/src/components/AboutMe.jsx index bd2cbb1..40baff4 100644 --- a/src/components/AboutMe.jsx +++ b/src/components/AboutMe.jsx @@ -1,11 +1,11 @@ -import profileImg from '../assets/images/cropped-profile.jpg' -import resumePdf from '../assets/resume.pdf' - import { FaLinkedin, FaGithub, FaFileDownload } from 'react-icons/fa' -import SectionArrow from "./SectionArrow.jsx"; - +import SectionArrow from "./SectionArrow.jsx" +import { useLanguage } from '../contexts/useLanguage.js' +import { getText } from '../utils/translationHelpers.js' +import { aboutData } from './data/about.js' function AboutMe() { + const { language } = useLanguage() return (
@@ -13,12 +13,14 @@ function AboutMe() {
-

Luan Tran

+

+ {aboutData.name} +

- I'm a Master's student in Applied Computer Science with industry experience in automation and web development at Broadsign, Ericsson, and Matrox. My projects include deploying automated CI/CD workflows, multimodal medical imaging applications, and automated language proficiency assessment systems. + {getText(aboutData.description1, language)}

- I'm currently researching LLM-based agents for linguistic education and seeking opportunities in software development and machine learning. + {getText(aboutData.description2, language)}

    @@ -35,18 +37,18 @@ function AboutMe() {
- Download CV + {getText(aboutData.downloadCV, language)}
Luan Tran profile picture diff --git a/src/components/Education.jsx b/src/components/Education.jsx index 1bc8953..ea18a46 100644 --- a/src/components/Education.jsx +++ b/src/components/Education.jsx @@ -1,8 +1,10 @@ import SectionArrow from "./SectionArrow.jsx"; -import {educationData} from "./data/education.js" +import { educationData } from "./data/education.js"; +import { useLanguage } from '../contexts/useLanguage.js'; +import { getText } from '../utils/translationHelpers'; -const EducationItem = ({ data, index }) => { - const { university, logo, degree, date, description, gpa } = data +const EducationItem = ({ data, index, language }) => { + const { university, logo, degree, date } = data; return (
0 ? 'my-[5px] sm:-mt-10 md:-mt-12 lg:-mt-14 xl:-mt-16' : 'my-[5px] sm:my-[3px]'} flex w-full sm:w-1/2 justify-start sm:justify-end sm:pr-[22px] sm:odd:justify-start sm:odd:self-end sm:odd:pl-[22px] sm:odd:pr-0 md:pr-[30px] md:odd:pl-[30px]`}> @@ -29,46 +31,68 @@ const EducationItem = ({ data, index }) => { sm:group-odd:after:border-r-0 sm:group-odd:after:border-b-0"> - {/* University Logo - responsive sizing */} + {/* University Logo */} {`${university} {/* Content */} -
-

{university}

-

{degree}

- +
+

+ {getText(university, language)} +

+

+ {getText(degree, language)} +

+
- {/* Timeline dot - hidden on mobile, visible on tablet+ */} + {/* Timeline dot */}
- ) + ); } function Education() { + const { language } = useLanguage(); + + const sectionTitle = { + en: "Education", + fr: "Éducation" + }; + return (

- Education + {getText(sectionTitle, language)}

- {/* Timeline - hidden on mobile via opacity, visible on tablet+ */} + {/* Timeline */}
{educationData.map((data, idx) => ( - + ))}
- ) + ); } export default Education \ No newline at end of file diff --git a/src/components/Experience.jsx b/src/components/Experience.jsx index f759052..a694f5b 100644 --- a/src/components/Experience.jsx +++ b/src/components/Experience.jsx @@ -1,8 +1,9 @@ import SectionArrow from "./SectionArrow.jsx"; -import {experienceData} from "./data/experience.js"; - -const ExperienceItem = ({ data }) => { +import { experienceData } from "./data/experience.js"; +import { useLanguage } from '../contexts/useLanguage.js'; +import { getText, getArray } from '../utils/translationHelpers'; +const ExperienceItem = ({ data, language }) => { return (
@@ -11,17 +12,21 @@ const ExperienceItem = ({ data }) => {
{data.hash} -

{data.title}

+

+ {getText(data.title, language)} +

{data.company}

- {data.period} + + {getText(data.period, language)} +
- {data.achievements.map((achievementPoint, index) => ( + {getArray(data.achievements, language).map((achievementPoint, index) => (
+{achievementPoint}
@@ -29,7 +34,9 @@ const ExperienceItem = ({ data }) => {
{data.tech.map((techPoint, index) => ( - {techPoint} + + {techPoint} + ))}
@@ -38,20 +45,26 @@ const ExperienceItem = ({ data }) => { } function Experience() { + const { language } = useLanguage(); + + const sectionTitle = { + en: "Work Experience", + fr: "Expérience Professionnelle" + }; + return (

- Work Experience + {getText(sectionTitle, language)}

-
+
{experienceData.map((experience) => ( - + ))}
diff --git a/src/components/LanguageSwitcher.jsx b/src/components/LanguageSwitcher.jsx new file mode 100644 index 0000000..fad2527 --- /dev/null +++ b/src/components/LanguageSwitcher.jsx @@ -0,0 +1,59 @@ +import { useLanguage } from '../contexts/useLanguage'; + +function LanguageSwitcher() { + const { language, setLanguage } = useLanguage(); + + return ( +
+ {/* Mobile */} +
+ + +
+ + {/* Desktop */} +
+ + +
+
+ ); +} + +export default LanguageSwitcher; diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 4e337b3..f69b6fe 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -1,9 +1,13 @@ import { useState, useEffect } from 'react' import { FaUser, FaLightbulb, FaGraduationCap, FaBriefcase, FaFolderOpen, FaBars, FaTimes } from 'react-icons/fa' +import LanguageSwitcher from './LanguageSwitcher' +import { useLanguage } from '../contexts/useLanguage.js'; +import { getText } from '../utils/translationHelpers' function Navbar() { const [activeSection, setActiveSection] = useState('about') const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false) + const { language } = useLanguage() useEffect(() => { const handleScroll = () => { @@ -29,32 +33,46 @@ function Navbar() { setIsMobileMenuOpen(false) } + // Navigation labels with translations + const navLabels = { + about: { en: 'About', fr: 'À propos' }, + skills: { en: 'Skills', fr: 'Compétences' }, + education: { en: 'Education', fr: 'Éducation' }, + experience: { en: 'Experience', fr: 'Expérience' }, + projects: { en: 'Projects', fr: 'Projets' } + } + const navItems = [ - { id: 'about', icon: FaUser, label: 'About' }, - { id: 'skills', icon: FaLightbulb, label: 'Skills' }, - { id: 'education', icon: FaGraduationCap, label: 'Education' }, - { id: 'experience', icon: FaBriefcase, label: 'Experience' }, - { id: 'projects', icon: FaFolderOpen, label: 'Projects' } + { id: 'about', icon: FaUser, label: navLabels.about }, + { id: 'skills', icon: FaLightbulb, label: navLabels.skills }, + { id: 'education', icon: FaGraduationCap, label: navLabels.education }, + { id: 'experience', icon: FaBriefcase, label: navLabels.experience }, + { id: 'projects', icon: FaFolderOpen, label: navLabels.projects } ] return ( <> - {/* Mobile Top Bar (visible on screens smaller than md) */} -
+ {/* Mobile Top Bar */} +
- Portfolio - + LT + + {/* Mobile: Language Switcher + Menu Button */} +
+ + +
{/* Mobile Dropdown Menu */} {isMobileMenuOpen && ( -
+
    {navItems.map(({ id, icon: Icon, label }) => (
  • @@ -68,7 +86,7 @@ function Navbar() { }`} > - {label} + {getText(label, language)}
  • ))} @@ -77,15 +95,17 @@ function Navbar() { )}
- {/* Desktop Side Navigation - fixed positioning within the sidebar column */} -
-
    + {/* Desktop Side Navigation */} + ) diff --git a/src/components/Projects.jsx b/src/components/Projects.jsx index 8949ae2..fd74e02 100644 --- a/src/components/Projects.jsx +++ b/src/components/Projects.jsx @@ -1,10 +1,13 @@ -import { ExternalLink, Code } from 'lucide-react'; +import { ExternalLink } from 'lucide-react'; import { projectData } from './data/projects.js'; -import {useState} from "react"; +import { useState } from "react"; +import { useLanguage } from '../contexts/useLanguage.js'; +import { getText, getArray } from '../utils/translationHelpers'; -const ProjectCard = ({ project }) => { +const ProjectCard = ({ project, language }) => { const [showGif, setShowGif] = useState(false); const [imageLoaded, setImageLoaded] = useState(false); + return (
    {/* Animated Glow Border */} @@ -19,17 +22,15 @@ const ProjectCard = ({ project }) => { onMouseEnter={() => setShowGif(true)} onMouseLeave={() => setShowGif(false)} > - {/* Placeholder - shows while image loads */} {!imageLoaded && (
    Loading...
    )} - {/* Actual Image */} {project.title} setImageLoaded(true)} @@ -40,44 +41,40 @@ const ProjectCard = ({ project }) => {
    {/* Card Header */} -
    - - {/* Category Badge */} -
    - {project.category} -
    - - {/* Title + Description */} -
    -

    - {project.title} -

    -

    - {project.description} -

    -
    +
    + {/* Category Badge */} +
    + {getText(project.category, language)} +
    + {/* Title + Description */} +
    +

    + {getText(project.title, language)} +

    +

    + {getText(project.description, language)} +

    +
    {/* Card Body */} -
    - + ); }; function Projects() { + const { language } = useLanguage(); + + const sectionTitle = { + en: "Projects", + fr: "Projets" + }; + return ( -
    +

    - Projects + {getText(sectionTitle, language)}

    -
    +
    {projectData.map((project, index) => ( - + ))}
    - ) + ); } export default Projects \ No newline at end of file diff --git a/src/components/Skills.jsx b/src/components/Skills.jsx index 4ff1e1c..8f21f55 100644 --- a/src/components/Skills.jsx +++ b/src/components/Skills.jsx @@ -1,26 +1,32 @@ import SectionArrow from "./SectionArrow.jsx"; -import {skillsData} from "./data/skills.js"; +import { skillsData } from "./data/skills.js"; import { iconMap } from "../utils/iconMap.js"; +import { useLanguage } from '../contexts/useLanguage.js'; +import { getText } from '../utils/translationHelpers'; function Skills() { + const { language } = useLanguage(); + + const sectionTitle = { + en: "Skills", + fr: "Compétences" + }; + const renderDomainCard = (domain) => { const MainIcon = iconMap[domain.icon]; return ( -
    - {/* Mobile: Stack vertically, Tablet+: Side by side */} -
    +
    +
    - {/* Domain Name with Icon - keep horizontal on mobile, vertical on desktop */} + {/* Domain Name with Icon */}
    - {/* ONLY DOMAIN ICON resized: Mobile: 3xl, Small: 4xl, Tablet: 3xl, Desktop: 5xl */} - {/* ONLY DOMAIN NAME resized: Mobile: base, Small: lg, Tablet: base, Desktop: xl */}

    - {domain.name} + {getText(domain.name, language)}

    - {/* Related Skills Icons - KEEP ORIGINAL PROGRESSIVE SIZING */} + {/* Related Skills Icons */}
    {domain.relatedSkills.map((skill) => { const SkillIcon = iconMap[skill.icon]; @@ -28,7 +34,6 @@ function Skills() { return (
    - {/* Skills keep progressive sizing: 2xl -> 3xl -> 4xl */}

    @@ -50,7 +55,7 @@ function Skills() {

    - Skills + {getText(sectionTitle, language)}

    diff --git a/src/components/data/about.js b/src/components/data/about.js new file mode 100644 index 0000000..bcd999c --- /dev/null +++ b/src/components/data/about.js @@ -0,0 +1,26 @@ +import resumePdf from '../../assets/resume.pdf' +import profileImg from '../../assets/images/cropped-profile.jpg' + +export const aboutData = { + name: "Luan Tran", // Name doesn't need translation + description1: { + en: "I'm a Master's student in Applied Computer Science with industry experience in automation and web development " + + "at Broadsign, Ericsson, and Matrox. My projects include deploying automated CI/CD workflows," + + " multimodal medical imaging applications, and automated language proficiency assessment systems.", + fr: "Je suis étudiant en maîtrise en informatique appliquée avec une expérience industrielle en automatisation et développement web " + + "chez Broadsign, Ericsson et Matrox. Mes projets incluent le déploiement de workflows CI/CD automatisés," + + " des applications d'imagerie médicale multimodale et des systèmes d'évaluation automatique de compétence linguistique." + }, + description2: { + en: "I'm currently researching LLM-based agents for linguistic education and seeking opportunities in " + + "software development and machine learning.", + fr: "Je fais actuellement de la recherche sur les agents basés sur les LLM pour l'enseignement linguistique et je recherche des opportunités en " + + "développement logiciel et apprentissage automatique." + }, + downloadCV: { + en: "Download CV", + fr: "Télécharger CV" + }, + resume: resumePdf, + image: profileImg +} \ No newline at end of file diff --git a/src/components/data/education.js b/src/components/data/education.js index a37eb8c..020afd6 100644 --- a/src/components/data/education.js +++ b/src/components/data/education.js @@ -5,21 +5,39 @@ import cimfLogo from '../../assets/images/cimf_logo.png' export const educationData = [ { - university: 'Concordia University', + university: { + en: 'Concordia University', + fr: 'Université Concordia' + }, date: '2025 - 2027', logo: concordiaLogo, - degree: 'Master\'s of Applied Computer Science' + degree: { + en: "Master's of Applied Computer Science", + fr: "Maîtrise en informatique appliquée" + } }, { - university: 'McGill University', + university: { + en: 'McGill University', + fr: 'Université McGill' + }, date: '2014 - 2019', logo: mcgillLogo, - degree: 'Bachelors\'s of Computer Engineering' + degree: { + en: "Bachelor's of Computer Engineering", + fr: "Baccalauréat en génie informatique" + } }, { - university: 'Collège Marie de France', + university: { + en: 'Collège Marie de France', + fr: 'Collège Marie de France' + }, date: '2012-2014', logo: cimfLogo, - degree: 'French Baccalaureate - Highest Honors' + degree: { + en: 'French Baccalaureate - Highest Honors', + fr: 'Baccalauréat français - Mention très bien' + } } ] \ No newline at end of file diff --git a/src/components/data/experience.js b/src/components/data/experience.js index 69e453a..66df1a4 100644 --- a/src/components/data/experience.js +++ b/src/components/data/experience.js @@ -1,56 +1,81 @@ export const experienceData = [ { - hash: "a3f52b9", // or auto-generate - title: "Web Automation Developer Intern", - company: "Broadsign", - period: "Jan - May 2019", - // description: "Brief overview...", - achievements: [ - "Designed a new automation framework that accurately reproduce a live environment against which integration\n" + - "and system tests can be run", - "Automated the clean deployment and database population of a web server using Docker and Bitbucket Pipelines", - "Developed a Python REST tool library which developers could use to write their unit and integration tests" - ], - // projects: [ // optional array - // { name: "Dashboard Redesign", link: "..." } - // ], - // impact: "40% increase in engagement", // optional + hash: "a3f52b9", + title: { + en: "Web Automation Developer Intern", + fr: "Stagiaire développeur en automatisation web" + }, + company: "Broadsign", // Company name stays the same + period: { + en: "Jan - May 2019", + fr: "Jan - Mai 2019" + }, + achievements: { + en: [ + "Designed a new automation framework that accurately reproduce a live environment against which integration and system tests can be run", + "Automated the clean deployment and database population of a web server using Docker and Bitbucket Pipelines", + "Developed a Python REST tool library which developers could use to write their unit and integration tests" + ], + fr: [ + "Conçu un nouveau framework d'automatisation qui reproduit fidèlement un environnement réel pour exécuter des tests d'intégration et système", + "Automatisé le déploiement propre et le peuplement de base de données d'un serveur web avec Docker et Bitbucket Pipelines", + "Développé une bibliothèque d'outils REST en Python que les développeurs peuvent utiliser pour écrire leurs tests unitaires et d'intégration" + ] + }, tech: ["Python", "Docker", "Bitbucket"], }, { - hash: "7c2d81e", // or auto-generate - title: "Software Developer Intern", + hash: "7c2d81e", + title: { + en: "Software Developer Intern", + fr: "Stagiaire développeur logiciel" + }, company: "Ericsson", - period: "May - Dec 2018", - // description: "Brief overview...", - achievements: [ - "Automated software installation processes to decrease runtime by 15% using Ansible", - "Implemented a build certification process in custom Concourse to improve testing team’s efficiency by 75%", - "Developed automated Oracle backup and restore solution to decrease Concourse jobs’ runtime by 20%", - "Proposed and implemented new dispatch process for CI infrastructure by integrating Mantis API to Concourse" - ], - // projects: [ // optional array - // { name: "Dashboard Redesign", link: "..." } - // ], - // impact: "40% increase in engagement", // optional + period: { + en: "May - Dec 2018", + fr: "Mai - Déc 2018" + }, + achievements: { + en: [ + "Automated software installation processes to decrease runtime by 15% using Ansible", + "Implemented a build certification process in custom Concourse to improve testing team's efficiency by 75%", + "Developed automated Oracle backup and restore solution to decrease Concourse jobs' runtime by 20%", + "Proposed and implemented new dispatch process for CI infrastructure by integrating Mantis API to Concourse" + ], + fr: [ + "Automatisé les processus d'installation logicielle pour réduire le temps d'exécution de 15% avec Ansible", + "Implémenté un processus de certification de build dans Concourse personnalisé pour améliorer l'efficacité de l'équipe de test de 75%", + "Développé une solution automatisée de sauvegarde et restauration Oracle pour réduire le temps d'exécution des jobs Concourse de 20%", + "Proposé et implémenté un nouveau processus de dispatch pour l'infrastructure CI en intégrant l'API Mantis à Concourse" + ] + }, tech: ["Python", "Mantis", "Concourse", "Ansible"], }, { - hash: "1f9a3bc", // or auto-generate - title: "Automation SQA Intern", + hash: "1f9a3bc", + title: { + en: "Automation SQA Intern", + fr: "Stagiaire SQA en automatisation" + }, company: "Matrox", - period: "Jan - Aug 2017", - // description: "Brief overview...", - achievements: [ - "Created a new automated testing workflow to improve reduce weekly manual testing by 10 hours", - "Integrated Silktest, Jenkins and Testrail API using Python scripts to 100% automate log parsing, reporting and archiving", - "Set up, configured and managed Jenkins servers and slaves", - "Developed Python and 4Test test suites to perform functional and performance tests" - ], - // projects: [ // optional array - // { name: "Dashboard Redesign", link: "..." } - // ], - // impact: "40% increase in engagement", // optional + period: { + en: "Jan - Aug 2017", + fr: "Jan - Août 2017" + }, + achievements: { + en: [ + "Created a new automated testing workflow to improve reduce weekly manual testing by 10 hours", + "Integrated Silktest, Jenkins and Testrail API using Python scripts to 100% automate log parsing, reporting and archiving", + "Set up, configured and managed Jenkins servers and slaves", + "Developed Python and 4Test test suites to perform functional and performance tests" + ], + fr: [ + "Créé un nouveau workflow de tests automatisés pour réduire les tests manuels hebdomadaires de 10 heures", + "Intégré Silktest, Jenkins et l'API Testrail avec des scripts Python pour automatiser à 100% l'analyse de logs, les rapports et l'archivage", + "Configuré et géré des serveurs Jenkins et leurs agents", + "Développé des suites de tests Python et 4Test pour effectuer des tests fonctionnels et de performance" + ] + }, tech: ["Python", "Bash", "Silktest", "Jenkins", "Testrail"], } ] \ No newline at end of file diff --git a/src/components/data/skills.js b/src/components/data/skills.js index c219102..08fcb80 100644 --- a/src/components/data/skills.js +++ b/src/components/data/skills.js @@ -1,204 +1,98 @@ export const skillsData = { + // These don't need translation - they're universal languages: [ - { - name: "Python", - icon: "FaPython" - }, - { - name: "Java", - icon: "FaJava" - }, - { - name: "Bash", - icon: "FaTerminal" - }, - { - name: "JavaScript", - icon: "FaJs" - } + { name: "Python", icon: "FaPython" }, + { name: "Java", icon: "FaJava" }, + { name: "Bash", icon: "FaTerminal" }, + { name: "JavaScript", icon: "FaJs" } ], environments: [ - { - name: "Windows", - icon: "FaWindows" - }, - { - name: "Linux", - icon: "FaLinux" - }, - { - name: "Ubuntu", - icon: "FaUbuntu" - } + { name: "Windows", icon: "FaWindows" }, + { name: "Linux", icon: "FaLinux" }, + { name: "Ubuntu", icon: "FaUbuntu" } ], virtualization: [ - { - name: "VirtualBox", - icon: "SiVirtualbox" - }, - { - name: "Docker", - icon: "FaDocker" - }, - { - name: "Hyper-V", - icon: "FaTerminal" - } + { name: "VirtualBox", icon: "SiVirtualbox" }, + { name: "Docker", icon: "FaDocker" }, + { name: "Hyper-V", icon: "FaTerminal" } ], frameworks: [ - { - name: "Flask", - icon: "SiFlask" - }, - { - name: "Node.js", - icon: "FaNode" - }, - { - name: "WordPress", - icon: "FaWordpress" - }, - { - name: "React", - icon: "FaReact" - }, - { - name: "Google Cloud", - icon: "FaGoogle" - }, - { - name: "Play", - icon: "FaPlay" - } + { name: "Flask", icon: "SiFlask" }, + { name: "Node.js", icon: "FaNode" }, + { name: "WordPress", icon: "FaWordpress" }, + { name: "React", icon: "FaReact" }, + { name: "Google Cloud", icon: "FaGoogle" }, + { name: "Play", icon: "FaPlay" } ], "CI/CD": [ - { - name: "Git", - icon: "FaGitAlt" - }, - { - name: "Github", - icon: "FaGithub" - }, - { - name: "Jenkins", - icon: "FaJenkins" - }, - { - name: "Bitbucket", - icon: "FaBitbucket" - }, - { - name: "JIRA", - icon: "FaJira" - }, - { - name: "Concourse", - icon: "SiConcourse" - } + { name: "Git", icon: "FaGitAlt" }, + { name: "Github", icon: "FaGithub" }, + { name: "Jenkins", icon: "FaJenkins" }, + { name: "Bitbucket", icon: "FaBitbucket" }, + { name: "JIRA", icon: "FaJira" }, + { name: "Concourse", icon: "SiConcourse" } ], "Machine Learning": [ - { - name: "PyTorch", - icon: "SiPytorch" - }, - { - name: "Scikit-learn", - icon: "SiScikitlearn" - }, - { - name: "Pandas", - icon: "SiPandas" - }, - { - name: "NumPy", - icon: "SiNumpy" - }, - { - name: "Jupyter", - icon: "SiJupyter" - } + { name: "PyTorch", icon: "SiPytorch" }, + { name: "Scikit-learn", icon: "SiScikitlearn" }, + { name: "Pandas", icon: "SiPandas" }, + { name: "NumPy", icon: "SiNumpy" }, + { name: "Jupyter", icon: "SiJupyter" } ], + // Domain sections need translation domains: [ { - name: "Web Development", - description: "Full-stack applications with modern frameworks and RESTful APIs", + name: { + en: "Web Development", + fr: "Développement Web" + }, + description: { + en: "Full-stack applications with modern frameworks and RESTful APIs", + fr: "Applications full-stack avec des frameworks modernes et des API RESTful" + }, icon: "FaCode", relatedSkills: [ - { - icon: "FaReact", - displayName: "React" - }, - { - icon: "FaNode", - displayName: "Node.js" - }, - { - icon: "SiFlask", - displayName: "Flask" - }, - { - icon: "FaJs", - displayName: "JavaScript" - }, - { - icon: "FaPython", - displayName: "Python" - } + { icon: "FaReact", displayName: "React" }, + { icon: "FaNode", displayName: "Node.js" }, + { icon: "SiFlask", displayName: "Flask" }, + { icon: "FaJs", displayName: "JavaScript" }, + { icon: "FaPython", displayName: "Python" } ] }, { - name: "Machine Learning", - description: "ML models, data analysis, and NLP solutions", + name: { + en: "Machine Learning", + fr: "Apprentissage Automatique" + }, + description: { + en: "ML models, data analysis, and NLP solutions", + fr: "Modèles ML, analyse de données et solutions NLP" + }, icon: "FaBrain", relatedSkills: [ - { - icon: "SiPytorch", - displayName: "PyTorch" - }, - { - icon: "SiScikitlearn", - displayName: "Scikit-learn" - }, - { - icon: "SiPandas", - displayName: "Pandas" - }, - { - icon: "SiNumpy", - displayName: "NumPy" - }, - { - icon: "SiJupyter", - displayName: "Jupyter" - } + { icon: "SiPytorch", displayName: "PyTorch" }, + { icon: "SiScikitlearn", displayName: "Scikit-learn" }, + { icon: "SiPandas", displayName: "Pandas" }, + { icon: "SiNumpy", displayName: "NumPy" }, + { icon: "SiJupyter", displayName: "Jupyter" } ] }, { - name: "DevOps & Cloud", - description: "CI/CD pipelines, containerization, and cloud infrastructure", + name: { + en: "DevOps & Cloud", + fr: "DevOps et Cloud" + }, + description: { + en: "CI/CD pipelines, containerization, and cloud infrastructure", + fr: "Pipelines CI/CD, conteneurisation et infrastructure cloud" + }, icon: "FaCloud", relatedSkills: [ - { - icon: "FaDocker", - displayName: "Docker" - }, - { - icon: "FaGitAlt", - displayName: "Git" - }, - { - icon: "FaJenkins", - displayName: "Jenkins" - }, - { - icon: "FaGoogle", - displayName: "Google Cloud" - }, - { - icon: "SiConcourse", - displayName: "Concourse" - } + { icon: "FaDocker", displayName: "Docker" }, + { icon: "FaGitAlt", displayName: "Git" }, + { icon: "FaJenkins", displayName: "Jenkins" }, + { icon: "FaGoogle", displayName: "Google Cloud" }, + { icon: "SiConcourse", displayName: "Concourse" } ] } ] diff --git a/src/contexts/LanguageContext.js b/src/contexts/LanguageContext.js new file mode 100644 index 0000000..6fd746b --- /dev/null +++ b/src/contexts/LanguageContext.js @@ -0,0 +1,3 @@ +import { createContext } from 'react'; + +export const LanguageContext = createContext(null); diff --git a/src/contexts/LanguageProvider.jsx b/src/contexts/LanguageProvider.jsx new file mode 100644 index 0000000..85745cf --- /dev/null +++ b/src/contexts/LanguageProvider.jsx @@ -0,0 +1,31 @@ +import React, { useState, useEffect } from 'react'; +import { LanguageContext } from './LanguageContext'; + +export const LanguageProvider = ({ children }) => { + const [language, setLanguage] = useState(() => { + const savedLang = localStorage.getItem('preferred-language'); + return savedLang || 'en'; + }); + + useEffect(() => { + localStorage.setItem('preferred-language', language); + }, [language]); + + const toggleLanguage = () => { + setLanguage(prev => (prev === 'en' ? 'fr' : 'en')); + }; + + const value = { + language, + setLanguage, + toggleLanguage, + isEnglish: language === 'en', + isFrench: language === 'fr', + }; + + return ( + + {children} + + ); +}; diff --git a/src/contexts/useLanguage.js b/src/contexts/useLanguage.js new file mode 100644 index 0000000..2bb3aa3 --- /dev/null +++ b/src/contexts/useLanguage.js @@ -0,0 +1,10 @@ +import {useContext} from "react"; +import {LanguageContext} from "./LanguageContext.js"; + +export const useLanguage = () => { + const context = useContext(LanguageContext); + if (!context) { + throw new Error('useLanguage must be used within a LanguageProvider'); + } + return context; +}; \ No newline at end of file diff --git a/src/utils/translationHelpers.js b/src/utils/translationHelpers.js new file mode 100644 index 0000000..a5fd8b5 --- /dev/null +++ b/src/utils/translationHelpers.js @@ -0,0 +1,39 @@ +/** + * Helper function to get translated text from bilingual data + * @param {string|object} value - Either a string or an object with 'en' and 'fr' keys + * @param {string} language - Current language ('en' or 'fr') + * @returns {string} - The translated text + */ +export const getText = (value, language = 'en') => { + // If it's already a string (not translated), return as is + if (typeof value === 'string') { + return value; + } + + // If it's an object with language keys, return the appropriate one + if (value && typeof value === 'object') { + return value[language] || value.en || ''; + } + + return ''; +}; + +/** + * Helper function to get translated array from bilingual data + * @param {array|object} value - Either an array or an object with 'en' and 'fr' keys containing arrays + * @param {string} language - Current language ('en' or 'fr') + * @returns {array} - The translated array + */ +export const getArray = (value, language = 'en') => { + // If it's already an array (not translated), return as is + if (Array.isArray(value)) { + return value; + } + + // If it's an object with language keys, return the appropriate array + if (value && typeof value === 'object') { + return value[language] || value.en || []; + } + + return []; +}; \ No newline at end of file