Use MUI, structure change, fix API
This commit is contained in:
parent
ef9f05e031
commit
8db8781f06
15 changed files with 831 additions and 350 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
frontend/node_modules
|
node_modules
|
||||||
frontend/.parcel-cache
|
.parcel-cache
|
||||||
public/assets/*
|
public/assets/*
|
||||||
!public/assets/.gitkeep
|
!public/assets/.gitkeep
|
||||||
|
|
|
||||||
45
frontend/components/App.js
Normal file
45
frontend/components/App.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
frontend/components/Navbar.js
Normal file
25
frontend/components/Navbar.js
Normal 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;
|
||||||
77
frontend/components/Result.js
Normal file
77
frontend/components/Result.js
Normal 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;
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect, useState } from 'npm:react';
|
import { useEffect, useState } from 'react';
|
||||||
import Form from 'react-bootstrap/Form';
|
import InputAdornment from '@mui/material/InputAdornment';
|
||||||
import InputGroup from 'react-bootstrap/InputGroup';
|
import TextField from '@mui/material/TextField';
|
||||||
import { Search } from 'react-bootstrap-icons';
|
import Search from '@mui/icons-material/Search';
|
||||||
|
|
||||||
function Component({callback}) {
|
function Component({callback}) {
|
||||||
const [searchValue, setSearchValue] = useState("");
|
const [searchValue, setSearchValue] = useState("");
|
||||||
|
|
@ -41,16 +41,26 @@ function Component({callback}) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InputGroup size="lg">
|
<TextField
|
||||||
<InputGroup.Text>
|
label="Rechercher un code postal ou une localité"
|
||||||
<Search />
|
variant="outlined"
|
||||||
</InputGroup.Text>
|
onChange={updateSearch}
|
||||||
<Form.Control
|
value={searchValue}
|
||||||
placeholder="Rechercher un code postal ou une localité"
|
slotProps={{
|
||||||
onChange={updateSearch}
|
input: {
|
||||||
value={searchValue}
|
startAdornment: (
|
||||||
/>
|
<InputAdornment position="start">
|
||||||
</InputGroup>
|
<Search />
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
marginTop: "1.5rem",
|
||||||
|
marginBottom: "1.5rem"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
12
frontend/main.js
Normal file
12
frontend/main.js
Normal 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 />);
|
||||||
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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 />);
|
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"source": "src/main.js",
|
"source": "frontend/main.js",
|
||||||
"targets": {
|
"targets": {
|
||||||
"default": {
|
"default": {
|
||||||
"distDir": "../public/assets"
|
"distDir": "public/assets"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"buffer": "^5.5.0||^6.0.0",
|
"buffer": "^6.0.3",
|
||||||
"parcel": "^2.13.3",
|
"parcel": "^2.13.3",
|
||||||
"process": "^0.11.10"
|
"process": "^0.11.10"
|
||||||
},
|
},
|
||||||
|
|
@ -35,11 +35,14 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"axios": "^1.8.1",
|
||||||
"bootstrap": "^5.3.3",
|
"notistack": "^3.0.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-bootstrap": "^2.10.9",
|
|
||||||
"react-bootstrap-icons": "^1.11.5",
|
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
861
frontend/pnpm-lock.yaml → pnpm-lock.yaml
generated
861
frontend/pnpm-lock.yaml → pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -44,7 +44,10 @@ if (isset($json->zips) && !empty($json->zips)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$zips[(int)$zip->zip] = $zip->city27;
|
$zips[] = [
|
||||||
|
"zip" => (int)$zip->zip,
|
||||||
|
"city" => $zip->city27
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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">
|
<link rel="stylesheet" href="/assets/main.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue