Initial commit
This commit is contained in:
commit
ef9f05e031
12 changed files with 3801 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
frontend/node_modules
|
||||||
|
frontend/.parcel-cache
|
||||||
|
public/assets/*
|
||||||
|
!public/assets/.gitkeep
|
||||||
45
frontend/package.json
Normal file
45
frontend/package.json
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"source": "src/main.js",
|
||||||
|
"targets": {
|
||||||
|
"default": {
|
||||||
|
"distDir": "../public/assets"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"watch": "parcel watch",
|
||||||
|
"build": "parcel build"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"packageManager": "pnpm@10.5.2",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.0.10",
|
||||||
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"buffer": "^5.5.0||^6.0.0",
|
||||||
|
"parcel": "^2.13.3",
|
||||||
|
"process": "^0.11.10"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"@parcel/watcher",
|
||||||
|
"@swc/core",
|
||||||
|
"core-js",
|
||||||
|
"deasync",
|
||||||
|
"lmdb",
|
||||||
|
"msgpackr-extract",
|
||||||
|
"parcel-bundler"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.8.1",
|
||||||
|
"bootstrap": "^5.3.3",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-bootstrap": "^2.10.9",
|
||||||
|
"react-bootstrap-icons": "^1.11.5",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
3530
frontend/pnpm-lock.yaml
generated
Normal file
3530
frontend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
32
frontend/src/components/App.js
Normal file
32
frontend/src/components/App.js
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
frontend/src/components/Navbar.js
Normal file
14
frontend/src/components/Navbar.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
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;
|
||||||
42
frontend/src/components/Result.js
Normal file
42
frontend/src/components/Result.js
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
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;
|
||||||
58
frontend/src/components/Search.js
Normal file
58
frontend/src/components/Search.js
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { useEffect, useState } from 'npm:react';
|
||||||
|
import Form from 'react-bootstrap/Form';
|
||||||
|
import InputGroup from 'react-bootstrap/InputGroup';
|
||||||
|
import { Search } from 'react-bootstrap-icons';
|
||||||
|
|
||||||
|
function Component({callback}) {
|
||||||
|
const [searchValue, setSearchValue] = useState("");
|
||||||
|
|
||||||
|
const updateSearch = event => {
|
||||||
|
var value = event.target.value;
|
||||||
|
setSearchValue(value);
|
||||||
|
parseSearch(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseSearch = value => {
|
||||||
|
var zip = 0;
|
||||||
|
var findZip = value.match(/[0-9]{4}/g);
|
||||||
|
if (findZip) {
|
||||||
|
zip = parseInt(findZip[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var city = "";
|
||||||
|
var findCity = value.replace(/[0-9]/g, '');
|
||||||
|
if (findCity.length > 0) {
|
||||||
|
city = findCity.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(value, zip, city);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const paramsString = window.location.search;
|
||||||
|
const searchParams = new URLSearchParams(paramsString);
|
||||||
|
const value = searchParams.get("s");
|
||||||
|
|
||||||
|
if (value && value.length > 0) {
|
||||||
|
setSearchValue(value);
|
||||||
|
parseSearch(value);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<InputGroup size="lg">
|
||||||
|
<InputGroup.Text>
|
||||||
|
<Search />
|
||||||
|
</InputGroup.Text>
|
||||||
|
<Form.Control
|
||||||
|
placeholder="Rechercher un code postal ou une localité"
|
||||||
|
onChange={updateSearch}
|
||||||
|
value={searchValue}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Component;
|
||||||
0
frontend/src/main.css
Normal file
0
frontend/src/main.css
Normal file
9
frontend/src/main.js
Normal file
9
frontend/src/main.js
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
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 />);
|
||||||
54
public/api/query.php
Normal file
54
public/api/query.php
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
function getParam(string $name, string $default = ""): string {
|
||||||
|
if (isset($_GET[$name]) && !empty($_GET[$name])) {
|
||||||
|
return (string)$_GET[$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParamInt(string $name, int $default = 0): int {
|
||||||
|
if (isset($_GET[$name]) && !empty($_GET[$name])) {
|
||||||
|
return (int)$_GET[$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip = getParamInt("zip", 0);
|
||||||
|
$city = getParam("city", "");
|
||||||
|
$time = time();
|
||||||
|
|
||||||
|
$url = "https://service.post.ch/zopa/app/api/addresschecker/v1/zips?limit=9999&noCache={$time}";
|
||||||
|
|
||||||
|
if (strlen($city) > 0) {
|
||||||
|
$url .= "&city={$city}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($zip >= 1000 && $zip <= 9999) {
|
||||||
|
$url .= "&zip={$zip}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = file_get_contents($url);
|
||||||
|
$json = json_decode($response);
|
||||||
|
|
||||||
|
$zips = [];
|
||||||
|
if (isset($json->zips) && !empty($json->zips)) {
|
||||||
|
foreach ($json->zips as $zip) {
|
||||||
|
if (!isset($zip->zip) || empty($zip->zip)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($zip->city27) || empty($zip->city27)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$zips[(int)$zip->zip] = $zip->city27;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header("Content-Type: application/json");
|
||||||
|
echo json_encode([
|
||||||
|
"zips" => $zips
|
||||||
|
], JSON_PRETTY_PRINT);
|
||||||
0
public/assets/.gitkeep
Normal file
0
public/assets/.gitkeep
Normal file
13
public/index.html
Normal file
13
public/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Codes postaux | Suisse</title>
|
||||||
|
<link rel="stylesheet" href="/assets/main.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script src="/assets/main.js" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue