15
README.md
@ -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
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 643 KiB |
|
Before Width: | Height: | Size: 923 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 271 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 232 KiB |
|
Before Width: | Height: | Size: 800 KiB |
|
Before Width: | Height: | Size: 312 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 239 KiB |
19
src/App.jsx
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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 (
|
||||
127
src/components/Filter/Filter.css
Normal 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);
|
||||
}
|
||||
154
src/components/Filter/Filter.jsx
Normal 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;
|
||||
@ -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);
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import '../styles/Footer.css';
|
||||
import './Footer.css';
|
||||
function Footer() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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 (
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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">
|
||||
@ -79,4 +86,4 @@ const NavBar = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default NavBar;
|
||||
export default NavBar;
|
||||
@ -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
|
||||
59
src/components/Projects/Projects.css
Normal 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;
|
||||
}
|
||||
143
src/components/Projects/Projects.jsx
Normal 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
|
||||
@ -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;
|
||||
@ -1,4 +1,4 @@
|
||||
import '../styles/SingleExperience.css';
|
||||
import './SingleExperience.css';
|
||||
|
||||
function SingleExperience({ experience, tasks }) {
|
||||
return (
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
54
src/components/SingleProject/SingleProject.jsx
Normal 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;
|
||||
@ -1,4 +1,4 @@
|
||||
import "../styles/SkillCard.css";
|
||||
import "./SkillCard.css";
|
||||
|
||||
function SkillCard({ text }) {
|
||||
return (
|
||||
@ -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))];
|
||||
@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import "../../styles/thirdParty/Background.css";
|
||||
import "./Background.css";
|
||||
|
||||
export default function Background() {
|
||||
return (
|
||||
@ -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);
|
||||
}
|
||||
@ -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
@ -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;
|
||||
12
src/pages/ProjectsPage.jsx
Normal 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;
|
||||
@ -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;
|
||||
}
|
||||