Przeglądaj źródła

Continue working and add autocomplete by google to backend.

Cixo Develop 5 miesięcy temu
rodzic
commit
ee5a2e91b7
36 zmienionych plików z 2217 dodań i 0 usunięć
  1. 21 0
      application/scripts/bool_response.js
  2. 74 0
      application/scripts/color_theme.js
  3. 111 0
      application/scripts/confirm_action.js
  4. 29 0
      application/scripts/core.js
  5. 35 0
      application/scripts/create_request.js
  6. 28 0
      application/scripts/delete_product_window.js
  7. 30 0
      application/scripts/delete_request.js
  8. 33 0
      application/scripts/edit_image_request.js
  9. 34 0
      application/scripts/edit_request.js
  10. 167 0
      application/scripts/formscreen.js
  11. 81 0
      application/scripts/fullscreen.js
  12. 32 0
      application/scripts/height_equaler.js
  13. 76 0
      application/scripts/login_bar.js
  14. 65 0
      application/scripts/login_manager.js
  15. 62 0
      application/scripts/login_prompt.js
  16. 72 0
      application/scripts/product.js
  17. 115 0
      application/scripts/product_adder.js
  18. 45 0
      application/scripts/product_base.js
  19. 214 0
      application/scripts/product_container.js
  20. 60 0
      application/scripts/product_containers.js
  21. 157 0
      application/scripts/product_editor.js
  22. 87 0
      application/scripts/product_fullscreen.js
  23. 33 0
      application/scripts/product_give_back.js
  24. 34 0
      application/scripts/product_give_back_request.js
  25. 33 0
      application/scripts/product_rent.js
  26. 31 0
      application/scripts/product_rent_request.js
  27. 20 0
      application/scripts/product_view.js
  28. 46 0
      application/scripts/products_loader.js
  29. 46 0
      application/scripts/rents_screen.js
  30. 71 0
      application/scripts/request.js
  31. 54 0
      application/scripts/reservation.js
  32. 44 0
      application/scripts/reservation_factory.js
  33. 3 0
      application/scripts/reservations_loader.js
  34. 44 0
      application/scripts/scroll_up.js
  35. 113 0
      application/scripts/searcher.js
  36. 17 0
      application/scripts/user.js

+ 21 - 0
application/scripts/bool_response.js

@@ -0,0 +1,21 @@
+export class bool_response {
+    #result;
+    #cause;
+
+    constructor(target) {
+        this.#result = (target.result === "success");
+        this.#cause = null;
+
+        if (!this.result) {
+            this.#cause = target.cause;
+        }
+    }
+
+    get cause() {
+        return this.#cause;
+    }
+
+    get result() {
+        return this.#result;
+    }
+}

+ 74 - 0
application/scripts/color_theme.js

@@ -0,0 +1,74 @@
+export class color_theme {
+    #button;
+    #themes;
+
+    get themes() {
+        return Object.keys(this.#themes); 
+    }
+
+    theme_name(target) {
+        return this.#themes[target];
+    }       
+
+    constructor(button, themes = null) {
+        this.#button = button;
+        this.#themes = themes;
+
+        if (this.#themes === null) {
+            this.#themes = {
+                "dark-theme": "Dark",
+                "white-theme": "White"
+            };
+        }
+
+        this.#load();
+
+        this.#button.addEventListener("click", () => {
+            this.change();
+        });
+    }
+
+    get current() {
+        if (localStorage.hasOwnProperty("theme")) {
+            return localStorage.getItem("theme");
+        }   
+
+        return this.themes[this.themes.length - 1];
+    }
+
+    #load() {
+        this.#show(this.current);
+    }
+
+    #save(target) {
+        localStorage.setItem("theme", target);
+    }
+
+    #show(target) {
+        const themes = this.themes;
+        const body = document.querySelector("body");
+        
+        body.classList.forEach(count => {
+            if (themes.indexOf(count) !== -1) {
+                body.classList.remove(count);
+            }
+        });
+
+        body.classList.add(target);
+    }
+
+    change() {
+        const themes = this.themes;
+        const current = this.current;
+        let position = themes.indexOf(current) + 1;
+
+        if (position === themes.length) {
+            position = 0;
+        }
+
+        const updated = themes[position];
+
+        this.#save(updated);
+        this.#show(updated)
+    }
+}

+ 111 - 0
application/scripts/confirm_action.js

@@ -0,0 +1,111 @@
+export class confirm_action {
+    #node;
+    #action;
+
+    constructor() {
+        this.#node = null;
+        this.#action = true;
+    }
+
+    get _description() {
+        throw new TypeError("It must be overwriten.");
+    }
+
+    get _title() {
+        return "You must confirm it.";
+    }
+
+    _action() {
+        throw new TypeError("It must be overwriten.");
+    }
+
+    show() {
+        if (this.#node !== null) {
+            return;
+        }
+
+        this.#action = true;
+        this.#node = this.#create_window();
+        document.querySelector("body").appendChild(this.#node);
+
+        setTimeout(() => {
+            this.#node.style.opacity = "1";
+        }, 100);
+    }
+
+    hide() {
+        if (this.#node === null) {
+            return;
+        }
+
+        this.#action = false;
+        this.#node.style.opacity = "0";
+        
+        setTimeout(() => {
+            if (this.#node === null) {
+                return;
+            }
+
+            this.#node.remove();
+            this.#node = null;
+        }, 500);
+    }
+
+    #create_window() {
+        const container = document.createElement("div");
+        container.classList.add("confirm-window");
+        container.style.transition = "opacity 0.5s";
+        container.style.opacity = "0";
+
+        const center = document.createElement("div");
+        center.classList.add("center");
+        container.appendChild(center);
+
+        const title = document.createElement("div");
+        title.classList.add("title");
+        center.appendChild(title);
+
+        const title_text = document.createElement("h3");
+        title_text.innerText = this._title;
+        title.appendChild(title_text);
+
+        const description = document.createElement("div");
+        description.classList.add("description");
+        center.appendChild(description);
+
+        const description_text = document.createElement("p");
+        description_text.innerText = this._description;
+        description.appendChild(description_text);
+
+        const buttons = document.createElement("div");
+        buttons.classList.add("buttons");
+        center.appendChild(buttons);
+
+        const cancel_button = document.createElement("button");
+        cancel_button.classList.add("cancel");
+        cancel_button.classList.add("material-icons");
+        cancel_button.innerText = "clear";
+        buttons.appendChild(cancel_button);
+
+        const confirm_button = document.createElement("button");
+        confirm_button.classList.add("confirm");
+        confirm_button.classList.add("material-icons");
+        confirm_button.innerText = "send";
+        buttons.appendChild(confirm_button);
+
+        cancel_button.addEventListener("click", () => {
+            this.hide();
+        });
+
+        confirm_button.addEventListener("click", () => {
+            if (this.#action === false) {
+                return;
+            }
+
+            this._action();
+            this.hide();
+        });
+
+        return container;
+    }
+}

+ 29 - 0
application/scripts/core.js

@@ -0,0 +1,29 @@
+import { height_equaler } from "./height_equaler.js";
+import { product } from "./product.js";
+import { products_loader } from "./products_loader.js";
+import { product_container } from "./product_container.js";
+import { product_containers } from "./product_containers.js";
+import { searcher } from "./searcher.js";
+import { login_bar } from "./login_bar.js";
+import { scroll_up } from "./scroll_up.js";
+import { color_theme } from "./color_theme.js";
+
+document.addEventListener("DOMContentLoaded", async () => {
+    const top_bar_spacing = new height_equaler(
+        document.querySelector(".top-bar"),
+        document.querySelector(".top-bar-spacing")
+    );
+
+    const container = document.querySelector(".products");
+    const search_bar = document.querySelector("form.search");
+    const search_title = document.querySelector(".search-title");
+    const login_space = document.querySelector(".top-bar .right");
+    const scroll_up_button = document.querySelector(".scroll-up-button");
+    const reverse_colors = document.querySelector(".reverse-colors");
+    const manager = new product_containers(container);
+
+    new login_bar(login_space);
+    new scroll_up(scroll_up_button);
+    new color_theme(reverse_colors);
+    new searcher(search_bar, manager, search_title).show_all();
+});

+ 35 - 0
application/scripts/create_request.js

@@ -0,0 +1,35 @@
+import { product_base } from "./product_base.js";
+import { login_manager } from "./login_manager.js";
+import { bool_response } from "./bool_response.js";
+import { request } from "./request.js";
+
+export class create_request extends request {
+    #image;
+    #product;
+
+    constructor(product, image) {
+        super();
+        
+        this.#image = image;
+        this.#product = product;
+    }
+
+    get _response() {
+        return bool_response;
+    }
+
+    get data() {
+        return Object.assign(this.#product.dump, {
+            "image": this.#image,
+            "apikey": this._apikey
+        });
+    }
+    
+    get method() {
+        return "POST";
+    }
+
+    get url() {
+        return "/product/create";
+    }
+}

+ 28 - 0
application/scripts/delete_product_window.js

@@ -0,0 +1,28 @@
+import { confirm_action } from "./confirm_action.js";
+import { delete_request } from "./delete_request.js";
+import { searcher } from "./searcher.js";
+
+export class delete_product_window extends confirm_action {
+    #target;
+
+    constructor(target) {
+        super();
+        this.#target = target;
+    }
+
+    get _title() {
+        return "Do you want remove it?";
+    }
+
+    get _description() {
+        let content = "You try to remove " + this.#target.name + ". ";
+        content += "You can not restore it, when confirm.";
+
+        return content;
+    }
+
+    async _action () {
+        new delete_request(this.#target).connect();
+        searcher.reload();
+    }
+}   

+ 30 - 0
application/scripts/delete_request.js

@@ -0,0 +1,30 @@
+import { login_manager } from "./login_manager.js";
+import { request } from "./request.js";
+import { bool_response } from "./bool_response.js";
+
+export class delete_request extends request {
+    #product
+
+    constructor(product) {
+        super();
+        this.#product = product;
+    }
+
+    get _response() {
+        return bool_response;
+    }
+    
+    get data() {
+        return {
+            "apikey": this._apikey,
+        };
+    }
+
+    get url() {
+        return "/product/barcode/" + this.#product.barcode;
+    }
+
+    get method() {
+        return "DELETE";
+    }
+}

+ 33 - 0
application/scripts/edit_image_request.js

@@ -0,0 +1,33 @@
+import { request } from "./request.js";
+import { bool_response } from "./bool_response.js";
+
+export class edit_image_request extends request {
+    #image;
+    #target;
+
+    constructor(target, image) {
+        super();
+
+        this.#target = target;
+        this.#image = image;
+    }
+
+    get _response() {
+        return bool_response;
+    }
+
+    get data() {
+        return {
+            "image": this.#image,
+            "apikey": this._apikey
+        };
+    }
+
+    get method() {
+        return "POST";
+    }
+
+    get url() {
+        return "/product/update/image/barcode/" + this.#target.barcode;
+    }
+}

+ 34 - 0
application/scripts/edit_request.js

@@ -0,0 +1,34 @@
+import { product_base } from "./product_base.js";
+import { login_manager } from "./login_manager.js";
+import { bool_response } from "./bool_response.js";
+import { request } from "./request.js";
+
+export class edit_request extends request {
+    #target;
+    #updated;
+
+    constructor(target, updated) {
+        super();
+
+        this.#target = target;
+        this.#updated = updated;
+    }
+
+    get _response() {
+        return bool_response;
+    }
+
+    get data() {
+        return Object.assign(this.#updated.dump, {
+            "apikey": this._apikey
+        });
+    }
+
+    get method() {
+        return "POST";
+    }
+
+    get url() {
+        return "/product/update/barcode/" + this.#target.barcode;
+    }
+}

+ 167 - 0
application/scripts/formscreen.js

@@ -0,0 +1,167 @@
+import { fullscreen } from "./fullscreen.js";
+
+export class formscreen extends fullscreen {
+    #form;
+    #result;
+
+    constructor() {
+        super();
+
+        this.#form = null;
+        this.#result = null;
+    }
+
+    get _name() {
+        throw new TypeError("This is virtual getter!");
+    }
+
+    _process() {
+        this._error = "This is abstract, and must be overwriten.";
+    }
+
+    _build_form() {
+        throw new TypeError("This is virtual method!");
+    }
+    
+    _get_input(name) {
+        return this.get_query("input[name=\"" + name + "\"]");
+    }
+
+    _build_node() {
+        const center = document.createElement("div");
+        center.classList.add("center");
+
+        const title = document.createElement("div");
+        title.classList.add("title");
+        center.appendChild(title);
+
+        const title_content = document.createElement("h3");
+        title_content.innerText = this._name;
+        title.appendChild(title_content);
+
+        const form = document.createElement("form");
+        center.appendChild(form);
+
+        form.addEventListener("click", () => {
+            this._clear_results();
+        });
+
+        this.#form = document.createElement("div");
+        this.#form.classList.add("content");
+        form.appendChild(this.#form);
+
+        this.#result = document.createElement("div");
+        this.#result.classList.add("result");
+        form.appendChild(this.#result);
+    
+        const bottom = document.createElement("div");
+        bottom.classList.add("bottom");
+        form.appendChild(bottom);
+
+        const close_button = document.createElement("button");
+        close_button.classList.add("close");
+        close_button.classList.add("material-icons");
+        close_button.innerText = "close";
+        close_button.type = "button";
+        bottom.appendChild(close_button);
+       
+        const send_button = document.createElement("button");
+        send_button.classList.add("send");
+        send_button.classList.add("material-icons");
+        send_button.innerText = "send";
+        send_button.type = "submit";
+        bottom.appendChild(send_button);
+
+        close_button.addEventListener("click", () => {
+            this.hide();
+        });
+ 
+        form.addEventListener("submit", (target) => {
+            target.preventDefault();
+
+            this._process();
+        });
+
+        this._build_form();
+
+        return center;
+    }
+
+    _create_input(name, label_text, placeholder, worker = null) {
+        const container = document.createElement("div");
+        container.classList.add("input-container");
+        container.classList.add("input-" + name);
+
+        const label = document.createElement("label");
+        label.htmlFor = name;
+        label.innerText = label_text;
+        container.appendChild(label);
+
+        const input = document.createElement("input");
+        input.type = "text";
+        input.placeholder = placeholder;
+        input.name = name;
+        input.id = name;
+        container.appendChild(input);
+
+        if (worker !== null) {
+            worker(input);
+        }
+
+        if (!this.#form) {
+            throw new Error("Screen is not visible yet!");
+        }
+
+        this.#form.appendChild(container);
+
+        return () => {
+            return input.value;
+        };
+    }
+
+    _clear_results() {
+        if (!this.#result) {
+            return;
+        }
+
+        while (this.#result.lastChild) {
+            this.#result.lastChild.remove();
+        }
+    }
+    
+    set _info(target) {
+        this._clear_results();
+
+        const info = document.createElement("p");
+        info.classList.add("info");
+        info.innerText = target;
+        
+        if (this.#result) {
+            this.#result.appendChild(info);
+        }
+    }
+    
+    set _error(target) {
+        this._clear_results();
+
+        const info = document.createElement("p");
+        info.classList.add("error");
+        info.innerText = target;
+        
+        if (this.#result) {
+            this.#result.appendChild(info);
+        }
+    }
+
+    set _success(target) {
+        this._clear_results();
+
+        const info = document.createElement("p");
+        info.classList.add("success");
+        info.innerText = target;
+       
+        if (this.#result) {
+            this.#result.appendChild(info);
+        }
+    }
+}

+ 81 - 0
application/scripts/fullscreen.js

@@ -0,0 +1,81 @@
+export class fullscreen {
+    #node;
+    
+    constructor() {
+        this.#node = null;
+    }
+
+    get visible() {
+        return this.#node !== null;
+    }
+
+    _build_node() {
+        throw new TypeError("This is virtual method!");
+    }
+
+    get #opacity() {
+        if (!this.visible) {
+            throw new TypeError("Can not change opacity of not existed.");
+        }
+
+        return Number(this.#node.style.opacity);
+    }
+
+    set #opacity(target) {
+        if (!this.visible) {
+            throw new TypeError("Can not change opacity of not existed.");
+        }
+
+        this.#node.style.opacity = String(target);
+    }
+
+    get_query(selector) {
+        if (!this.visible) {
+            throw new TypeError("Can not get item from not visible.");
+        }
+
+        return this.#node.querySelector(selector);
+    }
+
+    #prepare() {
+        const container = document.createElement("div");
+        
+        container.classList.add("fullscreen-viewer");
+        container.style.transition = "opacity 0.5s";
+        container.appendChild(this._build_node());
+    
+        return container;
+    }      
+
+    hide() {
+        if (!this.visible) {
+            return;
+        }
+
+        this.#opacity = 0;
+
+        setTimeout(() => {
+            if (!this.visible) {
+                return;
+            }
+                
+            this.#node.remove();
+            this.#node = null;
+        }, 500);
+    }   
+
+    show() {
+        if (this.visible) {
+            return;
+        }   
+        
+        this.#node = this.#prepare();
+        this.#opacity = 0;
+
+        document.querySelector("body").appendChild(this.#node);
+
+        setTimeout(() => {
+            this.#opacity = 1;
+        }, 100);
+    }
+}   

+ 32 - 0
application/scripts/height_equaler.js

@@ -0,0 +1,32 @@
+export class height_equaler {
+    #to;
+    #from;
+
+    constructor(from, to) {
+        this.#from = from;
+        this.#to = to;
+
+        this.#set_styles();
+    
+        new ResizeObserver(() => {
+            this.#update();
+        }).observe(from);
+
+        setTimeout(() => {
+            this.#update();
+        }, 100);
+    }
+
+    get height() {
+        return this.#from.offsetHeight;
+    }
+
+    #set_styles() {
+        this.#to.style.height = "0px";
+        this.#to.style.transition = "height 0.5s";
+    }
+
+    #update() {
+        this.#to.style.height = this.height + "px"; 
+    }
+}

+ 76 - 0
application/scripts/login_bar.js

@@ -0,0 +1,76 @@
+import { login_manager } from "./login_manager.js";
+import { login_prompt } from "./login_prompt.js";
+import { product_adder } from "./product_adder.js";
+
+export class login_bar {
+    #manager;
+
+    constructor(target) {
+        this.#manager = new login_manager();
+
+        if (!this.#manager.logged_in) {
+            this.#not_logged(target);
+            return;
+        }
+
+        this.#logged(target);
+    }
+
+    #not_login_propertly() {
+        this.#manager.logout();
+        location.reload();
+    }
+
+    async #logged(target) {
+        const user = await this.#manager.get_user();
+
+        if (user === null) {
+            this.#not_login_propertly();
+        }   
+
+        const info_icon = document.createElement("span");
+        info_icon.classList.add("icon");
+        info_icon.classList.add("material-icons");
+        info_icon.innerText = "account_circle";
+
+        const info_content = document.createElement("span");
+        info_content.innerText = user.nick;
+
+        const info = document.createElement("p");
+        info.classList.add("login-info");
+        info.appendChild(info_icon);
+        info.appendChild(info_content);
+        target.appendChild(info);
+
+        const logout_button = document.createElement("button");
+        logout_button.innerText = "logout";
+        logout_button.classList.add("logout-button");
+        logout_button.classList.add("material-icons");
+        target.appendChild(logout_button);
+
+        const add_product_button = document.createElement("button");
+        add_product_button.innerText = "add";
+        add_product_button.classList.add("add-product-button");
+        add_product_button.classList.add("material-icons");
+        target.appendChild(add_product_button);
+
+        add_product_button.addEventListener("click", () => {
+            new product_adder().show();
+        });
+
+        logout_button.addEventListener("click", () => {
+            this.#manager.logout();
+            location.reload();
+        });
+    }
+
+    #not_logged(target) {
+        const login_button = document.createElement("button");
+        login_button.innerText = "account_circle";
+        login_button.classList.add("login-button");
+        login_button.classList.add("material-icons");
+        target.appendChild(login_button);
+
+        new login_prompt(login_button);
+    }
+}

+ 65 - 0
application/scripts/login_manager.js

@@ -0,0 +1,65 @@
+import { user } from "./user.js";
+
+export class login_manager {
+    get apikey() {
+        return localStorage.getItem("apikey");
+    }
+
+    get logged_in() {
+        return localStorage.getItem("apikey") !== null;
+    }
+
+    #create_request(data) {
+        return {
+            method: "POST",
+            body: JSON.stringify(data),
+            headers: {
+                "Content-Type": "application/json"
+            }
+        };  
+    }
+
+    async get_user() {
+        if (!this.logged_in) {
+            return null;
+        }
+
+        const request_data = this.#create_request({
+            apikey: this.apikey
+        });
+
+        const request = await fetch("/user", request_data);
+        const response = await request.json();
+
+        if (response.result !== "success") {
+            return null;
+        }
+
+        return new user(
+            response.nick,
+            response.apikey
+        );
+    }
+
+    async login(nick, password) { 
+        const request_data = this.#create_request({
+            nick: nick,
+            password: password
+        });
+
+        const request = await fetch("/user/login", request_data);
+        const response = await request.json();
+
+        if (response.result !== "success") {
+            return false;
+        }
+
+        localStorage.setItem("apikey", response.apikey);
+
+        return true;
+    }
+
+    logout() {
+        localStorage.removeItem("apikey");
+    }
+}   

+ 62 - 0
application/scripts/login_prompt.js

@@ -0,0 +1,62 @@
+import { formscreen } from "./formscreen.js";
+import { login_manager } from "./login_manager.js";
+
+export class login_prompt extends formscreen {
+    #nick;
+    #password;
+
+    constructor(target) {
+        super();
+        
+        target.addEventListener("click", () => {
+            this.show();
+        });
+    }
+
+    get _name() {
+        return "Login";
+    }
+
+    async _process() {
+        try {
+            this._info = "Processing...";
+            await this.#login();
+            this._success = "Logged in!";
+
+            setTimeout(() => {
+                location.reload();
+            }, 250);
+        } catch (error) {
+            this._error = new String(error);
+        }
+    }
+
+    async #login() {
+        const manager = new login_manager();
+        const result = await manager.login(
+            this.#nick(), 
+            this.#password()
+        );
+
+        if (result) {
+            return;
+        } 
+        
+        throw new Error("Can not login. Check nick and password.");
+    }
+
+    _build_form() {
+        this.#nick = this._create_input(
+            "nick",
+            "Nick:",
+            "Sample..."
+        );
+
+        this.#password = this._create_input(
+            "password",
+            "Password:",
+            "ABCDEFGH",
+            (input) => { input.type = "password"; }
+        );
+    }
+}

+ 72 - 0
application/scripts/product.js

@@ -0,0 +1,72 @@
+/**
+ * Represents a product with various attributes.
+ * 
+ * @class
+ * @property {string|null} name - The name of the product
+ * @property {string|null} description - A description of the product
+ * @property {string|null} author - The author or creator of the product
+ * @property {string|null} image - An image URL or reference for the product
+ * @property {number|null} stock_count - The current inventory count
+ * @property {string|null} barcode - The unique barcode identifier 
+ */
+export class product {
+    name;
+    description;
+    author;
+    image;
+    stock_count;
+    barcode;
+    thumbnail;
+    on_stock;
+
+    constructor(target) {
+        this.name = null;
+        this.description = null;
+        this.author = null;
+        this.image = null;
+        this.stock_count = null;
+        this.barcode = null;
+        this.thumbnail = null;
+        this.on_stock = null;
+
+        if ("name" in target) this.name = target["name"];
+        if ("description" in target) this.description = target["description"];
+        if ("author" in target) this.author = target["author"];
+        if ("image" in target) this.image = target["image"];
+        if ("stock_count" in target) this.stock_count = target["stock_count"];
+        if ("barcode" in target) this.barcode = target["barcode"];
+        if ("thumbnail" in target) this.thumbnail = target["thumbnail"];
+        if ("on_stock" in target) this.on_stock = target["on_stock"];
+    }
+
+    get dump() {
+        const dumped = {
+            "name": this.name,
+            "description": this.description,
+            "author": this.author,
+            "image": this.image,
+            "stock_count": this.stock_count,
+            "barcode": this.barcode,
+            "thumbnail": this.thumbnail
+        };
+
+        if (this.on_stock !== null) {
+            dumped["on_stock"] = this.on_stock;
+        }
+
+        return dumped;
+    }   
+
+    get ready() {
+        if (this.name === null || this.description === null) return false;
+        if (this.author === null || this.image === null) return false;
+        if (this.stock_count === null || this.barcode === null) return false;
+        if (this.thumbnail === null) return false;
+
+        return true;
+    }   
+
+    copy() {
+        return new product(this.dump);
+    }
+}

+ 115 - 0
application/scripts/product_adder.js

@@ -0,0 +1,115 @@
+import { formscreen } from "./formscreen.js";
+import { create_request } from "./create_request.js";
+import { bool_response } from "./bool_response.js";
+import { product_base } from "./product_base.js";
+import { searcher } from "./searcher.js";
+
+export class product_adder extends formscreen {
+    #name;
+    #description;
+    #author;
+    #barcode;
+    #stock_count;
+    #image;
+
+    get _name() {
+        return "Add product";
+    }
+
+    _build_form() {
+        this.#name = this._create_input(
+            "name", 
+            "Name:", 
+            "Sample..."
+        );
+
+        this.#description = this._create_input(
+            "description",
+            "Description:",
+            "This is sample product..."
+        );
+
+        this.#author = this._create_input(
+            "author",
+            "Author:",
+            "Jack Black"
+        );
+
+        this.#barcode = this._create_input(
+            "barcode",
+            "Barcode (EAN):",
+            "123456789012...",
+            (input) => { input.type = "number"; }
+        );
+
+        this.#stock_count = this._create_input(
+            "stock_count",
+            "Stock count:",
+            "10...",
+            (input) => { input.type = "number"; }
+        );
+
+        this._create_input(
+            "image",
+            "Product image:",
+            "",
+            (input) => {
+                this.#image = input;
+                input.type = "file";
+                input.accept = "image/*";
+            }
+        );
+    }
+
+    async #code_image() {
+        if (this.#image.files.length === 0) {
+            throw new Error("Upload image for product.");
+        }
+
+        const file = this.#image.files.item(0);
+        const buffer = await file.arrayBuffer();
+        const list = new Uint8Array(buffer);
+        
+        let content = new String();
+
+        list.forEach((code) => {
+            content += String.fromCharCode(code);
+        });
+
+        return btoa(content);
+    }   
+
+    async #submit() {
+        const product = new product_base();
+        product.name = this.#name();
+        product.description = this.#description();
+        product.author = this.#author();
+        product.stock_count = this.#stock_count();
+        product.barcode = this.#barcode();
+
+        const image = await this.#code_image();
+
+        const request = new create_request(product, image);
+        const response = await request.connect();
+
+        if (!response.result) {
+            throw new Error(response.cause);
+        }
+    }
+
+    async _process() {
+        try {
+            this._info = "Uploading...";
+            await this.#submit();
+            this._success = "Created success!";
+        
+            searcher.reload();
+
+            setTimeout(() => {
+                this.hide();
+            }, 500);
+        } catch (error) {
+            this._error = new String(error);
+        }
+    }
+}

+ 45 - 0
application/scripts/product_base.js

@@ -0,0 +1,45 @@
+export class product_base {
+    name;
+    description;
+    author;
+    barcode;
+    stock_count;
+
+    constructor(target = null) {
+        this.name = this._extract(target, "name");
+        this.description = this._extract(target, "description");
+        this.author = this._extract(target, "author");
+        this.barcode = this._extract(target, "barcode");
+        this.stock_count = this._extract(target, "stock_count");
+
+        if (this.stock_count !== null) {
+            this.stock_count = Number(this.stock_count);
+        }
+    }
+
+    get dump() {
+        return {
+            "name": this.name,
+            "description": this.description,
+            "author": this.author,
+            "barcode": this.barcode,
+            "stock_count": this.stock_count
+        }
+    }
+
+    _extract(dict, name) {
+        if (dict === null) {
+            return null;
+        }
+
+        if (name in dict) {
+            return dict[name];
+        }
+
+        return null;
+    }
+
+    get avairable() {
+        return (this.stock_count > 0);
+    }
+}   

+ 214 - 0
application/scripts/product_container.js

@@ -0,0 +1,214 @@
+import { product } from "./product.js";
+import { product_fullscreen } from "./product_fullscreen.js";
+import { login_manager } from "./login_manager.js";
+import { delete_product_window } from "./delete_product_window.js";
+import { product_editor } from "./product_editor.js";
+import { product_rent } from "./product_rent.js";
+import { product_give_back } from "./product_give_back.js";
+
+export class product_container {
+    #target;
+    #node;
+    #login;
+
+    constructor(target) {
+        this.#target = new product(target.dump);
+        this.#node = null;
+        this.#login = new login_manager().logged_in; 
+    }
+
+    get #header() {
+        const header = document.createElement("div");
+        header.classList.add("header");
+
+        const title = document.createElement("h3");
+        title.innerText = this.#target.name;
+        header.appendChild(title);
+
+        if (this.#login) {
+            header.appendChild(this.#manage);
+        }   
+
+        return header;
+    }   
+
+    get #manage() {
+        const manage = document.createElement("div");
+        manage.classList.add("manage");
+
+        const all_rents_button = document.createElement("button");
+        all_rents_button.classList.add("material-icons");
+        all_rents_button.classList.add("all-rents-button");
+        all_rents_button.innerText = "list";
+        manage.appendChild(all_rents_button);
+
+        const rent_button = document.createElement("button");
+        rent_button.classList.add("material-icons");
+        rent_button.classList.add("rent-button");
+        rent_button.innerText = "backpack";
+        manage.appendChild(rent_button);
+
+        const give_back_button = document.createElement("button");
+        give_back_button.classList.add("material-icons");
+        give_back_button.classList.add("give-back-button");
+        give_back_button.innerText = "save_alt";
+        manage.appendChild(give_back_button);
+
+        const edit_button = document.createElement("button");
+        edit_button.classList.add("material-icons");
+        edit_button.classList.add("edit-button");
+        edit_button.innerText = "edit";
+        manage.appendChild(edit_button);
+
+        const delete_button = document.createElement("button");
+        delete_button.classList.add("material-icons");
+        delete_button.classList.add("delete-button");
+        delete_button.innerText = "remove_circle_outline";
+        manage.appendChild(delete_button);
+
+        all_rents_button.addEventListener("click", () => {
+            new product_all_rents(this.#target).show();
+        });
+
+        rent_button.addEventListener("click", () => {
+            new product_rent(this.#target).show();
+        });
+
+        give_back_button.addEventListener("click", () => {
+            new product_give_back(this.#target).show();
+        });
+
+        edit_button.addEventListener("click", () => {
+            new product_editor(this.#target).show();
+        });
+
+        delete_button.addEventListener("click", () => {
+            new delete_product_window(this.#target).show();
+        });
+
+        return manage;
+    }
+
+    get #description() {
+        const container = document.createElement("div");
+        container.classList.add("description");
+
+        const description = document.createElement("p");
+        description.innerText = this.#target.description;
+        description.classList.add("content");
+
+        const author_container = document.createElement("div");
+        author_container.classList.add("author");
+
+        const author = document.createElement("span");
+        author.innerText = this.#target.author;
+
+        const author_icon = document.createElement("span");
+        author_icon.classList.add("material-icons");
+        author_icon.innerText = "attribution";
+
+        author_container.appendChild(author_icon);
+        author_container.appendChild(author);
+
+        const stock_count = document.createElement("p");
+        stock_count.classList.add("stock-count");
+        stock_count.classList.add("material-icons");
+
+        if (this.#target.on_stock > 0) {
+            stock_count.innerText = "check_circle";
+            stock_count.classList.add("avairable");
+        } else {
+            stock_count.innerText = "cancel";
+            stock_count.classList.add("unavairable");
+        }
+
+        const barcode_container = document.createElement("p");
+        barcode_container.classList.add("barcode");
+
+        const barcode = document.createElement("span");
+        barcode.innerText = this.#target.barcode;
+        barcode.classList.add("numbers");
+
+        const barcode_icon = document.createElement("span");
+        barcode_icon.classList.add("material-icons");
+        barcode_icon.innerText = "qr_code_scanner";
+
+        barcode_container.appendChild(barcode_icon);
+        barcode_container.appendChild(barcode);
+
+        container.appendChild(description);
+        container.appendChild(author_container);
+        container.appendChild(barcode_container);
+        container.appendChild(stock_count);
+
+        return container;
+    }
+
+    get #cache_bypass() {
+        return "?cache=" + new String(Math.floor(Math.random() * 100));
+    }
+
+    get #image() {
+        const image = document.createElement("img");
+        image.classList.add("image");
+        image.src = this.#target.thumbnail + this.#cache_bypass;
+        image.alt = this.#target.name;
+
+        image.addEventListener("click", () => {
+            new product_fullscreen(this.#target).show();
+        });
+
+        return image;
+    }   
+
+    get node() {
+        if (this.#node !== null) {
+            return this.#node;
+        }
+
+        const bottom_container = document.createElement("div");
+        bottom_container.classList.add("bottom-container");
+        bottom_container.appendChild(this.#description);
+        bottom_container.appendChild(this.#image);
+
+        const container = document.createElement("div");
+        container.classList.add("product");
+        container.appendChild(this.#header);
+        container.appendChild(bottom_container);
+
+        return this.#node = container;
+    }
+
+    add(target) {
+        const node = this.node;
+
+        node.style.opacity = "0";
+        node.style.transition = "opacity 0.5s";
+        
+        target.appendChild(node);
+
+        setTimeout(() => {
+            node.style.opacity = "1";
+        }, 50);
+    }
+
+    drop() {
+        const container = this.#node;
+
+        if (container === null) {
+            throw new TypeError("It is not showed yet.");
+        }
+
+        container.style.opacity = "1";
+        container.style.transition = "opacity 0.5s";
+
+        setTimeout(() => {
+            container.style.opacity = "0";
+        }, 50);
+
+        setTimeout(() => {
+            this.#node = null;
+            container.remove();
+        }, 550);
+    }
+}

+ 60 - 0
application/scripts/product_containers.js

@@ -0,0 +1,60 @@
+import { product } from "./product.js";
+import { product_container } from "./product_container.js";
+
+export class product_containers {
+    #content;
+    #where;
+    #inserted;
+
+    constructor(where) {
+        this.#where = where;
+        this.#content = new Array();
+        this.#inserted = new Array();
+    }   
+    
+    add_list(target) {
+        target.forEach(count => {
+            this.add(count);
+        });
+
+        return this;
+    }
+
+    add(target) {
+        const current = new product_container(target);
+        this.#content.push(current);
+
+        return this;
+    }
+
+    clean() {
+        this.#content = new Array();
+
+        return this;
+    }
+
+    update() {
+        this.#hide();
+
+        setTimeout(() => {
+            this.#content.forEach(count => {
+                this.#inserted.push(count);
+                count.add(this.#where);
+            });
+        }, 500);
+
+        return this;
+    }
+
+    #hide() {
+        this.#inserted.forEach(count => {
+            if (!this.#content.includes(count)) {
+                count.drop();
+            }
+        });
+
+        this.#inserted = new Array();
+
+        return this;
+    }
+}

+ 157 - 0
application/scripts/product_editor.js

@@ -0,0 +1,157 @@
+import { formscreen } from "./formscreen.js";
+import { edit_request } from "./edit_request.js";
+import { searcher } from "./searcher.js";
+import { edit_image_request } from "./edit_image_request.js";
+
+export class product_editor extends formscreen {
+    #target;
+    #name;
+    #description;
+    #author;
+    #barcode;
+    #stock_count;
+    #image;
+
+    constructor(target) {
+        super();
+        
+        this.#target = target;
+    }
+
+    get target() {
+        return this.#target;
+    }
+
+    get _name() {
+        return "Product editor";
+    }
+
+    _build_form() {
+         this.#name = this._create_input(
+            "name", 
+            "Name:", 
+            "Sample...",
+            (input) => {
+                input.value = this.#target.name;
+            }
+        );
+
+        this.#description = this._create_input(
+            "description",
+            "Description:",
+            "This is sample product...",
+            (input) => {
+                input.value = this.#target.description;
+            }
+        );
+
+        this.#author = this._create_input(
+            "author",
+            "Author:",
+            "Jack Black",
+            (input) => {
+                input.value = this.#target.author;
+            }
+        );
+
+        this.#barcode = this._create_input(
+            "barcode",
+            "Barcode (EAN):",
+            "123456789012...",
+            (input) => { 
+                input.type = "number"; 
+                input.value = this.#target.barcode
+            }
+        );
+
+        this.#stock_count = this._create_input(
+            "stock_count",
+            "Stock count:",
+            "10...",
+            (input) => { 
+                input.type = "number"; 
+                input.value = this.#target.stock_count
+            }
+        );
+
+        this._create_input(
+            "image",
+            "Change product image:",
+            "",
+            (input) => {
+                this.#image = input;
+                input.type = "file";
+                input.accept = "image/*";
+            }
+        );
+    }
+
+    async #code_image() {
+        if (this.#image.files.length === 0) {
+            return null;
+        }
+
+        const file = this.#image.files.item(0);
+        const buffer = await file.arrayBuffer();
+        const list = new Uint8Array(buffer);
+
+        let content = new String();
+
+        list.forEach((code) => {
+            content += String.fromCharCode(code);
+        });
+
+        return btoa(content);
+    }
+
+    async #submit() {
+        const copy = this.#target.copy();
+        copy.name = this.#name();
+        copy.description = this.#description();
+        copy.author = this.#author();
+        copy.barcode = this.#barcode();
+        copy.stock_count = this.#stock_count();
+        
+        const request = new edit_request(this.#target, copy);
+        const response = await request.connect();
+
+        if (!response.result) {
+            throw new Error(response.cause);
+        }
+
+        this.#target = copy;
+    }   
+
+    async #image_submit() {
+        const image = await this.#code_image();
+
+        if (image === null) {
+            return;
+        }
+
+        const request = new edit_image_request(this.#target, image);
+        const response = await request.connect();
+
+        if (!response.result) {
+            throw new Error(response.cause);
+        }
+    }
+
+    async _process() {
+        try {
+            this._info = "Uploading...";
+            await this.#submit();
+            this._info = "Processing image...";
+            await this.#image_submit();
+            this._success = "Updated success!";
+
+            searcher.reload();
+
+            setTimeout(() => {
+                this.hide();
+            }, 500);
+        } catch (error) {
+            this._error = new String(error);
+        }
+    }
+}   

+ 87 - 0
application/scripts/product_fullscreen.js

@@ -0,0 +1,87 @@
+import { fullscreen } from "./fullscreen.js";
+import { product } from "./product.js";
+
+export class product_fullscreen extends fullscreen {
+    #target;
+
+    constructor(target) {
+        super();
+
+        this.#target = target;
+    }
+
+    get target() {
+        return this.#target;
+    }
+
+    _build_node() {
+        const container = document.createElement("div");
+        container.classList.add("product-fullscreen-viewer");
+
+        const image = document.createElement("div");
+        image.style.backgroundImage = "url(\"" + this.target.image + "\")";
+        image.classList.add("image");
+        container.appendChild(image);
+
+        const title = document.createElement("div");
+        title.classList.add("title");
+        container.appendChild(title);
+
+        const title_content = document.createElement("h1");
+        title_content.innerText = this.target.name;
+        title.appendChild(title_content);
+
+        const bottom = document.createElement("div");
+        bottom.classList.add("bottom-side");
+        container.appendChild(bottom);
+
+        const bottom_header = document.createElement("div");
+        bottom_header.classList.add("bottom-header");
+        bottom.appendChild(bottom_header);
+        
+        const barcode_icon = document.createElement("span");
+        barcode_icon.classList.add("material-icons");
+        barcode_icon.innerText = "qr_code_scanner";
+
+        const barcode_content = document.createElement("span");
+        barcode_content.innerText = this.target.barcode;
+        barcode_content.classList.add("numbers");
+
+        const barcode = document.createElement("p");
+        barcode.appendChild(barcode_icon);
+        barcode.appendChild(barcode_content);
+        bottom_header.appendChild(barcode);
+        
+        const author_icon = document.createElement("span");
+        author_icon.classList.add("material-icons");
+        author_icon.innerText = "attribution";
+
+        const author_content = document.createElement("span");
+        author_content.innerText = this.target.author;
+
+        const author = document.createElement("p");
+        author.appendChild(author_icon);
+        author.appendChild(author_content);
+        bottom_header.appendChild(author);
+
+        const description = document.createElement("div");
+        description.classList.add("description");
+        bottom.appendChild(description);
+
+        const description_content = document.createElement("p");
+        description_content.innerText = this.target.description;
+        description.appendChild(description_content);
+
+        const close_button = document.createElement("button");
+        close_button.classList.add("material-icons");
+        close_button.classList.add("close");
+        close_button.innerText = "close";
+        container.appendChild(close_button);
+
+        close_button.addEventListener("click", () => {
+            this.hide();
+        });
+
+        return container;
+    }
+}

+ 33 - 0
application/scripts/product_give_back.js

@@ -0,0 +1,33 @@
+import { rents_screen } from "./rents_screen.js";
+import { reservation } from "./reservation.js";
+import { reservation_factory } from "./reservation_factory.js";
+import { product_give_back_request } from "./product_give_back_request.js";
+
+export class product_give_back extends rents_screen {
+    get _name() {
+        return "Product give back";
+    }
+
+    async _process() {
+        try {
+            this._info = "Processing...";
+            
+            const target = new reservation_factory()
+            .email(this._email)
+            .phone_number(this._phone)
+            .product(this._target)
+            .result();
+
+            const request = new product_give_back_request(target);
+            const response = await request.connect();
+
+            if (!response.result) {
+                throw new Error(response.cause);
+            }
+
+            this._success = "Success!";
+        } catch (error) {
+            this._error = String(error);
+        }
+    }
+}

+ 34 - 0
application/scripts/product_give_back_request.js

@@ -0,0 +1,34 @@
+import { request } from "./request.js";
+import { reservation } from "./reservation.js";
+import { bool_response } from "./bool_response.js";
+
+export class product_give_back_request extends request {
+    #reservation;
+    
+    constructor(reservation) {
+        super();
+
+        this.#reservation = reservation;
+    }
+
+    get data() {
+        return Object.assign(this.#reservation.dump, {
+            "apikey": this._apikey
+        });
+    }
+
+    get _response() {
+        return bool_response;
+    }
+
+    get method() {
+        return "POST";
+    }
+
+    get url() {
+        return (
+            "/give_back/product/barcode/" 
+            + this.#reservation.product_barcode
+        );
+    }
+}

+ 33 - 0
application/scripts/product_rent.js

@@ -0,0 +1,33 @@
+import { rents_screen } from "./rents_screen.js";
+import { product_rent_request } from "./product_rent_request.js";
+import { reservation } from "./reservation.js";
+import { reservation_factory } from "./reservation_factory.js";
+
+export class product_rent extends rents_screen {
+    get _name() {
+        return "Product rent";
+    }
+
+    async _process() {
+        try {
+            this._info = "Processing...";
+
+            const target = new reservation_factory()
+            .email(this._email)
+            .phone_number(this._phone)
+            .product(this._target)
+            .result();
+
+            const request = new product_rent_request(target); 
+            const response = await request.connect();
+
+            if (!response.result) {
+                throw new Error(response.cause);
+            }
+
+            this._success = "New rent added.";
+        } catch (error) {
+            this._error = String(error);
+        }
+    }
+}

+ 31 - 0
application/scripts/product_rent_request.js

@@ -0,0 +1,31 @@
+import { request } from "./request.js";
+import { reservation } from "./reservation.js";
+import { bool_response } from "./bool_response.js";
+
+export class product_rent_request extends request {
+    #reservation;
+    
+    constructor(reservation) {
+        super();
+
+        this.#reservation = reservation;
+    }
+
+    get data() {
+        return Object.assign(this.#reservation.dump, {
+            "apikey": this._apikey
+        });
+    }
+
+    get _response() {
+        return bool_response;
+    }
+
+    get method() {
+        return "POST";
+    }
+
+    get url() {
+        return "/rent/product/barcode/" + this.#reservation.product_barcode;
+    }
+}

+ 20 - 0
application/scripts/product_view.js

@@ -0,0 +1,20 @@
+import { product_base } from "./product_base.js";
+
+export product_view export product_base {
+    image_url;
+    thumbnail_url;
+
+    constructor(target) {
+        super(target);
+        
+        this.image_url = this._extract(target, "image");
+        this.thumbnail_url = this._extract(target, "thumbnail");
+    }
+
+    dump() {
+        return Object.assign(super.dump(), {
+            "image": this.image_url,
+            "thumbnail": this.thumbnail_url
+        });
+    }
+}

+ 46 - 0
application/scripts/products_loader.js

@@ -0,0 +1,46 @@
+import { product } from "./product.js";
+
+export class products_loader {
+    static async all() {
+        const request = await fetch("/products/");
+        const response = await request.json();
+        
+        return products_loader.#response_to_collection(response);
+    }
+
+    static #response_to_collection(response) {
+        const result = new Array();
+        
+        if (response.result !== "success") {
+            return result;
+        }
+
+        response.collection.forEach(serialized => {
+            result.push(new product(serialized));
+        });
+
+        return result;
+    }
+
+    static async search_name(name) {
+        return await products_loader.#search(
+            "/product/search/name", 
+            name
+        ); 
+    }
+
+    static async search_author(author) {
+        return await products_loader.#search(
+            "/product/search/author", 
+            author
+        );
+    }
+
+    static async #search(path, parameter) {
+        const coded = encodeURI(parameter);
+        const request = await fetch(path + "/" + coded);
+        const response = await request.json();
+        
+        return products_loader.#response_to_collection(response);
+    }
+}

+ 46 - 0
application/scripts/rents_screen.js

@@ -0,0 +1,46 @@
+import { formscreen } from "./formscreen.js";
+
+export class rents_screen extends formscreen {
+    #target;
+    #email;
+    #phone;
+
+    get _email() {
+        return this.#email();
+    }   
+        
+    get _phone() {
+        return this.#phone();
+    }       
+
+    constructor(target) {
+        super();
+        
+        this.#target = target;
+    }
+
+    get _target() {
+        return this.#target;
+    }
+
+    _build_form() {
+        this.#email = this._create_input(
+            "email",
+            "E-mail:",
+            "[email protected]",
+            (input) => { 
+                input.type = "email"; 
+            }
+        );
+
+        this.#phone = this._create_input(
+            "phone",
+            "Phone number:",
+            "123-456-789",
+            (input) => {
+                input.type = "tel";
+                input.pattern = "[0-9]{3}-[0-9]{3}-[0-9]{3}";
+            }
+        );
+    }
+}

+ 71 - 0
application/scripts/request.js

@@ -0,0 +1,71 @@
+import { login_manager } from "./login_manager.js";
+
+export class request {
+    get settings() {
+        return {
+            "method":  this.method,
+            "headers": this.headers,
+            "body": this.body
+        };
+    }
+
+    get _apikey() {
+        const manager = new login_manager();
+
+        if (manager.logged_in) {
+            return manager.apikey;
+        }
+
+        throw new Error("User must be logged in.");
+    }
+
+    get method() {
+        throw new TypeError("It must be overwrite.");
+    }
+
+    get url() {
+        throw new TypeError("It must be overwrite.");
+    }
+    
+    get headers() {
+        if (this.method === "GET") {
+            return {};
+        }
+
+        return {
+            "Content-Type": "application/json"
+        };  
+    }   
+
+    get body() {
+        if (this.data === null) {
+            return "";
+        }
+
+        return JSON.stringify(this.data);
+    }
+
+    get _response() {
+        throw new TypeError("It must be overwrite.");
+    }
+
+    async connect() {
+        const request = await fetch(this.url, this.settings);
+
+        if (!request.ok) {
+            throw new Error("Fail when requested: \"" + this.url + "\".");
+        }
+
+        const response = await request.json();
+
+        if (!("result" in response)) {
+            throw new Error("Bad response, not contain result.");
+        }
+
+        return new this._response(response);
+    }
+
+    get data() {
+        throw new TypeError("This must be overwrite.");
+    }
+}

+ 54 - 0
application/scripts/reservation.js

@@ -0,0 +1,54 @@
+export class reservation {
+    email;
+    phone_number;
+    product_barcode;
+
+    constructor(target = null) {
+        this.email = null;
+        this.phone_number = null;
+        this.product_barcode = null;
+
+        if (target === null) {
+            return ;
+        }
+
+        if ("email" in target) {
+            this.email = target["email"];
+        }
+
+        if ("target_barcode" in target) {
+            this.product_barcode = target["target_barcode"];
+        }
+
+        if ("phone_number" in target) {
+            this.phone_number = target["phone_number"];
+        }
+    }
+
+    get dump() {
+        const dumped = {
+            "target_barcode": this.product_barcode
+        };
+
+        if (this.email !== null) {
+            dumped["email"] = this.email;
+        }
+
+        if (this.phone_number !== null) {
+            dumped["phone_number"] = this.phone_number;
+        }
+
+        return dumped;
+    }
+
+    get ready() {
+        if (this.product_barcode === null) return false;
+        if (this.email === null && this.phone_number === null) return false;
+
+        return true;
+    }
+
+    copy() {
+        return new reservation(this.dump);
+    }
+}

+ 44 - 0
application/scripts/reservation_factory.js

@@ -0,0 +1,44 @@
+import { reservation } from "./reservation.js";
+
+export class reservation_factory {
+    #target;
+
+    constructor() {
+        this.#target = new reservation();
+    }
+
+    phone_number(target) {
+        target = target.trim();
+
+        if (target.length === 0) {
+            target = null;
+        }
+
+        this.#target.phone_number = target;
+        return this;
+    }
+
+    email(target) {
+        target = target.trim();
+
+        if (target.length === 0) {
+            target = null;
+        }
+
+        this.#target.email = target;
+        return this;
+    }
+
+    product(target) {
+        this.#target.product_barcode = target.barcode;
+        return this;
+    }
+
+    result() {
+        if (this.#target.ready) {
+            return this.#target;
+        }
+
+        throw new Error("Target reservation is not ready yet.");
+    }
+}

+ 3 - 0
application/scripts/reservations_loader.js

@@ -0,0 +1,3 @@
+export class reservations_loader {
+    static 
+}   

+ 44 - 0
application/scripts/scroll_up.js

@@ -0,0 +1,44 @@
+export class scroll_up {
+    #button;
+
+    constructor(button) {
+        this.#button = button;
+        this.#update();
+        
+        document.addEventListener("scroll", () => {
+            this.#update()
+        });
+
+        this.#button.addEventListener("click", () => {
+            this.scroll();
+        });
+    }
+
+    scroll() {
+        this.#position = 0; 
+    }
+
+    get #position() {
+        return document.scrollingElement.scrollTop;
+    }
+
+    set #position(target) {
+        document.scrollingElement.scrollTop = target;
+    }
+
+    get #visible() {
+        return Number(this.#button.style.opacity) === 1;
+    }
+
+    set #visible(target) {
+        this.#button.style.opacity = (target) ? "1" : "0";
+    }
+
+    get #margin() {
+        return 20;
+    }
+
+    #update() {
+        this.#visible = (this.#position > this.#margin);
+    }
+}

+ 113 - 0
application/scripts/searcher.js

@@ -0,0 +1,113 @@
+import { products_loader } from "./products_loader.js";
+import { product_containers } from "./product_containers.js";
+
+export class searcher {
+    #input;
+    #category;
+    #manager;
+    #result;
+
+    static #instances;
+
+    static #add(instance) {
+        if (typeof(searcher.#instances) !== "object") {
+            searcher.#instances = new Array();
+        }
+
+        searcher.#instances.push(instance);
+    }
+
+    static reload() {
+        if (typeof(searcher.#instances) !== "object") {
+            return ;
+        }
+
+        searcher.#instances.forEach(instance => {
+            instance.update();
+        });
+    }
+    
+    constructor(search_form, manager, result) {
+        this.#input = search_form.querySelector("input[type=\"text\"]");
+        this.#category = search_form.querySelector("select");
+        this.#manager = manager;
+        this.#result = result;
+
+        this.#selector_complete();
+        search_form.addEventListener("submit", (target) => {
+            target.preventDefault();
+            this.update();
+        });
+
+        searcher.#add(this);
+    }
+
+    get categories() {
+        return {
+            "name": "Name",
+            "author": "Author"
+        }
+    }
+
+    #selector_complete() {
+        const category = this.#category;
+        const categories = this.categories;
+
+        Object.keys(categories).forEach(name => {
+            const option = document.createElement("option");
+            option.value = name;
+            option.innerText = categories[name];
+        
+            category.appendChild(option);
+        });
+    }
+
+    get #loader() {
+        return {
+            "name": products_loader.search_name,
+            "author": products_loader.search_author
+        }[this.category];
+    }
+
+    get category() {
+        return this.#category.value;
+    }
+
+    get phrase() {
+        return this.#input.value.trim();
+    }
+
+    get #result_title() {
+        return this.#result.innerText;
+    }
+
+    set #result_title(target) {
+        this.#result.innerText = target;
+    }
+
+    async update() {
+        if (this.phrase.length === 0) {
+            this.show_all();
+            return;
+        }
+
+        this.#insert(await this.#loader(this.phrase));
+    }
+
+    #insert(list) {
+        if (list.length === 0) {
+            this.#result_title = "Not found anything.";
+        } else {
+            this.#result_title = "Browse our products!";
+        }
+
+        this.#manager
+        .clean()
+        .add_list(list)
+        .update();
+    }
+
+    async show_all() {
+        this.#insert(await products_loader.all())
+    }
+}

+ 17 - 0
application/scripts/user.js

@@ -0,0 +1,17 @@
+export class user {
+    #nick;
+    #apikey;
+
+    constructor(nick, apikey) {
+        this.#nick = nick;
+        this.#apikey = apikey;
+    }
+
+    get nick() {
+        return this.#nick;
+    }
+
+    get apikey() {
+        return this.#apikey;
+    }
+}