e) 🍬 Librairie pour UI

Introduction aux librairies de composants React

Il existe de nombreuses librairies de composants React qui permettent de rendre plus facile et plus rapide la création de UI : Material UI, Ant Design, Chakra UI, ...

Pour ce tutoriel optionnel, nous allons utiliser Material UI, une librairie de composants React très populaire qui permet de créer des applications web modernes et réactives.

Toute la documentation de cette librairie est disponible : https://mui.com/material-ui/

Mise en place de Material UI

Installation de Material UI

Pour ce tutoriel, veuillez créer une copie de la solution de l'exercice 2.8, si nécessaire voici le code de ex2.8, et l'appeler ui-library. Changez le nom du projet dans package.json.

Commencez par installer les librairies de bases nécessaires :

sh
npm install @mui/material @emotion/react @emotion/styled @fontsource/roboto @mui/icons-material

Utilisation de Material UI

Par défaut, Material UI utilise le font Roboto qu'il faut installer. Pour utiliser un font, il faut ensuite faire un import, par exemple dans votre script d'entrée de votre application /src/main.tsx :

tsx
import "@fontsource/roboto/700.css";

Reset global du CSS

Il est de bonne pratique de normaliser tous les composants HTML en faisant appel à CssBaseline.

Veuillez mettre à jour /src/main.tsx ainsi :

tsx
import React from "react";
import ReactDOM from "react-dom/client";
import "@fontsource/roboto/700.css";
import CssBaseline from "@mui/material/CssBaseline";
import App from "./components/App";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<CssBaseline /> {/* Global CSS reset from Material-UI */}
<App />
</React.StrictMode>
);

Principe général de fonctionnement de MUI

MUI met à disposition beaucoup de composants qui permettent de créer des UI en utilisant les règles de Material Design comme référence.

Tous les composants peuvent être découverts ici : https://mui.com/material-ui/all-components/

Les composants peuvent être taillés sur mesure selon différentes stratégies. En voici les principales :

  • Customiser un seul élément d'un composant MUI via la prop sx (les valeurs sont un superset de CSS) ou via la prop className (pour utiliser des classes CSS personnelles);
  • Créer un composant réutilisable à partir d'un composant MUI et de l'utilitaire styled ;
  • Faire une surcharge d'un composant MUI via un theme ;
  • Faire une surcharge globale du CSS de certains éléments HTML en utilisant le composant GlobalStyles.

Dans ce cours, nous allons explorer la première option uniquement à l'aide de sx. N'hésitez pas à en découvrir plus par vous-même via : https://mui.com/material-ui/customization/how-to-customize/

Il existe des composants de layout qui permettent d'agencer d'autres composants horizontalement ou verticalement, principalement :

  • Box : Un composant qui sert de conteneur flexible pour appliquer des marges, des paddings, des alignements et d'autres styles CSS aux enfants.
  • Container : Un composant qui centre et limite la largeur du contenu à une taille prédéfinie pour maintenir des marges cohérentes et une mise en page réactive.
  • Grid : Un composant pour créer des mises en page en grille réactives, permettant de définir des rangées et des colonnes avec des espacements et des alignements configurables.
  • Stack : Un composant qui simplifie l'agencement des enfants en les empilant verticalement ou horizontalement avec des espacements uniformes.

Le système de breakpoints de Material UI permet de créer des mises en page réactives, c'est-à-dire en ajustant le rendu des composants en fonction de la taille de l'écran. Material UI propose plusieurs breakpoints par défaut qui correspondent à des largeurs d'écran courantes.

Les breakpoints par défaut de Material UI sont définis comme suit :

  • xs (extra-small): 0px et plus
  • sm (small): 600px et plus
  • md (medium): 900px et plus
  • lg (large): 1200px et plus
  • xl (extra-large): 1536px et plus

Par exemple, le composant Grid utilise ces breakpoints pour définir le nombre de colonnes à afficher à différentes largeurs d'écran :

tsx
<Grid container spacing={2}>
<Grid xs={12} md={6}>
<Item>xs=6 md=8</Item>
</Grid>
<Grid xs={12} md={6}>
<Item>xs=6 md=4</Item>
</Grid>
<Grid xs={12} md={6}>
<Item>xs=6 md=4</Item>
</Grid>
<Grid xs={12} md={6}>
<Item>xs=6 md=8</Item>
</Grid>
</Grid>

Ce Grid permet à un composant d'occuper 6 colonnes sur 12 du viewport quand la largeur du viewport est de 600 et plus pixel. Pour les viewport plus petits, le composant remplit les 12 colonnes disponibles. Cela permet de créer une mise en page réactive qui s'adaptent à la taille de l'écran : soit 2 colonnes sur un écran large, soit 1 colonne sur un écran plus petit.

Utilisation de base des composants MUI

Pour ce tutoriel, les composants MUI qui semblent applicables à l'UI de notre application gérant une pizzeria ont été sélectionnés sur base de la documentation de MUI.

Nous vous proposons de mettre à jour les scripts de votre projet sans aucune gestion du style : nous allons donc enlever toutes les références au CSS, et nous n'allons pas encore utiliser le système de MUI pour styler les éléments de notre applications.

Nous avons déjà mis à jour /src/main.tsx. Continuons donc par la mise à jour de Main (dans /src/components/App/index.tsx) :

tsx
1
import Footer from "../Footer";
2
import Header from "../Header";
3
import Main from "../Main";
4
import { useState } from "react";
5
import { Box } from "@mui/material";
6
7
function App() {
8
const [actionToBePerformed, setActionToBePerformed] = useState(false);
9
10
const handleHeaderClick = () => {
11
setActionToBePerformed(true);
12
};
13
14
const clearActionToBePerformed = () => {
15
setActionToBePerformed(false);
16
};
17
18
return (
19
<Box>
20
<Header
21
title="We love Pizza"
22
version={0 + 1}
23
handleHeaderClick={handleHeaderClick}
24
/>
25
<Main
26
actionToBePerformed={actionToBePerformed}
27
clearActionToBePerformed={clearActionToBePerformed}
28
/>
29
30
<Footer />
31
</Box>
32
);
33
}
34
35
export default App;

Nous avons juste utilisé Box pour prendre la place d'une div et nous n'utilisons plus App.css.

Veuillez ensuite mettre à jour le Header (dans /src/components/Header/index.tsx) :

tsx
1
import { Box, Container, Typography } from "@mui/material";
2
import { useState } from "react";
3
4
interface HeaderProps {
5
title: string;
6
version: number;
7
handleHeaderClick: () => void;
8
}
9
10
const Header = ({ title, handleHeaderClick }: HeaderProps) => {
11
const [menuPrinted, setMenuPrinted] = useState(false);
12
13
const handleClick = () => {
14
console.log(`value of menuPrinted before click: ${menuPrinted}`);
15
setMenuPrinted(!menuPrinted);
16
handleHeaderClick();
17
};
18
19
return (
20
<Box
21
component="header"
22
onClick={handleClick}
23
>
24
<Container maxWidth="sm">
25
<Typography variant="h1">
26
{menuPrinted ? `${title}... and rarely do we hate it!` : title}
27
</Typography>
28
</Container>
29
</Box>
30
);
31
};
32
33
export default Header;

Là nous utilisons :

  • Box : pour créer notre future élément <header>.
  • Container : pour créer un container qui centre ses éléments et dont la largeur vaut maximum sm (600px et plus).
  • Typography : pour gérer le texte (qui deviendra un élément <h1>).

Nous n'utilisons plus Header.css.

Veuillez ensuite mettre à jour le Main (dans /src/components/Main/index.tsx). Notons que nous souhaitons faire un refactor de l'application afin que le menu des boissons soit affiché sur base d'une collection de données :

tsx
1
import { useState } from "react";
2
import sound from "../../assets/sounds/Infecticide-11-Pizza-Spinoza.mp3";
3
import DrinkMenu from "./DrinkMenu";
4
import PizzaMenu from "./PizzaMenu";
5
import { NewPizza, Pizza, Drink} from "../../types";
6
import AddPizza from "./AddPizza";
7
import AudioPlayer from "./AudioPlayer";
8
import { Container, Typography } from "@mui/material";
9
10
const defaultPizzas: Pizza[] = [
11
{
12
id: 1,
13
title: "4 fromages",
14
content: "Gruyère, Sérac, Appenzel, Gorgonzola, Tomates",
15
},
16
{
17
id: 2,
18
title: "Vegan",
19
content: "Tomates, Courgettes, Oignons, Aubergines, Poivrons",
20
},
21
{
22
id: 3,
23
title: "Vegetarian",
24
content: "Mozarella, Tomates, Oignons, Poivrons, Champignons, Olives",
25
},
26
{
27
id: 4,
28
title: "Alpage",
29
content: "Gruyère, Mozarella, Lardons, Tomates",
30
},
31
{
32
id: 5,
33
title: "Diable",
34
content: "Tomates, Mozarella, Chorizo piquant, Jalapenos",
35
},
36
];
37
38
const drinks: Drink[] = [
39
{
40
title: "Coca-Cola",
41
image:
42
"https://media.istockphoto.com/id/1289738725/fr/photo/bouteille-en-plastique-de-coke-avec-la-conception-et-le-chapeau-rouges-d%C3%A9tiquette.jpg?s=1024x1024&w=is&k=20&c=HBWfROrGDTIgD6fuvTlUq6SrwWqIC35-gceDSJ8TTP8=",
43
volume: "Volume: 33cl",
44
price: "2,50 €",
45
},
46
{
47
title: "Pepsi",
48
image:
49
"https://media.istockphoto.com/id/185268840/fr/photo/bouteille-de-cola-sur-un-fond-blanc.jpg?s=1024x1024&w=is&k=20&c=xdsxwb4bLjzuQbkT_XvVLyBZyW36GD97T1PCW0MZ4vg=",
50
volume: "Volume: 33cl",
51
price: "2,50 €",
52
},
53
{
54
title: "Eau Minérale",
55
image:
56
"https://media.istockphoto.com/id/1397515626/fr/photo/verre-deau-gazeuse-%C3%A0-boire-isol%C3%A9.jpg?s=1024x1024&w=is&k=20&c=iEjq6OL86Li4eDG5YGO59d1O3Ga1iMVc_Kj5oeIfAqk=",
57
volume: "Volume: 50cl",
58
price: "1,50 €",
59
},
60
];
61
62
interface MainProps {
63
actionToBePerformed: boolean;
64
clearActionToBePerformed: () => void;
65
}
66
67
const Main = ({ actionToBePerformed, clearActionToBePerformed }: MainProps) => {
68
const [pizzas, setPizzas] = useState(defaultPizzas);
69
70
const addPizza = (newPizza: NewPizza) => {
71
const pizzaAdded = { ...newPizza, id: nextPizzaId(pizzas) };
72
setPizzas([...pizzas, pizzaAdded]);
73
};
74
75
return (
76
<Container component="main" sx={{ mt: 8, mb: 2, flex: "1" }} maxWidth="sm">
77
<Typography variant="h2" component="h1" gutterBottom>
78
My HomePage
79
</Typography>
80
<Typography variant="h5" component="h2" gutterBottom>
81
Because we love JS, you can also click on the header to stop / start the
82
music ; )
83
</Typography>
84
<AudioPlayer
85
sound={sound}
86
actionToBePerformed={actionToBePerformed}
87
clearActionToBePerformed={clearActionToBePerformed}
88
/>
89
90
<PizzaMenu pizzas={pizzas} />
91
92
<br/>
93
94
<AddPizza addPizza={addPizza} />
95
96
<br/>
97
98
<DrinkMenu title="Notre Menu de Boissons" drinks={drinks} />
99
</Container>
100
);
101
};
102
103
const nextPizzaId = (pizzas: Pizza[]) => {
104
return pizzas.reduce((maxId, pizza) => Math.max(maxId, pizza.id), 0) + 1;
105
};
106
107
export default Main;

Il n'y a pas de composants MUI non rencontrés précédemment. Il n'y a plus de CSS.

A ce stade-ci, il est normal qu'il y ait toujours des erreurs dans votre application. Nous allons les corriger en changeant plusieurs composants.

Voici le nouveau type Drink qui doit être ajouté à /src/types.ts :

ts
1
interface Pizza {
2
id: number;
3
title: string;
4
content: string;
5
}
6
7
type NewPizza = Omit<Pizza, "id">;
8
9
interface Drink {
10
title: string;
11
image: string;
12
volume: string;
13
price: string;
14
}
15
16
export type { Pizza, NewPizza, Drink };

Le composant AudioPlayer ne doit pas être mis à jour. Le composant CssBaseline offre déjà un joli style de base.

Veuillez ensuite mettre à jour PizzaMenu (dans /src/components/Main/PizzaMenu.tsx) :

tsx
1
import {
2
Table,
3
TableBody,
4
TableCell,
5
TableContainer,
6
TableHead,
7
TableRow,
8
Paper,
9
} from "@mui/material";
10
11
import { Pizza } from "../../types";
12
13
interface PizzaMenuProps {
14
pizzas: Pizza[];
15
}
16
const PizzaMenu = ({ pizzas }: PizzaMenuProps) => {
17
return (
18
<TableContainer component={Paper}>
19
<Table>
20
<TableHead>
21
<TableRow>
22
<TableCell>Pizza</TableCell>
23
<TableCell>æ©escription</TableCell>
24
</TableRow>
25
</TableHead>
26
<TableBody>
27
{pizzas.map((pizza) => (
28
<TableRow key={pizza.id}>
29
<TableCell>{pizza.title}</TableCell>
30
<TableCell>{pizza.content}</TableCell>
31
</TableRow>
32
))}
33
</TableBody>
34
</Table>
35
</TableContainer>
36
);
37
};
38
39
export default PizzaMenu;

Pour ce composant, nous avons utilisé tous les composants MUI permettant de créer une table HTML : https://mui.com/material-ui/react-table/#basic-table.

Pour le menu des boissons, nous avons créé un tout nouveau design pour être basé sur des données plutôt que des composants enfants (via children).
Veuillez mettre à jour DrinkMenu (dans /src/components/Main/DrinkMenu.tsx) :

tsx
1
import {
2
Container,
3
Card,
4
CardMedia,
5
CardContent,
6
Typography,
7
Grid,
8
} from "@mui/material";
9
import { Drink } from "../../types";
10
11
interface DrinkMenuProps {
12
title: string;
13
drinks: Drink[];
14
}
15
16
const DrinkMenu: React.FC<DrinkMenuProps> = ({ title, drinks }) => {
17
return (
18
<Container>
19
<Typography variant="h4" gutterBottom>
20
{title}
21
</Typography>
22
<Grid container spacing={3}>
23
{drinks.map((drink, index) => (
24
<Grid item xs={12} sm={6} key={index}>
25
<Card>
26
<CardMedia
27
component="img"
28
image={drink.image}
29
alt={drink.title}
30
style={{ objectFit: "contain", height: "200px" }} // Ensure image is fully visible
31
/>
32
<CardContent>
33
<Typography gutterBottom variant="h5" component="div">
34
{drink.title}
35
</Typography>
36
<Typography variant="body2" color="textSecondary" component="p">
37
{drink.volume}
38
</Typography>
39
<Typography variant="body2" color="textSecondary" component="p">
40
Prix: {drink.price}
41
</Typography>
42
</CardContent>
43
</Card>
44
</Grid>
45
))}
46
</Grid>
47
</Container>
48
);
49
};
50
51
export default DrinkMenu;

Nous avons utilisé le composant Card du MUI pour l'UI de chaque boisson : https://mui.com/material-ui/react-card/.

Veuillez ensuite mettre à jour AddPizza (dans /src/components/Main/AddPizza.tsx) :

tsx
1
import { useState, SyntheticEvent } from "react";
2
3
import { NewPizza } from "../../types";
4
import { Box, Button, TextField } from "@mui/material";
5
6
interface AddPizzaProps {
7
addPizza: (pizza: NewPizza) => void;
8
}
9
10
const AddPizza = ({ addPizza }: AddPizzaProps) => {
11
const [pizza, setPizza] = useState("");
12
const [description, setDescription] = useState("");
13
14
const handleSubmit = (e: SyntheticEvent) => {
15
e.preventDefault();
16
addPizza({ title: pizza, content: description });
17
};
18
19
const handlePizzaChange = (e: SyntheticEvent) => {
20
const pizzaInput = e.target as HTMLInputElement;
21
console.log("change in pizzaInput:", pizzaInput.value);
22
setPizza(pizzaInput.value);
23
};
24
25
const handleDescriptionChange = (e: SyntheticEvent) => {
26
const descriptionInput = e.target as HTMLInputElement;
27
console.log("change in descriptionInput:", descriptionInput.value);
28
setDescription(descriptionInput.value);
29
};
30
31
return (
32
<Box>
33
<form onSubmit={handleSubmit}>
34
<Box sx={{ marginBottom: 2 }}>
35
<TextField
36
fullWidth
37
id="pizza"
38
name="pizza"
39
label="Pizza"
40
variant="outlined"
41
value={pizza}
42
onChange={handlePizzaChange}
43
required
44
color="primary"
45
/>
46
</Box>
47
<Box sx={{ marginBottom: 2 }}>
48
<TextField
49
fullWidth
50
id="description"
51
name="description"
52
label="Description"
53
variant="outlined"
54
value={description}
55
onChange={handleDescriptionChange}
56
required
57
color="primary"
58
/>
59
</Box>
60
<Button type="submit" variant="contained" color="primary">
61
Ajouter
62
</Button>
63
</form>
64
</Box>
65
);
66
};
67
68
export default AddPizza;

Nous avons utilisé TextField afin de gérer les 2 inputs de notre formulaire.

Il ne reste plus qu'à mettre à jour Footer (dans /src/components/Footer/index.tsx) :

tsx
1
import { Box, Container, Typography } from "@mui/material";
2
import logo from "../../assets/images/js-logo.png";
3
import { Copyright } from "@mui/icons-material";
4
5
const Footer = () => {
6
return (
7
<Box component="footer" color="">
8
<Container maxWidth="sm">
9
<Box>
10
<Typography variant="body2">But we also love JS</Typography>
11
<Typography>
12
<Copyright />
13
myAmazingPizzeria
14
</Typography>
15
</Box>
16
<Box>
17
<img src={logo} alt="" width={50} />
18
</Box>
19
</Container>
20
</Box>
21
);
22
};
23
24
export default Footer;

Nous avons utilisé l'icône Copyright pour le Footer.

Veuillez exécutez l'application !
Le résultat est fort intéressant : l'interface est très épurée, avec un look assez professionnel. Mais ça manque de style !

Exemple d'utilisation de la prop sx

Notre application possède déjà un thème par défaut, même si nous ne l'utilisons actuellement pas vraiment.

Nous allons donc maintenant styler de manière individuelle chacun des éléments MUI à l'aide de la prop sx. En fait, vous allez faire du CSS très ciblé, sans devoir créer de classes.

Voici la mise à jour du composant App afin d'ajouter la photo de background et de s'assurer que l'application prendra au minimum une hauteur de 100% du viewport (pour avoir un footer qui sera toujours en bas de page) :

tsx
1
import pizza from "../../assets/images/pizza.jpg";
2
// Other imports...
3
function App() {
4
const [actionToBePerformed, setActionToBePerformed] = useState(false);
5
6
const handleHeaderClick = () => {
7
setActionToBePerformed(true);
8
};
9
10
const clearActionToBePerformed = () => {
11
setActionToBePerformed(false);
12
};
13
14
return (
15
<Box sx={{
16
display: 'flex',
17
flexDirection: 'column',
18
height: '100%',
19
backgroundImage: `url(${pizza})`,
20
backgroundSize: 'cover',
21
}}>
22
23
24
25
<Header
26
title="We love Pizza"
27
version={0 + 1}
28
handleHeaderClick={handleHeaderClick}
29
/>
30
<Main
31
actionToBePerformed={actionToBePerformed}
32
clearActionToBePerformed={clearActionToBePerformed}
33
/>
34
35
<Footer />
36
</Box>
37
);
38
}

Voici la mise à jour du composant Header afin d'obtenir la couleur du thème :

tsx
1
import { Box, Container, Typography, useTheme } from "@mui/material";
2
import { useState } from "react";
3
4
interface HeaderProps {
5
title: string;
6
version: number;
7
handleHeaderClick: () => void;
8
}
9
10
const Header = ({ title, handleHeaderClick }: HeaderProps) => {
11
const theme = useTheme();
12
const [menuPrinted, setMenuPrinted] = useState(false);
13
14
const handleClick = () => {
15
console.log(`value of menuPrinted before click: ${menuPrinted}`);
16
setMenuPrinted(!menuPrinted);
17
handleHeaderClick();
18
};
19
20
return (
21
<Box
22
component="header"
23
sx={{
24
px: 2,
25
backgroundColor:
26
theme.palette.mode === "light"
27
? theme.palette.primary.light
28
: theme.palette.primary.dark,
29
color: (theme) => theme.palette.primary.contrastText,
30
}}
31
onClick={handleClick}
32
>
33
<Container maxWidth="sm">
34
<Typography variant="h1">
35
{menuPrinted ? `${title}... and rarely do we hate it!` : title}
36
</Typography>
37
</Container>
38
</Box>
39
);
40
};
41
42
export default Header;

Il y a différents moyens d'obtenir le thème, mais nous trouvons que le hook useTheme est le plus simple. Ici, le thème par défaut de MUI sera utilisé pour la couleur primary (une sorte de bleu).

Nous allons maintenant styler le composant Main afin qu'il prenne tout l'espace disponible (flex:"1") pour assurer que le Footer soit toujours tout en bas de la page :

tsx
1
import { useState } from "react";
2
import sound from "../../assets/sounds/Infecticide-11-Pizza-Spinoza.mp3";
3
import DrinkMenu from "./DrinkMenu";
4
// import "./Main.css";
5
import PizzaMenu from "./PizzaMenu";
6
import { NewPizza, Pizza, Drink} from "../../types";
7
import AddPizza from "./AddPizza";
8
import AudioPlayer from "./AudioPlayer";
9
import { Container, Typography } from "@mui/material";
10
11
const defaultPizzas: Pizza[] = [
12
{
13
id: 1,
14
title: "4 fromages",
15
content: "Gruyère, Sérac, Appenzel, Gorgonzola, Tomates",
16
},
17
{
18
id: 2,
19
title: "Vegan",
20
content: "Tomates, Courgettes, Oignons, Aubergines, Poivrons",
21
},
22
{
23
id: 3,
24
title: "Vegetarian",
25
content: "Mozarella, Tomates, Oignons, Poivrons, Champignons, Olives",
26
},
27
{
28
id: 4,
29
title: "Alpage",
30
content: "Gruyère, Mozarella, Lardons, Tomates",
31
},
32
{
33
id: 5,
34
title: "Diable",
35
content: "Tomates, Mozarella, Chorizo piquant, Jalapenos",
36
},
37
];
38
39
const drinks: Drink[] = [
40
{
41
title: "Coca-Cola",
42
image:
43
"https://media.istockphoto.com/id/1289738725/fr/photo/bouteille-en-plastique-de-coke-avec-la-conception-et-le-chapeau-rouges-d%C3%A9tiquette.jpg?s=1024x1024&w=is&k=20&c=HBWfROrGDTIgD6fuvTlUq6SrwWqIC35-gceDSJ8TTP8=",
44
volume: "Volume: 33cl",
45
price: "2,50 €",
46
},
47
{
48
title: "Pepsi",
49
image:
50
"https://media.istockphoto.com/id/185268840/fr/photo/bouteille-de-cola-sur-un-fond-blanc.jpg?s=1024x1024&w=is&k=20&c=xdsxwb4bLjzuQbkT_XvVLyBZyW36GD97T1PCW0MZ4vg=",
51
volume: "Volume: 33cl",
52
price: "2,50 €",
53
},
54
{
55
title: "Eau Minérale",
56
image:
57
"https://media.istockphoto.com/id/1397515626/fr/photo/verre-deau-gazeuse-%C3%A0-boire-isol%C3%A9.jpg?s=1024x1024&w=is&k=20&c=iEjq6OL86Li4eDG5YGO59d1O3Ga1iMVc_Kj5oeIfAqk=",
58
volume: "Volume: 50cl",
59
price: "1,50 €",
60
},
61
];
62
63
interface MainProps {
64
actionToBePerformed: boolean;
65
clearActionToBePerformed: () => void;
66
}
67
68
const Main = ({ actionToBePerformed, clearActionToBePerformed }: MainProps) => {
69
const [pizzas, setPizzas] = useState(defaultPizzas);
70
71
const addPizza = (newPizza: NewPizza) => {
72
const pizzaAdded = { ...newPizza, id: nextPizzaId(pizzas) };
73
setPizzas([...pizzas, pizzaAdded]);
74
};
75
76
return (
77
<Container component="main" sx={{ mt: 8, mb: 2, flex: "1" }} maxWidth="sm">
78
<Typography variant="h2" component="h1" gutterBottom>
79
My HomePage
80
</Typography>
81
<Typography variant="h5" component="h2" gutterBottom>
82
Because we love JS, you can also click on the header to stop / start the
83
music ; )
84
</Typography>
85
<AudioPlayer
86
sound={sound}
87
actionToBePerformed={actionToBePerformed}
88
clearActionToBePerformed={clearActionToBePerformed}
89
/>
90
91
<PizzaMenu pizzas={pizzas} />
92
93
<AddPizza addPizza={addPizza} />
94
95
<DrinkMenu title="Notre Menu de Boissons" drinks={drinks} />
96
</Container>
97
);
98
};
99
100
const nextPizzaId = (pizzas: Pizza[]) => {
101
return pizzas.reduce((maxId, pizza) => Math.max(maxId, pizza.id), 0) + 1;
102
};
103
104
export default Main;

Nous allons maintenant mettre à jour le PizzaMenu afin d'ajouter des couleurs de la palette du thème par défaut à la table HTML :

tsx
1
import {
2
Table,
3
TableBody,
4
TableCell,
5
TableContainer,
6
TableHead,
7
TableRow,
8
Paper,
9
useTheme,
10
} from "@mui/material";
11
12
import { Pizza } from "../../types";
13
14
interface PizzaMenuProps {
15
pizzas: Pizza[];
16
}
17
const PizzaMenu = ({ pizzas }: PizzaMenuProps) => {
18
const theme = useTheme();
19
return (
20
<TableContainer component={Paper}>
21
<Table
22
sx={{
23
minWidth: 500,
24
"& .MuiTableCell-head": {
25
backgroundColor: theme.palette.primary.dark,
26
color: theme.palette.primary.contrastText,
27
fontWeight: "bold",
28
},
29
"& .MuiTableCell-body": {
30
backgroundColor: theme.palette.primary.light,
31
color: "white",
32
},
33
"& .MuiTableCell-root": {
34
border: `1px solid ${theme.palette.secondary.main} `,
35
},
36
}}
37
>
38
<TableHead>
39
<TableRow>
40
<TableCell>Pizza</TableCell>
41
<TableCell>Description</TableCell>
42
</TableRow>
43
</TableHead>
44
<TableBody>
45
{pizzas.map((pizza) => (
46
<TableRow key={pizza.id}>
47
<TableCell>{pizza.title}</TableCell>
48
<TableCell>{pizza.content}</TableCell>
49
</TableRow>
50
))}
51
</TableBody>
52
</Table>
53
</TableContainer>
54
);
55
};
56
57
export default PizzaMenu;

Nous utilisons ici la notion de & qui vient du monde CSS / SASS permettant de cibler des classes ou des pseudos-classes imbriquées à l'intérieur d'un sélecteur parent spécifié. Cela facilite la création de règles CSS spécifiques à des contextes particuliers sans avoir à répéter le sélecteur parent complet.
Si vous souhaitez en savoir plus sur cette pratique : https://mui.com/material-ui/customization/how-to-customize/#overriding-nested-component-styles

Voici comment nous stylons le composant DrinkMenu :

tsx
1
import {
2
Container,
3
Card,
4
CardMedia,
5
CardContent,
6
Typography,
7
Grid,
8
useTheme,
9
} from "@mui/material";
10
import { Drink } from "../../types";
11
12
interface DrinkMenuProps {
13
title: string;
14
drinks: Drink[];
15
}
16
17
const DrinkMenu: React.FC<DrinkMenuProps> = ({ title, drinks }) => {
18
const theme = useTheme();
19
20
return (
21
<Container>
22
<Typography
23
variant="h4"
24
gutterBottom
25
sx={{
26
color: theme.palette.primary.contrastText,
27
textAlign: "center",
28
marginTop: 2,
29
}}
30
>
31
{title}
32
</Typography>
33
<Grid container spacing={3}>
34
{drinks.map((drink, index) => (
35
<Grid item xs={12} sm={6} key={index}>
36
<Card>
37
<CardMedia
38
component="img"
39
image={drink.image}
40
alt={drink.title}
41
style={{ objectFit: "contain", height: "200px" }} // Ensure image is fully visible
42
/>
43
<CardContent>
44
<Typography gutterBottom variant="h5" component="div">
45
{drink.title}
46
</Typography>
47
<Typography variant="body2" color="textSecondary" component="p">
48
{drink.volume}
49
</Typography>
50
<Typography variant="body2" color="textSecondary" component="p">
51
Prix: {drink.price}
52
</Typography>
53
</CardContent>
54
</Card>
55
</Grid>
56
))}
57
</Grid>
58
</Container>
59
);
60
};
61
62
export default DrinkMenu;

Voici comment mettre à jour le style du formulaire au sein de AddPizza :

tsx
1
import { useState, SyntheticEvent } from "react";
2
3
import { NewPizza } from "../../types";
4
import { Box, Button, TextField, useTheme } from "@mui/material";
5
6
interface AddPizzaProps {
7
addPizza: (pizza: NewPizza) => void;
8
}
9
10
const AddPizza = ({ addPizza }: AddPizzaProps) => {
11
const theme = useTheme();
12
const [pizza, setPizza] = useState("");
13
const [description, setDescription] = useState("");
14
15
const handleSubmit = (e: SyntheticEvent) => {
16
e.preventDefault();
17
addPizza({ title: pizza, content: description });
18
};
19
20
const handlePizzaChange = (e: SyntheticEvent) => {
21
const pizzaInput = e.target as HTMLInputElement;
22
console.log("change in pizzaInput:", pizzaInput.value);
23
setPizza(pizzaInput.value);
24
};
25
26
const handleDescriptionChange = (e: SyntheticEvent) => {
27
const descriptionInput = e.target as HTMLInputElement;
28
console.log("change in descriptionInput:", descriptionInput.value);
29
setDescription(descriptionInput.value);
30
};
31
32
return (
33
<Box
34
sx={{
35
marginTop: 2,
36
padding: 3,
37
backgroundColor: "secondary.light",
38
borderRadius: 4,
39
boxShadow: 2,
40
}}
41
>
42
<form onSubmit={handleSubmit}>
43
<Box sx={{ marginBottom: 2 }}>
44
<TextField
45
fullWidth
46
id="pizza"
47
name="pizza"
48
label="Pizza"
49
variant="outlined"
50
value={pizza}
51
onChange={handlePizzaChange}
52
required
53
color="primary"
54
sx={{
55
input: { color: theme.palette.secondary.contrastText },
56
}}
57
/>
58
</Box>
59
<Box sx={{ marginBottom: 2 }}>
60
<TextField
61
fullWidth
62
id="description"
63
name="description"
64
label="Description"
65
variant="outlined"
66
value={description}
67
onChange={handleDescriptionChange}
68
required
69
color="primary"
70
sx={{
71
input: { color: theme.palette.secondary.contrastText },
72
}}
73
/>
74
</Box>
75
<Button type="submit" variant="contained" color="primary">
76
Ajouter
77
</Button>
78
</form>
79
</Box>
80
);
81
};
82
83
export default AddPizza;

Nous pouvons maintenant mettre à jour le Footer :

tsx
1
import { Box, Container, Typography, useTheme } from "@mui/material";
2
import logo from "../../assets/images/js-logo.png";
3
import { Copyright } from "@mui/icons-material";
4
5
const Footer = () => {
6
const theme = useTheme();
7
8
return (
9
<Box
10
component="footer"
11
sx={{
12
py: 3,
13
backgroundColor:
14
theme.palette.mode === "light"
15
? theme.palette.secondary.light
16
: theme.palette.secondary.dark,
17
}}
18
>
19
<Container maxWidth="sm">
20
<Box
21
sx={{
22
display: "inline-block",
23
paddingRight: 2,
24
color: theme.palette.secondary.contrastText,
25
}}
26
>
27
<Typography variant="body1">But we also love JS</Typography>
28
<Typography>
29
<Copyright />
30
myAmazingPizzeria
31
</Typography>
32
</Box>
33
<Box sx={{ display: "inline-block" }}>
34
<img src={logo} alt="" width={50} />
35
</Box>
36
</Container>
37
</Box>
38
);
39
};
40
41
export default Footer;

Nous avons maintenant une application qui commence à être bien stylée !

Il y a un souci qui est visible sur toutes les applications offertes par les templates de projet de MUI. Il y a toujours un espace, une sorte de marge en bas de page, après notre Footer.

Pour résoudre ce souci, qui ne semble malheureusement pas documenté sur le Web, il vous est proposé d'ajouter une seule feuille de style à votre application, au niveau de /src/main.tsx, veuillez importer /src/index.css contenant ce code :

css
div#root {
width: 100%;
display: inline-block; /* avoid margins to collapse to avoid vertical scrollbar */
}

Wow, nous avons quelque chose de fonctionnel !

Création de son propre thème

Pour ce tutoriel, nous vous proposons de créer la palette de couleurs la plus simple pour donner les couleurs primaires et secondaires que nous souhaitons pour un site d'une pizzeria.

Il existe des outils très intéressants pour créer ses thèmes et palettes de couleurs. Vous trouvez ceux-ci ici :

Veuillez créer un fichier pour y ajouter la définition d'un nouveau thème dans /src/themes.ts :

ts
import { createTheme } from "@mui/material/styles";
const theme = createTheme({
palette: {
primary: {
main: "#f0483b",
},
secondary: {
main: "#3bf048",
},
},
});
export default theme;

Et pour utiliser ce nouveau thème, nous devons créer un provider qui va "injecter" ce thème dans l'arbre de tous les composants React. Pour ce faire, veuillez mettre à jour /src/main.tsx :

tsx
1
import React from "react";
2
import ReactDOM from "react-dom/client";
3
import "@fontsource/roboto/700.css";
4
import CssBaseline from "@mui/material/CssBaseline";
5
import { ThemeProvider } from "@mui/material/styles";
6
import theme from "./themes";
7
8
import App from "./components/App";
9
import "./index.css";
10
11
ReactDOM.createRoot(document.getElementById("root")!).render(
12
<React.StrictMode>
13
<ThemeProvider theme={theme}>
14
<CssBaseline /> {/* Global CSS reset from Material-UI */}
15
<App />
16
</ThemeProvider>
17
</React.StrictMode>
18
);

Si nécessaire, vous pouvez trouver le code associé à ce tutoriel ici : ui-library.

🍬 Exercice 2.9 : Utilisation de composants MUI

Veuillez créer un nouveau projet sur base d'un copier/coller de l'exercice nommé /exercises/2.7 (gestion de films) dans votre git repo.

Pour une première étape, veuillez remplacer tous les composants TSX que vous pouvez par des composants MUI.

Une fois tout fonctionnel, veuillez faire un commit avec le message suivant : new:ex2.9+

Pour la dernière étape, veuillez créer votre palette de couleur, et styler votre application à l'aide du MUI System.

Une fois votre application peaufinée, veuillez faire un commit avec le message suivant : new:ex2.9++