wire shop LiveViews to DB queries and improve search UX
Replace PreviewData indirection in all shop LiveViews with direct Products context queries. Home, collection, product detail and error pages now query the database. Categories loaded once in ThemeHook. Cart hydration no longer falls back to mock data. PreviewData kept only for the theme editor. Search modal gains keyboard navigation (arrow keys, Enter, Escape), Cmd+K/Ctrl+K shortcut, full ARIA combobox pattern, LiveView navigate links, and 150ms debounce. SearchModal JS hook manages selection state and highlight. search.ex gets transaction safety on reindex and a public remove_product/1. 10 new integration tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
115
assets/js/app.js
115
assets/js/app.js
@@ -324,10 +324,123 @@ const ProductImageScroll = {
|
||||
}
|
||||
}
|
||||
|
||||
// Hook for search modal keyboard navigation and shortcuts
|
||||
const SearchModal = {
|
||||
mounted() {
|
||||
this.selectedIndex = -1
|
||||
|
||||
this._globalKeydown = (e) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
||||
e.preventDefault()
|
||||
if (this.isOpen()) {
|
||||
this.close()
|
||||
} else {
|
||||
this.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", this._globalKeydown)
|
||||
|
||||
this.el.addEventListener("keydown", (e) => {
|
||||
if (!this.isOpen()) return
|
||||
|
||||
switch (e.key) {
|
||||
case "Escape":
|
||||
e.preventDefault()
|
||||
this.close()
|
||||
break
|
||||
case "ArrowDown":
|
||||
e.preventDefault()
|
||||
this.moveSelection(1)
|
||||
break
|
||||
case "ArrowUp":
|
||||
e.preventDefault()
|
||||
this.moveSelection(-1)
|
||||
break
|
||||
case "Enter":
|
||||
e.preventDefault()
|
||||
this.navigateToSelected()
|
||||
break
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
document.removeEventListener("keydown", this._globalKeydown)
|
||||
},
|
||||
|
||||
updated() {
|
||||
this.selectedIndex = -1
|
||||
this.updateHighlight()
|
||||
},
|
||||
|
||||
isOpen() {
|
||||
return this.el.style.display !== "none" && this.el.style.display !== ""
|
||||
},
|
||||
|
||||
open() {
|
||||
this.el.style.display = "flex"
|
||||
const input = this.el.querySelector("#search-input")
|
||||
if (input) {
|
||||
input.focus()
|
||||
input.select()
|
||||
}
|
||||
this.selectedIndex = -1
|
||||
this.updateHighlight()
|
||||
},
|
||||
|
||||
close() {
|
||||
this.el.style.display = "none"
|
||||
this.pushEvent("clear_search", {})
|
||||
this.selectedIndex = -1
|
||||
this.updateHighlight()
|
||||
},
|
||||
|
||||
getResults() {
|
||||
return this.el.querySelectorAll('[role="option"]')
|
||||
},
|
||||
|
||||
moveSelection(delta) {
|
||||
const results = this.getResults()
|
||||
if (results.length === 0) return
|
||||
|
||||
this.selectedIndex += delta
|
||||
if (this.selectedIndex < -1) this.selectedIndex = results.length - 1
|
||||
if (this.selectedIndex >= results.length) this.selectedIndex = 0
|
||||
|
||||
this.updateHighlight()
|
||||
},
|
||||
|
||||
updateHighlight() {
|
||||
const results = this.getResults()
|
||||
results.forEach((r, i) => {
|
||||
const isSelected = i === this.selectedIndex
|
||||
r.style.background = isSelected ? "var(--t-surface-sunken)" : ""
|
||||
r.setAttribute("aria-selected", isSelected)
|
||||
})
|
||||
|
||||
if (this.selectedIndex >= 0 && results[this.selectedIndex]) {
|
||||
results[this.selectedIndex].scrollIntoView({ block: "nearest" })
|
||||
}
|
||||
},
|
||||
|
||||
navigateToSelected() {
|
||||
const results = this.getResults()
|
||||
const index = this.selectedIndex >= 0 ? this.selectedIndex : 0
|
||||
if (results[index]) {
|
||||
const link = results[index].querySelector("a")
|
||||
if (link) {
|
||||
this.el.style.display = "none"
|
||||
link.click()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
||||
const liveSocket = new LiveSocket("/live", Socket, {
|
||||
params: {_csrf_token: csrfToken},
|
||||
hooks: {...colocatedHooks, ColorSync, Lightbox, CartPersist, CartDrawer, ProductImageScroll},
|
||||
hooks: {...colocatedHooks, ColorSync, Lightbox, CartPersist, CartDrawer, ProductImageScroll, SearchModal},
|
||||
})
|
||||
|
||||
// Show progress bar on live navigation and form submits
|
||||
|
||||
Reference in New Issue
Block a user