add of Filter components
This commit is contained in:
parent
24426da015
commit
bf1ba8947b
@ -1,13 +1,12 @@
|
||||
# To do
|
||||
|
||||
- Régler problème de scoll entre page
|
||||
- Rajouter les images manquantes des projects
|
||||
- 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
|
||||
- Ajouter un fonction de filtrage des projects (en fonction de la date, des skills, school ou non)
|
||||
|
||||
|
||||
## Usefull commands
|
||||
|
||||
127
src/components/Filter/Filter.css
Normal file
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
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;
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -3,12 +3,20 @@ 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 {
|
||||
@ -62,9 +70,54 @@ function Projects() {
|
||||
<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.map(project => (
|
||||
{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}
|
||||
@ -83,10 +136,7 @@ function Projects() {
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}else {
|
||||
return <div>Page inexistante</div>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -20,13 +20,6 @@ body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
|
||||
|
||||
h1{
|
||||
font-size: 4rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 2.5rem;
|
||||
color: var(--title-color);
|
||||
@ -47,7 +40,16 @@ h1{
|
||||
}
|
||||
|
||||
|
||||
h1{
|
||||
font-size: 4rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
h2{
|
||||
color : var(--title-color);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: var(--title-color);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user