Use MUI, structure change, fix API

This commit is contained in:
William Bouzourène 2025-03-05 11:11:39 +01:00
parent ef9f05e031
commit 8db8781f06
15 changed files with 831 additions and 350 deletions

4
.gitignore vendored
View file

@ -1,4 +1,4 @@
frontend/node_modules
frontend/.parcel-cache
node_modules
.parcel-cache
public/assets/*
!public/assets/.gitkeep

View file

@ -0,0 +1,45 @@
import { useState } from 'react';
import CssBaseline from '@mui/material/CssBaseline';
import Container from '@mui/material/Container';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { SnackbarProvider } from 'notistack';
import NavbarComponent from './Navbar';
import SearchComponent from './Search';
import ResultComponent from './Result';
const darkTheme = createTheme({
palette: {
mode: 'dark',
},
});
export default function App() {
const [zip, setZip] = useState(0);
const [city, setCity] = useState("");
function handleSearch(value, zip, city) {
setZip(zip);
setCity(city);
if (window.history.replaceState) {
value = encodeURIComponent(value);
window.history.replaceState({}, null, `?s=${value}`);
}
}
return (
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<SnackbarProvider>
<NavbarComponent />
<Container sx={{ marginY: "1rem" }}>
<SearchComponent callback={handleSearch} />
</Container>
<Container sx={{ marginBottom: "2rem" }}>
<ResultComponent zip={zip} city={city} />
</Container>
</SnackbarProvider>
</ThemeProvider>
);
}

View file

@ -0,0 +1,25 @@
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import Place from '@mui/icons-material/Place';
import Container from '@mui/material/Container';
function Component() {
return (
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static">
<Container>
<Toolbar>
<Place sx={{ marginRight: ".5rem" }} />
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Chercher un code postal en Suisse
</Typography>
</Toolbar>
</Container>
</AppBar>
</Box>
);
}
export default Component;

View file

@ -0,0 +1,77 @@
import { useEffect, useState } from 'react';
import axios from 'axios';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import IconButton from '@mui/material/IconButton';
import ContentCopyRounded from '@mui/icons-material/ContentCopyRounded';
import { useSnackbar } from 'notistack';
function Component({zip, city}) {
const [zips, setZips] = useState([]);
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const fetchResults = (zip, city) => {
city = encodeURIComponent(city);
axios.get(`/api/query.php?zip=${zip}&city=${city}`)
.then(function (response) {
if (response.data.zips) {
setZips(response.data.zips);
}
})
.catch(function (error) {
console.log(error);
});
}
const copyValue = (value) => {
navigator.clipboard.writeText(value);
enqueueSnackbar("Copié dans le presse-papier", {
autoHideDuration: 2000,
variant: "success",
anchorOrigin: {
vertical: "top",
horizontal: "center"
}
});
}
useEffect(() => {
if (zip > 0 || city.length >= 4) {
fetchResults(zip, city);
} else {
setZips([]);
}
}, [zip, city]);
return (
<>
<List sx={{ width: '100%' }}>
{zips.map((zip, i) => {
return (
<ListItem
key={i}
secondaryAction={
<IconButton
edge="end"
aria-label="Copier"
onClick={() => {copyValue(`${zip.zip} ${zip.city}`)}}
>
<ContentCopyRounded />
</IconButton>
}
disablePadding
>
<ListItemButton onClick={() => {copyValue(`${zip.zip} ${zip.city}`)}}>
<ListItemText primary={`${zip.zip} ${zip.city}`} />
</ListItemButton>
</ListItem>
);
})}
</List>
</>
);
}
export default Component;

View file

@ -1,7 +1,7 @@
import { useEffect, useState } from 'npm:react';
import Form from 'react-bootstrap/Form';
import InputGroup from 'react-bootstrap/InputGroup';
import { Search } from 'react-bootstrap-icons';
import { useEffect, useState } from 'react';
import InputAdornment from '@mui/material/InputAdornment';
import TextField from '@mui/material/TextField';
import Search from '@mui/icons-material/Search';
function Component({callback}) {
const [searchValue, setSearchValue] = useState("");
@ -41,16 +41,26 @@ function Component({callback}) {
return (
<>
<InputGroup size="lg">
<InputGroup.Text>
<Search />
</InputGroup.Text>
<Form.Control
placeholder="Rechercher un code postal ou une localité"
onChange={updateSearch}
value={searchValue}
/>
</InputGroup>
<TextField
label="Rechercher un code postal ou une localité"
variant="outlined"
onChange={updateSearch}
value={searchValue}
slotProps={{
input: {
startAdornment: (
<InputAdornment position="start">
<Search />
</InputAdornment>
)
}
}}
sx={{
width: "100%",
marginTop: "1.5rem",
marginBottom: "1.5rem"
}}
/>
</>
);
}

12
frontend/main.js Normal file
View file

@ -0,0 +1,12 @@
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import { createRoot } from 'react-dom/client';
import './main.css';
import App from './components/App';
const container = document.getElementById("app");
const root = createRoot(container);
root.render(<App />);

View file

@ -1,32 +0,0 @@
import { useState } from 'react';
import Container from 'react-bootstrap/Container';
import NavbarComponent from './Navbar';
import SearchComponent from './Search';
import ResultComponent from './Result';
export function App() {
const [zip, setZip] = useState(0);
const [city, setCity] = useState("");
function handleSearch(value, zip, city) {
setZip(zip);
setCity(city);
if (window.history.replaceState) {
value = encodeURIComponent(value);
window.history.replaceState({}, null, `?s=${value}`);
}
}
return (
<>
<NavbarComponent />
<Container className="my-4">
<SearchComponent callback={handleSearch} />
</Container>
<Container className="my-4">
<ResultComponent zip={zip} city={city} />
</Container>
</>
);
}

View file

@ -1,14 +0,0 @@
import Container from 'react-bootstrap/Container';
import Navbar from 'react-bootstrap/Navbar';
function Component() {
return (
<Navbar expand="lg" className="navbar-dark bg-danger">
<Container>
<Navbar.Brand>Codes postaux | <b>Suisse</b></Navbar.Brand>
</Container>
</Navbar>
);
}
export default Component;

View file

@ -1,42 +0,0 @@
import { useEffect, useState } from 'react';
import axios from 'axios';
import { Card } from 'react-bootstrap';
function Component({zip, city}) {
const [zips, setZips] = useState(Object);
const fetchResults = (zip, city) => {
city = encodeURIComponent(city);
axios.get(`/api/query.php?zip=${zip}&city=${city}`)
.then(function (response) {
if (response.data.zips) {
setZips(response.data.zips);
}
})
.catch(function (error) {
console.log(error);
});
}
useEffect(() => {
if (zip > 0 || city.length >= 4) {
fetchResults(zip, city);
} else {
setZips((new Object));
}
}, [zip, city]);
return (
<>
{Object.entries(zips).map((zip, i) => {
return (
<Card body key={i} className="mb-2">
<b>{zip[0]}</b> {zip[1]}
</Card>
)
})}
</>
);
}
export default Component;

View file

@ -1,9 +0,0 @@
import "npm:bootstrap/dist/css/bootstrap.css"
import "./main.css"
import { createRoot } from "react-dom/client";
import { App } from "./components/App";
const container = document.getElementById("app");
const root = createRoot(container);
root.render(<App />);

View file

@ -2,10 +2,10 @@
"name": "frontend",
"version": "1.0.0",
"description": "",
"source": "src/main.js",
"source": "frontend/main.js",
"targets": {
"default": {
"distDir": "../public/assets"
"distDir": "public/assets"
}
},
"scripts": {
@ -19,7 +19,7 @@
"devDependencies": {
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"buffer": "^5.5.0||^6.0.0",
"buffer": "^6.0.3",
"parcel": "^2.13.3",
"process": "^0.11.10"
},
@ -35,11 +35,14 @@
]
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@fontsource/roboto": "^5.2.5",
"@mui/icons-material": "^6.4.6",
"@mui/material": "^6.4.6",
"axios": "^1.8.1",
"bootstrap": "^5.3.3",
"notistack": "^3.0.2",
"react": "^19.0.0",
"react-bootstrap": "^2.10.9",
"react-bootstrap-icons": "^1.11.5",
"react-dom": "^19.0.0"
}
}

File diff suppressed because it is too large Load diff

View file

@ -44,7 +44,10 @@ if (isset($json->zips) && !empty($json->zips)) {
continue;
}
$zips[(int)$zip->zip] = $zip->city27;
$zips[] = [
"zip" => (int)$zip->zip,
"city" => $zip->city27
];
}
}

View file

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Codes postaux | Suisse</title>
<title>Chercher un code postal en Suisse</title>
<link rel="stylesheet" href="/assets/main.css">
</head>
<body>