kodadot/nft-gallery

View on GitHub
components/gallery/GalleryItem.vue

Summary

Maintainability
Test Coverage
<template>
  <section class="py-5 gallery-item">
    <MessageNotify
      v-if="congratsNewNft"
      :title="$t('mint.success')"
      :subtitle="$t('mint.successCreateNewNft', [congratsNewNft])"
    />
    <div class="flex flex-col lg:flex-row">
      <div class="w-full lg:w-2/5 lg:pr-7 group">
        <div
          :id="CONTAINER_ID"
          ref="imgref"
          :class="{
            'relative': !isFullscreen,
            'fullscreen-fallback': isFallbackActive,
          }"
        >
          <NeoButton
            v-if="isFullscreen"
            class="back-button z-20"
            @click="toggleFullscreen"
          >
            <NeoIcon icon="chevron-left" />
            {{ $t('go back') }}
          </NeoButton>
          <BaseMediaItem
            :key="image"
            ref="mediaItemRef"
            class="gallery-item-media relative"
            :src="getMediaSrc(image)"
            :animation-src="nftAnimation"
            :mime-type="(nftAnimation && nftAnimationMimeType) || nftMimeType"
            :title="nftMetadata?.name"
            :is-fullscreen="isFullscreen"
            is-detail
            :is-lewd="galleryDescriptionRef?.isLewd"
            :placeholder="placeholder"
            :image-component="NuxtImg"
            :sizes="sizes"
            enable-normal-tag
            :audio-player-cover="image"
          />
        </div>
        <GalleryItemToolBar
          :container-id="CONTAINER_ID"
          @toggle="toggleFullscreen"
        />
      </div>

      <div class="w-full lg:w-3/5 lg:pl-5 py-7">
        <div class="flex flex-col justify-between h-full">
          <!-- title section -->
          <div class="pb-2">
            <div class="flex justify-between">
              <div class="name-container">
                <h1
                  class="title"
                  data-testid="item-title"
                >
                  {{ title }}
                  <span
                    v-if="nft?.burned"
                    class="text-k-red"
                  >「🔥」</span>
                </h1>
                <h2
                  class="subtitle"
                  data-testid="item-collection"
                >
                  <CollectionDetailsPopover
                    v-if="nft?.collection.id"
                    :collection="collection"
                    :nft="nft"
                  >
                    <template #content>
                      <nuxt-link
                        :to="`/${urlPrefix}/collection/${collection?.id}`"
                        class="text-k-blue hover:text-k-blue-hover"
                        data-testid="gallery-item-collection-link"
                      >
                        {{ collection?.name || collection?.id }}
                      </nuxt-link>
                    </template>
                  </CollectionDetailsPopover>
                </h2>
              </div>
              <GalleryItemButton v-if="!nft?.burned" />
            </div>

            <div
              class="text-neutral-7 flex items-center"
              :class="isMobile ? 'my-4' : 'my-6'"
            >
              <NeoIcon
                pack="fasl"
                icon="eye"
                class="mr-1"
              />
              <span v-if="pageViewCount === null">--</span>
              <span v-else>{{ formatNumber(pageViewCount) }}</span>
            </div>

            <div class="flex flex-row flex-wrap">
              <IdentityItem
                v-if="nftCreator"
                class="gallery-avatar"
                :class="isMobile ? 'mr-4' : 'mr-8'"
                :label="$t(nft?.dropCreator ? 'collectionCreator' : 'creator')"
                :prefix="urlPrefix"
                :account="nftCreator"
                data-testid="item-creator"
              />
              <IdentityItem
                v-if="nft?.currentOwner !== nftCreator"
                class="gallery-avatar"
                :label="$t('owner')"
                :prefix="urlPrefix"
                :account="nft?.currentOwner || ''"
                data-testid="item-owner"
              />
            </div>
          </div>

          <!-- LINE DIVIDER -->
          <hr>
          <template v-if="!nft?.burned">
            <UnlockableTag
              v-if="isUnlockable && isMobile"
              :nft="nft"
              :link="unlockLink"
              class="mt-4"
            />

            <!-- price section -->
            <GalleryItemHolderOf
              v-if="nft && isAssetHub"
              :nft="nft"
            />
            <GalleryItemAction
              :nft="nft"
              :highest-offer="nftHighestOffer"
            />
            <UnlockableTag
              v-if="isUnlockable && !isMobile"
              :link="unlockLink"
              :nft="nft"
              class="mt-7"
            />
          </template>
        </div>
      </div>
    </div>

    <div class="flex flex-col lg:flex-row gap-8 mt-8 lg:pb-2">
      <div class="w-full lg:w-2/5 lg:pr-4">
        <GalleryItemDescription ref="galleryDescriptionRef" />
      </div>

      <div class="w-full lg:w-3/5 gallery-item-tabs-panel-wrapper">
        <GalleryItemTabsPanel :active-tab="activeTab" />
      </div>
    </div>

    <CarouselTypeRelated
      v-if="nft?.collection.id"
      class="mt-10"
      :collection-id="nft?.collection.id"
      data-testid="carousel-related"
    />

    <CarouselTypeVisited class="mt-10" />
  </section>
</template>

<script setup lang="ts">
import { NeoButton, NeoIcon } from '@kodadot1/brick'
import { useFullscreen, useWindowSize } from '@vueuse/core'
import GalleryItemAction from './GalleryItemAction/GalleryItemAction.vue'
import GalleryItemButton from './GalleryItemButton/GalleryItemButton.vue'
import GalleryItemDescription from './GalleryItemDescription.vue'
import GalleryItemTabsPanel from './GalleryItemTabsPanel/GalleryItemTabsPanel.vue'
import UnlockableTag from './UnlockableTag.vue'
import { useGalleryItem } from './useGalleryItem'
import { GALLERY_ITEM_TABS } from '@/components/gallery/GalleryItemTabsPanel/types'
import CollectionDetailsPopover from '@/components/collectionDetailsPopover/CollectionDetailsPopover.vue'
import { MediaType } from '@/components/rmrk/types'
import { usePreferencesStore } from '@/stores/preferences'
import { exist } from '@/utils/exist'
import { formatBalanceEmptyOnZero, formatNumber } from '@/utils/format/balance'
import { resolveMedia } from '@/utils/gallery/media'
import { sanitizeIpfsUrl, toOriginalContentUrl } from '@/utils/ipfs'
import { convertMarkdownToText } from '@/utils/markdown'
import { generateNftImage } from '@/utils/seoImageGenerator'

const CONTAINER_ID = 'nft-img-container'

const NuxtImg = resolveComponent('NuxtImg')

const { urlPrefix } = usePrefix()
const { isAssetHub } = useIsChain(urlPrefix)
const route = useRoute()
const router = useRouter()
const { placeholder } = useTheme()
const mediaItemRef = ref<{
  isLewdBlurredLayer: boolean
  toggleFullscreen
} | null>(null)
const galleryDescriptionRef = ref<{ isLewd: boolean } | null>(null)
const preferencesStore = usePreferencesStore()
const { getTriggerBuySuccess: triggerBuySuccess, getTriggerOfferSuccess: triggerOfferSuccess } = storeToRefs(preferencesStore)
const pageViewCount = usePageViews()
const fiatStore = useFiatStore()

const { getNft: nft, getNftMetadata: nftMetadata, getNftImage: nftImage, getNftMimeType: nftMimeType, getNftAnimation: nftAnimation, getNftAnimationMimeType: nftAnimationMimeType } = storeToRefs(useNftStore())

const { nftHighestOffer } = useGalleryItem()
const collection = computed(() => nft.value?.collection)

const nftCreator = computed(() => nft.value?.dropCreator || nft.value?.issuer)

const breakPointWidth = 930
const isMobile = computed(() => useWindowSize().width.value < breakPointWidth)

const activeTab = ref(GALLERY_ITEM_TABS.ACTIVITY)

const onNFTBought = () => {
  activeTab.value = GALLERY_ITEM_TABS.ACTIVITY
}

const image = computed(() => {
  if (!nftImage.value) {
    return sanitizeIpfsUrl(nft.value?.meta?.image)
  }

  return nftImage.value
})

const getMediaSrc = (src: string | undefined) =>
  src && isFullscreen.value ? toOriginalContentUrl(src) : src

watch(triggerBuySuccess, (value, oldValue) => {
  if (value && !oldValue) {
    onNFTBought()
    preferencesStore.setTriggerBuySuccess(false)
  }
})

watch(triggerOfferSuccess, (value) => {
  if (value) {
    activeTab.value = GALLERY_ITEM_TABS.OFFERS
    preferencesStore.setTriggerOfferSuccess(false)
  }
})

const congratsNewNft = ref('')

onMounted(() => {
  exist(route.query.congratsNft as string, (val) => {
    congratsNewNft.value = val || ''
    router.replace({ query: {} })
  })
})

const { isUnlockable, unlockLink } = useUnlockable(collection)

const title = computed(() =>
  nftMetadata.value?.name || nft.value?.name || '',
)
const seoDescription = computed(
  () => convertMarkdownToText(nftMetadata.value?.description) || '',
)
const seoCard = computed(() => {
  if (nft.value) {
    return generateNftImage(
      title.value,
      formatBalanceEmptyOnZero(nft.value?.price as string),
      sanitizeIpfsUrl(nftImage.value || ''),
      nftMimeType.value,
    )
  }

  return ''
})

useSeoMeta({
  title,
  description: seoDescription,
  ogTitle: title,
  ogDescription: seoDescription,
  ogImage: seoCard,
  twitterImage: seoCard,
  twitterCard: 'summary_large_image',
})

const imgref = ref<HTMLElement | null>(null)
const isFallbackActive = ref(false)
const fullScreenDisabled = ref(false)
const { toggle, isFullscreen, isSupported } = useFullscreen(imgref)
const sizes = ref<string>('1000px')

watch(isFullscreen, (value) => {
  sizes.value = value ? 'original' : '1000px'
})

function toggleMediaFullscreen() {
  if (!isSupported.value || fullScreenDisabled.value) {
    toggleFallback()
    return
  }
  toggle().catch(() => {
    fullScreenDisabled.value = true
    toggleFallback()
  })
}

function toggleFullscreen() {
  const mediaType = resolveMedia(nftAnimationMimeType.value)
  if ([MediaType.VIDEO].includes(mediaType)) {
    mediaItemRef.value?.toggleFullscreen()
  }
  else {
    toggleMediaFullscreen()
  }
}

function toggleFallback() {
  if (imgref.value) {
    const mainElement = document.querySelector('main')
    const isCurrentlyFullscreen = imgref.value.classList.toggle(
      'fullscreen-fallback',
    )
    mainElement?.classList.toggle('!z-[unset]')
    isFallbackActive.value = isCurrentlyFullscreen
    isFullscreen.value = isCurrentlyFullscreen
  }
}

onBeforeMount(() => fiatStore.fetchFiatPrice())
</script>

<style lang="scss">
@import '@/assets/styles/abstracts/variables';
#nft-img-container:fullscreen,
#nft-img-container.fullscreen-fallback {
  @include ktheme() {
    background-color: theme('background-color');
  }
  .media-object {
    @include ktheme() {
      box-shadow: none;
      border: none;
    }
  }

  img {
    width: 100vw;
    height: 100vh;
    object-fit: contain;
  }
}
</style>

<style lang="scss" scoped>
@import '@/assets/styles/abstracts/variables';
$break-point-width: 930px;
.title {
  font-size: 2.4375em;
}

.name-container {
  max-width: 75%;
}

.gallery-item-tabs-panel-wrapper {
  margin-top: unset;
  height: 100%;
}

@media screen and (max-width: 768px) {
  .gallery-item-tabs-panel-wrapper {
    margin-top: 1.25rem;
  }
}

.back-button {
  @apply fixed z-[1] left-3 top-8;
  @include desktop {
    left: $fluid-container-padding;
  }
}

#nft-img-container.fullscreen-fallback {
  @apply fixed w-screen h-screen z-[9999] left-0 top-0;
}

.fullscreen-button {
  @apply absolute z-[2] hidden w-[35px] h-[35px] border border-solid right-11 top-8;
  @include ktheme() {
    background-color: rgba(theme('background-color'), 0.15);
    border-color: rgba(theme('background-color'), 0.3);
    color: theme('text-color');
  }
}

@media screen and (max-width: $break-point-width) {
  .fullscreen-button {
    display: flex;
  }
}

@media (hover: none) {
  .fullscreen-button {
    display: flex;
  }
}

.h-audio {
  height: 70%;
}

.gallery-item-carousel {
  :deep(.o-car) {
    .o-car__item {
      overflow: hidden;
    }

    .o-car__overlay {
      @include ktheme() {
        background: theme('background-color');
      }
    }

    .o-car__indicator {
      &__item {
        @apply rounded-[50%];

        @include ktheme() {
          background: theme('background-color-inverse');
          border: theme('background-color-inverse');
        }

        &--active {
          @include ktheme() {
            background: theme('k-primary');
            border: theme('k-primary');
          }
        }
      }
    }
  }
}
</style>