Commit initial : version fonctionnelle du projet
This commit is contained in:
20
client/.eslintrc.cjs
Normal file
20
client/.eslintrc.cjs
Normal file
@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||
settings: { react: { version: '18.2' } },
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
};
|
24
client/.gitignore
vendored
Normal file
24
client/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
18
client/index.html
Normal file
18
client/index.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Une application simple de gestion de tâches pour un projet académique." />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>Gestionnaire de Tâches</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
53
client/package.json
Normal file
53
client/package.json
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "gestionnaire-taches-ui",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"description": "Interface utilisateur pour l'application de gestion de tâches.",
|
||||
"author": "Ton Nom <ton.email@example.com>",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/TonNom/TonRepo.git"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite --host=0.0.0.0",
|
||||
"build": "vite build",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write \"**/*.jsx\"",
|
||||
"format-check": "prettier --check \"**/*.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free-regular": "^5.0.13",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"bootstrap": "^5.3.6",
|
||||
"date-fns": "^4.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.10.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sass": "^1.70.0",
|
||||
"sweetalert2": "^11.6.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"prettier": "^3.2.4",
|
||||
"vite": "^6.3.5"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true
|
||||
}
|
||||
}
|
1
client/public/vite.svg
Normal file
1
client/public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
39
client/src/App.jsx
Normal file
39
client/src/App.jsx
Normal file
@ -0,0 +1,39 @@
|
||||
// Fichier: client/src/App.jsx
|
||||
|
||||
import React from 'react';
|
||||
import { CarteTaches } from './components/CarteTaches';
|
||||
|
||||
// On définit les URLs directement comme des constantes.
|
||||
const imageUrl = 'https://images.unsplash.com/photo-1686934674798-2e390dc94019?fm=jpg&q=60&w=3000&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1yZWxhdGVkfDE0fHx8ZW58MHx8fHx8';
|
||||
const videoUrl = 'https://videos.pexels.com/video-files/4782135/4782135-hd_1920_1080_25fps.mp4';
|
||||
|
||||
// Le composant pour le fond vidéo
|
||||
function VideoBackground() {
|
||||
return (
|
||||
<div className="video-background">
|
||||
{/* CORRECTION : playsinline est devenu playsInline */}
|
||||
<video autoPlay muted loop playsInline src={videoUrl}>
|
||||
Votre navigateur ne supporte pas les vidéos.
|
||||
</video>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<VideoBackground />
|
||||
|
||||
{/* On applique l'image de fond via un style inline en utilisant notre constante */}
|
||||
<header className="app-header" style={{ backgroundImage: `url(${imageUrl})` }}>
|
||||
<h1>Gestionnaire de Tâches</h1>
|
||||
</header>
|
||||
|
||||
<main className="main-content">
|
||||
<CarteTaches />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
65
client/src/components/AffichageTache.jsx
Normal file
65
client/src/components/AffichageTache.jsx
Normal file
@ -0,0 +1,65 @@
|
||||
// Fichier: client/src/components/AffichageTache.jsx
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import { Form } from 'react-bootstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
// On importe l'icône de la poubelle
|
||||
import { faClock, faPenToSquare, faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { format, isPast, isToday } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import './AffichageTache.scss';
|
||||
|
||||
export function AffichageTache({ tache, onToggle, onSuppression, onEdit }) {
|
||||
|
||||
const estEnRetard = !tache.completed && tache.dueDate && isPast(new Date(tache.dueDate)) && !isToday(new Date(tache.dueDate));
|
||||
let statusClass = 'pending';
|
||||
if (estEnRetard) {
|
||||
statusClass = 'overdue';
|
||||
}
|
||||
|
||||
const handleDeleteClick = (e) => {
|
||||
e.stopPropagation();
|
||||
onSuppression(tache.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`tache-item-wrapper ${tache.completed ? 'complete' : ''}`}>
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
checked={tache.completed}
|
||||
onChange={() => onToggle(tache)}
|
||||
aria-label={`Marquer ${tache.name} comme ${tache.completed ? 'incomplète' : 'complétée'}`}
|
||||
/>
|
||||
<div className="tache-details">
|
||||
<span className="tache-nom">{tache.name}</span>
|
||||
{tache.dueDate && (
|
||||
<span className="tache-date">
|
||||
<FontAwesomeIcon icon={faClock} />
|
||||
{format(new Date(tache.dueDate), 'd MMM yy', { locale: fr })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="tache-actions">
|
||||
<button className="action-btn edit-btn" aria-label="Modifier la tâche" onClick={() => onEdit(tache)}>
|
||||
<FontAwesomeIcon icon={faPenToSquare} />
|
||||
</button>
|
||||
{/* On remplace l'indicateur par un bouton avec une icône */}
|
||||
<button className="action-btn delete-btn" aria-label="Supprimer la tâche" onClick={handleDeleteClick}>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
AffichageTache.propTypes = {
|
||||
tache: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
completed: PropTypes.bool.isRequired,
|
||||
dueDate: PropTypes.string,
|
||||
}).isRequired,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
onSuppression: PropTypes.func.isRequired,
|
||||
onEdit: PropTypes.func.isRequired,
|
||||
};
|
74
client/src/components/AffichageTache.scss
Normal file
74
client/src/components/AffichageTache.scss
Normal file
@ -0,0 +1,74 @@
|
||||
// Fichier: client/src/components/AffichageTache.scss
|
||||
|
||||
.tache-item-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid var(--couleur-bordure);
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.form-check-input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tache-details {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.tache-nom {
|
||||
font-weight: 500;
|
||||
color: var(--couleur-texte-principal);
|
||||
}
|
||||
|
||||
.tache-date {
|
||||
font-size: 0.875rem;
|
||||
color: var(--couleur-texte-secondaire);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tache-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--couleur-texte-secondaire);
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
font-size: 0.9rem;
|
||||
transition: color 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
color: var(--couleur-theme-vert);
|
||||
}
|
||||
}
|
||||
|
||||
// Style spécifique pour le bouton supprimer
|
||||
.delete-btn {
|
||||
&:hover {
|
||||
color: var(--couleur-danger); // Devient rouge au survol
|
||||
}
|
||||
}
|
||||
|
||||
&.complete {
|
||||
.tache-nom, .tache-date {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
184
client/src/components/CarteTaches.jsx
Normal file
184
client/src/components/CarteTaches.jsx
Normal file
@ -0,0 +1,184 @@
|
||||
// Fichier: client/src/components/CarteTaches.jsx
|
||||
|
||||
import { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import { Button, Modal, Form, Spinner, Alert } from 'react-bootstrap';
|
||||
import { isToday, isFuture, isPast, startOfDay, format } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faChevronDown } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { AffichageTache } from './AffichageTache';
|
||||
import './CarteTaches.scss';
|
||||
|
||||
export function CarteTaches() {
|
||||
const [taches, setTaches] = useState([]);
|
||||
const [chargement, setChargement] = useState(true);
|
||||
const [erreur, setErreur] = useState('');
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingTask, setEditingTask] = useState(null);
|
||||
const [activeFilter, setActiveFilter] = useState("Aujourd'hui");
|
||||
const [completedVisible, setCompletedVisible] = useState(true);
|
||||
|
||||
const filtresTraduits = {
|
||||
"Aujourd'hui": "Aujourd'hui",
|
||||
"À venir": "À venir",
|
||||
"En retard": "En retard"
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/items')
|
||||
.then(res => res.ok ? res.json() : Promise.reject(res))
|
||||
.then(data => setTaches(data.sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate))))
|
||||
.catch(() => setErreur("Impossible de charger les tâches. Le serveur est peut-être indisponible."))
|
||||
.finally(() => setChargement(false));
|
||||
}, []);
|
||||
|
||||
const listesDeTaches = useMemo(() => {
|
||||
const tachesEnCours = taches.filter(t => !t.completed);
|
||||
const filtres = {
|
||||
"Aujourd'hui": tachesEnCours.filter(t => t.dueDate && isToday(new Date(t.dueDate))),
|
||||
"À venir": tachesEnCours.filter(t => t.dueDate && isFuture(new Date(t.dueDate))),
|
||||
"En retard": tachesEnCours.filter(t => t.dueDate && isPast(new Date(t.dueDate)) && !isToday(new Date(t.dueDate)))
|
||||
};
|
||||
return {
|
||||
tachesAffichees: filtres[activeFilter] || [],
|
||||
tachesTerminees: taches.filter(t => t.completed)
|
||||
};
|
||||
}, [taches, activeFilter]);
|
||||
|
||||
const handleOpenAddModal = () => {
|
||||
setEditingTask(null);
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleOpenEditModal = (tache) => {
|
||||
setEditingTask(tache);
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setShowModal(false);
|
||||
setEditingTask(null);
|
||||
};
|
||||
|
||||
const handleToggle = useCallback((tacheToToggle) => {
|
||||
fetch(`/api/items/${tacheToToggle.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ completed: !tacheToToggle.completed })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(updatedTask => {
|
||||
setTaches(current => current.map(t => (t.id === updatedTask.id ? updatedTask : t)));
|
||||
}).catch(err => console.error("Erreur de mise à jour:", err));
|
||||
}, []);
|
||||
|
||||
const handleDelete = useCallback((idToDelete) => {
|
||||
if (window.confirm("Êtes-vous sûr de vouloir supprimer cette tâche ?")) {
|
||||
fetch(`/api/items/${idToDelete}`, { method: 'DELETE' })
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
setTaches(current => current.filter(t => t.id !== idToDelete));
|
||||
} else {
|
||||
alert("Erreur lors de la suppression.");
|
||||
}
|
||||
}).catch(err => console.error("Erreur de suppression:", err));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleFormSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
const name = event.target.elements.taskName.value;
|
||||
const dueDate = event.target.elements.taskDueDate.value;
|
||||
const taskData = { name, dueDate };
|
||||
|
||||
if (editingTask) {
|
||||
fetch(`/api/items/${editingTask.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(taskData)
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(updatedTask => {
|
||||
setTaches(current => current.map(t => (t.id === updatedTask.id ? updatedTask : t)));
|
||||
handleCloseModal();
|
||||
}).catch(console.error);
|
||||
} else {
|
||||
fetch('/api/items', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(taskData)
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(newTask => {
|
||||
setTaches(current => [...current, newTask].sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate)));
|
||||
handleCloseModal();
|
||||
}).catch(console.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="tasks-header">
|
||||
<h2>Tâches</h2>
|
||||
<Button onClick={handleOpenAddModal} className="add-task-btn">
|
||||
Ajouter une tâche
|
||||
</Button>
|
||||
</div>
|
||||
<div className="filter-tabs">
|
||||
{Object.keys(filtresTraduits).map(filter => (
|
||||
<button
|
||||
key={filter}
|
||||
className={`filter-btn ${activeFilter === filter ? 'active' : ''}`}
|
||||
onClick={() => setActiveFilter(filter)}>
|
||||
{filtresTraduits[filter]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="tasks-list">
|
||||
{chargement && <Spinner animation="border" />}
|
||||
{erreur && <Alert variant="danger">{erreur}</Alert>}
|
||||
{!chargement && !erreur && listesDeTaches.tachesAffichees.length === 0 && (
|
||||
<p className="empty-list-message">Aucune tâche pour "{filtresTraduits[activeFilter]}".</p>
|
||||
)}
|
||||
{listesDeTaches.tachesAffichees.map(tache => (
|
||||
<AffichageTache key={tache.id} tache={tache} onToggle={handleToggle} onSuppression={handleDelete} onEdit={handleOpenEditModal} />
|
||||
))}
|
||||
</div>
|
||||
<div className="completed-section-header" onClick={() => setCompletedVisible(!completedVisible)}>
|
||||
<h3>Terminé</h3>
|
||||
<FontAwesomeIcon icon={faChevronDown} className={`chevron-icon ${completedVisible ? 'open' : ''}`} />
|
||||
</div>
|
||||
{completedVisible && (
|
||||
<div className="tasks-list completed-list">
|
||||
{listesDeTaches.tachesTerminees.length === 0 && <p className="empty-list-message">Aucune tâche terminée.</p>}
|
||||
{listesDeTaches.tachesTerminees.map(tache => (
|
||||
<AffichageTache key={tache.id} tache={tache} onToggle={handleToggle} onSuppression={handleDelete} onEdit={handleOpenEditModal} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Modal show={showModal} onHide={handleCloseModal} centered>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
{editingTask ? "Modifier la tâche" : "Ajouter une nouvelle tâche"}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form onSubmit={handleFormSubmit}>
|
||||
<Form.Group className="mb-3" controlId="taskName">
|
||||
<Form.Label>Nom de la tâche</Form.Label>
|
||||
<Form.Control type="text" placeholder="Ex: Sortir les poubelles" required defaultValue={editingTask ? editingTask.name : ''} />
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3" controlId="taskDueDate">
|
||||
<Form.Label>Date d'échéance</Form.Label>
|
||||
<Form.Control type="date" required defaultValue={editingTask ? format(new Date(editingTask.dueDate), 'yyyy-MM-dd') : format(new Date(), 'yyyy-MM-dd')} />
|
||||
</Form.Group>
|
||||
<Button variant="success" type="submit" className="w-100">
|
||||
{editingTask ? "Enregistrer les modifications" : "Enregistrer la tâche"}
|
||||
</Button>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
89
client/src/components/CarteTaches.scss
Normal file
89
client/src/components/CarteTaches.scss
Normal file
@ -0,0 +1,89 @@
|
||||
// Fichier: client/src/components/CarteTaches.scss
|
||||
|
||||
.tasks-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.add-task-btn {
|
||||
background-color: var(--couleur-theme-vert);
|
||||
border-color: var(--couleur-theme-vert);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--couleur-bordure);
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.75rem 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--couleur-texte-secondaire);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: color 0.2s ease-in-out;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: var(--couleur-theme-vert);
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--couleur-texte-principal);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--couleur-theme-vert);
|
||||
&::after {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.completed-section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
margin-top: 2rem;
|
||||
padding: 0.5rem 0;
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
transition: transform 0.3s ease;
|
||||
&.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-list-message {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--couleur-texte-secondaire);
|
||||
}
|
71
client/src/components/FormulaireAjout.jsx
Normal file
71
client/src/components/FormulaireAjout.jsx
Normal file
@ -0,0 +1,71 @@
|
||||
// Fichier: client/src/components/FormulaireAjout.jsx
|
||||
// Gère le formulaire pour ajouter une nouvelle tâche.
|
||||
|
||||
import { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from 'react-bootstrap/Button';
|
||||
import Form from 'react-bootstrap/Form';
|
||||
import InputGroup from 'react-bootstrap/InputGroup';
|
||||
|
||||
export function FormulaireAjout({ onNouvelleTache }) {
|
||||
const [nouvelleTache, setNouvelleTache] = useState('');
|
||||
const [envoiEnCours, setEnvoiEnCours] = useState(false);
|
||||
const [erreur, setErreur] = useState('');
|
||||
|
||||
const soumettreNouvelleTache = (e) => {
|
||||
e.preventDefault();
|
||||
setEnvoiEnCours(true);
|
||||
setErreur(''); // Réinitialise l'erreur à chaque soumission
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: nouvelleTache }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
|
||||
fetch('/api/items', options)
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error('La requête a échoué');
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then((tache) => {
|
||||
onNouvelleTache(tache);
|
||||
setNouvelleTache(''); // Vide le champ après succès
|
||||
})
|
||||
.catch(() => {
|
||||
setErreur("Impossible d'ajouter la tâche. Veuillez réessayer.");
|
||||
})
|
||||
.finally(() => {
|
||||
setEnvoiEnCours(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={soumettreNouvelleTache}>
|
||||
<InputGroup className="mb-3">
|
||||
<Form.Control
|
||||
value={nouvelleTache}
|
||||
onChange={(e) => setNouvelleTache(e.target.value)}
|
||||
type="text"
|
||||
placeholder="Entrez une nouvelle tâche..."
|
||||
aria-label="Entrez une nouvelle tâche"
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={!nouvelleTache.length || envoiEnCours}
|
||||
>
|
||||
{envoiEnCours ? 'Ajout...' : 'Ajouter Tâche'}
|
||||
</Button>
|
||||
</InputGroup>
|
||||
{erreur && <p className="text-danger small mt-1">{erreur}</p>}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
FormulaireAjout.propTypes = {
|
||||
onNouvelleTache: PropTypes.func.isRequired,
|
||||
};
|
19
client/src/components/MessageAccueil.jsx
Normal file
19
client/src/components/MessageAccueil.jsx
Normal file
@ -0,0 +1,19 @@
|
||||
// Fichier: client/src/components/MessageAccueil.jsx
|
||||
// Affiche un message de bienvenue dynamique récupéré depuis le backend.
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function MessageAccueil() {
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/greeting')
|
||||
.then((res) => res.json())
|
||||
.then((data) => setMessage(data.greeting))
|
||||
.catch(() => setMessage('Bienvenue !')); // Message par défaut en cas d'erreur
|
||||
}, []); // Le tableau vide signifie que cet effet ne s'exécute qu'une fois
|
||||
|
||||
if (!message) return null;
|
||||
|
||||
return <p className="lead text-muted">{message}</p>;
|
||||
}
|
21
client/src/components/Navbar.jsx
Normal file
21
client/src/components/Navbar.jsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import Navbar from 'react-bootstrap/Navbar';
|
||||
import Nav from 'react-bootstrap/Nav';
|
||||
|
||||
export default function NavbarComponent() {
|
||||
return (
|
||||
<Navbar bg="dark" variant="dark" expand="lg">
|
||||
<Navbar.Brand href="#home" className="ms-3">
|
||||
Gestionnaire de Tâches Pro
|
||||
</Navbar.Brand>
|
||||
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
||||
<Navbar.Collapse id="basic-navbar-nav">
|
||||
<Nav className="me-auto ms-3">
|
||||
<Nav.Link href="#home">Accueil</Nav.Link>
|
||||
<Nav.Link href="#features">Fonctionnalités</Nav.Link>
|
||||
<Nav.Link href="#about">À Propos</Nav.Link>
|
||||
</Nav>
|
||||
</Navbar.Collapse>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
70
client/src/index.scss
Normal file
70
client/src/index.scss
Normal file
@ -0,0 +1,70 @@
|
||||
// Fichier: client/src/index.scss
|
||||
|
||||
@use 'bootstrap/scss/bootstrap-grid';
|
||||
@use 'bootstrap/scss/bootstrap-reboot';
|
||||
|
||||
:root {
|
||||
--couleur-fond-page: #eef2f7;
|
||||
--couleur-carte: #f8fafc; // Fond de carte moderne
|
||||
--couleur-texte-principal: #1e293b;
|
||||
--couleur-texte-secondaire: #64748b;
|
||||
--couleur-theme-vert: #166534;
|
||||
--couleur-theme-vert-clair: #22c55e;
|
||||
--couleur-danger: #ef4444;
|
||||
--couleur-warning: #f59e0b;
|
||||
--couleur-bordure: #e2e8f0;
|
||||
--bordure-radius: 12px;
|
||||
--header-height: 180px;
|
||||
}
|
||||
|
||||
.video-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.video-background video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
opacity: 0.07;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--couleur-fond-page);
|
||||
font-family: 'Poppins', sans-serif;
|
||||
color: var(--couleur-texte-principal);
|
||||
}
|
||||
|
||||
.app-header {
|
||||
height: var(--header-height);
|
||||
// L'image est maintenant appliquée via un style inline dans App.jsx
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
font-size: 3.5rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1.5px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
max-width: 900px;
|
||||
margin: -60px auto 50px auto;
|
||||
background-color: var(--couleur-carte);
|
||||
border-radius: var(--bordure-radius);
|
||||
box-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-height: 400px;
|
||||
padding: 2rem;
|
||||
}
|
18
client/src/main.jsx
Normal file
18
client/src/main.jsx
Normal file
@ -0,0 +1,18 @@
|
||||
// Fichier: client/src/main.jsx
|
||||
// Point d'entrée de l'application React.
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App.jsx';
|
||||
|
||||
// NOUVEAU : Importation du CSS de Bootstrap pour styliser les composants
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
|
||||
import './index.scss'; // Importation de nos styles globaux personnalisés
|
||||
|
||||
// Rendu du composant principal "App" dans l'élément HTML avec l'ID "root".
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
7
client/vite.config.js
Normal file
7
client/vite.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
2568
client/yarn.lock
Normal file
2568
client/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user