Initial commit

This commit is contained in:
William Bouzourène 2025-03-04 20:26:33 +01:00
commit ef9f05e031
12 changed files with 3801 additions and 0 deletions

4
.gitignore vendored Normal file
View file

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

45
frontend/package.json Normal file
View 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

File diff suppressed because it is too large Load diff

View 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>
</>
);
}

View 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;

View 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;

View 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
View file

9
frontend/src/main.js Normal file
View 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
View 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
View file

13
public/index.html Normal file
View 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>