first update

This commit is contained in:
sebvtl728
2025-01-02 13:39:40 +01:00
parent 32dc34afcf
commit 27d4a1e6a0
26 changed files with 7611 additions and 0 deletions
+365
View File
@@ -0,0 +1,365 @@
import React, { useState } from "react";
import {
AppBar,
Toolbar,
Typography,
Button,
Box,
Drawer,
List,
ListItem,
ListItemText,
TextField,
IconButton,
Divider,
Menu,
MenuItem,
CircularProgress,
} from "@mui/material";
import { Link, useLocation, useNavigate } from "react-router-dom";
import MenuIcon from "@mui/icons-material/Menu";
import SearchIcon from "@mui/icons-material/Search";
import HomeIcon from "@mui/icons-material/Home";
import ArticleIcon from "@mui/icons-material/Article";
import InfoIcon from "@mui/icons-material/Info";
import ContactMailIcon from "@mui/icons-material/ContactMail";
import WorkIcon from "@mui/icons-material/Work";
const Header = () => {
const [drawerOpen, setDrawerOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [isSearching, setIsSearching] = useState(false);
const [anchorEl, setAnchorEl] = useState(null); // Pour gérer le menu Services
const location = useLocation();
const navigate = useNavigate();
const toggleDrawer = (open) => (event) => {
if (
event.type === "keydown" &&
(event.key === "Tab" || event.key === "Shift")
) {
return;
}
setDrawerOpen(open);
};
const handleSearch = () => {
if (searchQuery.trim()) {
setIsSearching(true);
setTimeout(() => {
navigate(`/search?query=${encodeURIComponent(searchQuery.trim())}`);
setIsSearching(false);
}, 500);
}
};
const handleMenuOpen = (event) => {
setAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setAnchorEl(null);
};
const commonButtonStyles = {
transition: "color 0.3s",
position: "relative",
"&:hover": { color: "#00bcd4" },
"&:after": {
content: '""',
position: "absolute",
width: "0",
height: "2px",
bottom: 0,
left: 0,
backgroundColor: "#00bcd4",
transition: "width 0.3s",
},
"&:hover:after": {
width: "100%",
},
};
return (
<AppBar
position="static"
color="transparent"
elevation={0}
sx={{
background: "linear-gradient(to right, #0e467f, #00bcd4)",
color: "white",
transition: "all 0.3s ease",
}}
>
<Toolbar>
{/* Mobile menu icon */}
<IconButton
edge="start"
color="inherit"
aria-label="menu"
onClick={toggleDrawer(true)}
sx={{ display: { xs: "block", md: "none" } }}
>
<MenuIcon />
</IconButton>
{/* Logo as Home Link */}
<Typography
variant="h6"
component={Link}
to="/"
sx={{
flexGrow: 1,
textAlign: { xs: "center", md: "left" },
fontWeight: "bold",
textDecoration: "none",
color: "white",
"&:hover": {
color: "#00bcd4",
},
}}
>
Mon Site React
</Typography>
{/* Navigation Links */}
<Box sx={{ display: { xs: "none", md: "flex" }, gap: 2 }}>
<Button
component={Link}
to="/posts"
color="inherit"
sx={{
fontWeight: location.pathname === "/posts" ? "bold" : "normal",
textDecoration:
location.pathname === "/posts" ? "underline" : "none",
...commonButtonStyles,
}}
>
Articles
</Button>
{/* Services Dropdown */}
<Button
color="inherit"
aria-controls="services-menu"
aria-haspopup="true"
onClick={handleMenuOpen}
sx={{
fontWeight: location.pathname.includes("/services")
? "bold"
: "normal",
textDecoration: location.pathname.includes("/services")
? "underline"
: "none",
...commonButtonStyles,
}}
>
Services
</Button>
<Menu
id="services-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
MenuListProps={{
"aria-labelledby": "services-button",
}}
>
<MenuItem
component={Link}
to="/services/formation-web"
onClick={handleMenuClose}
>
<WorkIcon sx={{ marginRight: 1 }} />
Formation Web
</MenuItem>
<MenuItem
component={Link}
to="/services/formation-ia"
onClick={handleMenuClose}
>
<WorkIcon sx={{ marginRight: 1 }} />
Formation IA
</MenuItem>
<MenuItem
component={Link}
to="/services/formation-video"
onClick={handleMenuClose}
>
<WorkIcon sx={{ marginRight: 1 }} />
Formation Video
</MenuItem>
</Menu>
<Button
component={Link}
to="/about"
color="inherit"
sx={{
fontWeight: location.pathname === "/about" ? "bold" : "normal",
textDecoration:
location.pathname === "/about" ? "underline" : "none",
...commonButtonStyles,
}}
>
À propos
</Button>
<Button
component={Link}
to="/contact"
color="inherit"
sx={{
fontWeight: location.pathname === "/contact" ? "bold" : "normal",
textDecoration:
location.pathname === "/contact" ? "underline" : "none",
...commonButtonStyles,
}}
>
Contact
</Button>
</Box>
{/* Search Field */}
<Box
component="form"
onSubmit={(e) => {
e.preventDefault();
handleSearch();
}}
sx={{
display: { xs: "none", md: "flex" },
alignItems: "center",
gap: 1,
}}
>
<label
htmlFor="search-input"
style={{ position: "absolute", left: "-9999px" }}
>
Rechercher sur le site
</label>
<TextField
id="search-input"
variant="outlined"
size="small"
placeholder="Rechercher..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
sx={{
backgroundColor: "rgba(255, 255, 255, 0.9)",
borderRadius: 1,
"& .MuiOutlinedInput-root": {
"& fieldset": {
borderColor: "white",
},
"&:hover fieldset": {
borderColor: "#00bcd4",
},
"&.Mui-focused fieldset": {
borderColor: "#00bcd4",
},
},
}}
/>
<IconButton type="submit" color="inherit" aria-label="Rechercher">
{isSearching ? (
<CircularProgress size={20} color="inherit" />
) : (
<SearchIcon />
)}
</IconButton>
</Box>
{/* Drawer for mobile */}
<Drawer anchor="left" open={drawerOpen} onClose={toggleDrawer(false)}>
<Box
sx={{
width: 250,
backgroundColor: "#f5f5f5",
height: "100%",
padding: 2,
}}
role="presentation"
onClick={toggleDrawer(false)}
onKeyDown={toggleDrawer(false)}
>
<Typography
variant="h6"
sx={{ textAlign: "center", marginBottom: 2 }}
>
Menu
</Typography>
<Divider />
<List>
<ListItem
button
component={Link}
to="/"
selected={location.pathname === "/"}
>
<HomeIcon sx={{ marginRight: 1 }} />
<ListItemText primary="Accueil" />
</ListItem>
<ListItem
button
component={Link}
to="/posts"
selected={location.pathname === "/posts"}
>
<ArticleIcon sx={{ marginRight: 1 }} />
<ListItemText primary="Articles" />
</ListItem>
<ListItem
button
component={Link}
to="/services/formation-web"
selected={location.pathname.includes("/services/formation-web")}
>
<WorkIcon sx={{ marginRight: 1 }} />
<ListItemText primary="Formation Web" />
</ListItem>
<ListItem
button
component={Link}
to="/services/formation-ia"
selected={location.pathname.includes("/services/formation-ia")}
>
<WorkIcon sx={{ marginRight: 1 }} />
<ListItemText primary="Formation IA" />
</ListItem>
<ListItem
button
component={Link}
to="/services/formation-video"
selected={location.pathname.includes("/services/formation-video")}
>
<WorkIcon sx={{ marginRight: 1 }} />
<ListItemText primary="Formation Video" />
</ListItem>
<ListItem
button
component={Link}
to="/about"
selected={location.pathname === "/about"}
>
<InfoIcon sx={{ marginRight: 1 }} />
<ListItemText primary="À propos" />
</ListItem>
<ListItem
button
component={Link}
to="/contact"
selected={location.pathname === "/contact"}
>
<ContactMailIcon sx={{ marginRight: 1 }} />
<ListItemText primary="Contact" />
</ListItem>
</List>
</Box>
</Drawer>
</Toolbar>
</AppBar>
);
};
export default Header;
+71
View File
@@ -0,0 +1,71 @@
import React from 'react';
import { Box, Typography, Button } from '@mui/material';
const Hero = ({ backgroundImage, useGradient, title, titleColor, text, textColor }) => {
// Priorité pour appliquer le fond : Image > Dégradé > Aucun fond
const backgroundStyle = backgroundImage
? `url(${backgroundImage}) center/cover no-repeat`
: useGradient
? 'linear-gradient(to bottom, #0e467f, rgb(9, 48, 87))'
: '#0e467f'; // Fallback couleur
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: { xs: '50vh', md: '70vh', lg: '80vh' }, // Responsive
background: backgroundStyle,
color: 'white',
textAlign: 'center',
padding: '20px',
transition: 'background 0.5s ease-in-out', // Transition fluide pour le fond
}}
>
{/* Titre */}
<Typography
variant="h1"
sx={{
fontWeight: 'bold',
mb: 2,
color: titleColor || '#ffffff', // Couleur par défaut
fontSize: { xs: '2rem', md: '3rem', lg: '4rem' }, // Responsive
}}
>
{title}
</Typography>
{/* Texte */}
<Typography
variant="body1"
sx={{
mb: 4,
color: textColor || '#ffffff', // Couleur par défaut
fontSize: { xs: '1rem', md: '1.2rem', lg: '1.5rem' }, // Responsive
}}
>
{text}
</Typography>
{/* Bouton */}
<Button
variant="contained"
color="secondary"
size="large"
href="/posts"
sx={{
textTransform: 'none',
fontWeight: 'bold',
padding: { xs: '10px 20px', md: '15px 30px' }, // Responsive
fontSize: { xs: '0.8rem', md: '1rem' },
}}
>
Explorer les Articles
</Button>
</Box>
);
};
export default Hero;
+12
View File
@@ -0,0 +1,12 @@
import React from 'react';
const About = () => {
return (
<div>
<h1>À propos</h1>
<p>Bienvenue sur la page À propos.</p>
</div>
);
};
export default About;
+40
View File
@@ -0,0 +1,40 @@
import React, { useState, useEffect } from 'react';
import SEO from '../SEO';
import api from '../../api';
const Contact = () => {
// États pour le SEO
const [metaTitle, setMetaTitle] = useState('Titre par défaut');
const [metaDescription, setMetaDescription] = useState('Description par défaut.');
useEffect(() => {
const fetchPageData = async () => {
try {
// Appel à l'API pour récupérer les données
const response = await api.get('wp/v2/pages/116?_fields=acf,rank_math_title,rank_math_description');
const { rank_math_title, rank_math_description } = response.data;
// Mise à jour des métadonnées SEO
setMetaTitle(rank_math_title || 'Titre par défaut');
setMetaDescription(rank_math_description || 'Description par défaut.');
} catch (error) {
console.error('Erreur lors de la récupération des données :', error);
}
};
fetchPageData();
}, []); // Exécution au premier rendu
return (
<div>
{/* SEO */}
<SEO title={metaTitle} description={metaDescription} />
{/* Contenu de la page */}
<h1>Contact</h1>
<p>Bienvenue sur la page Contact.</p>
</div>
);
};
export default Contact;
+99
View File
@@ -0,0 +1,99 @@
import React, { useState, useEffect } from 'react';
import Hero from '../Hero';
import SEO from '../SEO';
import api from '../../api'; // Instance Axios configurée
const Home = () => {
const [backgroundImage, setBackgroundImage] = useState(null);
const [useGradient, setUseGradient] = useState(false);
const [heroTitle, setHeroTitle] = useState('Titre par défaut');
const [heroTitleColor, setHeroTitleColor] = useState('#ffffff');
const [heroText, setHeroText] = useState('Texte par défaut');
const [heroTextColor, setHeroTextColor] = useState('#ffffff');
const [metaTitle, setMetaTitle] = useState('Titre par défaut');
const [metaDescription, setMetaDescription] = useState('Description par défaut.');
const [isLoading, setIsLoading] = useState(true); // État de chargement
const [error, setError] = useState(null); // État des erreurs
useEffect(() => {
const fetchPageData = async () => {
try {
setIsLoading(true);
setError(null);
const response = await api.get('wp/v2/pages/13?_fields=acf,rank_math_title,rank_math_description');
const { acf, rank_math_title, rank_math_description } = response.data;
// Métadonnées SEO
setMetaTitle(rank_math_title || 'Titre par défaut');
setMetaDescription(rank_math_description || 'Description par défaut.');
// Données Hero
setUseGradient(acf?.enable_gradient ?? false);
setHeroTitle(acf?.hero_title || 'Titre par défaut');
setHeroTitleColor(acf?.hero_title_color || '#ffffff');
setHeroText(acf?.hero_text || 'Texte par défaut');
setHeroTextColor(acf?.hero_text_color || '#ffffff');
// Image de fond
if (acf?.img_hero) {
const mediaResponse = await api.get(`wp/v2/media/${acf.img_hero}`);
setBackgroundImage(mediaResponse.data.source_url);
} else {
setBackgroundImage(null);
}
} catch (error) {
console.error('Erreur lors de la récupération des données :', error);
setError('Impossible de charger les données. Veuillez réessayer.');
} finally {
setIsLoading(false);
}
};
fetchPageData();
}, []);
if (isLoading) {
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<p>Chargement des données...</p>
</div>
);
}
if (error) {
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<p style={{ color: 'red' }}>{error}</p>
</div>
);
}
return (
<div>
{/* SEO */}
<SEO title={metaTitle} description={metaDescription} />
{/* Hero */}
<Hero
backgroundImage={backgroundImage}
useGradient={useGradient}
title={heroTitle}
titleColor={heroTitleColor}
text={heroText}
textColor={heroTextColor}
/>
{/* Contenu */}
<div style={{ padding: '20px', textAlign: 'center' }}>
<h2>À propos de notre site</h2>
<p>
Bienvenue sur notre site ! Découvrez nos articles, explorez des sujets passionnants,
et profitez d'une expérience unique. Nous sommes heureux de vous accueillir.
</p>
</div>
</div>
);
};
export default Home;
+63
View File
@@ -0,0 +1,63 @@
import React, { useEffect, useState } from 'react';
import { Grid, Card, CardContent, CardMedia, Typography, Box } from '@mui/material';
import api from '../../api'; // Adaptez le chemin vers l'instance Axios
const Post = () => {
const [posts, setPosts] = useState([]);
useEffect(() => {
api.get('wp/v2/posts?_embed')
.then((response) => {
setPosts(response.data);
})
.catch((error) => {
console.error('Erreur lors de la récupération des articles :', error);
});
}, []);
return (
<Box sx={{ padding: 4 }}>
<Typography variant="h3" sx={{ marginBottom: 4, textAlign: 'center' }}>
Liste des Articles
</Typography>
<Grid container spacing={4}>
{posts.map((post) => (
<Grid item xs={12} sm={6} md={4} key={post.id}>
<Card sx={{ maxWidth: 345,
boxShadow: 3,
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
'&:hover': {
transform: 'scale(1.05)',
boxShadow: 6,
},
}}>
{/* Image de l'article */}
{post._embedded?.['wp:featuredmedia']?.[0]?.source_url && (
<CardMedia
component="img"
height="200"
image={post._embedded['wp:featuredmedia'][0].source_url}
alt={post.title.rendered}
/>
)}
<CardContent>
<Typography variant="h5" component="div" gutterBottom>
{post.title.rendered}
</Typography>
<Typography
variant="body2"
color="text.secondary"
dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }}
/>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Box>
);
};
export default Post;
+50
View File
@@ -0,0 +1,50 @@
import React, { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
import api from "../../api";
const Search = () => {
const [searchParams] = useSearchParams();
const query = searchParams.get("query"); // Récupère la valeur de "query"
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchSearchResults = async () => {
try {
setLoading(true);
const response = await api.get(`wp/v2/posts?search=${query}`);
setResults(response.data);
setLoading(false);
} catch (error) {
console.error("Erreur lors de la recherche :", error);
setLoading(false);
}
};
if (query) {
fetchSearchResults();
}
}, [query]);
return (
<div style={{ padding: "20px" }}>
<h1>Résultats pour : {query}</h1>
{loading ? (
<p>Chargement...</p>
) : results.length > 0 ? (
<ul>
{results.map((post) => (
<li key={post.id}>
<h2>{post.title.rendered}</h2>
<div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
</li>
))}
</ul>
) : (
<p>Aucun résultat trouvé.</p>
)}
</div>
);
};
export default Search;
+22
View File
@@ -0,0 +1,22 @@
import React from 'react';
import { Helmet } from 'react-helmet-async';
const SEO = ({ title, description }) => {
return (
<Helmet>
{/* Title et Description */}
<title>{title || 'Titre par défaut'}</title>
<meta name="description" content={description || 'Description par défaut.'} />
{/* Preconnect */}
<link rel="preconnect" href="https://octopusdesign.fr" />
<link rel="preconnect" href="https://preprod.octopusdesign.fr" />
{/* DNS Prefetch */}
<link rel="dns-prefetch" href="https://octopusdesign.fr" />
<link rel="dns-prefetch" href="https://preprod.octopusdesign.fr" />
</Helmet>
);
};
export default SEO;