Задача №1

Використовуючи один з АРІ https://github.com/public-apis/public-apis#animals та функцію fetch організувати завантаження та відображення даних. Намагайтесь зробити це з використанням класів. Окремо клас для побудови розмітки. Окремо клас, який буде робити запити і повертати результати.


Рішення:

import CatCardManager from './cat_card_manager.js'

const cardManager = new CatCardManager({
  amount: 6,
  imgSrc: './images/placeholder_image.webp',
})

cardManager.render('.js-app')
import Field from './field.js'
import CatCard from './cat_card.js'

export default class CatCardManager {
  constructor(options) {
    this.options = {
      amount: 1,
      imgSrc: '',
      btnLabel: 'Get cats!',
      placeholder: 'Cats say:',
      alt: 'A cat',
      spinnerLabel: 'Loading...',
      styles: {
        main: 'u-flow-400',
        controls: 'u-flex u-flex-wrap u-gap-200',
        btn: 'u-flex-none',
        imgContainer: 'u-flex u-justify-center u-flex-wrap u-gap-200',
        imgFrame:
          'u-relative u-grid u-place-items-center u-aspect-3/4 u-is-5600 u-rounded-lg u-overflow-hidden',
        img: 'u-ibg u-object-cover u-text-none',
        spinner: 'u-text-yellow-400 u-absolute',
      },
      ...options,
    }
  }

  /** @param {string} label */
  #renderBtn(label) {
    const btnEl = document.createElement('button')
    btnEl.textContent = label
    // btnEl.type = 'button'
    btnEl.className = this.options.styles.btn

    return btnEl
  }

  #renderControls() {
    const getBtn = this.#renderBtn(this.options.btnLabel)

    const {$el: fieldEl, inputEl} = new Field({
      placeholder: this.options.placeholder,
    }).render()

    inputEl.classList.add('u-flex-auto', 'u-is-4000')

    const controlsEl = document.createElement('div')
    controlsEl.className = this.options.styles.controls
    controlsEl.append(fieldEl, getBtn)

    return {controlsEl, fieldEl, getBtn, inputEl}
  }

  /** @param {SubmitEvent} e */
  async handleSubmit(e) {
    e.preventDefault()

    // prevent multiple requests
    if (this.getBtn.disabled) return
    this.getBtn.disabled = true

    const phrase = this.inputEl.value.trim()

    await Promise.allSettled(
      this.catCards.map((card) => card.reloadImg(phrase)),
    )

    this.getBtn.disabled = false
  }

  /** @param {string} [cssSelector] */
  render(cssSelector) {
    this.$el = document.createElement('form')
    this.$el.className = this.options.styles.main
    this.$el.addEventListener('submit', this.handleSubmit.bind(this))

    const {controlsEl, inputEl, getBtn} = this.#renderControls()
    this.inputEl = inputEl
    this.getBtn = getBtn
    this.$el.append(controlsEl)

    this.catCards = Array.from(
      {length: this.options.amount},
      () =>
        new CatCard({imgSrc: this.options.imgSrc, styles: this.options.styles}),
    )
    const imgContainer = document.createElement('div')
    imgContainer.className = this.options.styles.imgContainer
    imgContainer.append(...this.catCards.map((card) => card.render()))
    this.$el.append(imgContainer)

    if (cssSelector) document.querySelector(cssSelector).append(this.$el)

    return this.$el
  }
}
export default class Field {
  options
  id = crypto.randomUUID()

  constructor(options) {
    this.options = {
      type: 'text',
      initValue: '',
      placeholder: '',
      autocomplete: true,
      ...options,
    }
  }

  #renderInput() {
    const inputEl = document.createElement('input')
    inputEl.type = this.options.type
    inputEl.value = this.options.initValue

    if (!this.options.autocomplete) inputEl.autocomplete = 'off'

    if (this.options.placeholder) inputEl.placeholder = this.options.placeholder

    return inputEl
  }

  #renderLabel() {
    const labelEl = document.createElement('label')
    labelEl.textContent = this.options.label

    return labelEl
  }

  /**
   * @param {HTMLInputElement} inputEl
   * @param {HTMLLabelElement} labelEl
   */
  #bindInputWithLabel(inputEl, labelEl) {
    inputEl.id = this.id
    labelEl.setAttribute('for', this.id)
  }

  /** @param {string} [cssSelector] */
  render(cssSelector) {
    this.$el = new DocumentFragment()

    const inputEl = this.#renderInput()
    this.$el.append(inputEl)

    if (this.options.label) {
      const labelEl = this.#renderLabel()
      this.#bindInputWithLabel(inputEl, labelEl)
      this.$el.prepend(labelEl)
    }

    if (cssSelector) document.querySelector(cssSelector).append(this.$el)

    return {$el: this.$el, inputEl}
  }
}
import CataasAPI from './cataas_api.js'
import Spinner from './spinner.js'

export default class CatCard {
  $el
  spinnerEl
  imgEl

  constructor(options) {
    this.options = {
      imgSrc: '',
      alt: 'A cat',
      spinnerLabel: 'Loading...',
      styles: {
        imgFrame:
          'u-relative u-grid u-place-items-center u-aspect-3/4 u-is-6000 u-rounded-lg u-overflow-hidden',
        img: 'u-ibg u-object-cover u-text-none',
        spinner: 'u-text-yellow-400 u-absolute',
      },
      ...options,
    }
  }

  #renderSpinner() {
    const spinnerEl = new Spinner({
      className: this.options.styles.spinner,
      label: this.options.spinnerLabel,
    }).render()

    return spinnerEl
  }

  #renderPicture() {
    const imgEl = document.createElement('img')

    imgEl.src = this.options.imgSrc
    imgEl.alt = this.options.alt
    imgEl.className = this.options.styles.img

    const imgFrameEl = document.createElement('div')
    imgFrameEl.className = this.options.styles.imgFrame
    imgFrameEl.append(imgEl)

    return {imgFrameEl, imgEl}
  }

  async reloadImg(phrase = '') {
    this.$el.append(this.spinnerEl)

    try {
      const imgURL =
        phrase !== ''
          ? await CataasAPI.getCatSays(phrase)
          : await CataasAPI.getCat()

      this.imgEl.src = imgURL
    } catch (error) {
      console.error(error)
    } finally {
      this.spinnerEl.remove()
    }
  }

  /** @param {string} [cssSelector] */
  render(cssSelector) {
    const {imgFrameEl, imgEl} = this.#renderPicture()
    this.$el = imgFrameEl
    this.imgEl = imgEl

    this.spinnerEl = this.#renderSpinner()

    if (cssSelector) document.querySelector(cssSelector).append(this.$el)

    return this.$el
  }
}
export default class CataasAPI {
  static API_KEY = 'https://cataas.com/'

  /** @param {string} endPoint */
  static async #getURL(endPoint) {
    const resp = await fetch(CataasAPI.API_KEY + endPoint)

    if (!resp.ok) throw new Error(`${resp.status} – bad response.`)

    const blob = await resp.blob()

    return URL.createObjectURL(blob)
  }

  static getCat() {
    return this.#getURL('cat')
  }

  /** @param {string} text */
  static getCatSays(text) {
    return this.#getURL(`cat/says/${text}`)
  }
}
export default class Spinner {
  options
  $el

  constructor(options = {}) {
    this.options = {
      className: '',
      hint: 'Loading...',
      ...options,
    }
  }

  /** @param {string} [cssSelector] */
  render(cssSelector) {
    this.$el = document.createElement('div')
    this.$el.className = `c-spinner ${this.options.className}`
    this.$el.setAttribute('aria-live', 'polite')

    const hint = document.createElement('span')
    hint.textContent = this.options.hint
    hint.className = 'c-spinner__hint'

    this.$el.append(hint)

    if (cssSelector) document.querySelector(cssSelector).append(this.$el)

    return this.$el
  }
}