diff --git a/frontend/.prettierrc.tml b/frontend/.prettierrc.tml new file mode 100644 index 0000000..7365bf2 --- /dev/null +++ b/frontend/.prettierrc.tml @@ -0,0 +1,6 @@ +# .prettierrc.toml +trailingComma = "all" +tabWidth = 4 +semi = true +singleQuote = false +jsxSingleQuote = false diff --git a/frontend/.prettierrc.toml b/frontend/.prettierrc.toml new file mode 100644 index 0000000..7365bf2 --- /dev/null +++ b/frontend/.prettierrc.toml @@ -0,0 +1,6 @@ +# .prettierrc.toml +trailingComma = "all" +tabWidth = 4 +semi = true +singleQuote = false +jsxSingleQuote = false diff --git a/frontend/Makefile b/frontend/Makefile new file mode 100644 index 0000000..88ce765 --- /dev/null +++ b/frontend/Makefile @@ -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 + diff --git a/frontend/build.sh b/frontend/build.sh deleted file mode 100644 index 34393ca..0000000 --- a/frontend/build.sh +++ /dev/null @@ -1,5 +0,0 @@ - -set -xe - -esbuild src/main.ts --outfile=bundle.js --minify --sourcemap --bundle - diff --git a/frontend/build_watch.sh b/frontend/build_watch.sh deleted file mode 100644 index 817b0ce..0000000 --- a/frontend/build_watch.sh +++ /dev/null @@ -1,5 +0,0 @@ - -set -xe - -esbuild src/main.ts --outfile=bundle.js --minify --sourcemap --bundle --watch - diff --git a/frontend/bundle.js.map b/frontend/bundle.js.map index dab7829..d0f35a2 100644 --- a/frontend/bundle.js.map +++ b/frontend/bundle.js.map @@ -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 {\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: (${mouse.x}px, ${mouse.y}px)`;\n}\n\nfunction displayCoords(element: HTMLParagraphElement, coords: Coordinate) {\n element.innerHTML = `Coords: ${coords.longitude.toFixed(\n 3,\n )}, ${coords.latitude.toFixed(3)}`;\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: ${zipCode}, ${name}`;\n}\n\nfunction setupMap(\n mousePositionElement: HTMLParagraphElement,\n coordsElement: HTMLParagraphElement,\n zipCodeElement: HTMLParagraphElement,\n) {\n const mapImg = document.querySelector(\"#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(\"#search-input\")!;\n const searchButton =\n document.querySelector(\"#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(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"] } diff --git a/frontend/check.sh b/frontend/check.sh deleted file mode 100644 index 15d6550..0000000 --- a/frontend/check.sh +++ /dev/null @@ -1,5 +0,0 @@ - -set -xe - -tsc src/main.ts --noEmit - diff --git a/frontend/index.html b/frontend/index.html index 3744dde..4e1d911 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,14 +5,25 @@ Postnummer App + + + + +
-

Postnummer App

+

Postnummer App

+
-
+

+

+

diff --git a/frontend/map.jpg b/frontend/map.jpg index 14c1f33..94f99b6 100644 Binary files a/frontend/map.jpg and b/frontend/map.jpg differ diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 20f7a33..e96c5f8 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -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 { + 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: (${mouse.x}px, ${mouse.y}px)`; +} + +function displayCoords(element: HTMLParagraphElement, coords: Coordinate) { + element.innerHTML = `Coords: ${coords.longitude.toFixed( + 3, + )}, ${coords.latitude.toFixed(3)}`; +} + +function displayZipCode( + element: HTMLParagraphElement, + zipCode: number | null, + name: string, +) { + element.innerHTML = + zipCode === null + ? `Postnummer ikke fundet` + : `Postnummer: ${zipCode}, ${name}`; +} + +function setupMap( + mousePositionElement: HTMLParagraphElement, + coordsElement: HTMLParagraphElement, + zipCodeElement: HTMLParagraphElement, +) { + const mapImg = document.querySelector("#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("#search-input")!; + const searchButton = + document.querySelector("#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(id)!); + + setupSearchBar(zipCodeElement); + setupMap(mousePositionElement, coordsElement, zipCodeElement); } main(); - diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts new file mode 100644 index 0000000..c55e284 --- /dev/null +++ b/frontend/src/utils.ts @@ -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); + } +} diff --git a/frontend/style.css b/frontend/style.css index 6c709d3..d52be07 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -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%; + } } diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..a5a00b6 --- /dev/null +++ b/frontend/tsconfig.json @@ -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"] +}