Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 2 additions & 34 deletions components/BookshelfItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
<li
ref="lazyLoadTrigger"
class="flex flex-col justify-end"
:class="!props.isOwned && props.stakedAmount > 0 ? 'opacity-50' : 'opacity-100'"
:class="!props.isOwned ? 'opacity-50' : 'opacity-100'"
>
<BookCover
:src="bookCoverSrc"
:alt="bookInfo.name.value"
:lazy="props.lazy"
:is-claimable="isClaimable"
:is-staked="hasStakes"
:is-staked="false"
:has-shadow="true"
@click="handleCoverClick"
/>
Expand Down Expand Up @@ -103,28 +103,6 @@
>{{ bookInfo.authorName }}</NuxtLink>
</div>
</div>

<!-- Staking info section -->
<div class="mt-3 space-y-1 text-toned text-xs">
<BookItemStatsRow
:label="$t('staking_dashboard_staked')"
:is-hidden="stakedAmount <= 0"
>
<BalanceLabel
:value="stakedAmount"
:is-compact="true"
/>
</BookItemStatsRow>
<BookItemStatsRow
:label="$t('staking_dashboard_rewards')"
:is-hidden="pendingRewards <= 0"
>
<BalanceLabel
:value="pendingRewards"
:is-compact="true"
/>
</BookItemStatsRow>
</div>
</li>
</template>

Expand All @@ -148,14 +126,6 @@ const props = defineProps({
type: Boolean,
default: false,
},
stakedAmount: {
type: Number,
default: 0,
},
pendingRewards: {
type: Number,
default: 0,
},
isOwned: {
type: Boolean,
default: true,
Expand Down Expand Up @@ -184,8 +154,6 @@ const progressPercentage = computed(() => Math.round(props.progress * 100))

const isDesktopScreen = useDesktopScreen()

const hasStakes = computed(() => props.stakedAmount > 0)

const menuItems = computed<DropdownMenuItem[]>(() => {
const genericItems: DropdownMenuItem[] = []
const readerItems: DropdownMenuItem[] = []
Expand Down
41 changes: 10 additions & 31 deletions components/BookstoreItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,32 +49,15 @@
/>
</div>

<!-- Staking info for staking mode -->
<div class="mt-3 space-y-1 text-muted text-xs">
<BookItemStatsRow
:label="$t('staking_explore_total_staked')"
:is-hidden="totalStaked <= 0"
:to="stakingRoute"
>
<BalanceLabel
:value="totalStaked"
:is-bold="false"
:is-compact="true"
/>
</BookItemStatsRow>
<BookItemStatsRow
:label="$t('staking_explore_stakers')"
:is-hidden="totalStaked <= 0"
:to="stakingRoute"
>
<BalanceLabel
:value="stakerCount"
:is-compact="true"
:is-bold="false"
currency=""
/>
</BookItemStatsRow>
</div>
<!-- LikeRank -->
<BookItemStatsRow
class="mt-3 text-muted text-xs"
:is-hidden="likeRank <= 0"
:label="$t('staking_like_rank')"
:to="stakingRoute"
>
<span class="font-semibold">#{{ likeRank }}</span>
</BookItemStatsRow>
</li>
</template>

Expand Down Expand Up @@ -108,11 +91,7 @@ const props = defineProps({
type: String,
default: '',
},
totalStaked: {
type: Number,
default: 0,
},
stakerCount: {
likeRank: {
type: Number,
default: 0,
},
Expand Down
5 changes: 5 additions & 0 deletions composables/use-nft-class-staking-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ export function useNFTClassStakingData(nftClassId: ComputedRef<string>) {
return stakingStore.getNumberOfStakersCached(nftClassId.value)
})

const stakingRank = computed(() => {
return stakingStore.getStakingRankCached(nftClassId.value)
})

// Load staking data from blockchain
async function loadStakingData() {
try {
Expand Down Expand Up @@ -124,6 +128,7 @@ export function useNFTClassStakingData(nftClassId: ComputedRef<string>) {
pendingRewards,
isClaimingRewards,
numberOfStakers,
stakingRank,
// Computed
formattedTotalStake,
formattedUserStake,
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,7 @@
"staking_explore_total_staked": "Staked",
"staking_info_tab_staking_info": "Staking",
"staking_learn_more": "Learn more about Staking",
"staking_like_rank": "LikeRank",
"staking_of_total": "{percentage}% of total",
"staking_pending_rewards": "Pending rewards",
"staking_stake_button": "Stake",
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/zh-Hant.json
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,7 @@
"staking_explore_total_staked": "應援指數",
"staking_info_tab_staking_info": "應援指數",
"staking_learn_more": "甚麼是應援?",
"staking_like_rank": "LikeRank",
"staking_of_total": "佔比 {percentage}%",
"staking_pending_rewards": "待領收益",
"staking_stake_button": "質押",
Expand Down
2 changes: 0 additions & 2 deletions pages/shelf/[[walletAddress]].vue
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,6 @@
:class="getGridItemClassesByIndex(index)"
:nft-class-id="item.nftClassId"
:nft-ids="item.nftIds"
:staked-amount="item.stakedAmount"
:pending-rewards="item.pendingRewards"
:is-owned="item.isOwned"
:progress="item.progress"
:lazy="index >= columnMax"
Comment on lines 128 to 133
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After removing the :pending-rewards prop from <BookshelfItem>, this page still computes pendingRewards in BookshelfItemWithStaking (including formatUnits(...)) but no longer uses it anywhere (only stakedAmount is used for sorting). Consider removing the pendingRewards field and its associated conversions to avoid unnecessary work and reduce data shape complexity.

Copilot uses AI. Check for mistakes.
Comment on lines 128 to 133
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says shelf cards should replace "我的質押/收益" with LikeRank, but the shelf item rendering here only removes :staked-amount / :pending-rewards without adding any LikeRank display. Either update the PR description to match the actual scope, or add the LikeRank UI/data plumbing for shelf items.

Copilot uses AI. Check for mistakes.
Expand Down
15 changes: 15 additions & 0 deletions pages/store/[nftClassId]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
<ExpandableContent>
<div
class="markdown"
v-html="bookInfoDescriptionHTML"

Check warning on line 122 in pages/store/[nftClassId]/index.vue

View workflow job for this annotation

GitHub Actions / build

'v-html' directive can lead to XSS attack
/>
</ExpandableContent>
<template v-if="bookInfo.descriptionSummary?.value">
Expand All @@ -129,7 +129,7 @@
<ExpandableContent>
<div
class="markdown"
v-html="bookInfoDescriptionSummaryHTML"

Check warning on line 132 in pages/store/[nftClassId]/index.vue

View workflow job for this annotation

GitHub Actions / build

'v-html' directive can lead to XSS attack
/>
</ExpandableContent>
</template>
Expand Down Expand Up @@ -209,7 +209,7 @@
<ExpandableContent>
<div
class="markdown"
v-html="previewContentHTML"

Check warning on line 212 in pages/store/[nftClassId]/index.vue

View workflow job for this annotation

GitHub Actions / build

'v-html' directive can lead to XSS attack
/>
</ExpandableContent>
</template>
Expand All @@ -229,7 +229,7 @@
<ExpandableContent>
<div
class="markdown"
v-html="authorDescriptionHTML"

Check warning on line 232 in pages/store/[nftClassId]/index.vue

View workflow job for this annotation

GitHub Actions / build

'v-html' directive can lead to XSS attack
/>
</ExpandableContent>
</template>
Expand All @@ -238,7 +238,7 @@
<ExpandableContent>
<div
class="markdown"
v-html="tableOfContentsHTML"

Check warning on line 241 in pages/store/[nftClassId]/index.vue

View workflow job for this annotation

GitHub Actions / build

'v-html' directive can lead to XSS attack
/>
</ExpandableContent>
</template>
Expand Down Expand Up @@ -276,6 +276,20 @@
/>
</div>
</UCard>
<UCard
v-if="stakingRank > 0"
:ui="{ body: 'p-4' }"
>
<div class="text-center">
Comment on lines +279 to +283
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds a 4th stats card when stakingRank > 0, but the surrounding grid is still tablet:grid-cols-3. When hasLoggedIn is also true this section will render 4 cards, causing an awkward wrap/misalignment at tablet+ breakpoints. Consider updating the grid columns/responsive layout so the card count is handled consistently.

Copilot uses AI. Check for mistakes.
<div class="text-2xl font-semibold">
#{{ stakingRank }}
</div>
<div
class="mt-1 text-sm text-muted"
v-text="$t('staking_like_rank')"
/>
</div>
</UCard>
<UCard
v-if="hasLoggedIn"
:ui="{ body: 'p-4' }"
Expand Down Expand Up @@ -490,7 +504,7 @@
<div
v-if="item.renderedDescription"
class="markdown whitespace-normal text-left mt-2"
v-html="item.renderedDescription"

Check warning on line 507 in pages/store/[nftClassId]/index.vue

View workflow job for this annotation

GitHub Actions / build

'v-html' directive can lead to XSS attack
/>

<div class="flex flex-wrap gap-1 mt-3">
Expand Down Expand Up @@ -883,6 +897,7 @@
formattedUserStake,
formattedPendingRewards,
numberOfStakers,
stakingRank,
handleClaimRewards,
loadStakingData,
} = useNFTClassStakingData(nftClassId)
Expand Down
13 changes: 7 additions & 6 deletions pages/store/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -356,8 +356,7 @@
:book-name="item.title"
:book-cover-src="item.imageUrl"
:price="item.minPrice"
:total-staked="Number(formatUnits(item.totalStaked ?? 0n, likeCoinTokenDecimals))"
:staker-count="item.stakerCount ?? 0"
:like-rank="item.likeRank ?? 0"
:lazy="index >= columnMax"
:ll-medium="llMedium"
ll-source="bookstore"
Expand All @@ -379,10 +378,8 @@
</template>

<script setup lang="ts">
import { formatUnits } from 'viem'
import { getGenreI18nKey } from '~/constants/book-categories'

const { likeCoinTokenDecimals } = useRuntimeConfig().public
const { t: $t, locale } = useI18n()
const localeRoute = useLocaleRoute()
const route = useRoute()
Expand Down Expand Up @@ -668,13 +665,15 @@ const searchResults = computed<BookstoreItemList | null>(() => {

const cmsProducts = computed<BookstoreItemList>(() => {
const apiSortValue = mapTagIdToAPIStakingSortValue(STAKING_TAG_DEFAULT)
const stakingData = bookstoreStore.getStakingBooks(apiSortValue).items.reduce((map, item) => {
const stakingItems = bookstoreStore.getStakingBooks(apiSortValue).items
const stakingData = stakingItems.reduce((map, item) => {
map[item.nftClassId.toLowerCase()] = {
totalStaked: item.totalStaked,
stakerCount: item.stakerCount,
likeRank: item.likeRank,
}
return map
}, {} as Record<string, { totalStaked: bigint, stakerCount: number }>)
}, {} as Record<string, { totalStaked: bigint, stakerCount: number, likeRank?: number }>)

const listingProducts = bookstoreStore.getBookstoreCMSProductsByTagId(tagId.value)
const items = listingProducts.items.map((item) => {
Expand All @@ -683,6 +682,7 @@ const cmsProducts = computed<BookstoreItemList>(() => {
...item,
totalStaked: stakingInfo?.totalStaked ?? 0n,
stakerCount: stakingInfo?.stakerCount ?? 0,
likeRank: stakingInfo?.likeRank ?? 0,
}
})

Expand Down Expand Up @@ -731,6 +731,7 @@ const products = computed<BookstoreItemList>(() => {
minPrice: undefined,
totalStaked: item.totalStaked,
stakerCount: item.stakerCount,
likeRank: item.likeRank,
})
})
return {
Expand Down
1 change: 1 addition & 0 deletions shared/utils/collective-indexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface CollectiveBookNFT {
staked_amount: string
last_staked_at: string | null
number_of_stakers: number
staking_rank?: number
}

export interface CollectiveAccountBookNFT {
Expand Down
9 changes: 5 additions & 4 deletions stores/bookstore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface StakingBooks {
nftClassId: string
totalStaked: bigint
stakerCount: number
likeRank: number
}>
isFetching: boolean
hasFetched: boolean
Expand Down Expand Up @@ -383,16 +384,16 @@ export const useBookstoreStore = defineStore('bookstore', () => {
'sort_by': sortBy as 'staked_amount' | 'last_staked_at' | 'number_of_stakers',
})

const currentOffset = Number(stakingBooksMap.value[sortBy].offset ?? 0)
const bookNFTs = result.data
.map(bookNFT => ({
.filter(bookNFT => BigInt(bookNFT.staked_amount || 0) > 0)
.map((bookNFT, index) => ({
nftClassId: normalizeNFTClassId(bookNFT.evm_address),
totalStaked: BigInt(bookNFT.staked_amount || 0),
stakerCount: bookNFT.number_of_stakers,
lastStakedAt: bookNFT.last_staked_at,
likeRank: currentOffset + index + 1,
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

likeRank is derived from stakingBooksMap.value[sortBy].offset via Number(...), but this offset is used as the Collective API pagination.key (cursor), not a guaranteed numeric item offset. This can produce incorrect ranks (especially after loading the next page), and filtering out zero-stake items can also shift the computed index. Prefer using the API-provided staking_rank (now available on CollectiveBookNFT) or compute rank from the existing accumulated item count rather than the pagination cursor.

Copilot uses AI. Check for mistakes.
}))
Comment on lines 392 to 396
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

likeRank is currently computed as currentOffset + index + 1, but stakingBooksMap.value[sortBy].offset is the Collective indexer pagination.key cursor (stored as string), not a numeric offset. Converting it with Number(...) will often yield NaN or an unrelated value, producing incorrect ranks. Since the API type now exposes staking_rank, use bookNFT.staking_rank (with a sensible fallback) instead of deriving rank from the pagination cursor.

Copilot uses AI. Check for mistakes.
.filter((bookNFT) => {
return bookNFT.totalStaked > 0
})

if (isRefresh) {
stakingBooksMap.value[sortBy].items = bookNFTs
Expand Down
10 changes: 9 additions & 1 deletion stores/staking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface NFTClassTotalStake {
nftClassId: string
totalStake: bigint
numberOfStakers: number
stakingRank: number
isFetching: boolean
}

Expand Down Expand Up @@ -56,6 +57,10 @@ export const useStakingStore = defineStore('staking', () => {
return totalStakeByNFTClassMap.value[nftClassId]?.totalStake ?? 0n
})

const getStakingRankCached = computed(() => (nftClassId: string) => {
return totalStakeByNFTClassMap.value[nftClassId]?.stakingRank ?? 0
})

const getNumberOfStakersCached = computed(() => (nftClassId: string) => {
return totalStakeByNFTClassMap.value[nftClassId]?.numberOfStakers ?? 0
})
Expand Down Expand Up @@ -207,6 +212,7 @@ export const useStakingStore = defineStore('staking', () => {
nftClassId,
totalStake: 0n,
numberOfStakers: 0,
stakingRank: 0,
isFetching: false,
}
}
Expand All @@ -215,12 +221,13 @@ export const useStakingStore = defineStore('staking', () => {

const [totalStake, bookNFTData] = await Promise.all([
getTotalStakeOfNFTClass(nftClassId),
fetchCollectiveBookNFT(nftClassId).catch(() => ({ number_of_stakers: 0 })),
fetchCollectiveBookNFT(nftClassId).catch(() => ({ number_of_stakers: 0, staking_rank: undefined })),
])

if (totalStakeByNFTClassMap.value[nftClassId]) {
totalStakeByNFTClassMap.value[nftClassId].totalStake = totalStake
totalStakeByNFTClassMap.value[nftClassId].numberOfStakers = bookNFTData.number_of_stakers
totalStakeByNFTClassMap.value[nftClassId].stakingRank = bookNFTData.staking_rank ?? 0
}

return totalStake
Expand Down Expand Up @@ -270,6 +277,7 @@ export const useStakingStore = defineStore('staking', () => {
getUserStakingData,
getFormattedTotalRewards,
getTotalStakeOfNFTClassCached,
getStakingRankCached,
getNumberOfStakersCached,

fetchUserStakingData,
Expand Down
1 change: 1 addition & 0 deletions types/bookstore.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
declare interface BookstoreItem {
totalStaked?: bigint
stakerCount?: number
likeRank?: number
id?: string
classId?: string
classIds?: string[]
Expand Down
Loading