diff --git a/icons/hexagon.svg b/icons/hexagon.svg new file mode 100644 index 0000000000..833a3a954f --- /dev/null +++ b/icons/hexagon.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/api/services/rewards.ts b/lib/api/services/rewards.ts index ae26c2948d..10b2310fb3 100644 --- a/lib/api/services/rewards.ts +++ b/lib/api/services/rewards.ts @@ -34,6 +34,9 @@ export const REWARDS_API_RESOURCES = { user_referrals: { path: '/api/v1/user/referrals', }, + user_badges: { + path: '/api/v1/user/badges', + }, user_check_activity_pass: { path: '/api/v1/activity/check-pass', filterFields: [ 'address' as const ], @@ -74,6 +77,7 @@ R extends 'rewards:user_balances' ? rewards.GetUserBalancesResponse : R extends 'rewards:user_daily_check' ? rewards.DailyRewardCheckResponse : R extends 'rewards:user_daily_claim' ? rewards.DailyRewardClaimResponse : R extends 'rewards:user_referrals' ? rewards.GetReferralDataResponse : +R extends 'rewards:user_badges' ? rewards.GetAvailableBadgesResponse : R extends 'rewards:user_check_activity_pass' ? rewards.CheckActivityPassResponse : R extends 'rewards:user_activity' ? rewards.GetActivityRewardsResponse : R extends 'rewards:user_activity_track_tx' ? rewards.PreSubmitTransactionResponse : diff --git a/mocks/rewards/streakBadges.ts b/mocks/rewards/streakBadges.ts new file mode 100644 index 0000000000..7e69743e25 --- /dev/null +++ b/mocks/rewards/streakBadges.ts @@ -0,0 +1,71 @@ +import type { GetAvailableBadgesResponse } from '@blockscout/points-types'; + +export const base: GetAvailableBadgesResponse = { + items: [ + { + chain_id: '17000', + address: '0xf34B6A7d0BAbb5eBEe5521Ce7e5393A10782AE96', + requirements: { + streak: '30', + }, + is_qualified: false, + is_whitelisted: false, + is_minted: false, + }, + { + chain_id: '17000', + address: '0xf41583090c674E55d9755b0afcbbf9ea2FA378e7', + requirements: { + streak: '90', + }, + is_qualified: false, + is_whitelisted: false, + is_minted: false, + }, + { + chain_id: '17000', + address: '0x6a4676480C9E36652F62d4A751eeEf562E06a383', + requirements: { + streak: '180', + }, + is_qualified: false, + is_whitelisted: false, + is_minted: false, + }, + ], +}; + +export const filled: GetAvailableBadgesResponse = { + items: [ + { + chain_id: '17000', + address: '0xf34B6A7d0BAbb5eBEe5521Ce7e5393A10782AE96', + requirements: { + streak: '30', + }, + is_qualified: true, + is_whitelisted: true, + is_minted: true, + }, + { + chain_id: '17000', + address: '0xf41583090c674E55d9755b0afcbbf9ea2FA378e7', + requirements: { + streak: '90', + }, + is_qualified: true, + is_whitelisted: true, + is_minted: true, + }, + { + chain_id: '17000', + address: '0x6a4676480C9E36652F62d4A751eeEf562E06a383', + requirements: { + streak: '180', + }, + is_qualified: true, + is_whitelisted: true, + is_minted: false, + }, + ], +}; diff --git a/package.json b/package.json index bb6e894422..8441bbc5fa 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "dependencies": { "@blockscout/bens-types": "1.4.1", "@blockscout/multichain-aggregator-types": "1.6.0-alpha.2", - "@blockscout/points-types": "1.3.0-alpha.2", + "@blockscout/points-types": "1.4.0-alpha.1", "@blockscout/stats-types": "^2.9.0", "@blockscout/tac-operation-lifecycle-types": "0.0.1-alpha.6", "@blockscout/visualizer-types": "0.2.0", diff --git a/public/icons/name.d.ts b/public/icons/name.d.ts index 06b734a9d4..aeb2ea3dd2 100644 --- a/public/icons/name.d.ts +++ b/public/icons/name.d.ts @@ -83,6 +83,7 @@ | "globe" | "heart_filled" | "heart_outline" + | "hexagon" | "hourglass_slim" | "hourglass" | "info_filled" diff --git a/public/static/merits/streak_180.png b/public/static/merits/streak_180.png new file mode 100644 index 0000000000..e785dee97e Binary files /dev/null and b/public/static/merits/streak_180.png differ diff --git a/public/static/merits/streak_180_ghost.png b/public/static/merits/streak_180_ghost.png new file mode 100644 index 0000000000..25d1705b41 Binary files /dev/null and b/public/static/merits/streak_180_ghost.png differ diff --git a/public/static/merits/streak_30.png b/public/static/merits/streak_30.png new file mode 100644 index 0000000000..36a95a3dad Binary files /dev/null and b/public/static/merits/streak_30.png differ diff --git a/public/static/merits/streak_30_ghost.png b/public/static/merits/streak_30_ghost.png new file mode 100644 index 0000000000..f9c3bcc916 Binary files /dev/null and b/public/static/merits/streak_30_ghost.png differ diff --git a/public/static/merits/streak_90.png b/public/static/merits/streak_90.png new file mode 100644 index 0000000000..83b80e4f40 Binary files /dev/null and b/public/static/merits/streak_90.png differ diff --git a/public/static/merits/streak_90_ghost.png b/public/static/merits/streak_90_ghost.png new file mode 100644 index 0000000000..2ab2e8b0f6 Binary files /dev/null and b/public/static/merits/streak_90_ghost.png differ diff --git a/ui/pages/RewardsDashboard.tsx b/ui/pages/RewardsDashboard.tsx index 816b20d774..e75fd40f67 100644 --- a/ui/pages/RewardsDashboard.tsx +++ b/ui/pages/RewardsDashboard.tsx @@ -4,21 +4,27 @@ import React, { useEffect, useState } from 'react'; import config from 'configs/app'; import { useRewardsContext } from 'lib/contexts/rewards'; import { Alert } from 'toolkit/chakra/alert'; +import { Button } from 'toolkit/chakra/button'; import { Link } from 'toolkit/chakra/link'; import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs'; +import { useDisclosure } from 'toolkit/hooks/useDisclosure'; import { apos } from 'toolkit/utils/htmlEntities'; import DailyRewardClaimButton from 'ui/rewards/dashboard/DailyRewardClaimButton'; import RewardsDashboardCard from 'ui/rewards/dashboard/RewardsDashboardCard'; import RewardsDashboardCardValue from 'ui/rewards/dashboard/RewardsDashboardCardValue'; +import RewardsStreakModal from 'ui/rewards/dashboard/streakModal/RewardsStreakModal'; import ActivityTab from 'ui/rewards/dashboard/tabs/ActivityTab'; import ReferralsTab from 'ui/rewards/dashboard/tabs/ReferralsTab'; import ResourcesTab from 'ui/rewards/dashboard/tabs/ResourcesTab'; +import useStreakBadges from 'ui/rewards/hooks/useStreakBadges'; import AdBanner from 'ui/shared/ad/AdBanner'; import PageTitle from 'ui/shared/Page/PageTitle'; import useRedirectForInvalidAuthToken from 'ui/snippets/auth/useRedirectForInvalidAuthToken'; const RewardsDashboard = () => { const { balancesQuery, apiToken, referralsQuery, rewardsConfigQuery, dailyRewardQuery, isInitialized } = useRewardsContext(); + const { nextAchievementText, isLoading: isBadgesLoading, badgesQuery } = useStreakBadges(); + const streakModal = useDisclosure(); const [ isError, setIsError ] = useState(false); @@ -38,13 +44,6 @@ const RewardsDashboard = () => { return null; } - let shareText = `Claim your free @blockscout #Merits and start building your daily streak today! #Blockscout #Merits #IYKYK\n\nBoost your rewards instantly by using my referral code: ${ referralsQuery.data?.link }`; // eslint-disable-line max-len - - if (dailyRewardQuery.data?.streak && Number(dailyRewardQuery.data.streak) > 0) { - const days = `day${ Number(dailyRewardQuery.data.streak) === 1 ? '' : 's' }`; - shareText = `I${ apos }ve claimed Merits ${ dailyRewardQuery.data.streak } ${ days } in a row!\n\n` + shareText; - } - return ( <> @@ -69,7 +68,7 @@ const RewardsDashboard = () => { title="All Merits" description="Claim your daily Merits and any Merits received from referrals." contentDirection="column-reverse" - cardValueStyle={{ minH: { base: '64px', md: '88px' } }} + cardValueStyle={{ minH: { base: '64px', md: '116px' } }} contentAfter={ } hint={ ( <> @@ -90,7 +89,7 @@ const RewardsDashboard = () => { title="Referrals" description="Total number of users who have joined the program using your code or referral link." contentDirection="column-reverse" - cardValueStyle={{ minH: { base: '64px', md: '88px' } }} + cardValueStyle={{ minH: { base: '64px', md: '116px' } }} > { - Current number of consecutive days you{ apos }ve claimed your daily Merits.{ ' ' } - The longer your streak, the more daily Merits you can earn.{ ' ' } - - Share on X - - - ) } + description={ + `Current number of consecutive days you${ apos }ve claimed your daily Merits. The longer your streak, the more daily Merits you can earn.` + } hint={ ( <> See the{ ' ' } @@ -119,15 +112,20 @@ const RewardsDashboard = () => { ) } contentDirection="column-reverse" - cardValueStyle={{ minH: { base: '64px', md: '88px' } }} + cardValueStyle={{ minH: { base: '64px', md: '116px' } }} + contentAfter={ ( + + ) } > @@ -152,6 +150,14 @@ const RewardsDashboard = () => { ] } /> + { !isBadgesLoading && !dailyRewardQuery.isPending && ( + + ) } ); }; diff --git a/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_dark-color-mode_activity-tab-dark-mode-mobile-1.png b/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_dark-color-mode_activity-tab-dark-mode-mobile-1.png index 4362a9a293..3445672314 100644 Binary files a/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_dark-color-mode_activity-tab-dark-mode-mobile-1.png and b/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_dark-color-mode_activity-tab-dark-mode-mobile-1.png differ diff --git a/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_dark-color-mode_referrals-tab-dark-mode-mobile-1.png b/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_dark-color-mode_referrals-tab-dark-mode-mobile-1.png index bfa4bf9834..0140b13c30 100644 Binary files a/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_dark-color-mode_referrals-tab-dark-mode-mobile-1.png and b/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_dark-color-mode_referrals-tab-dark-mode-mobile-1.png differ diff --git a/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_dark-color-mode_resources-tab-dark-mode-mobile-1.png b/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_dark-color-mode_resources-tab-dark-mode-mobile-1.png index 1119942027..a0516f0777 100644 Binary files a/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_dark-color-mode_resources-tab-dark-mode-mobile-1.png and b/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_dark-color-mode_resources-tab-dark-mode-mobile-1.png differ diff --git a/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_default_activity-tab-dark-mode-mobile-1.png b/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_default_activity-tab-dark-mode-mobile-1.png index f58f092a87..73a9c17aa4 100644 Binary files a/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_default_activity-tab-dark-mode-mobile-1.png and b/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_default_activity-tab-dark-mode-mobile-1.png differ diff --git a/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_default_referrals-tab-dark-mode-mobile-1.png b/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_default_referrals-tab-dark-mode-mobile-1.png index d8f74cb396..40614c4a1b 100644 Binary files a/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_default_referrals-tab-dark-mode-mobile-1.png and b/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_default_referrals-tab-dark-mode-mobile-1.png differ diff --git a/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_default_resources-tab-dark-mode-mobile-1.png b/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_default_resources-tab-dark-mode-mobile-1.png index 6f80b5b6ba..d2dd967fd4 100644 Binary files a/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_default_resources-tab-dark-mode-mobile-1.png and b/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_default_resources-tab-dark-mode-mobile-1.png differ diff --git a/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_default_with-error-1.png b/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_default_with-error-1.png index 02c8a575a7..7260db4594 100644 Binary files a/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_default_with-error-1.png and b/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_default_with-error-1.png differ diff --git a/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_mobile_activity-tab-dark-mode-mobile-1.png b/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_mobile_activity-tab-dark-mode-mobile-1.png index 72eb98a54b..48e78809af 100644 Binary files a/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_mobile_activity-tab-dark-mode-mobile-1.png and b/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_mobile_activity-tab-dark-mode-mobile-1.png differ diff --git a/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_mobile_referrals-tab-dark-mode-mobile-1.png b/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_mobile_referrals-tab-dark-mode-mobile-1.png index 1e983a2d7c..ffb3dc0ec6 100644 Binary files a/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_mobile_referrals-tab-dark-mode-mobile-1.png and b/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_mobile_referrals-tab-dark-mode-mobile-1.png differ diff --git a/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_mobile_resources-tab-dark-mode-mobile-1.png b/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_mobile_resources-tab-dark-mode-mobile-1.png index 107100b7fb..1271bfa9c0 100644 Binary files a/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_mobile_resources-tab-dark-mode-mobile-1.png and b/ui/pages/__screenshots__/RewardsDashboard.pw.tsx_mobile_resources-tab-dark-mode-mobile-1.png differ diff --git a/ui/rewards/dashboard/DailyRewardClaimButton.tsx b/ui/rewards/dashboard/DailyRewardClaimButton.tsx index f7aee397d4..1dd843a373 100644 --- a/ui/rewards/dashboard/DailyRewardClaimButton.tsx +++ b/ui/rewards/dashboard/DailyRewardClaimButton.tsx @@ -1,4 +1,5 @@ import { Flex } from '@chakra-ui/react'; +import { useQueryClient } from '@tanstack/react-query'; import React, { useCallback, useEffect, useMemo } from 'react'; import { useRewardsContext } from 'lib/contexts/rewards'; @@ -7,6 +8,7 @@ import { SECOND } from 'toolkit/utils/consts'; import splitSecondsInPeriods from 'ui/blockCountdown/splitSecondsInPeriods'; const DailyRewardClaimButton = () => { + const queryClient = useQueryClient(); const { balancesQuery, dailyRewardQuery, claim } = useRewardsContext(); const [ isClaiming, setIsClaiming ] = React.useState(false); const [ timeLeft, setTimeLeft ] = React.useState(''); @@ -25,9 +27,10 @@ const DailyRewardClaimButton = () => { balancesQuery.refetch(), dailyRewardQuery.refetch(), ]); + queryClient.invalidateQueries({ queryKey: [ 'rewards:user_badges' ] }); } catch (error) {} setIsClaiming(false); - }, [ claim, setIsClaiming, balancesQuery, dailyRewardQuery ]); + }, [ claim, setIsClaiming, balancesQuery, dailyRewardQuery, queryClient ]); useEffect(() => { if (!dailyRewardQuery.data?.reset_at) { diff --git a/ui/rewards/dashboard/RewardsDashboardCard.tsx b/ui/rewards/dashboard/RewardsDashboardCard.tsx index 2ea3ab17f6..f7e09fade0 100644 --- a/ui/rewards/dashboard/RewardsDashboardCard.tsx +++ b/ui/rewards/dashboard/RewardsDashboardCard.tsx @@ -44,6 +44,7 @@ const RewardsDashboardCard = ({ pb={ contentDirection === 'column-reverse' ? { base: 1.5, md: 3 } : 0 } pt={ contentDirection === 'column-reverse' ? 0 : { base: 1.5, md: 3 } } w={{ base: 'full', md: contentDirection === 'row' ? '340px' : 'full' }} + flex={ 1 } > { label && { label } } { title && ( @@ -56,7 +57,9 @@ const RewardsDashboardCard = ({ { description } - { contentAfter } + + { contentAfter } + ( - +const RewardsDashboardCard = ({ label, value, withIcon, hint, isLoading, bottomText, isBottomTextLoading }: Props) => ( + { label && ( { hint && } @@ -40,8 +41,8 @@ const RewardsDashboardCard = ({ label, value, withIcon, hint, isLoading, bottomT { bottomText && ( - - + + { bottomText } diff --git a/ui/rewards/dashboard/streakModal/BadgeCard.tsx b/ui/rewards/dashboard/streakModal/BadgeCard.tsx new file mode 100644 index 0000000000..b0613c9a07 --- /dev/null +++ b/ui/rewards/dashboard/streakModal/BadgeCard.tsx @@ -0,0 +1,96 @@ +import { Flex, Text, Progress } from '@chakra-ui/react'; + +import type { GetAvailableBadgesResponse } from '@blockscout/points-types'; + +import { Image } from 'toolkit/chakra/image'; +import { Link } from 'toolkit/chakra/link'; + +const BADGE_BG_COLORS = [ '#DFE8F5', '#D2E5FE', '#EFE1FF' ]; + +const BADGES = [ + '/static/merits/streak_30.png', + '/static/merits/streak_90.png', + '/static/merits/streak_180.png', +] as const; + +const GHOST_BADGES = [ + '/static/merits/streak_30_ghost.png', + '/static/merits/streak_90_ghost.png', + '/static/merits/streak_180_ghost.png', +] as const; + +type Props = { + badge: GetAvailableBadgesResponse['items'][number]; + currentStreak: number; + index: number; +}; + +export default function BadgeCard({ badge, currentStreak, index }: Props) { + const target = Number(badge.requirements?.streak || 0); + const isUnlocked = badge.is_whitelisted || badge.is_minted; + const progress = Math.min(currentStreak, target); + + return ( + + + Streak badge + + + { target } Day streak + + { (() => { + if (badge.is_minted) { + return ( + + Minted + + ); + } + if (badge.is_whitelisted) { + return ( + + Mint a badge + + ); + } + return ( + <> + + { progress }/{ target } + + + + + + + + ); + })() } + + + + ); +} diff --git a/ui/rewards/dashboard/streakModal/ProgressSegment.tsx b/ui/rewards/dashboard/streakModal/ProgressSegment.tsx new file mode 100644 index 0000000000..b699250d30 --- /dev/null +++ b/ui/rewards/dashboard/streakModal/ProgressSegment.tsx @@ -0,0 +1,66 @@ +import { Flex, Text, Progress } from '@chakra-ui/react'; +import { clamp } from 'es-toolkit'; + +import IconSvg from 'ui/shared/IconSvg'; + +type Props = { + value: number; + target: number; + prevTarget: number; + isFirst: boolean; +}; + +export default function ProgressSegment({ value, target, prevTarget, isFirst }: Props) { + const isDone = value >= target; + const progress = clamp(value, prevTarget, target); + + return ( + + + + + + + + + + + + { isDone ? ( + + ) : ( + + ) } + + + { target } Days + + + ); +} diff --git a/ui/rewards/dashboard/streakModal/RewardsStreakModal.pw.tsx b/ui/rewards/dashboard/streakModal/RewardsStreakModal.pw.tsx new file mode 100644 index 0000000000..cb4b11364f --- /dev/null +++ b/ui/rewards/dashboard/streakModal/RewardsStreakModal.pw.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +import type { GetAvailableBadgesResponse } from '@blockscout/points-types'; + +import * as streakBadgesMock from 'mocks/rewards/streakBadges'; +import type { TestFnArgs } from 'playwright/lib'; +import { test, expect, devices } from 'playwright/lib'; + +import RewardsStreakModal from './RewardsStreakModal'; + +const onOpenChange = () => {}; + +const testFn = (streak: number, badges: GetAvailableBadgesResponse['items']) => + async({ render, page }: TestFnArgs) => { + await render( + , + ); + await expect(page).toHaveScreenshot(); + }; + +test('base view +@dark-mode', testFn(10, streakBadgesMock.base.items)); +test('filled view +@dark-mode', testFn(10, streakBadgesMock.filled.items)); + +test.describe('mobile', () => { + test.use({ viewport: devices['iPhone 13 Pro'].viewport }); + test('base view', testFn(10, streakBadgesMock.base.items)); + test('filled view', testFn(10, streakBadgesMock.filled.items)); +}); diff --git a/ui/rewards/dashboard/streakModal/RewardsStreakModal.tsx b/ui/rewards/dashboard/streakModal/RewardsStreakModal.tsx new file mode 100644 index 0000000000..007fb58618 --- /dev/null +++ b/ui/rewards/dashboard/streakModal/RewardsStreakModal.tsx @@ -0,0 +1,94 @@ +import { Flex, Text, Separator } from '@chakra-ui/react'; +import React from 'react'; + +import type { GetAvailableBadgesResponse } from '@blockscout/points-types'; + +import { DialogBody, DialogContent, DialogHeader, DialogRoot } from 'toolkit/chakra/dialog'; +import { Heading } from 'toolkit/chakra/heading'; + +import BadgeCard from './BadgeCard'; +import ProgressSegment from './ProgressSegment'; + +type Props = { + open: boolean; + onOpenChange: ({ open }: { open: boolean }) => void; + currentStreak: number; + badges?: GetAvailableBadgesResponse['items']; +}; + +const EMPTY_ARRAY: GetAvailableBadgesResponse['items'] = []; + +const RewardsStreakModal = ({ open, onOpenChange, currentStreak, badges = EMPTY_ARRAY }: Props) => { + + return ( + + + Streak progress + + + + + Build your streak day by day and unlock exclusive badges as a reward for staying consistent. + + + + { currentStreak } + Day streak + + + { badges.map((badge, i) => { + const target = Number(badge.requirements?.streak || 0); + const prevTarget = i > 0 ? Number(badges[i - 1]?.requirements?.streak || 0) : 0; + const value = (badge.is_whitelisted || badge.is_minted) ? target : currentStreak; + return ( + + ); + }) } + + + + + + Rewards + + { badges.map((badge, i) => ( + <> + + { i < badges.length - 1 && ( + + ) } + + )) } + + + + + + + ); +}; + +export default RewardsStreakModal; diff --git a/ui/rewards/dashboard/streakModal/__screenshots__/RewardsStreakModal.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/ui/rewards/dashboard/streakModal/__screenshots__/RewardsStreakModal.pw.tsx_dark-color-mode_base-view-dark-mode-1.png new file mode 100644 index 0000000000..07cd189659 Binary files /dev/null and b/ui/rewards/dashboard/streakModal/__screenshots__/RewardsStreakModal.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/ui/rewards/dashboard/streakModal/__screenshots__/RewardsStreakModal.pw.tsx_dark-color-mode_filled-view-dark-mode-1.png b/ui/rewards/dashboard/streakModal/__screenshots__/RewardsStreakModal.pw.tsx_dark-color-mode_filled-view-dark-mode-1.png new file mode 100644 index 0000000000..c8e8729ddf Binary files /dev/null and b/ui/rewards/dashboard/streakModal/__screenshots__/RewardsStreakModal.pw.tsx_dark-color-mode_filled-view-dark-mode-1.png differ diff --git a/ui/rewards/dashboard/streakModal/__screenshots__/RewardsStreakModal.pw.tsx_default_base-view-dark-mode-1.png b/ui/rewards/dashboard/streakModal/__screenshots__/RewardsStreakModal.pw.tsx_default_base-view-dark-mode-1.png new file mode 100644 index 0000000000..39e1b1876f Binary files /dev/null and b/ui/rewards/dashboard/streakModal/__screenshots__/RewardsStreakModal.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/ui/rewards/dashboard/streakModal/__screenshots__/RewardsStreakModal.pw.tsx_default_filled-view-dark-mode-1.png b/ui/rewards/dashboard/streakModal/__screenshots__/RewardsStreakModal.pw.tsx_default_filled-view-dark-mode-1.png new file mode 100644 index 0000000000..cc0ea4dc89 Binary files /dev/null and b/ui/rewards/dashboard/streakModal/__screenshots__/RewardsStreakModal.pw.tsx_default_filled-view-dark-mode-1.png differ diff --git a/ui/rewards/dashboard/streakModal/__screenshots__/RewardsStreakModal.pw.tsx_default_mobile-base-view-1.png b/ui/rewards/dashboard/streakModal/__screenshots__/RewardsStreakModal.pw.tsx_default_mobile-base-view-1.png new file mode 100644 index 0000000000..ab11ccf494 Binary files /dev/null and b/ui/rewards/dashboard/streakModal/__screenshots__/RewardsStreakModal.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/rewards/dashboard/streakModal/__screenshots__/RewardsStreakModal.pw.tsx_default_mobile-filled-view-1.png b/ui/rewards/dashboard/streakModal/__screenshots__/RewardsStreakModal.pw.tsx_default_mobile-filled-view-1.png new file mode 100644 index 0000000000..a5fbb94cc0 Binary files /dev/null and b/ui/rewards/dashboard/streakModal/__screenshots__/RewardsStreakModal.pw.tsx_default_mobile-filled-view-1.png differ diff --git a/ui/rewards/hooks/useStreakBadges.ts b/ui/rewards/hooks/useStreakBadges.ts new file mode 100644 index 0000000000..3543ae5a16 --- /dev/null +++ b/ui/rewards/hooks/useStreakBadges.ts @@ -0,0 +1,56 @@ +import { useMemo } from 'react'; + +import type { GetAvailableBadgesResponse } from '@blockscout/points-types'; + +import config from 'configs/app'; +import useApiQuery from 'lib/api/useApiQuery'; +import { useRewardsContext } from 'lib/contexts/rewards'; + +const feature = config.features.rewards; + +export default function useStreakBadges() { + const { apiToken, dailyRewardQuery } = useRewardsContext(); + + const badgesQuery = useApiQuery<'rewards:user_badges', unknown, GetAvailableBadgesResponse>('rewards:user_badges', { + queryOptions: { + enabled: feature.isEnabled && Boolean(apiToken), + select: (data) => ({ + ...data, + items: data.items + .sort((a, b) => Number(a.requirements?.streak || 0) - Number(b.requirements?.streak || 0)) + .slice(0, 3), // UI limit + }), + }, + fetchParams: { headers: { Authorization: `Bearer ${ apiToken }` } }, + }); + + const nextAchievementText = useMemo(() => { + try { + if (badgesQuery.isPending || dailyRewardQuery.isPending) { + return 'Next achievement in N/A day'; // for skeleton + } + if (!badgesQuery.data?.items || !dailyRewardQuery.data?.streak) { + return undefined; + } + const currentStreak = Number(dailyRewardQuery.data.streak); + const next = badgesQuery.data.items.find((b) => + !(b.is_qualified || b.is_whitelisted || b.is_minted) && + Number(b.requirements?.streak) > currentStreak, + ); + if (!next) { + return 'All achievements are earned'; + } + const target = Number(next.requirements?.streak || 0); + const diff = target - currentStreak; + return `Next achievement in ${ diff } day${ diff === 1 ? '' : 's' }`; + } catch { + return undefined; + } + }, [ badgesQuery, dailyRewardQuery ]); + + return { + badgesQuery, + nextAchievementText, + isLoading: badgesQuery.isPending, + }; +} diff --git a/yarn.lock b/yarn.lock index e95c0004d6..0edfc85b2e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1573,10 +1573,10 @@ resolved "https://registry.yarnpkg.com/@blockscout/multichain-aggregator-types/-/multichain-aggregator-types-1.6.0-alpha.2.tgz#13f07652029a5d3bdcf4a07207c22599e3a5b08a" integrity sha512-/013NEL+kC+vx3qOWWRTMhVFFifMjFXSRTjUkxnJPYrgdBZbYSNI1kB8vqe7z7nshu6uedkpgNMW2HpiVKUvQw== -"@blockscout/points-types@1.3.0-alpha.2": - version "1.3.0-alpha.2" - resolved "https://registry.yarnpkg.com/@blockscout/points-types/-/points-types-1.3.0-alpha.2.tgz#0308dcb4eef0dadf96f43b144835470e9f78f64f" - integrity sha512-tXCA51q3y08caCm7UhGyj+xsP0pd6yBhjElDHxEzM5SRop3culMiacaBXd0OPBszHjA0YdYgXFymuJhofB22ig== +"@blockscout/points-types@1.4.0-alpha.1": + version "1.4.0-alpha.1" + resolved "https://registry.yarnpkg.com/@blockscout/points-types/-/points-types-1.4.0-alpha.1.tgz#326885fcdda7c478440e4737efdfe105a29565fb" + integrity sha512-DaN42ulieCLjoKyDqVc5jCuho/A8g1k7OUpOMnVgc7k8D1esSlCg+LclJ7vrcigdUXsJ5cx2nlCkySKUOfr4mA== "@blockscout/stats-types@^2.9.0": version "2.9.0"