PageSourceSearch

https://www.nihondo.co.jp/assets/js/cms-shop-stores.js

js nihondo.co.jp collected 2026-10-02 10:17:53 UTC 23,939 bytes, 718 lines download raw bytes

1import { getCmsApiBaseUrl } from './cms-config.js'
2
3const AREA_ORDER = [
4  '関東エリア',
5  '北海道・東北エリア',
6  '北信越・東海エリア',
7  '関西エリア',
8  '中国・四国エリア',
9  '九州・沖縄エリア',
10]
11
12const PREFECTURE_ORDER = [
13  '東京都',
14  '神奈川県',
15  '埼玉県',
16  '千葉県',
17  '茨城県',
18  '栃木県',
19  '群馬県',
20  '北海道',
21  '青森県',
22  '岩手県',
23  '宮城県',
24  '秋田県',
25  '山形県',
26  '福島県',
27  '新潟県',
28  '富山県',
29  '石川県',
30  '福井県',
31  '山梨県',
32  '長野県',
33  '岐阜県',
34  '静岡県',
35  '愛知県',
36  '三重県',
37  '滋賀県',
38  '京都府',
39  '大阪府',
40  '兵庫県',
41  '奈良県',
42  '和歌山県',
43  '鳥取県',
44  '島根県',
45  '岡山県',
46  '広島県',
47  '山口県',
48  '徳島県',
49  '香川県',
50  '愛媛県',
51  '高知県',
52  '福岡県',
53  '佐賀県',
54  '長崎県',
55  '熊本県',
56  '大分県',
57  '宮崎県',
58  '鹿児島県',
59  '沖縄県',
60]
61
62const SHOP_CATEGORY_SLUGS = ['boutique', 'pharmacy', 'inside-sugi']
63
64const normalizePath = () => window.location.pathname.replace(/\/index\.html$/, '/')
65
66const isShopIndexPage = () => normalizePath().endsWith('/shop/')
67
68const shopCategorySlug = () => {
69  const parts = normalizePath().split('/').filter(Boolean)
70  if (parts.length === 2 && parts[0] === 'shop' && SHOP_CATEGORY_SLUGS.includes(parts[1])) {
71    return parts[1]
72  }
73  return ''
74}
75
76const isShopCategoryPage = () => Boolean(shopCategorySlug())
77
78const isShopDetailPage = () => document.body.classList.contains('page-shop-detail')
79
80const assetBase = () => {
81  if (document.body.dataset.assetBase) {
82    return document.body.dataset.assetBase.replace(/\/$/, '')
83  }
84
85  const parts = normalizePath().split('/').filter(Boolean)
86  return parts.length ? Array(parts.length).fill('..').join('/') : '.'
87}
88
89const apiBase = () => (getCmsApiBaseUrl() || document.body.dataset.cmsApiUrl || '').replace(/\/$/, '')
90
91const assetUrl = (path) => {
92  if (!path) return ''
93  if (/^https?:\/\//.test(path)) return path
94  return `${assetBase()}/${String(path).replace(/^\/+/, '')}`
95}
96
97const currentShopSlug = () => {
98  if (document.body.dataset.shopSlug) return document.body.dataset.shopSlug
99  const parts = normalizePath().split('/').filter(Boolean)
100  return parts[parts.length - 1] || ''
101}
102
103const fetchStores = async () => {
104  const response = await fetch(`${apiBase()}/shop-stores`, { headers: { Accept: 'application/json' } })
105  if (!response.ok) throw new Error(`shop stores api error: ${response.status}`)
106  return response.json()
107}
108
109const fetchStore = async (slug) => {
110  const response = await fetch(`${apiBase()}/shop-stores/${encodeURIComponent(slug)}`, { headers: { Accept: 'application/json' } })
111  if (!response.ok) throw new Error(`shop store api error: ${response.status}`)
112  return response.json()
113}
114
115const setShopIndexLoading = (isLoading) => {
116  if (!isShopIndexPage()) return
117
118  document.querySelectorAll('.area-nav__groups, .l-section').forEach((element) => {
119    if (element.querySelector('.store-area') || element.classList.contains('area-nav__groups')) {
120      element.style.visibility = isLoading ? 'hidden' : ''
121    }
122  })
123}
124
125const setShopCategoryLoading = (isLoading) => {
126  if (!isShopCategoryPage()) return
127
128  document.querySelectorAll('.l-section, .l-stack').forEach((element) => {
129    if (element.querySelector('.store-area, .store-card')) {
130      element.style.visibility = isLoading ? 'hidden' : ''
131    }
132  })
133}
134
135const setShopDetailLoading = (isLoading) => {
136  if (!isShopDetailPage()) return
137
138  document.querySelectorAll('.shop-detail__head, .shop-detail__hero, .shop-detail > .shop-detail__section').forEach((element) => {
139    element.style.visibility = isLoading ? 'hidden' : ''
140  })
141}
142
143const orderIndex = (list, value) => {
144  const index = list.indexOf(value)
145  return index === -1 ? list.length : index
146}
147
148const sortAreas = (areas) => [...areas].sort((a, b) => orderIndex(AREA_ORDER, a) - orderIndex(AREA_ORDER, b) || a.localeCompare(b, 'ja'))
149
150const sortPrefectures = (prefectures) => [...prefectures].sort((a, b) => orderIndex(PREFECTURE_ORDER, a) - orderIndex(PREFECTURE_ORDER, b) || a.localeCompare(b, 'ja'))
151
152const textWithBreaks = (element, value) => {
153  String(value || '').split(/\r?\n/).forEach((line, index) => {
154    if (index > 0) element.appendChild(document.createElement('br'))
155    element.appendChild(document.createTextNode(line))
156  })
157}
158
159const icon = (name) => {
160  const element = document.createElement('span')
161  element.className = `c-icon c-icon--${name}`
162  element.setAttribute('aria-hidden', 'true')
163  return element
164}
165
166const appendLinkText = (link, text, iconName) => {
167  const label = document.createElement('span')
168  label.className = 'btn__text'
169  label.textContent = text
170  link.appendChild(label)
171  link.appendChild(icon(iconName))
172}
173
174const createStoreCard = (store) => {
175  const item = document.createElement('li')
176  item.className = 'card-grid__item'
177
178  const article = document.createElement('article')
179  article.className = 'store-card'
180
181  const badge = document.createElement('span')
182  badge.className = `brand-badge ${store.brand_badge_class || 'brand-badge--nihondo'}`
183  badge.textContent = store.brand_label || ''
184  article.appendChild(badge)
185
186  const name = document.createElement('a')
187  name.className = 'store-card__name'
188  name.href = store.url || '#'
189  name.textContent = store.name || ''
190  if (store.detail_type === 'external') {
191    name.target = '_blank'
192    name.rel = 'noopener noreferrer'
193  }
194  article.appendChild(name)
195
196  const info = document.createElement('dl')
197  info.className = 'store-card__info'
198  ;[
199    ['住所', [store.postal_code, store.address].filter(Boolean).join(' ')],
200    ['アクセス', store.access_summary],
201    ['電話番号', store.tel],
202    ['営業時間', store.business_hours],
203    ['定休日', store.holiday],
204  ].forEach(([label, value]) => {
205    if (!value) return
206    const row = document.createElement('div')
207    row.className = 'store-card__row'
208    const dt = document.createElement('dt')
209    dt.textContent = label
210    const dd = document.createElement('dd')
211    textWithBreaks(dd, value)
212    row.appendChild(dt)
213    row.appendChild(dd)
214    info.appendChild(row)
215  })
216  article.appendChild(info)
217
218  const tags = document.createElement('div')
219  tags.className = 'store-card__tags'
220  if (store.has_kampo_consultation) {
221    const tag = document.createElement('span')
222    tag.className = 'store-card__tag store-card__tag--soudan'
223    tag.textContent = '漢方相談'
224    tags.appendChild(tag)
225  }
226  if (store.has_product_sales) {
227    const tag = document.createElement('span')
228    tag.className = 'store-card__tag store-card__tag--bukken'
229    tag.textContent = '物販'
230    tags.appendChild(tag)
231  }
232  if (store.is_tax_free) {
233    const tax = document.createElement('span')
234    tax.className = 'store-card__tax'
235    tax.textContent = 'TAX FREE'
236    tags.appendChild(tax)
237  }
238  article.appendChild(tags)
239
240  const detail = document.createElement('a')
241  detail.className = 'btn btn--outline-dark store-card__detail'
242  detail.href = store.url || '#'
243  if (store.detail_type === 'external') {
244    detail.target = '_blank'
245    detail.rel = 'noopener noreferrer'
246  }
247  appendLinkText(detail, '店舗詳細へ', store.detail_type === 'external' ? 'north-east' : 'arrow-forward')
248  article.appendChild(detail)
249
250  item.appendChild(article)
251  return item
252}
253
254const renderShopIndex = (stores) => {
255  renderAreaNav(stores)
256
257  const firstStoreArea = document.querySelector('.store-area')
258  const firstSection = firstStoreArea?.closest('.l-section')
259  const parent = firstSection?.parentNode
260  if (!firstSection || !parent) return
261
262  parent.querySelectorAll('.l-section').forEach((section) => {
263    if (section.querySelector('.store-area')) section.remove()
264  })
265
266  if (!stores.length) return
267
268  const areas = sortAreas([...new Set(stores.map((store) => store.area_label).filter(Boolean))])
269  areas.forEach((area) => {
270    const areaSection = document.createElement('section')
271    areaSection.className = 'l-section'
272    const sectionHead = document.createElement('div')
273    sectionHead.className = 'section-head section-head--brown-700'
274    const h2 = document.createElement('h2')
275    h2.className = 'section-head__title'
276    h2.textContent = area
277    sectionHead.appendChild(h2)
278    areaSection.appendChild(sectionHead)
279
280    const areaStores = stores.filter((store) => store.area_label === area)
281    const prefectures = sortPrefectures([...new Set(areaStores.map((store) =>
281 store.prefecture).filter(Boolean))])
282    prefectures.forEach((prefecture) => {
283      const storeArea = document.createElement('div')
284      storeArea.className = 'store-area'
285      const h3 = document.createElement('h3')
286      h3.className = 'region-head'
287      h3.id = areaStores.find((store) => store.prefecture === prefecture)?.prefecture_slug || ''
288      h3.textContent = prefecture
289      const grid = document.createElement('ul')
290      grid.className = 'card-grid'
291      areaStores.filter((store) => store.prefecture === prefecture).forEach((store) => grid.appendChild(createStoreCard(store)))
292      storeArea.appendChild(h3)
293      storeArea.appendChild(grid)
294      areaSection.appendChild(storeArea)
295    })
296
297    parent.appendChild(areaSection)
298  })
299}
300
301const appendStoreGrid = (parent, stores) => {
302  const grid = document.createElement('ul')
303  grid.className = 'card-grid'
304  stores.forEach((store) => grid.appendChild(createStoreCard(store)))
305  parent.appendChild(grid)
306}
307
308const appendPrefectureStoreGroups = (parent, stores) => {
309  const prefectures = sortPrefectures([...new Set(stores.map((store) => store.prefecture).filter(Boolean))])
310  prefectures.forEach((prefecture) => {
311    const storeArea = document.createElement('div')
312    storeArea.className = 'store-area'
313    const h3 = document.createElement('h3')
314    h3.className = 'region-head'
315    h3.id = stores.find((store) => store.prefecture === prefecture)?.prefecture_slug || ''
316    h3.textContent = prefecture
317    const grid = document.createElement('ul')
318    grid.className = 'card-grid'
319    stores.filter((store) => store.prefecture === prefecture).forEach((store) => grid.appendChild(createStoreCard(store)))
320    storeArea.appendChild(h3)
321    storeArea.appendChild(grid)
322    parent.appendChild(storeArea)
323  })
324}
325
326const createStoreListSection = (stores, sectionClassName = 'l-section', options = {}) => {
327  const section = document.createElement('section')
328  section.className = sectionClassName
329
330  const sectionHead = document.createElement('div')
331  sectionHead.className = sectionClassName === 'l-stack'
332    ? 'section-head section-head--brown'
333    : 'section-head section-head--brown-700'
334  const h2 = document.createElement('h2')
335  h2.className = 'section-head__title'
336  h2.textContent = '店舗一覧'
337  sectionHead.appendChild(h2)
338  section.appendChild(sectionHead)
339
340  if (options.groupByPrefecture) {
341    appendPrefectureStoreGroups(section, stores)
342  } else {
343    appendStoreGrid(section, stores)
344  }
345
346  return section
347}
348
349const renderShopCategory = (stores) => {
350  const categorySlug = shopCategorySlug()
351  const categoryStores = stores.filter((store) => store.category_slug === categorySlug)
352  const firstStoreList = document.querySelector('.store-area, .store-card')
353  const firstSection = firstStoreList?.closest('.l-section, .l-stack')
354  const parent = firstSection?.parentNode
355  if (!firstSection || !parent) return
356
357  const sectionClassName = firstSection.classList.contains('l-stack') ? 'l-stack' : 'l-section'
358  parent.querySelectorAll('.l-section, .l-stack').forEach((section) => {
359    if (section.querySelector('.store-area, .store-card')) section.remove()
360  })
361
362  if (!categoryStores.length) return
363
364  parent.appendChild(createStoreListSection(categoryStores, sectionClassName, {
365    groupByPrefecture: categorySlug !== 'pharmacy',
366  }))
367}
368
369const renderAreaNav = (stores) => {
370  const navGroups = document.querySelector('.area-nav__groups')
371  if (!navGroups) return
372
373  navGroups.replaceChildren()
374  if (!stores.length) return
375
376  const fragment = document.createDocumentFragment()
377  const areas = sortAreas([...new Set(stores.map((store) => store.area_label).filter(Boolean))])
378
379  areas.forEach((area) => {
380    const group = document.createElement('div')
381    group.className = 'area-nav__group'
382
383    const label = document.createElement('p')
384    label.className = 'area-nav__label'
385    label.textContent = area.replace(/エリア$/, '')
386
387    const links = document.createElement('div')
388    links.className = 'area-nav__links'
389
390    const areaStores = stores.filter((store) => store.area_label === area)
391    const prefectures = sortPrefectures([...new Map(areaStores
392      .filter((store) => store.prefecture && store.prefecture_slug)
393      .map((store) => [store.prefecture, store.prefecture_slug])).keys()])
394
395    prefectures.forEach((prefecture) => {
396      const slug = areaStores.find((store) => store.prefecture === prefecture)?.prefecture_slug
397      if (!slug) return
398      const link = document.createElement('a')
399      link.className = 'area-nav__link'
400      link.href = `#${slug}`
401      link.appendChild(document.createTextNode(prefecture))
402      link.appendChild(icon('keyboard-arrow-down'))
403      links.appendChild(link)
404    })
405
406    group.appendChild(label)
407    group.appendChild(links)
408    fragment.appendChild(group)
409  })
410
411  navGroups.replaceChildren(fragment)
412}
413
414const createInfoRow = (label, value, linkHref = '') => {
415  if (!value) return null
416  const row = document.createElement('div')
417  row.className = 'shop-detail__info-row'
418  const dt = document.createElement('dt')
419  dt.className = 'shop-detail__info-dt'
420  dt.textContent = label
421  const dd = document.createElement('dd')
422  dd.className = 'shop-detail__info-dd'
423  if (linkHref) {
424    const link = document.createElement('a')
425    link.href = linkHref
426    link.textContent = value
427    dd.appendChild(link)
428  } else {
429    textWithBreaks(dd, value)
430  }
431  row.appendChild(dt)
432  row.appendChild(dd)
433  return row
434}
435
436const shopDetailName = (store) => store.detail_name || store.name || ''
437
438const createSection = (heading, content) => {
439  const section = document.createElement('section')
440  section.className = 'shop-detail__section'
441  const h2 = document.createElement('h2')
442  h2.className = 'shop-detail__heading'
443  h2.textContent = heading
444  section.appendChild(h2)
445  section.appendChild(content)
446  return section
447}
448
449const renderHero = (store) => {
450  const hero = document.querySelector('[data-shop-hero]') || document.querySelector('.shop-detail__hero')
451  if (!hero) return
452  const images = store.hero_images || []
453  hero.replaceChildren()
454  if (!images.length) return
455
456  if (images.length === 1) {
457    const image = document.createElement('img')
458    image.src = assetUrl(images[0].path)
459    image.alt = images[0].alt || shopDetailName(store)
460    image.width = 1200
461    image.height = 500
462    image.loading = 'lazy'
463    hero.appendChild(image)
464    return
465  }
466
467  const swiper = document.createElement('div')
468  swiper.className = 'shop-detail__hero-swiper swiper'
469  swiper.id = 'js-shop-hero-swiper'
470  const wrapper = document.createElement('div')
471  wrapper.className = 'swiper-wrapper'
472  images.forEach((heroImage) => {
473    const slide = document.createElement('div')
474    slide.className = 'swiper-slide'
475    const image = document.createElement('img')
476    image.src = assetUrl(heroImage.path)
477    image.alt = heroImage.alt || shopDetailName(store)
478    image.width = 1200
479    image.height = 500
480    image.loading = 'lazy'
481    slide.appendChild(image)
482    wrapper.appendChild(slide)
483  })
484  swiper.appendChild(wrapper)
485  hero.appendChild(swiper)
486}
487
488const renderDetailSections = (store) => {
489  let mount = document.querySelector('[data-shop-sections]')
490  const shopDetail = document.querySelector('.shop-detail')
491  const insertBefore = document.querySelector('.shop-detail__form') || document.querySelector('.shop-detail__back')
492
493  if (!mount && shopDetail && insertBefore) {
494    document.querySelectorAll('.shop-detail > .shop-detail__section').forEach((section) => section.remove())
495    mount = document.createElement('div')
496    mount.dataset.shopSections = ''
497    shopDetail.insertBefore(mount, insertBefore)
498  }
499
500  if (!mount) return
501  const fragment = document.createDocumentFragment()
502
503  const infoWrap = document.createElement('div')
504  infoWrap.className = 'shop-detail__info'
505  const dl = document.createElement('dl')
506  dl.className = 'shop-detail__info-dl'
507  ;[
508    createInfoRow('住所', [store.postal_code, store.address].filter(
508Boolean).join(' ')),
509    createInfoRow('TEL', store.tel, store.tel_link),
510    createInfoRow('メール', store.email, store.email ? `mailto:${store.email}` : ''),
511    createInfoRow('営業時間', store.business_hours),
512    createInfoRow('定休日', store.holiday),
513  ].filter(Boolean).forEach((row) => dl.appendChild(row))
514  ;(store.info_rows || []).forEach((row) => {
515    const infoRow = createInfoRow(row.label, row.value)
516    if (infoRow) dl.appendChild(infoRow)
517  })
518  if (store.is_tax_free) {
519    const row = createInfoRow('Tax-Free', 'JAPAN TAX-FREE SHOP')
520    const dd = row.querySelector('dd')
521    if (dd) {
522      dd.replaceChildren()
523      const badge = document.createElement('span')
524      badge.className = 'shop-detail__taxfree'
525      badge.textContent = 'JAPAN TAX-FREE SHOP'
526      dd.appendChild(badge)
527    }
528    dl.appendChild(row)
529  }
530  infoWrap.appendChild(dl)
531  if (store.map_embed_url) {
532    const map = document.createElement('div')
533    map.className = 'shop-detail__map'
534    const iframe = document.createElement('iframe')
535    iframe.src = store.map_embed_url
536    iframe.title = `${shopDetailName(store)} 地図`
537    iframe.allowFullscreen = true
538    iframe.loading = 'lazy'
539    iframe.referrerPolicy = 'no-referrer-when-downgrade'
540    map.appendChild(iframe)
541    infoWrap.appendChild(map)
542  }
543  fragment.appendChild(createSection('店舗情報', infoWrap))
544
545  if ((store.access_rows || []).length) {
546    const access = document.createElement('div')
547    access.className = 'shop-detail__access'
548    store.access_rows.forEach((text) => {
549      const item = document.createElement('div')
550      item.className = 'shop-detail__access-item'
551      const accessIcon = icon('train')
552      accessIcon.classList.add('shop-detail__access-icon')
553      const p = document.createElement('p')
554      p.className = 'shop-detail__access-text'
555      textWithBreaks(p, text)
556      item.appendChild(accessIcon)
557      item.appendChild(p)
558      access.appendChild(item)
559    })
560    fragment.appendChild(createSection('アクセス', access))
561  }
562
563  if ((store.message_paragraphs || []).length) {
564    const message = document.createElement('div')
565    message.className = 'shop-detail__message'
566    store.message_paragraphs.forEach((text) => {
567      const p = document.createElement('p')
568      textWithBreaks(p, text)
569      message.appendChild(p)
570    })
571    const section = createSection('店舗からのメッセージ', message)
572    const messageBanner = store.message_banner || {}
573    if (messageBanner.image_path) {
574      const banner = document.createElement('div')
575      banner.className = 'shop-detail__banner'
576      const image = document.createElement('img')
577      image.src = assetUrl(messageBanner.image_path)
578      image.alt = messageBanner.alt || ''
579      image.width = 345
580      image.height = 90
581      image.loading = 'lazy'
582
583      if (messageBanner.link_url) {
584        const link = document.createElement('a')
585        link.className = 'shop-detail__banner-link'
586        link.href = messageBanner.link_url
587        link.appendChild(image)
588        banner.appendChild(link)
589      } else {
590        banner.appendChild(image)
591      }
592
593      section.appendChild(banner)
594    }
595    fragment.appendChild(section)
596  }
597
598  if ((store.services || []).length) {
599    const services = document.createElement('div')
600    services.className = 'shop-detail__services'
601    store.services.forEach((service) => {
602      const item = document.createElement('div')
603      item.className = 'shop-detail__service'
604      const head = document.createElement('div')
605      head.className = 'shop-detail__service-head'
606      const serviceIcon = icon(service.icon || 'service-store')
607      serviceIcon.classList.add('shop-detail__service-icon')
608      const title = document.createElement('h3')
609      title.className = 'shop-detail__service-title'
610      title.textContent = service.title || ''
611      head.appendChild(serviceIcon)
612      head.appendChild(title)
613      const desc = document.createElement('p')
614      desc.className = 'shop-detail__service-desc'
615      desc.textContent = service.description || ''
616      item.appendChild(head)
617      item.appendChild(desc)
618      services.appendChild(item)
619    })
620    fragment.appendChild(createSection('提供サービス', services))
621  }
622
623  mount.replaceChildren(fragment)
624}
625
626const renderConsultTypes = (form, store) => {
627  const group = form.querySelector('input[name="consult_type"]')?.closest('.contact-form__radios')
628  if (!group || !(store.consult_types || []).length) return
629  group.replaceChildren()
630  store.consult_types.forEach((type) => {
631    const label = document.createElement('label')
632    label.className = 'contact-form__radio-label'
633    const input = document.createElement('input')
634    input.type = 'radio'
635    input.className = 'contact-form__radio'
636    input.name = 'consult_type'
637    input.value = type
638    input.dataset.validate = 'required'
639    input.dataset.label = 'ご相談形式'
640    const span = document.createElement('span')
641    span.textContent = type
642    label.appendChild(input)
643    label.appendChild(span)
644    group.appendChild(label)
645  })
646}
647
648const renderReservation = (store) => {
649  const reserveLink = document.querySelector('[data-shop-reserve-link], .shop-detail__reserve')
650  if (reserveLink) reserveLink.hidden = !store.has_reservation_form
651  const existingForm = document.querySelector('form[data-storage-key="shop-reservation"]')
652  if (!store.has_reservation_form) {
653    existingForm?.closest('.shop-detail__form')?.remove()
654    return
655  }
656  if (!existingForm) return
657
658  const fields = {
659    shop_name: shopDetailName(store),
660    shop_tel: store.tel || '',
661    shop_email: store.email || '',
662  }
663  Object.entries(fields).forEach(([name, value]) => {
664    const input = existingForm.querySelector(`input[name="${name}"]`)
665    if (input) input.value = value
666  })
667  renderConsultTypes(existingForm, store)
668}
669
670const renderShopDetail = (store) => {
671  const detailName = shopDetailName(store)
672  const title = document.querySelector('[data-shop-title], .shop-detail__title')
673  if (title) title.textContent = detailName
674  const breadcrumb = document.querySelector('[data-shop-breadcrumb-current]')
675  if (breadcrumb) breadcrumb.textContent = detailName
676  const lead = document.querySelector('[data-shop-lead], .shop-detail__sub')
677  if (lead) {
678    lead.textContent = store.lead_text || ''
679    lead.hidden = !store.lead_text
680  }
681  const brand = document.querySelector('[data-shop-brand], .shop-detail__badge')
682  if (brand) {
683    brand.className = `brand-badge ${store.brand_badge_class || 'brand-badge--nihondo'} shop-detail__badge`
684    brand.textContent = store.brand_label || ''
685  }
686  renderHero(store)
687  renderDetailSections(store)
688  renderReservation(store)
689}
690
691export async function cmsShopStores() {
692  setShopIndexLoading(true)
693  setShopCategoryLoading(true)
694  setShopDetailLoading(true)
695
696  try {
697    if (isShopIndexPage() || isShopCategoryPage()) {
698      const stores = await fetchStores()
699      if (Array.isArray(stores)) {
700        if (isShopIndexPage()) renderShopIndex(stores)
701        if (isShopCategoryPage()) renderShopCategory(stores)
702      }
703    }
704
705    if (isShopDetailPage()) {
706      const slug = currentShopSlug()
707      if (!slug) return
708      const store = await fetchStore(slug)
709      renderShopDetail(store)
710    }
711  } catch (error) {
712    console.warn(error)
713  } finally {
714    setShopIndexLoading(false)
715    setShopCategoryLoading(false)
716    setShopDetailLoading(false)
717  }
718}

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.