diff --git a/Gemfile b/Gemfile index 9947cf12..6925ec7b 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,9 @@ gem "listen" # Sass gem "sassc-rails" +# Stimulus +gem "stimulus-rails" + # Not sure why gem "net-imap", require: false gem "net-pop", require: false diff --git a/Gemfile.lock b/Gemfile.lock index 34900246..02277290 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -444,6 +444,8 @@ GEM sqlite3 (2.9.5-x86_64-linux-gnu) sqlite3 (2.9.5-x86_64-linux-musl) stackprof (0.2.28) + stimulus-rails (1.3.4) + railties (>= 6.0.0) terrapin (1.1.1) climate_control thor (1.5.0) @@ -536,6 +538,7 @@ DEPENDENCIES spring sqlite3 stackprof + stimulus-rails tqdm tzinfo-data vanilla_nested diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 9f9c6f42..08acf2f4 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -92,11 +92,6 @@ body { } } -// Add support for Petite Vue "v-cloak" attribute. -[v-cloak] { - display: none; -} - .landing { &-hero { background-image: asset-url("landing/background.svg"); diff --git a/app/assets/javascript/application.js b/app/javascript/application.js similarity index 60% rename from app/assets/javascript/application.js rename to app/javascript/application.js index c6fe4e90..62685f22 100644 --- a/app/assets/javascript/application.js +++ b/app/javascript/application.js @@ -1,22 +1,17 @@ import "@hotwired/turbo-rails"; import "chartkick/chart.js"; import "vanilla-nested"; -import * as PetiteVue from "petite-vue"; import Quagga from "@ericblade/quagga2"; -import Rails from "@rails/ujs"; - -// Start petite-vue on page and frame load -document.addEventListener("turbo:load", () => PetiteVue.createApp().mount()); -document.addEventListener("turbo:frame-load", () => PetiteVue.createApp().mount()); // Globally mount Quagga window.Quagga = Quagga; -Rails.start(); - // Register a service worker import "./pwa"; +// Register stimulus controllers +import "./controllers"; + // Globally mount some utils import "./utils/filepicker"; import "./utils/modal"; diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js new file mode 100644 index 00000000..1213e85c --- /dev/null +++ b/app/javascript/controllers/application.js @@ -0,0 +1,9 @@ +import { Application } from "@hotwired/stimulus" + +const application = Application.start() + +// Configure Stimulus development experience +application.debug = false +window.Stimulus = application + +export { application } diff --git a/app/javascript/controllers/current_dagschotel_controller.js b/app/javascript/controllers/current_dagschotel_controller.js new file mode 100644 index 00000000..3928f654 --- /dev/null +++ b/app/javascript/controllers/current_dagschotel_controller.js @@ -0,0 +1,16 @@ +import { Controller } from "@hotwired/stimulus" + +// Connects to data-controller="current-dagschotel" +export default class extends Controller { + static targets = [ "button" ] + + startHover() { + this.buttonTarget.innerText = "Remove dagschotel"; + this.buttonTarget.classList.add("is-danger"); + } + + endHover() { + this.buttonTarget.innerText = "Current dagschotel"; + this.buttonTarget.classList.remove("is-danger"); + } +} diff --git a/app/javascript/controllers/flash_controller.js b/app/javascript/controllers/flash_controller.js new file mode 100644 index 00000000..bb956e0a --- /dev/null +++ b/app/javascript/controllers/flash_controller.js @@ -0,0 +1,8 @@ +import { Controller } from "@hotwired/stimulus" + +// Connects to data-controller="flash" +export default class extends Controller { + remove() { + this.element.remove(); + } +} diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js new file mode 100644 index 00000000..386b8ddc --- /dev/null +++ b/app/javascript/controllers/index.js @@ -0,0 +1,29 @@ +// This file is auto-generated by ./bin/rails stimulus:manifest:update +// Run that command whenever you add a new controller or create them with +// ./bin/rails generate stimulus controllerName + +import { application } from "./application" + +import CurrentDagschotelController from "./current_dagschotel_controller" +application.register("current-dagschotel", CurrentDagschotelController) + +import FlashController from "./flash_controller" +application.register("flash", FlashController) + +import NavbarController from "./navbar_controller" +application.register("navbar", NavbarController) + +import OrderController from "./order_controller" +application.register("order", OrderController) + +import ProductsController from "./products_controller" +application.register("products", ProductsController) + +import ScannerController from "./scanner_controller" +application.register("scanner", ScannerController) + +import StockController from "./stock_controller" +application.register("stock", StockController) + +import UserController from "./user_controller" +application.register("user", UserController) diff --git a/app/javascript/controllers/navbar_controller.js b/app/javascript/controllers/navbar_controller.js new file mode 100644 index 00000000..e4bc0e13 --- /dev/null +++ b/app/javascript/controllers/navbar_controller.js @@ -0,0 +1,16 @@ +import { Controller } from "@hotwired/stimulus" + +// Connects to data-controller="navbar" +export default class extends Controller { + static targets = [ "burger", "menu" ] + + initialize() { + this.open = false; + } + + toggleOpen() { + this.open = !this.open; + this.burgerTarget.classList.toggle("is-active", this.open); + this.menuTarget.classList.toggle("is-active", this.open); + } +} diff --git a/app/javascript/controllers/order_controller.js b/app/javascript/controllers/order_controller.js new file mode 100644 index 00000000..389ada2e --- /dev/null +++ b/app/javascript/controllers/order_controller.js @@ -0,0 +1,22 @@ +import { Controller } from "@hotwired/stimulus" + +// Connects to data-controller="order" +export default class extends Controller { + static targets = [ "billOverview", "spinner", "actualForm" ]; + + setLoading() { + this.billOverviewTarget.classList.add("is-hidden"); + this.spinnerTarget.classList.remove("is-hidden"); + } + + submit(event) { + event.currentTarget.disabled = true; + event.currentTarget.value = "Please wait..."; + this.actualFormTarget.submit(); + } + + addItem(event) { + this.setLoading(); + window.closeModal('modalOrderProducts'); + } +} diff --git a/app/javascript/controllers/products_controller.js b/app/javascript/controllers/products_controller.js new file mode 100644 index 00000000..0e12df2f --- /dev/null +++ b/app/javascript/controllers/products_controller.js @@ -0,0 +1,48 @@ +import { Controller } from "@hotwired/stimulus" + +// Connects to data-controller="products" +export default class extends Controller { + static targets = [ "item", "placeholder", "tab" ] + + initialize() { + this.tab = "all"; + this.searchValue = ""; + } + + changeTab(event) { + this.tab = event.params.category; + this.tabTargets.forEach(element => element.closest("li").classList.remove("is-active")); + event.target.closest("li").classList.add("is-active"); + this.updateItems(); + } + + search(event) { + this.searchValue = event.currentTarget.value; + this.updateItems(); + } + + updateItems() { + let anyMatched = false; + this.itemTargets.forEach(element => { + const matches = this.itemMatches(element.dataset.name, element.dataset.category) + anyMatched ||= matches; + element.classList.toggle('is-hidden', !matches); + }); + this.placeholderTarget.classList.toggle('is-hidden', anyMatched); + } + + itemMatches(name, category) { + // If the product doesn't match the category, return false + if (category && this.tab !== "all" && this.tab !== category) { + return false; + } + + // If the product doesn't match the search query, return false + if (this.searchValue && !name.toLowerCase().includes(this.searchValue.toLowerCase())) { + return false; + } + + return true; + } + +} diff --git a/app/javascript/controllers/scanner_controller.js b/app/javascript/controllers/scanner_controller.js new file mode 100644 index 00000000..147b8f3a --- /dev/null +++ b/app/javascript/controllers/scanner_controller.js @@ -0,0 +1,73 @@ +import { Controller } from "@hotwired/stimulus" + +// Connects to data-controller="scanner" +export default class extends Controller { + static targets = [ "laser", "loader", "error", "errorTitle", "errorMessage" ]; + + initialize() { + this.scanner = new BarcodeScanner("#barcodeCanvas"); + this.loading = true; + this.error = null; + } + + connect() { + this.scanner.onDetected = (barcode) => { + // Update the hidden barcode field. + document.getElementById("orderBarcodeScannerFormInput").value = barcode.codeResult.code; + + // Submit the form + const form = document.getElementById("orderBarcodeScannerForm"); + if (form.requestSubmit) { + form.requestSubmit(); + } else { + form.submit(); + } + + // Close modal + window.closeModal("modalOrderScanner"); + } + + this.scanner.onSuccess = () => { + this.loading = false; + this.updateViews(); + } + + this.scanner.onError = (error) => { + this.loading = false; + this.error = {}; + + if (error.name === "NotAllowedError") { + this.error.title = "Camera access denied!"; + this.error.message = "Please allow camera access for this site and reload the webpage."; + } else { + this.error.title = error.name; + this.error.message = error.message; + } + this.updateViews(); + } + this.updateViews(); + } + + init(event) { + if (event.currentTarget === this.element) { + this.scanner.init(); + } + } + + destroy() { + if (event.currentTarget === this.element) { + this.scanner.destroy(); + this.loading = true; + this.error = null; + this.updateViews(); + } + } + + updateViews() { + this.laserTarget.classList.toggle("is-hidden", this.error || this.loading); + this.loaderTarget.classList.toggle("is-hidden", !this.loading); + this.errorTarget.classList.toggle("is-hidden", !this.error); + this.errorTitleTarget.innerText = this.error?.title ?? ""; + this.errorMessageTarget.innerText = this.error?.message ?? ""; + } +} diff --git a/app/javascript/controllers/stock_controller.js b/app/javascript/controllers/stock_controller.js new file mode 100644 index 00000000..6aa7d150 --- /dev/null +++ b/app/javascript/controllers/stock_controller.js @@ -0,0 +1,16 @@ +import { Controller } from "@hotwired/stimulus" + +// Connects to data-controller="stock" +export default class extends Controller { + static targets = [ "totalOutput", "readableTotalOutput" ] + + connect() { + this.currentValue = Number.parseInt(this.element.dataset.currentStock); + } + + setPurchasedStock(event) { + const purchasedValue = Number.parseInt(event.currentTarget.value); + this.totalOutputTarget.value = this.currentValue + purchasedValue; + this.readableTotalOutputTarget.innerText = this.currentValue + purchasedValue; + } +} diff --git a/app/javascript/controllers/user_controller.js b/app/javascript/controllers/user_controller.js new file mode 100644 index 00000000..b3b4b493 --- /dev/null +++ b/app/javascript/controllers/user_controller.js @@ -0,0 +1,23 @@ +import { Controller } from "@hotwired/stimulus" + +// Connects to data-controller="user" +export default class extends Controller { + static targets = [ "dagschotel", "dagschotelLoader", "avatar", "avatarLoader", "userLink" ] + + orderedDagschotel(event) { + this.dagschotelTarget.classList.add("user-dagschotel--loading"); + this.dagschotelTarget.disabled = true; + this.dagschotelLoaderTarget.classList.remove("is-hidden"); + event.currentTarget.closest('form').submit(); + } + + openedUser() { + if (this.hasDagschotelTarget) { + this.dagschotelTarget.classList.add("is-hidden"); + } + this.avatarTarget.classList.add("user-avatar--loading"); + this.avatarLoaderTarget.classList.remove("is-hidden"); + this.userLinkTarget.disabled = true; + } + +} diff --git a/app/assets/javascript/mqtt.js b/app/javascript/mqtt.js similarity index 100% rename from app/assets/javascript/mqtt.js rename to app/javascript/mqtt.js diff --git a/app/assets/javascript/pwa.js b/app/javascript/pwa.js similarity index 100% rename from app/assets/javascript/pwa.js rename to app/javascript/pwa.js diff --git a/app/assets/javascript/utils/filepicker.js b/app/javascript/utils/filepicker.js similarity index 100% rename from app/assets/javascript/utils/filepicker.js rename to app/javascript/utils/filepicker.js diff --git a/app/assets/javascript/utils/modal.js b/app/javascript/utils/modal.js similarity index 100% rename from app/assets/javascript/utils/modal.js rename to app/javascript/utils/modal.js diff --git a/app/assets/javascript/utils/remove.js b/app/javascript/utils/remove.js similarity index 100% rename from app/assets/javascript/utils/remove.js rename to app/javascript/utils/remove.js diff --git a/app/assets/javascript/utils/scanner.js b/app/javascript/utils/scanner.js similarity index 100% rename from app/assets/javascript/utils/scanner.js rename to app/javascript/utils/scanner.js diff --git a/app/assets/javascript/utils/submit.js b/app/javascript/utils/submit.js similarity index 100% rename from app/assets/javascript/utils/submit.js rename to app/javascript/utils/submit.js diff --git a/app/views/layouts/_flash.html.erb b/app/views/layouts/_flash.html.erb index 19757e7d..123b1540 100644 --- a/app/views/layouts/_flash.html.erb +++ b/app/views/layouts/_flash.html.erb @@ -1,8 +1,8 @@ <% flash.each do |type, msg| %> <% if flash_is_notification?(type) %> -