Commit initial : version fonctionnelle du projet

This commit is contained in:
Mouktar Kimba
2025-06-10 05:20:19 +02:00
commit a8acba240b
39 changed files with 7798 additions and 0 deletions

20
client/.eslintrc.cjs Normal file
View 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
View 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
View 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
View 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
View 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
View 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;

View 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,
};

View 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;
}
}
}

View 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>
</>
);
}

View 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);
}

View 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,
};

View 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>;
}

View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff