stuff
This commit is contained in:
parent
96a6ef446a
commit
7e064b611e
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 +1,7 @@
|
||||
{
|
||||
"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"]
|
||||
"sources": ["src/utils.ts", "src/main.ts"],
|
||||
"sourcesContent": ["export class Throttler {\n private hasBeenCalledWithinTime = false;\n private lastCallFunc: (() => any) | null = null;\n\n public constructor(private minimumTimeBetweenCall: number) {}\n\n public call(func: () => any) {\n this.lastCallFunc = func;\n if (this.hasBeenCalledWithinTime) return;\n this.hasBeenCalledWithinTime = true;\n func();\n setTimeout(() => {\n this.hasBeenCalledWithinTime = false;\n if (this.lastCallFunc) this.lastCallFunc();\n }, this.minimumTimeBetweenCall);\n }\n}\n", "import { Throttler } from \"./utils\";\n\ntype Position = {\n x: number;\n y: number;\n};\n\ntype Size = { width: number; height: number };\n\ntype Coordinate = {\n longitude: number;\n latitude: number;\n};\n\ntype ZipCodeReverseResponse = {\n nr: number | null;\n navn: string;\n};\n\nasync function fetchZipCode({\n longitude,\n latitude,\n}: Coordinate): Promise<ZipCodeReverseResponse> {\n return fetch(\n `https://api.dataforsyningen.dk/postnumre/reverse?x=${longitude}&y=${latitude}`,\n )\n .then((request) => request.json())\n .then((data) => {\n let zipCode = parseInt(data.nr);\n return {\n ...data,\n nr: isNaN(zipCode) ? null : zipCode,\n } as ZipCodeReverseResponse;\n })\n .catch(() => null as never);\n}\n\nfunction convertPixelsToCoordinate(mouse: Position, map: Size): Coordinate {\n const scalar = { x: 8, y: 3.6 };\n const offset = { x: 6.2, y: 57.93 };\n return {\n longitude: (mouse.x / map.width) * scalar.x + offset.x,\n latitude: Math.abs((mouse.y / map.height) * scalar.y - offset.y),\n };\n}\n\nfunction displayMousePosition(element: HTMLParagraphElement, mouse: Position) {\n element.innerHTML = `Mouse position: <code>(${mouse.x}px, ${mouse.y}px)</code>`;\n}\n\nfunction displayCoords(element: HTMLParagraphElement, coords: Coordinate) {\n element.innerHTML = `Coords: <code>${coords.longitude.toFixed(\n 3,\n )}, ${coords.latitude.toFixed(3)}</code>`;\n}\n\nfunction displayZipCode(\n element: HTMLParagraphElement,\n zipCode: number | null,\n name: string,\n) {\n element.innerHTML =\n zipCode === null\n ? `Postnummer ikke fundet`\n : `Postnummer: <code>${zipCode}</code>, ${name}`;\n}\n\nfunction setupMap(\n mousePositionElement: HTMLParagraphElement,\n coordsElement: HTMLParagraphElement,\n zipCodeElement: HTMLParagraphElement,\n) {\n const mapImg = document.querySelector<HTMLImageElement>(\"#map\")!;\n const fetcher = new Throttler(500);\n\n mapImg.onmousemove = async (event: MouseEvent) => {\n const mousePosition: Position = { x: event.offsetX, y: event.offsetY };\n displayMousePosition(mousePositionElement, mousePosition);\n const mapSize: Size = {\n width: mapImg.clientWidth,\n height: mapImg.clientHeight,\n };\n const coords = convertPixelsToCoordinate(mousePosition, mapSize);\n displayCoords(coordsElement, coords);\n fetcher.call(async () => {\n const response = await fetchZipCode(coords);\n displayZipCode(zipCodeElement, response.nr, response.navn);\n });\n };\n\n mapImg.onmouseleave = (_event: MouseEvent) => {\n [mousePositionElement, coordsElement, zipCodeElement].forEach(\n (e) => (e.innerHTML = \"\"),\n );\n };\n}\n\nfunction setupSearchBar(zipCodeElement: HTMLParagraphElement) {\n const searchInput =\n document.querySelector<HTMLInputElement>(\"#search-input\")!;\n const searchButton =\n document.querySelector<HTMLButtonElement>(\"#search-button\")!;\n\n searchButton.onclick = async (_event: MouseEvent) => {\n const inputValue = searchInput.value;\n if (!/^\\d+$/.test(inputValue)) return;\n const data = await (\n await fetch(\n `https://api.dataforsyningen.dk/postnumre?nr=${inputValue}`,\n )\n ).json();\n displayZipCode(\n zipCodeElement,\n parseInt(data[0][\"nr\"]),\n data[0][\"navn\"],\n );\n };\n}\n\nfunction main() {\n const [mousePositionElement, coordsElement, zipCodeElement] = [\n \"#mouse-position\",\n \"#coords\",\n \"#zip-code\",\n ].map((id) => document.querySelector<HTMLParagraphElement>(id)!);\n\n setupSearchBar(zipCodeElement);\n setupMap(mousePositionElement, coordsElement, zipCodeElement);\n}\n\nmain();\n"],
|
||||
"mappings": "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",
|
||||
"names": ["Throttler", "minimumTimeBetweenCall", "func", "fetchZipCode", "_0", "__async", "longitude", "latitude", "request", "data", "zipCode", "__spreadProps", "__spreadValues", "convertPixelsToCoordinate", "mouse", "map", "scalar", "offset", "displayMousePosition", "element", "displayCoords", "coords", "displayZipCode", "name", "setupMap", "mousePositionElement", "coordsElement", "zipCodeElement", "mapImg", "fetcher", "Throttler", "event", "mousePosition", "mapSize", "response", "_event", "e", "setupSearchBar", "searchInput", "searchButton", "inputValue", "main", "id"]
|
||||
}
|
||||
|
@ -1,5 +0,0 @@
|
||||
|
||||
set -xe
|
||||
|
||||
tsc src/main.ts --noEmit
|
||||
|
@ -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>
|
||||
|
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() {
|
||||
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
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 {
|
||||
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
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