From 05d98b38476875ebc3a99bd696f647109e90e38c Mon Sep 17 00:00:00 2001 From: FrederikBaerentsen Date: Mon, 19 Jan 2026 17:24:57 +0100 Subject: [PATCH] feat(frontend): add socket support and styling for individual items --- static/scripts/socket/minifigure.js | 258 ++++++++++++++++++++++++++++ static/scripts/socket/set.js | 57 ++++++ static/styles.css | 49 ++++++ 3 files changed, 364 insertions(+) create mode 100644 static/scripts/socket/minifigure.js diff --git a/static/scripts/socket/minifigure.js b/static/scripts/socket/minifigure.js new file mode 100644 index 0000000..34a987a --- /dev/null +++ b/static/scripts/socket/minifigure.js @@ -0,0 +1,258 @@ +// Minifigure Socket class +class BrickMinifigureSocket extends BrickSocket { + constructor(id, path, namespace, messages) { + super(id, path, namespace, messages, false); + + // Listeners + this.add_listener = undefined; + this.input_listener = undefined; + this.confirm_listener = undefined; + + // Form elements (built based on the initial id) + this.html_button = document.getElementById(id); + this.html_input = document.getElementById(`${id}-set`); + this.html_no_confim = document.getElementById(`${id}-no-confirm`); + this.html_owners = document.getElementById(`${id}-owners`); + this.html_purchase_location = document.getElementById(`${id}-purchase-location`); + this.html_storage = document.getElementById(`${id}-storage`); + this.html_tags = document.getElementById(`${id}-tags`); + + // Card elements + this.html_card = document.getElementById(`${id}-card`); + this.html_card_set = document.getElementById(`${id}-card-set`); + this.html_card_name = document.getElementById(`${id}-card-name`); + this.html_card_image_container = document.getElementById(`${id}-card-image-container`); + this.html_card_image = document.getElementById(`${id}-card-image`); + this.html_card_footer = document.getElementById(`${id}-card-footer`); + this.html_card_confirm = document.getElementById(`${id}-card-confirm`); + this.html_card_dismiss = document.getElementById(`${id}-card-dismiss`); + + if (this.html_button) { + this.add_listener = this.html_button.addEventListener("click", ((bricksocket) => (e) => { + bricksocket.execute(); + })(this)); + + this.input_listener = this.html_input.addEventListener("keyup", ((bricksocket) => (e) => { + if (e.key === 'Enter') { + bricksocket.execute(); + } + })(this)) + } + + if (this.html_card_dismiss && this.html_card) { + this.html_card_dismiss.addEventListener("click", ((card) => (e) => { + card.classList.add("d-none"); + })(this.html_card)); + } + + // Setup the socket + this.setup(); + } + + // Clear form + clear() { + super.clear(); + + if (this.html_card) { + this.html_card.classList.add("d-none"); + } + + if (this.html_card_footer) { + this.html_card_footer.classList.add("d-none"); + + if (this.html_card_confirm) { + this.html_card_footer.classList.add("d-none"); + } + } + } + + // Execute the action + execute() { + if (!this.disabled && this.socket !== undefined && this.socket.connected) { + this.toggle(false); + + if (this.html_no_confim && this.html_no_confim.checked) { + this.import_minifigure(true); + } else { + this.load_minifigure(); + } + } + } + + // Import a minifigure + import_minifigure(no_confirm, figure) { + if (this.html_input) { + if (no_confirm) { + this.clear(); + } else { + this.clear_status(); + } + + // Grab the owners + const owners = []; + if (this.html_owners) { + this.html_owners.querySelectorAll('input').forEach(input => { + if (input.checked) { + owners.push(input.value); + } + }); + } + + // Grab the purchase location + let purchase_location = null; + if (this.html_purchase_location) { + purchase_location = this.html_purchase_location.value; + } + + // Grab the storage + let storage = null; + if (this.html_storage) { + storage = this.html_storage.value; + } + + // Grab the tags + const tags = []; + if (this.html_tags) { + this.html_tags.querySelectorAll('input').forEach(input => { + if (input.checked) { + tags.push(input.value); + } + }); + } + + this.spinner(true); + + if (this.html_progress_bar) { + this.html_progress_bar.scrollIntoView(); + } + + this.socket.emit(this.messages.IMPORT_MINIFIGURE, { + figure: (figure !== undefined) ? figure : this.html_input.value, + owners: owners, + purchase_location: purchase_location, + storage: storage, + tags: tags, + quantity: 1 + }); + } else { + this.fail("Could not find the input field for the minifigure number"); + } + } + + // Load a minifigure + load_minifigure() { + if (this.html_input) { + // Reset the progress + this.clear() + this.spinner(true); + + this.socket.emit(this.messages.LOAD_MINIFIGURE, { + figure: this.html_input.value + }); + } else { + this.fail("Could not find the input field for the minifigure number"); + } + } + + // Minifigure is loaded + minifigure_loaded(data) { + if (this.html_card) { + this.html_card.classList.remove("d-none"); + + if (this.html_card_set) { + this.html_card_set.textContent = data["figure"]; + } + + if (this.html_card_name) { + this.html_card_name.textContent = data["name"]; + } + + if (this.html_card_image_container) { + this.html_card_image_container.setAttribute("style", `background-image: url(${data["image"]})`); + } + + if (this.html_card_image) { + this.html_card_image.setAttribute("src", data["image"]); + this.html_card_image.setAttribute("alt", data["figure"]); + } + + if (this.html_card_footer) { + this.html_card_footer.classList.add("d-none"); + + if (!data.download) { + this.html_card_footer.classList.remove("d-none"); + + if (this.html_card_confirm) { + if (this.confirm_listener !== undefined) { + this.html_card_confirm.removeEventListener("click", this.confirm_listener); + } + + this.confirm_listener = ((bricksocket, figure) => (e) => { + if (!bricksocket.disabled) { + bricksocket.toggle(false); + bricksocket.import_minifigure(false, figure); + } + })(this, data["figure"]); + + this.html_card_confirm.addEventListener("click", this.confirm_listener); + + this.html_card_confirm.scrollIntoView(); + } + } + } + } + } + + // Setup the actual socket + setup() { + super.setup(); + + if (this.socket !== undefined) { + // Minifigure loaded + this.socket.on(this.messages.MINIFIGURE_LOADED, ((bricksocket) => (data) => { + bricksocket.minifigure_loaded(data); + })(this)); + } + } + + // Toggle clicking on the button, or sending events + toggle(enabled) { + super.toggle(enabled); + + if (this.html_button) { + this.html_button.disabled = !enabled; + } + + if (this.html_input) { + this.html_input.disabled = !enabled; + } + + if (this.html_no_confim) { + this.html_no_confim.disabled = !enabled; + } + + if (this.html_owners) { + this.html_owners.querySelectorAll('input').forEach(input => input.disabled = !enabled); + } + + if (this.html_purchase_location) { + this.html_purchase_location.disabled = !enabled; + } + + if (this.html_storage) { + this.html_storage.disabled = !enabled; + } + + if (this.html_tags) { + this.html_tags.querySelectorAll('input').forEach(input => input.disabled = !enabled); + } + + if (this.html_card_confirm) { + this.html_card_confirm.disabled = !enabled; + } + + if (this.html_card_dismiss) { + this.html_card_dismiss.disabled = !enabled; + } + } +} diff --git a/static/scripts/socket/set.js b/static/scripts/socket/set.js index aea2993..992ea12 100644 --- a/static/scripts/socket/set.js +++ b/static/scripts/socket/set.js @@ -271,6 +271,56 @@ class BrickSetSocket extends BrickSocket { } } + // Minifigure is loaded (when bulk adding minifigures through set socket) + minifigure_loaded(data) { + if (this.html_card) { + this.html_card.classList.remove("d-none"); + + if (this.html_card_set) { + this.html_card_set.textContent = data["figure"]; + } + + if (this.html_card_name) { + this.html_card_name.textContent = data["name"]; + } + + if (this.html_card_image_container) { + this.html_card_image_container.setAttribute("style", `background-image: url(${data["image"]})`); + } + + if (this.html_card_image) { + this.html_card_image.setAttribute("src", data["image"]); + this.html_card_image.setAttribute("alt", data["figure"]); + } + + if (this.html_card_footer) { + this.html_card_footer.classList.add("d-none"); + + if (!data.download) { + this.html_card_footer.classList.remove("d-none"); + + if (this.html_card_confirm) { + if (this.confirm_listener !== undefined) { + this.html_card_confirm.removeEventListener("click", this.confirm_listener); + } + + this.confirm_listener = ((bricksocket, figure) => (e) => { + if (!bricksocket.disabled) { + bricksocket.toggle(false); + // For minifigures, we use import_set with the figure number + bricksocket.import_set(false, figure); + } + })(this, data["figure"]); + + this.html_card_confirm.addEventListener("click", this.confirm_listener); + + this.html_card_confirm.scrollIntoView(); + } + } + } + } + } + // Setup the actual socket setup() { super.setup(); @@ -280,6 +330,13 @@ class BrickSetSocket extends BrickSocket { this.socket.on(this.messages.SET_LOADED, ((bricksocket) => (data) => { bricksocket.set_loaded(data); })(this)); + + // Minifigure loaded (for bulk add with mixed sets/minifigures) + if (this.messages.MINIFIGURE_LOADED) { + this.socket.on(this.messages.MINIFIGURE_LOADED, ((bricksocket) => (data) => { + bricksocket.minifigure_loaded(data); + })(this)); + } } } diff --git a/static/styles.css b/static/styles.css index 857c3c6..2671992 100644 --- a/static/styles.css +++ b/static/styles.css @@ -272,3 +272,52 @@ [data-bs-theme="dark"] .table tbody th { color: var(--bs-body-color); } + +/* Navbar split button dropdown styling */ +.navbar-nav .dropdown { + display: flex; + align-items: center; + position: relative; +} + +.navbar-nav .dropdown .dropdown-toggle-split { + padding-left: 0.375rem; + padding-right: 0.375rem; + margin-left: -1px; +} + +.navbar-nav .dropdown .dropdown-toggle-split::after { + margin-left: 0; +} + +/* Navbar dropdown positioning */ +.navbar-nav .dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + margin-top: 0.125rem; +} + +/* Navbar dropdown styling to match navbar theme */ +.navbar-dark .navbar-nav .dropdown-menu { + background-color: #212529; + border-color: rgba(255, 255, 255, 0.15); +} + +.navbar-dark .navbar-nav .dropdown-menu .dropdown-item { + color: rgba(255, 255, 255, 0.55); +} + +.navbar-dark .navbar-nav .dropdown-menu .dropdown-item:hover, +.navbar-dark .navbar-nav .dropdown-menu .dropdown-item:focus { + color: rgba(255, 255, 255, 0.75); + background-color: rgba(255, 255, 255, 0.1); +} + +/* Navbar dropdown hover support for desktop */ +@media (min-width: 992px) { + .navbar-nav .dropdown:hover > .dropdown-menu { + display: block; + } +}