PageSourceSearch

https://snapnsell.ai/assets/controllers/app_shell_controller-5b5bf4cf.js

js snapnsell.ai collected 2026-10-05 13:57:20 UTC 13,197 bytes, 474 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3const APP_SHELL_STATE_KEY = "snapnsell:app-shell-state"
4
5export default class extends Controller {
6  static targets = [
7    "startPage",
8    "imagesPage",
9    "sellPane",
10    "sellFrame",
11    "sellTab",
12    "snapTab",
13    "snapBackground",
14    "menu",
15    "collectionsMenu",
16    "overlay",
17    "card",
18    "cardFrame",
19    "chats",
20    "chatsFrame",
21    "chatsBackdrop"
22  ]
23  static values = {
24    initialPane: { type: String, default: "snap" },
25    initialCardOpen: { type: Boolean, default: false },
26    rootUrl: String,
27    chatsDeeplink: { type: Object, default: {} }
28  }
29
30  connect() {
31    this.boundShowSnap = () => this.activatePane("snap", { explicit: true })
32    this.boundShowSell = () => this.activatePane("sell", { explicit: true })
33    this.boundShowImages = () => this.activatePane("images", { explicit: true })
34    this.boundOpenCard = (event) => this.openCardForEvent(event)
35    this.boundCloseCard = () => this.closeCard()
36    this.boundCloseMenus = () => this.closeMenus()
37
38    document.addEventListener("ss:show-snap", this.boundShowSnap)
39    document.addEventListener("ss:show-sell", this.boundShowSell)
40    document.addEventListener("ss:show-images", this.boundShowImages)
41    document.addEventListener("ss:open-card", this.boundOpenCard)
42    document.addEventListener("ss:close-card", this.boundCloseCard)
43    document.addEventListener("ss:close-menus", this.boundCloseMenus)
44
45    this.normalizeBrowserUrl()
46    this.restoreShellState()
47    this.maybeOpenChatsFromDeeplink()
48  }
49
50  disconnect() {
51    document.removeEventListener("ss:show-snap", this.boundShowSnap)
52    document.removeEventListener("ss:show-sell", this.boundShowSell)
53    document.removeEventListener("ss:show-images", this.boundShowImages)
54    document.removeEventListener("ss:open-card", this.boundOpenCard)
55    document.removeEventListener("ss:close-card", this.boundCloseCard)
56    document.removeEventListener("ss:close-menus", this.boundCloseMenus)
57  }
58
59  resetToSnap(event) {
60    event.preventDefault()
61    this.closeCard()
62    if (window.ssApp && typeof window.ssApp.resetApp === "function") {
63      window.ssApp.resetApp()
64      return
65    }
66
67    this.activatePane("snap", { explicit: true })
68  }
69
70  showSell(event) {
71    if (event) {
72      event.preventDefault()
73    }
74
75    this.activatePane("sell", { explicit: true })
76  }
77
78  showSellFromLink(event) {
79    this.activatePane("sell", { explicit: true })
80  }
81
82  openCardFromLink(event) {
83    event.preventDefault()
84
85    const url = event.currentTarget?.href
86    if (!url) return
87
88    this.openCardAtUrl(url)
89  }
90
91  openChatsFromLink(event) {
92    event.preventDefault()
93    const url = event.currentTarget?.href
94    if (!url) return
95    this.openChatsAtUrl(url)
96  }
97
98  handleSellFrameLoad() {
99  }
100
101  handleCardFrameLoad() {
102    this.activatePane("sell")
103    this.openCard()
104  }
105
106  handleChatsFrameLoad() {
107    this.activatePane("sell")
108    this.openChats()
109  }
110
111  handleBeforeStreamRender(event) {
112    const streamElement = event.target
113    if (!(streamElement instanceof HTMLElement)) return
114
115    const target = streamElement.getAttribute("target")
116    if (!["sellPage", "sellCard", "chatsCard"].includes(target)) return
117
118    const originalRender = event.detail.render
119    event.detail.render = (currentStreamElement) => {
120      originalRender(currentStreamElement)
121      if (target === "sellCard") {
122        this.activatePane("sell")
123        this.openCard()
124      }
125      if (target === "chatsCard") {
126        this.activatePane("sell")
127        this.openChats()
128      }
129    }
130  }
131
132  toggleMenu(event) {
133    event.preventDefault()
134    event.stopPropagation()
135    this.toggleDrawer(this.menuTarget)
136  }
137
138  closeMenu() {
139    if (!this.hasMenuTarget) return
140
141    this.menuTarget.classList.remove("open")
142    this.updateOverlayVisibility()
143  }
144
145  toggleCollectionsMenu(event) {
146    event.preventDefault()
147    event.stopPropagation()
148    if (!this.hasCollectionsMenuTarget) return
149
150    this.toggleDrawer(this.collectionsMenuTarget)
151  }
152
153  closeCollectionsMenu() {
154    if (!this.hasCollectionsMenuTarget) return
155
156    this.collectionsMenuTarget.classList.remove("open")
157    this.updateOverlayVisibility()
158  }
159
160  closeMenus() {
161    if (this.hasMenuTarget) {
162      this.menuTarget.classList.remove("open")
163    }
164
165    if (this.hasCollectionsMenuTarget) {
166      this.collectionsMenuTarget.classList.remove("open")
167    }
168
169    this.updateOverlayVisibility()
170  }
171
172  handleDocumentClick(event) {
173    if (!this.anyMenuOpen()) return
174    if (this.hasMenuTarget && this.menuTarget.contains(event.target)) return
175    if (this.hasCollectionsMenuTarget && this.collectionsMenuTarget.contains(event.target)) return
176
177    this.closeMenus()
178  }
179
180  dismissCard(event) {
181    event.preventDefault()
182    this.closeCard()
183  }
184
185  dismissChats(event) {
186    if (event) {
187      event.preventDefault()
188    }
189    this.closeChats()
190  }
191
192  closeCardAfterDelete() {
193    this.closeCard()
194  }
195
196  showImages() {
197    this.activatePane("images", { explicit: true })
198  }
199
200  openCardForEvent(event) {
201    const url = event.detail?.url
202    const showLoading = event.detail?.showLoading !== false
203    const loadingText = event.detail?.loadingText
204
205    this.activatePane("sell", { explicit: true })
206    this.openCardAtUrl(url, { showLoading, loadingText })
207  }
208
209  activatePane(pane, options = {}) {
210    const normalizedPane = this.normalizePane(pane)
211    const snapActive = normalizedPane !== "sell"
212    const imagesActive = normalizedPane === "images"
213    const sellActive = normalizedPane === "sell"
214
215    this.element.classList.toggle("is-sell-active", sellActive)
216    this.element.classList.toggle("is-snap-active", snapActive)
217    this.element.classList.toggle("is-images-active", imagesActive)
218
219    if (this.hasSnapTabTarget) {
220      this.snapTabTarget.classList.toggle("active", !sellActive)
221      this.snapTabTarget.classList.toggle("inactive", sellActive)
222    }
223
224    if (this.hasSellTabTarget) {
225      this.sellTabTarget.classList.toggle("active", sellActive)
226      this.sellTabTarget.classList.toggle("inactive", !sellActive)
227    }
228
229    if (this.hasStartPageTarget) {
230      this.startPageTarget.classList.toggle("hidden", imagesActive)
231      this.startPageTarget.classList.toggle("visible", !imagesActive)
232    }
233
234    if (this.hasImagesPageTarget) {
235      this.imagesPageTarget.classList.toggle("hidden", !imagesActive)
236      this.imagesPageTarget.classList.toggle("visible", imagesActive)
237    }
238
239    if (this.hasSnapBackgroundTarget) {
240      this.snapBackgroundTarget.classList.toggle("hidden", sellActive || imagesActive)
241    }
242
243    if (sellActive) {
244      this.closeMenus()
245    }
246
247    const nextState = { pane: normalizedPane }
248    if (options.explicit) {
249      nextState.lastExplicitPane = normalizedPane
250    }
251
252    this.persistState(nextState)
253  }
254
255  openCard() {
256    if (!this.hasCardTarget) return
257
258    this.element.classList.add("is-card-open")
259    this.cardTarget.classList.add("is-open")
260    this.persistState({ cardOpen: true })
261  }
262
263  closeCard() {
264    if (!this.hasCardTarget) return
265
266    this.element.classList.remove("is-card-open")
267    this.cardTarget.classList.remove("is-open")
268    this.persistState({ cardOpen: false })
269  }
270
271  openChats() {
272    if (!this.hasChatsTarget) return
273    this.element.classList.add("is-chats-open")
274    this.chatsTarget.classList.add("is-open")
275  }
276
277  closeChats() {
278    if (!this.hasChatsTarget) return
279    this.element.classList.remove("is-chats-open")
280    this.chatsTarget.classList.remove("is-open")
281    this.persistState({})
282  }
283
284  openChatsAtUrl(url, options = {}) {
285    const showLoading = options.showLoading !== false
286    const loadingText = options.loadingText || "Loading…"
287    if (showLoading) {
288      this.showChatsLoading(loadingText)
289    }
290    this.openChats()
291    this.loadChatsFrame(url)
292  }
293
294  showChatsLoading(message = "Loading…") {
295    if (!this.hasChatsFrameTarget) return
296    this.chatsFrameTarget.innerHTML = `
297      <div class="sell-card-loading">
298        <div class="sell-card-loading__spinner"></div>
299        <div class="sell-card-loading__text">${message}</div>
300      </div>
301    `
302  }
303
304  loadChatsFrame(url) {
305    if (!this.hasChatsFrameTarget) return
306    if (!url) return
307    const requestUrl = new URL(url, window.location.origin)
308    requestUrl.searchParams.set("_chats_request_id", Date.now().toString())
309    this.chatsFrameTarget.setAttribute("src", requestUrl.toString())
310  }
311
312  showCardLoading(message = "Loading item details...") {
313    if (!this.hasCardFrameTarget) return
314
315    this.cardFrameTarget.innerHTML = `
316      <div class="sell-card-loading">
317        <div class="sell-card-loading__spinner"></div>
318        <div class="sell-card-loading__text">${message}</div>
319      </div>
320    `
321  }
322
323  restoreShellState() {
324    const savedState = this.readState()
325    const hasServerChatsDeeplink = this.hasChatsDeeplinkValue && this.chatsDeeplinkValue && this.chatsDeeplinkValue.itemId
326    const restoredPane = this.shouldRestoreSavedState() && !hasServerChatsDeeplink
327      ? (savedState.cardOpen && savedState.cardUrl ? "sell" : savedState.lastExplicitPane)
328      : null
329    const initialPane = this.normalizePane(restoredPane || this.initialPaneValue)
330
331    this.activatePane(initialPane)
332
333    if (initialPane === "sell" && savedState.cardOpen && savedState.cardUrl && !hasServerChatsDeeplink) {
334      this.openCardAtUrl(savedState.cardUrl)
335      return
336    }
337
338    if (this.initialCardOpenValue) {
339      this.openCard()
340    }
341  }
342
343  maybeOpenChatsFromDeeplink() {
344    if (!this.hasChatsDeeplinkValue) {
345      return
346    }
347    const d = this.chatsDeeplinkValue
348    if (!d || !d.itemId) {
349      return
350    }
351
352    setTimeout(() => {
353      this.closeCard()
354      this.activatePane("sell", { explicit: true })
355      const u = new URL("/app_v2/items/" + encodeURIComponent(d.itemId) + "/chats", window.location.origin)
356      if (d.threadId) {
357        u.searchParams.set("thread_id", d.threadId)
358      }
359      u.searchParams.set("_chats_request_id", String(Date.now()))
360      this.openChatsAtUrl(u.toString(), { showLoading: true })
361      this.clearOpenChatsQueryParams()
362    }, 0)
363  }
364
365  clearOpenChatsQueryParams() {
366    const sp = new URLSearchParams(window.location.search)
367    if (!sp.has("open_chats")) {
368      return
369    }
370    const c = new URL(window.location)
371    c.searchParams.delete("open_chats")
372    c.searchParams.delete("item_id")
373    c.searchParams.delete("thread_id")
374    c.searchParams.delete("collection_id")
375    c.searchParams.delete("restore_pane")
376    const q = c.searchParams.toString()
377    const next = c.pathname + (q ? "?" + q : "")
378    window.history.replaceState(window.history.state, "", next)
379  }
380
381  openCardAtUrl(url, options = {}) {
382    const showLoading = options.showLoading !== false
383    const loadingText = options.loadingText || "Loading item details..."
384
385    if (showLoading) {
386      this.showCardLoading(loadingText)
387    }
388
389    this.openCard()
390
391    if (!url) return
392
393    this.loadCardFrame(url)
394  }
395
396  loadCardFrame(url) {
397    if (!this.hasCardFrameTarget) return
398
399    this.persistState({ cardUrl: url, cardOpen: true })
400
401    // Always force a fresh request when opening a sell card.
402    // This avoids stale frame reuse when rapidly opening different items.
403    const requestUrl = new URL(url, window.location.origin)
404    requestUrl.searchParams.set("_card_request_id", Date.now().toString())
405    this.cardFrameTarget.setAttribute("src", requestUrl.toString())
406  }
407
408  toggleDrawer(target) {
409    if (!target) return
410
411    const shouldOpen = !target.classList.contains("open")
412    this.closeMenus()
413
414    if (shouldOpen) {
415      target.classList.add("open")
416    }
417
418    this.updateOverlayVisibility()
419  }
420
421  anyMenuOpen() {
422    const leftOpen = this.hasMenuTarget && this.menuTarget.classList.contains("open")
423    const rightOpen = this.hasCollectionsMenuTarget && this.collectionsMenuTarget.classList.contains("open")
424
425    return leftOpen || rightOpen
426  }
427
428  updateOverlayVisibility() {
429    if (!this.hasOverlayTarget) return
430
431    this.overlayTarget.classList.toggle("is-visible", this.anyMenuOpen())
432  }
433
434  normalizePane(pane) {
435    return ["snap", "sell", "images"].includes(pane) ? pane : "snap"
436  }
437
438  normalizeBrowserUrl() {
439    if (!this.hasRootUrlValue) return
440    if (window.location.pathname === this.rootUrlValue) return
441
442    window.history.replaceState(window.history.state, "", this.rootUrlValue)
443  }
444
445  shouldRestoreSavedState() {
446    const navigationEntry = window.performance?.getEntriesByType?.("navigation")?.[0]
447    const navigationType = navigationEntry?.type
448
449    return navigationType === "reload" || navigationType === "back_forward"
450  }
451
452  readState() {
453    try {
454      const rawState = window.sessionStorage.getItem(APP_SHELL_STATE_KEY)
455      return rawState ? JSON.parse(rawState) : {}
456    } catch (error) {
457      console.warn("Unable to restore app shell state", error)
458      return {}
459    }
460  }
461
462  persistState(attributes = {}) {
463    try {
464      const nextState = {
465        ...this.readState(),
466        ...attributes
467      }
468
469      window.sessionStorage.setItem(APP_SHELL_STATE_KEY, JSON.stringify(nextState))
470    } catch (error) {
471      console.warn("Unable to persist app shell state", error)
472    }
473  }
474}

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.