Compare commits
1 Commits
main
...
frontend_f
Author | SHA1 | Date | |
---|---|---|---|
a5b7800784 |
@ -15,29 +15,5 @@
|
|||||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="topbar">
|
|
||||||
<h1>Postnummer App</h1>
|
|
||||||
<div class="spacer"></div>
|
|
||||||
<button id="dropdown-button">≡</button>
|
|
||||||
<div id="dropdown">
|
|
||||||
<button id="map-redirect">Kort</button>
|
|
||||||
<button id="reviews-redirect">Anmeldelser</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<main id="main">
|
|
||||||
<form id="search-bar">
|
|
||||||
<input id="search-input" type="text" placeholder="Postnummer" maxlength="4">
|
|
||||||
<button id="search-button" type="submit">Search</button>
|
|
||||||
</form>
|
|
||||||
<img src="assets/map.jpg" id="map">
|
|
||||||
<div id="dot"></div>
|
|
||||||
<div id="boundary"></div>
|
|
||||||
<div id="info">
|
|
||||||
<p id="zip-code">Postnummer ikke fundet</p>
|
|
||||||
<p id="mouse-position"></p>
|
|
||||||
<p id="coords"></p>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<div id="tooltip"></div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -120,11 +120,136 @@ function displayZipCode(
|
|||||||
boundaryElem.style.height = `${bottomleft.y - topright.y}px`;
|
boundaryElem.style.height = `${bottomleft.y - topright.y}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupMap(
|
type Component = {
|
||||||
mousePositionElement: HTMLParagraphElement,
|
children?: Component[],
|
||||||
coordsElement: HTMLParagraphElement,
|
render(): string,
|
||||||
zipCodeElement: HTMLParagraphElement,
|
hydrate?(update: (action?: () => Promise<any>) => Promise<void>): Promise<void>,
|
||||||
) {
|
};
|
||||||
|
|
||||||
|
function makeReviewsPage(): Component {
|
||||||
|
return {
|
||||||
|
render: () => /*html*/`
|
||||||
|
<h2 id="reviews-title">Anmeldelser</h2>
|
||||||
|
<div id="reviews-container">${loadReviews()}</div>
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeSearchBar(updateZipCodeInfo: (zipCode: string) => Promise<void>): Component {
|
||||||
|
return {
|
||||||
|
render: () => /*html*/`
|
||||||
|
<form id="search-bar">
|
||||||
|
<input id="search-input" type="text" placeholder="Postnummer" maxlength="4">
|
||||||
|
<button id="search-button" type="submit">Search</button>
|
||||||
|
</form>
|
||||||
|
`,
|
||||||
|
async hydrate(update) {
|
||||||
|
const searchBar =
|
||||||
|
domSelect<HTMLFormElement>("#search-bar")!;
|
||||||
|
const searchInput =
|
||||||
|
domSelect<HTMLInputElement>("#search-input")!;
|
||||||
|
|
||||||
|
searchInput.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key.length === 1 && !"0123456789".includes(event.key))
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
searchBar.addEventListener("submit", async (event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
update(() => updateZipCodeInfo(searchInput.value));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZipCodeInfo = {
|
||||||
|
zipCode: number | null,
|
||||||
|
name: string | null,
|
||||||
|
center: Coordinate | null,
|
||||||
|
boundary: Square | null,
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeInfoBox(info: ZipCodeInfo): Component {
|
||||||
|
const { zipCode, name, center, boundary } = info;
|
||||||
|
return {
|
||||||
|
render: () => /*html*/`
|
||||||
|
<div id="info">
|
||||||
|
<p id="zip-code">${info.zipCode === null ? "Postnummer ikke fundet" : `Postnummer: <code>${zipCode}</code>, ${name}`}</p>
|
||||||
|
<p id="mouse-position"></p>
|
||||||
|
<p id="coords"></p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
async hydrate() {
|
||||||
|
tooltip.setText(zipCode ? `<code>${zipCode}</code> ${name}` : "");
|
||||||
|
|
||||||
|
const dot = document.getElementById("dot")!;
|
||||||
|
const boundaryElem = document.getElementById("boundary")!;
|
||||||
|
|
||||||
|
if (!center || !boundary) {
|
||||||
|
dot.style.display = "none";
|
||||||
|
boundaryElem.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapImg = document.getElementById("map")!;
|
||||||
|
const mapSize: Size = {
|
||||||
|
width: mapImg.clientWidth,
|
||||||
|
height: mapImg.clientHeight,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Draw dot
|
||||||
|
const position = convertCoordinateToPixels(center, mapSize);
|
||||||
|
const rect = document.getElementById("map")!.getBoundingClientRect();
|
||||||
|
dot.style.display = "block";
|
||||||
|
dot.style.left = `${position.x + rect.left}px`;
|
||||||
|
dot.style.top = `${position.y + rect.top + document.documentElement.scrollTop}px`;
|
||||||
|
|
||||||
|
// Draw boundary
|
||||||
|
const bottomleft = convertCoordinateToPixels({ longitude: boundary.x1, latitude: boundary.y1 }, mapSize);
|
||||||
|
const topright = convertCoordinateToPixels({ longitude: boundary.x2, latitude: boundary.y2 }, mapSize);
|
||||||
|
|
||||||
|
boundaryElem.style.display = "block";
|
||||||
|
boundaryElem.style.left = `${bottomleft.x + rect.left}px`;
|
||||||
|
boundaryElem.style.top = `${topright.y + rect.top + document.documentElement.scrollTop}px`;
|
||||||
|
boundaryElem.style.width = `${topright.x - bottomleft.x}px`;
|
||||||
|
boundaryElem.style.height = `${bottomleft.y - topright.y}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function makeMainPage(): Component {
|
||||||
|
const zipCodeInfo: ZipCodeInfo = {
|
||||||
|
zipCode: null,
|
||||||
|
name: null,
|
||||||
|
center: null,
|
||||||
|
boundary: null,
|
||||||
|
}
|
||||||
|
const searchBar = makeSearchBar(async (searchInputValue) => {
|
||||||
|
const inputValue = searchInputValue;
|
||||||
|
if (!/^\d+$/.test(inputValue)) return;
|
||||||
|
const data = await fetch(
|
||||||
|
`https://api.dataforsyningen.dk/postnumre?nr=${inputValue}`,
|
||||||
|
).then((response) => response.json())
|
||||||
|
|
||||||
|
zipCodeInfo.zipCode = data.length ? parseInt(data[0]["nr"]) : null;
|
||||||
|
zipCodeInfo.name = data.length ? data[0]["navn"] : null;
|
||||||
|
zipCodeInfo.center = data.length ? { longitude: data[0]["visueltcenter"][0], latitude: data[0]["visueltcenter"][1] } : null;
|
||||||
|
zipCodeInfo.boundary = data.length ? { x1: data[0]["bbox"][0], y1: data[0]["bbox"][1], x2: data[0]["bbox"][2], y2: data[0]["bbox"][3] } : null;
|
||||||
|
});
|
||||||
|
const infoBox = makeInfoBox(zipCodeInfo);
|
||||||
|
return {
|
||||||
|
children: [searchBar, infoBox],
|
||||||
|
render: () => /*html*/`
|
||||||
|
${searchBar.render()}
|
||||||
|
<img src="assets/map.jpg" id="map">
|
||||||
|
<div id="dot"></div>
|
||||||
|
<div id="boundary"></div>
|
||||||
|
`,
|
||||||
|
async hydrate() {
|
||||||
|
const mousePositionElement = domSelect<HTMLParagraphElement>("#mouse-position")!;
|
||||||
|
const coordsElement = domSelect<HTMLParagraphElement>("#coords")!;
|
||||||
|
const zipCodeElement = domSelect<HTMLParagraphElement>("#zip-code")!;
|
||||||
const mapImg = domSelect<HTMLImageElement>("#map")!;
|
const mapImg = domSelect<HTMLImageElement>("#map")!;
|
||||||
const fetcher = new Throttler(200);
|
const fetcher = new Throttler(200);
|
||||||
|
|
||||||
@ -161,97 +286,93 @@ function setupMap(
|
|||||||
mapImg.addEventListener("mouseleave", (_event: MouseEvent) => {
|
mapImg.addEventListener("mouseleave", (_event: MouseEvent) => {
|
||||||
displayZipCode(zipCodeElement, null, null, null, null);
|
displayZipCode(zipCodeElement, null, null, null, null);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupSearchBar(zipCodeElement: HTMLParagraphElement) {
|
function makeRouter(route: "main" | "reviews"): Component {
|
||||||
const searchBar =
|
switch (route) {
|
||||||
domSelect<HTMLFormElement>("#search-bar")!;
|
case "main":
|
||||||
const searchInput =
|
{
|
||||||
domSelect<HTMLInputElement>("#search-input")!;
|
const mainPage = makeMainPage();
|
||||||
|
return {
|
||||||
searchInput.addEventListener("keydown", (event) => {
|
children: [mainPage],
|
||||||
if (event.key.length === 1 && !"0123456789".includes(event.key))
|
render: () => mainPage.render(),
|
||||||
event.preventDefault();
|
};
|
||||||
});
|
}
|
||||||
|
case "reviews": {
|
||||||
searchBar.addEventListener("submit", async (event: Event) => {
|
const reviewsPage = makeReviewsPage();
|
||||||
event.preventDefault();
|
return {
|
||||||
|
children: [reviewsPage],
|
||||||
const inputValue = searchInput.value;
|
render: () => reviewsPage.render(),
|
||||||
if (!/^\d+$/.test(inputValue)) return;
|
}
|
||||||
const data = await fetch(
|
}
|
||||||
`https://api.dataforsyningen.dk/postnumre?nr=${inputValue}`,
|
}
|
||||||
).then((response) => response.json())
|
|
||||||
|
|
||||||
displayZipCode(
|
|
||||||
zipCodeElement,
|
|
||||||
data.length ? parseInt(data[0]["nr"]) : null,
|
|
||||||
data.length ? data[0]["navn"] : null,
|
|
||||||
data.length ? { longitude: data[0]["visueltcenter"][0], latitude: data[0]["visueltcenter"][1] } : null,
|
|
||||||
data.length ? { x1: data[0]["bbox"][0], y1: data[0]["bbox"][1], x2: data[0]["bbox"][2], y2: data[0]["bbox"][3] } : null,
|
|
||||||
);
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pageRedirects() {
|
function makeTooltip(): Component {
|
||||||
const reviewRedirect = document.getElementById("reviews-redirect")!
|
return {
|
||||||
const mapRedirect = document.getElementById("map-redirect")!
|
render: () => /*html*/`
|
||||||
const mainElement = document.getElementById("main")!
|
<div id="tooltip"></div>
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeLayout(): Component {
|
||||||
reviewRedirect.addEventListener("click", () => {
|
let route: "main" | "reviews" = "main";
|
||||||
mainElement.innerHTML = /*html*/`
|
const router = makeRouter(route);
|
||||||
<h2 id="reviews-title">Anmeldelser</h2>
|
const tooltip = makeTooltip();
|
||||||
<div id="reviews-container">${loadReviews()}</div>
|
return {
|
||||||
`;
|
children: [router, tooltip],
|
||||||
const dropdown = document.getElementById("dropdown")!;
|
render: () => /**/`
|
||||||
dropdown.classList.remove("enabled");
|
<div id="topbar">
|
||||||
|
<h1>Postnummer App</h1>
|
||||||
});
|
<div class="spacer"></div>
|
||||||
|
<button id="dropdown-button">≡</button>
|
||||||
mapRedirect.addEventListener("click", () => {
|
<div id="dropdown">
|
||||||
mainElement.innerHTML = /*html*/`
|
<button id="map-redirect">Kort</button>
|
||||||
<form id="search-bar">
|
<button id="reviews-redirect">Anmeldelser</button>
|
||||||
<input id="search-input" type="text" placeholder="Postnummer" maxlength="4">
|
|
||||||
<button id="search-button" type="submit">Search</button>
|
|
||||||
</form>
|
|
||||||
<img src="assets/map.jpg" id="map">
|
|
||||||
<div id="dot"></div>
|
|
||||||
<div id="boundary"></div>
|
|
||||||
<div id="info">
|
|
||||||
<p id="zip-code">Postnummer ikke fundet</p>
|
|
||||||
<p id="mouse-position"></p>
|
|
||||||
<p id="coords"></p>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
</div>
|
||||||
const [mousePositionElement, coordsElement, zipCodeElement] = [
|
<main id="main">
|
||||||
"#mouse-position",
|
${router.render()}
|
||||||
"#coords",
|
</main>
|
||||||
"#zip-code",
|
${tooltip.render()}
|
||||||
].map((id) => domSelect<HTMLParagraphElement>(id)!);
|
`,
|
||||||
setupSearchBar(zipCodeElement);
|
async hydrate(update) {
|
||||||
setupMap(mousePositionElement, coordsElement, zipCodeElement);
|
setTopbarOffset();
|
||||||
const dropdown = document.getElementById("dropdown")!;
|
addToggleDropdownListener();
|
||||||
dropdown.classList.remove("enabled");
|
document.getElementById("reviews-redirect")!.addEventListener('click', () => {
|
||||||
|
update(async () => route = "reviews");
|
||||||
})
|
})
|
||||||
|
document.getElementById("map-redirect")!.addEventListener('click', () => {
|
||||||
|
update(async () => route = "main");
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateComponentAndChildren(component: Component, update: (action?: () => Promise<any>) => Promise<void>) {
|
||||||
|
component.hydrate && component.hydrate(update);
|
||||||
|
component.children?.forEach((child) => hydrateComponentAndChildren(child, update));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderComponent(element: HTMLElement, component: Component) {
|
||||||
|
element.innerHTML = component.render();
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
hydrateComponentAndChildren(component, async (action) => {
|
||||||
|
action && await action();
|
||||||
|
element.innerHTML = component.render();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
if (navigator.userAgent.match("Chrome")) {
|
if (navigator.userAgent.match("Chrome")) {
|
||||||
location.href = "https://mozilla.org/firefox";
|
location.href = "https://mozilla.org/firefox";
|
||||||
}
|
}
|
||||||
|
const bodyElement = document.querySelector<HTMLElement>("body")!;
|
||||||
const mousePositionElement = domSelect<HTMLParagraphElement>("#mouse-position")!;
|
renderComponent(bodyElement, makeLayout());
|
||||||
const coordsElement = domSelect<HTMLParagraphElement>("#coords")!;
|
|
||||||
const zipCodeElement = domSelect<HTMLParagraphElement>("#zip-code")!;
|
|
||||||
|
|
||||||
setupSearchBar(zipCodeElement);
|
|
||||||
setupMap(mousePositionElement, coordsElement, zipCodeElement);
|
|
||||||
setTopbarOffset();
|
|
||||||
addToggleDropdownListener();
|
|
||||||
pageRedirects();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
Loading…
Reference in New Issue
Block a user