Compare commits

..

No commits in common. "main" and "feature/boundaries" have entirely different histories.

27 changed files with 75 additions and 1085 deletions

View File

@ -1,21 +1,2 @@
# postnummer-app-frontend
## usecase
Når man navigere Danmark er det træls at komme trælse steder hen. Derfor vil man gerne vide hvilke postnumre er bedre end andre, så man kan vurderer hvor man vil hen.
Kunden skal kunne skrive anmeldelser af forskellige postnumre.
Kunden skal kunne se andres anmeldelser af forskellige postnumre.
Kunden skal kunne søge efter postnumre, så kunden ikke behøver at finde dem i hånden hver gang.
Kunden skal nemt kunne se postnumre rundt i danmark gennem et interaktivt kort.
## implementeret
- [x] Slå postnumre op
- [x] Vælge postnumre på interaktivt kort
- [ ] Lave reviews
- [ ] Læse reviews

48
api.md
View File

@ -1,48 +0,0 @@
# Api specification
## Review
### Review model
```ts
{
id: number,
location: string,
title: string,
content: string,
stars: number
}
```
### GET reviews
#### Response
```ts
{
reviews: Review[]
}
```
### Post createReview
#### Request
```ts
{
location: string,
title: string,
content: string,
stars: number
}
```
#### Response
```ts
{
message: "Ok" | "Invalid request"
}
```

View File

@ -1,6 +0,0 @@
Language: Cpp
BasedOnStyle: WebKit
IndentWidth: 4
ColumnLimit: 100
IndentCaseLabels: true

4
backend/.gitignore vendored
View File

@ -1,4 +0,0 @@
*.o
compile_flags.txt
server

View File

@ -1,26 +0,0 @@
CFLAGS = \
-std=c17 \
-Wall \
-Wextra \
-Wpedantic \
-Wconversion \
CC = gcc
HEADERS = $(wildcard *.h)
all: compile_flags.txt server
server: main.o http.o linux.o
$(CC) $^ -o $@
%.o: %.c $(HEADERS)
$(CC) $< -c -o $@ $(CFLAGS)
clean:
rm -rf *.o server client
compile_flags.txt:
echo -xc $(CFLAGS) | sed 's/\s\+/\n/g' > compile_flags.txt

View File

@ -1,12 +0,0 @@
OBJS=main.obj windows.obj http.obj
all: $(OBJS)
link /out:server.exe $(OBJS) WS2_32.lib
.obj:
cl $*.c
clean:
del *.obj server.exe

View File

@ -1,156 +0,0 @@
#include "http.h"
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
const char* message;
size_t message_size;
size_t i;
} HttpRequestParser;
#define PARSER_PANIC(message) \
(printf("error: http parser: %s\n at %s:%d in '%s()'\n", message, __FILE__, __LINE__, \
__func__), \
exit(1))
HttpMethod parse_method(HttpRequestParser* parser)
{
if (parser->i + 3 < parser->message_size && strncmp(&parser->message[0], "GET", 3) == 0) {
parser->i += 3;
return HttpMethodGet;
} else if (parser->i + 4 < parser->message_size
&& strncmp(&parser->message[0], "POST", 4) == 0) {
parser->i += 4;
return HttpMethodPost;
} else {
PARSER_PANIC("failed to parse http method");
}
}
void skip_char(HttpRequestParser* parser, char value)
{
if (parser->i >= parser->message_size)
PARSER_PANIC("unexpected end");
if (parser->message[parser->i] != value)
PARSER_PANIC("unexpected character, expected ' '");
parser->i += 1;
}
typedef struct {
size_t index, length;
} PathSpan;
PathSpan parse_path(HttpRequestParser* parser)
{
size_t index = parser->i;
while (parser->i < parser->message_size && parser->message[parser->i] != ' '
&& parser->message[parser->i] != '\r') {
parser->i += 1;
}
if (parser->i >= parser->message_size)
PARSER_PANIC("unexpected end");
else if (parser->message[parser->i] != ' ')
PARSER_PANIC("unexpected char, expected ' '");
return (PathSpan) {
.index = index,
.length = parser->i - index,
};
}
void skip_newline(HttpRequestParser* parser)
{
skip_char(parser, '\r');
skip_char(parser, '\n');
}
void skip_http_version_tag(HttpRequestParser* parser)
{
skip_char(parser, 'H');
skip_char(parser, 'T');
skip_char(parser, 'T');
skip_char(parser, 'P');
skip_char(parser, '/');
skip_char(parser, '1');
skip_char(parser, '.');
skip_char(parser, '1');
skip_newline(parser);
}
typedef struct {
size_t key_index, key_length;
size_t value_index, value_length;
} HeaderPair;
HeaderPair parse_header_pair(HttpRequestParser* parser)
{
size_t key_begin = parser->i;
while (parser->i < parser->message_size && parser->message[parser->i] != ':'
&& parser->message[parser->i] != '\r') {
parser->i += 1;
}
if (parser->i >= parser->message_size)
PARSER_PANIC("unexpected end");
size_t key_end = parser->i;
skip_char(parser, ':');
skip_char(parser, ' ');
size_t value_begin = parser->i;
while (parser->i < parser->message_size && parser->message[parser->i] != '\r') {
parser->i += 1;
}
if (parser->i >= parser->message_size)
PARSER_PANIC("unexpected end");
return (HeaderPair) {
.key_index = key_begin,
.key_length = key_end - key_begin,
.value_index = value_begin,
.value_length = parser->i - value_begin,
};
}
bool is_content_length_header(HttpRequestParser* parser, HeaderPair pair)
{
return strncmp(&parser->message[pair.key_index], "Content-Length", pair.key_length) == 0;
}
size_t extract_content_length_value(HttpRequestParser* parser, HeaderPair pair)
{
char string_value[21] = { 0 };
strncpy(string_value, &parser->message[pair.value_index], pair.value_length);
int64_t int_value = atoll(string_value);
if (int_value < 0)
PARSER_PANIC("Content-Length < 0");
return (size_t)int_value;
}
HttpRequestHeader parse_http_request_header(const char* message, size_t message_size)
{
HttpRequestParser parser = (HttpRequestParser) {
.message = message,
.message_size = message_size,
.i = 0,
};
HttpMethod method = parse_method(&parser);
skip_char(&parser, ' ');
PathSpan path_span = parse_path(&parser);
skip_char(&parser, ' ');
skip_http_version_tag(&parser);
size_t content_length = 0;
while (parser.i < message_size && message[parser.i] != '\r') {
HeaderPair pair = parse_header_pair(&parser);
if (is_content_length_header(&parser, pair)) {
content_length = extract_content_length_value(&parser, pair);
}
skip_newline(&parser);
}
skip_newline(&parser);
return (HttpRequestHeader) {
.method = method,
.path_index = path_span.index,
.path_length = path_span.length,
.content_length = content_length,
.body_index = parser.i,
};
}

View File

@ -1,20 +0,0 @@
#ifndef HTTP_H
#define HTTP_H
#include <stdlib.h>
typedef enum {
HttpMethodGet,
HttpMethodPost,
} HttpMethod;
typedef struct {
HttpMethod method;
size_t path_index, path_length;
size_t content_length;
size_t body_index;
} HttpRequestHeader;
HttpRequestHeader parse_http_request_header(const char* message, size_t message_size);
#endif

View File

@ -1,98 +0,0 @@
#include "tcp.h"
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
struct TcpServer {
int server_socket;
struct sockaddr_in server_address;
};
struct TcpConnection {
int client_socket;
struct sockaddr_in client_address;
};
void tcp_global_initialize_sockets(void) { }
TcpServer* tcp_server_create(const char* ip, uint16_t port)
{
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket < 0) {
printf("error: tcp: could not open socket\n");
return NULL;
}
if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &(int) { 1 }, sizeof(int)) < 0) {
printf("warning: tcp: could not setsockopt SO_REUSEADDR\n");
}
struct sockaddr_in server_address;
server_address.sin_addr.s_addr = inet_addr(ip);
server_address.sin_family = AF_INET;
server_address.sin_port = htons(port);
if (bind(server_socket, (struct sockaddr*)&server_address, sizeof(server_address)) < 0) {
printf("error: tcp: could not bind socket\n");
close(server_socket);
return NULL;
}
if (listen(server_socket, SOMAXCONN - 1) < 0) {
printf("error: tcp: could not listen on server\n");
close(server_socket);
return NULL;
}
TcpServer* self = malloc(sizeof(TcpServer));
*self = (TcpServer) {
.server_socket = server_socket,
.server_address = server_address,
};
return self;
}
void tcp_server_destroy(TcpServer* server)
{
close(server->server_socket);
free(server);
}
TcpConnection* tcp_server_accept(TcpServer* server)
{
struct sockaddr_in client_address = { 0 };
socklen_t client_size = 0;
int client_socket
= accept(server->server_socket, (struct sockaddr*)&client_address, &client_size);
if (client_socket < 0) {
printf("error: tcp: could not accept connection\n");
printf("errno: %d %s\n", errno, strerror(errno));
return NULL;
}
TcpConnection* connection = malloc(sizeof(TcpConnection));
*connection = (TcpConnection) {
.client_socket = client_socket,
.client_address = client_address,
};
return connection;
}
void tcp_connection_destroy(TcpConnection* connection)
{
close(connection->client_socket);
free(connection);
}
ssize_t tcp_recieve(TcpConnection* connection, uint8_t* data, size_t amount)
{
return recv(connection->client_socket, data, amount, 0);
}
ssize_t tcp_send(TcpConnection* connection, uint8_t* data, size_t amount)
{
return send(connection->client_socket, data, amount, 0);
}

View File

@ -1,167 +0,0 @@
#include "http.h"
#include "tcp.h"
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
TcpServer* server = NULL;
void interrupt_handler(int a)
{
(void)a;
printf("\nShutting down gracefully...\n");
if (server != NULL)
tcp_server_destroy(server);
exit(1);
}
void handle_client_connection(TcpConnection* connection)
{
uint8_t buffer[8192] = { 0 };
ssize_t recieved = tcp_recieve(connection, buffer, 8192);
if (recieved < 0) {
printf("error: could not recieve\n");
return;
} else if (recieved == 0) {
printf("client disconnected\n");
return;
}
HttpRequestHeader header = parse_http_request_header((char*)buffer, strlen((char*)buffer));
char* path = calloc(header.path_length + 1, sizeof(char));
strncpy(path, (char*)&buffer[header.path_index], header.path_length);
if (strncmp(path, "/api", 4) == 0) {
// something something api
} else {
if (strstr(path, "..") != NULL) {
uint8_t send_buffer[]
= "HTTP/1.1 400 BAD\r\n"
"Content-Type: text/html\r\n"
"\r\n"
"<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Bad "
"request</title></head><body><h1>Fuck you!</h1></body></html>\r\n";
ssize_t written = tcp_send(connection, send_buffer, sizeof(send_buffer));
if (written < 0) {
printf("error: could not write\n");
return;
}
} else if (header.path_length == 0 || strncmp(path, "/", header.path_length) == 0) {
FILE* file = fopen("../frontend/index.html", "r");
if (file == NULL) {
printf("error: could not open file\n");
return;
}
uint8_t send_buffer[] = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/html\r\n"
"\r\n";
ssize_t written = tcp_send(connection, send_buffer, sizeof(send_buffer));
if (written < 0) {
printf("error: could not write\n");
return;
}
char char_read;
while ((char_read = (char)fgetc(file)) != EOF) {
tcp_send(connection, (uint8_t*)&char_read, sizeof(char));
}
} else {
char rootpath[] = "../frontend";
size_t filepath_size = sizeof(rootpath) + header.path_length + 1;
char* filepath = calloc(filepath_size, sizeof(char));
snprintf(filepath, filepath_size, "%s%s", rootpath, path);
char* dot = strrchr(path, '.');
char mime_type[20] = { 0 };
char file_flag[3] = { 'r', 0 };
if (dot != NULL && strncmp(dot, ".html", 5) == 0) {
snprintf(mime_type, 20, "text/html");
} else if (dot != NULL && strncmp(dot, ".css", 4) == 0) {
snprintf(mime_type, 20, "text/css");
} else if (dot != NULL && strncmp(dot, ".js", 3) == 0) {
snprintf(mime_type, 20, "text/javascript");
} else if (dot != NULL && strncmp(dot, ".map", 4) == 0) {
snprintf(mime_type, 20, "application/json");
} else if (dot != NULL && strncmp(dot, ".ico", 4) == 0) {
snprintf(mime_type, 20, "image/x-icon");
} else if (dot != NULL && strncmp(dot, ".jpg", 4) == 0) {
snprintf(mime_type, 20, "image/jpeg");
file_flag[1] = 'b';
} else if (dot != NULL && strncmp(dot, ".png", 4) == 0) {
snprintf(mime_type, 20, "image/png");
file_flag[1] = 'b';
} else if (dot != NULL && strncmp(dot, ".woff2", 6) == 0) {
snprintf(mime_type, 20, "font/woff2");
file_flag[1] = 'b';
} else {
printf("error: unknown file type\n");
return;
}
FILE* file = fopen(filepath, file_flag);
if (file == NULL) {
printf("error: could not open file\n");
return;
}
char send_buffer_1[] = "HTTP/1.1 200 OK\r\n"
"Content-Type: ";
char send_buffer_2[] = "\r\n"
"\r\n";
ssize_t written = tcp_send(connection, (uint8_t*)send_buffer_1, strlen(send_buffer_1));
if (written < 0) {
printf("error: could not write\n");
return;
}
written = tcp_send(connection, (uint8_t*)mime_type, strlen(mime_type));
if (written < 0) {
printf("error: could not write\n");
return;
}
written = tcp_send(connection, (uint8_t*)send_buffer_2, strlen(send_buffer_2));
if (written < 0) {
printf("error: could not write\n");
return;
}
int char_read;
while ((char_read = fgetc(file)) != EOF) {
tcp_send(connection, (uint8_t*)&char_read, sizeof(char));
}
free(filepath);
fclose(file);
}
}
free(path);
}
int main(void)
{
tcp_global_initialize_sockets();
signal(SIGINT, &interrupt_handler);
const uint16_t port = 8000;
printf("starting server...\n");
server = tcp_server_create("127.0.0.1", port);
if (server == NULL)
return 1;
printf("listening on port %d\n", port);
while (true) {
printf("waiting for client...\n");
TcpConnection* connection = tcp_server_accept(server);
if (connection == NULL) {
printf("error: could not accept client\n");
continue;
}
printf("client connected\n");
handle_client_connection(connection);
printf("disconnecting client\n");
tcp_connection_destroy(connection);
}
tcp_server_destroy(server);
return 0;
}

View File

@ -1,32 +0,0 @@
#ifndef TCP_H
#define TCP_H
#include <stdbool.h>
#include <stdint.h>
#ifdef _WIN32
#include <BaseTsd.h>
typedef SSIZE_T ssize_t;
#else
#include <sys/types.h>
#endif
void tcp_global_initialize_sockets(void);
typedef struct TcpServer TcpServer;
typedef struct TcpConnection TcpConnection;
// returns NULL on errors, and prints the error
TcpServer* tcp_server_create(const char* ip, uint16_t port);
void tcp_server_destroy(TcpServer* server);
// returns NULL on errors, and prints the error
TcpConnection* tcp_server_accept(TcpServer* server);
void tcp_connection_destroy(TcpConnection* connection);
// returns amount transmittet >0 on success, ==0 if client was disconnected, and <0 on error
ssize_t tcp_recieve(TcpConnection* connection, uint8_t* data, size_t amount);
// returns amount transmittet >0 on success, ==0 if client was disconnected, and <0 on error
ssize_t tcp_send(TcpConnection* connection, uint8_t* data, size_t amount);
#endif

View File

@ -1,4 +0,0 @@
#ifndef UTILS_H
#define UTILS_H
#endif

View File

@ -1,95 +0,0 @@
#include "tcp.h"
#include <BaseTsd.h>
#include <WS2tcpip.h>
#include <WinSock2.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
struct TcpServer {
int server_socket;
struct sockaddr_in server_address;
};
struct TcpConnection {
int client_socket;
struct sockaddr_in client_address;
};
void tcp_global_initialize_sockets(void)
{
WSADATA data;
WSAStartup(0x0202, &data);
}
TcpServer* tcp_server_create(const char* ip, uint16_t port)
{
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket < 0) {
printf("error: tcp: could not open socket\n");
return NULL;
}
if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &(int) { 1 }, sizeof(int)) < 0) {
printf("warning: tcp: could not setsockopt SO_REUSEADDR\n");
}
struct sockaddr_in server_address;
server_address.sin_addr.s_addr = inet_addr(ip);
server_address.sin_family = AF_INET;
server_address.sin_port = htons(port);
if (bind(server_socket, (struct sockaddr*)&server_address, sizeof(server_address)) < 0) {
printf("error: tcp: could not bind socket\n");
return NULL;
}
if (listen(server_socket, SOMAXCONN) < 0) {
printf("error: tcp: could not listen on server\n");
return NULL;
}
TcpServer* self = malloc(sizeof(TcpServer));
*self = (TcpServer) {
.server_socket = server_socket,
.server_address = server_address,
};
return self;
}
void tcp_server_destroy(TcpServer* server)
{
closesocket(server->server_socket);
free(server);
}
TcpConnection* tcp_server_accept(TcpServer* server)
{
struct sockaddr_in client_address;
socklen_t client_size;
int client_socket
= accept(server->server_socket, (struct sockaddr*)&client_address, &client_size);
if (client_socket < 0) {
printf("error: tcp: could not accept connection\n");
return NULL;
}
TcpConnection* connection = malloc(sizeof(TcpConnection));
*connection = (TcpConnection) {
.client_socket = client_socket,
.client_address = client_address,
};
return connection;
}
void tcp_connection_destroy(TcpConnection* connection)
{
closesocket(connection->client_socket);
free(connection);
}
ssize_t tcp_recieve(TcpConnection* connection, uint8_t* data, size_t amount)
{
return recv(connection->client_socket, data, amount, 0);
}
ssize_t tcp_send(TcpConnection* connection, uint8_t* data, size_t amount)
{
return send(connection->client_socket, data, amount, 0);
}

View File

@ -8,8 +8,6 @@
<script src="bundle.js" defer></script>
<title>Postnummer App</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0" />
<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">
@ -20,11 +18,11 @@
<div class="spacer"></div>
<button id="dropdown-button"></button>
<div id="dropdown">
<button id="map-redirect">Kort</button>
<button id="reviews-redirect">Anmeldelser</button>
<a href="https://tpho.dk">based site</a>
<a href="https://tpho.dk">based site</a>
</div>
</div>
<main id="main">
<main>
<form id="search-bar">
<input id="search-input" type="text" placeholder="Postnummer" maxlength="4">
<button id="search-button" type="submit">Search</button>

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

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

View File

@ -7,7 +7,7 @@ export class Tooltip {
document.body.addEventListener("mousemove", (event: MouseEvent) => {
this.element.style.opacity = "1";
this.element.style.left = event.x + OFFSET + "px";
this.element.style.top = event.y + OFFSET + document.documentElement.scrollTop + "px";
this.element.style.top = event.y + OFFSET + "px";
if (this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(() => {

View File

@ -3,9 +3,6 @@ import { setTopbarOffset, addToggleDropdownListener } from "./topbar";
import { Coordinate, Position, convertPixelsToCoordinate, convertCoordinateToPixels } from "./coordinates";
import { Size } from "./size";
import { Tooltip } from "./Tooltip";
import { loadReviews } from "./review";
const domSelect = <E extends Element>(query: string) => document.querySelector<E>(query);
const tooltip = new Tooltip(document.getElementById("tooltip")!);
@ -41,28 +38,6 @@ async function fetchZipCode({
.catch(() => null as never);
}
let currentBoundary: Array<number> | null = null;
async function fetchAndDisplayZipCode(coords: Coordinate) {
if (currentBoundary &&
coords.longitude > currentBoundary[0] &&
coords.latitude > currentBoundary[1] &&
coords.longitude < currentBoundary[2] &&
coords.latitude < currentBoundary[3]
) return;
const response = await fetchZipCode(coords);
currentBoundary = response.bbox;
displayZipCode(
domSelect<HTMLParagraphElement>("#zip-code")!,
response.nr,
response.navn,
response.visueltcenter ? { longitude: response.visueltcenter[0], latitude: response.visueltcenter[1] } : null,
response.bbox ? { x1: response.bbox[0], y1: response.bbox[1], x2: response.bbox[2], y2: response.bbox[3] } : null,
);
}
function displayMousePosition(element: HTMLParagraphElement, mouse: Position) {
element.innerHTML = `Mouse position: <code>(${mouse.x}px, ${mouse.y}px)</code>`;
}
@ -88,11 +63,9 @@ function displayZipCode(
tooltip.setText(zipCode ? `<code>${zipCode}</code> ${name}` : "");
const dot = document.getElementById("dot")!;
const boundaryElem = document.getElementById("boundary")!;
if (!center || !boundary) {
if (!center) {
dot.style.display = "none";
boundaryElem.style.display = "none";
return;
}
@ -106,18 +79,20 @@ function displayZipCode(
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`;
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);
if (!boundary) return;
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`;
const bottomleft = convertCoordinateToPixels({ longitude: boundary.x1, latitude: boundary.y1 }, mapSize);
const topright = convertCoordinateToPixels({ longitude: boundary.x2, latitude: boundary.y2 }, mapSize);
const boundaryElem = document.getElementById("boundary")!;
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 + document.documentElement.scrollTop + "px";
}
function setupMap(
@ -125,63 +100,82 @@ function setupMap(
coordsElement: HTMLParagraphElement,
zipCodeElement: HTMLParagraphElement,
) {
const mapImg = domSelect<HTMLImageElement>("#map")!;
const mapImg = document.querySelector<HTMLImageElement>("#map")!;
const fetcher = new Throttler(200);
mapImg.addEventListener('mousemove', async (event: MouseEvent) => {
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,
response.visueltcenter ? { longitude: response.visueltcenter[0], latitude: response.visueltcenter[1] } : null,
response.bbox ? { x1: response.bbox[0], y1: response.bbox[1], x2: response.bbox[2], y2: response.bbox[3] } : null,
);
});
};
fetcher.call(async () => await fetchAndDisplayZipCode(coords));
});
mapImg.addEventListener("mouseup", async (event: MouseEvent) => {
mapImg.onmouseup = 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,
response.visueltcenter ? { longitude: response.visueltcenter[0], latitude: response.visueltcenter[1] } : null,
response.bbox ? { x1: response.bbox[0], y1: response.bbox[1], x2: response.bbox[2], y2: response.bbox[3] } : null,
);
});
fetcher.call(async () => await fetchAndDisplayZipCode(coords));
});
}
mapImg.addEventListener("mouseleave", (_event: MouseEvent) => {
displayZipCode(zipCodeElement, null, null, null, null);
});
mapImg.onmouseleave = (_event: MouseEvent) => {
document.getElementById("dot")!.style.display = "none";
[mousePositionElement, coordsElement].forEach(
(e) => (e.innerHTML = ""),
);
zipCodeElement.innerHTML = "Postnummer ikke fundet";
};
}
function setupSearchBar(zipCodeElement: HTMLParagraphElement) {
const searchBar =
domSelect<HTMLFormElement>("#search-bar")!;
document.querySelector<HTMLFormElement>("#search-bar")!;
const searchInput =
domSelect<HTMLInputElement>("#search-input")!;
document.querySelector<HTMLInputElement>("#search-input")!;
searchInput.addEventListener("keydown", (event) => {
if (event.key.length === 1 && !"0123456789".includes(event.key))
event.preventDefault();
});
// Prevent typing letters
searchBar.onkeypress = event => {
event.key !== "Enter" || !isNaN(parseInt(event.key));
}
searchBar.addEventListener("submit", async (event: Event) => {
event.preventDefault();
const inputValue = searchInput.value;
if (!/^\d+$/.test(inputValue)) return;
const data = await fetch(
`https://api.dataforsyningen.dk/postnumre?nr=${inputValue}`,
).then((response) => response.json())
const data = await (
await fetch(
`https://api.dataforsyningen.dk/postnumre?nr=${inputValue}`,
)
).json();
displayZipCode(
zipCodeElement,
@ -194,64 +188,21 @@ function setupSearchBar(zipCodeElement: HTMLParagraphElement) {
});
}
function pageRedirects() {
const reviewRedirect = document.getElementById("reviews-redirect")!
const mapRedirect = document.getElementById("map-redirect")!
const mainElement = document.getElementById("main")!
reviewRedirect.addEventListener("click", () => {
mainElement.innerHTML = /*html*/`
<h2 id="reviews-title">Anmeldelser</h2>
<div id="reviews-container">${loadReviews()}</div>
`;
const dropdown = document.getElementById("dropdown")!;
dropdown.classList.remove("enabled");
});
mapRedirect.addEventListener("click", () => {
mainElement.innerHTML = /*html*/`
<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>
`;
const [mousePositionElement, coordsElement, zipCodeElement] = [
"#mouse-position",
"#coords",
"#zip-code",
].map((id) => domSelect<HTMLParagraphElement>(id)!);
setupSearchBar(zipCodeElement);
setupMap(mousePositionElement, coordsElement, zipCodeElement);
const dropdown = document.getElementById("dropdown")!;
dropdown.classList.remove("enabled");
})
}
function main() {
if (navigator.userAgent.match("Chrome")) {
location.href = "https://mozilla.org/firefox";
}
const mousePositionElement = domSelect<HTMLParagraphElement>("#mouse-position")!;
const coordsElement = domSelect<HTMLParagraphElement>("#coords")!;
const zipCodeElement = domSelect<HTMLParagraphElement>("#zip-code")!;
const [mousePositionElement, coordsElement, zipCodeElement] = [
"#mouse-position",
"#coords",
"#zip-code",
].map((id) => document.querySelector<HTMLParagraphElement>(id)!);
setupSearchBar(zipCodeElement);
setupMap(mousePositionElement, coordsElement, zipCodeElement);
setTopbarOffset();
addToggleDropdownListener();
pageRedirects();
}
main();

View File

@ -1,28 +0,0 @@
export function loadReviews() {
let result: string = ""
result += `
<div class="review">
<h3>Hvor er min scooter?</h3>
<div class="location-and-stars">
<p>Randers</p>
<div>
<span class="material-symbols-outlined">star</span>
<span class="material-symbols-outlined">star</span>
<span class="material-symbols-outlined">star</span>
<span class="material-symbols-outlined">star</span>
<span class="material-symbols-outlined">star</span>
</div>
</div>
<p class="review-content">lorem ipsum dolor sit amet, lorem ipsum dolor sit amet, lorem ipsum dolor sit amet, lorem ipsum dolor sit amet, lorem ipsum dolor sit amet, lorem ipsum dolor sit amet, lorem ipsum dolor sit amet, lorem ipsum dolor sit amet, lorem ipsum dolor sit amet, lorem ipsum dolor sit amet, lorem ipsum dolor sit amet, lorem ipsum dolor sit amet</p>
</div>`
// const body = (await (await fetch("/api/review")).json()).body()
// if (!body.reviews) return;
return result
}

View File

@ -89,22 +89,20 @@ body {
transform: scaleY(1);
}
#dropdown button {
border: none;
#dropdown a {
background-color: var(--brand);
color: var(--light);
padding: 1rem 1rem;
font-weight: bold;
text-decoration: none;
outline: none;
cursor: pointer;
}
#dropdown button:hover, #dropdown button:focus {
#dropdown a:hover, #dropdown a:focus {
background-color: var(--brand-300);
}
#dropdown button:last-child {
#dropdown a:last-child {
border-radius: 0 0 0 5px;
}
@ -194,12 +192,11 @@ code {
position: absolute;
display: none;
z-index: 2;
pointer-events: none;
}
#tooltip {
display: none;
position: absolute;
position: fixed;
background-color: white;
color: black;
padding: 5px;
@ -222,23 +219,3 @@ code {
}
}
#reviews-title {
display: flex;
justify-content: center;
}
.review h3, p {
margin: 0;
}
.location-and-stars {
display: flex;
justify-content: center;
padding: 0;
gap: 0.4em;
flex-direction: row;
}
.material-symbols-outlined {
color: yellow;
fill: yellow;
}

View File

@ -1 +0,0 @@
target/

7
rs-backend/Cargo.lock generated
View File

@ -1,7 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "rs-backend"
version = "0.1.0"

View File

@ -1,8 +0,0 @@
[package]
name = "rs-backend"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -1,39 +0,0 @@
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
mod parse;
fn parse_client(stream_buffer: &mut [u8]) -> parse::Result<()> {
let start = parse::http_start(stream_buffer)?;
loop {
match parse::http_header(stream_buffer, start.total_length) {
Ok(header) => {
todo!();
}
Err(err) => match err {
parse::Error::InvalidHeader => {
break;
}
err => return Err(err),
},
}
}
Ok(())
}
fn handle_client(mut stream: TcpStream) -> parse::Result<()> {
let mut stream_buffer = Vec::from([0u8; 4096]);
let bytes_read = stream.read(&mut stream_buffer)?;
stream_buffer.truncate(bytes_read);
parse_client(&mut stream_buffer)
}
fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:8080")?;
for stream in listener.incoming() {
if let Err(err) = handle_client(stream?) {
println!("error occurred: {err:#?}");
};
}
Ok(())
}

View File

@ -1,136 +0,0 @@
fn read_until_whitespace(
stream_buffer: &mut [u8],
stream_initial_index: usize,
) -> super::Result<String> {
let buffer = String::from_utf8(
(stream_initial_index..stream_buffer.len())
.map_while(|stream_index| {
let byte = stream_buffer[stream_index];
if byte == b' ' || byte == b'\r' && stream_buffer[stream_index + 1] == b'\n' {
return None;
}
Some(byte)
})
.collect(),
)?;
Ok(buffer)
}
pub struct HttpStart {
pub method: String,
pub path: String,
pub version: String,
pub total_length: usize,
}
pub fn http_start(stream_buffer: &mut [u8]) -> super::Result<HttpStart> {
let mut total_length = 0;
let method = read_until_whitespace(stream_buffer, total_length)?;
total_length += method.len() + 1;
let path = read_until_whitespace(stream_buffer, total_length)?;
total_length += path.len() + 1;
let version = read_until_whitespace(stream_buffer, total_length)?;
total_length += version.len() + 2; // CRLF
Ok(HttpStart {
method,
path,
version,
total_length,
})
}
pub struct HttpHeader {
pub key: String,
pub content: String,
pub total_length: usize,
}
pub fn http_header(
stream_buffer: &mut [u8],
stream_initial_index: usize,
) -> super::Result<HttpHeader> {
if read_until_whitespace(stream_buffer, stream_initial_index)?.is_empty() {
return Err(super::Error::InvalidHeader);
}
let key = String::from_utf8(
(stream_initial_index..stream_buffer.len())
.map_while(|stream_index| {
let byte = stream_buffer[stream_index];
if byte == b':' {
None
} else {
Some(byte)
}
})
.collect(),
)?;
let content = String::from_utf8(
(stream_initial_index + key.len() + 2..stream_buffer.len())
.map_while(|stream_index| {
let byte = stream_buffer[stream_index];
if byte == b'\r' && stream_buffer[stream_index + 1] == b'\n' {
None
} else {
Some(byte)
}
})
.collect(),
)?;
let total_length = key.len() + content.len() + 4; // ': ' + '\r\n'
Ok(HttpHeader {
key,
content,
total_length,
})
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn valid_http_header() {
let mut buffer = b"Content-Type: application/json\r\n".to_owned();
let len = buffer.len();
let header = http_header(&mut buffer, 0).expect("should not fail with valid input");
assert_eq!(header.total_length, len, "total length should be {len}");
assert_eq!(
&header.content, "application/json",
"header content should be application/json"
);
assert_eq!(
&header.key, "Content-Type",
"header key should be Content-Type"
);
}
#[test]
fn invalid_http_header() {
let mut buffer = b"\r\n".to_owned();
let result = http_header(&mut buffer, 0)
.err()
.expect("should fail with invalid input");
assert_eq!(
result,
crate::parse::Error::InvalidHeader,
"should return ParseError::InvalidHeader on invalid header"
);
}
#[test]
fn valid_http_start() {
let mut buffer = b"GET /resource HTTP/1.1\r\n".to_owned();
let len = buffer.len();
let start = http_start(&mut buffer).expect("should not fail with valid input");
assert_eq!(start.total_length, len, "total length should be {len}");
assert_eq!(&start.method, "GET", "method should be GET");
assert_eq!(&start.path, "/resource", "path should be /resource");
assert_eq!(&start.version, "HTTP/1.1", "version should be HTTP/1.1");
}
}

View File

@ -1,20 +0,0 @@
#[derive(Debug, PartialEq, Eq)]
pub enum Error {
InvalidHeader,
InvalidString,
Io,
}
impl From<std::string::FromUtf8Error> for Error {
fn from(_: std::string::FromUtf8Error) -> Self {
Error::InvalidString
}
}
impl From<std::io::Error> for Error {
fn from(_: std::io::Error) -> Self {
Error::Io
}
}
pub type Result<T> = std::result::Result<T, Error>;

View File

@ -1,4 +0,0 @@
pub mod client;
pub mod error;
pub use client::*;
pub use error::*;