From 5c721eb08cf45524c0bd338569b62bd8c6b2492f Mon Sep 17 00:00:00 2001 From: insects Date: Mon, 10 Mar 2025 23:39:05 +0100 Subject: [PATCH] feat: implement popping nms --- app/assets/stylesheets/application.css | 44 +- app/controllers/instance_controller.rb | 16 +- app/javascript/application.js | 9 +- app/javascript/controllers/application.js | 9 - .../controllers/hello_controller.js | 7 - app/javascript/controllers/index.js | 4 - app/models/instance.rb | 2 + app/models/pop.rb | 3 + app/views/instance/_list.html.erb | 48 + app/views/instance/show.html.erb | 35 +- app/views/layouts/application.html.erb | 2 +- config/importmap.rb | 5 +- config/routes.rb | 1 + test/fixtures/pops.yml | 11 + test/models/pop_test.rb | 7 + vendor/javascript/htmx.org.js | 1264 +++++++++++++++++ 16 files changed, 1397 insertions(+), 70 deletions(-) delete mode 100644 app/javascript/controllers/application.js delete mode 100644 app/javascript/controllers/hello_controller.js delete mode 100644 app/javascript/controllers/index.js create mode 100644 app/models/pop.rb create mode 100644 app/views/instance/_list.html.erb create mode 100644 test/fixtures/pops.yml create mode 100644 test/models/pop_test.rb create mode 100644 vendor/javascript/htmx.org.js diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index e2dc060..fc2ac7b 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -28,17 +28,27 @@ header .muted { padding: 10px; } -.nm-list { +#nm-list { } -.nm-list section { +#nm-list section { margin-bottom: 5px; display: grid; - grid-template-columns: .05fr 1fr 1fr .5fr; + grid-template-columns: .05fr 1fr 1fr .3fr; align-items: center; - padding: 0 10px; - background-color: #eee; + padding-left: 10px; + background-color: #3D9970; + color: white; +} + +#nm-list section.popped { + background-color: #b5443a; + color: #63f0fd; +} + +#nm-list section div.button { + height: 100%; } img { @@ -59,7 +69,7 @@ h3.nm-info { .badge { font-size: 12px; font-weight: bold; - border: 1px solid black; + border: 1px solid white; vertical-align: middle; padding: 1px 6px; border-radius: 10px; @@ -70,13 +80,31 @@ small.badge { font-size: 10px; } +.popped .badge { + border-color: #63f0fd; +} + button.action { width: 100%; height: 100%; + border: 0; + display: block; + background-color: #005ba4; + color: #daffbe; + font-size: 18px; + text-transform: uppercase; +} + +button.reset { + background-color: tomato; +} + +button.action:hover { + cursor: pointer; } section .meta { padding-left: 10px; - padding-top: 4px; - padding-bottom: 4px; + padding-top: 10px; + padding-bottom: 10px; } diff --git a/app/controllers/instance_controller.rb b/app/controllers/instance_controller.rb index ae7cb55..9a39c6c 100644 --- a/app/controllers/instance_controller.rb +++ b/app/controllers/instance_controller.rb @@ -11,7 +11,17 @@ class InstanceController < ApplicationController end def show - @instance = Instance.find_by(public_id: show_instance_params) + @instance = Instance.includes(:pops).find_by(public_id: show_instance_params) + end + + def pop + instance_id, nm = pop_instance_params + parent_instance = Instance.find_by(public_id: instance_id) + pop = Pop.new(instance_id: parent_instance.id, name: nm) + if pop.save + @instance = Instance.includes(:pops).find_by(public_id: instance_id) + render partial: "list", locals: { instance: @instance } + end end private @@ -23,4 +33,8 @@ class InstanceController < ApplicationController def show_instance_params params.expect(:public_id) end + + def pop_instance_params + params.expect(:instance, :nm) + end end diff --git a/app/javascript/application.js b/app/javascript/application.js index 0d7b494..de6bf57 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1,3 +1,8 @@ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails -import "@hotwired/turbo-rails" -import "controllers" +import "htmx.org" +import htmx from "htmx.org" +htmx.logger = function(elt, event, data) { + if(console) { + console.log(event, elt, data); + } +} diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js deleted file mode 100644 index 1213e85..0000000 --- a/app/javascript/controllers/application.js +++ /dev/null @@ -1,9 +0,0 @@ -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/hello_controller.js b/app/javascript/controllers/hello_controller.js deleted file mode 100644 index 5975c07..0000000 --- a/app/javascript/controllers/hello_controller.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Controller } from "@hotwired/stimulus" - -export default class extends Controller { - connect() { - this.element.textContent = "Hello World!" - } -} diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js deleted file mode 100644 index 1156bf8..0000000 --- a/app/javascript/controllers/index.js +++ /dev/null @@ -1,4 +0,0 @@ -// Import and register all your controllers from the importmap via controllers/**/*_controller -import { application } from "controllers/application" -import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" -eagerLoadControllersFrom("controllers", application) diff --git a/app/models/instance.rb b/app/models/instance.rb index f77247f..c8eaed7 100644 --- a/app/models/instance.rb +++ b/app/models/instance.rb @@ -1,3 +1,5 @@ class Instance < ApplicationRecord + has_many :pops + validates :zone, inclusion: { in: %w[anemos pagos pyros hydatos] } end diff --git a/app/models/pop.rb b/app/models/pop.rb new file mode 100644 index 0000000..716c670 --- /dev/null +++ b/app/models/pop.rb @@ -0,0 +1,3 @@ +class Pop < ApplicationRecord + belongs_to :instance +end diff --git a/app/views/instance/_list.html.erb b/app/views/instance/_list.html.erb new file mode 100644 index 0000000..aedb3c9 --- /dev/null +++ b/app/views/instance/_list.html.erb @@ -0,0 +1,48 @@ +
+ <% APP_DATA[instance.zone.to_sym][:nms].each do |nm| %> + <% is_popped = instance.pops.any? { |pop| pop.name == nm[:name].parameterize } %> +
+
+ " alt="<%= nm[:element] %>" width="30" /> +
+
+

+ LV<%= nm[:level].to_s.rjust(2, "0") %> + <%= nm[:name] %> + <% if nm[:weather] %> + + <% end %> +

+
+ « + <%= nm[:spawned_by][:name] %> + <% if nm[:spawned_by][:night_only] %> + 🌙 + <% end %> + <% if nm[:spawned_by][:weather] %> + + <% end %> + LV<%= nm[:spawned_by][:level].to_s.rjust(2, "0") %> + +
+
+
+
+ <% if is_popped %> + + <% else %> + + <% end %> + +
+
+ <% end %> +
diff --git a/app/views/instance/show.html.erb b/app/views/instance/show.html.erb index 8250cf9..7b9d2bb 100644 --- a/app/views/instance/show.html.erb +++ b/app/views/instance/show.html.erb @@ -4,37 +4,4 @@ <%= render partial: "zone_img", locals: { zone: @instance.zone, alt: @instance.zone, title: @instance.zone.upcase_first } %> -
- <% APP_DATA[@instance.zone.to_sym][:nms].each do |nm| %> -
-
- " alt="<%= nm[:element] %>" width="30" /> -
-
-

- LV<%= nm[:level].to_s.rjust(2, "0") %> - <%= nm[:name] %> - <% if nm[:weather] %> - - <% end %> -

-
- « - <%= nm[:spawned_by][:name] %> - <% if nm[:spawned_by][:night_only] %> - 🌙 - <% end %> - <% if nm[:spawned_by][:weather] %> - - <% end %> - LV<%= nm[:spawned_by][:level].to_s.rjust(2, "0") %> - -
-
-
-
- -
-
- <% end %> -
+<%= render partial: "list", locals: { instance: @instance } %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index bd8792a..aabbd02 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -21,7 +21,7 @@ <%= javascript_importmap_tags %> - + <%= yield %> diff --git a/config/importmap.rb b/config/importmap.rb index 909dfc5..d331e97 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -1,7 +1,4 @@ # Pin npm packages by running ./bin/importmap pin "application" -pin "@hotwired/turbo-rails", to: "turbo.min.js" -pin "@hotwired/stimulus", to: "stimulus.min.js" -pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" -pin_all_from "app/javascript/controllers", under: "controllers" +pin "htmx.org" # @2.0.1 diff --git a/config/routes.rb b/config/routes.rb index 8def87a..e9487da 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,7 @@ Rails.application.routes.draw do post "/new", to: "instance#create", as: :new_instance get "/:public_id", to: "instance#show", as: :show_instance + post "/pop", to: "instance#pop", as: :pop_in_instance get "up" => "rails/health#show", as: :rails_health_check diff --git a/test/fixtures/pops.yml b/test/fixtures/pops.yml new file mode 100644 index 0000000..d7a3329 --- /dev/null +++ b/test/fixtures/pops.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the "{}" from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/models/pop_test.rb b/test/models/pop_test.rb new file mode 100644 index 0000000..608906e --- /dev/null +++ b/test/models/pop_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class PopTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/vendor/javascript/htmx.org.js b/vendor/javascript/htmx.org.js new file mode 100644 index 0000000..3975876 --- /dev/null +++ b/vendor/javascript/htmx.org.js @@ -0,0 +1,1264 @@ +// htmx.org@2.0.1 downloaded from https://ga.jspm.io/npm:htmx.org@2.0.1/dist/htmx.esm.js + +var htmx=function(){const htmx={ +/** @type {typeof onLoadHelper} */ +onLoad:null, +/** @type {typeof processNode} */ +process:null, +/** @type {typeof addEventListenerImpl} */ +on:null, +/** @type {typeof removeEventListenerImpl} */ +off:null, +/** @type {typeof triggerEvent} */ +trigger:null, +/** @type {typeof ajaxHelper} */ +ajax:null, +/** @type {typeof find} */ +find:null, +/** @type {typeof findAll} */ +findAll:null, +/** @type {typeof closest} */ +closest:null, +/** + * Returns the input values that would resolve for a given element via the htmx value resolution mechanism + * + * @see https://htmx.org/api/#values + * + * @param {Element} elt the element to resolve values on + * @param {HttpVerb} type the request type (e.g. **get** or **post**) non-GET's will include the enclosing form of the element. Defaults to **post** + * @returns {Object} + */ +values:function(e,t){const n=getInputValues(e,t||"post");return n.values}, +/** @type {typeof removeElement} */ +remove:null, +/** @type {typeof addClassToElement} */ +addClass:null, +/** @type {typeof removeClassFromElement} */ +removeClass:null, +/** @type {typeof toggleClassOnElement} */ +toggleClass:null, +/** @type {typeof takeClassForElement} */ +takeClass:null, +/** @type {typeof swap} */ +swap:null, +/** @type {typeof defineExtension} */ +defineExtension:null, +/** @type {typeof removeExtension} */ +removeExtension:null, +/** @type {typeof logAll} */ +logAll:null, +/** @type {typeof logNone} */ +logNone:null,logger:null,config:{ +/** + * Whether to use history. + * @type boolean + * @default true + */ +historyEnabled:true, +/** + * The number of pages to keep in **localStorage** for history support. + * @type number + * @default 10 + */ +historyCacheSize:10, +/** + * @type boolean + * @default false + */ +refreshOnHistoryMiss:false, +/** + * The default swap style to use if **[hx-swap](https://htmx.org/attributes/hx-swap)** is omitted. + * @type HtmxSwapStyle + * @default 'innerHTML' + */ +defaultSwapStyle:"innerHTML", +/** + * The default delay between receiving a response from the server and doing the swap. + * @type number + * @default 0 + */ +defaultSwapDelay:0, +/** + * The default delay between completing the content swap and settling attributes. + * @type number + * @default 20 + */ +defaultSettleDelay:20, +/** + * If true, htmx will inject a small amount of CSS into the page to make indicators invisible unless the **htmx-indicator** class is present. + * @type boolean + * @default true + */ +includeIndicatorStyles:true, +/** + * The class to place on indicators when a request is in flight. + * @type string + * @default 'htmx-indicator' + */ +indicatorClass:"htmx-indicator", +/** + * The class to place on triggering elements when a request is in flight. + * @type string + * @default 'htmx-request' + */ +requestClass:"htmx-request", +/** + * The class to temporarily place on elements that htmx has added to the DOM. + * @type string + * @default 'htmx-added' + */ +addedClass:"htmx-added", +/** + * The class to place on target elements when htmx is in the settling phase. + * @type string + * @default 'htmx-settling' + */ +settlingClass:"htmx-settling", +/** + * The class to place on target elements when htmx is in the swapping phase. + * @type string + * @default 'htmx-swapping' + */ +swappingClass:"htmx-swapping", +/** + * Allows the use of eval-like functionality in htmx, to enable **hx-vars**, trigger conditions & script tag evaluation. Can be set to **false** for CSP compatibility. + * @type boolean + * @default true + */ +allowEval:true, +/** + * If set to false, disables the interpretation of script tags. + * @type boolean + * @default true + */ +allowScriptTags:true, +/** + * If set, the nonce will be added to inline scripts. + * @type string + * @default '' + */ +inlineScriptNonce:"", +/** + * If set, the nonce will be added to inline styles. + * @type string + * @default '' + */ +inlineStyleNonce:"", +/** + * The attributes to settle during the settling phase. + * @type string[] + * @default ['class', 'style', 'width', 'height'] + */ +attributesToSettle:["class","style","width","height"], +/** + * Allow cross-site Access-Control requests using credentials such as cookies, authorization headers or TLS client certificates. + * @type boolean + * @default false + */ +withCredentials:false, +/** + * @type number + * @default 0 + */ +timeout:0, +/** + * The default implementation of **getWebSocketReconnectDelay** for reconnecting after unexpected connection loss by the event code **Abnormal Closure**, **Service Restart** or **Try Again Later**. + * @type {'full-jitter' | ((retryCount:number) => number)} + * @default "full-jitter" + */ +wsReconnectDelay:"full-jitter", +/** + * The type of binary data being received over the WebSocket connection + * @type BinaryType + * @default 'blob' + */ +wsBinaryType:"blob", +/** + * @type string + * @default '[hx-disable], [data-hx-disable]' + */ +disableSelector:"[hx-disable], [data-hx-disable]", +/** + * @type {'auto' | 'instant' | 'smooth'} + * @default 'smooth' + */ +scrollBehavior:"instant", +/** + * If the focused element should be scrolled into view. + * @type boolean + * @default false + */ +defaultFocusScroll:false, +/** + * If set to true htmx will include a cache-busting parameter in GET requests to avoid caching partial responses by the browser + * @type boolean + * @default false + */ +getCacheBusterParam:false, +/** + * If set to true, htmx will use the View Transition API when swapping in new content. + * @type boolean + * @default false + */ +globalViewTransitions:false, +/** + * htmx will format requests with these methods by encoding their parameters in the URL, not the request body + * @type {(HttpVerb)[]} + * @default ['get', 'delete'] + */ +methodsThatUseUrlParams:["get","delete"], +/** + * If set to true, disables htmx-based requests to non-origin hosts. + * @type boolean + * @default false + */ +selfRequestsOnly:true, +/** + * If set to true htmx will not update the title of the document when a title tag is found in new content + * @type boolean + * @default false + */ +ignoreTitle:false, +/** + * Whether the target of a boosted element is scrolled into the viewport. + * @type boolean + * @default true + */ +scrollIntoViewOnBoost:true, +/** + * The cache to store evaluated trigger specifications into. + * You may define a simple object to use a never-clearing cache, or implement your own system using a [proxy object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy) + * @type {Object|null} + * @default null + */ +triggerSpecsCache:null, +/** @type boolean */ +disableInheritance:false, +/** @type HtmxResponseHandlingConfig[] */ +responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}], +/** + * Whether to process OOB swaps on elements that are nested within the main response element. + * @type boolean + * @default true + */ +allowNestedOobSwaps:true}, +/** @type {typeof parseInterval} */ +parseInterval:null, +/** @type {typeof internalEval} */ +_:null,version:"2.0.1"};htmx.onLoad=onLoadHelper;htmx.process=processNode;htmx.on=addEventListenerImpl;htmx.off=removeEventListenerImpl;htmx.trigger=triggerEvent;htmx.ajax=ajaxHelper;htmx.find=find;htmx.findAll=findAll;htmx.closest=closest;htmx.remove=removeElement;htmx.addClass=addClassToElement;htmx.removeClass=removeClassFromElement;htmx.toggleClass=toggleClassOnElement;htmx.takeClass=takeClassForElement;htmx.swap=swap;htmx.defineExtension=defineExtension;htmx.removeExtension=removeExtension;htmx.logAll=logAll;htmx.logNone=logNone;htmx.parseInterval=parseInterval;htmx._=internalEval;const internalAPI={addTriggerHandler:addTriggerHandler,bodyContains:bodyContains,canAccessLocalStorage:canAccessLocalStorage,findThisElement:findThisElement,filterValues:filterValues,swap:swap,hasAttribute:hasAttribute,getAttributeValue:getAttributeValue,getClosestAttributeValue:getClosestAttributeValue,getClosestMatch:getClosestMatch,getExpressionVars:getExpressionVars,getHeaders:getHeaders,getInputValues:getInputValues,getInternalData:getInternalData,getSwapSpecification:getSwapSpecification,getTriggerSpecs:getTriggerSpecs,getTarget:getTarget,makeFragment:makeFragment,mergeObjects:mergeObjects,makeSettleInfo:makeSettleInfo,oobSwap:oobSwap,querySelectorExt:querySelectorExt,settleImmediately:settleImmediately,shouldCancel:shouldCancel,triggerEvent:triggerEvent,triggerErrorEvent:triggerErrorEvent,withExtensions:withExtensions};const VERBS=["get","post","put","delete","patch"];const VERB_SELECTOR=VERBS.map((function(e){return"[hx-"+e+"], [data-hx-"+e+"]"})).join(", ");const HEAD_TAG_REGEX=makeTagRegEx("head"); +/** + * @param {string} tag + * @param {boolean} global + * @returns {RegExp} + */function makeTagRegEx(e,t=false){return new RegExp(`<${e}(\\s[^>]*>|>)([\\s\\S]*?)<\\/${e}>`,t?"gim":"im")} +/** + * Parses an interval string consistent with the way htmx does. Useful for plugins that have timing-related attributes. + * + * Caution: Accepts an int followed by either **s** or **ms**. All other values use **parseFloat** + * + * @see https://htmx.org/api/#parseInterval + * + * @param {string} str timing string + * @returns {number|undefined} + */function parseInterval(e){if(e==void 0)return;let t=NaN;t=e.slice(-2)=="ms"?parseFloat(e.slice(0,-2)):e.slice(-1)=="s"?parseFloat(e.slice(0,-1))*1e3:e.slice(-1)=="m"?parseFloat(e.slice(0,-1))*1e3*60:parseFloat(e);return isNaN(t)?void 0:t} +/** + * @param {Node} elt + * @param {string} name + * @returns {(string | null)} + */function getRawAttribute(e,t){return e instanceof Element&&e.getAttribute(t)} +/** + * @param {Element} elt + * @param {string} qualifiedName + * @returns {boolean} + */function hasAttribute(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))} +/** + * + * @param {Node} elt + * @param {string} qualifiedName + * @returns {(string | null)} + */function getAttributeValue(e,t){return getRawAttribute(e,t)||getRawAttribute(e,"data-"+t)} +/** + * @param {Node} elt + * @returns {Node | null} + */function parentElt(e){const t=e.parentElement;return!t&&e.parentNode instanceof ShadowRoot?e.parentNode:t} +/** + * @returns {Document} + */function getDocument(){return document} +/** + * @param {Node} elt + * @param {boolean} global + * @returns {Node|Document} + */function getRootNode(e,t){return e.getRootNode?e.getRootNode({composed:t}):getDocument()} +/** + * @param {Node} elt + * @param {(e:Node) => boolean} condition + * @returns {Node | null} + */function getClosestMatch(e,t){while(e&&!t(e))e=parentElt(e);return e||null} +/** + * @param {Element} initialElement + * @param {Element} ancestor + * @param {string} attributeName + * @returns {string|null} + */function getAttributeValueWithDisinheritance(e,t,n){const r=getAttributeValue(t,n);const o=getAttributeValue(t,"hx-disinherit");var s=getAttributeValue(t,"hx-inherit");if(e!==t){if(htmx.config.disableInheritance)return s&&(s==="*"||s.split(" ").indexOf(n)>=0)?r:null;if(o&&(o==="*"||o.split(" ").indexOf(n)>=0))return"unset"}return r} +/** + * @param {Element} elt + * @param {string} attributeName + * @returns {string | null} + */function getClosestAttributeValue(e,t){let n=null;getClosestMatch(e,(function(r){return!!(n=getAttributeValueWithDisinheritance(e,asElement(r),t))}));if(n!=="unset")return n} +/** + * @param {Node} elt + * @param {string} selector + * @returns {boolean} + */function matches(e,t){const n=e instanceof Element&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector);return!!n&&n.call(e,t)} +/** + * @param {string} str + * @returns {string} + */function getStartTag(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);return n?n[1].toLowerCase():""} +/** + * @param {string} resp + * @returns {Document} + */function parseHTML(e){const t=new DOMParser;return t.parseFromString(e,"text/html")} +/** + * @param {DocumentFragment} fragment + * @param {Node} elt + */function takeChildrenFor(e,t){while(t.childNodes.length>0)e.append(t.childNodes[0])} +/** + * @param {HTMLScriptElement} script + * @returns {HTMLScriptElement} + */function duplicateScript(e){const t=getDocument().createElement("script");forEach(e.attributes,(function(e){t.setAttribute(e.name,e.value)}));t.textContent=e.textContent;t.async=false;htmx.config.inlineScriptNonce&&(t.nonce=htmx.config.inlineScriptNonce);return t} +/** + * @param {HTMLScriptElement} script + * @returns {boolean} + */function isJavaScriptScriptNode(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")} +/** + * we have to make new copies of script tags that we are going to insert because + * SOME browsers (not saying who, but it involves an element and an animal) don't + * execute scripts created in