write doc
Took 2 hours 59 minutes
This commit is contained in:
parent
82ab47e9fd
commit
7d9dfca62e
5
ToDo.md
5
ToDo.md
@ -5,4 +5,7 @@
|
|||||||
- [ ] Fix Fehler, wenn keine Getränke hinzugefügt worden sind
|
- [ ] Fix Fehler, wenn keine Getränke hinzugefügt worden sind
|
||||||
- [ ] Schriftarten Lokal machen
|
- [ ] Schriftarten Lokal machen
|
||||||
- [x] Probleme beim Laden der Container im Setup
|
- [x] Probleme beim Laden der Container im Setup
|
||||||
-
|
- [ ] Container option "Auto tare" hinzufügen
|
||||||
|
- Heißt, sobald ein Inhalt in einen Container eingestellt wird und das Volumen angegeben ist, werden die Sensoren auf den aktuellen Füllstand als 100% gesetzt
|
||||||
|
- [ ] Nach Speichern des Setups und bei Einmessen auf Schließen, Setup verlassen
|
||||||
|
- [ ]
|
60
doc/Notes.md
60
doc/Notes.md
@ -1,4 +1,5 @@
|
|||||||
# Notes und kleine Dokumentation
|
# Notes und kleine Dokumentation
|
||||||
|
|
||||||
Was haben wir bereits am iTender Projekt gemacht?
|
Was haben wir bereits am iTender Projekt gemacht?
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
@ -6,6 +7,7 @@ Was haben wir bereits am iTender Projekt gemacht?
|
|||||||
## Konzept-Erstellung
|
## Konzept-Erstellung
|
||||||
|
|
||||||
#### Ideen
|
#### Ideen
|
||||||
|
|
||||||
- Grund-Ideen
|
- Grund-Ideen
|
||||||
- Smarten Cocktail-Mischer
|
- Smarten Cocktail-Mischer
|
||||||
- 4 Getränke Behälter (mit Saft, Sirup oder Likör bzw. Schnapps)
|
- 4 Getränke Behälter (mit Saft, Sirup oder Likör bzw. Schnapps)
|
||||||
@ -13,7 +15,8 @@ Was haben wir bereits am iTender Projekt gemacht?
|
|||||||
- Raspberry Pi als Prozessoreinheit
|
- Raspberry Pi als Prozessoreinheit
|
||||||
- Display in der Front mit Benutzeroberfläche
|
- Display in der Front mit Benutzeroberfläche
|
||||||
- Automatisches filtern von Getränken, je nachdem welche "Zutaten" in den Behältern sind
|
- Automatisches filtern von Getränken, je nachdem welche "Zutaten" in den Behältern sind
|
||||||
- Messung der aktuellen Füllmenge der Behälter, basierend auf Gewicht (mittels Wägezelle) oder Abstand zur Wasseroberfläche (mittels Ultraschall-Sensor)
|
- Messung der aktuellen Füllmenge der Behälter, basierend auf Gewicht (mittels Wägezelle) oder Abstand zur
|
||||||
|
Wasseroberfläche (mittels Ultraschall-Sensor)
|
||||||
|
|
||||||
- Nice to have
|
- Nice to have
|
||||||
- LED-Stripes für schöne Beleuchtung, basierend auf dem aktuellen Status der Maschine
|
- LED-Stripes für schöne Beleuchtung, basierend auf dem aktuellen Status der Maschine
|
||||||
@ -22,10 +25,12 @@ Was haben wir bereits am iTender Projekt gemacht?
|
|||||||
- Kühlung der Container mittels Peltierelement und Lüftern
|
- Kühlung der Container mittels Peltierelement und Lüftern
|
||||||
|
|
||||||
#### Erstes 3D-Modell
|
#### Erstes 3D-Modell
|
||||||
|
|
||||||
<img src="./Screenshot_Model1.1_FrontTopRight.png" width="50%">
|
<img src="./Screenshot_Model1.1_FrontTopRight.png" width="50%">
|
||||||
<img src="./Screenshot_Model1.1_BackDownLeft.png" width="50%">
|
<img src="./Screenshot_Model1.1_BackDownLeft.png" width="50%">
|
||||||
|
|
||||||
#### Neues 3D-Modell
|
#### Neues 3D-Modell
|
||||||
|
|
||||||
<img src="./Screenshot_Model1.2_Front.png" width="50%">
|
<img src="./Screenshot_Model1.2_Front.png" width="50%">
|
||||||
<img src="./Screenshot_Model1.2_Back.png" width="50%">
|
<img src="./Screenshot_Model1.2_Back.png" width="50%">
|
||||||
|
|
||||||
@ -35,12 +40,23 @@ Was haben wir bereits am iTender Projekt gemacht?
|
|||||||
## Das Programm
|
## Das Programm
|
||||||
|
|
||||||
#### Aufbau
|
#### Aufbau
|
||||||
|
|
||||||
- Das Programm ist aufgebaut in eine Client-Seite und eine Server-Seite
|
- Das Programm ist aufgebaut in eine Client-Seite und eine Server-Seite
|
||||||
- Diese sind zur Sicherheit des Geräts voneinander getrennt
|
- Diese sind zur Sicherheit des Geräts voneinander getrennt
|
||||||
- Server und Client kommunizieren über einen WebSocket, welchen man sich als eine Art Chat-Kanal vorstellen kann
|
- Server und Client kommunizieren über einen WebSocket, welchen man sich als eine Art Chat-Kanal vorstellen kann
|
||||||
- Client und Server haben bestimmte Status, ein Status ist beispielsweise READY oder FILLING
|
- Client und Server haben bestimmte Status, ein Status ist beispielsweise READY oder FILLING
|
||||||
- Die Oberfläche ist sowohl über das Display, aber auch über ein Tablet steuerbar
|
- Die Oberfläche ist sowohl über das Display, aber auch über ein Tablet steuerbar
|
||||||
- Oberfläche sendet Befehle an den Server → Server verarbeitet und gibt ggfs. eine Antwort
|
- Oberfläche sendet Befehle an den Server → Server verarbeitet und gibt ggf. eine Antwort
|
||||||
|
- Bei jedem start wird der Status des Netzwerkes erfasst
|
||||||
|
- Sollte Netzwerk zu verfügung stehen, versucht iTender die Getränke vom Hauptserver (im Internet) zu aktualisieren
|
||||||
|
- Somit kommen auch beim bestehenden Produkt immer neue Getränke und mögliche Zutaten dazu
|
||||||
|
- Nach jedem mischen und alle 5 minuten werden die Füllstände der Behälter erfasst
|
||||||
|
- Danach werden auch die verfügbaren Cocktails berechnet, diese werden dann im Webinterface angezeigt
|
||||||
|
|
||||||
|
#### Code fakten
|
||||||
|
|
||||||
|
- Inzwischen hat der Programmiercode 14492 Zeilen
|
||||||
|
- In Stunden wurde das Projekt (Stand 29.11) ~80 Stunden programmiert
|
||||||
|
|
||||||
#### Fotos des Webinterfaces (Stand 21.11)
|
#### Fotos des Webinterfaces (Stand 21.11)
|
||||||
|
|
||||||
@ -48,27 +64,55 @@ Was haben wir bereits am iTender Projekt gemacht?
|
|||||||
<br>
|
<br>
|
||||||
Die Main Pane ist der Hauptteil und direkt die Einstiegsseite des iTenders<br>
|
Die Main Pane ist der Hauptteil und direkt die Einstiegsseite des iTenders<br>
|
||||||
Hier können Getränke ausgewählt werden, welche dann "gemacht" werden
|
Hier können Getränke ausgewählt werden, welche dann "gemacht" werden
|
||||||
<img src="./v1Main.png">
|
|
||||||
|
<img src="./v1Main.png" width="50%">
|
||||||
<br><br>
|
<br><br>
|
||||||
<strong>Menu</strong><br>
|
<strong>Menu</strong><br>
|
||||||
Das Menü ist das Navigationsherz, von hier aus können alle anderen Panels erreicht werden<br>
|
Das Menü ist das Navigationsherz, von hier aus können alle anderen Panels erreicht werden<br>
|
||||||
<img src="./v1Menu.png">
|
<img src="./v1Menu.png" width="50%">
|
||||||
<br><br>
|
<br><br>
|
||||||
<strong>Containers</strong><br>
|
<strong>Containers</strong><br>
|
||||||
Hier können die Behälter inhalte aktualisiert werden<br>
|
Hier können die Behälter inhalte aktualisiert werden<br>
|
||||||
Man wählt die "Zutat" aus und danach wie voll der Behälter nun ist<br>
|
Man wählt die "Zutat" aus und danach wie voll der Behälter nun ist<br>
|
||||||
In der Regel kann das auch automatisch eingemessen werden, wenn alle Sensoren eingestellt sind<br>
|
In der Regel kann das auch automatisch eingemessen werden, wenn alle Sensoren eingestellt sind<br>
|
||||||
<br>Trotzdem sollte das hier eingestellt werden
|
|
||||||
<img src="./v1Containers.png">
|
<img src="./v1Containers.png" width="50%">
|
||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
<strong>Fill</strong><br>
|
<strong>Fill</strong><br>
|
||||||
Einfach ein "Popup" welches anzeigt dass das Getränk gefüllt wird
|
Einfach ein "Popup" welches anzeigt dass das Getränk gefüllt wird
|
||||||
<img src="./v1Fill.png">
|
|
||||||
|
<img src="./v1Fill.png" width="50%">
|
||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
<strong>Setup</strong><br>
|
<strong>Setup</strong><br>
|
||||||
Das Setup ist das erste menü was nach dem ersten einrichten erscheint<br>
|
Das Setup ist das erste menü was nach dem ersten einrichten erscheint<br>
|
||||||
es dient zur Grundkonfiguration
|
es dient zur Grundkonfiguration
|
||||||
<img src="./v1Setup.png">
|
|
||||||
|
<img src="./v1Setup.png" width="50%">
|
||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
### Erklärung der Dateien und Modulen
|
||||||
|
|
||||||
|
| Context | Datei | Beschreibung |
|
||||||
|
|-----------|---------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| iTender | main | Die Hauptdatei, sie ist der Einstiegspunkt des Programms, von hier aus wird die Datenbank verbunden und der Webserver gestartet |
|
||||||
|
| iTender | MyGPIO | Die eigene GPIO Library von Tobias Hopp. Sie dient zum Steuern von GPIO-Pins am Raspberry Pi |
|
||||||
|
| iTender | Utils | Hier befinden sich einige schnelle Funktionen, welche des öfteren von mehreren Methoden im Programm genutzt werden |
|
||||||
|
| iTender | RejectReason, SensorType, RequestType | Ein paar Klassen welche Enums (sogenannte feste Platzhalter für Variablen) bereitstellen |
|
||||||
|
| iTender | SensorType | Die Art des Sensors, aktuell sind Ultraschallsensoren und Wäge sensoren unterstützt |
|
||||||
|
| Webserver | App | Die App ist die Instanz des Webservers, welcher für die Oberfläche genutzt wird, dieser wird vom iTender selbst, sowie etwaigen anderen Geräten aufgerufen |
|
||||||
|
| Webserver | WebsocketApp | Ähnlich wie die normale App, nur spezifisch für die Direktverbindung zwischen Oberfläche und Server |
|
||||||
|
| iTender | LEDHandler | Übernimmt die Steuerung der WS2812b LEDs |
|
||||||
|
| iTender | Category | Kategorie des Getränks |
|
||||||
|
| iTender | Settings | Die Einstellungen des iTenders, werden gespeichert in der config.json |
|
||||||
|
| Webseite | error.pug, index.pug, layout.pug | Die statische Webseite für den iTender, diese Oberfläche liest die JavaScript Dateien ein und verbindet sich dann mit dem WebSocket (mit dem iTender) |
|
||||||
|
| Compiler | dist/ Ordner | Hier sind alle kompilierten Dateien zu finden |
|
||||||
|
| Webserver | web/main | Der Einstiegspunkt für die Weboberfläche, von hier aus wird mit dem WebSocket verbunden |
|
||||||
|
| | | |
|
||||||
|
@ -48,6 +48,32 @@ body {
|
|||||||
user-select: none; /* Standard syntax */
|
user-select: none; /* Standard syntax */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#blockPanel {
|
||||||
|
z-index: 999;
|
||||||
|
position: fixed;
|
||||||
|
top:0;
|
||||||
|
left:0;
|
||||||
|
background-color: rgba(0,0,0,0.9);
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
;
|
||||||
|
transition: opacity 0.4s;;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacityOutDisplayNone {
|
||||||
|
animation: opacityOutDisplayNone 0.4s linear forwards;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes opacityOutDisplayNone {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 1.74em;
|
font-size: 1.74em;
|
||||||
|
@ -42,6 +42,8 @@ export class Utils {
|
|||||||
reject(new Error(`Request Failed With a Status Code: ${res.statusCode}`));
|
reject(new Error(`Request Failed With a Status Code: ${res.statusCode}`));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}).on("error", (e) => {
|
||||||
|
reject(new Error("Request failed " + e))
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,6 @@ export enum WebSocketEvent {
|
|||||||
SETUP = "SETUP",
|
SETUP = "SETUP",
|
||||||
REQUEST = "REQUEST",
|
REQUEST = "REQUEST",
|
||||||
RESPONSE = "RESPONSE",
|
RESPONSE = "RESPONSE",
|
||||||
FILL = "FILL",
|
CANCEL = "CANCEL",
|
||||||
CANCEL = "CANCEL"
|
ERROR = "ERROR",
|
||||||
}
|
}
|
@ -17,7 +17,6 @@ import {IIngredient} from "./database/IIngredient";
|
|||||||
import Ingredient from "./database/Ingredient";
|
import Ingredient from "./database/Ingredient";
|
||||||
import {clearInterval} from "timers";
|
import {clearInterval} from "timers";
|
||||||
import {RejectReason} from "./RejectReason";
|
import {RejectReason} from "./RejectReason";
|
||||||
import {Settings} from "./Settings";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import GPIO from "rpi-gpio";
|
import GPIO from "rpi-gpio";
|
||||||
import {MyGPIO} from "./MyGPIO";
|
import {MyGPIO} from "./MyGPIO";
|
||||||
@ -27,6 +26,10 @@ const isPI = require("detect-rpi");
|
|||||||
const log = debug("itender:station");
|
const log = debug("itender:station");
|
||||||
const mixLog = debug("itender:mix");
|
const mixLog = debug("itender:mix");
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main class of the itender, here a located all main features of the system, like starting pumps, firing events and stuff
|
||||||
|
*/
|
||||||
export class iTender {
|
export class iTender {
|
||||||
private static secondsPer100ml: number = 35.3335;
|
private static secondsPer100ml: number = 35.3335;
|
||||||
|
|
||||||
@ -34,6 +37,9 @@ export class iTender {
|
|||||||
return this._drinks;
|
return this._drinks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current job of the itender
|
||||||
|
*/
|
||||||
static get currentJob(): IJob | null {
|
static get currentJob(): IJob | null {
|
||||||
return this._currentJob;
|
return this._currentJob;
|
||||||
}
|
}
|
||||||
@ -43,6 +49,9 @@ export class iTender {
|
|||||||
private static _jobCheckInterval: NodeJS.Timer;
|
private static _jobCheckInterval: NodeJS.Timer;
|
||||||
private static _internetConnection: boolean = false;
|
private static _internetConnection: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if internet connection is active
|
||||||
|
*/
|
||||||
static get internetConnection(): boolean {
|
static get internetConnection(): boolean {
|
||||||
return this._internetConnection;
|
return this._internetConnection;
|
||||||
}
|
}
|
||||||
@ -60,6 +69,11 @@ export class iTender {
|
|||||||
return this._status;
|
return this._status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is fired if the user likes to mix a drink
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
static onReceiveFill(data: { drink: IDrink, amounts?: { ingredient: String, amount: number }[], amount?: number }): Promise<IJob> {
|
static onReceiveFill(data: { drink: IDrink, amounts?: { ingredient: String, amount: number }[], amount?: number }): Promise<IJob> {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
mixLog("Receiving fill");
|
mixLog("Receiving fill");
|
||||||
@ -71,6 +85,7 @@ export class iTender {
|
|||||||
|
|
||||||
const job = new Job();
|
const job = new Job();
|
||||||
|
|
||||||
|
|
||||||
let amounts: { ingredient: IIngredient, amount: number, container?: IContainer }[] = [];
|
let amounts: { ingredient: IIngredient, amount: number, container?: IContainer }[] = [];
|
||||||
job.completeAmount = 0;
|
job.completeAmount = 0;
|
||||||
if (data.amounts) {
|
if (data.amounts) {
|
||||||
@ -121,8 +136,7 @@ export class iTender {
|
|||||||
console.log(amounts);
|
console.log(amounts);
|
||||||
job.drink = drink
|
job.drink = drink
|
||||||
job.amounts = amounts as { ingredient: IIngredient, amount: number, container: IContainer }[];
|
job.amounts = amounts as { ingredient: IIngredient, amount: number, container: IContainer }[];
|
||||||
if( job.estimatedTime < 0.5 )
|
if (job.estimatedTime < 0.5) {
|
||||||
{
|
|
||||||
job.estimatedTime = 1;
|
job.estimatedTime = 1;
|
||||||
}
|
}
|
||||||
await job.save()
|
await job.save()
|
||||||
@ -134,6 +148,11 @@ export class iTender {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the internal fill method
|
||||||
|
* @param job
|
||||||
|
*/
|
||||||
static async startFill(job: IJob) {
|
static async startFill(job: IJob) {
|
||||||
job.startedAt = new Date();
|
job.startedAt = new Date();
|
||||||
await job.populate([{path: "amounts.ingredient"}, {path: "amounts.container"}, {path: "drink"}]);
|
await job.populate([{path: "amounts.ingredient"}, {path: "amounts.container"}, {path: "drink"}]);
|
||||||
@ -209,6 +228,10 @@ export class iTender {
|
|||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel the fill
|
||||||
|
*/
|
||||||
static async cancelFill() {
|
static async cancelFill() {
|
||||||
if (!this._currentJob || this.status != iTenderStatus.FILLING)
|
if (!this._currentJob || this.status != iTenderStatus.FILLING)
|
||||||
return;
|
return;
|
||||||
@ -231,6 +254,10 @@ export class iTender {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measure all containers based on their sensor values
|
||||||
|
*/
|
||||||
static measureContainers(): Promise<void> {
|
static measureContainers(): Promise<void> {
|
||||||
log("Measuring containers...");
|
log("Measuring containers...");
|
||||||
|
|
||||||
@ -269,6 +296,11 @@ export class iTender {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the drinks to the local variable
|
||||||
|
* Check which drinks can be done, based on the current container ingredients
|
||||||
|
*/
|
||||||
static refreshDrinks(): Promise<void> {
|
static refreshDrinks(): Promise<void> {
|
||||||
log("Refreshing drinks...");
|
log("Refreshing drinks...");
|
||||||
return new Promise(async resolve => {
|
return new Promise(async resolve => {
|
||||||
@ -326,6 +358,10 @@ export class iTender {
|
|||||||
try {
|
try {
|
||||||
const requestIngredients = await axios.get("https://itender.iif.li/api/ingredients");
|
const requestIngredients = await axios.get("https://itender.iif.li/api/ingredients");
|
||||||
let serverIngredients = requestIngredients.data as IIngredient[];
|
let serverIngredients = requestIngredients.data as IIngredient[];
|
||||||
|
if (serverIngredients.length == 0) {
|
||||||
|
log("Got 0 ingredients from the server... aborting.");
|
||||||
|
throw new Error("Got 0 ingredients from the server, invalid");
|
||||||
|
}
|
||||||
log("Got " + serverIngredients.length + " ingredients from server");
|
log("Got " + serverIngredients.length + " ingredients from server");
|
||||||
|
|
||||||
let localIngredients = await Ingredient.find();
|
let localIngredients = await Ingredient.find();
|
||||||
@ -362,6 +398,10 @@ export class iTender {
|
|||||||
|
|
||||||
const requestDrinks = await axios.get("https://itender.iif.li/api/drinks");
|
const requestDrinks = await axios.get("https://itender.iif.li/api/drinks");
|
||||||
let serverDrinks = requestDrinks.data as IDrink[];
|
let serverDrinks = requestDrinks.data as IDrink[];
|
||||||
|
if (serverDrinks.length == 0) {
|
||||||
|
log("Got 0 drinks from the server... aborting.");
|
||||||
|
throw new Error("Got 0 drinks from the server, invalid");
|
||||||
|
}
|
||||||
log("Got " + serverDrinks.length + " drinks from server");
|
log("Got " + serverDrinks.length + " drinks from server");
|
||||||
|
|
||||||
|
|
||||||
@ -405,16 +445,16 @@ export class iTender {
|
|||||||
log("Drink " + remote.name + " failed to download thumbnail! (" + url + ") | " + e);
|
log("Drink " + remote.name + " failed to download thumbnail! (" + url + ") | " + e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error("Could not refresh drinks " + e);
|
||||||
|
await WebSocketHandler.send(new WebSocketPayload(WebSocketEvent.ERROR, false, "Beim aktualisieren der Getränke ist ein Netzwerk-Fehler aufgetreten.<br>Bitte später erneut versuchen!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
iTender.setStatus(iTenderStatus.READY);
|
iTender.setStatus(iTenderStatus.READY);
|
||||||
resolve();
|
resolve();
|
||||||
iTender.refreshDrinks();
|
await iTender.refreshDrinks();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,10 @@ import {WebSocketPayload} from "../WebSocketPayload";
|
|||||||
import {WebSocketEvent} from "../WebSocketEvent";
|
import {WebSocketEvent} from "../WebSocketEvent";
|
||||||
|
|
||||||
export class Containers {
|
export class Containers {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the menu for the container ingredient setup
|
||||||
|
*/
|
||||||
static openMenu() {
|
static openMenu() {
|
||||||
let modal = new Modal("containers", "Behälter aktualisieren");
|
let modal = new Modal("containers", "Behälter aktualisieren");
|
||||||
let txt = document.createElement("p");
|
let txt = document.createElement("p");
|
||||||
@ -24,6 +28,7 @@ export class Containers {
|
|||||||
btnSave.disabled = true;
|
btnSave.disabled = true;
|
||||||
|
|
||||||
let containerVolumes: Record<any, number> = {};
|
let containerVolumes: Record<any, number> = {};
|
||||||
|
let containers: Record<string, IContainer> = {};
|
||||||
|
|
||||||
let volume = document.createElement("span");
|
let volume = document.createElement("span");
|
||||||
volume.innerText = "";
|
volume.innerText = "";
|
||||||
@ -35,6 +40,7 @@ export class Containers {
|
|||||||
volumeSlider.style.visibility = "hidden";
|
volumeSlider.style.visibility = "hidden";
|
||||||
volumeSlider.id = "containers_volumeSlider"
|
volumeSlider.id = "containers_volumeSlider"
|
||||||
|
|
||||||
|
// When volume slider is changed
|
||||||
function onChange() {
|
function onChange() {
|
||||||
volume.innerText = volumeSlider.value + " ml ";
|
volume.innerText = volumeSlider.value + " ml ";
|
||||||
txt.innerText = "Speichern zum abschließen"
|
txt.innerText = "Speichern zum abschließen"
|
||||||
@ -52,6 +58,8 @@ export class Containers {
|
|||||||
let selectIngredient = document.createElement("select");
|
let selectIngredient = document.createElement("select");
|
||||||
selectIngredient.style.visibility = "hidden";
|
selectIngredient.style.visibility = "hidden";
|
||||||
selectIngredient.classList.add("input");
|
selectIngredient.classList.add("input");
|
||||||
|
|
||||||
|
// When ingredient is changed
|
||||||
selectIngredient.onchange = () => {
|
selectIngredient.onchange = () => {
|
||||||
if (selectIngredient.value == "null") {
|
if (selectIngredient.value == "null") {
|
||||||
volumeSlider.value = "0";
|
volumeSlider.value = "0";
|
||||||
@ -78,27 +86,40 @@ export class Containers {
|
|||||||
|
|
||||||
let selectContainer = document.createElement("select");
|
let selectContainer = document.createElement("select");
|
||||||
selectContainer.classList.add("input");
|
selectContainer.classList.add("input");
|
||||||
|
//let containers : IContainer[] = [];
|
||||||
selectContainer.onchange = () => {
|
selectContainer.onchange = () => {
|
||||||
|
// Enable select ingredient field and set max and min to the slider
|
||||||
selectIngredient.style.visibility = "visible";
|
selectIngredient.style.visibility = "visible";
|
||||||
volumeSlider.max = String(containerVolumes[selectContainer.value]);
|
volumeSlider.max = String(containerVolumes[selectContainer.value]);
|
||||||
volumeSlider.min = String(0);
|
volumeSlider.min = String(0);
|
||||||
volumeSlider.value = String(containerVolumes[selectContainer.value] / 2);
|
volumeSlider.value = String(containerVolumes[selectContainer.value] / 2);
|
||||||
txt.innerText = "Ingredient des Behälters auswählen";
|
txt.innerText = "Ingredient des Behälters auswählen";
|
||||||
|
|
||||||
|
// When content of container is filled, preselect the ingredient selector
|
||||||
|
if (containers[selectContainer.value].content) {
|
||||||
|
selectIngredient.value = containers[selectContainer.value].content?._id;
|
||||||
|
let event = new Event('change', {bubbles: true});
|
||||||
|
selectIngredient.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
selectContainer.append(nonSelect.cloneNode(true));
|
selectContainer.append(nonSelect.cloneNode(true));
|
||||||
selectContainer.selectedIndex = 0;
|
selectContainer.selectedIndex = 0;
|
||||||
|
|
||||||
|
|
||||||
WebWebSocketHandler.request(RequestType.CONTAINERS).then((payload) => {
|
WebWebSocketHandler.request(RequestType.CONTAINERS).then((payload) => {
|
||||||
for (let container of (payload.data["content"] as IContainer[])) {
|
for (let container of (payload.data["content"] as IContainer[])) {
|
||||||
containerVolumes[container._id] = container.volume;
|
containerVolumes[container._id] = container.volume;
|
||||||
let option = document.createElement("option");
|
let option = document.createElement("option");
|
||||||
option.value = container._id;
|
option.value = container._id;
|
||||||
option.innerText = "Behälter Slot " + container.slot + "[" + (container.content && container.content.name ? container.content.name : "Kein Inhalt") + "]";
|
option.innerText = "Behälter Slot " + (container.slot+1) + "[" + (container.content && container.content.name ? container.content.name : "Kein Inhalt") + "]";
|
||||||
selectContainer.append(option);
|
selectContainer.append(option);
|
||||||
|
containers[container._id] = container;
|
||||||
}
|
}
|
||||||
|
//containers = payload.data["content"] as IContainer[];
|
||||||
});
|
});
|
||||||
WebWebSocketHandler.request(RequestType.INGREDIENTS).then((payload) => {
|
WebWebSocketHandler.request(RequestType.INGREDIENTS).then((payload) => {
|
||||||
for (let ingredient of (payload.data["content"] as IIngredient[])) {
|
for (let ingredient of (payload.data["content"] as IIngredient[])) {
|
||||||
@ -134,7 +155,14 @@ export class Containers {
|
|||||||
ingredient: (selectIngredient.value == "null") ? null : selectIngredient.value,
|
ingredient: (selectIngredient.value == "null") ? null : selectIngredient.value,
|
||||||
filled: volumeSlider.value
|
filled: volumeSlider.value
|
||||||
});
|
});
|
||||||
WebWebSocketHandler.send(payload).then(() => modal.close());
|
|
||||||
|
WebWebSocketHandler.send(payload).then(() => {
|
||||||
|
selectContainer.value = "-1";
|
||||||
|
selectIngredient.value = "-1";
|
||||||
|
let event = new Event('change', {bubbles: true});
|
||||||
|
selectContainer.dispatchEvent(event);
|
||||||
|
selectIngredient.dispatchEvent(event);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
modal.open();
|
modal.open();
|
||||||
|
101
src/web/Fill.ts
Normal file
101
src/web/Fill.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import {WebSocketPayload} from "../WebSocketPayload";
|
||||||
|
import {Modal} from "./Modal";
|
||||||
|
import {WebSocketEvent} from "../WebSocketEvent";
|
||||||
|
import {RequestType} from "../RequestType";
|
||||||
|
import {IJob} from "../database/IJob";
|
||||||
|
import {WebWebSocketHandler} from "./WebWebSocketHandler";
|
||||||
|
|
||||||
|
export class Fill {
|
||||||
|
static onFillEvent(payload: WebSocketPayload) {
|
||||||
|
let modal = new Modal("fill", "Cocktail wird zubereitet");
|
||||||
|
let header = document.createElement("h2");
|
||||||
|
header.innerText = "";
|
||||||
|
|
||||||
|
modal.addContent(header);
|
||||||
|
|
||||||
|
let txt = document.createElement("p");
|
||||||
|
txt.innerHTML = `Der Cocktail wird gerade zubereitet`;
|
||||||
|
txt.id = "main_fillTxt";
|
||||||
|
|
||||||
|
let waterAnimDiv = document.createElement("div");
|
||||||
|
waterAnimDiv.classList.add("water");
|
||||||
|
modal.addContent(txt);
|
||||||
|
|
||||||
|
modal.addContent(waterAnimDiv);
|
||||||
|
|
||||||
|
let seconds = document.createElement("span");
|
||||||
|
seconds.innerText = "60s";
|
||||||
|
seconds.style.marginRight = "3%";
|
||||||
|
|
||||||
|
modal.addContent(seconds);
|
||||||
|
|
||||||
|
let ml = document.createElement("span");
|
||||||
|
ml.innerText = "200ml";
|
||||||
|
modal.addContent(ml);
|
||||||
|
|
||||||
|
modal.addContent(document.createElement("br"));
|
||||||
|
modal.addContent(document.createElement("br"));
|
||||||
|
|
||||||
|
|
||||||
|
let cancelBtn = document.createElement("button");
|
||||||
|
cancelBtn.classList.add("btn", "btn-danger");
|
||||||
|
cancelBtn.innerText = "Abbrechen";
|
||||||
|
cancelBtn.disabled = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
cancelBtn.disabled = false;
|
||||||
|
}, 1000);
|
||||||
|
cancelBtn.onclick = () => {
|
||||||
|
cancelBtn.disabled = true;
|
||||||
|
txt.innerHTML = "Der Vorgang wird abgebrochen...";
|
||||||
|
waterAnimDiv.classList.add("waterCancel");
|
||||||
|
|
||||||
|
WebWebSocketHandler.send(new WebSocketPayload(WebSocketEvent.CANCEL));
|
||||||
|
};
|
||||||
|
modal.addContent(cancelBtn);
|
||||||
|
|
||||||
|
function riseSlowlyUp(lastNumber: number, number: number) {
|
||||||
|
for (let i = lastNumber; i < number; i++) {
|
||||||
|
setTimeout(() => {
|
||||||
|
ml.innerText = Math.floor(i) + "ml";
|
||||||
|
}, (number - lastNumber / 1000) + i * 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.open().then(() => {
|
||||||
|
WebWebSocketHandler.request(RequestType.JOB).then((payload) => {
|
||||||
|
let minus = 0;
|
||||||
|
let job = payload.data.content as IJob;
|
||||||
|
ml.innerText = Math.floor((job.completeAmount / job.estimatedTime) * minus) + "ml";
|
||||||
|
waterAnimDiv.style.setProperty("--fillTime", job.estimatedTime + "s");
|
||||||
|
waterAnimDiv.style.backgroundImage = `url("/images/${job.drink._id}.png")`;
|
||||||
|
header.innerText = job.drink.name;
|
||||||
|
seconds.innerText = Math.floor(job.estimatedTime) + "s";
|
||||||
|
|
||||||
|
let last = 0;
|
||||||
|
let interval = setInterval(() => {
|
||||||
|
minus++;
|
||||||
|
if (minus + 1 > (job.estimatedTime as number)) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
seconds.innerText = (Math.floor(job.estimatedTime as number - minus)) + "s";
|
||||||
|
let calc = Math.floor((job.completeAmount / job.estimatedTime) * minus);
|
||||||
|
riseSlowlyUp(last, calc)
|
||||||
|
last = calc;
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
txt.innerHTML = "Bitte entnehme den Cocktail";
|
||||||
|
modal.title.innerHTML = "Cocktail fertig gestellt"
|
||||||
|
|
||||||
|
cancelBtn.classList.add("btn-blendout");
|
||||||
|
waterAnimDiv.classList.add("waterFinished");
|
||||||
|
cancelBtn.onclick = () => {
|
||||||
|
modal.close();
|
||||||
|
}
|
||||||
|
}, job.estimatedTime * 1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,13 @@
|
|||||||
import {ButtonType} from "./ButtonType";
|
import {ButtonType} from "./ButtonType";
|
||||||
|
|
||||||
export class Modal {
|
export class Modal {
|
||||||
|
get title(): HTMLHeadingElement {
|
||||||
|
return this._title;
|
||||||
|
}
|
||||||
|
|
||||||
private static currentModalId: string | undefined = "";
|
private static currentModalId: string | undefined = "";
|
||||||
|
|
||||||
private _title: string = "iTender";
|
private _title: HTMLHeadingElement;
|
||||||
private _id: string = "";
|
private _id: string = "";
|
||||||
private _loader: boolean = false;
|
private _loader: boolean = false;
|
||||||
private _buttons: { type: string, content: string, onclick: Function }[] = [];
|
private _buttons: { type: string, content: string, onclick: Function }[] = [];
|
||||||
@ -19,11 +22,10 @@ export class Modal {
|
|||||||
|
|
||||||
constructor(id, title: string) {
|
constructor(id, title: string) {
|
||||||
this._id = id;
|
this._id = id;
|
||||||
this._title = title;
|
|
||||||
|
|
||||||
let t = document.createElement("h1");
|
this._title = document.createElement("h1") as HTMLHeadingElement;
|
||||||
t.innerText = title;
|
this._title.innerText = title;
|
||||||
this._elements.push(t);
|
this._elements.push(this._title);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static isModalOpen(): boolean {
|
public static isModalOpen(): boolean {
|
||||||
@ -91,6 +93,10 @@ export class Modal {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setTitle(title) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param elements
|
* @param elements
|
||||||
* @param id
|
* @param id
|
||||||
|
@ -7,7 +7,7 @@ import {WebHandler} from "./WebHandler";
|
|||||||
import {Setup} from "./Setup";
|
import {Setup} from "./Setup";
|
||||||
import {Pane} from "./Pane";
|
import {Pane} from "./Pane";
|
||||||
import {RequestType} from "../RequestType";
|
import {RequestType} from "../RequestType";
|
||||||
import {IJob} from "../database/IJob";
|
import {Fill} from "./Fill";
|
||||||
|
|
||||||
export class WebWebSocketHandler {
|
export class WebWebSocketHandler {
|
||||||
private static socket: WebSocket;
|
private static socket: WebSocket;
|
||||||
@ -29,8 +29,7 @@ export class WebWebSocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static registerForEvent(event: WebSocketEvent, fn: (payload: WebSocketPayload) => void) {
|
public static registerForEvent(event: WebSocketEvent, fn: (payload: WebSocketPayload) => void) {
|
||||||
for( let e of WebWebSocketHandler.eventRegister )
|
for (let e of WebWebSocketHandler.eventRegister) {
|
||||||
{
|
|
||||||
if (e.fn == fn) {
|
if (e.fn == fn) {
|
||||||
console.log("Event fn already registered");
|
console.log("Event fn already registered");
|
||||||
return;
|
return;
|
||||||
@ -55,11 +54,27 @@ export class WebWebSocketHandler {
|
|||||||
|
|
||||||
switch (payload.event) {
|
switch (payload.event) {
|
||||||
case WebSocketEvent.CONFIG: {
|
case WebSocketEvent.CONFIG: {
|
||||||
// Incoming WebSocketStatus
|
|
||||||
Setup.onConfigUpdate(payload);
|
Setup.onConfigUpdate(payload);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case WebSocketEvent.DRINKS: {
|
||||||
|
WebHandler.onDrinkUpdate(payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case WebSocketEvent.ERROR: {
|
||||||
|
let modal = new Modal("error", "Aww crap!");
|
||||||
|
let txt = document.createElement("p");
|
||||||
|
txt.innerHTML = payload.data;
|
||||||
|
modal.addContent(txt);
|
||||||
|
modal.addContent(document.createElement("br"));
|
||||||
|
modal.addButton(ButtonType.PRIMARY, "Schließen", () => modal.close() );
|
||||||
|
modal.open();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Incoming WebSocketStatus
|
||||||
case WebSocketEvent.STATUS: {
|
case WebSocketEvent.STATUS: {
|
||||||
let statusElement = document.getElementById("status");
|
let statusElement = document.getElementById("status");
|
||||||
if (statusElement)
|
if (statusElement)
|
||||||
@ -90,19 +105,16 @@ export class WebWebSocketHandler {
|
|||||||
case iTenderStatus.DOWNLOADING: {
|
case iTenderStatus.DOWNLOADING: {
|
||||||
let modal = new Modal("download", "Aktualisieren");
|
let modal = new Modal("download", "Aktualisieren");
|
||||||
let txt = document.createElement("p");
|
let txt = document.createElement("p");
|
||||||
txt.innerHTML = `Einen Augenblick bitte<br>iTender aktualisiert die Getränke vom Server.`;
|
txt.innerHTML = `Einen Augenblick bitte<br>iTender aktualisiert die Getränke vom Server...`;
|
||||||
modal.addContent(txt);
|
modal.addContent(txt);
|
||||||
modal.loader = true;
|
modal.loader = true;
|
||||||
modal.open();
|
modal.open();
|
||||||
break;
|
setTimeout( () => {
|
||||||
|
if( txt )
|
||||||
|
{
|
||||||
|
txt.innerHTML = txt.innerHTML + "<br><br>Der Vorgang dauert länger als gewöhnlich.<br>Überprüfe deine Internetverbindung!"
|
||||||
}
|
}
|
||||||
case iTenderStatus.REFRESHING: {
|
}, 1000 * 15 )
|
||||||
/* let modal = new Modal("refreshing", "Aktualisieren...");
|
|
||||||
let txt = document.createElement("p");
|
|
||||||
txt.innerHTML = `Einen Augenblick bitte<br>iTender aktualisiert die Getränke...`;
|
|
||||||
modal.addContent(txt);
|
|
||||||
modal.loader = true;
|
|
||||||
modal.open();*/
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case iTenderStatus.SETUP: {
|
case iTenderStatus.SETUP: {
|
||||||
@ -111,111 +123,16 @@ export class WebWebSocketHandler {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case iTenderStatus.FILLING: {
|
case iTenderStatus.FILLING: {
|
||||||
|
Fill.onFillEvent(payload);
|
||||||
let modal = new Modal("fill", "Getränk wird ausgegeben");
|
|
||||||
let header = document.createElement("h2");
|
|
||||||
header.innerText = "";
|
|
||||||
|
|
||||||
modal.addContent(header);
|
|
||||||
|
|
||||||
let txt = document.createElement("p");
|
|
||||||
txt.innerHTML = `Dein Cocktail wird gerade zubereitet`;
|
|
||||||
txt.id = "main_fillTxt";
|
|
||||||
|
|
||||||
let waterAnimDiv = document.createElement("div");
|
|
||||||
waterAnimDiv.classList.add("water");
|
|
||||||
modal.addContent(txt);
|
|
||||||
|
|
||||||
modal.addContent(waterAnimDiv);
|
|
||||||
|
|
||||||
let seconds = document.createElement("span");
|
|
||||||
seconds.innerText = "60s";
|
|
||||||
seconds.style.marginRight = "3%";
|
|
||||||
|
|
||||||
modal.addContent(seconds);
|
|
||||||
|
|
||||||
let ml = document.createElement("span");
|
|
||||||
ml.innerText = "200ml";
|
|
||||||
modal.addContent(ml);
|
|
||||||
|
|
||||||
modal.addContent(document.createElement("br"));
|
|
||||||
modal.addContent(document.createElement("br"));
|
|
||||||
|
|
||||||
|
|
||||||
let cancelBtn = document.createElement("button");
|
|
||||||
cancelBtn.classList.add("btn", "btn-danger");
|
|
||||||
cancelBtn.innerText = "Abbrechen";
|
|
||||||
cancelBtn.disabled = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
cancelBtn.disabled = false;
|
|
||||||
}, 1000);
|
|
||||||
cancelBtn.onclick = () => {
|
|
||||||
cancelBtn.disabled = true;
|
|
||||||
txt.innerHTML = "Der Vorgang wird abgebrochen...";
|
|
||||||
waterAnimDiv.classList.add("waterCancel");
|
|
||||||
|
|
||||||
WebWebSocketHandler.send(new WebSocketPayload(WebSocketEvent.CANCEL));
|
|
||||||
};
|
|
||||||
modal.addContent(cancelBtn);
|
|
||||||
|
|
||||||
function riseSlowlyUp(lastNumber:number, number: number) {
|
|
||||||
for (let i = lastNumber; i < number; i++) {
|
|
||||||
setTimeout(() => {
|
|
||||||
ml.innerText = i + "ml";
|
|
||||||
}, (number-lastNumber/1000)+i*4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
modal.open().then(() => {
|
|
||||||
WebWebSocketHandler.request(RequestType.JOB).then((payload) => {
|
|
||||||
let minus = 0;
|
|
||||||
let job = payload.data.content as IJob;
|
|
||||||
ml.innerText = Math.floor((job.completeAmount / job.estimatedTime) * minus) + "ml";
|
|
||||||
waterAnimDiv.style.setProperty("--fillTime", job.estimatedTime + "s");
|
|
||||||
waterAnimDiv.style.backgroundImage = `url("/images/${job.drink._id}.png")`;
|
|
||||||
header.innerText = job.drink.name;
|
|
||||||
seconds.innerText = job.estimatedTime + "s";
|
|
||||||
|
|
||||||
let last = 0;
|
|
||||||
let interval = setInterval(() => {
|
|
||||||
minus++;
|
|
||||||
if (minus + 1 > (job.estimatedTime as number)) {
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
|
|
||||||
seconds.innerText = (Math.floor(job.estimatedTime as number - minus)) + "s";
|
|
||||||
let calc = Math.floor((job.completeAmount / job.estimatedTime) * minus);
|
|
||||||
riseSlowlyUp(last, calc)
|
|
||||||
last = calc;
|
|
||||||
//ml.innerText = + "ml";
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
txt.innerHTML = "Bitte entnehme den Cocktail!";
|
|
||||||
/*cancelBtn.classList.add("btn-primary");
|
|
||||||
cancelBtn.classList.remove("btn-danger");
|
|
||||||
cancelBtn.innerText = "Schließen";*/
|
|
||||||
cancelBtn.classList.add("btn-blendout");
|
|
||||||
waterAnimDiv.classList.add("waterFinished");
|
|
||||||
cancelBtn.onclick = () => {
|
|
||||||
modal.close();
|
|
||||||
}
|
|
||||||
//setTimeout(() => modal.close(), 1000 * 4.5);
|
|
||||||
}, job.estimatedTime * 1000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
default: {
|
||||||
|
console.log("Unknown to handle " + status);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case WebSocketEvent.DRINKS: {
|
|
||||||
WebHandler.onDrinkUpdate(payload);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,12 +140,8 @@ export class WebWebSocketHandler {
|
|||||||
private onOpen(event) {
|
private onOpen(event) {
|
||||||
console.log("[WS] Connected", event);
|
console.log("[WS] Connected", event);
|
||||||
|
|
||||||
/*let connectionElement = document.getElementById("right");
|
const blockPanel = document.getElementById("blockPanel") as HTMLDivElement;
|
||||||
if (connectionElement) {
|
blockPanel.classList.add("opacityOutDisplayNone");
|
||||||
connectionElement.innerText = "Verbunden";
|
|
||||||
connectionElement.style.color = "green";
|
|
||||||
}*/
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onClose(event) {
|
private onClose(event) {
|
||||||
|
@ -7,6 +7,7 @@ html
|
|||||||
link(rel='stylesheet', href='/stylesheets/style.css')
|
link(rel='stylesheet', href='/stylesheets/style.css')
|
||||||
meta(charset="UTF-8")
|
meta(charset="UTF-8")
|
||||||
body
|
body
|
||||||
|
div#blockPanel
|
||||||
div.modal#modal
|
div.modal#modal
|
||||||
div.modal-content#modal-content
|
div.modal-content#modal-content
|
||||||
div.modalInnerContent#modalInnerContent
|
div.modalInnerContent#modalInnerContent
|
||||||
|
Loading…
x
Reference in New Issue
Block a user