1import { Controller } from "@hotwired/stimulus" 2import { CHANGED, announce, read, remember } from "tools/menu_store" 3 4// Renders the store of edit URLs: the "Vos menus" section on the home page and 5// the navbar link that points at it. 6// 7// The store itself is tools/menu_store, because forget-token writes to it too 8// and it has no list to render. 9export default class extends Controller { 10 static targets = ["link", "list", "section"] 11 static values = { token: String, name: String, slug: String, claimUrl: String } 12 13 connect() { 14 document.addEventListener(CHANGED, this.onChanged) 15 16 // The builder page carries the token it is editing, so simply being there 17 // is what puts a menu in the store. Nothing to click, nothing to sign up 18 // for, and it survives the tab being closed. 19 if (this.hasTokenValue && this.tokenValue) { 20 remember({ token: this.tokenValue, name: this.nameValue, slug: this.slugValue }) 21 // The layout puts a second instance of this controller on the body, and 22 // it connected before this one had anything to show it. 23 announce() 24 } 25 26 this.render() 27 28 // The dashboard, and only the dashboard, carries a claim URL. 29 if (this.hasClaimUrlValue && this.claimUrlValue) this.claim() 30 } 31 32 disconnect() { 33 document.removeEventListener(CHANGED, this.onChanged) 34 } 35 36 onChanged = () => this.render() 37 38 // Signing in cannot attach the menus built anonymously, because the server has 39 // never seen their edit tokens: they are here, in this browser, which is the 40 // whole reason an account is optional. So the dashboard asks for them, once, 41 // and the server takes only the ones that belong to nobody. 42 // 43 // Failure is silent by design. The dashboard is still a correct page without 44 // it, and an author who is offline or has storage disabled has lost nothing: 45 // their edit link still works, exactly as it did before they had an account. 46 async claim() { 47 const tokens = read().map((entry) => entry.token).filter(Boolean) 48 if (tokens.length === 0) return 49 50 try { 51 const response = await fetch(this.claimUrlValue, { 52 method: "POST", 53 headers: { 54 "Content-Type": "application/json", 55 "Accept": "application/json", 56 "X-CSRF-Token": document.querySelector("meta[name=csrf-token]")?.content || "" 57 }, 58 body: JSON.stringify({ tokens }) 59 }) 60 if (!response.ok) return 61 62 // Only when something actually changed, so the ninety nine visits that 63 // claim nothing cost no second render. 64 const { claimed } = await response.json() 65 if (claimed > 0) window.location.reload() 66 } catch { 67 // Offline, or a claim that raced a sign out. Nothing to say and nothing 68 // to retry: the next visit asks again. 69 } 70 } 71 72 render() { 73 const entries = read() 74 75 this.linkTargets.forEach((link) => { 76 link.classList.toggle("hidden", entries.length === 0) 77 link.classList.toggle("flex", entries.length > 0) 78 }) 79 80 this.sectionTargets.forEach((section) => (section.hidden = entries.length === 0)) 81 82 this.listTargets.forEach((list) => { 83 list.replaceChildren(...entries.map((entry) => this.entryElement(entry))) 84 }) 85 } 86 87 // Built rather than templated, because the only strings in it are the menu's 88 // own name and its address: no copy, so nothing to translate here and nothing 89 // that could go untranslated. 90 entryElement({ token, name, slug }) { 91 const item = document.createElement("li") 92 const link = document.createElement("a") 93 94 link.href = `/edit/${token}` 95 link.className = "flex min-h-11 flex-col justify-center rounded-box border border-base-300 px-4 py-2 hover:border-base-content/30" 96 97 const title = document.createElement("span") 98 title.className = "font-medium text-base-content" 99 title.textContent = name || slug 100 101 const address = document.createElement("span") 102 address.className = "text-xs text-base-content/60" 103 address.textContent = `lfmenu.fr/r/${slug}` 104 105 link.append(title, address) 106 item.append(link) 107 return item 108 } 109}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.