Compare commits

...

29 Commits

Author SHA1 Message Date
d9ee82597e backend: fixies 2023-02-13 01:47:36 +01:00
28a91d9f6b frontend: small format and refactor 2023-02-10 18:51:58 +01:00
5e66bc7446 frontend: fix event listeners 2023-02-10 18:35:18 +01:00
9b9f31420e frontend: fix event listeners 2023-02-10 18:33:33 +01:00
8564ac5240 backend: readded windows tcp 2023-02-10 17:42:58 +01:00
d5d4dc6455 backend: refactored tcp 2023-02-10 17:39:04 +01:00
d24f0ae010 review page template 2023-02-10 11:10:16 +01:00
6ca997dafb backend: fix webserver 2023-02-10 11:11:06 +01:00
b4b2305fde add readme usecases 2023-02-10 10:40:41 +01:00
cb9f8c30c9 Fix type errors 2023-02-10 09:16:15 +01:00
5a7553f902 Fix layout when having scrolled down 2023-02-10 09:14:36 +01:00
101473dccc add api specification 2023-02-10 08:45:40 +01:00
70579365c1 backend: add http to nmake 2023-02-09 23:04:47 +01:00
3e6dfd3d5a backend: make http request parser 2023-02-09 23:02:54 +01:00
a30ee9d249 backend: add parser char skipper 2023-02-09 15:56:03 +01:00
02d5fa4b8e commit rust backend 2023-02-09 15:08:28 +01:00
db6bc13a49 fixed icon 2023-02-09 15:04:37 +01:00
8b95ade0be Optimize amount of API calls 2023-02-09 15:02:26 +01:00
f9472dc426 dropdown buttons changing page 2023-02-09 14:54:52 +01:00
f7183dbfac backend: add clang format 2023-02-09 14:05:18 +01:00
1780a67e1f move reviews into global files 2023-02-09 13:56:38 +01:00
eef103fd9a backend: use tabs in makefile 2023-02-09 13:52:25 +01:00
d3859091a2 backend: extracted http 2023-02-09 13:51:46 +01:00
3c6bd4dfc3 Fix errors on linux 2023-02-09 13:46:06 +01:00
603ab98a14 Add support for Windows 2023-02-09 13:38:44 +01:00
2e087254c9 add http parser 2023-02-09 13:35:15 +01:00
7e8d7e1b11 create review site 2023-02-09 13:05:18 +01:00
1d41948720 Merge branch 'feature/boundaries' 2023-02-09 11:07:32 +01:00
19fc5a2791 add backend 2023-02-09 10:31:20 +01:00
27 changed files with 1084 additions and 74 deletions

View File

@ -1,2 +1,21 @@
# 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 Normal file
View File

@ -0,0 +1,48 @@
# 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"
}
```

6
backend/.clang-format Normal file
View File

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

4
backend/.gitignore vendored Normal file
View File

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

26
backend/Makefile Normal file
View File

@ -0,0 +1,26 @@
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

12
backend/NMakefile Normal file
View File

@ -0,0 +1,12 @@
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

156
backend/http.c Normal file
View File

@ -0,0 +1,156 @@
#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,
};
}

20
backend/http.h Normal file
View File

@ -0,0 +1,20 @@
#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

98
backend/linux.c Normal file
View File

@ -0,0 +1,98 @@
#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);
}

167
backend/main.c Normal file
View File

@ -0,0 +1,167 @@
#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;
}

32
backend/tcp.h Normal file
View File

@ -0,0 +1,32 @@
#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

4
backend/utils.h Normal file
View File

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

95
backend/windows.c Normal file
View File

@ -0,0 +1,95 @@
#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

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -8,6 +8,8 @@
<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">
@ -18,11 +20,11 @@
<div class="spacer"></div>
<button id="dropdown-button"></button>
<div id="dropdown">
<a href="https://tpho.dk">based site</a>
<a href="https://tpho.dk">based site</a>
<button id="map-redirect">Kort</button>
<button id="reviews-redirect">Anmeldelser</button>
</div>
</div>
<main>
<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>

View File

@ -1,17 +1,23 @@
export class Throttler {
private hasBeenCalledWithinTime = false;
private lastCallFunc: (() => any) | null = null;
private lastCallFunc: (() => Promise<any>) | null = null;
private timeout: number | null = null;
public constructor(private minimumTimeBetweenCall: number) {}
public call(func: () => any) {
public async call(func: () => any) {
this.lastCallFunc = func;
if (this.hasBeenCalledWithinTime) return;
this.hasBeenCalledWithinTime = true;
func();
setTimeout(() => {
this.hasBeenCalledWithinTime = false;
if (this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
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 + "px";
this.element.style.top = event.y + OFFSET + document.documentElement.scrollTop + "px";
if (this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(() => {

View File

@ -3,6 +3,9 @@ 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")!);
@ -38,6 +41,28 @@ 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>`;
}
@ -63,9 +88,11 @@ function displayZipCode(
tooltip.setText(zipCode ? `<code>${zipCode}</code> ${name}` : "");
const dot = document.getElementById("dot")!;
const boundaryElem = document.getElementById("boundary")!;
if (!center) {
if (!center || !boundary) {
dot.style.display = "none";
boundaryElem.style.display = "none";
return;
}
@ -79,20 +106,18 @@ 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
if (!boundary) return;
const bottomleft = convertCoordinateToPixels({ longitude: boundary.x1, latitude: boundary.y1 }, mapSize);
const topright = convertCoordinateToPixels({ longitude: boundary.x2, latitude: boundary.y2 }, 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";
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 setupMap(
@ -100,82 +125,63 @@ function setupMap(
coordsElement: HTMLParagraphElement,
zipCodeElement: HTMLParagraphElement,
) {
const mapImg = document.querySelector<HTMLImageElement>("#map")!;
const mapImg = domSelect<HTMLImageElement>("#map")!;
const fetcher = new Throttler(200);
mapImg.onmousemove = async (event: MouseEvent) => {
mapImg.addEventListener('mousemove', 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,
);
});
};
mapImg.onmouseup = async (event: MouseEvent) => {
fetcher.call(async () => await fetchAndDisplayZipCode(coords));
});
mapImg.addEventListener("mouseup", 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.onmouseleave = (_event: MouseEvent) => {
document.getElementById("dot")!.style.display = "none";
[mousePositionElement, coordsElement].forEach(
(e) => (e.innerHTML = ""),
);
zipCodeElement.innerHTML = "Postnummer ikke fundet";
};
mapImg.addEventListener("mouseleave", (_event: MouseEvent) => {
displayZipCode(zipCodeElement, null, null, null, null);
});
}
function setupSearchBar(zipCodeElement: HTMLParagraphElement) {
const searchBar =
document.querySelector<HTMLFormElement>("#search-bar")!;
domSelect<HTMLFormElement>("#search-bar")!;
const searchInput =
document.querySelector<HTMLInputElement>("#search-input")!;
domSelect<HTMLInputElement>("#search-input")!;
// Prevent typing letters
searchBar.onkeypress = event => {
event.key !== "Enter" || !isNaN(parseInt(event.key));
}
searchInput.addEventListener("keydown", (event) => {
if (event.key.length === 1 && !"0123456789".includes(event.key))
event.preventDefault();
});
searchBar.addEventListener("submit", async (event: Event) => {
event.preventDefault();
const inputValue = searchInput.value;
if (!/^\d+$/.test(inputValue)) return;
const data = await (
await fetch(
`https://api.dataforsyningen.dk/postnumre?nr=${inputValue}`,
)
).json();
const data = await fetch(
`https://api.dataforsyningen.dk/postnumre?nr=${inputValue}`,
).then((response) => response.json())
displayZipCode(
zipCodeElement,
@ -188,21 +194,64 @@ 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, coordsElement, zipCodeElement] = [
"#mouse-position",
"#coords",
"#zip-code",
].map((id) => document.querySelector<HTMLParagraphElement>(id)!);
const mousePositionElement = domSelect<HTMLParagraphElement>("#mouse-position")!;
const coordsElement = domSelect<HTMLParagraphElement>("#coords")!;
const zipCodeElement = domSelect<HTMLParagraphElement>("#zip-code")!;
setupSearchBar(zipCodeElement);
setupMap(mousePositionElement, coordsElement, zipCodeElement);
setTopbarOffset();
addToggleDropdownListener();
pageRedirects();
}
main();

28
frontend/src/review.ts Normal file
View File

@ -0,0 +1,28 @@
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,20 +89,22 @@ body {
transform: scaleY(1);
}
#dropdown a {
#dropdown button {
border: none;
background-color: var(--brand);
color: var(--light);
padding: 1rem 1rem;
font-weight: bold;
text-decoration: none;
outline: none;
cursor: pointer;
}
#dropdown a:hover, #dropdown a:focus {
#dropdown button:hover, #dropdown button:focus {
background-color: var(--brand-300);
}
#dropdown a:last-child {
#dropdown button:last-child {
border-radius: 0 0 0 5px;
}
@ -192,11 +194,12 @@ code {
position: absolute;
display: none;
z-index: 2;
pointer-events: none;
}
#tooltip {
display: none;
position: fixed;
position: absolute;
background-color: white;
color: black;
padding: 5px;
@ -219,3 +222,23 @@ 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;
}

1
rs-backend/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
target/

7
rs-backend/Cargo.lock generated Normal file
View File

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

8
rs-backend/Cargo.toml Normal file
View File

@ -0,0 +1,8 @@
[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]

39
rs-backend/src/main.rs Normal file
View File

@ -0,0 +1,39 @@
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

@ -0,0 +1,136 @@
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

@ -0,0 +1,20 @@
#[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

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