PageSourceSearch

https://lfmenu.fr/assets/controllers/edit_tokens_controller-22a81dcd.js

js lfmenu.fr collected 2026-10-08 05:50:12 UTC 4,090 bytes, 109 lines download raw bytes

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.