make frontend
This commit is contained in:
parent
96a6ef446a
commit
0e93a44421
6
frontend/.prettierrc.tml
Normal file
6
frontend/.prettierrc.tml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# .prettierrc.toml
|
||||||
|
trailingComma = "all"
|
||||||
|
tabWidth = 4
|
||||||
|
semi = true
|
||||||
|
singleQuote = false
|
||||||
|
jsxSingleQuote = false
|
6
frontend/.prettierrc.toml
Normal file
6
frontend/.prettierrc.toml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# .prettierrc.toml
|
||||||
|
trailingComma = "all"
|
||||||
|
tabWidth = 4
|
||||||
|
semi = true
|
||||||
|
singleQuote = false
|
||||||
|
jsxSingleQuote = false
|
15
frontend/Makefile
Normal file
15
frontend/Makefile
Normal 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
|
||||||
|
|
@ -1,5 +0,0 @@
|
|||||||
|
|
||||||
set -xe
|
|
||||||
|
|
||||||
esbuild src/main.ts --outfile=bundle.js --minify --sourcemap --bundle
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
|||||||
|
|
||||||
set -xe
|
|
||||||
|
|
||||||
esbuild src/main.ts --outfile=bundle.js --minify --sourcemap --bundle --watch
|
|
||||||
|
|
@ -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"]
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
|
|
||||||
set -xe
|
|
||||||
|
|
||||||
tsc src/main.ts --noEmit
|
|
||||||
|
|
@ -5,14 +5,25 @@
|
|||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
<script src="bundle.js" defer></script>
|
<script src="bundle.js" defer></script>
|
||||||
<title>Postnummer App</title>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="topbar">
|
<div id="topbar">
|
||||||
<h2>Postnummer App</h2>
|
<h1>Postnummer App</h1>
|
||||||
</div>
|
</div>
|
||||||
<main>
|
<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>
|
<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>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
BIN
frontend/map.jpg
BIN
frontend/map.jpg
Binary file not shown.
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 122 KiB |
@ -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() {
|
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();
|
main();
|
||||||
|
|
||||||
|
17
frontend/src/utils.ts
Normal file
17
frontend/src/utils.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -6,13 +6,69 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height: 100vh;
|
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 {
|
main {
|
||||||
text-align: center;
|
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 {
|
#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
13
frontend/tsconfig.json
Normal 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"]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user