make frontend

This commit is contained in:
Simon 2023-02-06 19:39:22 +01:00
parent 96a6ef446a
commit 0e93a44421
13 changed files with 253 additions and 27 deletions

6
frontend/.prettierrc.tml Normal file
View File

@ -0,0 +1,6 @@
# .prettierrc.toml
trailingComma = "all"
tabWidth = 4
semi = true
singleQuote = false
jsxSingleQuote = false

View File

@ -0,0 +1,6 @@
# .prettierrc.toml
trailingComma = "all"
tabWidth = 4
semi = true
singleQuote = false
jsxSingleQuote = false

15
frontend/Makefile Normal file
View File

@ -0,0 +1,15 @@
.PHONY: build watch check clean
build:
esbuild src/main.ts --outfile=bundle.js --minify --sourcemap --bundle
watch:
esbuild src/main.ts --outfile=bundle.js --minify --sourcemap --bundle --watch
check:
tsc --noEmit -p tsconfig.json
clean:
rm -rf bundle.js bundle.js.map

View File

@ -1,5 +0,0 @@
set -xe
esbuild src/main.ts --outfile=bundle.js --minify --sourcemap --bundle

View File

@ -1,5 +0,0 @@
set -xe
esbuild src/main.ts --outfile=bundle.js --minify --sourcemap --bundle --watch

View File

@ -1,7 +0,0 @@
{
"version": 3,
"sources": ["src/main.ts"],
"sourcesContent": ["\nfunction main() {\n console.log(\"hello world\");\n}\n\nmain();\n\n"],
"mappings": "MACA,SAASA,GAAO,CACZ,QAAQ,IAAI,aAAa,CAC7B,CAEAA,EAAK",
"names": ["main"]
}

View File

@ -1,5 +0,0 @@
set -xe
tsc src/main.ts --noEmit

View File

@ -5,14 +5,25 @@
<link rel="stylesheet" href="style.css">
<script src="bundle.js" defer></script>
<title>Postnummer App</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
</head>
<body>
<div id="topbar">
<h2>Postnummer App</h2>
<h1>Postnummer App</h1>
</div>
<main>
<div id="search-bar">
<input id="search-input" placeholder="Postnummer">
<button id="search-button">Search</button>
</div>
<img src="map.jpg" id="map"><br>
<div id="debug"></div>
<p id="mouse-position"></p>
<p id="coords"></p>
<p id="zip-code"></p>
</main>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 122 KiB

View File

@ -1,7 +1,131 @@
import { Throttler } from "./utils";
type Position = {
x: number;
y: number;
};
type Size = { width: number; height: number };
type Coordinate = {
longitude: number;
latitude: number;
};
type ZipCodeReverseResponse = {
nr: number | null;
navn: string;
};
async function fetchZipCode({
longitude,
latitude,
}: Coordinate): Promise<ZipCodeReverseResponse> {
return fetch(
`https://api.dataforsyningen.dk/postnumre/reverse?x=${longitude}&y=${latitude}`,
)
.then((request) => request.json())
.then((data) => {
let zipCode = parseInt(data.nr);
return {
...data,
nr: isNaN(zipCode) ? null : zipCode,
} as ZipCodeReverseResponse;
})
.catch(() => null as never);
}
function convertPixelsToCoordinate(mouse: Position, map: Size): Coordinate {
const scalar = { x: 8, y: 3.6 };
const offset = { x: 6.2, y: 57.93 };
return {
longitude: (mouse.x / map.width) * scalar.x + offset.x,
latitude: Math.abs((mouse.y / map.height) * scalar.y - offset.y),
};
}
function displayMousePosition(element: HTMLParagraphElement, mouse: Position) {
element.innerHTML = `Mouse position: <code>(${mouse.x}px, ${mouse.y}px)</code>`;
}
function displayCoords(element: HTMLParagraphElement, coords: Coordinate) {
element.innerHTML = `Coords: <code>${coords.longitude.toFixed(
3,
)}, ${coords.latitude.toFixed(3)}</code>`;
}
function displayZipCode(
element: HTMLParagraphElement,
zipCode: number | null,
name: string,
) {
element.innerHTML =
zipCode === null
? `Postnummer ikke fundet`
: `Postnummer: <code>${zipCode}</code>, ${name}`;
}
function setupMap(
mousePositionElement: HTMLParagraphElement,
coordsElement: HTMLParagraphElement,
zipCodeElement: HTMLParagraphElement,
) {
const mapImg = document.querySelector<HTMLImageElement>("#map")!;
const fetcher = new Throttler(500);
mapImg.onmousemove = async (event: MouseEvent) => {
const mousePosition: Position = { x: event.offsetX, y: event.offsetY };
displayMousePosition(mousePositionElement, mousePosition);
const mapSize: Size = {
width: mapImg.clientWidth,
height: mapImg.clientHeight,
};
const coords = convertPixelsToCoordinate(mousePosition, mapSize);
displayCoords(coordsElement, coords);
fetcher.call(async () => {
const response = await fetchZipCode(coords);
displayZipCode(zipCodeElement, response.nr, response.navn);
});
};
mapImg.onmouseleave = (_event: MouseEvent) => {
[mousePositionElement, coordsElement, zipCodeElement].forEach(
(e) => (e.innerHTML = ""),
);
};
}
function setupSearchBar(zipCodeElement: HTMLParagraphElement) {
const searchInput =
document.querySelector<HTMLInputElement>("#search-input")!;
const searchButton =
document.querySelector<HTMLButtonElement>("#search-button")!;
searchButton.onclick = async (_event: MouseEvent) => {
const inputValue = searchInput.value;
if (!/^\d+$/.test(inputValue)) return;
const data = await (
await fetch(
`https://api.dataforsyningen.dk/postnumre?nr=${inputValue}`,
)
).json();
displayZipCode(
zipCodeElement,
parseInt(data[0]["nr"]),
data[0]["navn"],
);
};
}
function main() {
console.log("hello world");
const [mousePositionElement, coordsElement, zipCodeElement] = [
"#mouse-position",
"#coords",
"#zip-code",
].map((id) => document.querySelector<HTMLParagraphElement>(id)!);
setupSearchBar(zipCodeElement);
setupMap(mousePositionElement, coordsElement, zipCodeElement);
}
main();

17
frontend/src/utils.ts Normal file
View File

@ -0,0 +1,17 @@
export class Throttler {
private hasBeenCalledWithinTime = false;
private lastCallFunc: (() => any) | null = null;
public constructor(private minimumTimeBetweenCall: number) {}
public call(func: () => any) {
this.lastCallFunc = func;
if (this.hasBeenCalledWithinTime) return;
this.hasBeenCalledWithinTime = true;
func();
setTimeout(() => {
this.hasBeenCalledWithinTime = false;
if (this.lastCallFunc) this.lastCallFunc();
}, this.minimumTimeBetweenCall);
}
}

View File

@ -6,13 +6,69 @@
body {
margin: 0;
height: 100vh;
font-family: "Open Sans", sans-serif;
}
#topbar {
background-color: #aaa;
color: #000;
text-align: center;
margin: 0;
padding: 0.5rem;
/* box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.5); */
}
#topbar > h1 {
margin: 0;
font-size: 2rem;
}
main {
text-align: center;
padding: 1rem;
margin: auto;
width: 50%;
}
main > * {
margin: 2px;
}
#search-bar {
width: 100%;
display: flex;
flex-direction: row;
padding: 5px;
border: 1px solid #666;
border-radius: 5px;
}
#search-input {
width: 100%;
font-size: 1rem;
border: 0;
outline: 0;
padding-bottom: 2px;
}
#search-input:focus {
border-bottom: 2px solid black;
padding-bottom: 0;
}
#search-button {
width: 5rem;
font-size: 1rem;
}
#map {
max-width: 100%;
width: 100%;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.4);
}
@media screen and (max-width: 1000px) {
main {
width: 100%;
}
}

13
frontend/tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es6",
"module": "es6",
"moduleResolution": "nodenext",
"skipLibCheck": true,
"lib": ["es6", "dom"],
"strict": true,
"noEmit": true,
"pretty": true
},
"include": ["./src/**/*.ts"]
}