From af8ba2b84131ba448e86af8faaf726c730a10876 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 23 Jun 2023 13:52:13 +0300 Subject: [PATCH 01/21] TW-826: Collectibles re-design From 8bc0474024bce8fa4e1ab1c9a26e3f10d5644df5 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 24 Jun 2023 03:05:47 +0300 Subject: [PATCH 02/21] TW-828: Collectibles grid layout --- src/app/atoms/ActivitySpinner.tsx | 7 +- src/app/icons/manage.svg | 3 + .../pages/Collectibles/CollectibleItem.tsx | 28 ++- .../Collectibles/CollectibleItemImage.tsx | 55 ++++++ ...llectiblesList.tsx => CollectiblesTab.tsx} | 50 +++--- src/app/pages/Home/ContentSection.tsx | 165 ++++++++++++++++++ src/app/pages/Home/Home.tsx | 160 +---------------- .../Home/OtherComponents/Tokens/Tokens.tsx | 2 +- src/app/templates/AssetIcon.tsx | 7 +- src/lib/ui/Image.tsx | 4 +- 10 files changed, 276 insertions(+), 205 deletions(-) create mode 100644 src/app/icons/manage.svg create mode 100644 src/app/pages/Collectibles/CollectibleItemImage.tsx rename src/app/pages/Collectibles/{CollectiblesList.tsx => CollectiblesTab.tsx} (65%) create mode 100644 src/app/pages/Home/ContentSection.tsx diff --git a/src/app/atoms/ActivitySpinner.tsx b/src/app/atoms/ActivitySpinner.tsx index 7db3606e3..5979b6f8a 100644 --- a/src/app/atoms/ActivitySpinner.tsx +++ b/src/app/atoms/ActivitySpinner.tsx @@ -1,13 +1,16 @@ import * as React from 'react'; +import clsx from 'clsx'; + import Spinner from './Spinner/Spinner'; interface ActivitySpinnerProps { height?: string; + className?: string; } -export const ActivitySpinner: React.FC = ({ height = '21px' }) => ( -
+export const ActivitySpinner: React.FC = ({ height = '21px', className }) => ( +
); diff --git a/src/app/icons/manage.svg b/src/app/icons/manage.svg new file mode 100644 index 000000000..85bdefb0f --- /dev/null +++ b/src/app/icons/manage.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/app/pages/Collectibles/CollectibleItem.tsx b/src/app/pages/Collectibles/CollectibleItem.tsx index 2d80dcab4..984580a7d 100644 --- a/src/app/pages/Collectibles/CollectibleItem.tsx +++ b/src/app/pages/Collectibles/CollectibleItem.tsx @@ -1,6 +1,7 @@ import React, { FC, useCallback, useRef, useState } from 'react'; -import { AssetIcon } from 'app/templates/AssetIcon'; +import { useAppEnv } from 'app/env'; +import { CollectibleItemImage } from 'app/pages/Collectibles/CollectibleItemImage'; import { useAssetMetadata, getAssetName } from 'lib/metadata'; import { useIntersectionDetection } from 'lib/ui/use-intersection-detection'; import { Link } from 'lib/woozie'; @@ -9,9 +10,11 @@ interface Props { assetSlug: string; index: number; itemsLength: number; + detailsShown?: boolean; } -export const CollectibleItem: FC = ({ assetSlug, index, itemsLength }) => { +export const CollectibleItem: FC = ({ assetSlug, detailsShown }) => { + const { popup } = useAppEnv(); const metadata = useAssetMetadata(assetSlug); const toDisplayRef = useRef(null); const [displayed, setDisplayed] = useState(true); @@ -25,21 +28,16 @@ export const CollectibleItem: FC = ({ assetSlug, index, itemsLength }) => if (metadata == null) return null; return ( - -
-
-
- {displayed && } -
-
-
+ +
+ {displayed && } +
+ + {detailsShown && ( +

{getAssetName(metadata)}

-
+ )} ); }; diff --git a/src/app/pages/Collectibles/CollectibleItemImage.tsx b/src/app/pages/Collectibles/CollectibleItemImage.tsx new file mode 100644 index 000000000..4c35a22c3 --- /dev/null +++ b/src/app/pages/Collectibles/CollectibleItemImage.tsx @@ -0,0 +1,55 @@ +import React, { FC, memo, useMemo } from 'react'; + +import { ReactComponent as CollectiblePlaceholderSvg } from 'app/icons/collectible-placeholder.svg'; +import { AssetMetadataBase, isCollectible } from 'lib/metadata'; +import { buildTokenIconURLs, buildCollectibleImageURLs } from 'lib/temple/front'; +import { Image } from 'lib/ui/Image'; + +interface Props { + assetSlug: string; + metadata: AssetMetadataBase; + className?: string; + size?: number; + style?: React.CSSProperties; +} + +export const CollectibleItemImage: FC = memo(({ metadata, assetSlug, className, size, style }) => { + const src = useMemo(() => { + if (metadata && isCollectible(metadata)) return buildCollectibleImageURLs(assetSlug, metadata, size == null); + else return buildTokenIconURLs(metadata?.thumbnailUri, size == null); + }, [metadata, assetSlug]); + + const styleMemo: React.CSSProperties = useMemo( + () => ({ + objectFit: 'contain', + maxWidth: '100%', + maxHeight: '100%', + ...style + }), + [style] + ); + + return ( + } + fallback={} + alt={metadata?.name} + className={className} + style={styleMemo} + height={size} + width={size} + /> + ); +}); + +interface PlaceholderProps { + metadata: AssetMetadataBase | nullish; + size?: number; +} + +const ImagePlaceholder: FC = ({ size }) => { + const styleMemo = useMemo(() => ({ maxWidth: `${size}px`, width: '100%', height: '100%' }), [size]); + + return ; +}; diff --git a/src/app/pages/Collectibles/CollectiblesList.tsx b/src/app/pages/Collectibles/CollectiblesTab.tsx similarity index 65% rename from src/app/pages/Collectibles/CollectiblesList.tsx rename to src/app/pages/Collectibles/CollectiblesTab.tsx index 70f26213b..560f106ed 100644 --- a/src/app/pages/Collectibles/CollectiblesList.tsx +++ b/src/app/pages/Collectibles/CollectiblesTab.tsx @@ -1,20 +1,20 @@ import React from 'react'; -import classNames from 'clsx'; +import clsx from 'clsx'; import { ActivitySpinner } from 'app/atoms'; import { useAppEnv } from 'app/env'; -import { ReactComponent as AddToListIcon } from 'app/icons/add-to-list.svg'; +import { ReactComponent as ManageIcon } from 'app/icons/manage.svg'; import { CollectibleItem } from 'app/pages/Collectibles/CollectibleItem'; import { AssetsSelectors } from 'app/pages/Home/OtherComponents/Assets.selectors'; import SearchAssetField from 'app/templates/SearchAssetField'; import { AssetTypesEnum } from 'lib/assets/types'; -import { T } from 'lib/i18n'; +import { T, t } from 'lib/i18n'; import { useAccount, useChainId, useCollectibleTokens, useFilteredAssets } from 'lib/temple/front'; import { useSyncTokens } from 'lib/temple/front/sync-tokens'; import { Link } from 'lib/woozie'; -export const CollectiblesList = () => { +export const CollectiblesTab = () => { const chainId = useChainId(true)!; const { popup } = useAppEnv(); const { publicKeyHash } = useAccount(); @@ -27,9 +27,9 @@ export const CollectiblesList = () => { const { filteredAssets, searchValue, setSearchValue } = useFilteredAssets(collectibleSlugs); return ( -
-
-
+
+
+
{ - - +
-
- {filteredAssets.length === 0 ? ( + + {isSyncing && filteredAssets.length === 0 ? ( + + ) : filteredAssets.length === 0 ? ( +

- ) : ( - <> +
+ ) : ( + <> +
{filteredAssets.map((slug, index) => ( ))} - - )} - {isSyncing && ( -
-
- )} -
+ {isSyncing && } + + )}
); diff --git a/src/app/pages/Home/ContentSection.tsx b/src/app/pages/Home/ContentSection.tsx new file mode 100644 index 000000000..0863a8659 --- /dev/null +++ b/src/app/pages/Home/ContentSection.tsx @@ -0,0 +1,165 @@ +import React, { FC, ReactNode, Suspense, useMemo } from 'react'; + +import classNames from 'clsx'; + +import Spinner from 'app/atoms/Spinner/Spinner'; +import { useTabSlug } from 'app/atoms/useTabSlug'; +import { useAppEnv } from 'app/env'; +import ErrorBoundary from 'app/ErrorBoundary'; +import { ActivityComponent } from 'app/templates/activity/Activity'; +import AssetInfo from 'app/templates/AssetInfo'; +import { isTezAsset } from 'lib/assets'; +import { T, t, TID } from 'lib/i18n'; +import { Link } from 'lib/woozie'; + +import { useUserTestingGroupNameSelector } from '../../store/ab-testing/selectors'; +import { CollectiblesTab } from '../Collectibles/CollectiblesTab'; +import { HomeSelectors } from './Home.selectors'; +import BakingSection from './OtherComponents/BakingSection'; +import { TokensTab } from './OtherComponents/Tokens/Tokens'; + +const Delegation: FC = () => ( + + + +); + +type ActivityTabProps = { + assetSlug?: string; +}; + +const ActivityTab: FC = ({ assetSlug }) => ( + + + +); + +type Props = { + assetSlug?: string | null; + className?: string; +}; + +interface TabData { + slug: string; + titleI18nKey: TID; + Component: FC; + testID: string; +} + +export const ContentSection: FC = ({ assetSlug, className }) => { + const { fullPage } = useAppEnv(); + const tabSlug = useTabSlug(); + const testGroupName = useUserTestingGroupNameSelector(); + + const tabs = useMemo(() => { + if (!assetSlug) { + return [ + { + slug: 'tokens', + titleI18nKey: 'tokens', + Component: TokensTab, + testID: HomeSelectors.assetsTab + }, + { + slug: 'collectibles', + titleI18nKey: 'collectibles', + Component: CollectiblesTab, + testID: HomeSelectors.collectiblesTab + }, + { + slug: 'activity', + titleI18nKey: 'activity', + Component: ActivityTab, + testID: HomeSelectors.activityTab + } + ]; + } + + const activity: TabData = { + slug: 'activity', + titleI18nKey: 'activity', + Component: () => , + testID: HomeSelectors.activityTab + }; + + if (isTezAsset(assetSlug)) { + return [ + activity, + { + slug: 'delegation', + titleI18nKey: 'delegate', + Component: Delegation, + testID: HomeSelectors.delegationTab + } + ]; + } + + return [ + activity, + { + slug: 'info', + titleI18nKey: 'info', + Component: () => , + testID: HomeSelectors.aboutTab + } + ]; + }, [assetSlug]); + + const { slug, Component } = useMemo(() => { + const tab = tabSlug ? tabs.find(currentTab => currentTab.slug === tabSlug) : null; + return tab ?? tabs[0]; + }, [tabSlug, tabs]); + + return ( +
+
+ {tabs.map(currentTab => { + const active = slug === currentTab.slug; + + return ( + ({ ...lctn, search: `?tab=${currentTab.slug}` })} + replace + className={classNames( + 'flex1 w-full', + 'text-center cursor-pointer py-2', + 'text-gray-500 text-xs font-medium', + 'border-t-3', + active ? 'border-primary-orange' : 'border-transparent', + active ? 'text-primary-orange' : 'hover:text-primary-orange', + 'transition ease-in-out duration-300', + 'truncate' + )} + testID={currentTab.testID} + testIDProperties={{ + ...(currentTab.slug === 'delegation' && { abTestingCategory: testGroupName }) + }} + > + + + ); + })} +
+ + {Component && } +
+ ); +}; + +interface SuspenseContainerProps extends PropsWithChildren { + whileMessage: string; + fallback?: ReactNode; +} + +const SuspenseContainer: FC = ({ whileMessage, fallback = , children }) => ( + + {children} + +); + +const SpinnerSection: FC = () => ( +
+ +
+); diff --git a/src/app/pages/Home/Home.tsx b/src/app/pages/Home/Home.tsx index d592c7c69..7861de7e3 100644 --- a/src/app/pages/Home/Home.tsx +++ b/src/app/pages/Home/Home.tsx @@ -1,23 +1,18 @@ -import React, { FC, FunctionComponent, ReactNode, Suspense, SVGProps, useLayoutEffect, useMemo } from 'react'; +import React, { FC, FunctionComponent, SVGProps, useLayoutEffect, useMemo } from 'react'; import classNames from 'clsx'; import { Props as TippyProps } from 'tippy.js'; import { Anchor } from 'app/atoms'; -import Spinner from 'app/atoms/Spinner/Spinner'; -import { useTabSlug } from 'app/atoms/useTabSlug'; import { useAppEnv } from 'app/env'; -import ErrorBoundary from 'app/ErrorBoundary'; import { ReactComponent as BuyIcon } from 'app/icons/buy.svg'; import { ReactComponent as ReceiveIcon } from 'app/icons/receive.svg'; import { ReactComponent as SendIcon } from 'app/icons/send-alt.svg'; import { ReactComponent as SwapIcon } from 'app/icons/swap.svg'; import { ReactComponent as WithdrawIcon } from 'app/icons/withdraw.svg'; import PageLayout from 'app/layouts/PageLayout'; -import { ActivityComponent } from 'app/templates/activity/Activity'; -import AssetInfo from 'app/templates/AssetInfo'; import { TestIDProps } from 'lib/analytics'; -import { TEZ_TOKEN_SLUG, isTezAsset } from 'lib/assets'; +import { TEZ_TOKEN_SLUG } from 'lib/assets'; import { T, t } from 'lib/i18n'; import { useAssetMetadata, getAssetSymbol } from 'lib/metadata'; import { useAccount, useNetwork } from 'lib/temple/front'; @@ -26,15 +21,12 @@ import useTippy from 'lib/ui/useTippy'; import { createUrl, HistoryAction, Link, navigate, To, useLocation } from 'lib/woozie'; import { createLocationState } from 'lib/woozie/location'; -import { useUserTestingGroupNameSelector } from '../../store/ab-testing/selectors'; -import { CollectiblesList } from '../Collectibles/CollectiblesList'; import { useOnboardingProgress } from '../Onboarding/hooks/useOnboardingProgress.hook'; import Onboarding from '../Onboarding/Onboarding'; +import { ContentSection } from './ContentSection'; import { HomeSelectors } from './Home.selectors'; -import BakingSection from './OtherComponents/BakingSection'; import EditableTitle from './OtherComponents/EditableTitle'; import MainBanner from './OtherComponents/MainBanner'; -import { Tokens } from './OtherComponents/Tokens/Tokens'; type ExploreProps = { assetSlug?: string | null; @@ -141,7 +133,7 @@ const Home: FC = ({ assetSlug }) => {
- + ) : ( @@ -218,147 +210,3 @@ const ActionButton: FC = ({ return ; }; - -const Delegation: FC = () => ( - - - -); - -type ActivityTabProps = { - assetSlug?: string; -}; - -const ActivityTab: FC = ({ assetSlug }) => ( - - - -); - -type SecondarySectionProps = { - assetSlug?: string | null; - className?: string; -}; - -const SecondarySection: FC = ({ assetSlug, className }) => { - const { fullPage } = useAppEnv(); - const tabSlug = useTabSlug(); - const testGroupName = useUserTestingGroupNameSelector(); - - const tabs = useMemo< - { - slug: string; - title: string; - Component: FC; - testID: string; - }[] - >(() => { - if (!assetSlug) { - return [ - { - slug: 'tokens', - title: t('tokens'), - Component: Tokens, - testID: HomeSelectors.assetsTab - }, - { - slug: 'collectibles', - title: t('collectibles'), - Component: CollectiblesList, - testID: HomeSelectors.collectiblesTab - }, - { - slug: 'activity', - title: t('activity'), - Component: ActivityTab, - testID: HomeSelectors.activityTab - } - ]; - } - - const activity = { - slug: 'activity', - title: t('activity'), - Component: () => , - testID: HomeSelectors.activityTab - }; - - const info = { - slug: 'info', - title: t('info'), - Component: () => , - testID: HomeSelectors.aboutTab - }; - - if (isTezAsset(assetSlug)) { - return [ - activity, - { - slug: 'delegation', - title: t('delegate'), - Component: Delegation, - testID: HomeSelectors.delegationTab - } - ]; - } - - return [activity, info]; - }, [assetSlug]); - - const { slug, Component } = useMemo(() => { - const tab = tabSlug ? tabs.find(currentTab => currentTab.slug === tabSlug) : null; - return tab ?? tabs[0]; - }, [tabSlug, tabs]); - - return ( -
-
- {tabs.map(currentTab => { - const active = slug === currentTab.slug; - - return ( - ({ ...lctn, search: `?tab=${currentTab.slug}` })} - replace - className={classNames( - 'flex1 w-full', - 'text-center cursor-pointer py-2', - 'text-gray-500 text-xs font-medium', - 'border-t-3', - active ? 'border-primary-orange' : 'border-transparent', - active ? 'text-primary-orange' : 'hover:text-primary-orange', - 'transition ease-in-out duration-300', - 'truncate' - )} - testID={currentTab.testID} - testIDProperties={{ - ...(currentTab.slug === 'delegation' && { abTestingCategory: testGroupName }) - }} - > - {currentTab.title} - - ); - })} -
- {Component && } -
- ); -}; - -interface SuspenseContainerProps extends PropsWithChildren { - whileMessage: string; - fallback?: ReactNode; -} - -const SuspenseContainer: FC = ({ whileMessage, fallback = , children }) => ( - - {children} - -); - -const SpinnerSection: FC = () => ( -
- -
-); diff --git a/src/app/pages/Home/OtherComponents/Tokens/Tokens.tsx b/src/app/pages/Home/OtherComponents/Tokens/Tokens.tsx index cd37eb2c6..74e93a0fb 100644 --- a/src/app/pages/Home/OtherComponents/Tokens/Tokens.tsx +++ b/src/app/pages/Home/OtherComponents/Tokens/Tokens.tsx @@ -22,7 +22,7 @@ import { AssetsSelectors } from '../Assets.selectors'; import { ListItem } from './components/ListItem'; import { toExploreAssetLink } from './utils'; -export const Tokens: FC = () => { +export const TokensTab: FC = () => { const dispatch = useDispatch(); const chainId = useChainId(true)!; const balances = useBalancesWithDecimals(); diff --git a/src/app/templates/AssetIcon.tsx b/src/app/templates/AssetIcon.tsx index f2f3cd466..ccb573401 100644 --- a/src/app/templates/AssetIcon.tsx +++ b/src/app/templates/AssetIcon.tsx @@ -1,6 +1,6 @@ import React, { FC, memo, useMemo } from 'react'; -import classNames from 'clsx'; +import clsx from 'clsx'; import Identicon from 'app/atoms/Identicon'; import { ReactComponent as CollectiblePlaceholder } from 'app/icons/collectible-placeholder.svg'; @@ -25,9 +25,10 @@ interface Props { assetSlug: string; className?: string; size?: number; + style?: React.CSSProperties; } -export const AssetIcon: FC = memo(({ assetSlug, className, size }) => { +export const AssetIcon: FC = memo(({ assetSlug, className, size, style }) => { const metadata = useAssetMetadata(assetSlug); const src = useMemo(() => { @@ -36,7 +37,7 @@ export const AssetIcon: FC = memo(({ assetSlug, className, size }) }, [metadata, assetSlug]); return ( -
+
} diff --git a/src/lib/ui/Image.tsx b/src/lib/ui/Image.tsx index 28171b408..06af2350c 100644 --- a/src/lib/ui/Image.tsx +++ b/src/lib/ui/Image.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from 'react'; import ReactImageFallback from 'react-image-fallback'; -interface Props { +interface ImageProps { src?: string | (string | undefined)[]; alt?: string; className?: string; @@ -21,7 +21,7 @@ interface Props { ReactImageFallback.prototype.componentDidUpdate = ReactImageFallback.prototype.componentWillReceiveProps; delete ReactImageFallback.prototype.componentWillReceiveProps; -export const Image: React.FC = ({ src: sources, alt, loader, fallback, ...rest }) => { +export const Image: React.FC = ({ src: sources, alt, loader, fallback, ...rest }) => { const localFallback = useMemo(() => fallback || {alt}, [alt, rest]); const { src, fallbackImage } = useMemo(() => { From 3430d151c9f3641511663c43da129f9bf00c732c Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 26 Jun 2023 03:10:11 +0300 Subject: [PATCH 03/21] TW-828: Collectibles grid layout. + Scroll tabsbar into view --- src/app/atoms/useTabSlug.tsx | 2 + src/app/layouts/PageLayout.tsx | 23 ++++++- src/app/pages/Home/ContentSection.tsx | 90 ++++++++++++++++++--------- src/lib/ui/hooks/useDidUpdate.ts | 5 ++ 4 files changed, 86 insertions(+), 34 deletions(-) diff --git a/src/app/atoms/useTabSlug.tsx b/src/app/atoms/useTabSlug.tsx index aa5a59864..bf042e590 100644 --- a/src/app/atoms/useTabSlug.tsx +++ b/src/app/atoms/useTabSlug.tsx @@ -4,9 +4,11 @@ import { useLocation } from 'lib/woozie'; export const useTabSlug = () => { const { search } = useLocation(); + const tabSlug = useMemo(() => { const usp = new URLSearchParams(search); return usp.get('tab'); }, [search]); + return useMemo(() => tabSlug, [tabSlug]); }; diff --git a/src/app/layouts/PageLayout.tsx b/src/app/layouts/PageLayout.tsx index df02dfd61..a922f31f9 100644 --- a/src/app/layouts/PageLayout.tsx +++ b/src/app/layouts/PageLayout.tsx @@ -1,4 +1,14 @@ -import React, { ComponentProps, FC, ReactNode, Suspense, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import React, { + ComponentProps, + FC, + ReactNode, + Suspense, + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState +} from 'react'; import classNames from 'clsx'; @@ -96,6 +106,8 @@ type ToolbarProps = { attention?: boolean; }; +export let ToolbarElement: HTMLDivElement | null = null; + const Toolbar: FC = ({ pageTitle, hasBackAction = true, @@ -137,7 +149,7 @@ const Toolbar: FC = ({ const [sticked, setSticked] = useState(false); - const rootRef = useRef(null); + const rootRef = useRef(); useEffect(() => { const toolbarEl = rootRef.current; @@ -157,9 +169,14 @@ const Toolbar: FC = ({ return undefined; }, [setSticked]); + const updateRootRef = useCallback((elem: HTMLDivElement | null) => { + rootRef.current = elem; + ToolbarElement = elem; + }, []); + return (
= ({ assetSlug, className }) => { return tab ?? tabs[0]; }, [tabSlug, tabs]); + const tabBarElemRef = useRef(null); + + useDidUpdate(() => { + if (!tabBarElemRef.current) return; + + const stickyBarHeight = ToolbarElement?.scrollHeight ?? 0; + + window.scrollTo({ + top: window.pageYOffset + tabBarElemRef.current.getBoundingClientRect().top - stickyBarHeight, + behavior: 'smooth' + }); + }, [tabSlug]); + return ( -
-
- {tabs.map(currentTab => { - const active = slug === currentTab.slug; - - return ( - ({ ...lctn, search: `?tab=${currentTab.slug}` })} - replace - className={classNames( - 'flex1 w-full', - 'text-center cursor-pointer py-2', - 'text-gray-500 text-xs font-medium', - 'border-t-3', - active ? 'border-primary-orange' : 'border-transparent', - active ? 'text-primary-orange' : 'hover:text-primary-orange', - 'transition ease-in-out duration-300', - 'truncate' - )} - testID={currentTab.testID} - testIDProperties={{ - ...(currentTab.slug === 'delegation' && { abTestingCategory: testGroupName }) - }} - > - - - ); - })} +
+
+ {tabs.map(tab => ( + + ))}
{Component && } @@ -163,3 +160,34 @@ const SpinnerSection: FC = () => (
); + +interface TabButtonProps { + tab: TabData; + active: boolean; + testGroupName: ABTestGroup; +} + +const TabButton: FC = ({ tab, active, testGroupName }) => { + return ( + ({ ...lctn, search: `?tab=${tab.slug}` })} + replace + className={clsx( + 'flex1 w-full', + 'text-center cursor-pointer py-2', + 'text-gray-500 text-xs font-medium', + 'border-t-3', + active ? 'border-primary-orange' : 'border-transparent', + active ? 'text-primary-orange' : 'hover:text-primary-orange', + 'transition ease-in-out duration-300', + 'truncate' + )} + testID={tab.testID} + testIDProperties={{ + ...(tab.slug === 'delegation' && { abTestingCategory: testGroupName }) + }} + > + + + ); +}; diff --git a/src/lib/ui/hooks/useDidUpdate.ts b/src/lib/ui/hooks/useDidUpdate.ts index 554111cdf..401b5c99d 100644 --- a/src/lib/ui/hooks/useDidUpdate.ts +++ b/src/lib/ui/hooks/useDidUpdate.ts @@ -5,6 +5,7 @@ import { useWillUnmount } from './useWillUnmount'; export function useDidUpdate(callback: EmptyFn, conditions?: unknown[]) { const hasMountedRef = useRef(false); + const internalConditions = useMemo(() => { if (typeof conditions !== 'undefined' && !Array.isArray(conditions)) { return [conditions]; @@ -13,16 +14,20 @@ export function useDidUpdate(callback: EmptyFn, conditions?: unknown[]) { 'Using [] as the second argument makes useDidUpdate a noop. The second argument should either be `undefined` or an array of length greater than 0.' ); } + return conditions; }, [conditions]); + useEffect(() => { if (hasMountedRef.current) { callback(); } }, internalConditions); + useDidMount(() => { hasMountedRef.current = true; }); + useWillUnmount(() => { hasMountedRef.current = false; }); From 65183c2043b8c559674bab36c0860a17271b3a0e Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 26 Jun 2023 05:00:45 +0300 Subject: [PATCH 04/21] TW-828: Collectibles grid layout. ++ Images --- src/app/atoms/Spinner/Spinner.tsx | 5 +- src/app/icons/broken-image.svg | 13 +++++ .../pages/Collectibles/CollectibleItem.tsx | 28 ++++++++- src/app/templates/AssetIcon.tsx | 57 +++++++------------ .../AssetImage.tsx} | 26 +++------ src/lib/ui/Image.tsx | 2 +- tailwind.config.js | 1 + 7 files changed, 71 insertions(+), 61 deletions(-) create mode 100644 src/app/icons/broken-image.svg rename src/app/{pages/Collectibles/CollectibleItemImage.tsx => templates/AssetImage.tsx} (51%) diff --git a/src/app/atoms/Spinner/Spinner.tsx b/src/app/atoms/Spinner/Spinner.tsx index 408323528..08e61e698 100644 --- a/src/app/atoms/Spinner/Spinner.tsx +++ b/src/app/atoms/Spinner/Spinner.tsx @@ -5,7 +5,7 @@ import classNames from 'clsx'; import styles from './Spinner.module.css'; type SpinnerProps = HTMLAttributes & { - theme?: 'primary' | 'white' | 'gray'; + theme?: 'primary' | 'white' | 'gray' | 'dark-gray'; }; const Spinner = memo(({ theme = 'primary', className, ...rest }) => ( @@ -24,6 +24,9 @@ const Spinner = memo(({ theme = 'primary', className, ...rest }) = case 'white': return 'bg-white shadow-sm'; + case 'dark-gray': + return 'bg-gray-600'; + case 'gray': default: return 'bg-gray-400'; diff --git a/src/app/icons/broken-image.svg b/src/app/icons/broken-image.svg new file mode 100644 index 000000000..5afdcbf53 --- /dev/null +++ b/src/app/icons/broken-image.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/app/pages/Collectibles/CollectibleItem.tsx b/src/app/pages/Collectibles/CollectibleItem.tsx index 984580a7d..5d7791d3c 100644 --- a/src/app/pages/Collectibles/CollectibleItem.tsx +++ b/src/app/pages/Collectibles/CollectibleItem.tsx @@ -1,7 +1,9 @@ import React, { FC, useCallback, useRef, useState } from 'react'; +import Spinner from 'app/atoms/Spinner/Spinner'; import { useAppEnv } from 'app/env'; -import { CollectibleItemImage } from 'app/pages/Collectibles/CollectibleItemImage'; +import { ReactComponent as BrokenImageSvg } from 'app/icons/broken-image.svg'; +import { AssetImage } from 'app/templates/AssetImage'; import { useAssetMetadata, getAssetName } from 'lib/metadata'; import { useIntersectionDetection } from 'lib/ui/use-intersection-detection'; import { Link } from 'lib/woozie'; @@ -29,8 +31,16 @@ export const CollectibleItem: FC = ({ assetSlug, detailsShown }) => { return ( -
- {displayed && } +
+ {displayed && ( + } + fallback={} + /> + )}
{detailsShown && ( @@ -41,3 +51,15 @@ export const CollectibleItem: FC = ({ assetSlug, detailsShown }) => { ); }; + +const ImageLoader: FC = () => ( +
+ +
+); + +const ImageFallback: FC = () => ( +
+ +
+); diff --git a/src/app/templates/AssetIcon.tsx b/src/app/templates/AssetIcon.tsx index ccb573401..1c6a2026d 100644 --- a/src/app/templates/AssetIcon.tsx +++ b/src/app/templates/AssetIcon.tsx @@ -1,12 +1,29 @@ -import React, { FC, memo, useMemo } from 'react'; +import React, { FC, memo } from 'react'; import clsx from 'clsx'; import Identicon from 'app/atoms/Identicon'; import { ReactComponent as CollectiblePlaceholder } from 'app/icons/collectible-placeholder.svg'; import { AssetMetadataBase, getAssetSymbol, isCollectible, useAssetMetadata } from 'lib/metadata'; -import { buildTokenIconURLs, buildCollectibleImageURLs } from 'lib/temple/front'; -import { Image } from 'lib/ui/Image'; + +import { AssetImage, AssetImageProps } from './AssetImage'; + +type Props = Omit; + +export const AssetIcon: FC = memo(({ className, style, ...props }) => { + const metadata = useAssetMetadata(props.assetSlug); + + return ( +
+ } + fallback={} + /> +
+ ); +}); interface PlaceholderProps { metadata: AssetMetadataBase | nullish; @@ -20,37 +37,3 @@ const AssetIconPlaceholder: FC = ({ metadata, size }) => { ); }; - -interface Props { - assetSlug: string; - className?: string; - size?: number; - style?: React.CSSProperties; -} - -export const AssetIcon: FC = memo(({ assetSlug, className, size, style }) => { - const metadata = useAssetMetadata(assetSlug); - - const src = useMemo(() => { - if (metadata && isCollectible(metadata)) return buildCollectibleImageURLs(assetSlug, metadata, size == null); - else return buildTokenIconURLs(metadata?.thumbnailUri, size == null); - }, [metadata, assetSlug]); - - return ( -
- } - fallback={} - alt={metadata?.name} - style={{ - objectFit: 'contain', - maxWidth: '100%', - maxHeight: '100%' - }} - height={size} - width={size} - /> -
- ); -}); diff --git a/src/app/pages/Collectibles/CollectibleItemImage.tsx b/src/app/templates/AssetImage.tsx similarity index 51% rename from src/app/pages/Collectibles/CollectibleItemImage.tsx rename to src/app/templates/AssetImage.tsx index 4c35a22c3..7c78371a8 100644 --- a/src/app/pages/Collectibles/CollectibleItemImage.tsx +++ b/src/app/templates/AssetImage.tsx @@ -1,19 +1,18 @@ -import React, { FC, memo, useMemo } from 'react'; +import React, { memo, useMemo } from 'react'; -import { ReactComponent as CollectiblePlaceholderSvg } from 'app/icons/collectible-placeholder.svg'; import { AssetMetadataBase, isCollectible } from 'lib/metadata'; import { buildTokenIconURLs, buildCollectibleImageURLs } from 'lib/temple/front'; -import { Image } from 'lib/ui/Image'; +import { Image, ImageProps } from 'lib/ui/Image'; -interface Props { +export interface AssetImageProps extends Pick { assetSlug: string; - metadata: AssetMetadataBase; + metadata?: AssetMetadataBase; className?: string; size?: number; style?: React.CSSProperties; } -export const CollectibleItemImage: FC = memo(({ metadata, assetSlug, className, size, style }) => { +export const AssetImage = memo(({ metadata, assetSlug, className, size, style, loader, fallback }) => { const src = useMemo(() => { if (metadata && isCollectible(metadata)) return buildCollectibleImageURLs(assetSlug, metadata, size == null); else return buildTokenIconURLs(metadata?.thumbnailUri, size == null); @@ -32,8 +31,8 @@ export const CollectibleItemImage: FC = memo(({ metadata, assetSlu return ( } - fallback={} + loader={loader} + fallback={fallback} alt={metadata?.name} className={className} style={styleMemo} @@ -42,14 +41,3 @@ export const CollectibleItemImage: FC = memo(({ metadata, assetSlu /> ); }); - -interface PlaceholderProps { - metadata: AssetMetadataBase | nullish; - size?: number; -} - -const ImagePlaceholder: FC = ({ size }) => { - const styleMemo = useMemo(() => ({ maxWidth: `${size}px`, width: '100%', height: '100%' }), [size]); - - return ; -}; diff --git a/src/lib/ui/Image.tsx b/src/lib/ui/Image.tsx index 06af2350c..d8cc8c318 100644 --- a/src/lib/ui/Image.tsx +++ b/src/lib/ui/Image.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from 'react'; import ReactImageFallback from 'react-image-fallback'; -interface ImageProps { +export interface ImageProps { src?: string | (string | undefined)[]; alt?: string; className?: string; diff --git a/tailwind.config.js b/tailwind.config.js index 97c9fe6ff..f627c0308 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -100,6 +100,7 @@ module.exports = { 900: '#234e52' }, blue: { + 50: '#e8f1fd', 100: '#ebf8ff', 150: '#E5F2FF', 200: '#bee3f8', From 079102d34c2567ce1d3d08f9ae20be161c89c040 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 26 Jun 2023 05:12:36 +0300 Subject: [PATCH 05/21] TW-864: Collectibles hover effect --- src/app/pages/Collectibles/CollectibleItem.tsx | 6 +++++- tailwind.config.js | 13 ------------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/app/pages/Collectibles/CollectibleItem.tsx b/src/app/pages/Collectibles/CollectibleItem.tsx index 5d7791d3c..4076bc64c 100644 --- a/src/app/pages/Collectibles/CollectibleItem.tsx +++ b/src/app/pages/Collectibles/CollectibleItem.tsx @@ -31,7 +31,11 @@ export const CollectibleItem: FC = ({ assetSlug, detailsShown }) => { return ( -
+
{displayed && ( Date: Mon, 26 Jun 2023 15:04:21 +0300 Subject: [PATCH 06/21] TW-828: Collectibles grid layout. Refactor --- .../pages/Collectibles/CollectibleItem.tsx | 33 +++---------------- .../Collectibles/CollectibleItemImage.tsx | 27 +++++++++++++++ 2 files changed, 32 insertions(+), 28 deletions(-) create mode 100644 src/app/pages/Collectibles/CollectibleItemImage.tsx diff --git a/src/app/pages/Collectibles/CollectibleItem.tsx b/src/app/pages/Collectibles/CollectibleItem.tsx index 4076bc64c..9d908b013 100644 --- a/src/app/pages/Collectibles/CollectibleItem.tsx +++ b/src/app/pages/Collectibles/CollectibleItem.tsx @@ -1,13 +1,12 @@ import React, { FC, useCallback, useRef, useState } from 'react'; -import Spinner from 'app/atoms/Spinner/Spinner'; import { useAppEnv } from 'app/env'; -import { ReactComponent as BrokenImageSvg } from 'app/icons/broken-image.svg'; -import { AssetImage } from 'app/templates/AssetImage'; import { useAssetMetadata, getAssetName } from 'lib/metadata'; import { useIntersectionDetection } from 'lib/ui/use-intersection-detection'; import { Link } from 'lib/woozie'; +import { CollectibleItemImage } from './CollectibleItemImage'; + interface Props { assetSlug: string; index: number; @@ -27,24 +26,14 @@ export const CollectibleItem: FC = ({ assetSlug, detailsShown }) => { useIntersectionDetection(toDisplayRef, handleIntersection, !displayed); - if (metadata == null) return null; - return ( - +
- {displayed && ( - } - fallback={} - /> - )} + {displayed && }
{detailsShown && ( @@ -55,15 +44,3 @@ export const CollectibleItem: FC = ({ assetSlug, detailsShown }) => { ); }; - -const ImageLoader: FC = () => ( -
- -
-); - -const ImageFallback: FC = () => ( -
- -
-); diff --git a/src/app/pages/Collectibles/CollectibleItemImage.tsx b/src/app/pages/Collectibles/CollectibleItemImage.tsx new file mode 100644 index 000000000..062efcfd5 --- /dev/null +++ b/src/app/pages/Collectibles/CollectibleItemImage.tsx @@ -0,0 +1,27 @@ +import React, { FC } from 'react'; + +import Spinner from 'app/atoms/Spinner/Spinner'; +import { ReactComponent as BrokenImageSvg } from 'app/icons/broken-image.svg'; +import { AssetImage } from 'app/templates/AssetImage'; +import { AssetMetadataBase } from 'lib/metadata'; + +interface Props { + assetSlug: string; + metadata?: AssetMetadataBase; +} + +export const CollectibleItemImage: FC = ({ metadata, assetSlug }) => ( + } fallback={} /> +); + +const ImageLoader: FC = () => ( +
+ +
+); + +const ImageFallback: FC = () => ( +
+ +
+); From 91b219a6966315964a5405c006a7f092baddebdc Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Jun 2023 13:59:26 +0300 Subject: [PATCH 07/21] TW-828: Collectibles grid layout. Refactor --- src/app/atoms/ActivitySpinner.tsx | 16 ---------------- src/app/atoms/ImportTabSwitcher.tsx | 9 +++------ src/app/atoms/SyncSpinner.tsx | 15 +++++++++++++++ src/app/atoms/index.ts | 2 +- src/app/layouts/TabsPageLayout.tsx | 5 +++-- src/app/pages/Collectibles/CollectiblesTab.tsx | 7 ++++--- src/app/pages/Home/ContentSection.tsx | 10 +++------- .../pages/Home/OtherComponents/Tokens/Tokens.tsx | 9 +++------ src/app/templates/activity/Activity.tsx | 6 +++--- 9 files changed, 35 insertions(+), 44 deletions(-) delete mode 100644 src/app/atoms/ActivitySpinner.tsx create mode 100644 src/app/atoms/SyncSpinner.tsx diff --git a/src/app/atoms/ActivitySpinner.tsx b/src/app/atoms/ActivitySpinner.tsx deleted file mode 100644 index 5979b6f8a..000000000 --- a/src/app/atoms/ActivitySpinner.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import * as React from 'react'; - -import clsx from 'clsx'; - -import Spinner from './Spinner/Spinner'; - -interface ActivitySpinnerProps { - height?: string; - className?: string; -} - -export const ActivitySpinner: React.FC = ({ height = '21px', className }) => ( -
- -
-); diff --git a/src/app/atoms/ImportTabSwitcher.tsx b/src/app/atoms/ImportTabSwitcher.tsx index b9bad5a98..c8dae2fb7 100644 --- a/src/app/atoms/ImportTabSwitcher.tsx +++ b/src/app/atoms/ImportTabSwitcher.tsx @@ -27,13 +27,10 @@ const ImportTabSwitcher: React.FC = ({ className, tabs,
diff --git a/src/app/atoms/SyncSpinner.tsx b/src/app/atoms/SyncSpinner.tsx new file mode 100644 index 000000000..6a384cd64 --- /dev/null +++ b/src/app/atoms/SyncSpinner.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; + +import clsx from 'clsx'; + +import Spinner from './Spinner/Spinner'; + +interface Props { + className?: string; +} + +export const SyncSpinner: React.FC = ({ className }) => ( +
+ +
+); diff --git a/src/app/atoms/index.ts b/src/app/atoms/index.ts index e06d275ea..ec97f9524 100644 --- a/src/app/atoms/index.ts +++ b/src/app/atoms/index.ts @@ -16,7 +16,7 @@ export { FormSecondaryButton } from './FormSecondaryButton'; export type { FormCheckboxProps } from './FormCheckbox'; export { FormCheckbox } from './FormCheckbox'; -export { ActivitySpinner } from './ActivitySpinner'; +export { SyncSpinner } from './SyncSpinner'; export { NoSpaceField } from './NoSpaceField'; diff --git a/src/app/layouts/TabsPageLayout.tsx b/src/app/layouts/TabsPageLayout.tsx index f7084becb..160f0e227 100644 --- a/src/app/layouts/TabsPageLayout.tsx +++ b/src/app/layouts/TabsPageLayout.tsx @@ -57,8 +57,9 @@ export const TabsPageLayout: FC = ({ tabs, icon, title, description }) => 'flex1 w-full text-center cursor-pointer pb-2', 'border-b-2 text-gray-700 text-lg truncate', tabs.length === 1 && 'mx-20', - active ? 'border-primary-orange' : 'border-transparent', - active ? 'text-primary-orange' : 'hover:text-primary-orange', + active + ? 'border-primary-orange text-primary-orange' + : 'border-transparent hover:text-primary-orange', 'transition ease-in-out duration-300' )} testID={tab.testID} diff --git a/src/app/pages/Collectibles/CollectiblesTab.tsx b/src/app/pages/Collectibles/CollectiblesTab.tsx index 560f106ed..c53cacb26 100644 --- a/src/app/pages/Collectibles/CollectiblesTab.tsx +++ b/src/app/pages/Collectibles/CollectiblesTab.tsx @@ -2,7 +2,7 @@ import React from 'react'; import clsx from 'clsx'; -import { ActivitySpinner } from 'app/atoms'; +import { SyncSpinner } from 'app/atoms'; import { useAppEnv } from 'app/env'; import { ReactComponent as ManageIcon } from 'app/icons/manage.svg'; import { CollectibleItem } from 'app/pages/Collectibles/CollectibleItem'; @@ -53,7 +53,7 @@ export const CollectiblesTab = () => {
{isSyncing && filteredAssets.length === 0 ? ( - + ) : filteredAssets.length === 0 ? (

@@ -67,7 +67,8 @@ export const CollectiblesTab = () => { ))}

- {isSyncing && } + + {isSyncing && } )}
diff --git a/src/app/pages/Home/ContentSection.tsx b/src/app/pages/Home/ContentSection.tsx index efed1304e..9ccfb66ea 100644 --- a/src/app/pages/Home/ContentSection.tsx +++ b/src/app/pages/Home/ContentSection.tsx @@ -173,14 +173,10 @@ const TabButton: FC = ({ tab, active, testGroupName }) => { to={lctn => ({ ...lctn, search: `?tab=${tab.slug}` })} replace className={clsx( - 'flex1 w-full', - 'text-center cursor-pointer py-2', - 'text-gray-500 text-xs font-medium', - 'border-t-3', - active ? 'border-primary-orange' : 'border-transparent', - active ? 'text-primary-orange' : 'hover:text-primary-orange', + 'flex1 w-full text-center cursor-pointer py-2 border-t-3', + 'text-gray-500 text-xs font-medium truncate', 'transition ease-in-out duration-300', - 'truncate' + active ? 'border-primary-orange text-primary-orange' : 'border-transparent hover:text-primary-orange' )} testID={tab.testID} testIDProperties={{ diff --git a/src/app/pages/Home/OtherComponents/Tokens/Tokens.tsx b/src/app/pages/Home/OtherComponents/Tokens/Tokens.tsx index e94a36d9f..cf11b5c05 100644 --- a/src/app/pages/Home/OtherComponents/Tokens/Tokens.tsx +++ b/src/app/pages/Home/OtherComponents/Tokens/Tokens.tsx @@ -4,7 +4,7 @@ import { BigNumber } from 'bignumber.js'; import classNames from 'clsx'; import { useDispatch } from 'react-redux'; -import { ActivitySpinner } from 'app/atoms'; +import { SyncSpinner } from 'app/atoms'; import { PartnersPromotion, PartnersPromotionVariant } from 'app/atoms/partners-promotion'; import { useAppEnv } from 'app/env'; import { useBalancesWithDecimals } from 'app/hooks/use-balances-with-decimals.hook'; @@ -204,11 +204,8 @@ export const TokensTab: FC = () => { {tokensView}
)} - {isSyncing && ( -
- -
- )} + + {isSyncing && }
); }; diff --git a/src/app/templates/activity/Activity.tsx b/src/app/templates/activity/Activity.tsx index 81e1f0e49..74d61aab7 100644 --- a/src/app/templates/activity/Activity.tsx +++ b/src/app/templates/activity/Activity.tsx @@ -4,7 +4,7 @@ import classNames from 'clsx'; import InfiniteScroll from 'react-infinite-scroll-component'; import { useDispatch } from 'react-redux'; -import { ActivitySpinner } from 'app/atoms'; +import { SyncSpinner } from 'app/atoms'; import { PartnersPromotion, PartnersPromotionVariant } from 'app/atoms/partners-promotion'; import { useAppEnv } from 'app/env'; import { ReactComponent as LayersIcon } from 'app/icons/layers.svg'; @@ -68,12 +68,12 @@ export const ActivityComponent: React.FC = ({ assetSlug }) => { return (
-
+
} + loader={loading && } onScroll={onScroll} > {activities.map((activity, index) => ( From fd976af83a5c70ba222a671bb888838476d8716a Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Jun 2023 19:14:50 +0300 Subject: [PATCH 08/21] TW-830: Collectibles Manage dropdown. + Detailed grid --- .../pages/Collectibles/CollectibleItem.tsx | 37 +++++++++++++++---- .../pages/Collectibles/CollectiblePage.tsx | 10 +++++ .../pages/Collectibles/CollectiblesTab.tsx | 10 ++++- tailwind.config.js | 12 +----- 4 files changed, 48 insertions(+), 21 deletions(-) diff --git a/src/app/pages/Collectibles/CollectibleItem.tsx b/src/app/pages/Collectibles/CollectibleItem.tsx index 9d908b013..3da144163 100644 --- a/src/app/pages/Collectibles/CollectibleItem.tsx +++ b/src/app/pages/Collectibles/CollectibleItem.tsx @@ -1,7 +1,11 @@ import React, { FC, useCallback, useRef, useState } from 'react'; +import clsx from 'clsx'; + +import Money from 'app/atoms/Money'; import { useAppEnv } from 'app/env'; -import { useAssetMetadata, getAssetName } from 'lib/metadata'; +import { useAssetMetadata, getAssetName, TEZOS_METADATA } from 'lib/metadata'; +import { useBalance } from 'lib/temple/front'; import { useIntersectionDetection } from 'lib/ui/use-intersection-detection'; import { Link } from 'lib/woozie'; @@ -9,16 +13,17 @@ import { CollectibleItemImage } from './CollectibleItemImage'; interface Props { assetSlug: string; - index: number; - itemsLength: number; - detailsShown?: boolean; + accountPkh: string; + detailsShown: boolean; + floorPrice?: string; } -export const CollectibleItem: FC = ({ assetSlug, detailsShown }) => { +export const CollectibleItem: FC = ({ assetSlug, accountPkh, detailsShown, floorPrice }) => { const { popup } = useAppEnv(); const metadata = useAssetMetadata(assetSlug); const toDisplayRef = useRef(null); const [displayed, setDisplayed] = useState(true); + const { data: balance } = useBalance(assetSlug, accountPkh, { displayed }); const handleIntersection = useCallback(() => { setDisplayed(true); @@ -30,15 +35,31 @@ export const CollectibleItem: FC = ({ assetSlug, detailsShown }) => {
{displayed && } + + {balance ? ( +
+ {balance.toFixed()}× +
+ ) : null}
{detailsShown && ( -
-

{getAssetName(metadata)}

+
+
{getAssetName(metadata)}
+
+ Floor: + + {floorPrice ?? 0} + + TEZ +
)} diff --git a/src/app/pages/Collectibles/CollectiblePage.tsx b/src/app/pages/Collectibles/CollectiblePage.tsx index 291451393..aafcf50af 100644 --- a/src/app/pages/Collectibles/CollectiblePage.tsx +++ b/src/app/pages/Collectibles/CollectiblePage.tsx @@ -46,19 +46,23 @@ const CollectiblePage: FC = ({ assetSlug }) => {
+ +

{collectibleName}

+

{collectibleBalance ? collectibleBalance.toFixed() : ''}

+

@@ -67,6 +71,7 @@ const CollectiblePage: FC = ({ assetSlug }) => {

+ = ({ assetSlug }) => {
+

+

{assetId.toFixed()}

+ = ({ assetSlug }) => {
+ + { ) : ( <>
- {filteredAssets.map((slug, index) => ( - + {filteredAssets.map(slug => ( + ))}
diff --git a/tailwind.config.js b/tailwind.config.js index b2c9c67f7..b44944e71 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -311,6 +311,7 @@ module.exports = { }; })(), fontSize: { + '2xs': '0.625rem', xs: '0.75rem', sm: '0.875rem', base: '1rem', @@ -343,17 +344,6 @@ module.exports = { full: '100%', screen: '100vh' }), - inset: { - 0: '0', - 1: '0.25rem', - 2: '0.5rem', - 3: '0.75rem', - 4: '1rem', - 8: '2rem', - 12: '3rem', - '1/2': '50%', - auto: 'auto' - }, letterSpacing: { tighter: '-0.05em', tight: '-0.025em', From b47a12732451c82633aa85117587ecdbf2f6788e Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 3 Jul 2023 15:10:55 +0300 Subject: [PATCH 09/21] TW-830: Collectibles Manage dropdown. + Detailes toggle --- public/_locales/en/messages.json | 3 + public/_locales/uk/messages.json | 3 + src/app/atoms/Checkbox.tsx | 27 ++--- src/app/atoms/Divider.tsx | 6 +- src/app/atoms/FormCheckbox.tsx | 4 +- src/app/icons/editing.svg | 6 ++ src/app/icons/manage.svg | 7 +- .../pages/Collectibles/CollectibleItem.tsx | 5 +- .../pages/Collectibles/CollectiblesTab.tsx | 98 ++++++++++++++++--- .../Home/OtherComponents/Assets.selectors.ts | 4 +- tailwind.config.js | 27 +---- 11 files changed, 127 insertions(+), 63 deletions(-) create mode 100644 src/app/icons/editing.svg diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index 974ff8109..c3d93eef1 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -2358,6 +2358,9 @@ "message": "less", "description": "Show less" }, + "showInfo": { + "message": "Show info" + }, "recentDestinations": { "message": "Recent destinations" }, diff --git a/public/_locales/uk/messages.json b/public/_locales/uk/messages.json index a385532ff..e0cc71651 100644 --- a/public/_locales/uk/messages.json +++ b/public/_locales/uk/messages.json @@ -1855,5 +1855,8 @@ "content": "$1" } } + }, + "showInfo": { + "message": "Деталі" } } diff --git a/src/app/atoms/Checkbox.tsx b/src/app/atoms/Checkbox.tsx index 1c7bbc958..5ff13cb49 100644 --- a/src/app/atoms/Checkbox.tsx +++ b/src/app/atoms/Checkbox.tsx @@ -6,17 +6,18 @@ import { ReactComponent as OkIcon } from 'app/icons/ok.svg'; import { TestIDProps, setTestID, useAnalytics, AnalyticsEventCategory } from 'lib/analytics'; import { blurHandler, checkedHandler, focusHandler } from 'lib/ui/inputHandlers'; -export type CheckboxProps = TestIDProps & - Pick, 'name' | 'checked' | 'className' | 'onFocus' | 'onBlur' | 'onClick'> & { - containerClassName?: string; - errored?: boolean; - onChange?: (checked: boolean, event: React.ChangeEvent) => void; - }; +export interface CheckboxProps + extends TestIDProps, + Pick, 'name' | 'checked' | 'className' | 'onFocus' | 'onBlur' | 'onClick'> { + overrideClassNames?: string; + errored?: boolean; + onChange?: (checked: boolean, event: React.ChangeEvent) => void; +} const Checkbox = forwardRef( ( { - containerClassName, + overrideClassNames, errored = false, className, checked, @@ -64,8 +65,8 @@ const Checkbox = forwardRef( const classNameMemo = useMemo( () => classNames( - 'flex justify-center items-center h-6 w-6 flex-shrink-0', - 'text-white border rounded-md overflow-hidden', + 'flex justify-center items-center flex-shrink-0', + 'text-white border overflow-hidden', 'transition ease-in-out duration-200 disable-outline-for-click', localChecked ? 'bg-primary-orange' : 'bg-black-40', localFocused && 'shadow-outline', @@ -81,9 +82,9 @@ const Checkbox = forwardRef( return 'border-gray-400'; } })(), - containerClassName + overrideClassNames || 'h-6 w-6 rounded-md' ), - [localChecked, localFocused, errored, containerClassName] + [localChecked, localFocused, errored] ); return ( @@ -101,8 +102,8 @@ const Checkbox = forwardRef( />
); diff --git a/src/app/atoms/Divider.tsx b/src/app/atoms/Divider.tsx index c257d37fb..9cea9e37f 100644 --- a/src/app/atoms/Divider.tsx +++ b/src/app/atoms/Divider.tsx @@ -1,5 +1,7 @@ import React, { FC } from 'react'; +import clsx from 'clsx'; + interface DividerProps { style?: React.CSSProperties; className?: string; @@ -8,12 +10,10 @@ interface DividerProps { const Divider: FC = ({ style, className }) => (
); diff --git a/src/app/atoms/FormCheckbox.tsx b/src/app/atoms/FormCheckbox.tsx index 51bb793dc..6e62b5703 100644 --- a/src/app/atoms/FormCheckbox.tsx +++ b/src/app/atoms/FormCheckbox.tsx @@ -5,13 +5,13 @@ import classNames from 'clsx'; import Checkbox, { CheckboxProps } from 'app/atoms/Checkbox'; import { AnalyticsEventCategory, setTestID, useAnalytics } from 'lib/analytics'; -export type FormCheckboxProps = CheckboxProps & { +export interface FormCheckboxProps extends CheckboxProps { label?: ReactNode; labelDescription?: ReactNode; errorCaption?: ReactNode; containerClassName?: string; labelClassName?: string; -}; +} export const FormCheckbox = forwardRef( ( diff --git a/src/app/icons/editing.svg b/src/app/icons/editing.svg new file mode 100644 index 000000000..867c679f3 --- /dev/null +++ b/src/app/icons/editing.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/app/icons/manage.svg b/src/app/icons/manage.svg index 85bdefb0f..8d228ec42 100644 --- a/src/app/icons/manage.svg +++ b/src/app/icons/manage.svg @@ -1,3 +1,6 @@ - - + + + + diff --git a/src/app/pages/Collectibles/CollectibleItem.tsx b/src/app/pages/Collectibles/CollectibleItem.tsx index 3da144163..6bc927047 100644 --- a/src/app/pages/Collectibles/CollectibleItem.tsx +++ b/src/app/pages/Collectibles/CollectibleItem.tsx @@ -31,6 +31,8 @@ export const CollectibleItem: FC = ({ assetSlug, accountPkh, detailsShown useIntersectionDetection(toDisplayRef, handleIntersection, !displayed); + const assetName = getAssetName(metadata); + return (
= ({ assetSlug, accountPkh, detailsShown 'relative flex items-center justify-center bg-blue-50 rounded-lg overflow-hidden hover:opacity-70', detailsShown ? 'border-b border-gray-300' : undefined )} + title={assetName} style={{ height: popup ? 106 : 125 }} > {displayed && } @@ -52,7 +55,7 @@ export const CollectibleItem: FC = ({ assetSlug, accountPkh, detailsShown {detailsShown && (
-
{getAssetName(metadata)}
+
{assetName}
Floor: diff --git a/src/app/pages/Collectibles/CollectiblesTab.tsx b/src/app/pages/Collectibles/CollectiblesTab.tsx index 24a885ca9..3e3fe50a1 100644 --- a/src/app/pages/Collectibles/CollectiblesTab.tsx +++ b/src/app/pages/Collectibles/CollectiblesTab.tsx @@ -1,9 +1,13 @@ -import React from 'react'; +import React, { FC, useMemo } from 'react'; import clsx from 'clsx'; -import { SyncSpinner } from 'app/atoms'; +import { Button, SyncSpinner } from 'app/atoms'; +import Checkbox from 'app/atoms/Checkbox'; +import Divider from 'app/atoms/Divider'; +import DropdownWrapper from 'app/atoms/DropdownWrapper'; import { useAppEnv } from 'app/env'; +import { ReactComponent as EditingIcon } from 'app/icons/editing.svg'; import { ReactComponent as ManageIcon } from 'app/icons/manage.svg'; import { CollectibleItem } from 'app/pages/Collectibles/CollectibleItem'; import { AssetsSelectors } from 'app/pages/Home/OtherComponents/Assets.selectors'; @@ -12,17 +16,23 @@ import { AssetTypesEnum } from 'lib/assets/types'; import { T, t } from 'lib/i18n'; import { useAccount, useChainId, useCollectibleTokens, useFilteredAssets } from 'lib/temple/front'; import { useSyncTokens } from 'lib/temple/front/sync-tokens'; +import { useLocalStorage } from 'lib/ui/local-storage'; +import Popper, { PopperRenderProps } from 'lib/ui/Popper'; import { Link } from 'lib/woozie'; +const LOCAL_STORAGE_TOGGLE_KEY = 'collectibles-grid:show-items-details'; +const svgIconClassName = 'w-4 h-4 stroke-current fill-current text-gray-600'; + export const CollectiblesTab = () => { const chainId = useChainId(true)!; const { popup } = useAppEnv(); const { publicKeyHash } = useAccount(); const { isSyncing } = useSyncTokens(); + const [detailsShown, setDetailsShown] = useLocalStorage(LOCAL_STORAGE_TOGGLE_KEY, false); const { data: collectibles = [] } = useCollectibleTokens(chainId, publicKeyHash, true); - const collectibleSlugs = collectibles.map(collectible => collectible.tokenSlug); + const collectibleSlugs = useMemo(() => collectibles.map(collectible => collectible.tokenSlug), [collectibles]); const { filteredAssets, searchValue, setSearchValue } = useFilteredAssets(collectibleSlugs); @@ -36,20 +46,34 @@ export const CollectiblesTab = () => { testID={AssetsSelectors.searchAssetsInputCollectibles} /> - ( + void setDetailsShown(!detailsShown)} + /> )} - testID={AssetsSelectors.manageButton} > - - + {({ ref, opened, toggleOpened }) => ( + + )} +
{isSyncing && filteredAssets.length === 0 ? ( @@ -68,7 +92,7 @@ export const CollectiblesTab = () => { key={slug} assetSlug={slug} accountPkh={publicKeyHash} - detailsShown={true} + detailsShown={detailsShown} floorPrice={'1234.0001'} /> ))} @@ -81,3 +105,45 @@ export const CollectiblesTab = () => {
); }; + +interface ManageButtonDropdownProps extends PopperRenderProps { + detailsShown: boolean; + toggleDetailsShown: EmptyFn; +} + +const ManageButtonDropdown: FC = ({ opened, detailsShown, toggleDetailsShown }) => { + const buttonClassName = 'flex items-center px-3 py-2.5 rounded hover:bg-gray-200'; + + return ( + + + + + + + + + + + + + ); +}; diff --git a/src/app/pages/Home/OtherComponents/Assets.selectors.ts b/src/app/pages/Home/OtherComponents/Assets.selectors.ts index f8579550a..4881a0779 100644 --- a/src/app/pages/Home/OtherComponents/Assets.selectors.ts +++ b/src/app/pages/Home/OtherComponents/Assets.selectors.ts @@ -1,5 +1,7 @@ export enum AssetsSelectors { - manageButton = 'Assets/Manage Button', + manageButton = 'Assets/Manage Dropdown Button', + dropdownManageButton = 'Assets/Manage Dropdown/Manage Button', + dropdownShowInfoCheckbox = 'Assets/Manage Dropdown/Show Info Checkbox', assetItemButton = 'Assets/Asset Item Button', assetItemApyButton = 'Assets/Asset Item Apy Button', assetItemCryptoBalanceButton = 'Assets/Asset Item Crypto Balance Button', diff --git a/tailwind.config.js b/tailwind.config.js index b44944e71..d4d4858fd 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -352,22 +352,6 @@ module.exports = { wider: '0.05em', widest: '0.1em' }, - lineHeight: { - none: '1', - tight: '1.25', - snug: '1.375', - normal: '1.5', - relaxed: '1.625', - loose: '2', - 3: '.75rem', - 4: '1rem', - 5: '1.25rem', - 6: '1.5rem', - 7: '1.75rem', - 8: '2rem', - 9: '2.25rem', - 10: '2.5rem' - }, listStyleType: { none: 'none', disc: 'disc', @@ -399,15 +383,8 @@ module.exports = { full: '100%', ...breakpoints(theme('screens')) }), - minHeight: { - 0: '0', - full: '100%', - screen: '100vh' - }, - minWidth: { - 0: '0', - full: '100%' - }, + minHeight: theme => theme('height'), + minWidth: theme => theme('width'), objectPosition: { bottom: 'bottom', center: 'center', From 6672f109326d29affea6c6f548c684ff4cef4c80 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 4 Jul 2023 02:09:56 +0300 Subject: [PATCH 10/21] TW-830: Collectibles Manage dropdown. + Floor prices --- src/app/WithDataLoading.tsx | 2 ++ .../hooks/use-collectibles-details-loading.ts | 17 ++++++++++ .../pages/Collectibles/CollectibleItem.tsx | 23 +++++++++---- .../pages/Collectibles/CollectiblesTab.tsx | 10 ++---- src/app/store/collectibles/actions.ts | 7 ++++ src/app/store/collectibles/epics.ts | 34 +++++++++++++++++++ src/app/store/collectibles/reducer.ts | 20 +++++++++++ src/app/store/collectibles/selectors.ts | 5 +++ src/app/store/collectibles/state.mock.ts | 7 ++++ src/app/store/collectibles/state.ts | 16 +++++++++ src/app/store/index.ts | 11 ++++-- src/app/store/root-state.mock.ts | 4 ++- src/lib/apis/objkt/constants.ts | 5 +++ src/lib/apis/objkt/index.ts | 18 ++++++++++ src/lib/apis/objkt/queries.ts | 13 +++++++ src/lib/fixed-times.ts | 2 ++ 16 files changed, 176 insertions(+), 18 deletions(-) create mode 100644 src/app/hooks/use-collectibles-details-loading.ts create mode 100644 src/app/store/collectibles/actions.ts create mode 100644 src/app/store/collectibles/epics.ts create mode 100644 src/app/store/collectibles/reducer.ts create mode 100644 src/app/store/collectibles/selectors.ts create mode 100644 src/app/store/collectibles/state.mock.ts create mode 100644 src/app/store/collectibles/state.ts create mode 100644 src/lib/apis/objkt/constants.ts create mode 100644 src/lib/apis/objkt/index.ts create mode 100644 src/lib/apis/objkt/queries.ts diff --git a/src/app/WithDataLoading.tsx b/src/app/WithDataLoading.tsx index 233f086ab..4411067e2 100644 --- a/src/app/WithDataLoading.tsx +++ b/src/app/WithDataLoading.tsx @@ -3,6 +3,7 @@ import React, { FC, useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { useAdvertisingLoading } from 'app/hooks/use-advertising.hook'; +import { useCollectiblesDetailsLoading } from 'app/hooks/use-collectibles-details-loading'; import { useLongRefreshLoading } from 'app/hooks/use-long-refresh-loading.hook'; import { useMetadataLoading } from 'app/hooks/use-metadata-loading'; import { useTokensLoading } from 'app/hooks/use-tokens-loading'; @@ -15,6 +16,7 @@ export const WithDataLoading: FC = ({ children }) => { useMetadataLoading(); useTokensLoading(); useBalancesLoading(); + useCollectiblesDetailsLoading(); useLongRefreshLoading(); useAdvertisingLoading(); diff --git a/src/app/hooks/use-collectibles-details-loading.ts b/src/app/hooks/use-collectibles-details-loading.ts new file mode 100644 index 000000000..2dcd4db65 --- /dev/null +++ b/src/app/hooks/use-collectibles-details-loading.ts @@ -0,0 +1,17 @@ +import { useDispatch } from 'react-redux'; + +import { loadCollectiblesDetailsActions } from 'app/store/collectibles/actions'; +import { COLLECTIBLES_DETAILS_SYNC_INTERVAL } from 'lib/fixed-times'; +import { useAccount } from 'lib/temple/front'; +import { useInterval } from 'lib/ui/hooks'; + +export const useCollectiblesDetailsLoading = () => { + const { publicKeyHash } = useAccount(); + const dispatch = useDispatch(); + + useInterval( + () => void dispatch(loadCollectiblesDetailsActions.submit(publicKeyHash)), + COLLECTIBLES_DETAILS_SYNC_INTERVAL, + [publicKeyHash, dispatch] + ); +}; diff --git a/src/app/pages/Collectibles/CollectibleItem.tsx b/src/app/pages/Collectibles/CollectibleItem.tsx index 6bc927047..3ad139243 100644 --- a/src/app/pages/Collectibles/CollectibleItem.tsx +++ b/src/app/pages/Collectibles/CollectibleItem.tsx @@ -1,11 +1,14 @@ import React, { FC, useCallback, useRef, useState } from 'react'; +import { isDefined } from '@rnw-community/shared'; import clsx from 'clsx'; import Money from 'app/atoms/Money'; import { useAppEnv } from 'app/env'; +import { useOneCollectibleDetailsSelector } from 'app/store/collectibles/selectors'; import { useAssetMetadata, getAssetName, TEZOS_METADATA } from 'lib/metadata'; import { useBalance } from 'lib/temple/front'; +import { atomsToTokens } from 'lib/temple/helpers'; import { useIntersectionDetection } from 'lib/ui/use-intersection-detection'; import { Link } from 'lib/woozie'; @@ -15,15 +18,17 @@ interface Props { assetSlug: string; accountPkh: string; detailsShown: boolean; - floorPrice?: string; } -export const CollectibleItem: FC = ({ assetSlug, accountPkh, detailsShown, floorPrice }) => { +export const CollectibleItem: FC = ({ assetSlug, accountPkh, detailsShown }) => { const { popup } = useAppEnv(); const metadata = useAssetMetadata(assetSlug); const toDisplayRef = useRef(null); const [displayed, setDisplayed] = useState(true); const { data: balance } = useBalance(assetSlug, accountPkh, { displayed }); + const details = useOneCollectibleDetailsSelector(assetSlug); + + const floorPrice = details?.floorPrice; const handleIntersection = useCallback(() => { setDisplayed(true); @@ -58,10 +63,16 @@ export const CollectibleItem: FC = ({ assetSlug, accountPkh, detailsShown
{assetName}
Floor: - - {floorPrice ?? 0} - - TEZ + {isDefined(floorPrice) ? ( + <> + + {atomsToTokens(floorPrice, TEZOS_METADATA.decimals)} + + TEZ + + ) : ( + '---' + )}
)} diff --git a/src/app/pages/Collectibles/CollectiblesTab.tsx b/src/app/pages/Collectibles/CollectiblesTab.tsx index 3e3fe50a1..e064f210c 100644 --- a/src/app/pages/Collectibles/CollectiblesTab.tsx +++ b/src/app/pages/Collectibles/CollectiblesTab.tsx @@ -88,13 +88,7 @@ export const CollectiblesTab = () => { <>
{filteredAssets.map(slug => ( - + ))}
@@ -112,7 +106,7 @@ interface ManageButtonDropdownProps extends PopperRenderProps { } const ManageButtonDropdown: FC = ({ opened, detailsShown, toggleDetailsShown }) => { - const buttonClassName = 'flex items-center px-3 py-2.5 rounded hover:bg-gray-200'; + const buttonClassName = 'flex items-center px-3 py-2.5 rounded hover:bg-gray-200 cursor-pointer'; return ( ( + 'collectibles/DETAILS' +); diff --git a/src/app/store/collectibles/epics.ts b/src/app/store/collectibles/epics.ts new file mode 100644 index 000000000..27477f583 --- /dev/null +++ b/src/app/store/collectibles/epics.ts @@ -0,0 +1,34 @@ +import { combineEpics, Epic } from 'redux-observable'; +import { catchError, map, Observable, of, switchMap } from 'rxjs'; +import { Action } from 'ts-action'; +import { ofType, toPayload } from 'ts-action-operators'; + +import { fetchAllUserObjktCollectibles$ } from 'lib/apis/objkt'; +import { toTokenSlug } from 'lib/assets'; + +import { loadCollectiblesDetailsActions } from './actions'; +import { CollectibleDetailsRecord } from './state'; + +const loadCollectiblesDetailsEpic: Epic = (action$: Observable) => + action$.pipe( + ofType(loadCollectiblesDetailsActions.submit), + toPayload(), + switchMap(publicKeyHash => + fetchAllUserObjktCollectibles$(publicKeyHash).pipe( + map(data => { + const entries = data.token.map( + ({ fa_contract, token_id, lowest_ask }) => + [toTokenSlug(fa_contract, token_id), { floorPrice: lowest_ask }] as const + ); + const details: CollectibleDetailsRecord = Object.fromEntries(entries); + + return loadCollectiblesDetailsActions.success(details); + }), + catchError((error: unknown) => + of(loadCollectiblesDetailsActions.fail(error instanceof Error ? error.message : 'Unknown error')) + ) + ) + ) + ); + +export const collectiblesEpics = combineEpics(loadCollectiblesDetailsEpic); diff --git a/src/app/store/collectibles/reducer.ts b/src/app/store/collectibles/reducer.ts new file mode 100644 index 000000000..09da6842b --- /dev/null +++ b/src/app/store/collectibles/reducer.ts @@ -0,0 +1,20 @@ +import { createReducer } from '@reduxjs/toolkit'; + +import { createEntity } from 'lib/store'; + +import { loadCollectiblesDetailsActions } from './actions'; +import { collectiblesInitialState, CollectiblesState } from './state'; + +export const collectiblesReducer = createReducer(collectiblesInitialState, builder => { + builder.addCase(loadCollectiblesDetailsActions.submit, state => { + state.details.isLoading = true; + }); + builder.addCase(loadCollectiblesDetailsActions.success, (state, { payload }) => ({ + ...state, + details: createEntity(payload) + })); + builder.addCase(loadCollectiblesDetailsActions.fail, (state, { payload }) => { + state.details.isLoading = false; + state.details.error = payload; + }); +}); diff --git a/src/app/store/collectibles/selectors.ts b/src/app/store/collectibles/selectors.ts new file mode 100644 index 000000000..70b95f667 --- /dev/null +++ b/src/app/store/collectibles/selectors.ts @@ -0,0 +1,5 @@ +import { useSelector } from '../index'; +import type { CollectibleDetails } from './state'; + +export const useOneCollectibleDetailsSelector = (slug: string): CollectibleDetails | undefined => + useSelector(({ collectibles }) => collectibles.details.data[slug]); diff --git a/src/app/store/collectibles/state.mock.ts b/src/app/store/collectibles/state.mock.ts new file mode 100644 index 000000000..d8324afd3 --- /dev/null +++ b/src/app/store/collectibles/state.mock.ts @@ -0,0 +1,7 @@ +import { createEntity } from 'lib/store'; + +import { CollectiblesState } from './state'; + +export const mockCollectiblesState: CollectiblesState = { + details: createEntity({}) +}; diff --git a/src/app/store/collectibles/state.ts b/src/app/store/collectibles/state.ts new file mode 100644 index 000000000..29f2c5d99 --- /dev/null +++ b/src/app/store/collectibles/state.ts @@ -0,0 +1,16 @@ +import { createEntity, LoadableEntityState } from 'lib/store'; + +export interface CollectibleDetails { + /** In muTEZ */ + floorPrice: number | null; +} + +export type CollectibleDetailsRecord = Record; + +export interface CollectiblesState { + details: LoadableEntityState; +} + +export const collectiblesInitialState: CollectiblesState = { + details: createEntity({}) +}; diff --git a/src/app/store/index.ts b/src/app/store/index.ts index 056d39554..e27174525 100644 --- a/src/app/store/index.ts +++ b/src/app/store/index.ts @@ -14,6 +14,8 @@ import { balancesEpics } from './balances/epics'; import { balancesReducer } from './balances/reducers'; import { buyWithCreditCardEpics } from './buy-with-credit-card/epics'; import { buyWithCreditCardReducer } from './buy-with-credit-card/reducers'; +import { collectiblesEpics } from './collectibles/epics'; +import { collectiblesReducer } from './collectibles/reducer'; import { currencyEpics } from './currency/epics'; import { currencyReducer } from './currency/reducers'; import { dAppsReducer } from './d-apps/reducers'; @@ -36,7 +38,8 @@ const baseReducer = rootStateReducer({ balances: balancesReducer, tokensMetadata: tokensMetadataReducer, abTesting: abTestingReducer, - buyWithCreditCard: buyWithCreditCardReducer + buyWithCreditCard: buyWithCreditCardReducer, + collectibles: collectiblesReducer }); export type RootState = GetStateType; @@ -45,7 +48,8 @@ const persistConfig: PersistConfig = { key: 'temple-root', version: 1, storage: storage, - stateReconciler: autoMergeLevel2 + stateReconciler: autoMergeLevel2, + blacklist: ['collectibles'] }; const epics = [ @@ -57,7 +61,8 @@ const epics = [ balancesEpics, tokensMetadataEpics, abTestingEpics, - buyWithCreditCardEpics + buyWithCreditCardEpics, + collectiblesEpics ]; export const { store, persistor } = createStore(persistConfig, baseReducer, epics); diff --git a/src/app/store/root-state.mock.ts b/src/app/store/root-state.mock.ts index 09db2d0af..ebcb2a1b4 100644 --- a/src/app/store/root-state.mock.ts +++ b/src/app/store/root-state.mock.ts @@ -4,6 +4,7 @@ import { mockABTestingState } from './ab-testing/state.mock'; import { mockAdvertisingState } from './advertising/state.mock'; import { mockBalancesState } from './balances/state.mock'; import { mockBuyWithCreditCardState } from './buy-with-credit-card/state.mock'; +import { mockCollectiblesState } from './collectibles/state.mock'; import { mockCurrencyState } from './currency/state.mock'; import { mockDAppsState } from './d-apps/state.mock'; import { RootState } from './index'; @@ -24,5 +25,6 @@ export const mockRootState: RootState = { balances: mockBalancesState, tokensMetadata: mockTokensMetadataState, abTesting: mockABTestingState, - buyWithCreditCard: mockBuyWithCreditCardState + buyWithCreditCard: mockBuyWithCreditCardState, + collectibles: mockCollectiblesState }; diff --git a/src/lib/apis/objkt/constants.ts b/src/lib/apis/objkt/constants.ts new file mode 100644 index 000000000..f7b31f6d6 --- /dev/null +++ b/src/lib/apis/objkt/constants.ts @@ -0,0 +1,5 @@ +import { getApolloConfigurableClient } from '../apollo'; + +const OBJKT_API = 'https://data.objkt.com/v3/graphql/'; + +export const apolloObjktClient = getApolloConfigurableClient(OBJKT_API); diff --git a/src/lib/apis/objkt/index.ts b/src/lib/apis/objkt/index.ts new file mode 100644 index 000000000..78c7a5635 --- /dev/null +++ b/src/lib/apis/objkt/index.ts @@ -0,0 +1,18 @@ +import { apolloObjktClient } from './constants'; +import { buildGetAllUserCollectiblesQuery } from './queries'; + +interface GetUserObjktCollectiblesResponse { + token: UserObjktCollectible[]; +} + +interface UserObjktCollectible { + fa_contract: string; + token_id: string; + lowest_ask: number | null; +} + +export const fetchAllUserObjktCollectibles$ = (address: string) => { + const request = buildGetAllUserCollectiblesQuery(address); + + return apolloObjktClient.query(request); +}; diff --git a/src/lib/apis/objkt/queries.ts b/src/lib/apis/objkt/queries.ts new file mode 100644 index 000000000..36b4ac10f --- /dev/null +++ b/src/lib/apis/objkt/queries.ts @@ -0,0 +1,13 @@ +import { gql } from '@apollo/client'; + +export const buildGetAllUserCollectiblesQuery = (address: string) => { + return gql` + query MyQuery { + token(where: {holders: {holder_address: {_eq: "${address}"}}}) { + fa_contract + token_id + lowest_ask + } + } + `; +}; diff --git a/src/lib/fixed-times.ts b/src/lib/fixed-times.ts index bd5faf701..a99b6d047 100644 --- a/src/lib/fixed-times.ts +++ b/src/lib/fixed-times.ts @@ -17,3 +17,5 @@ export const USER_ACTION_TIMEOUT = 30_000; export const CONFIRM_TIMEOUT = LONG_INTERVAL; export const WALLET_AUTOLOCK_TIME = LONG_INTERVAL; + +export const COLLECTIBLES_DETAILS_SYNC_INTERVAL = LONG_INTERVAL; From 1583a18e85c8bfe6570f32a89175a2f29c924fec Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 4 Jul 2023 15:18:37 +0300 Subject: [PATCH 11/21] TW-828: Collectibles grid layout. Fix after QA --- src/app/pages/Collectibles/CollectiblesTab.tsx | 16 ++++++++++++---- src/app/pages/Home/ContentSection.tsx | 8 +++++--- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/app/pages/Collectibles/CollectiblesTab.tsx b/src/app/pages/Collectibles/CollectiblesTab.tsx index c53cacb26..26784db3b 100644 --- a/src/app/pages/Collectibles/CollectiblesTab.tsx +++ b/src/app/pages/Collectibles/CollectiblesTab.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import clsx from 'clsx'; @@ -7,6 +7,7 @@ import { useAppEnv } from 'app/env'; import { ReactComponent as ManageIcon } from 'app/icons/manage.svg'; import { CollectibleItem } from 'app/pages/Collectibles/CollectibleItem'; import { AssetsSelectors } from 'app/pages/Home/OtherComponents/Assets.selectors'; +import { useTokensMetadataLoadingSelector } from 'app/store/tokens-metadata/selectors'; import SearchAssetField from 'app/templates/SearchAssetField'; import { AssetTypesEnum } from 'lib/assets/types'; import { T, t } from 'lib/i18n'; @@ -18,14 +19,21 @@ export const CollectiblesTab = () => { const chainId = useChainId(true)!; const { popup } = useAppEnv(); const { publicKeyHash } = useAccount(); - const { isSyncing } = useSyncTokens(); + const { isSyncing: tokensAreSyncing } = useSyncTokens(); + const metadatasLoading = useTokensMetadataLoadingSelector(); - const { data: collectibles = [] } = useCollectibleTokens(chainId, publicKeyHash, true); + const { data: collectibles = [], isValidating: readingCollectibles } = useCollectibleTokens( + chainId, + publicKeyHash, + true + ); - const collectibleSlugs = collectibles.map(collectible => collectible.tokenSlug); + const collectibleSlugs = useMemo(() => collectibles.map(collectible => collectible.tokenSlug), [collectibles]); const { filteredAssets, searchValue, setSearchValue } = useFilteredAssets(collectibleSlugs); + const isSyncing = tokensAreSyncing || metadatasLoading || readingCollectibles; + return (
diff --git a/src/app/pages/Home/ContentSection.tsx b/src/app/pages/Home/ContentSection.tsx index 9ccfb66ea..9c383e4a9 100644 --- a/src/app/pages/Home/ContentSection.tsx +++ b/src/app/pages/Home/ContentSection.tsx @@ -42,8 +42,10 @@ type Props = { className?: string; }; +type TabSlug = 'tokens' | 'collectibles' | 'activity' | 'delegation' | 'info'; + interface TabData { - slug: string; + slug: TabSlug; titleI18nKey: TID; Component: FC; testID: string; @@ -116,7 +118,7 @@ export const ContentSection: FC = ({ assetSlug, className }) => { const tabBarElemRef = useRef(null); useDidUpdate(() => { - if (!tabBarElemRef.current) return; + if (!tabBarElemRef.current || slug !== 'collectibles') return; const stickyBarHeight = ToolbarElement?.scrollHeight ?? 0; @@ -124,7 +126,7 @@ export const ContentSection: FC = ({ assetSlug, className }) => { top: window.pageYOffset + tabBarElemRef.current.getBoundingClientRect().top - stickyBarHeight, behavior: 'smooth' }); - }, [tabSlug]); + }, [slug]); return (
From 0757ded13686eedc357540dfa099344b1994a463 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 5 Jul 2023 00:34:48 +0300 Subject: [PATCH 12/21] TW-830: Collectibles Manage dropdown. Fix floor price currency --- public/_locales/en/messages.json | 3 ++ .../pages/Collectibles/CollectibleItem.tsx | 37 +++++++++++------ .../pages/Collectibles/CollectiblesTab.tsx | 19 +++++---- src/app/store/collectibles/epics.ts | 17 ++++++-- src/app/store/collectibles/selectors.ts | 2 +- src/app/store/collectibles/state.ts | 7 +++- src/lib/apis/objkt/constants.ts | 40 +++++++++++++++++++ src/lib/apis/objkt/index.ts | 9 ++++- src/lib/apis/objkt/queries.ts | 5 ++- 9 files changed, 110 insertions(+), 29 deletions(-) diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index 88d4850ff..597943931 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -3052,5 +3052,8 @@ }, "creditCard": { "message": "credit card" + }, + "notListed": { + "message": "Not listed" } } diff --git a/src/app/pages/Collectibles/CollectibleItem.tsx b/src/app/pages/Collectibles/CollectibleItem.tsx index 3ad139243..8ff694387 100644 --- a/src/app/pages/Collectibles/CollectibleItem.tsx +++ b/src/app/pages/Collectibles/CollectibleItem.tsx @@ -1,11 +1,13 @@ -import React, { FC, useCallback, useRef, useState } from 'react'; +import React, { FC, useCallback, useRef, useState, useMemo } from 'react'; import { isDefined } from '@rnw-community/shared'; import clsx from 'clsx'; import Money from 'app/atoms/Money'; import { useAppEnv } from 'app/env'; -import { useOneCollectibleDetailsSelector } from 'app/store/collectibles/selectors'; +import { useCollectibleDetailsSelector } from 'app/store/collectibles/selectors'; +import { objktCurrencies } from 'lib/apis/objkt'; +import { T } from 'lib/i18n'; import { useAssetMetadata, getAssetName, TEZOS_METADATA } from 'lib/metadata'; import { useBalance } from 'lib/temple/front'; import { atomsToTokens } from 'lib/temple/helpers'; @@ -17,18 +19,27 @@ import { CollectibleItemImage } from './CollectibleItemImage'; interface Props { assetSlug: string; accountPkh: string; - detailsShown: boolean; + areDetailsShown: boolean; } -export const CollectibleItem: FC = ({ assetSlug, accountPkh, detailsShown }) => { +export const CollectibleItem: FC = ({ assetSlug, accountPkh, areDetailsShown }) => { const { popup } = useAppEnv(); const metadata = useAssetMetadata(assetSlug); const toDisplayRef = useRef(null); const [displayed, setDisplayed] = useState(true); const { data: balance } = useBalance(assetSlug, accountPkh, { displayed }); - const details = useOneCollectibleDetailsSelector(assetSlug); + const details = useCollectibleDetailsSelector(assetSlug); - const floorPrice = details?.floorPrice; + const listing = useMemo(() => { + if (!isDefined(details)) return null; + + const { floorPrice, currencyId } = details.listing; + const currency = objktCurrencies[currencyId]; + + if (!isDefined(currency)) return null; + + return { floorPrice, decimals: currency.decimals, symbol: currency.symbol }; + }, [details]); const handleIntersection = useCallback(() => { setDisplayed(true); @@ -44,7 +55,7 @@ export const CollectibleItem: FC = ({ assetSlug, accountPkh, detailsShown ref={toDisplayRef} className={clsx( 'relative flex items-center justify-center bg-blue-50 rounded-lg overflow-hidden hover:opacity-70', - detailsShown ? 'border-b border-gray-300' : undefined + areDetailsShown ? 'border-b border-gray-300' : undefined )} title={assetName} style={{ height: popup ? 106 : 125 }} @@ -58,20 +69,20 @@ export const CollectibleItem: FC = ({ assetSlug, accountPkh, detailsShown ) : null}
- {detailsShown && ( + {areDetailsShown && (
{assetName}
- Floor: - {isDefined(floorPrice) ? ( + {isDefined(listing) ? ( <> + Floor: - {atomsToTokens(floorPrice, TEZOS_METADATA.decimals)} + {atomsToTokens(listing.floorPrice, listing.decimals)} - TEZ + {listing.symbol} ) : ( - '---' + )}
diff --git a/src/app/pages/Collectibles/CollectiblesTab.tsx b/src/app/pages/Collectibles/CollectiblesTab.tsx index 42ec920d8..3126909e2 100644 --- a/src/app/pages/Collectibles/CollectiblesTab.tsx +++ b/src/app/pages/Collectibles/CollectiblesTab.tsx @@ -31,7 +31,7 @@ export const CollectiblesTab = () => { const { isSyncing: tokensAreSyncing } = useSyncTokens(); const metadatasLoading = useTokensMetadataLoadingSelector(); - const [detailsShown, setDetailsShown] = useLocalStorage(LOCAL_STORAGE_TOGGLE_KEY, false); + const [areDetailsShown, setDetailsShown] = useLocalStorage(LOCAL_STORAGE_TOGGLE_KEY, false); const { data: collectibles = [], isValidating: readingCollectibles } = useCollectibleTokens( chainId, @@ -61,8 +61,8 @@ export const CollectiblesTab = () => { popup={props => ( void setDetailsShown(!detailsShown)} + areDetailsShown={areDetailsShown} + toggleDetailsShown={() => void setDetailsShown(!areDetailsShown)} /> )} > @@ -97,7 +97,12 @@ export const CollectiblesTab = () => { <>
{filteredAssets.map(slug => ( - + ))}
@@ -110,11 +115,11 @@ export const CollectiblesTab = () => { }; interface ManageButtonDropdownProps extends PopperRenderProps { - detailsShown: boolean; + areDetailsShown: boolean; toggleDetailsShown: EmptyFn; } -const ManageButtonDropdown: FC = ({ opened, detailsShown, toggleDetailsShown }) => { +const ManageButtonDropdown: FC = ({ opened, areDetailsShown, toggleDetailsShown }) => { const buttonClassName = 'flex items-center px-3 py-2.5 rounded hover:bg-gray-200 cursor-pointer'; return ( @@ -139,7 +144,7 @@ const ManageButtonDropdown: FC = ({ opened, detailsSh