From 829f7b5630887935bb36ca458f9f661ffc1963c0 Mon Sep 17 00:00:00 2001 From: daiagi Date: Wed, 22 Mar 2023 14:00:12 +0700 Subject: [PATCH 01/55] feat: collection activity chart --- components/collection/activity/activity.vue | 22 ++++++ .../collection/utils/useCollectionDetails.ts | 25 +++++- components/rmrk/service/scheme.ts | 10 ++- pages/_prefix/collection/_id/activity.vue | 76 ++++++++++++++++++- pages/_prefix/collection/_id/index.vue | 2 +- .../general/collectionActivityEvents.graphql | 18 +++++ styles/global.scss | 3 + 7 files changed, 146 insertions(+), 10 deletions(-) create mode 100644 components/collection/activity/activity.vue create mode 100644 queries/subsquid/general/collectionActivityEvents.graphql diff --git a/components/collection/activity/activity.vue b/components/collection/activity/activity.vue new file mode 100644 index 0000000000..bddfebec80 --- /dev/null +++ b/components/collection/activity/activity.vue @@ -0,0 +1,22 @@ + + + diff --git a/components/collection/utils/useCollectionDetails.ts b/components/collection/utils/useCollectionDetails.ts index 7f95a79ced..8e48b48903 100644 --- a/components/collection/utils/useCollectionDetails.ts +++ b/components/collection/utils/useCollectionDetails.ts @@ -1,5 +1,9 @@ import { getVolume } from '@/utils/math' -import { CollectionMetadata, NFT } from '@/components/rmrk/service/scheme' +import { + ActivityInteraction, + CollectionMetadata, + NFT, +} from '@/components/rmrk/service/scheme' import { NFTListSold } from '@/components/identity/utils/useIdentity' import { chainsSupportingOffers } from './useCollectionDetails.config' @@ -149,3 +153,22 @@ export const useCollectionMinimal = ({ collectionId }) => { }) return { collection } } + +export const useCollectionActivity = ({ collectionId }) => { + const events = ref() + + const { data } = useGraphql({ + queryPrefix: 'subsquid', + queryName: 'collectionActivityEvents', + variables: { + id: collectionId, + }, + }) + + watch(data, (result) => { + if (result?.collection) { + events.value = result.collection.nfts.map((nft) => nft.events).flat() + } + }) + return events +} diff --git a/components/rmrk/service/scheme.ts b/components/rmrk/service/scheme.ts index ff4d4bb84d..9fb57b34d5 100644 --- a/components/rmrk/service/scheme.ts +++ b/components/rmrk/service/scheme.ts @@ -176,15 +176,17 @@ export type EntityWithId = { name: string } -export interface Interaction { - blockNumber: string | number - caller: string - currentOwner: string +export interface ActivityInteraction { id: string interaction: string meta: string timestamp: string } +export interface Interaction extends ActivityInteraction { + blockNumber: string | number + caller: string + currentOwner: string +} export interface BasePack { _id: string diff --git a/pages/_prefix/collection/_id/activity.vue b/pages/_prefix/collection/_id/activity.vue index b3c7314ded..64dec956c7 100644 --- a/pages/_prefix/collection/_id/activity.vue +++ b/pages/_prefix/collection/_id/activity.vue @@ -1,11 +1,79 @@ - + diff --git a/pages/_prefix/collection/_id/index.vue b/pages/_prefix/collection/_id/index.vue index fdbb48eeed..abda2e9bda 100644 --- a/pages/_prefix/collection/_id/index.vue +++ b/pages/_prefix/collection/_id/index.vue @@ -17,7 +17,7 @@ import Items from '@/components/items/Items.vue' import { useHistoryStore } from '@/stores/history' import { usePreferencesStore } from '@/stores/preferences' -type CurrentCollection = { +export type CurrentCollection = { name: string numberOfItems: number image: string diff --git a/queries/subsquid/general/collectionActivityEvents.graphql b/queries/subsquid/general/collectionActivityEvents.graphql new file mode 100644 index 0000000000..e96208e09b --- /dev/null +++ b/queries/subsquid/general/collectionActivityEvents.graphql @@ -0,0 +1,18 @@ +query collectionActivityEvents($id: String!) { + collection: collectionEntityById(id: $id) { + id + nfts { + events( + where: { + interaction_eq: MINT + OR: { interaction_eq: LIST, OR: { interaction_eq: BUY } } + } + ) { + timestamp + meta + interaction + id + } + } + } +} diff --git a/styles/global.scss b/styles/global.scss index 093cef6a36..e64d646084 100644 --- a/styles/global.scss +++ b/styles/global.scss @@ -220,6 +220,9 @@ body { height: 100%; } +.w-full { + width: 100%; +} hr { height: 1px; @include ktheme() { From b030b79ddc46e1a550c6452a1f9a915bf945fc86 Mon Sep 17 00:00:00 2001 From: daiagi Date: Wed, 22 Mar 2023 14:00:47 +0700 Subject: [PATCH 02/55] remove controls on activity tab --- components/explore/Controls.vue | 8 ++++---- components/explore/DesktopControls.vue | 5 ++++- components/explore/MobileControls.vue | 8 ++++++-- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/components/explore/Controls.vue b/components/explore/Controls.vue index c5387d9017..e7cdefe0e3 100644 --- a/components/explore/Controls.vue +++ b/components/explore/Controls.vue @@ -1,11 +1,11 @@ diff --git a/components/explore/DesktopControls.vue b/components/explore/DesktopControls.vue index 8893105e95..8872ea91f9 100644 --- a/components/explore/DesktopControls.vue +++ b/components/explore/DesktopControls.vue @@ -2,7 +2,7 @@
-
+
@@ -24,6 +24,9 @@ const route = useRoute() const isCollection = computed(() => route.name?.includes('prefix-explore-collectibles') ) +const isActivityTab = computed(() => + route.name?.includes('prefix-collection-id-activity') +) +a diff --git a/components/collection/activity/Chart.vue b/components/collection/activity/Chart.vue new file mode 100644 index 0000000000..13ebe07be8 --- /dev/null +++ b/components/collection/activity/Chart.vue @@ -0,0 +1,11 @@ + + + diff --git a/components/collection/activity/OwnerInsights.vue b/components/collection/activity/OwnerInsights.vue new file mode 100644 index 0000000000..521f5ca698 --- /dev/null +++ b/components/collection/activity/OwnerInsights.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/components/collection/activity/activity.vue b/components/collection/activity/activity.vue deleted file mode 100644 index bddfebec80..0000000000 --- a/components/collection/activity/activity.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/components/collection/activity/holdersFlippersTabs/FlipperTab.vue b/components/collection/activity/holdersFlippersTabs/FlipperTab.vue new file mode 100644 index 0000000000..8d75e07086 --- /dev/null +++ b/components/collection/activity/holdersFlippersTabs/FlipperTab.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/components/collection/activity/holdersFlippersTabs/HolderTab.vue b/components/collection/activity/holdersFlippersTabs/HolderTab.vue new file mode 100644 index 0000000000..dad2851124 --- /dev/null +++ b/components/collection/activity/holdersFlippersTabs/HolderTab.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/components/collection/activity/holdersFlippersTabs/moreNFTS.vue b/components/collection/activity/holdersFlippersTabs/moreNFTS.vue new file mode 100644 index 0000000000..5d82351e8b --- /dev/null +++ b/components/collection/activity/holdersFlippersTabs/moreNFTS.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/components/collection/activity/utils.ts b/components/collection/activity/utils.ts new file mode 100644 index 0000000000..6ad6e219f7 --- /dev/null +++ b/components/collection/activity/utils.ts @@ -0,0 +1,17 @@ +export const timeAgo = (timestamp: number): string => { + const now = new Date().getTime() + const secondsSince = Math.floor((now - timestamp) / 1000) + if (secondsSince < 60) { + return `${secondsSince} Second${secondsSince !== 1 ? 's' : ''} Ago` + } + const minutesSince = Math.floor(secondsSince / 60) + if (minutesSince < 60) { + return `${minutesSince} Minute${minutesSince !== 1 ? 's' : ''} Ago` + } + const hoursSince = Math.floor(minutesSince / 60) + if (hoursSince < 24) { + return `${hoursSince} Hour${hoursSince !== 1 ? 's' : ''} Ago` + } + const daysSince = Math.floor(hoursSince / 24) + return `${daysSince} Day${daysSince !== 1 ? 's' : ''} Ago` +} diff --git a/components/collection/utils/useCollectionDetails.ts b/components/collection/utils/useCollectionDetails.ts index 8e48b48903..1c2313a2bd 100644 --- a/components/collection/utils/useCollectionDetails.ts +++ b/components/collection/utils/useCollectionDetails.ts @@ -1,11 +1,13 @@ import { getVolume } from '@/utils/math' import { - ActivityInteraction, CollectionMetadata, + Interaction as InteractionType, NFT, + NFTMetadata, } from '@/components/rmrk/service/scheme' import { NFTListSold } from '@/components/identity/utils/useIdentity' import { chainsSupportingOffers } from './useCollectionDetails.config' +import { Interaction } from '@kodadot1/minimark' type Stats = { listedCount?: number @@ -153,9 +155,48 @@ export const useCollectionMinimal = ({ collectionId }) => { }) return { collection } } +export type NFTExcludingEvents = { + currentOwner: string + name: string + price?: string + metadata: string + meta: NFTMetadata + updatedAt: string + id: string +} +type InteractionWithNFT = InteractionType & { + nft: NFTExcludingEvents +} +type NFTMap = { + [nftId: string]: { + owner: string + nft: NFTExcludingEvents + latestInteraction: Interaction + latestPrice: number + } +} +export type Flippers = { + [identity: string]: { + nftId: string + soldPrice: number + soldTo: string + sellTimeStamp: number + boughtPrice: number + }[] +} +export type Owners = { + [identity: string]: { + nftCount: number + totalBought: number + nfts: NFTExcludingEvents[] + lastActivityTimestamp: number + } +} export const useCollectionActivity = ({ collectionId }) => { - const events = ref() + const events = ref() + const owners = ref() + const flippers = ref() const { data } = useGraphql({ queryPrefix: 'subsquid', @@ -167,8 +208,157 @@ export const useCollectionActivity = ({ collectionId }) => { watch(data, (result) => { if (result?.collection) { - events.value = result.collection.nfts.map((nft) => nft.events).flat() + // flat events for chart + const interactions: InteractionWithNFT[] = result.collection.nfts + .map((nft) => + nft.events.map((e) => ({ ...e, nft: { ...nft, events: undefined } })) + ) + .flat() + events.value = interactions + owners.value = getOwners(result.collection.nfts) + flippers.value = getFlippers(interactions) + } + }) + return { + events, + owners, + flippers, + } +} + +const getOwners = (nfts) => { + const owners: Owners = {} + + nfts.forEach((nft) => { + const interactions = nft.events.map((e) => e.interaction) + const { events, ...nftExcludingEvents } = nft + + if (interactions.includes(Interaction.CONSUME)) { + // no owner + return + } + if ( + interactions.includes(Interaction.BUY) || + interactions.includes(Interaction.SEND) + ) { + // NFT changed hands + const latestInteraction = events[events.length - 1] + const lastestTimeStamp = new Date(latestInteraction.timestamp).getTime() + + const owner = owners[nft.currentOwner] + if (owner) { + // update entry + owner.nftCount++ + owner.totalBought += parseInt(latestInteraction.meta) + owner.lastActivityTimestamp = + lastestTimeStamp > owner.lastActivityTimestamp + ? lastestTimeStamp + : owner.lastActivityTimestamp + owner.nfts = [...owner.nfts, nftExcludingEvents] + return + } + // new owner entry + owners[nft.currentOwner] = { + nftCount: 1, + totalBought: parseInt(latestInteraction.meta), + lastActivityTimestamp: lastestTimeStamp, + nfts: [nftExcludingEvents], + } + return + } + + // nft isn't consumed and it hasn't change hands => it is owned by it's creator + const mintInteraction = events[0] + const mintTimeStamp = new Date(mintInteraction.timestamp).getTime() + const owner = owners[nft.currentOwner] + + if (owner) { + // update entry + owner.nftCount++ + owner.lastActivityTimestamp = + mintTimeStamp > owner.lastActivityTimestamp + ? mintTimeStamp + : owner.lastActivityTimestamp + owner.nfts = [...owner.nfts, nftExcludingEvents] + return + } + // new owner entry + owners[nft.currentOwner] = { + nftCount: 1, + totalBought: 0, + lastActivityTimestamp: mintTimeStamp, + nfts: [nftExcludingEvents], + } + }) + return owners +} + +const preProccessForFindingFlippers = (interactions: InteractionWithNFT[]) => { + const changeHandsInteractions: InteractionWithNFT[] = [] + const NFTS: NFTMap = {} + + interactions.forEach((event) => { + if (event.interaction === Interaction.MINTNFT) { + // mintInteractions.push(event) + NFTS[event.nft.id] = { + owner: event.caller, + nft: event.nft, + latestInteraction: Interaction.MINTNFT, + latestPrice: 0, + } + } + + if ( + event.interaction === Interaction.SEND || + event.interaction === Interaction.BUY + ) { + changeHandsInteractions.push(event) + } + }) + + return { NFTS, changeHandsInteractions } +} + +const getFlippers = (interactions: InteractionWithNFT[]) => { + const { NFTS, changeHandsInteractions } = + preProccessForFindingFlippers(interactions) + + const flippers: Flippers = {} + changeHandsInteractions.forEach((interaction) => { + if (interaction.interaction === Interaction.SEND) { + NFTS[interaction.nft.id].owner = interaction.meta + NFTS[interaction.nft.id].latestInteraction = Interaction.SEND + } + if (interaction.interaction === Interaction.BUY) { + //it's a Flip! + const nftId = interaction.nft.id + const PreviousNFTState = NFTS[nftId] + const baseInfo = { + nftId, + soldPrice: parseInt(interaction.meta), + soldTo: interaction.caller, + sellTimeStamp: new Date(interaction.timestamp).getTime(), + } + + //nft has been bought from previous owner -> previous owner is the flipper + const flipperHistory = flippers[PreviousNFTState.owner] || [] + const thisFlip = { + ...baseInfo, + boughtPrice: + PreviousNFTState.latestInteraction === Interaction.BUY + ? PreviousNFTState.latestPrice + : 0, + } + flippers[PreviousNFTState.owner] = [...flipperHistory, thisFlip] + + // update last state of NFT + NFTS[nftId] = { + ...PreviousNFTState, + owner: interaction.caller, + latestInteraction: interaction.interaction, + latestPrice: parseInt(interaction.meta), + } } }) - return events + return flippers } diff --git a/locales/en.json b/locales/en.json index 7617d8085d..2de7873101 100644 --- a/locales/en.json +++ b/locales/en.json @@ -18,6 +18,7 @@ "transfer": "Transfer", "chart": "Chart", "holders": "Holders", + "flippers": "Flippers", "assets": "Assets", "incomingOffers": "Incoming Offers", "transform": "Transform", diff --git a/pages/_prefix/collection/_id/activity.vue b/pages/_prefix/collection/_id/activity.vue index 64dec956c7..c105b890cb 100644 --- a/pages/_prefix/collection/_id/activity.vue +++ b/pages/_prefix/collection/_id/activity.vue @@ -7,7 +7,7 @@ diff --git a/components/collection/utils/useCollectionDetails.ts b/components/collection/utils/useCollectionDetails.ts index 1c2313a2bd..a5185194c3 100644 --- a/components/collection/utils/useCollectionDetails.ts +++ b/components/collection/utils/useCollectionDetails.ts @@ -175,14 +175,23 @@ type NFTMap = { latestPrice: number } } +type FlipEvent = { + nft: NFTExcludingEvents + soldPrice: number + soldTo: string + sellTimeStamp: number + boughtPrice: number +} + export type Flippers = { [identity: string]: { - nftId: string - soldPrice: number - soldTo: string - sellTimeStamp: number - boughtPrice: number - }[] + flips: FlipEvent[] + owned: number + totalBought: number + totalsold: number + bestFlip: number + latestflipTimestamp: number + } } export type Owners = { [identity: string]: { @@ -324,6 +333,7 @@ const getFlippers = (interactions: InteractionWithNFT[]) => { preProccessForFindingFlippers(interactions) const flippers: Flippers = {} + changeHandsInteractions.forEach((interaction) => { if (interaction.interaction === Interaction.SEND) { NFTS[interaction.nft.id].owner = interaction.meta @@ -331,17 +341,29 @@ const getFlippers = (interactions: InteractionWithNFT[]) => { } if (interaction.interaction === Interaction.BUY) { //it's a Flip! + const nftId = interaction.nft.id const PreviousNFTState = NFTS[nftId] const baseInfo = { - nftId, + nft: NFTS[nftId].nft, soldPrice: parseInt(interaction.meta), soldTo: interaction.caller, sellTimeStamp: new Date(interaction.timestamp).getTime(), } + if (flippers[PreviousNFTState.owner] === undefined) { + flippers[PreviousNFTState.owner] = { + flips: [], + bestFlip: 0, + latestflipTimestamp: 0, + owned: 0, + totalBought: 0, + totalsold: 0, + } + } //nft has been bought from previous owner -> previous owner is the flipper - const flipperHistory = flippers[PreviousNFTState.owner] || [] + + const flipperHistory = flippers[PreviousNFTState.owner].flips const thisFlip = { ...baseInfo, boughtPrice: @@ -349,7 +371,8 @@ const getFlippers = (interactions: InteractionWithNFT[]) => { ? PreviousNFTState.latestPrice : 0, } - flippers[PreviousNFTState.owner] = [...flipperHistory, thisFlip] + + flippers[PreviousNFTState.owner].flips = [...flipperHistory, thisFlip] // update last state of NFT NFTS[nftId] = { @@ -360,5 +383,29 @@ const getFlippers = (interactions: InteractionWithNFT[]) => { } } }) + for (const flipper in flippers) { + flippers[flipper].owned = flippers[flipper].flips.length + + flippers[flipper].totalBought = sum( + flippers[flipper].flips.map((flip) => flip.boughtPrice) + ) + flippers[flipper].totalsold = sum( + flippers[flipper].flips.map((flip) => flip.soldPrice) + ) + const flipsPercentages = flippers[flipper].flips + .map((flip) => + flip.boughtPrice > 0 ? flip.soldPrice / flip.boughtPrice : 0 + ) + .filter(Boolean) + flippers[flipper].bestFlip = + flipsPercentages.length > 0 ? Math.max(...flipsPercentages) * 100 : 0 //to percents + + flippers[flipper].latestflipTimestamp = Math.max( + ...flippers[flipper].flips.map((flip) => flip.sellTimeStamp) + ) + } return flippers } + +const sum = (array: number[]): number => + array.reduce((accumulator, currentValue) => accumulator + currentValue, 0) From a0f5b6fa824294bcaabf07a3d35a99d96e718031 Mon Sep 17 00:00:00 2001 From: daiagi Date: Fri, 24 Mar 2023 00:03:29 +0700 Subject: [PATCH 05/55] control chart height --- components/chart/PriceChart.vue | 7 ++++++- components/collection/activity/Chart.vue | 2 +- .../gallery/GalleryItemTabsPanel/GalleryItemChart.vue | 6 +++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/components/chart/PriceChart.vue b/components/chart/PriceChart.vue index feda2979f1..842565491f 100644 --- a/components/chart/PriceChart.vue +++ b/components/chart/PriceChart.vue @@ -22,7 +22,7 @@ -
+
@@ -69,7 +69,12 @@ const setTimeRange = (value: { value: number; label: string }) => { const props = defineProps<{ priceChartData?: [Date, number][][] + chartHeight?: string }>() + +const heightStyle = computed(() => + props.chartHeight ? `height: ${props.chartHeight}` : '' +) let Chart: ChartJS<'line', any, unknown> onMounted(() => { diff --git a/components/collection/activity/Chart.vue b/components/collection/activity/Chart.vue index 13ebe07be8..65c9a63637 100644 --- a/components/collection/activity/Chart.vue +++ b/components/collection/activity/Chart.vue @@ -1,5 +1,5 @@ - - diff --git a/components/collection/activity/holdersFlippersTabs/moreNFTs/Flipper.vue b/components/collection/activity/holdersFlippersTabs/moreNFTs/Flipper.vue new file mode 100644 index 0000000000..854e9c6314 --- /dev/null +++ b/components/collection/activity/holdersFlippersTabs/moreNFTs/Flipper.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/components/collection/activity/holdersFlippersTabs/moreNFTs/Holder.vue b/components/collection/activity/holdersFlippersTabs/moreNFTs/Holder.vue new file mode 100644 index 0000000000..1eb7c13397 --- /dev/null +++ b/components/collection/activity/holdersFlippersTabs/moreNFTs/Holder.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/components/collection/activity/utils.ts b/components/collection/activity/utils.ts index 6ad6e219f7..3b1682a7b4 100644 --- a/components/collection/activity/utils.ts +++ b/components/collection/activity/utils.ts @@ -1,17 +1,29 @@ export const timeAgo = (timestamp: number): string => { + const { $i18n } = useNuxtApp() + const { ago, second, seconds, minute, minutes, hour, hours, day, days } = { + ago: $i18n.t('time.ago'), + second: $i18n.t('time.second'), + seconds: $i18n.t('time.seconds'), + minute: $i18n.t('time.minute'), + minutes: $i18n.t('time.minutes'), + hour: $i18n.t('time.hour'), + hours: $i18n.t('time.hours'), + day: $i18n.t('time.day'), + days: $i18n.t('time.days'), + } const now = new Date().getTime() const secondsSince = Math.floor((now - timestamp) / 1000) if (secondsSince < 60) { - return `${secondsSince} Second${secondsSince !== 1 ? 's' : ''} Ago` + return `${secondsSince} ${secondsSince !== 1 ? seconds : second} ${ago}` } const minutesSince = Math.floor(secondsSince / 60) if (minutesSince < 60) { - return `${minutesSince} Minute${minutesSince !== 1 ? 's' : ''} Ago` + return `${minutesSince} ${minutesSince !== 1 ? minutes : minute} ${ago}` } const hoursSince = Math.floor(minutesSince / 60) if (hoursSince < 24) { - return `${hoursSince} Hour${hoursSince !== 1 ? 's' : ''} Ago` + return `${hoursSince} ${hoursSince !== 1 ? hours : hour} ${ago}` } const daysSince = Math.floor(hoursSince / 24) - return `${daysSince} Day${daysSince !== 1 ? 's' : ''} Ago` + return `${daysSince} ${daysSince !== 1 ? days : day} ${ago}` } diff --git a/components/collection/utils/types.ts b/components/collection/utils/types.ts new file mode 100644 index 0000000000..35ef3ce5fb --- /dev/null +++ b/components/collection/utils/types.ts @@ -0,0 +1,76 @@ +import { + CollectionMetadata, + Interaction as InteractionType, + NFTMetadata, +} from '@/components/rmrk/service/scheme' + +import { Interaction } from '@kodadot1/minimark' + +export type Stats = { + listedCount?: number + collectionLength?: number + collectionFloorPrice?: number + bestOffer?: number + uniqueOwners?: number + uniqueOwnersPercent?: string + collectionTradedVolumeNumber?: bigint +} + +export type CollectionEntityMinimal = { + id: string + issuer: string + meta: CollectionMetadata + metadata: string + name: string + currentOwner: string + type: string +} + +export type NFTExcludingEvents = { + currentOwner: string + name: string + price?: string + metadata: string + meta: NFTMetadata + updatedAt: string + id: string +} +export type InteractionWithNFT = InteractionType & { + nft: NFTExcludingEvents +} +export type NFTMap = { + [nftId: string]: { + owner: string + nft: NFTExcludingEvents + latestInteraction: Interaction + latestPrice: number + } +} +export type FlipEvent = { + nft: NFTExcludingEvents + soldPrice: number + soldTo: string + sellTimeStamp: number + boughtPrice: number + profit: number +} + +export type Flippers = { + [identity: string]: { + flips: FlipEvent[] + owned: number + totalBought: number + totalsold: number + bestFlip: number + latestflipTimestamp: number + } +} +export type Owners = { + [identity: string]: { + nftCount: number + totalBought: number + totalSold: number + nfts: NFTExcludingEvents[] + lastActivityTimestamp: number + } +} diff --git a/components/collection/utils/useCollectionDetails.ts b/components/collection/utils/useCollectionDetails.ts index a5185194c3..c33b068bb4 100644 --- a/components/collection/utils/useCollectionDetails.ts +++ b/components/collection/utils/useCollectionDetails.ts @@ -1,33 +1,9 @@ -import { getVolume } from '@/utils/math' -import { - CollectionMetadata, - Interaction as InteractionType, - NFT, - NFTMetadata, -} from '@/components/rmrk/service/scheme' +import { getVolume, sum } from '@/utils/math' +import { NFT } from '@/components/rmrk/service/scheme' import { NFTListSold } from '@/components/identity/utils/useIdentity' import { chainsSupportingOffers } from './useCollectionDetails.config' import { Interaction } from '@kodadot1/minimark' - -type Stats = { - listedCount?: number - collectionLength?: number - collectionFloorPrice?: number - bestOffer?: number - uniqueOwners?: number - uniqueOwnersPercent?: string - collectionTradedVolumeNumber?: bigint -} - -export type CollectionEntityMinimal = { - id: string - issuer: string - meta: CollectionMetadata - metadata: string - name: string - currentOwner: string - type: string -} +import { Flippers, InteractionWithNFT, NFTMap, Owners, Stats } from './types' const differentOwner = (nft: { issuer: string @@ -155,52 +131,6 @@ export const useCollectionMinimal = ({ collectionId }) => { }) return { collection } } -export type NFTExcludingEvents = { - currentOwner: string - name: string - price?: string - metadata: string - meta: NFTMetadata - updatedAt: string - id: string -} -type InteractionWithNFT = InteractionType & { - nft: NFTExcludingEvents -} -type NFTMap = { - [nftId: string]: { - owner: string - nft: NFTExcludingEvents - latestInteraction: Interaction - latestPrice: number - } -} -type FlipEvent = { - nft: NFTExcludingEvents - soldPrice: number - soldTo: string - sellTimeStamp: number - boughtPrice: number -} - -export type Flippers = { - [identity: string]: { - flips: FlipEvent[] - owned: number - totalBought: number - totalsold: number - bestFlip: number - latestflipTimestamp: number - } -} -export type Owners = { - [identity: string]: { - nftCount: number - totalBought: number - nfts: NFTExcludingEvents[] - lastActivityTimestamp: number - } -} export const useCollectionActivity = ({ collectionId }) => { const events = ref() @@ -224,8 +154,22 @@ export const useCollectionActivity = ({ collectionId }) => { ) .flat() events.value = interactions - owners.value = getOwners(result.collection.nfts) - flippers.value = getFlippers(interactions) + + // not to repeat ref names + const ownersTemp = getOwners(result.collection.nfts) + const flippersTemp = getFlippers(interactions) + + const flipperdIds = Object.keys(flippersTemp) + const OwnersIds = Object.keys(ownersTemp) + + flipperdIds.forEach((id) => { + if (OwnersIds.includes(id)) { + ownersTemp[id].totalSold = flippersTemp[id].totalsold + } + }) + + owners.value = ownersTemp + flippers.value = flippersTemp } }) return { @@ -328,7 +272,7 @@ const preProccessForFindingFlippers = (interactions: InteractionWithNFT[]) => { return { NFTS, changeHandsInteractions } } -const getFlippers = (interactions: InteractionWithNFT[]) => { +const getFlippers = (interactions: InteractionWithNFT[]): Flippers => { const { NFTS, changeHandsInteractions } = preProccessForFindingFlippers(interactions) @@ -364,12 +308,16 @@ const getFlippers = (interactions: InteractionWithNFT[]) => { //nft has been bought from previous owner -> previous owner is the flipper const flipperHistory = flippers[PreviousNFTState.owner].flips + const boughtPrice = + PreviousNFTState.latestInteraction === Interaction.BUY + ? PreviousNFTState.latestPrice + : 0 + const profit = + boughtPrice > 0 ? (baseInfo.soldPrice / boughtPrice) * 100 : 0 const thisFlip = { ...baseInfo, - boughtPrice: - PreviousNFTState.latestInteraction === Interaction.BUY - ? PreviousNFTState.latestPrice - : 0, + boughtPrice, + profit, } flippers[PreviousNFTState.owner].flips = [...flipperHistory, thisFlip] @@ -383,29 +331,16 @@ const getFlippers = (interactions: InteractionWithNFT[]) => { } } }) - for (const flipper in flippers) { - flippers[flipper].owned = flippers[flipper].flips.length - - flippers[flipper].totalBought = sum( - flippers[flipper].flips.map((flip) => flip.boughtPrice) - ) - flippers[flipper].totalsold = sum( - flippers[flipper].flips.map((flip) => flip.soldPrice) - ) - const flipsPercentages = flippers[flipper].flips - .map((flip) => - flip.boughtPrice > 0 ? flip.soldPrice / flip.boughtPrice : 0 - ) - .filter(Boolean) - flippers[flipper].bestFlip = - flipsPercentages.length > 0 ? Math.max(...flipsPercentages) * 100 : 0 //to percents - - flippers[flipper].latestflipTimestamp = Math.max( - ...flippers[flipper].flips.map((flip) => flip.sellTimeStamp) - ) - } + + Object.entries(flippers).forEach(([flipperId, { flips }]) => { + flippers[flipperId] = { + owned: flips.length, + totalBought: sum(flips.map((flip) => flip.boughtPrice)), + totalsold: sum(flips.map((flip) => flip.soldPrice)), + bestFlip: Math.max(...flips.map((flip) => flip.profit)), + latestflipTimestamp: Math.max(...flips.map((flip) => flip.sellTimeStamp)), + flips, + } + }) return flippers } - -const sum = (array: number[]): number => - array.reduce((accumulator, currentValue) => accumulator + currentValue, 0) diff --git a/locales/en.json b/locales/en.json index 2de7873101..0fdb282683 100644 --- a/locales/en.json +++ b/locales/en.json @@ -884,6 +884,17 @@ "history": "History", "chart": "Chart" }, + "time": { + "ago": "Ago", + "second": "Second", + "seconds": "Seconds", + "minute": "Minute", + "minutes": "Minutes", + "hour": "Hour", + "hours": "Hours", + "day": "Day", + "days": "Days" + }, "activity": { "listed": "Listed", "totalItems": "Σ Items", @@ -899,7 +910,17 @@ "bestOffer": "Best Offer", "volume": "Volume", "network": "Network", - "creator": "Creator" + "creator": "Creator", + "owned": "Owned", + "totalBought": "Total Bought", + "totalSold": "Total Sold", + "date": "Date", + "bestFlip": "Best Flip", + "latestActivity": "LatestActivity", + "nftDetails": "NFT Details", + "profit": "Profit", + "bought": "Bought", + "sold": "Sold" }, "profileStats": { "listed": "Listed", diff --git a/utils/math.ts b/utils/math.ts index bbff5e33cc..407998de96 100644 --- a/utils/math.ts +++ b/utils/math.ts @@ -21,6 +21,9 @@ export function pairListBuyEvent(events: Interaction[]): Interaction[] { } } +export const sum = (array: number[]): number => + array.reduce((accumulator, currentValue) => accumulator + currentValue, 0) + export function getSum(list: Array): bigint | number { return list .map((x) => x) From 6693f984a685f3f900cc69b3b83dd090d8cf1a9f Mon Sep 17 00:00:00 2001 From: daiagi Date: Fri, 24 Mar 2023 13:34:19 +0700 Subject: [PATCH 07/55] reponsivity --- components/collection/activity/Activity.vue | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/components/collection/activity/Activity.vue b/components/collection/activity/Activity.vue index 9fb9657b00..7ac384653c 100644 --- a/components/collection/activity/Activity.vue +++ b/components/collection/activity/Activity.vue @@ -3,7 +3,7 @@
-
+
@@ -30,9 +30,4 @@ const { events, flippers, owners } = useCollectionActivity({ -a From 7b6de3b00b226ab8dfcfd25d8fc8c1bab2926d09 Mon Sep 17 00:00:00 2001 From: daiagi Date: Fri, 24 Mar 2023 22:05:07 +0700 Subject: [PATCH 08/55] feat: events table --- components/collection/activity/Activity.vue | 41 ++++- .../collection/activity/OwnerInsights.vue | 18 +- .../collection/activity/events/EventRow.vue | 173 ++++++++++++++++++ .../collection/activity/events/Events.vue | 87 +++++++++ .../holdersFlippersTabs/moreNFTs/Flipper.vue | 2 +- components/collection/utils/types.ts | 1 + .../collection/utils/useCollectionDetails.ts | 6 +- styles/abstracts/_theme.scss | 8 + 8 files changed, 323 insertions(+), 13 deletions(-) create mode 100644 components/collection/activity/events/EventRow.vue create mode 100644 components/collection/activity/events/Events.vue diff --git a/components/collection/activity/Activity.vue b/components/collection/activity/Activity.vue index 7ac384653c..b58c4bacb1 100644 --- a/components/collection/activity/Activity.vue +++ b/components/collection/activity/Activity.vue @@ -1,5 +1,5 @@ @@ -18,16 +22,49 @@ import SidebarFilter from '@/components/explore/SidebarFilter.vue' import ActivityChart from './Chart.vue' import OwnerInsights from './OwnerInsights.vue' - +import Events from './events/Events.vue' +import BreadcrumbsFilter from '@/components/shared/BreadcrumbsFilter.vue' import { useCollectionActivity } from '@/components/collection/utils/useCollectionDetails' +import { Interaction } from '@kodadot1/minimark' const route = useRoute() +const isFiltersActive = ref(false) + +const toggleBreadcrumbsVisible = (active: boolean) => { + isFiltersActive.value = active +} + const collectionId = computed(() => route.params.id) const { events, flippers, owners } = useCollectionActivity({ collectionId: collectionId.value, }) + +const InteractionIncluded = [ + Interaction.BUY, + Interaction.LIST, + Interaction.MINTNFT, + Interaction.SEND, +] + +const filteredEvents = computed(() => + events.value + ? events.value.filter((event) => + InteractionIncluded.includes(event.interaction as Interaction) + ) + : [] +) + +const sortedEvents = computed(() => + filteredEvents.value.sort((a, b) => b.timestamp - a.timestamp) +) diff --git a/components/collection/activity/OwnerInsights.vue b/components/collection/activity/OwnerInsights.vue index 5460cfa4ba..62e4e8718c 100644 --- a/components/collection/activity/OwnerInsights.vue +++ b/components/collection/activity/OwnerInsights.vue @@ -3,20 +3,20 @@
+ :class="{ 'has-text-weight-bold': activeTab === Tabs.holders }" + @click="activeTab = Tabs.holders"> {{ $t('holders') }}
+ :class="{ 'has-text-weight-bold': activeTab === Tabs.flippers }" + @click="activeTab = Tabs.flippers"> {{ $t('flippers') }}
- - + +
@@ -27,15 +27,15 @@ import FlippersTab from './holdersFlippersTabs/FlipperTab.vue' import { Flippers, Owners } from '@/components/collection/utils/types' enum Tabs { - Holders, - Flippers, + holders, + flippers, } defineProps<{ owners?: Owners flippers?: Flippers }>() -const activeTab = ref(Tabs.Holders) +const activeTab = ref(Tabs.holders) diff --git a/components/collection/activity/events/Events.vue b/components/collection/activity/events/Events.vue new file mode 100644 index 0000000000..d9568d9733 --- /dev/null +++ b/components/collection/activity/events/Events.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/components/collection/activity/holdersFlippersTabs/moreNFTs/Flipper.vue b/components/collection/activity/holdersFlippersTabs/moreNFTs/Flipper.vue index 854e9c6314..e9081f2574 100644 --- a/components/collection/activity/holdersFlippersTabs/moreNFTs/Flipper.vue +++ b/components/collection/activity/holdersFlippersTabs/moreNFTs/Flipper.vue @@ -12,7 +12,7 @@ width="40" height="40" class="border mr-2rem" /> - + {{ nft.name }}
{ // flat events for chart const interactions: InteractionWithNFT[] = result.collection.nfts .map((nft) => - nft.events.map((e) => ({ ...e, nft: { ...nft, events: undefined } })) + nft.events.map((e) => ({ + ...e, + timestamp: new Date(e.timestamp).getTime(), + nft: { ...nft, events: undefined }, + })) ) .flat() events.value = interactions diff --git a/styles/abstracts/_theme.scss b/styles/abstracts/_theme.scss index afd6667aae..f8c01badbc 100644 --- a/styles/abstracts/_theme.scss +++ b/styles/abstracts/_theme.scss @@ -15,6 +15,10 @@ $themes: ( 'k-green': #04af00, 'k-red': #ff5757, 'k-grey': #999999, + 'k-pink': #FFB6EF, + 'k-yellow': #FEFFB6, + 'k-blueaccent': #B6CBFF, + 'k-greenaccent': #C2FFAC, 'k-hovergrey': #6b6b6b, 'k-blue': #6188e7, 'k-blue-hover': #3567e0, @@ -42,6 +46,10 @@ $themes: ( 'k-green': #04af00, 'k-red': #ff5757, 'k-grey': #cccccc, + 'k-pink': #7A2A68, + 'k-yellow': #363234, + 'k-blueaccent': #2E50A2, + 'k-greenaccent': #056A02, 'k-hovergrey': #6b6b6b, 'k-blue': #6188e7, 'k-blue-hover': #3567e0, From 76b7fa0a4d7f46a0bf5daae2934b0288d1f467c9 Mon Sep 17 00:00:00 2001 From: daiagi Date: Fri, 24 Mar 2023 22:52:43 +0700 Subject: [PATCH 09/55] touch up events table --- .../collection/activity/events/EventRow.vue | 173 +---------------- .../collection/activity/events/Events.vue | 52 +++-- .../events/eventRow/EventRowDesktop.vue | 181 ++++++++++++++++++ .../events/eventRow/EventRowTablet.vue | 174 +++++++++++++++++ 4 files changed, 387 insertions(+), 193 deletions(-) create mode 100644 components/collection/activity/events/eventRow/EventRowDesktop.vue create mode 100644 components/collection/activity/events/eventRow/EventRowTablet.vue diff --git a/components/collection/activity/events/EventRow.vue b/components/collection/activity/events/EventRow.vue index 7c5b94fe5d..b8c94ea8da 100644 --- a/components/collection/activity/events/EventRow.vue +++ b/components/collection/activity/events/EventRow.vue @@ -1,173 +1,14 @@ - - diff --git a/components/collection/activity/events/Events.vue b/components/collection/activity/events/Events.vue index d9568d9733..a7212d72b4 100644 --- a/components/collection/activity/events/Events.vue +++ b/components/collection/activity/events/Events.vue @@ -1,34 +1,32 @@ diff --git a/components/collection/activity/events/eventRow/EventRowDesktop.vue b/components/collection/activity/events/eventRow/EventRowDesktop.vue new file mode 100644 index 0000000000..995132997e --- /dev/null +++ b/components/collection/activity/events/eventRow/EventRowDesktop.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/components/collection/activity/events/eventRow/EventRowTablet.vue b/components/collection/activity/events/eventRow/EventRowTablet.vue new file mode 100644 index 0000000000..29531ec541 --- /dev/null +++ b/components/collection/activity/events/eventRow/EventRowTablet.vue @@ -0,0 +1,174 @@ + + + + + From 3557ecd0dc52cd4b953d16e6958530d9a6048dda Mon Sep 17 00:00:00 2001 From: daiagi Date: Sat, 25 Mar 2023 11:01:05 +0700 Subject: [PATCH 10/55] feat: add offers to event table --- components/collection/activity/Activity.vue | 15 +- .../collection/activity/events/EventRow.vue | 4 +- .../collection/activity/events/Events.vue | 6 +- .../events/eventRow/EventRowDesktop.vue | 27 +- .../events/eventRow/EventRowTablet.vue | 2 +- .../holdersFlippersTabs/FlipperTab.vue | 2 +- .../holdersFlippersTabs/HolderTab.vue | 2 +- .../activity/holdersFlippersTabs/moreNFTS.vue | 5 +- .../holdersFlippersTabs/moreNFTs/Holder.vue | 2 +- .../{activity/utils.ts => utils/timeAgo.ts} | 0 components/collection/utils/types.ts | 11 + .../collection/utils/useCollectionActivity.ts | 252 ++++++++++++++++++ .../collection/utils/useCollectionDetails.ts | 217 --------------- .../bsx/collectionActivityEvents.graphql | 32 +++ 14 files changed, 337 insertions(+), 240 deletions(-) rename components/collection/{activity/utils.ts => utils/timeAgo.ts} (100%) create mode 100644 components/collection/utils/useCollectionActivity.ts create mode 100644 queries/subsquid/bsx/collectionActivityEvents.graphql diff --git a/components/collection/activity/Activity.vue b/components/collection/activity/Activity.vue index b58c4bacb1..8b584bd640 100644 --- a/components/collection/activity/Activity.vue +++ b/components/collection/activity/Activity.vue @@ -24,7 +24,7 @@ import ActivityChart from './Chart.vue' import OwnerInsights from './OwnerInsights.vue' import Events from './events/Events.vue' import BreadcrumbsFilter from '@/components/shared/BreadcrumbsFilter.vue' -import { useCollectionActivity } from '@/components/collection/utils/useCollectionDetails' +import { useCollectionActivity } from '~~/components/collection/utils/useCollectionActivity' import { Interaction } from '@kodadot1/minimark' const route = useRoute() @@ -35,7 +35,7 @@ const toggleBreadcrumbsVisible = (active: boolean) => { } const collectionId = computed(() => route.params.id) -const { events, flippers, owners } = useCollectionActivity({ +const { events, flippers, owners, offers } = useCollectionActivity({ collectionId: collectionId.value, }) @@ -47,15 +47,14 @@ const InteractionIncluded = [ ] const filteredEvents = computed(() => - events.value - ? events.value.filter((event) => - InteractionIncluded.includes(event.interaction as Interaction) - ) - : [] + events.value.filter((event) => + InteractionIncluded.includes(event.interaction as Interaction) + ) ) +const withOffers = computed(() => [...filteredEvents.value, ...offers.value]) const sortedEvents = computed(() => - filteredEvents.value.sort((a, b) => b.timestamp - a.timestamp) + withOffers.value.sort((a, b) => b.timestamp - a.timestamp) ) diff --git a/components/collection/activity/events/EventRow.vue b/components/collection/activity/events/EventRow.vue index b8c94ea8da..e0664c29d3 100644 --- a/components/collection/activity/events/EventRow.vue +++ b/components/collection/activity/events/EventRow.vue @@ -6,9 +6,9 @@ diff --git a/components/collection/activity/events/Events.vue b/components/collection/activity/events/Events.vue index a7212d72b4..c655e3077a 100644 --- a/components/collection/activity/events/Events.vue +++ b/components/collection/activity/events/Events.vue @@ -32,20 +32,20 @@ diff --git a/components/collection/activity/events/EventRow.vue b/components/collection/activity/events/EventRow.vue index e0664c29d3..909c6c17da 100644 --- a/components/collection/activity/events/EventRow.vue +++ b/components/collection/activity/events/EventRow.vue @@ -1,7 +1,7 @@ diff --git a/components/collection/activity/events/eventRow/EventRowTablet.vue b/components/collection/activity/events/eventRow/EventRowTablet.vue index 6642a13731..bb9c089541 100644 --- a/components/collection/activity/events/eventRow/EventRowTablet.vue +++ b/components/collection/activity/events/eventRow/EventRowTablet.vue @@ -1,62 +1,76 @@ From 8caac9197568641677463b7815df34c4cb84149c Mon Sep 17 00:00:00 2001 From: daiagi Date: Sun, 26 Mar 2023 14:27:20 +0700 Subject: [PATCH 18/55] visual touchups --- components/collection/activity/Activity.vue | 6 +- .../collection/activity/events/Events.vue | 5 +- .../events/eventRow/EventRowDesktop.vue | 78 ++++++++++++------- .../events/eventRow/EventRowTablet.vue | 16 ++-- .../holdersFlippersTabs/FlipperTab.vue | 5 +- components/identity/module/IdentityChain.vue | 2 +- 6 files changed, 68 insertions(+), 44 deletions(-) diff --git a/components/collection/activity/Activity.vue b/components/collection/activity/Activity.vue index 1a0d5c6a73..2d529c5bed 100644 --- a/components/collection/activity/Activity.vue +++ b/components/collection/activity/Activity.vue @@ -12,7 +12,6 @@
-
@@ -22,6 +21,7 @@
+
@@ -34,11 +34,9 @@ import Events from './events/Events.vue' import BreadcrumbsFilter from '@/components/shared/BreadcrumbsFilter.vue' import { useCollectionActivity } from '@/components/collection/utils/useCollectionActivity' import { Interaction } from '@kodadot1/minimark' -import { useResizeObserver, useWindowSize } from '@vueuse/core' +import { useResizeObserver } from '@vueuse/core' import SidebarFilter from '@/components/shared/filters/SidebarFilter.vue' -// const sidebarwidth = 55 const mobileBreakpoint = 800 -// const windowWidth = useWindowSize().width.value const route = useRoute() const tablet = ref(true) const mobile = ref(false) diff --git a/components/collection/activity/events/Events.vue b/components/collection/activity/events/Events.vue index 52bee70327..4e9a3ad025 100644 --- a/components/collection/activity/events/Events.vue +++ b/components/collection/activity/events/Events.vue @@ -1,6 +1,6 @@