Browse Source

Continue working on project.

Cixo Develop 6 months ago
parent
commit
4c8ca72c1b

+ 79 - 0
\\

@@ -0,0 +1,79 @@
+import { fullscreen } from "./fullscreen.js";
+
+export class product_adder extends fullscreen {
+    _build_node() {
+        const container = document.createElement("div");
+        container.classList.add("product-adder");
+
+        const label_name = document.createElement("label");
+        label_name.setAttribute("for", "name");
+        label_name.textContent = "Name:";
+
+        const name = document.createElement("input");
+        name.type = "text";
+        name.id = "name";
+        name.name = "name";
+        name.placeholder = "Sample...";
+
+        const label_description = document.createElement("label");
+        label_description.setAttribute("for", "description");
+        label_description.textContent = "Description:";
+
+        const description = document.createElement("input");
+        description.type = "text";
+        description.id = "description";
+        description.name = "description";
+        description.placeholder = "This is exa...";
+
+        const label_author = document.createElement("label");
+        label_author.setAttribute("for", "author");
+        label_author.textContent = "Author:";
+
+        const author = document.createElement("input");
+        author.type = "text";
+        author.id = "author";
+        author.name = "author";
+        author.placeholder = "John Snow...";
+
+        const label_barcode = document.createElement("label");
+        label_barcode.setAttribute("for", "barcode");
+        label_barcode.textContent = "Barcode:";
+
+        const barcode = document.createElement("input");
+        barcode.type = "number";
+        barcode.id = "barcode";
+        barcode.name = "barcode";
+        barcode.placeholder = "Enter EAN-12...";
+
+        const label_stock_count = document.createElement("label");
+        label_stock_count.setAttribute("for", "stock-count");
+        label_stock_count.textContent = "On stock:";
+
+        const stock_count = document.createElement("input");
+        stock_count.type = "number";
+        stock_count.id = "stock-count";
+        stock_count.name = "stock-count";
+        stock_count.placeholder = "20...";
+
+        const button = document.createElement("button");
+        button.type = "submit";
+        button.id = "add";
+        button.name = "add";
+        button.className = "material-icons";
+        button.textContent = "add";
+
+        container.appendChild(label_name);
+        container.appendChild(name);
+        container.appendChild(label_description);
+        container.appendChild(description);
+        container.appendChild(label_author);
+        container.appendChild(author);
+        container.appendChild(label_barcode);
+        container.appendChild(barcode);
+        container.appendChild(label_stock_count);
+        container.appendChild(stock_count);
+        container.appendChild(button);
+
+        return container;
+    }
+}

+ 3 - 0
application/theme/body.sass

@@ -0,0 +1,3 @@
+body    
+    margin: 0px
+    padding: 0px

+ 5 - 0
application/theme/colors.sass

@@ -0,0 +1,5 @@
+$primary: #6BAB90
+$secondary: #364652
+$background: #FFFFFF
+$font: #000000
+$dark: #444444

+ 6 - 0
application/theme/core.sass

@@ -0,0 +1,6 @@
+@import colors
+@import fonts 
+@import positions
+@import body
+@import login
+@import fullscreen

+ 20 - 0
application/theme/fonts.sass

@@ -0,0 +1,20 @@
+body, input, button 
+    font-size: 16px
+    font-family: "Raleway", sans-serif
+    font-style: normal
+    font-weight: normal
+    font-optical-sizing: auto
+
+h1, h2, h3, h4, h5, h6
+    font-family: "Cal Sans", sans-serif
+    font-optical-sizing: auto
+    font-weight: normal
+    font-style: normal
+    color: $primary
+
+.numbers
+    font-family: "Roboto", sans-serif
+    font-optical-sizing: auto
+    font-weight: normal
+    font-style: normal
+    font-variation-settings: "wdth" 100

+ 131 - 0
application/theme/fullscreen.sass

@@ -0,0 +1,131 @@
+.fullscreen-viewer
+    width: 100%
+    height: 100%
+    position: fixed
+    top: 0px
+    left: 0px
+    background-color: rgba(40, 40, 40, 0.7)
+
+    .close
+        position: absolute
+        top: 30px
+        right: 30px
+        padding: 10px
+        border-radius: 10px
+        color: red
+        border: red 2px solid
+        background-color: rgba(0, 0, 0, 0)
+        transition: transform 0.5s
+
+        &:hover
+            transform: scale(1.2)
+
+.product-fullscreen-viewer
+    .image
+        width: 100%
+        height: 100%
+        position: absolute
+        top: 0px
+        left: 0px
+        background-repeat: no-repeat
+        background-size: contain
+        background-position: center
+        z-index: -1
+
+    .title
+        width: 100%
+        position: absolute
+        top: 0px
+        left: 0px
+        background-color: $primary
+        border-radius: 0px 0px 10px 10px
+
+        h1
+            color: $secondary
+            padding: 10px
+            text-align: center
+
+    .bottom-side
+        position: absolute
+        bottom: 0px
+        left: 0px
+        width: calc(100% - 20px)
+        background-color: $font
+        color: $background
+        display: flex
+        flex-direction: column
+        border-radius: 10px 10px 0px 0px
+        padding: 10px
+        align-items: center
+        justify-content: center
+        vertical-align: center
+
+        .bottom-header, .description
+            width: calc(100% - 40px)
+            max-width: 500px
+            margin-left: 20px
+            margin-right: 20px
+
+        .bottom-header
+            display: flex
+            flex-direction: row
+            justify-content: space-between
+            vertical-align: center
+            align-items: center
+
+            p
+                display: flex
+                flex-direction: row
+                vertical-align: center
+                align-items: center
+
+                span
+                    display: block
+
+                    &:last-child
+                        margin-left: 5px
+
+.login-prompt, .product-adder
+    display: flex
+    vertical-align: center
+    align-items: center
+    justify-content: center
+    flex-direction: column
+
+    .center
+        padding: 20px
+        border-radius: 10px
+        border: 2px solid $primary
+        background-color: $secondary
+        display: flex
+        flex-direction: column
+        justify-content: center
+        align-items: center
+        
+        input
+            border: $background 2px solid
+            padding: 10px 5px
+            border-radius: 10px
+            color: $background
+            background-color: $dark
+            transition: transform 0.5s
+            margin-bottom: 20px
+
+            &:focus
+                transform: scale(1.1)
+                outline: 0 !important
+
+        button
+            border: $primary 2px solid
+            padding: 10px 15px
+            border-radius: 10px
+            background-color: $secondary
+            color: $primary
+            transition: transform 0.5s
+
+            &:hover
+                transform: scale(1.2)
+
+        label
+            color: $primary
+            margin-bottom: 5px

+ 16 - 0
application/theme/login.sass

@@ -0,0 +1,16 @@
+.top-bar .right
+    display: flex
+    flex-direction: row
+    vertical-align: center
+    justify-content: center
+    align-items: center
+
+    .login-info
+        margin-right: 10px
+        display: flex
+        vertical-align: center
+        justify-content: center
+        align-items: center
+
+        .icon
+            margin-right: 5px

+ 121 - 0
application/theme/positions.sass

@@ -0,0 +1,121 @@
+.top-bar
+    position: fixed
+    top: 0px
+    left: 0px
+    width: 100%
+    display: flex
+    flex-direction: row
+    justify-content: space-between
+    flex-wrap: nowrap
+    align-content: center
+    align-items: center
+    background-color: $secondary
+    color: $primary
+
+    input, button, select
+        background-color: transparent
+        color: $primary
+        transition: transform 0.5s
+        border: 2px solid $primary
+        border-radius: 10px
+        padding: 10px
+        margin-left: 10px
+        margin-right: 10px
+
+    input:focus
+        transform: scale(1.1)
+        outline: 0 !important
+
+    button:hover
+        transform: rotate(20deg)
+
+    .left, .right
+        margin: 10px
+
+    .left form
+        display: flex
+        flex-direction: row
+        align-items: stretch
+        align-content: stretch
+
+.products   
+    display: flex
+    justify-content: space-evenly
+    align-items: center
+    align-content: center
+    flex-wrap: wrap
+    flex-direction: row
+
+    .top-bar-spacing
+        width: 100%
+
+    .product
+        border: 2px solid $primary
+        border-radius: 10px
+        padding: 10px
+        margin: 10px
+        max-width: 500px
+
+        .header
+            width: 100%
+            display: flex
+            flex-direction: row
+            align-items: center
+            justify-content: space-between
+
+            button
+                background-color: $primary
+                color: $secondary
+                border: none
+                padding: 10px
+                border-radius: 10px
+                margin-left: 5px
+                margin-right: 5px
+                transition: transform 0.5s
+
+                &:hover
+                    transform: rotate(30deg)
+
+        .avairable
+            color: green
+
+        .unavairable
+            color: red
+    
+        .image 
+            border-radius: 10px
+            border: 2px solid $primary
+            max-width: 50%
+            transition: transform 0.5s
+
+            &:hover
+                transform: scale(1.1)
+
+        .author, .barcode
+            display: flex
+            align-items: center
+            align-content: center
+            flex-direction: row
+            flex-wrap: nowrap
+
+            span:last-child
+                margin-left: 5px
+
+        .bottom-container
+            display: flex
+            align-items: center
+            align-content: center
+            flex-direction: row
+            flex-wrap: nowrap
+            justify-content: space-between
+
+    .title
+        width: calc(100% - 40px)
+        text-align: center
+        margin: 20px
+
+        h1
+            border: 2px solid $primary
+            padding: 10px
+            border-radius: 10px
+            display: inline

+ 47 - 0
application/views/core.html

@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+
+<html>
+    <head>
+        <title>Reservationer</title>
+        
+        <meta charset="UTF-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta name="description" content="Simple reservations app.">
+
+        <link rel="preconnect" href="https://fonts.googleapis.com">
+        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+        <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cal+Sans&family=Raleway:ital,wght@0,100..900;1,100..900&display=swap">
+        <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap">
+        <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
+
+        <script src="app/bundle/app.js" type="text/javascript"></script>
+        <link rel="stylesheet" type="text/css" href="app/bundle/theme.css">
+    </head>
+
+    <body>
+        <main>
+            <nav>
+                <div class="top-bar">
+                    <div class="left">
+                        <form class="search">
+                            <input type="text" name="content" placeholder="Search" class="search-content">
+                            <select name="search-by"></select>
+                            <button type="submit" name="submit" class="search-submit material-icons">search</button>
+                        </form>
+                    </div>
+
+                    <div class="right">
+                    </div>
+                </div>
+            </nav>
+
+            <div class="products">
+                <div class="top-bar-spacing"></div>
+
+                <div class="title">
+                    <h1 class="search-title"></h1>
+                </div>
+            </div>
+        </main>
+    </body>
+</html>

+ 3 - 0
assets/__init__.py

@@ -50,3 +50,6 @@ from .config import config_generator
 from .app_config import app_config
 from .app_resources import app_resources
 
+""" Image managment. """
+from .image import image
+from .directory_image import image

+ 43 - 0
assets/directory_image.py

@@ -0,0 +1,43 @@
+import pathlib
+
+from .image import image
+from .exception import directory_image_exception
+
+class directory_image:
+    def __init__(self, target: pathlib.Path, thumbnail: int = 400) -> None:
+        self.__target = target
+        self.__thumbnail_size = thumbnail
+
+        if not self.__target.is_dir():
+            content = "Directory for image hosting: \""
+            content = content + str(target) + "\"."
+
+            raise directory_image_exception(content)
+    
+    @property
+    def thumbnail_size(self) -> int:
+        return self.__thumbnail_size
+
+    @property
+    def target(self) -> pathlib.Path:
+        return self.__target
+
+    def __name_full(self, image_id: int) -> str:
+        return "full_" + str(image_id) + "_.png"
+
+    def __name_thumbnail(self, image_id: int) -> str:
+        return "thumbnail_" + str(image_id) + "_.webp"
+
+    def __full_path(self, image_id: int) -> pathlib.Path:
+        return self.target / pathlib.Path(self.__full_name(image_id))
+
+    def __thumbnail_path(self, image_id: int) -> pathlib.Path:
+        return self.target / pathlib.Path(self.__thumbnail_name(image_id))
+
+    def store(self, coded: str, image_id: int) -> None:
+        full = self.__full_path(image_id)
+        thumbnail = self.__full_path(image_id)
+        
+        image(coded, self.thumbnail_size) \
+        .save_full(full) \
+        .save_thumbnail(thumbnail)

+ 11 - 0
assets/exception.py

@@ -34,6 +34,17 @@ class access_denied_exception(Exception):
     def __init__(self) -> None:
         super().__init__("Being logged in is required to access this.")
 
+class image_exception(Exception):
+    def __init__(self) -> None:
+        super().__init__("This is not property image file.")
+
+class directory_image_exception(Exception):
+    pass
+
+class image_save_exception(Exception):
+    def __init__(self, before: Exception) -> None:
+        super().__init__("Can not save image: " + str(before))
+
 class incomplete_request_exception(Exception):
     def __init__(self, name: str) -> None:
         content = "Request is not complete. This endpoint require " 

+ 66 - 0
assets/image.py

@@ -0,0 +1,66 @@
+import io
+import PIL
+import PIL.Image
+import base64
+import pathlib
+
+from .exception import image_exception
+
+class image:
+    def __init__(
+        self, 
+        coded: str, 
+        thumbnail: int = 400,
+        max_size: int = 2 * 1024 * 1024 * 16
+    ) -> None:
+        if len(coded) > max_size:
+            raise image_exception()
+
+        self.__thumbnail = thumbnail
+        self.__content = base64.standard_b64decode(coded)
+
+        if not self.valid:
+            raise image_exception()
+
+    @property
+    def thumbnail(self) -> tuple:
+        return (self.__thumbnail, self.__thumbnail)
+
+    @property
+    def content(self) -> bytes:
+        return self.__content
+
+    @property
+    def __file(self) -> io.BytesIO:
+        return io.BytesIO(self.content)
+
+    @property
+    def __image(self) -> PIL.Image:
+        try:
+            return PIL.Image.open(self.__file)
+        
+        except:
+            raise image_exception()
+        
+    def save_thumbnail(self, location: pathlib.Path) -> object:
+        with self.__image as image:
+            image.thumbnail(self.thumbnail)
+            image.save(location)
+        
+        return self
+
+    def save_full(self, location: pathlib.Path) -> object:
+        with self.__image as image:
+            image.save(location)
+
+        return self
+
+    @property
+    def valid(self) -> bool:    
+        try:
+            with self.__image as image:
+                image.verify()
+                return True
+        
+        except:
+            return False

+ 4 - 2
core.py

@@ -55,7 +55,9 @@ def server(
         "assets.server:instance", 
         port = port, 
         host = address, 
-        log_level = "info"
+        log_level = "info",
+        proxy_headers = True,
+        forwarded_allow_ips = "*"
     )
 
     app = uvicorn.Server(server_config)
@@ -156,7 +158,7 @@ def user(
     except validator_exception as error:
         print("Password is not correct, too easy to break.")
 
-    except json.JSONDecodeError as error:
+    except json.JSONDecodeError as error:  
         print("User JSON has syntax exception.")
         print(str(error))
 

+ 54 - 0
make-app.py

@@ -0,0 +1,54 @@
+#!/bin/python
+
+from pathlib import Path
+from json import loads
+from shutil import rmtree
+from shutil import copytree
+from shutil import copy
+
+from maketool import sass_compiler
+from maketool import esbuild_compiler
+from maketool import render
+from maketool import script
+from maketool import link
+
+root = Path(__file__).absolute().parent
+source = root / Path("application/")
+build = root / Path("static/")
+
+views = source / Path("views/")
+styles = source / Path("theme/")
+scripts = source / Path("scripts/")
+assets = source / Path("assets/")
+
+bundle = build / Path("bundle/")
+assets_output = build / Path("assets/")
+
+scripts_loader = scripts / Path("core.js")
+theme_loader = styles / Path("core.sass")
+
+script_bundle = bundle / Path("app.js")
+theme_bundle = bundle / Path("theme.css")
+
+if build.exists():
+    rmtree(build)
+
+build.mkdir()
+bundle.mkdir()
+
+sass_compiler(theme_loader).build(theme_bundle)
+esbuild_compiler(scripts_loader).build(script_bundle)
+copytree(assets, assets_output)
+
+for view in views.iterdir():
+    if not view.is_file():
+        continue
+
+    if str(view.suffix) != ".html":
+        continue
+
+    copy(view, build / Path(view.name))
+    
+    
+
+    

+ 1 - 0
requirements.txt

@@ -2,3 +2,4 @@ sqlmodel
 fastapi
 typer-slim
 uvicorn
+pillow

+ 770 - 0
static/bundle/app.js

@@ -0,0 +1,770 @@
+(() => {
+  // application/scripts/height_equaler.js
+  var height_equaler = class {
+    #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";
+    }
+  };
+
+  // application/scripts/product.js
+  var product = class {
+    name;
+    description;
+    author;
+    image;
+    stock_count;
+    barcode;
+    constructor(target) {
+      this.name = null;
+      this.description = null;
+      this.author = null;
+      this.image = null;
+      this.stock_count = null;
+      this.barcode = 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"];
+    }
+    get dump() {
+      return {
+        "name": this.name,
+        "description": this.description,
+        "author": this.author,
+        "image": this.image,
+        "stock_count": this.stock_count,
+        "barcode": this.barcode
+      };
+    }
+    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;
+      return true;
+    }
+  };
+
+  // application/scripts/products_loader.js
+  var products_loader = 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);
+    }
+  };
+
+  // application/scripts/fullscreen.js
+  var fullscreen = class {
+    #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);
+    }
+    get_query(selector) {
+      if (!this.visible) {
+        throw new TypeError("Can not get item from not visible.");
+      }
+      return this.#node.querySelector(selector);
+    }
+    get #close() {
+      const close_button = document.createElement("button");
+      close_button.classList.add("material-icons");
+      close_button.classList.add("close");
+      close_button.innerText = "close";
+      close_button.addEventListener("click", () => {
+        this.hide();
+      });
+      return close_button;
+    }
+    #prepare() {
+      if (!this.visible) {
+        throw new TypeError("Can not prepare not existed.");
+      }
+      this.#node.style.transition = "opacity 0.5s";
+      this.#node.classList.add("fullscreen-viewer");
+      this.#node.appendChild(this.#close);
+    }
+    set #opacity(target) {
+      if (!this.visible) {
+        throw new TypeError("Can not change opacity of not existed.");
+      }
+      this.#node.style.opacity = String(target);
+    }
+    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._build_node();
+      this.#prepare();
+      this.#opacity = 0;
+      document.querySelector("body").appendChild(this.#node);
+      setTimeout(() => {
+        this.#opacity = 1;
+      }, 100);
+    }
+  };
+
+  // application/scripts/product_fullscreen.js
+  var product_fullscreen = class 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);
+      return container;
+    }
+  };
+
+  // application/scripts/user.js
+  var user = class {
+    #nick;
+    #apikey;
+    constructor(nick, apikey) {
+      this.#nick = nick;
+      this.#apikey = apikey;
+    }
+    get nick() {
+      return this.#nick;
+    }
+    get apikey() {
+      return this.#apikey;
+    }
+  };
+
+  // application/scripts/login_manager.js
+  var login_manager = class {
+    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,
+        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");
+    }
+  };
+
+  // application/scripts/product_container.js
+  var product_container = class {
+    #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) {
+        const manage = document.createElement("div");
+        manage.classList.add("manage");
+        header.appendChild(manage);
+        manage.appendChild(this.#rent);
+        manage.appendChild(this.#give_back);
+      }
+      return header;
+    }
+    get #rent() {
+      const rent_button = document.createElement("button");
+      rent_button.classList.add("material-icons");
+      rent_button.classList.add("rent-button");
+      rent_button.innerText = "backpack";
+      return rent_button;
+    }
+    get #give_back() {
+      const give_back_button = document.createElement("button");
+      give_back_button.classList.add("material-icons");
+      give_back_button.classList.add("rent-button");
+      give_back_button.innerText = "save_alt";
+      return give_back_button;
+    }
+    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.stock_count > 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 #image() {
+      const image = document.createElement("img");
+      image.classList.add("image");
+      image.src = this.#target.image;
+      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);
+    }
+  };
+
+  // application/scripts/product_containers.js
+  var product_containers = class {
+    #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;
+    }
+  };
+
+  // application/scripts/searcher.js
+  var searcher = class {
+    #input;
+    #category;
+    #manager;
+    #result;
+    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();
+      });
+    }
+    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());
+    }
+  };
+
+  // application/scripts/login_prompt.js
+  var login_prompt = class extends fullscreen {
+    constructor(target) {
+      super();
+      target.addEventListener("click", () => {
+        this.show();
+      });
+    }
+    get _nick() {
+      return this.get_query("#nick").value;
+    }
+    get _password() {
+      return this.get_query("#password").value;
+    }
+    get _result() {
+      return this.get_query("#result");
+    }
+    async _login() {
+      const manager = new login_manager();
+      const result = await manager.login(this._nick, this._password);
+      if (result) {
+        this._result.style.color = "green";
+        this._result.innerText = "Login success!";
+        setTimeout(() => {
+          location.reload();
+        }, 500);
+        return;
+      }
+      this._result.style.color = "red";
+      this._result.innerText = "Can not login! Check login and password.";
+    }
+    _build_node() {
+      const container = document.createElement("div");
+      container.classList.add("login-prompt");
+      const center = document.createElement("form");
+      center.classList.add("center");
+      container.appendChild(center);
+      const nick_label = document.createElement("label");
+      nick_label.innerText = "Your nick:";
+      nick_label.htmlFor = "nick";
+      center.appendChild(nick_label);
+      const nick = document.createElement("input");
+      nick.type = "text";
+      nick.name = "nick";
+      nick.id = "nick";
+      nick.placeholder = "Nick...";
+      center.appendChild(nick);
+      const password_label = document.createElement("label");
+      password_label.innerText = "Your password:";
+      password_label.htmlFor = "password";
+      center.appendChild(password_label);
+      const password = document.createElement("input");
+      password.type = "password";
+      password.name = "password";
+      password.id = "password";
+      password.placeholder = "Password...";
+      center.appendChild(password);
+      const submit = document.createElement("button");
+      submit.type = "submit";
+      submit.classList.add("material-icons");
+      submit.innerText = "send";
+      center.appendChild(submit);
+      const result = document.createElement("p");
+      result.id = "result";
+      result.classList.add("result");
+      center.appendChild(result);
+      center.addEventListener("submit", (target) => {
+        target.preventDefault();
+        this._login();
+      });
+      return container;
+    }
+  };
+
+  // application/scripts/product_adder.js
+  var product_adder = class extends fullscreen {
+    _build_node() {
+      const container = document.createElement("div");
+      container.classList.add("product-adder");
+      const center = document.createElement("form");
+      center.classList.add("center");
+      container.appendChild(center);
+      const label_name = document.createElement("label");
+      label_name.setAttribute("for", "name");
+      label_name.textContent = "Name:";
+      center.appendChild(label_name);
+      const name = document.createElement("input");
+      name.type = "text";
+      name.id = "name";
+      name.name = "name";
+      name.placeholder = "Sample...";
+      center.appendChild(name);
+      const label_description = document.createElement("label");
+      label_description.setAttribute("for", "description");
+      label_description.textContent = "Description:";
+      center.appendChild(label_description);
+      const description = document.createElement("input");
+      description.type = "text";
+      description.id = "description";
+      description.name = "description";
+      description.placeholder = "This is exa...";
+      center.appendChild(description);
+      const label_author = document.createElement("label");
+      label_author.setAttribute("for", "author");
+      label_author.textContent = "Author:";
+      center.appendChild(label_author);
+      const author = document.createElement("input");
+      author.type = "text";
+      author.id = "author";
+      author.name = "author";
+      author.placeholder = "John Snow...";
+      center.appendChild(author);
+      const label_barcode = document.createElement("label");
+      label_barcode.setAttribute("for", "barcode");
+      label_barcode.textContent = "Barcode:";
+      center.appendChild(label_barcode);
+      const barcode = document.createElement("input");
+      barcode.type = "number";
+      barcode.id = "barcode";
+      barcode.name = "barcode";
+      barcode.placeholder = "Enter EAN-12...";
+      center.appendChild(barcode);
+      const label_stock_count = document.createElement("label");
+      label_stock_count.setAttribute("for", "stock-count");
+      label_stock_count.textContent = "On stock:";
+      center.appendChild(label_stock_count);
+      const stock_count = document.createElement("input");
+      stock_count.type = "number";
+      stock_count.id = "stock-count";
+      stock_count.name = "stock-count";
+      stock_count.placeholder = "20...";
+      center.appendChild(stock_count);
+      const button = document.createElement("button");
+      button.type = "submit";
+      button.id = "add";
+      button.name = "add";
+      button.className = "material-icons";
+      button.textContent = "add";
+      center.appendChild(button);
+      return container;
+    }
+  };
+
+  // application/scripts/login_bar.js
+  var login_bar = class {
+    #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 user2 = await this.#manager.get_user();
+      if (user2 === 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 = user2.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);
+    }
+  };
+
+  // application/scripts/core.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 manager = new product_containers(container);
+    new login_bar(login_space);
+    new searcher(search_bar, manager, search_title).show_all();
+  });
+})();

File diff suppressed because it is too large
+ 0 - 0
static/bundle/theme.css


+ 42 - 0
static/core.html

@@ -1,5 +1,47 @@
 <!DOCTYPE html>
 
 <html>
+    <head>
+        <title>Reservationer</title>
+        
+        <meta charset="UTF-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta name="description" content="Simple reservations app.">
 
+        <link rel="preconnect" href="https://fonts.googleapis.com">
+        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+        <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cal+Sans&family=Raleway:ital,wght@0,100..900;1,100..900&display=swap">
+        <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap">
+        <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
+
+        <script src="app/bundle/app.js" type="text/javascript"></script>
+        <link rel="stylesheet" type="text/css" href="app/bundle/theme.css">
+    </head>
+
+    <body>
+        <main>
+            <nav>
+                <div class="top-bar">
+                    <div class="left">
+                        <form class="search">
+                            <input type="text" name="content" placeholder="Search" class="search-content">
+                            <select name="search-by"></select>
+                            <button type="submit" name="submit" class="search-submit material-icons">search</button>
+                        </form>
+                    </div>
+
+                    <div class="right">
+                    </div>
+                </div>
+            </nav>
+
+            <div class="products">
+                <div class="top-bar-spacing"></div>
+
+                <div class="title">
+                    <h1 class="search-title"></h1>
+                </div>
+            </div>
+        </main>
+    </body>
 </html>

+ 38 - 0
tests/007-image.py

@@ -0,0 +1,38 @@
+import pathlib
+
+current = pathlib.Path(__file__).parent
+root = current.parent
+
+import sys
+sys.path.append(str(root))
+
+import assets
+
+import base64
+
+print("Opening \"sample.png\" sample image.")
+
+with open("sample.png", "rb") as sample:
+    result = base64.b64encode(sample.read())
+
+print("Opened, size: " + str(len(result)))
+print()
+
+full = pathlib.Path("sample.full.png")
+thumbnail = pathlib.Path("sample.thumbnail.webp")
+
+print("Create image object:")
+sample = assets.image(result)
+sample.save_full(full)
+sample.save_thumbnail(thumbnail)
+
+print("File saved. All went well.")
+print()
+
+input("Press enter to remove:")
+print("Removing files...")
+
+full.unlink()
+thumbnail.unlink()
+
+

BIN
tests/sample.png


Some files were not shown because too many files changed in this diff