Merge pull request 'dev' (#26) from dev into main

Reviewed-on: #26
This commit is contained in:
Giovanni-Josserand 2025-09-06 09:48:46 +00:00
commit fe2abff5e5
46 changed files with 779 additions and 306 deletions

View File

@ -1,15 +1,12 @@
# To do
## For V1 :
- Revoir les titres de section (+espace au dessus des titres)
## For V2 :
- Mettre la base de donnée en place
- Refaire en conséquence les choses nécessaires
## For V3 :
- Faire une page pour lister tous les projets
- Refaire les images qui vont pas
- Faire une page explicative par projet
- Faire du responsive
## Plus tard
- Régler problème de scoll entre page
- Refaire la section des skills
## Usefull commands

56
package-lock.json generated
View File

@ -9,7 +9,8 @@
"version": "0.0.0",
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
"react-dom": "^19.1.0",
"react-router-dom": "^7.8.2"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
@ -1600,6 +1601,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -2471,6 +2481,44 @@
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.2.tgz",
"integrity": "sha512-7M2fR1JbIZ/jFWqelpvSZx+7vd7UlBTfdZqf6OSdF9g6+sfdqJDAWcak6ervbHph200ePlu+7G8LdoiC3ReyAQ==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.2.tgz",
"integrity": "sha512-Z4VM5mKDipal2jQ385H6UBhiiEDlnJPx6jyWsTYoZQdl5TrjxEV2a9yl3Fi60NBJxYzOTGTTHXPi0pdizvTwow==",
"license": "MIT",
"dependencies": {
"react-router": "7.8.2"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@ -2544,6 +2592,12 @@
"semver": "bin/semver.js"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@ -11,7 +11,8 @@
},
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
"react-dom": "^19.1.0",
"react-router-dom": "^7.8.2"
},
"devDependencies": {
"@eslint/js": "^9.25.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 643 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 923 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 800 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

View File

@ -1,20 +1,15 @@
import './App.css'
import Home from './components/Home.jsx'
import Experiences from './components/Experiences.jsx'
import Projects from './components/Projects.jsx'
import Skills from './components/Skills.jsx'
import Footer from './components/Footer.jsx'
import { Routes, Route, Link } from 'react-router-dom';
import HomePage from './pages/HomePage.jsx';
import ProjectsPage from './pages/ProjectsPage';
function App() {
return (
<div className="App">
<Home/>
<Experiences />
<Projects />
<Skills />
<Footer />
</div>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/projects" element={<ProjectsPage />} />
</Routes>
)
}

View File

@ -1,5 +1,5 @@
import '../styles/Experiences.css';
import SingleExperience from './SingleExperience';
import './Experiences.css';
import SingleExperience from '../SingleExperience/SingleExperience.jsx';
import React, {useEffect, useState} from "react";
function Experiences() {
@ -31,7 +31,7 @@ function Experiences() {
}, []);
if (error) {
return <div>Erreur lors de la récupération des données : {error}</div>;
return <div>Error retrieving data: {error}</div>;
}
return (

View File

@ -0,0 +1,127 @@
.filter-btn {
position: absolute;
top: 2em;
right: 20px;
border: none;
background: none;
padding: 14px;
cursor: pointer;
z-index: 10;
transition: background-color 0.3s ease;
}
.filter-btn svg {
color: var(--text-color);
transition: all 0.3s ease;
}
.filter-btn svg:hover {
color: var(--title-color);
transform: translateY(-2px);
}
.filter-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(5px);
z-index: 100;
}
.filter-sidebar {
position: fixed;
top: 0;
right: -100%;
width: 100%;
max-width: 350px;
height: 100%;
background: rgba(42, 42, 42, 0.5);
backdrop-filter: blur(15px);
border-left: solid rgba(100,100,100,0.5) 1px;
z-index: 101;
display: flex;
flex-direction: column;
transition: right 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.filter-sidebar.visible {
right: 0;
}
.filter-sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid rgba(100, 100, 100, 0.5);
}
.close-filter-btn {
background: none;
border: none;
color: var(--text-color);
cursor: pointer;
padding: 5px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.3s ease;
}
.close-filter-btn:hover {
color: var(--title-color);
}
.filter-sidebar-content {
padding: 1.5rem;
overflow-y: auto;
}
.filter-sidebar-content h4 {
color: var(--text-color);
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 1px;
margin-top: 0;
margin-bottom: 1rem;
display: flex;
justify-content: start;
}
.filter-group {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-bottom: 2rem;
}
.filter-tag {
background-color: transparent;
border: 1px solid var(--text-color);
color: var(--text-color);
padding: 8px 16px;
border-radius: 20px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
text-decoration: none;
transition: all 0.3s ease;
}
.filter-tag:hover {
background-color: var(--text-color);
color: #1E1E1E;
transform: translateY(-2px);
}
.filter-tag.active {
background-color: rgba(217, 95, 70, 0.3);
color: var(--important-color);
border-color: var(--important-color);
}

View File

@ -0,0 +1,154 @@
import './Filter.css';
import React, {useEffect, useState} from "react";
function Filter({ filters, setFilters }) {
const [isFilterVisible, setIsFilterVisible] = useState(false);
const showFilters = () => setIsFilterVisible(true);
const hideFilters = () => setIsFilterVisible(false);
const [skills, setSkills] = useState([]);
const [error, setError] = useState(null);
const handleCategoryFilter = (category) => {
setFilters(prev => {
const alreadySelected = prev.category.includes(category);
return {
...prev,
category: alreadySelected
? prev.category.filter(c => c !== category)
: [...prev.category, category],
};
});
};
const toggleTechnologyFilter = (tech) => {
setFilters(prev => {
const alreadySelected = prev.technology.includes(tech);
return {
...prev,
technology: alreadySelected
? prev.technology.filter(t => t !== tech)
: [...prev.technology, tech],
};
});
};
const setYearOrder = (order) => {
setFilters(prev => ({
...prev,
yearOrder: prev.yearOrder === order ? null : order
}));
};
const resetFilters = () => {
setFilters({
category: [],
technology: [],
yearOrder: filters.yearOrder,
});
};
useEffect(() => {
if (isFilterVisible) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [isFilterVisible]);
useEffect(() => {
const fetchSkills = async () => {
try {
const response = await fetch('/api/skills/');
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);
}
const data = await response.json();
setSkills(data.data);
} catch (err) {
setError(err.message);
}
};
fetchSkills();
}, []);
if (error) {
return <div>Error retrieving data: {error}</div>;
}
return (
<div>
<button className="filter-btn" onClick={showFilters}>
<svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 -960 960 960" width="30" fill="currentColor">
<path d="M440-160q-17 0-28.5-11.5T400-200v-240L168-736q-15-20-4.5-42t36.5-22h560q26 0 36.5 22t-4.5 42L560-440v240q0 17-11.5 28.5T520-160h-80Zm40-308 198-252H282l198 252Zm0 0Z"/>
</svg>
</button>
{isFilterVisible ? <div className="filter-overlay" onClick={hideFilters}></div> : null}
<div className={`filter-sidebar ${isFilterVisible ? 'visible' : ''}`}>
<div className="filter-sidebar-header">
<h3>Filter Projects</h3>
<button className="close-filter-btn" onClick={hideFilters}>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="currentColor">
<path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/>
</svg>
</button>
</div>
<div className="filter-sidebar-content">
<div className="filter-group">
<button
className={`filter-tag ${filters.category.length === 0 && filters.technology.length === 0 ? "active" : ""}`}
onClick={resetFilters}
>All</button>
</div>
<h4>Category</h4>
<div className="filter-group">
<button
className={`filter-tag ${filters.category.includes("school") ? "active" : ""}`}
onClick={() => handleCategoryFilter("school")}
>School</button>
<button
className={`filter-tag ${filters.category.includes("personal") ? "active" : ""}`}
onClick={() => handleCategoryFilter("personal")}
>Personal</button>
</div>
<h4>Technology</h4>
<div className="filter-group">
{skills.map(skill => (
<button
className={`filter-tag ${filters.technology.includes(skill.name) ? "active" : ""}`}
onClick={() => toggleTechnologyFilter(skill.name)}
>{skill.name}</button>
))}
</div>
<h4>YEAR</h4>
<div className="filter-group">
<button
className={`filter-tag ${filters.yearOrder === "asc" ? "active" : ""}`}
onClick={() => setYearOrder("asc")}
>Ascending</button>
<button
className={`filter-tag ${filters.yearOrder === "desc" ? "active" : ""}`}
onClick={() => setYearOrder("desc")}
>Descending</button>
</div>
</div>
</div>
</div>
);
}
export default Filter;

View File

@ -2,7 +2,7 @@
padding: 2rem 0;
margin-top: 4rem;
border-top: 1px solid #333;
color: #B0B0B0;
color: var(--text-color);
font-size: 0.9rem;
}
@ -22,7 +22,7 @@
display: flex;
align-items: center;
justify-content: center;
color: #B0B0B0;
color: var(--text-color);
transition: color 0.3s ease, transform 0.3s ease;
svg{
width: 24px;
@ -32,6 +32,6 @@
}
.footer-links a:hover {
color: #EAEAEA;
color: var(--title-color);
transform: translateY(-3px);
}

View File

@ -1,4 +1,4 @@
import '../styles/Footer.css';
import './Footer.css';
function Footer() {
const currentYear = new Date().getFullYear();

View File

@ -16,19 +16,19 @@
#home-section h2 {
font-size: 1.8rem;
color: #EAEAEA;
color: var(--title-color);
margin-bottom: 1rem;
}
#home-section p {
margin-bottom: 2rem;
color: #EAEAEA;
color: var(--title-color);
max-width: 40%;
}
.highlight {
color: #D95F46;
color: var(--important-color);
}
.btn {
@ -37,8 +37,8 @@
border-radius: 5px;
text-decoration: none;
font-weight: bold;
background-color: #D95F46;
color: #fff;
background-color: var(--important-color);
color: white;
transition: background-color 0.3s ease;
margin-bottom: 200px;
}

View File

@ -1,6 +1,6 @@
import '../styles/Home.css';
import Background from "./thirdParty/Background.jsx";
import NavBar from "./NavBar.jsx";
import './Home.css';
import Background from "../thirdParty/Background/Background.jsx";
import NavBar from "../NavBar/NavBar.jsx";
function Home() {
return (

View File

@ -5,7 +5,7 @@
backdrop-filter: blur(12px);
border-radius: 25px;
padding: 6px 10px;
z-index: 1000;
z-index: 100;
border: solid rgba(100,100,100,0.5) 0.001rem;
}
@ -25,7 +25,7 @@
text-decoration: none;
padding: 0.3rem 0.8rem;
border-radius: 20px;
color: #B0B0B0;
color: var(--text-color);
transition: background-color 0.3s ease, color 0.3s ease;
cursor: pointer;
border: none;
@ -34,10 +34,10 @@
}
.nav-link:hover {
color: #D95F46;
color: var(--important-color);
}
.nav-link.active {
background-color: rgba(217, 95, 70, 0.3);
color: #D95F46;
color: var(--important-color);
}

View File

@ -1,44 +1,51 @@
import { useEffect, useState } from "react";
import "../styles/NavBar.css";
import { useNavigate } from "react-router-dom";
import "./NavBar.css";
const NavBar = () => {
const [active, setActive] = useState("home-section");
const [isScrolling, setIsScrolling] = useState(false);
const navigate = useNavigate();
useEffect(() => {
const sections = document.querySelectorAll("section");
const navHeight = document.querySelector(".navbar")?.offsetHeight + 40 || 120;
if (location.pathname === '/') {
const sections = document.querySelectorAll("section");
const navHeight = document.querySelector(".navbar")?.offsetHeight + 40 || 120;
const handleScroll = () => {
if (isScrolling) return;
const handleScroll = () => {
if (isScrolling) return;
let current = "home-section";
sections.forEach((section) => {
const sectionTop = section.offsetTop - navHeight;
if (window.scrollY >= sectionTop) {
current = section.id;
}
});
let current = "home-section";
sections.forEach((section) => {
const sectionTop = section.offsetTop - navHeight;
if (window.scrollY >= sectionTop) {
current = section.id;
}
});
setActive(current);
};
setActive(current);
};
window.addEventListener("scroll", handleScroll);
handleScroll();
window.addEventListener("scroll", handleScroll);
handleScroll();
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [isScrolling]);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}
}, [isScrolling, location.pathname]);
const handleClick = (id) => {
setActive(id);
setIsScrolling(true);
document.getElementById(id)?.scrollIntoView({ behavior: "smooth" });
setTimeout(() => setIsScrolling(false), 800);
if (location.pathname === '/') {
setActive(id);
setIsScrolling(true);
document.getElementById(id)?.scrollIntoView({ behavior: "smooth" });
setTimeout(() => setIsScrolling(false), 800);
} else {
navigate(`/#${id}`);
}
};
return (
<nav className="navbar">
<ul className="nav-list">

View File

@ -1,47 +0,0 @@
import SingleProject from "./SingleProject.jsx";
import "../styles/Projects.css"
import React, {useEffect, useState} from "react";
function Projects() {
const [projects, setProjects] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
const fetchProjects = async () => {
try {
const response = await fetch('/api/projects/');
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);
}
const data = await response.json();
setProjects(data.data);
} catch (err) {
setError(err.message);
}
};
fetchProjects();
}, []);
if (error) {
return <div>Erreur lors de la récupération des données : {error}</div>;
}
return (
<section id="projects-section">
<h1 className="section-title">Projects</h1>
<div className="projects-section-list">
{projects.map(project => (
<SingleProject image={project.image_name} title={project.title} description={project.description} skills={project.skills} color={project.color} nbImage={project.nb_image}/>
))}
</div>
<div className="show-more-container">
<p className="show-more-link">
Show more
</p>
</div>
</section>
)
}
export default Projects

View File

@ -0,0 +1,59 @@
#projects-section{
display: flex;
flex-direction: column;
scroll-margin-top: 80px;
margin-top: 100px;
}
.show-more-container {
text-align: center;
}
.projects-link {
display: inline-block;
color: var(--text-color);
border: 1px solid var(--text-color);
background-color: transparent;
padding: 12px 28px;
border-radius: 8px;
font-weight: 600;
text-decoration: none;
transition: all 0.3s ease;
}
.projects-link:hover {
background-color: var(--text-color);
color: #1E1E1E;
transform: translateY(-3px);
cursor: pointer;
}
.projects-section-list {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.projects-section-header {
text-align: center;
margin-bottom: 2rem;
position: relative;
display: flex;
align-items: center;
flex-direction: column;
}
.projects-section-subtitle {
font-size: 1.2rem;
color: var(--title-color);
}
.projects-back-link {
text-align: center;
margin-bottom: 3rem;
}

View File

@ -0,0 +1,143 @@
import SingleProject from "../SingleProject/SingleProject.jsx";
import "./Projects.css"
import React, {useEffect, useState} from "react";
import {Link, useLocation} from "react-router-dom";
import NavBar from "../NavBar/NavBar.jsx";
import Filter from "../Filter/Filter.jsx";
function Projects() {
const [projects, setProjects] = useState([]);
const [error, setError] = useState(null);
const location = useLocation();
const [filters, setFilters] = useState({
category: [],
technology: [],
yearOrder: "asc",
});
useEffect(() => {
const fetchProjects = async () => {
try {
const response = await fetch('/api/shortProjects/');
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);
}
const data = await response.json();
setProjects(data.data);
} catch (err) {
setError(err.message);
}
};
fetchProjects();
}, []);
if (error) {
return <div>Error retrieving data: {error}</div>;
}
if (location.pathname === '/') {
return (
<section id="projects-section">
<h1 className="section-title">Projects</h1>
<div className="projects-section-list">
{projects
.filter(project => project.id <= 3)
.map(project => (
<SingleProject
image={project.image_name}
title={project.title}
description={project.short_description}
skills={project.skills}
id={project.id}
school={project.school}
beginningYear={project.beginning_year}
endYear={project.end_year}
/>
))}
</div>
<div className="show-more-container">
<Link to="/projects" className="projects-link">Show more</Link>
</div>
</section>
)
}else if (location.pathname === '/projects') {
return (
<section id="projects-section">
<div className="projects-section-header">
<NavBar />
<h1 className="section-title">All Projects</h1>
<p className="projects-section-subtitle">Here you can find a collection of my work.</p>
<Filter filters={filters} setFilters={setFilters} />
</div>
<div className="projects-section-list">
{projects
.filter(project => {
const categoryFilters = filters.category;
if (categoryFilters.length > 0) {
const isSchool = project.school === 1;
const isFilterSchool = categoryFilters.includes("school");
const isFilterPersonal = categoryFilters.includes("personal");
if ((isSchool && !isFilterSchool) || (!isSchool && !isFilterPersonal)) {
return false;
}
}
if (filters.technology.length > 0) {
const isSkill = filters.technology.every(tech => project.skills.includes(tech));
if(!isSkill){
return false
}
}
return true;
})
.sort((a, b) => {
const aInProgress = !a.end_year;
const bInProgress = !b.end_year;
if (filters.yearOrder === "asc") {
const yearDiff = a.beginning_year - b.beginning_year;
if (yearDiff !== 0) return yearDiff;
if (!aInProgress && bInProgress) return -1;
if (aInProgress && !bInProgress) return 1;
return 0;
} else if (filters.yearOrder === "desc") {
const yearDiff = b.beginning_year - a.beginning_year;
if (yearDiff !== 0) return yearDiff;
if (aInProgress && !bInProgress) return -1;
if (!aInProgress && bInProgress) return 1;
return 0;
} else {
return 0;
}
})
.map(project => (
<SingleProject
image={project.image_name}
title={project.title}
description={project.short_description}
skills={project.skills}
id={project.id}
nbImage={project.nb_image}
school={project.school}
beginningYear={project.beginning_year}
endYear={project.end_year}
/>
))}
</div>
<div className="projects-back-link">
<Link to="/" className="projects-link"> Back to Home</Link>
</div>
</section>
)
}
}
export default Projects

View File

@ -31,7 +31,7 @@
}
.experience-company {
color: #D95F46;
color: var(--important-color);
font-size: 1.2rem;
margin: 0;
}
@ -39,14 +39,14 @@
.experience-role {
font-size: 1.5rem;
font-weight: bold;
color: #EAEAEA;
color: var(--title-color);
margin: 0;
}
.experience-location,
.experience-duration {
font-size: 0.95rem;
color: #B0B0B0;
color: var(--text-color);
margin: 0.25rem 0 0;
white-space: nowrap;
}
@ -61,7 +61,7 @@
position: relative;
padding-left: 1.5rem;
margin-bottom: 0.5rem;
color: #B0B0B0;
color: var(--text-color);
text-align: left;
}
@ -70,7 +70,7 @@
position: absolute;
left: 0;
top: 0;
color: #D95F46;
color: var(--important-color);
font-weight: bold;
font-size: 1.2rem;
line-height: 1;

View File

@ -1,4 +1,4 @@
import '../styles/SingleExperience.css';
import './SingleExperience.css';
function SingleExperience({ experience, tasks }) {
return (

View File

@ -1,79 +0,0 @@
import { useState, useEffect, useRef } from "react";
import SkillCard from "./SkillCard";
import "../styles/SingleProject.css";
function SingleProject({ image, title, description, skills, color, nbImage }) {
const [imageID, setImageID] = useState(1);
const [isFading, setIsFading] = useState(true);
const intervalRef = useRef(null);
const handleChangeImage = (direction) => {
if (nbImage <= 1) return;
setIsFading(false);
setTimeout(() => {
setImageID((prevID) =>
direction === 1
? (prevID % nbImage) + 1
: prevID === 1 ? nbImage : prevID - 1
);
setIsFading(true);
}, 300);
clearInterval(intervalRef.current);
startAutoSlide();
};
const startAutoSlide = () => {
intervalRef.current = setInterval(() => {
handleChangeImage(1);
}, 5000);
};
useEffect(() => {
startAutoSlide();
return () => clearInterval(intervalRef.current);
}, []);
return (
<div className="single-project">
<div className="single-project-left">
<button onClick={() => handleChangeImage(-1)} className="arrow preview">{'<'}</button>
<img
src={`/assets/images/${image}/${image}_${imageID}.png`}
alt={image}
className={isFading ? 'fade-in' : 'fade-out'}
/>
<button onClick={() => handleChangeImage(1)} className="arrow next">{'>'}</button>
</div>
<div className="single-project-right">
<div className="single-project-right-top">
<div className={`single-project-line color-${color}`}></div>
<h3 className="single-project-title">{title}</h3>
</div>
<div className="single-project-right-bottom">
<p className="single-project-description" style={{ whiteSpace: "pre-line" }}>
{description}
</p>
<ul className="single-project-skills-list">
{skills.map((skill) => (
<li key={skill}>
<SkillCard text={skill} />
</li>
))}
</ul>
<p className="single-project-link">Learn more</p>
</div>
</div>
</div>
);
}
export default SingleProject;

View File

@ -14,7 +14,7 @@
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.single-project-left {
.single-project-top {
position: relative;
flex-shrink: 0;
width: 100%;
@ -25,7 +25,7 @@
height: 210px;
}
.single-project-left img {
.single-project-top img {
width: 100%;
height: auto;
display: block;
@ -34,27 +34,27 @@
object-fit: cover;
}
.single-project-left img.fade-out {
opacity: 0;
}
.single-project-right {
.single-project-bottom {
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 2rem 2rem 2rem 0rem;
padding: 2rem 2rem 2rem 0;
align-items: flex-start;
height: 100%;
}
.single-project-right-top {
display: flex;
margin-left: 2rem;
.single-project-bottom-header {
margin-bottom : 1em;
}
.single-project-right-bottom {
.single-project-title-wrapper{
display: flex;
margin-left: 2rem;
width: 100%;
}
.single-project-bottom-container {
margin-left: 2rem;
display: flex;
flex-direction: column;
justify-content: space-between;
@ -65,31 +65,43 @@
border-radius: 38px;
min-width: 1.5rem;
height: 0.25rem;
margin-top: 1.5rem;
margin-top: 1.25rem;
margin-right: 1rem;
}
.color-orange {
background-color: #D95F46;
}
.color-purple {
background-color: #a646d9;
.color-blue {
background-color: #2556ff;
}
.color-green {
background-color: #36a837;
}
.color-purple {
background-color: #a646d9;
}
.color-red {
background-color: #ff0000;
}
.color-yellow {
background-color: #ffd427;
}
.single-project-title {
font-size: 2rem;
color: #EAEAEA;
color: var(--title-color);
margin-top: 0;
margin-bottom: 1rem;
margin-bottom: 0;
display: flex;
align-items: center;
gap: 1rem;
}
.single-project-description {
color: #B0B0B0;
color: var(--text-color);
margin-bottom: 1.5rem;
text-align: justify;
}
@ -104,7 +116,7 @@
}
.single-project-link {
color: #B0B0B0;
color: var(--text-color);
text-decoration: none;
font-weight: 600;
margin-bottom : 0;
@ -112,37 +124,14 @@
.single-project-link:hover {
cursor: pointer;
color: #EAEAEA;
color: var(--title-color);
}
.arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #EAEAEA;
font-size: 35px;
display: none;
transition: all 0.3s ease;
text-shadow: 0 0 6px rgba(0, 0, 0, 0.7);
}
.arrow:hover {
color: #D95F46;
opacity: 0.8;
transform: translateY(-50%) scale(1.2);
cursor: pointer;
}
.arrow.preview {
left: 10px;
}
.arrow.next {
right: 10px;
}
.single-project-left:hover .arrow {
display: flex;
.single-project-years{
color : var(--text-color);
margin : 0;
margin-left : 2em;
font-weight: bold;
font-style: italic;
}

View File

@ -0,0 +1,54 @@
import { useState, useEffect, useRef } from "react";
import SkillCard from "../SkillCard/SkillCard.jsx";
import "./SingleProject.css";
function SingleProject({ image, title, description, skills, id, school, beginningYear, endYear }) {
const color = ["blue", "green", "purple", "red", "yellow"]
return (
<div className="single-project">
<div className="single-project-top">
<img src={`/assets/images/projects/${image}/${image}_1.png`} alt={image}/>
</div>
<div className="single-project-bottom">
<div className="single-project-bottom-header">
<div className="single-project-title-wrapper">
<div className={`single-project-line color-${color[(id-1)%color.length]}`}></div>
<h3 className="single-project-title">
{title}
{school ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" fill="#D95F46" width="1.25em" height="1.25em">
<path d="M80 259.8L289.2 345.9C299 349.9 309.4 352 320 352C330.6 352 341 349.9 350.8 345.9L593.2 246.1C602.2 242.4 608 233.7 608 224C608 214.3 602.2 205.6 593.2 201.9L350.8 102.1C341 98.1 330.6 96 320 96C309.4 96 299 98.1 289.2 102.1L46.8 201.9C37.8 205.6 32 214.3 32 224L32 520C32 533.3 42.7 544 56 544C69.3 544 80 533.3 80 520L80 259.8zM128 331.5L128 448C128 501 214 544 320 544C426 544 512 501 512 448L512 331.4L369.1 390.3C353.5 396.7 336.9 400 320 400C303.1 400 286.5 396.7 270.9 390.3L128 331.4z"/>
</svg>
) : null}
</h3>
</div>
{endYear === null ? (
<p className="single-project-years">{beginningYear + ' in progress'}</p>
) : beginningYear === endYear ? (
<p className="single-project-years">{beginningYear}</p>
) : (
<p className="single-project-years">{beginningYear + ' ' + endYear}</p>
)}
</div>
<div className="single-project-bottom-container">
<p className="single-project-description" style={{ whiteSpace: "pre-line" }}>
{description}
</p>
<ul className="single-project-skills-list">
{skills.map((skill) => (
<li key={skill}>
<SkillCard text={skill} />
</li>
))}
</ul>
<p className="single-project-link">Learn more</p>
</div>
</div>
</div>
);
}
export default SingleProject;

View File

@ -1,4 +1,4 @@
import "../styles/SkillCard.css";
import "./SkillCard.css";
function SkillCard({ text }) {
return (

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import SkillCard from "./SkillCard.jsx";
import "../styles/Skills.css";
import SkillCard from "../SkillCard/SkillCard.jsx";
import "./Skills.css";
function Skills() {
const [skills, setSkills] = useState([]);
@ -23,7 +23,7 @@ function Skills() {
}, []);
if (error) {
return <div>Erreur lors de la récupération des données : {error}</div>;
return <div>Error retrieving data: {error}</div>;
}
const uniqueSkillTypes = [...new Set(skills.map(skill => skill.type))];

View File

@ -1,5 +1,5 @@
import React from "react";
import "../../styles/thirdParty/Background.css";
import "./Background.css";
export default function Background() {
return (

View File

@ -2,6 +2,9 @@
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
font-weight: 400;
background-color: #0D0D0D;
--title-color: #EAEAEA;
--text-color: #B0B0B0;
--important-color: #D95F46;
}
html{
@ -17,17 +20,9 @@ body {
min-height: 100vh;
}
h1{
font-size: 4rem;
color: white;
}
.section-title {
font-family: Arial;
font-size: 2.5rem;
color: #f0f0f0;
color: var(--title-color);
text-transform: uppercase;
letter-spacing: 0.15rem;
text-align: center;
@ -39,13 +34,22 @@ h1{
display: block;
width: 60px;
height: 3px;
background-color: #D95F46;
background-color: var(--important-color);
margin: 1rem auto 0;
border-radius: 2px;
}
h1{
font-size: 4rem;
color: white;
}
h2{
color: #f0f0f0;
color : var(--title-color);
}
h3 {
margin: 0;
color: var(--title-color);
}

View File

@ -2,9 +2,12 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import { BrowserRouter } from 'react-router-dom';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
)

34
src/pages/HomePage.jsx Normal file
View File

@ -0,0 +1,34 @@
import {useEffect, useState} from 'react'; // 1. Import useEffect
import { useLocation } from 'react-router-dom'; // 2. Import useLocation
import Home from '../components/Home/Home.jsx';
import Experiences from '../components/Experiences/Experiences.jsx';
import Projects from '../components/Projects/Projects.jsx';
import Skills from '../components/Skills/Skills.jsx';
import Footer from '../components/Footer/Footer.jsx';
function HomePage() {
const location = useLocation();
useEffect(() => {
if (location.hash) {
const id = location.hash.replace('#', '');
const element = document.getElementById(id);
if (element) {
setTimeout(() => {
element.scrollIntoView({ behavior: 'smooth' });
}, 100);
}
}
}, [location]);
return (
<div>
<Home/>
<Experiences/>
<Projects/>
<Skills/>
<Footer/>
</div>
);
}
export default HomePage;

View File

@ -0,0 +1,12 @@
import Projects from "../components/Projects/Projects.jsx";
import Footer from "../components/Footer/Footer.jsx";
function ProjectsPage() {
return (
<div>
<Projects/>
<Footer/>
</div>
);
}
export default ProjectsPage;

View File

@ -1,34 +0,0 @@
#projects-section{
display: flex;
flex-direction: column;
scroll-margin-top: 80px;
margin-top: 100px;
}
.show-more-container {
text-align: center;
}
.show-more-link {
display: inline-block;
color: #B0B0B0;
border: 1px solid #B0B0B0;
background-color: transparent;
padding: 12px 28px;
border-radius: 8px;
font-weight: 600;
text-decoration: none;
transition: all 0.3s ease;
}
.show-more-link:hover {
background-color: #B0B0B0;
color: #1E1E1E;
transform: translateY(-3px);
cursor: pointer;
}
.projects-section-list {
display: flex;
}