kodadot/nft-gallery

View on GitHub
components/profile/ProfileDetail.vue

Summary

Maintainability
Test Coverage
<template>
  <div>
    <ProfileFollowModal
      :key="`${followersCount}-${followingCount}`"
      v-model="isFollowModalActive"
      :initial-tab="followModalTab"
      :followers-count="followersCount"
      :following-count="followingCount"
      @close="refresh"
    />

    <ProfileBannerSkeleton v-if="isFetchingProfile" />

    <div
      v-else
      class="bg-no-repeat bg-cover bg-center md:h-[360px] h-40 border-b bg-neutral-3 dark:bg-neutral-11"
      :style="{
        backgroundImage:
          userProfile?.banner
            ? `url(${getHigherResolutionCloudflareImage(userProfile.banner)})`
            : undefined,
      }"
    >
      <div
        class="collection-banner-content flex md:items-end items-center h-full md:pb-10 max-sm:mx-5 mx-12 2xl:mx-auto max-w-[89rem]"
      >
        <div class="!rounded-full overflow-hidden p-2.5 bg-background-color border aspect-square">
          <BaseMediaItem
            v-if="userProfile?.image"
            :src="userProfile.image"
            :image-component="NuxtImg"
            :title="'User Avatar'"
            class="md:w-[124px] md:h-[124px] h-[78px] w-[78px] object-cover rounded-full"
            inner-class="object-cover"
          />
          <div
            v-else
            class="mb-[-7px]"
          >
            <Avatar
              :value="id"
              :size="78"
              class="md:hidden"
            />
            <Avatar
              :value="id"
              :size="124"
              class="max-md:hidden"
            />
          </div>
        </div>
      </div>
    </div>

    <div
      class="pt-6 pb-7 max-sm:mx-5 mx-12 2xl:mx-auto flex justify-between max-w-[89rem]"
    >
      <ProfileSkeleton v-if="isFetchingProfile" />

      <div
        v-else
        class="flex flex-col gap-6"
      >
        <!-- Identity Link -->
        <h1
          class="font-bold text-2xl md:text-[31px] mb-0"
          data-testid="profile-user-identity"
        >
          <span v-if="userProfile?.name">{{ userProfile.name }}</span>
          <Identity
            v-else
            ref="identity"
            hide-identity-popover
            :address="id"
            emit
            @change="handleIdentity"
          />
        </h1>

        <!-- Buttons and Dropdowns -->
        <div class="flex gap-3 flex-wrap">
          <ProfileButtonConfig
            v-if="isOwner"
            :button="buttonConfig"
            test-id="profile-button-multi-action"
          />
          <ProfileFollowButton
            v-else
            ref="followButton"
            :target="id"
            @follow:success="handleFollowRefresh"
            @follow:fail="openProfileCreateModal"
            @unfollow:success="handleFollowRefresh"
          />
          <NeoButton
            v-if="swapVisible(urlPrefix)"
            variant="outlined-rounded"
            icon-left="arrow-right-arrow-left"
            @click="handleSwapPageRedirect"
          >
            {{ $t('swaps') }}
          </NeoButton>

          <!-- Wallet And Links Dropdown -->
          <NeoDropdown position="bottom-auto">
            <template #trigger="{ active }">
              <NeoButton
                variant="outlined-rounded"
                data-testid="profile-wallet-links-button"
                :active="active"
                :icon-right="active ? 'chevron-up' : 'chevron-down'"
              >
                {{ $t('profile.walletAndLinks') }}
              </NeoButton>
            </template>
            <NeoDropdownItem class="hover:!bg-transparent hover:!cursor-default">
              <div class="flex flex-col gap-4 py-2.5">
                <!-- Copy Address -->
                <div class="flex items-center">
                  <Identity
                    hide-identity-popover
                    hide-display-name
                    :address="id"
                    show-onchain-identity
                    class="bg-neutral-3 dark:bg-neutral-9 text-base rounded-2xl text-center px-2"
                  />
                  <NeoButton
                    v-clipboard:copy="id"
                    variant="text"
                    no-shadow
                    icon="copy"
                    data-testid="profile-wallet-links-button-copy"
                    :icon-pack="'fas'"
                    class="ml-2.5"
                    @click="toast($t('general.copyAddressToClipboard'))"
                  />
                </div>
                <!-- View on Subscan and SubID -->
                <div class="flex items-center">
                  <NeoButton
                    v-safe-href="`https://subscan.io/account/${id}`"
                    no-shadow
                    variant="text"
                    class="text-xs"
                    :label="$t('profile.subscan')"
                    tag="a"
                    target="_blank"
                    rel="nofollow noopener noreferrer"
                  />
                  <span class="w-px h-1.5 bg-k-shade mx-2" />
                  <NeoButton
                    v-safe-href="`https://sub.id/#/${id}`"
                    no-shadow
                    variant="text"
                    class="text-xs"
                    :label="$t('profile.subId')"
                    tag="a"
                    target="_blank"
                    rel="nofollow noopener noreferrer"
                  />
                </div>
                <!-- Transfer Button -->
                <NeoButton
                  variant="outlined-rounded"
                  class="!w-full text-xs"
                  data-testid="profile-wallet-links-button-transfer"
                  :label="`${$t('transfer')} $`"
                  :tag="NuxtLink"
                  :to="`/${urlPrefix}/transfer?target=${id}`"
                />
              </div>
            </NeoDropdownItem>
            <NeoDropdownItem
              v-for="(item, index) in socialDropdownItems"
              :key="index"
            >
              <a
                v-safe-href="item?.url"
                target="_blank"
                class="flex items-center w-full text-left hover:!text-text-color"
                rel="noopener noreferrer"
              >
                <NeoIcon
                  v-if="typeof item?.icon === 'string'"
                  :class="'mr-2.5'"
                  :icon="item?.icon"
                  :pack="item?.iconPack"
                />
                <component
                  :is="item?.icon"
                  v-else
                  class="mr-2.5"
                />
                <span>{{ item?.label }}</span>
              </a>
            </NeoDropdownItem>
          </NeoDropdown>

          <!-- Share Dropdown -->
          <NeoDropdown>
            <template #trigger="{ active }">
              <NeoButton
                variant="outlined-rounded"
                icon="arrow-up-from-bracket"
                :active="active"
              />
            </template>

            <NeoDropdownItem
              v-clipboard:copy="shareURL"
              @click="toast(String($t('toast.urlCopy')))"
            >
              <div class="flex text-nowrap w-max items-center">
                <NeoIcon
                  icon="copy"
                  pack="fas"
                  class="mr-3"
                />
                {{ $t('share.copyLink') }}
              </div>
            </NeoDropdownItem>

            <NeoDropdownItem @click="shareOnX($i18n.t('sharing.profile'), shareURL, '')">
              <div class="flex text-nowrap w-max items-center">
                <NeoIcon
                  icon="x-twitter"
                  pack="fab"
                  class="mr-3"
                />
                {{ $t('share.twitter') }}
              </div>
            </NeoDropdownItem>
            <NeoDropdownItem @click="shareOnFarcaster($i18n.t('sharing.profile'), [shareURL])">
              <div class="flex text-nowrap w-max items-center">
                <FarcasterIcon class="mr-3" />
                {{ $t('share.farcaster') }}
              </div>
            </NeoDropdownItem>
          </NeoDropdown>
        </div>
        <!-- Profile Description -->
        <div
          v-if="userProfile?.description"
          class="max-w-lg whitespace-break-spaces text-sm"
        >
          <Markdown
            :source="userProfile.description"
            data-testid="profile-description"
          />
        </div>
        <!-- Followers -->
        <div v-if="!isOwner">
          <span
            v-if="!hasProfile || followersCount == 0"
            class="text-sm text-k-grey"
          >
            {{ $t('profile.notFollowed') }}
          </span>
          <div
            v-else
            class="flex gap-4 items-center followed-by"
          >
            <span class="text-sm text-k-grey">
              {{ $t('profile.followedBy') }}:
            </span>
            <NeoButton
              variant="text"
              no-shadow
              @click="onFollowersClick"
            >
              <div class="flex -space-x-3">
                <div
                  v-for="(follower, index) in followers?.followers"
                  :key="index"
                  class="flex"
                  :style="{ zIndex: 3 - index }"
                >
                  <ProfileAvatar
                    class="border"
                    :profile-image="follower.image"
                    :address="follower.address"
                    :size="30"
                  />
                </div>
              </div>
            </NeoButton>

            <span
              v-if="followersCount > 3"
              class="text-sm"
            >
              +
              {{ followersCount - (followers?.followers?.length ?? 0) }}
              More
            </span>
          </div>
        </div>
      </div>
      <!-- Mobile Profile Activity -->
      <ProfileActivity
        :profile-data="userProfile"
        class="pt-4 max-md:hidden w-60"
        :followers-count="followersCount"
        :following-count="followingCount"
        @click-followers="onFollowersClick"
        @click-following="onFollowingClick"
      />
    </div>

    <ProfileCuratedDrops :id="$route.params.id" />

    <div
      class="visible md:invisible py-7 md:!py-0 md:h-0 border-b border-neutral-5 dark:border-neutral-9 max-sm:mx-5 mx-12"
    >
      <ProfileActivity
        :profile-data="userProfile"
        class="w-full"
        :followers-count="followersCount"
        :following-count="followingCount"
        @click-followers="onFollowersClick"
        @click-following="onFollowingClick"
      />
    </div>

    <div class="pb-8">
      <div class="max-sm:mx-5 mx-12 2xl:mx-auto max-w-[89rem] py-7">
        <div class="flex gap-6 is-hidden-touch is-hidden-desktop-only">
          <div class="flex w-full">
            <TabItem
              v-for="tab in tabs"
              :key="tab"
              class="capitalize !w-full [&>*]:!w-full max-w-[12rem]"
              :data-testid="`profile-${tab}-tab`"
              :active="activeTab === tab"
              :count="counts[tab]"
              :show-active-check="tabsWithActiveCheck.includes(tab)"
              :text="tab"
              @click="() => switchToTab(tab)"
            />
          </div>
          <ChainDropdown />
          <OrderByDropdown v-if="showOrderByDropdown" />
        </div>
        <div class="flex flex-col gap-4 is-hidden-widescreen mobile">
          <div class="flex flex-wrap">
            <TabItem
              v-for="tab in tabs"
              :key="tab"
              :active="activeTab === tab"
              :text="tab"
              :count="counts[tab]"
              :show-active-check="tabsWithActiveCheck.includes(tab)"
              class="capitalize !w-[50%]"
              @click="() => switchToTab(tab)"
            />
          </div>
          <div class="flex flex-wrap gap-4">
            <ChainDropdown />
            <OrderByDropdown v-if="showOrderByDropdown" />
          </div>
        </div>
      </div>
      <hr class="my-0 !bg-background-color-inverse">
      <div class="max-sm:mx-5 mx-12 2xl:mx-auto max-w-[89rem] pb-6">
        <div
          v-if="[ProfileTab.OWNED, ProfileTab.CREATED].includes(activeTab)"
          class="flex-grow"
        >
          <div class="flex justify-between pb-4 pt-5 content-center">
            <div class="flex">
              <FilterButton
                :label="$t('sort.listed')"
                variant="outlined-rounded"
                url-param="buy_now"
                data-testid="profile-filter-button-buynow"
              />
              <FilterButton
                v-if="activeTab === 'created'"
                :label="$t('activity.sold')"
                variant="outlined-rounded"
                url-param="sold"
                class="ml-4"
              />
              <CollectionFilter
                :id="id.toString()"
                v-model="collections"
                variant="outlined-rounded"
                :search="itemsGridSearch"
                :tab-key="tabKey"
                class="ml-4"
              />
            </div>
            <div class="is-hidden-mobile">
              <GridLayoutControls
                class="is-hidden-mobile"
                :section="gridSection"
              />
            </div>
          </div>
          <hr class="my-0">
          <ItemsGrid
            :search="itemsGridSearch"
            :grid-section="gridSection"
            :loading-other-network="loadingOtherNetwork"
            :reset-search-query-params="['sort']"
          >
            <template
              v-if="hasAssetPrefixMap[activeTab]?.length && !listed && !addSold"
              #empty-result
            >
              <ProfileEmptyResult :prefix-list-with-asset="hasAssetPrefixMap[activeTab]" />
            </template>
          </ItemsGrid>
        </div>
        <CollectionGrid
          v-if="activeTab === ProfileTab.COLLECTIONS"
          :id="id"
          :loading-other-network="loadingOtherNetwork"
          class="pt-7"
        >
          <template
            v-if="hasAssetPrefixMap[activeTab]?.length"
            #empty-result
          >
            <ProfileEmptyResult
              :prefix-list-with-asset="hasAssetPrefixMap[ProfileTab.COLLECTIONS]
              "
            />
          </template>
        </CollectionGrid>
        <Activity
          v-if="activeTab === ProfileTab.ACTIVITY"
          :id="id"
        />
        <ProfileActivityTabTrades
          v-if="[ProfileTab.SWAPS, ProfileTab.OFFERS].includes(activeTab)"
          :id="id"
          :key="activeTab"
          :type="{
            [ProfileTab.SWAPS]: TradeType.SWAP,
            [ProfileTab.OFFERS]: TradeType.OFFER,
          }[activeTab]"
        />
      </div>
    </div>
  </div>
</template>

<script lang="ts" setup>
import {
  NeoButton,
  NeoDropdown,
  NeoDropdownItem,
  NeoIcon,
} from '@kodadot1/brick'
import { resolveComponent } from 'vue'
import { Interaction } from '@kodadot1/minimark/v1'
import type { ChainVM, Prefix } from '@kodadot1/static'
import { CHAINS } from '@kodadot1/static'
import { decodeAddress, encodeAddress } from '@polkadot/util-crypto'
import ProfileActivity from './ProfileActivitySummery.vue'
import FilterButton from './FilterButton.vue'
import OrderByDropdown from './OrderByDropdown.vue'
import Activity from './activityTab/Activity.vue'
import CollectionFilter from './CollectionFilter.vue'
import type { ButtonConfig } from './types'
import { ProfileTab } from './types'
import TabItem from '@/components/shared/TabItem.vue'
import Identity from '@/components/identity/IdentityIndex.vue'
import ItemsGrid from '@/components/items/ItemsGrid/ItemsGrid.vue'
import Avatar from '@/components/shared/Avatar.vue'
import ChainDropdown from '@/components/common/ChainDropdown.vue'
import CollectionGrid from '@/components/collection/CollectionGrid.vue'
import { useListingCartStore } from '@/stores/listingCart'
import { chainsWithMintInteraction } from '@/composables/collectionActivity/helpers'
import GridLayoutControls from '@/components/shared/GridLayoutControls.vue'
import { fetchFollowersOf, fetchFollowing } from '@/services/profile'
import { removeHttpFromUrl } from '@/utils/url'
import profileTabsCount from '@/queries/subsquid/general/profileTabsCount.query'
import { openProfileCreateModal } from '@/components/profile/create/openProfileModal'
import { getHigherResolutionCloudflareImage } from '@/utils/ipfs'
import { offerVisible, swapVisible } from '@/utils/config/permission.config'
import { TradeType } from '@/composables/useTrades'
import { doAfterCheckCurrentChainVM } from '@/components/common/ConnectWallet/openReconnectWalletModal'

const NuxtImg = resolveComponent('NuxtImg')
const NuxtLink = resolveComponent('NuxtLink')
const FarcasterIcon = defineAsyncComponent(
  () => import('@/assets/icons/farcaster-icon.svg?component'),
)

const gridSection = GridSection.PROFILE_GALLERY
const tabsWithActiveCheck = [ProfileTab.OFFERS, ProfileTab.SWAPS]

const socials = {
  [Socials.Farcaster]: {
    icon: FarcasterIcon,
    order: 1,
  },
  [Socials.Twitter]: {
    icon: 'x-twitter',
    iconPack: 'fab',
    order: 2,
  },
  [Socials.Website]: {
    icon: 'globe',
    order: 3,
  },
}

const route = useRoute()
const { $i18n } = useNuxtApp()
const { toast } = useToast()
const { replaceUrl } = useReplaceUrl()
const { isCurrentAccount } = useAuth()
const { urlPrefix, client } = usePrefix()
const { shareOnX, shareOnFarcaster } = useSocialShare()
const profileOnboardingStore = useProfileOnboardingStore()
const { getIsOnboardingShown } = storeToRefs(profileOnboardingStore)

const { isSub } = useIsChain(urlPrefix)
const listingCartStore = useListingCartStore()
const { vm } = useChain()
const { params } = useRoute()

const { hasProfile, userProfile, isFetchingProfile } = useProfile(computed(() => params?.id as string))

const { data: followers, refresh: refreshFollowers } = useAsyncData(
  `followersof${route.params.id}`,
  () =>
    fetchFollowersOf(route.params.id as string, {
      limit: 3,
    }),
)

const { data: following, refresh: refreshFollowing } = useAsyncData(
  `following${route.params.id}`,
  () => fetchFollowing(route.params.id as string, { limit: 1 }),
)

const refresh = ({ fetchFollowing = true } = {}) => {
  refreshFollowers()
  refreshFollowing()
  fetchFollowing && followButton.value?.refresh()
}
const followersCount = computed(() => followers.value?.totalCount ?? 0)
const followingCount = computed(() => following.value?.totalCount ?? 0)

const editProfileConfig: ButtonConfig = {
  label: $i18n.t('profile.editProfile'),
  icon: 'pen',
  onClick: () => openProfileCreateModal(true),
  classes: 'hover:!bg-transparent',
}

const createProfileConfig: ButtonConfig = {
  label: $i18n.t('profile.createProfile'),
  icon: 'sparkles',
  onClick: () => openProfileCreateModal(true),
  variant: 'primary',
}

const handleFollowRefresh = () => {
  refresh({ fetchFollowing: false })
}

const followButton = ref()
const counts = ref({})
const hasAssetPrefixMap = ref<Partial<Record<ProfileTab, Prefix[]>>>({})
const loadingOtherNetwork = ref(false)
const id = computed(() => route.params.id.toString() || '')
const email = ref('')
const twitter = ref('')
const displayName = ref('')
const web = ref('')
const legal = ref('')
const riot = ref('')
const isFollowModalActive = ref(false)
const followModalTab = ref<'followers' | 'following'>('followers')
const collections = ref(
  route.query.collections?.toString().split(',').filter(Boolean) || [],
)

const showOrderByDropdown = computed(() => [ProfileTab.OWNED, ProfileTab.CREATED, ProfileTab.COLLECTIONS].includes(activeTab.value))
const tabs = computed(() => {
  const tabs = [
    ProfileTab.OWNED,
    ProfileTab.CREATED,
    ProfileTab.COLLECTIONS,
    ProfileTab.ACTIVITY,
  ]

  if (offerVisible(urlPrefix.value)) {
    tabs.push(ProfileTab.OFFERS)
  }

  if (swapVisible(urlPrefix.value)) {
    tabs.push(ProfileTab.SWAPS)
  }

  return tabs
})

const shareURL = computed(() => `${window.location.origin}${route.path}`)

const socialDropdownItems = computed(() => {
  return userProfile.value?.socials
    .map(({ handle, platform, link }) => {
      const socialConfig = socials[platform]
      if (socialConfig) {
        const { icon, iconPack, order } = socialConfig
        return {
          label: removeHttpFromUrl(handle || link),
          icon,
          iconPack,
          url: link,
          order,
        }
      }
    })
    .sort((a, b) => a?.order - b?.order)
})

const isOwner = computed(() => isCurrentAccount(id.value))

const buttonConfig = computed<ButtonConfig>(() =>
  hasProfile.value ? editProfileConfig : createProfileConfig,
)

const switchToTab = (tab: ProfileTab) => {
  if (activeTab.value !== tab) {
    activeTab.value = tab
  }
}

const onFollowersClick = () => {
  followModalTab.value = 'followers'
  isFollowModalActive.value = true
}

const onFollowingClick = () => {
  followModalTab.value = 'following'
  isFollowModalActive.value = true
}

const handleSwapPageRedirect = () => {
  doAfterCheckCurrentChainVM(() => {
    return navigateTo(`/${urlPrefix.value}/swap/${isOwner.value ? '' : id.value}`)
  })
}

const tabKey = computed(() =>
  activeTab.value === ProfileTab.OWNED ? 'currentOwner_eq' : 'issuer_eq',
)

const itemsGridSearch = computed(() => {
  const query: Record<string, unknown> = {
    [tabKey.value]: toChainAddres(id.value, urlPrefix.value),
    burned_eq: false,
  }

  if (listed.value) {
    query['price_gt'] = 0
  }

  if (addSold.value) {
    query['events_some'] = {
      interaction_eq: 'BUY',
      AND: { caller_not_eq: toChainAddres(id.value, urlPrefix.value) },
    }
  }

  if (collections.value?.length) {
    query['collection'] = {
      id_in: collections.value,
    }
  }

  return query
})

const activeTab = computed({
  get: () => {
    const tab = route.query.tab as ProfileTab

    if (!tab || !tabs.value.includes(tab)) {
      return ProfileTab.OWNED
    }

    return tab
  },
  set: (val) => {
    replaceUrl({ tab: val }, { override: true })
  },
})

const listed = computed(() => route.query.buy_now === 'true')

const sold = computed(() => route.query.sold === 'true')
const addSold = computed(
  () => activeTab.value === ProfileTab.CREATED && sold.value,
)

const handleIdentity = (identityFields: Record<string, string>) => {
  displayName.value = identityFields?.display
  email.value = identityFields?.email
  twitter.value = identityFields?.twitter
  riot.value = identityFields?.riot
  web.value = identityFields?.web
  legal.value = identityFields?.legal
}

const interactionIn = computed(() => {
  const interactions = [Interaction.LIST, Interaction.SEND, Interaction.BUY]

  if (!chainsWithMintInteraction.includes(urlPrefix.value)) {
    interactions.push(Interaction.MINTNFT)
  }

  return interactions
})

useAsyncData('tabs-count', async () => {
  const address = toChainAddres(id.value, urlPrefix.value)
  const searchParams = {
    currentOwner_eq: address,
  }

  if (!isCurrentAccount(address)) {
    Object.assign(searchParams, { supply_not_eq: 0 })
  }

  searchParams['burned_eq'] = false

  const { data } = await useAsyncQuery({
    query: profileTabsCount,
    clientId: client.value,
    variables: {
      id: address,
      interactionIn: interactionIn.value,
      denyList: getDenyList(urlPrefix.value),
      search: [searchParams],
    },
  })

  if (!data.value) {
    return
  }

  counts.value = {
    [ProfileTab.OWNED]: data.value?.owned.totalCount,
    [ProfileTab.CREATED]: data.value?.created.totalCount,
    [ProfileTab.ACTIVITY]: data.value?.events.totalCount,
    [ProfileTab.COLLECTIONS]: data.value?.collections.totalCount,
  }
})

const fetchTabsCountByNetwork = async (chain: Prefix) => {
  const account = toChainAddres(id.value, urlPrefix.value)
  let address = account

  if (isSub.value) {
    const publicKey = decodeAddress(account)
    address = encodeAddress(publicKey, CHAINS[chain].ss58Format)
  }

  const searchParams = {
    currentOwner_eq: address,
  }

  searchParams['burned_eq'] = false

  const { data } = await useAsyncQuery({
    query: profileTabsCount,
    clientId: chain,
    variables: {
      id: address,
      interactionIn: [],
      denyList: getDenyList(urlPrefix.value),
      search: [searchParams],
    },
  })

  if (!data.value) {
    return
  }

  updateEmptyResultTab(ProfileTab.OWNED, data.value?.owned?.totalCount, chain)
  updateEmptyResultTab(
    ProfileTab.CREATED,
    data.value?.created?.totalCount,
    chain,
  )
  updateEmptyResultTab(
    ProfileTab.COLLECTIONS,
    data.value?.collections?.totalCount,
    chain,
  )
}

useAsyncData('tabs-empty-result', async () => {
  const chains = (
    {
      SUB: ['ahp', 'ahk'],
      EVM: ['base', 'imx'],
    } as Record<ChainVM, Prefix[]>
  )[vm.value]

  hasAssetPrefixMap.value = {
    [ProfileTab.OWNED]: [],
    [ProfileTab.CREATED]: [],
    [ProfileTab.COLLECTIONS]: [],
  }

  loadingOtherNetwork.value = true
  for (const chain of chains) {
    await fetchTabsCountByNetwork(chain as Prefix)
  }
  loadingOtherNetwork.value = false
})

const updateEmptyResultTab = (
  tab: ProfileTab,
  count: number,
  prefix: Prefix,
) => {
  if (count && hasAssetPrefixMap.value[tab]) {
    hasAssetPrefixMap.value[tab]!.push(prefix)
  }
}

watch(itemsGridSearch, (searchTerm, prevSearchTerm) => {
  if (JSON.stringify(searchTerm) !== JSON.stringify(prevSearchTerm)) {
    listingCartStore.clear()
  }
})

watch(collections, (value) => {
  replaceUrl({
    collections: value.length ? value.toString() : undefined,
  })
})

watchEffect(() => {
  if (!hasProfile.value && !isFetchingProfile.value && isOwner.value && !getIsOnboardingShown.value) {
    profileOnboardingStore.setOnboardingShown()
    openProfileCreateModal()
  }
})
</script>

<style lang="scss" scoped>
@import '@/assets/styles/abstracts/variables';

:deep(.rounded-full) {
  img {
    border-radius: 9999px !important;
  }
}

.invisible-tab>nav.tabs {
  display: none;
}

@include until-widescreen {
  .mobile {
    flex-wrap: wrap;

    >* {
      flex: 1 0 50%;

      &:nth-child(2) {
        :deep(.explore-tabs-button) {
          border-right: solid;
        }
      }

      &:nth-child(1),
      &:nth-child(2) {
        :deep(.explore-tabs-button) {
          @apply border-b-0;
        }
      }

      &:nth-child(2n + 1) {
        :deep(.explore-tabs-button) {
          border-right: none;
        }
      }
    }

    :deep(.explore-tabs-button) {
      width: 100% !important;
    }
  }
}

.tab-counter::before {
  content: ' - ';
  white-space: pre;
}

.title {
  flex-grow: 0;
  flex-basis: auto;
}

.divider {
  width: 1px;
  height: 5px;
  background-color: grey;
  margin: 0 10px;
}

.followed-by {
  :deep(.o-btn.is-neo:hover) {
    @apply text-text-color;
  }
}
</style>