Files
portfolio/src/components/App.tsx
Louis a55a7fd58a
All checks were successful
Deploy React portfolio / deploy (push) Successful in 31s
Adds CI/CD pipeline for portfolio deployment
Sets up a CI/CD pipeline to automate the deployment of the portfolio website to a web server.

The pipeline includes steps for:
- Checking out the code
- Setting up Node.js
- Installing dependencies
- Building the application
- Deploying the built application to the web server via SSH.

This automated deployment ensures that the portfolio is always up-to-date with the latest changes.

feat: update to use correct ssh

Updates deployment script to use dist folder

Modifies the deployment script to correctly reference the `dist` directory instead of the `build` directory.

This ensures that the correct files are deployed to the web server.

Automates deployment workflow using Gitea Actions

Sets up a Gitea Actions workflow to automatically build and deploy the React portfolio website to the production server.

This includes:
- Using a containerized environment for consistent builds.
- Installing dependencies and building the project.
- Deploying the built files to the web server via SSH and rsync.
- Reloading the web server configuration to apply changes.

Also, this change updates the .gitignore file to exclude the 'dist' directory.

edit to use ssh secret instead

fix: english translation for education

fix: correct contact mail

fix: store system choice for theme
2025-10-29 13:04:13 +01:00

147 lines
6.5 KiB
TypeScript

import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next';
import Hero from './Hero';
import About from './About';
import Experience from './Experience/Experience';
import Skills from './Skills';
import Education from './Education';
import Contact from './Contact';
import { ThemeMenu, ThemeType, ThemeSelected } from './ThemeMenu';
function App() {
const { t, i18n } = useTranslation();
const [theme, setTheme] = useState<ThemeType>(() => {
const savedTheme = localStorage.getItem('theme');
return (savedTheme as ThemeType) || 'dark';
});
const [themeSelected, setThemeSelected] = useState<ThemeSelected>(() => {
const savedThemeSelected = localStorage.getItem('themeSelected');
return (savedThemeSelected as ThemeSelected) || 'system';
});
// Charger la langue sauvegardée au démarrage
useEffect(() => {
const savedLanguage = localStorage.getItem('language');
if (savedLanguage && (savedLanguage === 'fr' || savedLanguage === 'en')) {
i18n.changeLanguage(savedLanguage);
}
}, [i18n]);
useEffect(() => {
if (themeSelected === 'system') {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => {
const systemTheme = mediaQuery.matches ? 'dark' : 'light';
setTheme(systemTheme);
document.documentElement.className = `theme-${systemTheme}`;
};
handleChange();
localStorage.setItem('themeSelected', themeSelected);
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
} else {
setTheme(themeSelected);
document.documentElement.className = `theme-${themeSelected}`;
}
// Sauvegarder le thème sélectionné
localStorage.setItem('themeSelected', themeSelected);
localStorage.setItem('theme', theme);
}, [themeSelected, theme]);
const toggleLanguage = () => {
const newLang = i18n.language === 'fr' ? 'en' : 'fr';
i18n.changeLanguage(newLang);
localStorage.setItem('language', newLang);
};
const handleThemeChange = (newTheme: ThemeSelected) => {
setThemeSelected(newTheme);
};
const scrollToSection = (e: React.MouseEvent<HTMLAnchorElement>, id: string) => {
e.preventDefault();
const section = document.getElementById(id);
if (section) {
section.scrollIntoView({ behavior: 'smooth' });
window.history.pushState(null, '', `#${id}`);
}
}
return (
<div className="min-h-screen text-secondary bg-primary/80 transition-colors duration-100">
{/* Background Blobs */}
<div className="background-blobs">
<div className="blob blob-1"></div>
<div className="blob blob-2"></div>
<div className="blob blob-3"></div>
</div>
{/* Header */}
<header className='fixed top-0 left-0 right-0 z-50 flex justify-between items-center gap-4 text-sm w-full px-8 py-6 bg-primary/80 backdrop-blur-md border-b border-secondary/10'>
<h1 className='text-2xl font-bold'>
Louis EMARD
</h1>
<nav className='flex gap-6 h-full justify-center items-center'>
<a href='#about' onClick={(e) => scrollToSection(e, 'about')} className='hidden md:block hover:text-accent transition-colors cursor-pointer'>{t('nav.about')}</a>
<a href='#experience' onClick={(e) => scrollToSection(e, 'experience')} className='hidden md:block hover:text-accent transition-colors cursor-pointer'>{t('nav.projects')}</a>
<a href='#skills' onClick={(e) => scrollToSection(e, 'skills')} className='hidden md:block hover:text-accent transition-colors cursor-pointer'>{t('nav.skills')}</a>
<a href='#education' onClick={(e) => scrollToSection(e, 'education')} className='hidden md:block hover:text-accent transition-colors cursor-pointer'>{t('nav.education')}</a>
<a href='#contact' onClick={(e) => scrollToSection(e, 'contact')} className='hidden md:block hover:text-accent transition-colors cursor-pointer'>{t('nav.contact')}</a>
{/* Language Toggle */}
<button
onClick={toggleLanguage}
className='px-3 py-1 border border-secondary/20 rounded hover:border-accent hover:text-accent transition-all'
>
{i18n.language.toUpperCase()}
</button>
{/* Theme Menu */}
<ThemeMenu
theme={theme}
themeSelected={themeSelected}
onThemeChange={handleThemeChange}
/>
</nav>
</header>
{/* Main Content */}
<main>
<Hero />
<About />
<Experience />
<Skills />
<Education />
<Contact />
</main>
{/* Footer */}
<footer className='py-8 border-t border-secondary/10 bg-secondary/5'>
<div className='flex gap-6 justify-center items-center text-sm'>
<a href='https://git.louisemard.dev' target='_blank' rel='noopener noreferrer' className='hover:text-accent transition-colors'>
Gitea
</a>
<a href='https://github.com/LouisDrame' target='_blank' rel='noopener noreferrer' className='hover:text-accent transition-colors'>
GitHub
</a>
<a href='https://www.linkedin.com/in/louis-emard-9b6a931a2/' target='_blank' rel='noopener noreferrer' className='hover:text-accent transition-colors'>
LinkedIn
</a>
<a href='https://www.collective.work/profile/louis-emard' target='_blank' rel='noopener noreferrer' className='hover:text-accent transition-colors'>
Collective
</a>
</div>
<p className='text-center mt-4 text-secondary/50 text-xs'>
© {new Date().getFullYear()} Louis EMARD. All rights reserved.
</p>
</footer>
</div>
)
}
export default App