diff --git a/main.pl b/main.pl index a344b7c..35e899a 100644 --- a/main.pl +++ b/main.pl @@ -1,16 +1,28 @@ -:- use_module(library(http/http_server)). +:- use_module(library(http/html_head)). +:- use_module(library(http/html_write)). :- use_module(library(http/http_client)). -:- use_module(library(http/http_json)). :- use_module(library(http/http_header)). -:- use_module(library(http/json_convert)). +:- use_module(library(http/http_json)). +:- use_module(library(http/http_server)). +:- use_module(library(http/http_session)). +:- use_module(library(http/js_write)). :- use_module(library(http/json)). +:- use_module(library(http/json_convert)). :- use_module(library(persistency)). +:- consult('routes/products.pl'). +:- consult('routes/login.pl'). +:- consult('routes/users.pl'). +:- consult('routes/cart.pl'). +:- consult('views/products.pl'). +:- consult('views/login.pl'). + :- initialization http_server([port(8080)]). :- initialization absolute_file_name('database.db', File, [access(write)]), - db_attach(File, []). + db_attach(File, []), + db_sync_all(gc(always)). :- json_object product(name:atom, price:integer). @@ -30,9 +42,15 @@ :- persistent token(username:atom, token:atom). +:- persistent + session(session:atom, username:atom). + +:- persistent + cart_entry(session:atom, items:atom). + :- json_object error(error:atom). -:- consult('routes/products.pl'). -:- consult('routes/login.pl'). -:- consult('routes/users.pl'). +reply_prolog(Term) :- + prolog_to_json(Term, Json), + reply_json(Json). diff --git a/routes/cart.pl b/routes/cart.pl new file mode 100644 index 0000000..38774aa --- /dev/null +++ b/routes/cart.pl @@ -0,0 +1,8 @@ +:- http_handler(root(cart/add/ProductName), cart_add(ProductName), []). + +% GET /cart/add/ProductName +cart_add(ProductName, Request) :- + http_session_id(SessionId), + assert_cart_entry(SessionId, ProductName), + http_redirect(see_other, root(.), Request). + diff --git a/routes/login.pl b/routes/login.pl index 9407b81..4ce28c6 100644 --- a/routes/login.pl +++ b/routes/login.pl @@ -1,25 +1,4 @@ -:- http_handler( - root(login), - login_route, - [] -). - -% Handles logging in, returning the correct Json in Reply. -handle_login(Username, Password, Reply) :- - user(Username, Hash), - crypto_password_hash(Password, Hash), !, - crypto_n_random_bytes(16, Bytes), - hex_bytes(Token, Bytes), - assert_token(Username, Token), - prolog_to_json(token(Username, Token), Reply). - -handle_login(Username, _, Reply) :- - user(Username, _), !, - prolog_to_json(error(incorrect_password), Reply). - -handle_login(_, _, Reply) :- - prolog_to_json(error(unknown_user), Reply). - +:- http_handler(root(api/login), login_route, []). % POST /login login_route(Request) :- @@ -27,4 +6,4 @@ login_route(Request) :- http_read_data(Request, Json, []), json_to_prolog(Json, login(Username, Password)), handle_login(Username, Password, Reply), - reply_json(Reply). + reply_prolog(Reply). diff --git a/routes/products.pl b/routes/products.pl index 03fcd8f..ce924fe 100644 --- a/routes/products.pl +++ b/routes/products.pl @@ -1,8 +1,4 @@ -:- http_handler( - root(products), - products_page, - [] -). +:- http_handler(root(api/products), products_page, []). % POST /products products_page(Request) :- @@ -10,10 +6,9 @@ products_page(Request) :- http_read_data(Request, Json, []), json_to_prolog(Json, product(Name, Price)), assert_product(Name, Price), - reply_json(Json). + reply_prolog(product(Name, Price)). % GET /products products_page(_Request) :- bagof(product(Name, Price), product(Name, Price), Products), - prolog_to_json(Products, Json), - reply_json(Json, []). + reply_prolog(Products). diff --git a/routes/users.pl b/routes/users.pl index e49cc5e..cda73ed 100644 --- a/routes/users.pl +++ b/routes/users.pl @@ -1,7 +1,6 @@ -:- http_handler(root(users), users_route, []). +:- http_handler(root(api/users), users_route, []). % GET /users users_route(_Request) :- bagof(user(Username), user(Username, _), Users), - prolog_to_json(Users, Json), - reply_json(Json). + reply_prolog(Users). diff --git a/style.css b/style.css new file mode 100644 index 0000000..d505d70 --- /dev/null +++ b/style.css @@ -0,0 +1,65 @@ +body { + font-family: sans-serif; +} + +main { + display: flex; +} + +.products { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + grid-gap: 10px; + flex: 1; +} + +.cart { + width: 300px; +} + +.product { + height: 110px; + display: flex; + flex-flow: column; + border: 1px solid rgba(0, 0, 0, .2); + border-radius: 5px; + color: rgba(0, 0, 0, .87); + text-decoration: none; +} + +.product__name { + display: flex; + justify-content: center; + align-items: center; + flex: 1; +} + +.product__price { + display: flex; + justify-content: center; + align-items: center; + background-color: #27ae60; + color: #fff; + height: 30px; +} + +.cart__entry { + border-top: 1px solid rgba(0, 0, 0, .12); + display: flex; +} + +.cart__entry:last-child { + border-bottom: 1px solid rgba(0, 0, 0, .12); +} + +.cart__entry > div { + margin: 12px; +} + +.cart__name { + flex: 1; +} + +.cart__price { + width: 65px; +} diff --git a/views/login.pl b/views/login.pl new file mode 100644 index 0000000..6b9b34c --- /dev/null +++ b/views/login.pl @@ -0,0 +1,64 @@ +:- http_handler(root(login), login_view(Method), [method(Method), methods([get, post])]). +:- http_handler(root('style.css'), http_reply_file('style.css', []), []). + +login_view(get, _Request) :- + reply_html_page( + [ + title('point of shit'), + link([ + type('text/css'), + rel('stylesheet'), + href('style.css') + ]) + ], + [ + h1('Login'), + form([method=post], [ + div([ + label(for=username, 'Username'), + input([ + type=text, + placeholder='Username', + name=username, + id=username + ]) + ]), + + div([ + label(for=password, 'Password'), + input([ + type=password, + placeholder='Username', + name=password, + id=password + ]) + ]), + + div( + input([ + type=submit, + value='Log in' + ]) + ) + ]) + ] + ). + +login_view(post, Request) :- + http_read_data(Request, [username=Username, password=Password], []), + handle_login(Username, Password, Reply), + reply_prolog(Reply). + + +% Handles logging in, returning the correct Json in Reply. +handle_login(Username, Password, Reply) :- + user(Username, Hash), + crypto_password_hash(Password, Hash), !, + assert_session(Username, Token), + Reply = true. + +handle_login(Username, _, Reply) :- + user(Username, _), !, + Reply = incorrect_password. + +handle_login(_, _, incorrect_user). diff --git a/views/products.pl b/views/products.pl new file mode 100644 index 0000000..09b4de5 --- /dev/null +++ b/views/products.pl @@ -0,0 +1,57 @@ +:- http_handler(root(.), products_view, []). +:- http_handler(root('style.css'), http_reply_file('style.css', []), []). + +price_str(Price, PriceStr) :- + Euro is Price / 100, + format(atom(PriceStr), '€ ~2f', Euro). + +products_list([]) --> []. +products_list([product(Name, Price)|Products]) --> + {price_str(Price, PriceStr)}, + html([ + a([ + class(product), + 'data-name'(Name), + href('/cart/add/'+Name) + ], [ + div(class(product__name), [Name]), + div(class(product__price), [PriceStr]) + ]) + ]), + products_list(Products). + +cart([]) --> []. +cart([Name|Items]) --> + { + product(Name, Cents), + price_str(Cents, Price) + }, + html([ + div(class(cart__entry), [ + div(class(cart__name), Name), + div(class(cart__price), Price) + ]) + ]), + cart(Items). + +products_view(_Request) :- + http_session_id(SessionId), + bagof(product(X, Y), product(X, Y), Products), + (bagof(Name, cart_entry(SessionId, Name), CartEntries); CartEntries = []), + reply_html_page( + [ + title('point of shit'), + link([ + type('text/css'), + rel('stylesheet'), + href('style.css') + ]) + ], + [ + h1('Products'), + main([ + div(class(products), \products_list(Products)), + div(class(cart), \cart(CartEntries)) + ]) + ] + ).