Commit 29f3a72f authored by Max Alekseenko's avatar Max Alekseenko

create rewards login modal and dashboard

parent 99446524
...@@ -23,6 +23,7 @@ export { default as multichainButton } from './multichainButton'; ...@@ -23,6 +23,7 @@ export { default as multichainButton } from './multichainButton';
export { default as nameService } from './nameService'; export { default as nameService } from './nameService';
export { default as publicTagsSubmission } from './publicTagsSubmission'; export { default as publicTagsSubmission } from './publicTagsSubmission';
export { default as restApiDocs } from './restApiDocs'; export { default as restApiDocs } from './restApiDocs';
export { default as rewards } from './rewards';
export { default as rollup } from './rollup'; export { default as rollup } from './rollup';
export { default as safe } from './safe'; export { default as safe } from './safe';
export { default as saveOnGas } from './saveOnGas'; export { default as saveOnGas } from './saveOnGas';
......
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const apiHost = getEnvValue('NEXT_PUBLIC_REWARDS_SERVICE_API_HOST');
const title = 'Rewards service integration';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (apiHost) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: apiHost,
basePath: '',
},
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
...@@ -62,4 +62,5 @@ NEXT_PUBLIC_SENTRY_ENABLE_TRACING=true ...@@ -62,4 +62,5 @@ NEXT_PUBLIC_SENTRY_ENABLE_TRACING=true
NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s.blockscout.com NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=noves NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=noves
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
\ No newline at end of file NEXT_PUBLIC_REWARDS_SERVICE_API_HOST=https://points.k8s-dev.blockscout.com
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.229 9.946c.401 0 .727.329.727.735v.636a.731.731 0 0 1-.727.735h-.353a.731.731 0 0 0-.727.735v6.532a.731.731 0 0 1-.727.735h-.695A.731.731 0 0 1 10 19.32v-6.532c0-.406.326-.735.727-.735h.352c.402 0 .728-.33.728-.736v-.635c0-.406.325-.735.727-.735h.694ZM17.54 9.946c.402 0 .728.329.728.735v.636c0 .405.326.735.727.735h.278c.401 0 .727.329.727.735v6.532a.731.731 0 0 1-.727.735h-.695a.731.731 0 0 1-.727-.735v-6.532a.731.731 0 0 0-.727-.735h-.278a.731.731 0 0 1-.727-.736v-.635c0-.406.326-.735.727-.735h.695ZM15.362 13.977c.402 0 .727.33.727.735v2.622a.731.731 0 0 1-.727.735h-.694a.731.731 0 0 1-.728-.735v-2.622c0-.406.326-.735.728-.735h.694Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.894 4.213a1.98 1.98 0 0 0-1.788 0l-8 4.044A2.024 2.024 0 0 0 5 10.065v9.87c0 .766.428 1.466 1.106 1.808l8 4.044a1.981 1.981 0 0 0 1.788 0l8-4.044A2.024 2.024 0 0 0 25 19.935v-9.87c0-.766-.428-1.466-1.106-1.808l-8-4.044ZM7 10.065l8-4.043 8 4.043v9.87l-8 4.043-8-4.043v-9.87Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 44 46" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#a)">
<path d="M20.692 8.355a2.614 2.614 0 0 1 2.615 0l9.856 5.69a2.615 2.615 0 0 1 1.307 2.264V27.69c0 .935-.498 1.798-1.307 2.265l-9.856 5.69a2.614 2.614 0 0 1-2.615 0l-9.856-5.69A2.614 2.614 0 0 1 9.53 27.69V16.309c0-.934.498-1.797 1.307-2.264l9.856-5.69Z" fill="url(#b)"/>
<path d="M23.787 7.523a3.574 3.574 0 0 0-3.575 0l-9.856 5.69A3.574 3.574 0 0 0 8.57 16.31V27.69c0 1.277.681 2.458 1.787 3.096l9.856 5.69a3.574 3.574 0 0 0 3.575 0l9.856-5.69a3.574 3.574 0 0 0 1.787-3.096V16.31a3.574 3.574 0 0 0-1.787-3.096l-9.856-5.69Z" stroke="#fff" stroke-width="1.92"/>
</g>
<path d="M20.692 8.355a2.614 2.614 0 0 1 2.615 0l9.856 5.69a2.615 2.615 0 0 1 1.307 2.264V27.69c0 .935-.498 1.798-1.307 2.265l-9.856 5.69a2.614 2.614 0 0 1-2.615 0l-9.856-5.69A2.614 2.614 0 0 1 9.53 27.69V16.309c0-.934.498-1.797 1.307-2.264l9.856-5.69Z" fill="url(#c)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.797 17.078a.838.838 0 0 0-.838-.838h-.8a.838.838 0 0 0-.837.838v.724a.838.838 0 0 1-.838.838h-.406a.838.838 0 0 0-.838.838v7.444c0 .463.375.838.838.838h.8a.838.838 0 0 0 .838-.838v-7.444c0-.463.375-.838.838-.838h.405a.838.838 0 0 0 .838-.838v-.724Zm4.968 0a.838.838 0 0 0-.838-.838h-.8a.838.838 0 0 0-.837.838v.724c0 .463.375.838.837.838h.32c.463 0 .838.375.838.838v7.444c0 .463.375.838.837.838h.8a.838.838 0 0 0 .838-.838v-7.444a.838.838 0 0 0-.838-.838h-.32a.838.838 0 0 1-.837-.838v-.724Zm-2.51 4.594a.838.838 0 0 0-.838-.838h-.8a.838.838 0 0 0-.837.838v2.987c0 .463.375.838.837.838h.8a.838.838 0 0 0 .838-.838v-2.987Z" fill="#fff"/>
<defs>
<linearGradient id="b" x1="22" y1="7.6" x2="22" y2="36.4" gradientUnits="userSpaceOnUse">
<stop stop-color="#2C5282"/>
<stop offset="1" stop-color="#153967"/>
</linearGradient>
<linearGradient id="c" x1="22" y1="7.6" x2="22" y2="36.4" gradientUnits="userSpaceOnUse">
<stop stop-color="#008BE4"/>
<stop offset="1" stop-color="#81C5F1"/>
</linearGradient>
<filter id="a" x="3.609" y="6.084" width="36.781" height="39.831" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix values="0 0 0 0 0.551643 0 0 0 0 0.703233 0 0 0 0 0.800684 0 0 0 0.25 0"/>
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_412_42663"/>
<feBlend in="SourceGraphic" in2="effect1_dropShadow_412_42663" result="shape"/>
</filter>
</defs>
</svg>
import { useBoolean } from '@chakra-ui/react';
import React, { createContext, useContext, useMemo } from 'react';
type Props = {
children: React.ReactNode;
}
type TRewardsContext = {
isLoginModalOpen: boolean;
openLoginModal: () => void;
closeLoginModal: () => void;
}
const RewardsContext = createContext<TRewardsContext>({
isLoginModalOpen: false,
openLoginModal: () => {},
closeLoginModal: () => {},
});
export function RewardsContextProvider({ children }: Props) {
const [ isLoginModalOpen, setIsLoginModalOpen ] = useBoolean(false);
const value = useMemo(() => ({
isLoginModalOpen,
openLoginModal: setIsLoginModalOpen.on,
closeLoginModal: setIsLoginModalOpen.off,
}), [ isLoginModalOpen, setIsLoginModalOpen ]);
return (
<RewardsContext.Provider value={ value }>
{ children }
</RewardsContext.Provider>
);
}
export function useRewardsContext() {
return useContext(RewardsContext);
}
...@@ -4,6 +4,7 @@ import React from 'react'; ...@@ -4,6 +4,7 @@ import React from 'react';
import type { NavItemInternal, NavItem, NavGroupItem } from 'types/client/navigation'; import type { NavItemInternal, NavItem, NavGroupItem } from 'types/client/navigation';
import config from 'configs/app'; import config from 'configs/app';
import { useRewardsContext } from 'lib/contexts/rewards';
import { rightLineArrow } from 'lib/html-entities'; import { rightLineArrow } from 'lib/html-entities';
import UserAvatar from 'ui/shared/UserAvatar'; import UserAvatar from 'ui/shared/UserAvatar';
...@@ -18,12 +19,13 @@ export function isGroupItem(item: NavItem | NavGroupItem): item is NavGroupItem ...@@ -18,12 +19,13 @@ export function isGroupItem(item: NavItem | NavGroupItem): item is NavGroupItem
} }
export function isInternalItem(item: NavItem): item is NavItemInternal { export function isInternalItem(item: NavItem): item is NavItemInternal {
return 'nextRoute' in item; return !('url' in item);
} }
export default function useNavItems(): ReturnType { export default function useNavItems(): ReturnType {
const router = useRouter(); const router = useRouter();
const pathname = router.pathname; const pathname = router.pathname;
const { openLoginModal: openRewardsLoginModal } = useRewardsContext();
return React.useMemo(() => { return React.useMemo(() => {
let blockchainNavItems: Array<NavItem> | Array<Array<NavItem>> = []; let blockchainNavItems: Array<NavItem> | Array<Array<NavItem>> = [];
...@@ -265,6 +267,13 @@ export default function useNavItems(): ReturnType { ...@@ -265,6 +267,13 @@ export default function useNavItems(): ReturnType {
].filter(Boolean); ].filter(Boolean);
const accountNavItems: ReturnType['accountNavItems'] = [ const accountNavItems: ReturnType['accountNavItems'] = [
config.features.rewards.isEnabled ? {
text: 'Merits',
// nextRoute: { pathname: '/account/rewards' as const },
onClick: openRewardsLoginModal,
icon: 'merits',
// isActive: pathname === '/account/rewards',
} : null,
{ {
text: 'Watch list', text: 'Watch list',
nextRoute: { pathname: '/account/watchlist' as const }, nextRoute: { pathname: '/account/watchlist' as const },
...@@ -305,5 +314,5 @@ export default function useNavItems(): ReturnType { ...@@ -305,5 +314,5 @@ export default function useNavItems(): ReturnType {
}; };
return { mainNavItems, accountNavItems, profileItem }; return { mainNavItems, accountNavItems, profileItem };
}, [ pathname ]); }, [ pathname, openRewardsLoginModal ]);
} }
...@@ -27,6 +27,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { ...@@ -27,6 +27,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/graphiql': 'Regular page', '/graphiql': 'Regular page',
'/search-results': 'Regular page', '/search-results': 'Regular page',
'/auth/profile': 'Root page', '/auth/profile': 'Root page',
'/account/rewards': 'Regular page',
'/account/watchlist': 'Regular page', '/account/watchlist': 'Regular page',
'/account/api-key': 'Regular page', '/account/api-key': 'Regular page',
'/account/custom-abi': 'Regular page', '/account/custom-abi': 'Regular page',
......
...@@ -31,6 +31,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -31,6 +31,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/graphiql': DEFAULT_TEMPLATE, '/graphiql': DEFAULT_TEMPLATE,
'/search-results': DEFAULT_TEMPLATE, '/search-results': DEFAULT_TEMPLATE,
'/auth/profile': DEFAULT_TEMPLATE, '/auth/profile': DEFAULT_TEMPLATE,
'/account/rewards': DEFAULT_TEMPLATE,
'/account/watchlist': DEFAULT_TEMPLATE, '/account/watchlist': DEFAULT_TEMPLATE,
'/account/api-key': DEFAULT_TEMPLATE, '/account/api-key': DEFAULT_TEMPLATE,
'/account/custom-abi': DEFAULT_TEMPLATE, '/account/custom-abi': DEFAULT_TEMPLATE,
......
...@@ -27,6 +27,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -27,6 +27,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/graphiql': 'GraphQL for %network_name% - %network_name% data query', '/graphiql': 'GraphQL for %network_name% - %network_name% data query',
'/search-results': '%network_name% search result for %q%', '/search-results': '%network_name% search result for %q%',
'/auth/profile': '%network_name% - my profile', '/auth/profile': '%network_name% - my profile',
'/account/rewards': '%network_name% - rewards',
'/account/watchlist': '%network_name% - watchlist', '/account/watchlist': '%network_name% - watchlist',
'/account/api-key': '%network_name% - API keys', '/account/api-key': '%network_name% - API keys',
'/account/custom-abi': '%network_name% - custom ABI', '/account/custom-abi': '%network_name% - custom ABI',
......
...@@ -25,6 +25,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -25,6 +25,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/graphiql': 'GraphQL', '/graphiql': 'GraphQL',
'/search-results': 'Search results', '/search-results': 'Search results',
'/auth/profile': 'Profile', '/auth/profile': 'Profile',
'/account/rewards': 'Merits',
'/account/watchlist': 'Watchlist', '/account/watchlist': 'Watchlist',
'/account/api-key': 'API keys', '/account/api-key': 'API keys',
'/account/custom-abi': 'Custom ABI', '/account/custom-abi': 'Custom ABI',
......
...@@ -75,7 +75,7 @@ Type extends EventTypes.VERIFY_TOKEN ? { ...@@ -75,7 +75,7 @@ Type extends EventTypes.VERIFY_TOKEN ? {
'Action': 'Form opened' | 'Submit'; 'Action': 'Form opened' | 'Submit';
} : } :
Type extends EventTypes.WALLET_CONNECT ? { Type extends EventTypes.WALLET_CONNECT ? {
'Source': 'Header' | 'Smart contracts' | 'Swap button'; 'Source': 'Header' | 'Smart contracts' | 'Swap button' | 'Merits';
'Status': 'Started' | 'Connected'; 'Status': 'Started' | 'Connected';
} : } :
Type extends EventTypes.WALLET_ACTION ? ( Type extends EventTypes.WALLET_ACTION ? (
......
...@@ -9,6 +9,7 @@ declare module "nextjs-routes" { ...@@ -9,6 +9,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/404"> | StaticRoute<"/404">
| StaticRoute<"/account/api-key"> | StaticRoute<"/account/api-key">
| StaticRoute<"/account/custom-abi"> | StaticRoute<"/account/custom-abi">
| StaticRoute<"/account/rewards">
| StaticRoute<"/account/tag-address"> | StaticRoute<"/account/tag-address">
| StaticRoute<"/account/verified-addresses"> | StaticRoute<"/account/verified-addresses">
| StaticRoute<"/account/watchlist"> | StaticRoute<"/account/watchlist">
......
...@@ -13,11 +13,13 @@ import useQueryClientConfig from 'lib/api/useQueryClientConfig'; ...@@ -13,11 +13,13 @@ import useQueryClientConfig from 'lib/api/useQueryClientConfig';
import { AppContextProvider } from 'lib/contexts/app'; import { AppContextProvider } from 'lib/contexts/app';
import { ChakraProvider } from 'lib/contexts/chakra'; import { ChakraProvider } from 'lib/contexts/chakra';
import { MarketplaceContextProvider } from 'lib/contexts/marketplace'; import { MarketplaceContextProvider } from 'lib/contexts/marketplace';
import { RewardsContextProvider } from 'lib/contexts/rewards';
import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection'; import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection';
import { growthBook } from 'lib/growthbook/init'; import { growthBook } from 'lib/growthbook/init';
import useLoadFeatures from 'lib/growthbook/useLoadFeatures'; import useLoadFeatures from 'lib/growthbook/useLoadFeatures';
import useNotifyOnNavigation from 'lib/hooks/useNotifyOnNavigation'; import useNotifyOnNavigation from 'lib/hooks/useNotifyOnNavigation';
import { SocketProvider } from 'lib/socket/context'; import { SocketProvider } from 'lib/socket/context';
import RewardsLoginModal from 'ui/rewards/RewardsLoginModal';
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary'; import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
import AppErrorGlobalContainer from 'ui/shared/AppError/AppErrorGlobalContainer'; import AppErrorGlobalContainer from 'ui/shared/AppError/AppErrorGlobalContainer';
import GoogleAnalytics from 'ui/shared/GoogleAnalytics'; import GoogleAnalytics from 'ui/shared/GoogleAnalytics';
...@@ -69,9 +71,12 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { ...@@ -69,9 +71,12 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<GrowthBookProvider growthbook={ growthBook }> <GrowthBookProvider growthbook={ growthBook }>
<ScrollDirectionProvider> <ScrollDirectionProvider>
<SocketProvider url={ `${ config.api.socket }${ config.api.basePath }/socket/v2` }> <SocketProvider url={ `${ config.api.socket }${ config.api.basePath }/socket/v2` }>
<MarketplaceContextProvider> <RewardsContextProvider>
{ getLayout(<Component { ...pageProps }/>) } <MarketplaceContextProvider>
</MarketplaceContextProvider> { getLayout(<Component { ...pageProps }/>) }
{ config.features.rewards.isEnabled && <RewardsLoginModal/> }
</MarketplaceContextProvider>
</RewardsContextProvider>
</SocketProvider> </SocketProvider>
</ScrollDirectionProvider> </ScrollDirectionProvider>
</GrowthBookProvider> </GrowthBookProvider>
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
const RewardsDashboard = dynamic(() => import('ui/pages/RewardsDashboard'), { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/account/rewards">
<RewardsDashboard/>
</PageNextJs>
);
};
export default Page;
export { account as getServerSideProps } from 'nextjs/getServerSideProps';
...@@ -85,6 +85,8 @@ ...@@ -85,6 +85,8 @@
| "link_external" | "link_external"
| "link" | "link"
| "lock" | "lock"
| "merits_colored"
| "merits"
| "minus" | "minus"
| "monaco/file" | "monaco/file"
| "monaco/folder-open" | "monaco/folder-open"
......
...@@ -15,8 +15,9 @@ type NavItemCommon = { ...@@ -15,8 +15,9 @@ type NavItemCommon = {
} & NavIconOrComponent; } & NavIconOrComponent;
export type NavItemInternal = NavItemCommon & { export type NavItemInternal = NavItemCommon & {
nextRoute: Route; nextRoute?: Route;
isActive?: boolean; isActive?: boolean;
onClick?: () => void;
} }
export type NavItemExternal = { export type NavItemExternal = {
......
import { Button, Flex, Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import CopyField from 'ui/rewards/CopyField';
import RewardsDashboardCard from 'ui/rewards/RewardsDashboardCard';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/links/LinkExternal';
import PageTitle from 'ui/shared/Page/PageTitle';
const RewardsDashboard = () => {
return (
<>
<PageTitle
title="Dashboard"
secondRow={ (
<>
The Blockscout Merits Program is just getting started! Learn more about the details,
features, and future plans in our <LinkExternal ml={ 1 } href="">blog post</LinkExternal>.
</>
) }
/>
<Flex flexDirection="column" alignItems="flex-start" w="full" gap={ 6 }>
<Button variant="outline">
Pre-staking dashboard
</Button>
<Flex gap={ 6 }>
<RewardsDashboardCard
description="Claim your daily merits and any merits received from referrals."
values={ [ { label: 'Total balance', value: 250 } ] }
contentAfter={ <Button>Claim X Merits</Button> }
/>
<RewardsDashboardCard
title="Title"
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit sed do."
values={ [ { label: 'Staked amount', value: 0 } ] }
availableSoon
/>
<RewardsDashboardCard
title="Title"
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit sed do."
values={ [ { label: 'Staking rewards', value: 0 } ] }
availableSoon
/>
</Flex>
<Flex
gap={ 10 }
w="full"
border="1px solid"
borderColor={ useColorModeValue('gray.200', 'whiteAlpha.200') }
borderRadius="lg"
p={ 2 }
>
<Flex flexDirection="column" gap={ 2 } p={ 3 } w="340px">
<Text fontSize="lg" fontWeight="500">
Referral program
</Text>
<Text fontSize="sm">
Refer friends and boost your merits! You receive a 10% bonus on all merits earned by your referrals.
</Text>
</Flex>
<Flex
flex={ 1 }
alignItems="center"
gap={ 6 }
borderRadius="8px"
backgroundColor={ useColorModeValue('gray.50', 'whiteAlpha.50') }
px={ 6 }
flexShrink={ 0 }
>
<CopyField label="Referral link" value="blockscout.com/ref/0x789a9201d10029139101"/>
<CopyField label="Referral code" value="CODE10"/>
<Flex flexDirection="column">
<Flex alignItems="center" gap={ 1 } w="120px">
<IconSvg name="info" boxSize={ 5 } color="gray.500"/>
<Text fontSize="xs" fontWeight="500" variant="secondary">
Referrals
</Text>
</Flex>
<Text fontSize="32px" fontWeight="500">
0
</Text>
</Flex>
</Flex>
</Flex>
<Flex gap={ 6 }>
<RewardsDashboardCard
title="Activity"
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
values={ [ { label: 'Activity', value: 0, type: 'percentages' }, { label: 'Received', value: 0 } ] }
availableSoon
/>
<RewardsDashboardCard
title="Verify contracts"
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
values={ [ { label: 'Activity', value: 0, type: 'percentages' }, { label: 'Received', value: 0 } ] }
availableSoon
/>
</Flex>
</Flex>
</>
);
};
export default RewardsDashboard;
import { Flex, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const AvailableSoonLabel = () => (
<Flex
px={ 1 }
borderRadius="sm"
backgroundColor={ useColorModeValue('blue.50', 'blue.800') }
color={ useColorModeValue('blue.500', 'blue.100') }
fontSize="sm"
fontWeight="500"
h={ 6 }
alignItems="center"
>
Available soon
</Flex>
);
export default AvailableSoonLabel;
import { FormControl, Input, InputGroup, InputRightElement, chakra } from '@chakra-ui/react';
import React from 'react';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props = {
label: string;
value: string;
className?: string;
};
const CopyField = ({ label, value, className }: Props) => (
<FormControl variant="floating" id={ label } className={ className }>
<InputGroup>
<Input readOnly fontWeight="500" value={ value } overflow="hidden" textOverflow="ellipsis" pr="40px !important"/>
<InputPlaceholder text={ label }/>
<InputRightElement w="40px" display="flex" justifyContent="flex-end" pr={ 2 }>
<CopyToClipboard text={ value }/>
</InputRightElement>
</InputGroup>
</FormControl>
);
export default chakra(CopyField);
import { Flex, Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import IconSvg from 'ui/shared/IconSvg';
import AvailableSoonLabel from './AvailableSoonLabel';
type Value = {
label: string;
value: number;
type?: 'percentages';
}
type Props = {
title?: string;
description: string;
values: Array<Value>;
availableSoon?: boolean;
contentAfter?: React.ReactNode;
};
const RewardsDashboardCard = ({ title, description, values, availableSoon, contentAfter }: Props) => {
return (
<Flex
flexDirection="column"
p={ 2 }
border="1px solid"
borderColor={ useColorModeValue('gray.200', 'whiteAlpha.200') }
borderRadius="lg"
gap={ 1 }
>
<Flex
alignItems="center"
justifyContent="space-around"
borderRadius="8px"
backgroundColor={ useColorModeValue('gray.50', 'whiteAlpha.50') }
h="128px"
filter="auto"
blur={ availableSoon ? '4px' : '0' }
>
{ values.map(({ label, value, type }) => (
<Flex key={ label } flexDirection="column" alignItems="center" gap={ 2 }>
<Flex alignItems="center" gap={ 1 }>
<IconSvg name="info" boxSize={ 5 } color="gray.500"/>
<Text fontSize="xs" fontWeight="500" variant="secondary">
{ label }
</Text>
</Flex>
<Flex alignItems="center">
{ !type && <IconSvg name="merits_colored" boxSize={ 12 }/> }
<Text fontSize="32px" fontWeight="500">
{ type === 'percentages' ? `${ value }%` : value }
</Text>
</Flex>
</Flex>
)) }
</Flex>
<Flex flexDirection="column" gap={ 2 } p={ 3 }>
{ title && (
<Flex alignItems="center" gap={ 2 }>
<Text fontSize="lg" fontWeight="500">{ title }</Text>
{ availableSoon && <AvailableSoonLabel/> }
</Flex>
) }
<Text fontSize="sm">
{ description }
</Text>
{ contentAfter }
</Flex>
</Flex>
);
};
export default RewardsDashboardCard;
import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, useBoolean } from '@chakra-ui/react';
import React, { useEffect } from 'react';
import { useRewardsContext } from 'lib/contexts/rewards';
import useIsMobile from 'lib/hooks/useIsMobile';
import useWallet from 'ui/snippets/walletMenu/useWallet';
import CongratsStepContent from './steps/CongratsStepContent';
import LoginStepContent from './steps/LoginStepContent';
const RewardsLoginModal = () => {
const { isModalOpen: isWalletModalOpen } = useWallet({ source: 'Merits' });
const isMobile = useIsMobile();
const { isLoginModalOpen, closeLoginModal } = useRewardsContext();
const [ isLoginStep, setIsLoginStep ] = useBoolean(true);
useEffect(() => {
if (!isLoginModalOpen) {
setIsLoginStep.on();
}
}, [ isLoginModalOpen, setIsLoginStep ]);
return (
<Modal
isOpen={ isLoginModalOpen && !isWalletModalOpen }
onClose={ closeLoginModal }
size={ isMobile ? 'full' : 'sm' }
isCentered
>
<ModalOverlay/>
<ModalContent width={ isLoginStep ? '400px' : '560px' } p={ 6 }>
<ModalHeader fontWeight="500" textStyle="h3" mb={ 3 }>
{ isLoginStep ? 'Login' : 'Congratulations' }
</ModalHeader>
<ModalCloseButton top={ 6 } right={ 6 }/>
<ModalBody mb={ 0 }>
{ isLoginStep ?
<LoginStepContent goNext={ setIsLoginStep.off }/> :
<CongratsStepContent/>
}
</ModalBody>
</ModalContent>
</Modal>
);
};
export default RewardsLoginModal;
import { Text, Box, Flex, useColorModeValue, Button } from '@chakra-ui/react';
import React from 'react';
import IconSvg from 'ui/shared/IconSvg';
import AvailableSoonLabel from '../AvailableSoonLabel';
import CopyField from '../CopyField';
const CongratsStepContent = () => {
return (
<>
<Flex
flexDirection="column"
background="linear-gradient(254.96deg, #9CD8FF 9.09%, #D0EFFF 88.45%)"
borderRadius="md"
padding={ 2 }
pt={ 6 }
mb={ 8 }
>
<Flex alignItems="center" pl={ 2 } mb={ 4 }>
<IconSvg name="merits_colored" boxSize={ 16 }/>
<Text fontSize="30px" fontWeight="700" color="blue.700">
+250
</Text>
</Flex>
<Flex
flexDirection="column"
backgroundColor={ useColorModeValue('white', 'gray.900') }
borderRadius="8px"
padding={ 4 }
gap={ 2 }
>
<Flex alignItems="center" gap={ 2 }>
<Text fontSize="lg" fontWeight="500">
Pre-staking
</Text>
<AvailableSoonLabel/>
</Flex>
<Text fontSize="sm">Support your favorite networks and earn 10% APR</Text>
</Flex>
</Flex>
<Flex flexDirection="column" alignItems="flex-start" px={ 3 } mb={ 8 }>
<Flex alignItems="center" gap={ 2 }>
<Box w={ 8 } h={ 8 } p={ 1.5 } borderRadius="8px" backgroundColor="blue.50">
<IconSvg name="profile" boxSize={ 5 } color="blue.500"/>
</Box>
<Text fontSize="lg" fontWeight="500">
Referral program
</Text>
</Flex>
<Text fontSize="md" mt={ 2 }>
Receive a 10% bonus on all merits earned by your referrals
</Text>
<CopyField label="Code" value="Test value" mt={ 3 }/>
<Button mt={ 6 }>
Share on <IconSvg name="social/twitter" boxSize={ 6 } ml={ 1 }/>
</Button>
</Flex>
<Flex flexDirection="column" alignItems="flex-start" px={ 3 }>
<Flex alignItems="center" gap={ 2 }>
<Box w={ 8 } h={ 8 } p={ 1 } borderRadius="8px" backgroundColor="blue.50">
<IconSvg name="stats" boxSize={ 6 } color="blue.500"/>
</Box>
<Text fontSize="lg" fontWeight="500">
Dashboard
</Text>
</Flex>
<Text fontSize="md" mt={ 2 }>
Explore your current merits balance, find activities to boost your merits,
and view your capybara NFT badge collection on the dashboard
</Text>
<Button mt={ 3 }>
Open
</Button>
</Flex>
</>
);
};
export default CongratsStepContent;
import { Text, Button, useColorModeValue, Image, Box, Flex, Switch, useBoolean, Input, FormControl } from '@chakra-ui/react';
import React from 'react';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import LinkExternal from 'ui/shared/links/LinkExternal';
import useWallet from 'ui/snippets/walletMenu/useWallet';
const LoginStepContent = ({ goNext }: { goNext: () => void }) => {
const { connect, isWalletConnected } = useWallet({ source: 'Merits' });
const [ isSwitchChecked, setIsSwitchChecked ] = useBoolean(false);
const dividerColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<>
<Image src="/static/merits_program.png" alt="Rewards program" mb={ 3 }/>
<Box mb={ 6 }>
Merits are awarded for a variety of different Blockscout activities. Connect a wallet to get started.
<LinkExternal href="https://docs.blockscout.com/using-blockscout/my-account/merits" ml={ 1 } fontWeight="500">
More about Blockscout Merits
</LinkExternal>
</Box>
{ isWalletConnected && (
<>
<Box w="full" mb={ 6 } borderTop="1px solid" borderColor={ dividerColor }/>
<Flex w="full" alignItems="center" justifyContent="space-between">
I have a referral code
<Switch
colorScheme="blue"
size="md"
isChecked={ isSwitchChecked }
onChange={ setIsSwitchChecked.toggle }
aria-label="Referral code switch"
/>
</Flex>
{ isSwitchChecked && (
<FormControl variant="floating" id="referral-code" mt={ 3 }>
<Input fontWeight="500"/>
<InputPlaceholder text="Code"/>
</FormControl>
) }
</>
) }
<Button
variant="solid"
colorScheme="blue"
w="full"
mt={ isWalletConnected ? 6 : 0 }
mb={ 4 }
onClick={ isWalletConnected ? goNext : connect }
>
{ isWalletConnected ? 'Get started' : 'Connect wallet' }
</Button>
<Text fontSize="sm" color={ useColorModeValue('blackAlpha.500', 'whiteAlpha.500') } textAlign="center">
Already registered for Blockscout Merits on another network or chain? Connect the same wallet here.
</Text>
</>
);
};
export default LoginStepContent;
...@@ -7,5 +7,5 @@ export function checkRouteHighlight(item: NavItem | Array<NavItem> | Array<Array ...@@ -7,5 +7,5 @@ export function checkRouteHighlight(item: NavItem | Array<NavItem> | Array<Array
if (Array.isArray(item)) { if (Array.isArray(item)) {
return item.some((subItem) => checkRouteHighlight(subItem)); return item.some((subItem) => checkRouteHighlight(subItem));
} }
return isInternalItem(item) && (config.UI.navigation.highlightedRoutes.includes(item.nextRoute.pathname)); return isInternalItem(item) && item.nextRoute !== undefined && (config.UI.navigation.highlightedRoutes.includes(item.nextRoute.pathname));
} }
...@@ -34,7 +34,12 @@ const NavLink = ({ item, isCollapsed, px, className, onClick, disableActiveState ...@@ -34,7 +34,12 @@ const NavLink = ({ item, isCollapsed, px, className, onClick, disableActiveState
const styleProps = useNavLinkStyleProps({ isCollapsed, isExpanded, isActive: isInternalLink && item.isActive && !disableActiveState }); const styleProps = useNavLinkStyleProps({ isCollapsed, isExpanded, isActive: isInternalLink && item.isActive && !disableActiveState });
const isXLScreen = useBreakpointValue({ base: false, xl: true }); const isXLScreen = useBreakpointValue({ base: false, xl: true });
const href = isInternalLink ? route(item.nextRoute) : item.url; let href;
if (isInternalLink) {
href = item.nextRoute ? route(item.nextRoute) : undefined;
} else if ('url' in item) {
href = item.url;
}
const isHighlighted = checkRouteHighlight(item); const isHighlighted = checkRouteHighlight(item);
...@@ -49,7 +54,7 @@ const NavLink = ({ item, isCollapsed, px, className, onClick, disableActiveState ...@@ -49,7 +54,7 @@ const NavLink = ({ item, isCollapsed, px, className, onClick, disableActiveState
px={ px || { base: 2, lg: isExpanded ? 2 : '15px', xl: isCollapsed ? '15px' : 2 } } px={ px || { base: 2, lg: isExpanded ? 2 : '15px', xl: isCollapsed ? '15px' : 2 } }
aria-label={ `${ item.text } link` } aria-label={ `${ item.text } link` }
whiteSpace="nowrap" whiteSpace="nowrap"
onClick={ onClick } onClick={ 'onClick' in item ? item.onClick : onClick }
_hover={{ _hover={{
[`& *:not(.${ LIGHTNING_LABEL_CLASS_NAME }, .${ LIGHTNING_LABEL_CLASS_NAME } *)`]: { [`& *:not(.${ LIGHTNING_LABEL_CLASS_NAME }, .${ LIGHTNING_LABEL_CLASS_NAME } *)`]: {
color: 'link_hovered', color: 'link_hovered',
...@@ -82,7 +87,7 @@ const NavLink = ({ item, isCollapsed, px, className, onClick, disableActiveState ...@@ -82,7 +87,7 @@ const NavLink = ({ item, isCollapsed, px, className, onClick, disableActiveState
return ( return (
<Box as="li" listStyleType="none" w="100%" className={ className }> <Box as="li" listStyleType="none" w="100%" className={ className }>
{ isInternalLink ? ( { isInternalLink && item.nextRoute ? (
<NextLink href={ item.nextRoute } passHref legacyBehavior> <NextLink href={ item.nextRoute } passHref legacyBehavior>
{ content } { content }
</NextLink> </NextLink>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment