kodadot/nft-gallery

View on GitHub
components/collection/CollectionGrid.vue

Summary

Maintainability
Test Coverage
<template>
  <div class="collections">
    <LoadPreviousPage
      v-if="startPage > 1 && !isLoading && total > 0"
      @click="reachTopHandler"
    />

    <DynamicGrid
      v-if="!isLoading && total"
      :id="scrollContainerId"
      v-slot="{ cols }"
      grid-size="medium"
      :default-width="GRID_DEFAULT_WIDTH"
    >
      <div
        v-for="(collection, index) in collections"
        :key="collection.id"
        :class="scrollItemClassName"
        :data-testid="`collection-index-${index}`"
      >
        <CollectionCard
          :collection="collection"
          :lazy-loading="
            shouldLazyLoad({ index, cols, limit: first, skipRows: 3 })
          "
        />
      </div>
    </DynamicGrid>

    <DynamicGrid
      v-else-if="isLoading || loadingOtherNetwork"
      :id="scrollContainerId"
      grid-size="medium"
      :default-width="GRID_DEFAULT_WIDTH"
    >
      <CollectionCard
        v-for="n in skeletonCount"
        :key="n"
        is-loading
      />
    </DynamicGrid>

    <template v-else>
      <slot
        v-if="slots['empty-result']"
        name="empty-result"
      />
      <EmptyResult v-else />
    </template>

    <ScrollTopButton />
  </div>
</template>

<script lang="ts" setup>
import isEqual from 'lodash/isEqual'
import type { Collection } from '@/types'
import type { SearchQuery } from '@/components/search/types'
import collectionListWithSearch from '@/queries/subsquid/general/collectionListWithSearch.graphql'
import collectionListWithSearchProfile from '@/queries/subsquid/general/collectionListWithSearchProfile.graphql'
import { getDenyList } from '@/utils/prefix'
import CollectionCard from '@/components/collection/CollectionCard.vue'
import { GRID_DEFAULT_WIDTH } from '@/components/collection/utils/constants'
import { usePreferencesStore } from '@/stores/preferences'
import DynamicGrid from '@/components/shared/DynamicGrid.vue'

const emit = defineEmits(['total', 'isLoading'])
const props = defineProps<{
  id?: string
  loadingOtherNetwork?: boolean
}>()

const slots = useSlots()
const route = useRoute()
const { accountId } = useAuth()
const { urlPrefix, client } = usePrefix()
const preferencesStore = usePreferencesStore()

const isProfilePage = route.name === 'prefix-u-id'
const collections = ref<Collection[]>([])
const loadedPages = ref<number[]>([])
const isLoading = ref(true)

const searchQuery = reactive<SearchQuery>({
  search: '',
  sortBy: undefined,
  type: route.query?.type?.toString() ?? '',
  listed: route.query?.listed?.toString() === 'true',
})

const resetPage = useDebounceFn(() => {
  gotoPage(1)
}, 1000)

const buildSearchParam = (): Record<string, unknown>[] => {
  const params: any[] = []
  if (searchQuery.search) {
    params.push({
      name_containsInsensitive: searchQuery.search,
    })
  }

  if (searchQuery.listed) {
    params.push({ nfts_some: { price_gt: '0' } })
  }

  return params
}

onBeforeMount(() => {
  fetchPageData(startPage.value)
  // setting the default layout until redesign explorer menubar: YOLO
  preferencesStore.setLayoutClass('is-one-quarter-desktop is-one-third-tablet')
})

const getQueryVariables = (page: number) => {
  const searchParams = isProfilePage
    ? { currentOwner_eq: props.id, burned_eq: false }
    : { issuer_eq: props.id }

  if (isProfilePage && accountId.value !== props.id) {
    Object.assign(searchParams, { nftCount_not_eq: 0 })
  }

  return props.id
    ? {
        search: [searchParams],
        first: first.value,
        offset: (page - 1) * first.value,
        orderBy: searchQuery.sortBy,
      }
    : {
        denyList: getDenyList(urlPrefix.value),
        orderBy: searchQuery.sortBy,
        search: buildSearchParam(),
        listed: searchQuery.listed ? [{ price: { greaterThan: '0' } }] : [],
        first: first.value,
        offset: (page - 1) * first.value,
      }
}

const fetchPageData = async (page: number, loadDirection = 'down') => {
  if (isFetchingData.value) {
    return false
  }

  const variables = getQueryVariables(page)

  isFetchingData.value = true

  const { data: result } = await useAsyncQuery({
    query: isProfilePage
      ? collectionListWithSearchProfile
      : collectionListWithSearch,
    variables: variables,
    clientId: client.value,
  })

  handleResult(result.value, { page, loadDirection, variables })

  isFetchingData.value = false

  return true
}

const gotoPage = (page: number) => {
  currentPage.value = page
  startPage.value = page
  endPage.value = page
  collections.value = []
  loadedPages.value = []
  isFetchingData.value = false
  isLoading.value = true
  fetchPageData(page)
}
const {
  first,
  total,
  startPage,
  endPage,
  currentPage,
  scrollItemClassName,
  isFetchingData,
  scrollContainerId,
  reachTopHandler,
} = useListInfiniteScroll({
  gotoPage,
  fetchPageData,
})

const skeletonCount = first.value

const handleResult = (
  data,
  {
    loadDirection = 'down',
    page,
    variables,
  }: { loadDirection?: string, page: number, variables: object },
) => {
  total.value = data.stats.totalCount

  const isLatestPageResult = isEqual(variables, getQueryVariables(page))

  if (loadedPages.value.includes(page) || !isLatestPageResult) {
    return
  }

  const newCollections = data.collectionEntities.map((e: any) => ({
    ...e,
  }))

  if (loadDirection === 'up') {
    collections.value = newCollections.concat(collections.value)
  }
  else {
    collections.value = collections.value.concat(newCollections)
  }

  loadedPages.value.push(page)
  isLoading.value = false
}

watch(total, val => emit('total', val))
watch(isLoading, val => emit('isLoading', val))

watch(
  () => route.query.search,
  (search, prevSearch) => {
    if (!isEqual(search, prevSearch)) {
      searchQuery.search = search?.toString() ?? ''
    }
  },
  { immediate: true },
)

watch(
  () => route.query.sort,
  (sort, prevSort) => {
    if (!isEqual(sort, prevSort)) {
      searchQuery.sortBy = sort ? parseQueryParamToArray(sort) : undefined
    }
  },
  { immediate: true },
)

watch(searchQuery, () => resetPage())
</script>

<style lang="scss" scoped>
.skeleton-container-fixed-width {
  width: 80px;
}
</style>