Refactor: move frontend in apps/frontend

This commit is contained in:
Giovanni committed 2026-10-03 12:04:17 +02:00
1 parent 2ab10c7803
commit 6b36ab8bfa
88 files changed
+4 -4

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
#root {
margin: 0 auto;
width: 100%;
}
+19
View File
@@ -0,0 +1,19 @@
import './App.css'
import { Routes, Route } from 'react-router-dom';
import HomePage from './pages/Home/HomePage.jsx';
import ProjectsPage from './pages/Projects/ProjectsPage.jsx';
import ProjectDetailsPage from "./pages/ProjectDetails/ProjectDetailsPage.jsx";
function App() {
return (
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/projects" element={<ProjectsPage />} />
<Route path="/projectDetails/:id" element={<ProjectDetailsPage />} />
</Routes>
)
}
export default App
@@ -0,0 +1,33 @@
.footer {
padding: 2rem 0;
margin-top: 4rem;
border-top: 1px solid #333;
color: var(--text-color);
font-size: 0.9rem;
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 80%;
max-width: 1000px;
margin: 0 auto;
}
.footer-links {
display: flex;
gap: 1.5rem;
a{
display: flex;
align-items: center;
justify-content: center;
color: var(--text-color);
transition: color 0.3s ease, transform 0.3s ease;
}
}
.footer-links a:hover {
color: var(--title-color);
transform: translateY(-3px);
}
@@ -0,0 +1,28 @@
import './Footer.css';
function Footer() {
const currentYear = new Date().getFullYear();
return (
<footer className="footer">
<div className="footer-content">
<div className="footer-copyright">
© {currentYear} • Giovanni Josserand
</div>
<div className="footer-links">
<a href="https://github.com/Giovanni-Josserand" target="_blank" rel="noopener noreferrer">
<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="0 0 24 24" fill="currentColor" width="24px" height="24px">
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
</svg>
</a>
<a href="https://gitea.josserand.ovh/Giovanni-Josserand" target="_blank" rel="noopener noreferrer">
<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="0 0 24 24" fill="currentColor" width="24px" height="24px">
<path d="M4.209 4.603c-.247 0-.525.02-.84.088-.333.07-1.28.283-2.054 1.027C-.403 7.25.035 9.685.089 10.052c.065.446.263 1.687 1.21 2.768 1.749 2.141 5.513 2.092 5.513 2.092s.462 1.103 1.168 2.119c.955 1.263 1.936 2.248 2.89 2.367 2.406 0 7.212-.004 7.212-.004s.458.004 1.08-.394c.535-.324 1.013-.893 1.013-.893s.492-.527 1.18-1.73c.21-.37.385-.729.538-1.068 0 0 2.107-4.471 2.107-8.823-.042-1.318-.367-1.55-.443-1.627-.156-.156-.366-.153-.366-.153s-4.475.252-6.792.306c-.508.011-1.012.023-1.512.027v4.474l-.634-.301c0-1.39-.004-4.17-.004-4.17-1.107.016-3.405-.084-3.405-.084s-5.399-.27-5.987-.324c-.187-.011-.401-.032-.648-.032zm.354 1.832h.111s.271 2.269.6 3.597C5.549 11.147 6.22 13 6.22 13s-.996-.119-1.641-.348c-.99-.324-1.409-.714-1.409-.714s-.73-.511-1.096-1.52C1.444 8.73 2.021 7.7 2.021 7.7s.32-.859 1.47-1.145c.395-.106.863-.12 1.072-.12zm8.33 2.554c.26.003.509.127.509.127l.868.422-.529 1.075a.686.686 0 0 0-.614.359.685.685 0 0 0 .072.756l-.939 1.924a.69.69 0 0 0-.66.527.687.687 0 0 0 .347.763.686.686 0 0 0 .867-.206.688.688 0 0 0-.069-.882l.916-1.874a.667.667 0 0 0 .237-.02.657.657 0 0 0 .271-.137 8.826 8.826 0 0 1 1.016.512.761.761 0 0 1 .286.282c.073.21-.073.569-.073.569-.087.29-.702 1.55-.702 1.55a.692.692 0 0 0-.676.477.681.681 0 1 0 1.157-.252c.073-.141.141-.282.214-.431.19-.397.515-1.16.515-1.16.035-.066.218-.394.103-.814-.095-.435-.48-.638-.48-.638-.467-.301-1.116-.58-1.116-.58s0-.156-.042-.27a.688.688 0 0 0-.148-.241l.516-1.062 2.89 1.401s.48.218.583.619c.073.282-.019.534-.069.657-.24.587-2.1 4.317-2.1 4.317s-.232.554-.748.588a1.065 1.065 0 0 1-.393-.045l-.202-.08-4.31-2.1s-.417-.218-.49-.596c-.083-.31.104-.691.104-.691l2.073-4.272s.183-.37.466-.497a.855.855 0 0 1 .35-.077z"/>
</svg>
</a>
</div>
</div>
</footer>
);
}
export default Footer;
@@ -0,0 +1,50 @@
.navbar {
position: fixed;
top: 30px;
background: rgba(100,100,100, 0.25);
backdrop-filter: blur(12px);
border-radius: 25px;
padding: 6px 10px;
z-index: 100;
border: solid rgba(100,100,100,0.5) 0.001rem;
}
.nav-list {
list-style: none;
display: flex;
gap: 1.3rem;
margin: 0;
padding: 0;
align-items: center;
li{
display: flex;
}
}
.nav-link {
text-decoration: none;
padding: 0.3rem 0.8rem;
border-radius: 20px;
color: var(--text-color);
transition: background-color 0.3s ease, color 0.3s ease;
cursor: pointer;
border: none;
background: none;
}
.nav-link:hover {
color: var(--important-color);
}
.nav-link.active {
background-color: rgba(217, 95, 70, 0.3);
color: var(--important-color);
}
@media (max-width: 768px) {
.nav-list{
gap: 0.5rem;
}
}
@@ -0,0 +1,89 @@
import { useEffect, useState } from "react";
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(() => {
if (location.pathname === '/') {
const sections = document.querySelectorAll("section");
const navHeight = document.querySelector(".navbar")?.offsetHeight + 40 || 120;
const handleScroll = () => {
if (isScrolling) return;
let current = "home-section";
sections.forEach((section) => {
const sectionTop = section.offsetTop - navHeight;
if (window.scrollY >= sectionTop) {
current = section.id;
}
});
setActive(current);
};
window.addEventListener("scroll", handleScroll);
handleScroll();
return () => {
window.removeEventListener("scroll", handleScroll);
};
}
}, [isScrolling, location.pathname]);
const handleClick = (id) => {
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">
<li>
<button
onClick={() => handleClick("home-section")}
className={`nav-link ${active === "home-section" ? "active" : ""}`}
>
Home
</button>
</li>
<li>
<button
onClick={() => handleClick("experiences-section")}
className={`nav-link ${active === "experiences-section" ? "active" : ""}`}
>
Experiences
</button>
</li>
<li>
<button
onClick={() => handleClick("projects-section")}
className={`nav-link ${active === "projects-section" ? "active" : ""}`}
>
Projects
</button>
</li>
<li>
<button
onClick={() => handleClick("skills-section")}
className={`nav-link ${active === "skills-section" ? "active" : ""}`}
>
Skills
</button>
</li>
</ul>
</nav>
);
};
export default NavBar;
@@ -0,0 +1,62 @@
#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;
}
@@ -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 "../../pages/Projects/components/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/projects/');
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);
}
const data = await response.json();
setProjects(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.some(skill => skill.name === 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
@@ -0,0 +1,139 @@
.single-project {
display: flex;
flex-direction: column;
width: 29%;
margin: 2rem auto;
overflow: hidden;
transition: all 0.3s ease-in-out;
border-radius: 12px;
background-color: #1E1E1E;
}
.single-project:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
cursor: pointer;
}
.single-project-top {
position: relative;
flex-shrink: 0;
width: 100%;
display: flex;
justify-content: center;
align-items: start;
overflow: hidden;
height: 210px;
}
.single-project-top img {
width: 100%;
height: auto;
display: block;
transition: opacity 0.3s ease-in-out;
opacity: 1;
object-fit: cover;
}
.single-project-bottom {
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 2rem 2rem 2rem 0;
align-items: flex-start;
height: 100%;
}
.single-project-bottom-header {
margin-bottom : 1em;
}
.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;
height: 100%;
}
.single-project-line {
border-radius: 38px;
min-width: 1.5rem;
height: 0.25rem;
margin-top: 1.25rem;
margin-right: 1rem;
}
.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: var(--title-color);
margin-top: 0;
margin-bottom: 0;
display: flex;
align-items: center;
gap: 1rem;
}
.single-project-description {
color: var(--text-color);
margin-bottom: 1.5rem;
text-align: justify;
}
.single-project-skills-list {
display: flex;
flex-wrap: wrap;
list-style: none;
padding: 0;
margin: 0;
gap: 8px;
}
.single-project-years{
color : var(--text-color);
margin : 0;
margin-left : 2em;
font-weight: bold;
font-style: italic;
}
@media (max-width: 1366px) {
.single-project {
width: 47%;
}
}
@media (max-width: 768px) {
.single-project {
width: 90%;
}
}
@@ -0,0 +1,60 @@
import React, { useState, useEffect, useRef } from "react";
import SkillCard from "../SkillCard/SkillCard.jsx";
import "./SingleProject.css";
import {Link, useNavigate} from "react-router-dom";
function SingleProject({ image, title, description, skills, id, school, beginningYear, endYear }) {
const color = ["blue", "green", "purple", "red", "yellow"]
const navigate = useNavigate();
const clickProject = () => {
navigate(`/projectDetails/${id}`);
}
return (
<div className="single-project" onClick={clickProject}>
<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="1em" height="1em">
<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.id}>
<SkillCard text={skill.name} />
</li>
))}
</ul>
</div>
</div>
</div>
);
}
export default SingleProject;
@@ -0,0 +1,24 @@
.skill-card {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 0.5rem;
padding: 0.25rem 0.75rem;
font-size: 0.875rem;
width: fit-content;
white-space: nowrap;
flex-shrink: 0;
gap: 0.5rem;
background-color: #171717;
color: white;
border: solid rgba(100,100,100) 0.001rem;
height: 1.5rem;
margin: 0.5% 2%;
}
.skill-card img {
width: 1rem;
height: 1.125rem;
pointer-events: none;
}
@@ -0,0 +1,12 @@
import "./SkillCard.css";
function SkillCard({ text }) {
return (
<div className="skill-card">
<img src={`/assets/images/skills/${text}.svg`} alt={text} />
<p>{text}</p>
</div>
);
}
export default SkillCard;
@@ -0,0 +1,112 @@
.space-background {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 123vh;
background: radial-gradient(ellipse at center, #0a001f 0%, #000000 80%);
overflow: hidden;
z-index: -1;
}
.star {
position: absolute;
width: 2px;
height: 2px;
background: white;
border-radius: 50%;
opacity: 0.8;
animation: twinkle 3s infinite ease-in-out alternate;
filter: drop-shadow(0 0 2px white);
}
.planet {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 120vw;
height: 23vw;
max-height: 400px;
background: #0D0D0D;
border-radius: 60% 60% 0 0 / 100% 100% 0 0;
z-index: 1;
box-shadow:
0 -20px 60px 10px rgba(255, 255, 255, 0.35),
inset 0 20px 60px 10px rgba(255, 255, 255, 0.25);
}
.planet::after {
content: "";
position: absolute;
top: -3px;
left: 50%;
transform: translateX(-50%);
width: 100%;
height: 100%;
border-top: 2.5px solid white;
border-radius: 60% 60% 0 0 / 100% 100% 0 0;
mask-image: linear-gradient(to right, transparent 0%, white 40%, white 60%, transparent 100%);
pointer-events: none;
z-index: 2;
}
@keyframes twinkle {
0% { opacity: 0.3; transform: scale(0.8); }
100% { opacity: 1; transform: scale(1.2); }
}
.nebula {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.5;
mix-blend-mode: screen;
animation: nebulaMove 15s ease-in-out infinite alternate;
}
.nebula1 {
width: 400px;
height: 400px;
background: radial-gradient(circle, #9b59b6 0%, transparent 70%);
top: 10%;
left: 15%;
animation-delay: 0s;
}
.nebula2 {
width: 600px;
height: 600px;
background: radial-gradient(circle, #8e44ad 0%, transparent 70%);
top: 50%;
left: 65%;
animation-delay: 5s;
}
.nebula3 {
width: 350px;
height: 350px;
background: radial-gradient(circle, #663399 0%, transparent 70%);
top: 75%;
left: 35%;
animation-delay: 10s;
}
@keyframes nebulaMove {
0% {
transform: translate(0, 0) scale(1);
opacity: 0.5;
}
100% {
transform: translate(20px, -20px) scale(1.05);
opacity: 0.6;
}
}
@media (max-width: 840px) {
.planet {
display: none;
}
}
@@ -0,0 +1,24 @@
import React from "react";
import "./Background.css";
export default function Background() {
return (
<div className="space-background">
{[...Array(100)].map((_, i) => (
<div
key={i}
className="star"
style={{
top: `${Math.random() * 100}%`,
left: `${Math.random() * 100}%`,
animationDelay: `${Math.random() * 5}s`,
}}
/>
))}
<div className="nebula nebula1"></div>
<div className="nebula nebula2"></div>
<div className="nebula nebula3"></div>
<div className="planet"></div>
</div>
);
}
+53
View File
@@ -0,0 +1,53 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
font-weight: 400;
background-color: #0D0D0D;
--title-color: #EAEAEA;
--text-color: #7c7c7c;
--important-color: #D95F46;
}
html{
scroll-behavior: smooth;
}
body {
margin: 0;
display: flex;
place-items: center;
}
.section-title {
font-size: 2.5rem;
color: var(--title-color);
text-transform: uppercase;
letter-spacing: 0.15rem;
text-align: center;
margin-bottom: 3rem;
}
.section-title::after {
content: '';
display: block;
width: 60px;
height: 3px;
background-color: var(--important-color);
margin: 1rem auto 0;
border-radius: 2px;
}
h1{
font-size: 4rem;
color: white;
}
h2{
color : var(--title-color);
}
h3 {
margin: 0;
color: var(--title-color);
}
+13
View File
@@ -0,0 +1,13 @@
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>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
)
+34
View File
@@ -0,0 +1,34 @@
import {useEffect} from 'react';
import { useLocation } from 'react-router-dom';
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;
@@ -0,0 +1,37 @@
#experiences-section {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
scroll-margin-top: 80px;
}
.experiences-container {
width: 100%;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
.experiences-container::before {
content: '';
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 2px;
height: 100%;
background-color: rgba(176, 176, 176, 0.2);
}
@media (max-width: 768px) {
.experiences-container {
width: 90%;
align-items: flex-start;
margin-right: 20px;
}
.experiences-container::before {
left: 20px;
}
}
@@ -0,0 +1,41 @@
import './Experiences.css';
import SingleExperience from '../SingleExperience/SingleExperience.jsx';
import React, {useEffect, useState} from "react";
function Experiences() {
const [experiences, setExperiences] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
const fetchExperiences = async () => {
try {
let response = await fetch('/api/experiences/');
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);
}
let data = await response.json();
setExperiences(data);
} catch (err) {
setError(err.message);
}
};
fetchExperiences();
}, []);
if (error) {
return <div>Error retrieving data: {error}</div>;
}
return (
<section id="experiences-section">
<h1 className="section-title">Experiences</h1>
<div className="experiences-container">
{experiences.map((exp) => (
<SingleExperience experience={exp}/>
))}
</div>
</section>
);
}
export default Experiences;
@@ -0,0 +1,83 @@
#home-section {
position: relative;
min-height: 110vh;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
text-align: center;
}
#home-section h1{
margin-bottom: 0;
margin-left: 40px;
margin-right: 40px;
}
#home-section h2 {
font-size: 1.8rem;
color: var(--title-color);
margin-bottom: 1rem;
}
#home-section p {
margin-bottom: 2rem;
color: var(--title-color);
max-width: 40%;
}
.highlight {
color: var(--important-color);
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 5px;
text-decoration: none;
font-weight: bold;
background-color: var(--important-color);
color: white;
transition: background-color 0.3s ease;
margin-bottom: 200px;
}
.btn:hover {
background-color: #b94a36;
cursor: pointer;
}
@media (max-width: 840px) {
.btn {
margin-bottom: 10%;
}
#home-section p{
max-width: 60%;
}
}
@media (max-width: 656px) {
#home-section h1{
font-size: 3.5rem;
}
#home-section h2 {
font-size: 1.5rem;
}
}
@media (max-width: 584px) {
#home-section h1{
font-size: 3rem;
}
}
@@ -0,0 +1,22 @@
import './Home.css';
import Background from "../../../../components/thirdParty/Background/Background.jsx";
import NavBar from "../../../../components/NavBar/NavBar.jsx";
function Home() {
return (
<section id="home-section">
<Background />
<NavBar/>
<h1>Hello, I'm<span className="highlight"> Giovanni Josserand</span></h1>
<h2>Junior Developer</h2>
<p>
Passionate about development, I am currently looking
for an opportunity to put my skills into practice and continue learning.
</p>
<a href="/assets/documents/josserand_giovanni_cv.pdf" target="_blank" className="btn">See my CV</a>
</section>
);
}
export default Home;
@@ -0,0 +1,99 @@
.experience-card {
position: relative;
width: 90%;
margin-bottom: 3rem;
padding: 1.5em;
display: flex;
gap: 6rem;
justify-content: center;
}
.experience-card::before {
content: '';
position: absolute;
top: 25px;
left: 50%;
transform: translateX(-50%);
width: 15px;
height: 15px;
border-radius: 50%;
background-color: #D95F46;
border: 3px solid rgba(176, 176, 176, 0.2);
}
.experience-card-left {
width: 25%;
text-align: end;
}
.experience-card-right {
width: 25%;
}
.experience-company {
color: var(--important-color);
font-size: 1.5rem;
margin: 0;
}
.experience-role {
font-size: 1.5rem;
font-weight: bold;
color: var(--title-color);
margin: 0;
}
.experience-location,
.experience-duration {
font-size: 0.95rem;
color: var(--text-color);
margin: 0.25rem 0 0;
white-space: nowrap;
}
.experience-tasks {
list-style: none;
padding: 0;
margin-top: 1rem;
}
.experience-tasks li {
position: relative;
padding-left: 1.5rem;
margin-bottom: 0.5rem;
color: var(--text-color);
text-align: left;
}
.experience-tasks li::before {
content: '›';
position: absolute;
left: 0;
top: 0;
color: var(--important-color);
font-weight: bold;
font-size: 1.2rem;
line-height: 1;
}
@media (max-width: 768px) {
.experience-card{
display: flex;
flex-direction: column;
gap: 0;
}
.experience-card::before {
left: 20px;
top: 31px;
}
.experience-card-left, .experience-card-right{
width: fit-content;
margin-left: 20px;
}
.experience-card-left{
text-align: left;
}
}
@@ -0,0 +1,22 @@
import './SingleExperience.css';
function SingleExperience({ experience }) {
return (
<div className="experience-card">
<div className="experience-card-left">
<h3 className="experience-company">{experience.company}</h3>
<p className="experience-location">{experience.location}</p>
<p className="experience-duration">{experience.duration}</p>
</div>
<div className="experience-card-right">
<h3 className="experience-role">{experience.role}</h3>
<ul className="experience-tasks">
{experience.tasks.map((task) => (
<li>{task.description}</li>
))}
</ul>
</div>
</div>
);
}
export default SingleExperience;
@@ -0,0 +1,25 @@
#skills-section{
display: flex;
flex-direction: column;
scroll-margin-top: 80px;
margin-top: 100px;
}
.skills-span{
width: 100%;
}
.skill-category{
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-bottom: 2%;
}
.skills{
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
@@ -0,0 +1,52 @@
import React, { useState, useEffect } from 'react';
import SkillCard from "../../../../components/SkillCard/SkillCard.jsx";
import "./Skills.css";
function Skills() {
const [skills, setSkills] = useState([]);
const [error, setError] = useState(null);
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);
} catch (err) {
setError(err.message);
}
};
fetchSkills();
}, []);
if (error) {
return <div>Error retrieving data: {error}</div>;
}
const uniqueSkillTypes = [...new Set(skills.map(skill => skill.type))];
return (
<section id="skills-section">
<h1 className="section-title">Skills</h1>
{uniqueSkillTypes.map(type => (
<div id="languages" className="skill-category">
<h2>{type}</h2>
<div className="skills">
{skills
.filter(skill => skill.type === type)
.map(skill => (
<SkillCard text={skill.name}/>
))}
</div>
</div>
))}
</section>
)
}
export default Skills
@@ -0,0 +1,41 @@
import { useParams } from "react-router-dom";
import React, { useEffect, useState } from "react";
import ProjectDetails from "./components/ProjectDetails/ProjectDetails.jsx";
import Footer from "../../components/Footer/Footer.jsx";
function ProjectDetailsPage() {
const { id } = useParams();
const [project, setProject] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
const fetchProject = async () => {
try {
const response = await fetch(`/api/projects/${id}`);
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);
}
const data = await response.json();
setProject(data);
} catch (err) {
setError(err.message);
}
};
fetchProject();
}, [id]);
if (error) {
return <p>Erreur : {error}</p>;
}
return (
<div>
<ProjectDetails project={project}/>
<Footer/>
</div>
);
}
export default ProjectDetailsPage;
@@ -0,0 +1,193 @@
.project-details {
display: flex;
width: 70%;
margin: 0 15% 0 15%;
gap: 4%;
flex-direction: column;
}
.nav-bar{
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
overflow: hidden;
height: 280px;
background-image: url("/apps/frontend/publicrontend/public/assets/images/projectDetailsHeadband.png");
background-size: cover;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 200px;
background: linear-gradient(to top, #0D0D0D 0%, rgba(26, 26, 26, 0) 100%);
}
}
.project-details-header{
width: 100%;
border-bottom: 2px solid #333;
}
.project-details-header h1{
color: var(--title-color);
display: flex;
align-items: center;
gap: 1rem;
margin: 0;
justify-content: space-between;
}
.project-details-container {
width: 92%;
color: var(--text-color);
display: flex;
padding: 0 4%;
gap: 5%;
}
.project-details-content {
width: 80%
}
.project-details-content h2 {
margin-top: 2rem;
color: var(--title-color);
border-left: 4px solid var(--important-color);
padding-left: 0.75rem;
}
.project-details-years {
color : var(--text-color);
margin : 0;
font-weight: bold;
font-style: italic;
}
.project-details-short-description{
color: var(--text-color);
}
.project-details-header ul{
display: flex;
list-style: none;
padding: 0;
gap: 1em;
flex-wrap: wrap;
}
aside {
width: 20%;
height: fit-content;
position: sticky;
top: 150px;
color: var(--text-color);
}
.aside-title {
font-weight: bold;
color: var(--title-color);
margin-bottom: 1rem;
font-size: 1.1em;
}
aside ul{
border-left: 2px solid #333;
list-style: none;
padding: 0 0 0 1em;
}
aside ul li {
margin-bottom: 0.25em;
}
aside ul li a {
text-decoration: none;
color: var(--text-color);
}
aside ul li a:hover {
color: var(--title-color)
}
.return-button{
position: absolute;
top: 2em;
left: 20px;
border: none;
background: none;
padding: 7px;
cursor: pointer;
z-index: 10;
transition: background-color 0.3s ease;
justify-content: center;
align-items: center;
border-radius: 50%;
width: 38px;
height: 38px;
}
.return-button:hover {
background: rgba(100,100,100, 0.5);
cursor: pointer;
}
.return-button svg {
color: var(--text-color);
transition: all 0.3s ease;
}
.return-button:hover svg {
color: var(--title-color);
}
.repo_link {
display: flex;
gap: 1.5rem;
svg{
display: flex;
align-items: center;
justify-content: center;
color: var(--text-color);
transition: color 0.3s ease, transform 0.3s ease;
}
}
.repo_link svg:hover {
color: var(--title-color);
}
.project-details-header-left{
display: flex;
align-items: center;
gap: 0.5em;
}
.project-details-image {
width: 100%;
margin-bottom: 2%;
}
@media (max-width: 480px) {
.return-button {
top: 7em;
}
aside {
display: none;
}
.project-details-content{
width: 100%;
}
}
@@ -0,0 +1,119 @@
import "./ProjectDetails.css"
import SkillCard from "../../../../components/SkillCard/SkillCard.jsx";
import React, {useEffect, useState} from "react";
import NavBar from "../../../../components/NavBar/NavBar.jsx";
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import {useNavigate} from "react-router-dom";
function ProjectDetails({project}) {
const [mdTitle, setMdTitle] = useState([]);
const navigate = useNavigate();
useEffect(() =>{
if (!project.long_description) return;
const matches = [...project.long_description.matchAll(/^## (?!#)(.+)$/gm)];
setMdTitle(matches.map(match => match[1].trim()));
}, [project.long_description])
const handleSummaryClick = (event, anchor) => {
event.preventDefault();
const element = document.getElementById(anchor);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
return (
<div>
<button className="return-button" onClick={() => navigate(-1)}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" width="24px" height="24px" fill="currentColor">
<path d="m313-440 224 224-57 56-320-320 320-320 57 56-224 224h487v80H313Z"/>
</svg>
</button>
<div className="nav-bar">
<NavBar/>
</div>
<div className="project-details">
<div className="project-details-header">
<h1>
<div className="project-details-header-left">
{project.title}
{project.school ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" fill="#D95F46" width="1em" height="1em">
<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}
</div>
{typeof project.repo_link === "string" && project.repo_link.length > 0 ? (
<a href={project.repo_link} className="repo_link">
{project.repo_link.includes("github") ? (
<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em">
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em">
<path d="M4.209 4.603c-.247 0-.525.02-.84.088-.333.07-1.28.283-2.054 1.027C-.403 7.25.035 9.685.089 10.052c.065.446.263 1.687 1.21 2.768 1.749 2.141 5.513 2.092 5.513 2.092s.462 1.103 1.168 2.119c.955 1.263 1.936 2.248 2.89 2.367 2.406 0 7.212-.004 7.212-.004s.458.004 1.08-.394c.535-.324 1.013-.893 1.013-.893s.492-.527 1.18-1.73c.21-.37.385-.729.538-1.068 0 0 2.107-4.471 2.107-8.823-.042-1.318-.367-1.55-.443-1.627-.156-.156-.366-.153-.366-.153s-4.475.252-6.792.306c-.508.011-1.012.023-1.512.027v4.474l-.634-.301c0-1.39-.004-4.17-.004-4.17-1.107.016-3.405-.084-3.405-.084s-5.399-.27-5.987-.324c-.187-.011-.401-.032-.648-.032zm.354 1.832h.111s.271 2.269.6 3.597C5.549 11.147 6.22 13 6.22 13s-.996-.119-1.641-.348c-.99-.324-1.409-.714-1.409-.714s-.73-.511-1.096-1.52C1.444 8.73 2.021 7.7 2.021 7.7s.32-.859 1.47-1.145c.395-.106.863-.12 1.072-.12zm8.33 2.554c.26.003.509.127.509.127l.868.422-.529 1.075a.686.686 0 0 0-.614.359.685.685 0 0 0 .072.756l-.939 1.924a.69.69 0 0 0-.66.527.687.687 0 0 0 .347.763.686.686 0 0 0 .867-.206.688.688 0 0 0-.069-.882l.916-1.874a.667.667 0 0 0 .237-.02.657.657 0 0 0 .271-.137 8.826 8.826 0 0 1 1.016.512.761.761 0 0 1 .286.282c.073.21-.073.569-.073.569-.087.29-.702 1.55-.702 1.55a.692.692 0 0 0-.676.477.681.681 0 1 0 1.157-.252c.073-.141.141-.282.214-.431.19-.397.515-1.16.515-1.16.035-.066.218-.394.103-.814-.095-.435-.48-.638-.48-.638-.467-.301-1.116-.58-1.116-.58s0-.156-.042-.27a.688.688 0 0 0-.148-.241l.516-1.062 2.89 1.401s.48.218.583.619c.073.282-.019.534-.069.657-.24.587-2.1 4.317-2.1 4.317s-.232.554-.748.588a1.065 1.065 0 0 1-.393-.045l-.202-.08-4.31-2.1s-.417-.218-.49-.596c-.083-.31.104-.691.104-.691l2.073-4.272s.183-.37.466-.497a.855.855 0 0 1 .35-.077z"/>
</svg>
)}
</a>
) : null}
</h1>
{project.end_year === null ? (
<p className="project-details-years">{project.beginning_year + ' – in progress'}</p>
) : project.beginning_year === project.end_year ? (
<p className="project-details-years">{project.beginning_year}</p>
) : (
<p className="project-details-years">{project.beginning_year + ' – ' + project.end_year}</p>
)}
<p className="project-details-short-description">{project.short_description}</p>
<ul>
{project.skills && project.skills.map((skill) => (
<li>
<SkillCard text={skill.name} />
</li>
))}
</ul>
</div>
<div className="project-details-container">
<div className="project-details-content">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
h2({node, ...props}) {
const id = String(props.children).toLowerCase().replace(/\s+/g, "-");
return <h2 id={id} {...props} />;
}
}}
>
{project.long_description}
</ReactMarkdown>
{[...Array(project.nb_image)].map((_, i) => (
<img src={`/assets/images/projects/${project.image_name}/${project.image_name}_${i+1}.png`} alt={project.image_name} className="project-details-image"/>
))}
</div>
<aside>
<ul>
{mdTitle.map(title => {
const anchor = title.toLowerCase().replace(/\s+/g, "-");
return (
<li key={anchor}>
<a href={`#${anchor}`} onClick={(e) => handleSummaryClick(e, anchor)}>
{title}
</a>
</li>
);
})}
</ul>
</aside>
</div>
</div>
</div>
);
}
export default ProjectDetails
@@ -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;
@@ -0,0 +1,134 @@
.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);
}
@media (max-width: 480px) {
.filter-btn {
top: 5em;
right: 20px;
}
}
@@ -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);
} 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;