diff --git a/package.json b/package.json index 56d9742cc..1e88515dd 100644 --- a/package.json +++ b/package.json @@ -181,6 +181,7 @@ "ts-node": "^10.4.0", "ts-prune": "^0.10.3", "typescript": "4.5.5", + "use-custom-compare": "^1.4.0", "use-debounce": "7.0.1", "use-force-update": "1.0.7", "use-onclickoutside": "0.4.1", diff --git a/public/_locales/de/messages.json b/public/_locales/de/messages.json index 45e311cb4..f994b218b 100644 --- a/public/_locales/de/messages.json +++ b/public/_locales/de/messages.json @@ -1856,7 +1856,7 @@ "message": "Es wurden keine passenden dApps gefunden" }, "thousandFormat": { - "message": "$thousand$k", + "message": "$thousand$K", "placeholders": { "thousand": { "content": "$1" diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index 2e261a3b9..69cdf9403 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -2149,7 +2149,7 @@ "message": "No matching dApps were found" }, "thousandFormat": { - "message": "$thousand$k", + "message": "$thousand$K", "placeholders": { "thousand": { "content": "$1" @@ -2358,6 +2358,9 @@ "message": "less", "description": "Show less" }, + "showInfo": { + "message": "Show info" + }, "recentDestinations": { "message": "Recent destinations" }, diff --git a/public/_locales/pt/messages.json b/public/_locales/pt/messages.json index 0747a117c..33f7ed617 100644 --- a/public/_locales/pt/messages.json +++ b/public/_locales/pt/messages.json @@ -1856,7 +1856,7 @@ "message": "Não foram encontrados dApps correspondentes" }, "thousandFormat": { - "message": "$thousand$k", + "message": "$thousand$K", "placeholders": { "thousand": { "content": "$1" diff --git a/public/_locales/tr/messages.json b/public/_locales/tr/messages.json index a079ac6e9..675d4ffc2 100644 --- a/public/_locales/tr/messages.json +++ b/public/_locales/tr/messages.json @@ -1856,7 +1856,7 @@ "message": "Eşleşen dApps bulunamadı" }, "thousandFormat": { - "message": "$thousand$k", + "message": "$thousand$K", "placeholders": { "thousand": { "content": "$1" 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/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/atoms/Banner.tsx b/src/app/atoms/Banner.tsx index 29811d0a3..bff108b2d 100644 --- a/src/app/atoms/Banner.tsx +++ b/src/app/atoms/Banner.tsx @@ -27,11 +27,11 @@ export const Banner: FC = ({

{description}

- + {t(disableButtonText)} - + {t(enableButtonText)}
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/DonationBanner/DonationBanner.tsx b/src/app/atoms/DonationBanner/DonationBanner.tsx index a15918fb6..a3310101f 100644 --- a/src/app/atoms/DonationBanner/DonationBanner.tsx +++ b/src/app/atoms/DonationBanner/DonationBanner.tsx @@ -10,7 +10,7 @@ const DONATE_MAD_FISH_URL = 'https://donate.mad.fish'; export const DonationBanner: FC = () => ( diff --git a/src/app/atoms/DropdownWrapper.tsx b/src/app/atoms/DropdownWrapper.tsx index 14fb02bc4..7ad3868fb 100644 --- a/src/app/atoms/DropdownWrapper.tsx +++ b/src/app/atoms/DropdownWrapper.tsx @@ -5,12 +5,21 @@ import CSSTransition from 'react-transition-group/CSSTransition'; type DropdownWrapperProps = HTMLAttributes & { opened: boolean; + design?: Design; hiddenOverflow?: boolean; scaleAnimation?: boolean; }; +const DESIGN_CLASS_NAMES = { + light: 'bg-white border-gray-300', + dark: 'bg-gray-910 border-gray-850' +}; + +type Design = keyof typeof DESIGN_CLASS_NAMES; + const DropdownWrapper: FC = ({ opened, + design = 'light', hiddenOverflow = true, scaleAnimation = true, className, @@ -36,13 +45,10 @@ const DropdownWrapper: FC = ({ 'mt-2 border rounded-md shadow-xl', hiddenOverflow && 'overflow-hidden', process.env.TARGET_BROWSER === 'firefox' && 'grayscale-firefox-fix', + DESIGN_CLASS_NAMES[design], className )} - style={{ - backgroundColor: '#1b262c', - borderColor: '#212e36', - ...style - }} + style={style} {...rest} /> 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/atoms/Money.tsx b/src/app/atoms/Money.tsx index ea9e136f9..5b6b40195 100644 --- a/src/app/atoms/Money.tsx +++ b/src/app/atoms/Money.tsx @@ -68,7 +68,7 @@ const Money = memo( ); } - if (!fiat && decimalsLength > cryptoDecimals && !shortened) { + if (!fiat && !shortened && decimalsLength > cryptoDecimals) { return ( { + const chainId = useChainId()!; + const { publicKeyHash } = useAccount(); + const { data: collectibles } = useCollectibleTokens(chainId, publicKeyHash); + const dispatch = useDispatch(); + + const slugs = useCustomCompareMemo( + () => collectibles.map(({ tokenSlug }) => tokenSlug).sort(), + [collectibles], + isEqual + ); + + useInterval( + () => { + if (slugs.length < 1) return; + + dispatch(loadCollectiblesDetailsActions.submit(slugs)); + }, + COLLECTIBLES_DETAILS_SYNC_INTERVAL, + [slugs, dispatch] + ); +}; 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/layouts/PageLayout/Header/AccountDropdown/index.tsx b/src/app/layouts/PageLayout/Header/AccountDropdown/index.tsx index fb33fffaf..ded64f121 100644 --- a/src/app/layouts/PageLayout/Header/AccountDropdown/index.tsx +++ b/src/app/layouts/PageLayout/Header/AccountDropdown/index.tsx @@ -140,9 +140,9 @@ const AccountDropdown: FC = ({ opened, setOpened }) => { return ( = () => { placement="bottom-end" strategy="fixed" popup={({ opened, setOpened }) => ( - +

{
+ +

{collectibleName}

+

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

+

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

+ = ({ assetSlug }) => {
+

+

{assetId.toFixed()}

+ = ({ assetSlug }) => {
+ + { +const LOCAL_STORAGE_TOGGLE_KEY = 'collectibles-grid:show-items-details'; +const svgIconClassName = 'w-4 h-4 stroke-current fill-current text-gray-600'; + +interface Props { + scrollToTheTabsBar: EmptyFn; +} + +export const CollectiblesTab: FC = ({ scrollToTheTabsBar }) => { const chainId = useChainId(true)!; const { popup } = useAppEnv(); const { publicKeyHash } = useAccount(); const { isSyncing: tokensAreSyncing } = useSyncTokens(); const metadatasLoading = useTokensMetadataLoadingSelector(); + const [areDetailsShown, setDetailsShown] = useLocalStorage(LOCAL_STORAGE_TOGGLE_KEY, false); + const { data: collectibles = [], isValidating: readingCollectibles } = useCollectibleTokens( chainId, publicKeyHash, @@ -32,6 +47,8 @@ export const CollectiblesTab = () => { const { filteredAssets, searchValue, setSearchValue } = useFilteredAssets(collectibleSlugs); + useEffect(() => void scrollToTheTabsBar(), [collectibles.length > 0]); + const isSyncing = tokensAreSyncing || metadatasLoading || readingCollectibles; return ( @@ -44,20 +61,34 @@ export const CollectiblesTab = () => { testID={AssetsSelectors.searchAssetsInputCollectibles} /> - ( + void setDetailsShown(!areDetailsShown)} + /> )} - testID={AssetsSelectors.manageButton} > - - + {({ ref, opened, toggleOpened }) => ( + + )} +

{isSyncing && filteredAssets.length === 0 ? ( @@ -71,8 +102,13 @@ export const CollectiblesTab = () => { ) : ( <>
- {filteredAssets.map((slug, index) => ( - + {filteredAssets.map(slug => ( + ))}
@@ -83,3 +119,45 @@ export const CollectiblesTab = () => {
); }; + +interface ManageButtonDropdownProps extends PopperRenderProps { + areDetailsShown: boolean; + toggleDetailsShown: EmptyFn; +} + +const ManageButtonDropdown: FC = ({ opened, areDetailsShown, toggleDetailsShown }) => { + const buttonClassName = 'flex items-center px-3 py-2.5 rounded hover:bg-gray-200 cursor-pointer'; + + return ( + + + + + + + + + + + + + ); +}; diff --git a/src/app/pages/Home/ContentSection.tsx b/src/app/pages/Home/ContentSection.tsx index 9c383e4a9..268cade22 100644 --- a/src/app/pages/Home/ContentSection.tsx +++ b/src/app/pages/Home/ContentSection.tsx @@ -1,4 +1,4 @@ -import React, { FC, ReactNode, Suspense, useMemo, useRef } from 'react'; +import React, { FC, ReactNode, Suspense, useCallback, useMemo, useRef } from 'react'; import clsx from 'clsx'; @@ -12,7 +12,6 @@ import AssetInfo from 'app/templates/AssetInfo'; import { ABTestGroup } from 'lib/apis/temple'; import { isTezAsset } from 'lib/assets'; import { T, t, TID } from 'lib/i18n'; -import { useDidUpdate } from 'lib/ui/hooks'; import { Link } from 'lib/woozie'; import { useUserTestingGroupNameSelector } from '../../store/ab-testing/selectors'; @@ -21,22 +20,6 @@ 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; @@ -49,6 +32,7 @@ interface TabData { titleI18nKey: TID; Component: FC; testID: string; + whileMessageI18nKey?: TID; } export const ContentSection: FC = ({ assetSlug, className }) => { @@ -56,6 +40,19 @@ export const ContentSection: FC = ({ assetSlug, className }) => { const tabSlug = useTabSlug(); const testGroupName = useUserTestingGroupNameSelector(); + const tabBarElemRef = useRef(null); + + const scrollToTheTabsBar = useCallback(() => { + if (!tabBarElemRef.current) return; + + const stickyBarHeight = ToolbarElement?.scrollHeight ?? 0; + + window.scrollTo({ + top: window.pageYOffset + tabBarElemRef.current.getBoundingClientRect().top - stickyBarHeight, + behavior: 'smooth' + }); + }, []); + const tabs = useMemo(() => { if (!assetSlug) { return [ @@ -68,14 +65,15 @@ export const ContentSection: FC = ({ assetSlug, className }) => { { slug: 'collectibles', titleI18nKey: 'collectibles', - Component: CollectiblesTab, + Component: () => , testID: HomeSelectors.collectiblesTab }, { slug: 'activity', titleI18nKey: 'activity', - Component: ActivityTab, - testID: HomeSelectors.activityTab + Component: ActivityComponent, + testID: HomeSelectors.activityTab, + whileMessageI18nKey: 'operationHistoryWhileMessage' } ]; } @@ -83,7 +81,7 @@ export const ContentSection: FC = ({ assetSlug, className }) => { const activity: TabData = { slug: 'activity', titleI18nKey: 'activity', - Component: () => , + Component: () => , testID: HomeSelectors.activityTab }; @@ -93,8 +91,9 @@ export const ContentSection: FC = ({ assetSlug, className }) => { { slug: 'delegation', titleI18nKey: 'delegate', - Component: Delegation, - testID: HomeSelectors.delegationTab + Component: BakingSection, + testID: HomeSelectors.delegationTab, + whileMessageI18nKey: 'delegationInfoWhileMessage' } ]; } @@ -108,26 +107,13 @@ export const ContentSection: FC = ({ assetSlug, className }) => { testID: HomeSelectors.aboutTab } ]; - }, [assetSlug]); + }, [assetSlug, scrollToTheTabsBar]); - const { slug, Component } = useMemo(() => { + const { slug, Component, whileMessageI18nKey } = useMemo(() => { const tab = tabSlug ? tabs.find(currentTab => currentTab.slug === tabSlug) : null; return tab ?? tabs[0]; }, [tabSlug, tabs]); - const tabBarElemRef = useRef(null); - - useDidUpdate(() => { - if (!tabBarElemRef.current || slug !== 'collectibles') return; - - const stickyBarHeight = ToolbarElement?.scrollHeight ?? 0; - - window.scrollTo({ - top: window.pageYOffset + tabBarElemRef.current.getBoundingClientRect().top - stickyBarHeight, - behavior: 'smooth' - }); - }, [slug]); - return (
@@ -141,7 +127,9 @@ export const ContentSection: FC = ({ assetSlug, className }) => { ))}
- {Component && } + + {Component && } +
); }; 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/src/app/store/collectibles/actions.ts b/src/app/store/collectibles/actions.ts new file mode 100644 index 000000000..d53862d2e --- /dev/null +++ b/src/app/store/collectibles/actions.ts @@ -0,0 +1,7 @@ +import { createActions } from 'lib/store'; + +import { CollectibleDetailsRecord } from './state'; + +export const loadCollectiblesDetailsActions = createActions( + 'collectibles/DETAILS' +); diff --git a/src/app/store/collectibles/epics.ts b/src/app/store/collectibles/epics.ts new file mode 100644 index 000000000..4c3f3b54a --- /dev/null +++ b/src/app/store/collectibles/epics.ts @@ -0,0 +1,43 @@ +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 { fetchObjktCollectibles$ } from 'lib/apis/objkt'; +import { toTokenSlug } from 'lib/assets'; +import { isTruthy } from 'lib/utils'; + +import { loadCollectiblesDetailsActions } from './actions'; +import { CollectibleDetailsRecord } from './state'; + +const loadCollectiblesDetailsEpic: Epic = (action$: Observable) => + action$.pipe( + ofType(loadCollectiblesDetailsActions.submit), + toPayload(), + switchMap(slugs => + fetchObjktCollectibles$(slugs).pipe( + map(data => { + const entries = data.token + .map(({ fa_contract, token_id, listings_active }) => { + const cheepestListing = listings_active[0]; + const listing = cheepestListing && { + floorPrice: cheepestListing.price, + currencyId: cheepestListing.currency_id + }; + + return listing && ([toTokenSlug(fa_contract, token_id), { listing }] as const); + }) + .filter(isTruthy); + + 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..08f784c02 --- /dev/null +++ b/src/app/store/collectibles/selectors.ts @@ -0,0 +1,5 @@ +import { useSelector } from '../index'; +import type { CollectibleDetails } from './state'; + +export const useCollectibleDetailsSelector = (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..937aec63e --- /dev/null +++ b/src/app/store/collectibles/state.ts @@ -0,0 +1,19 @@ +import { createEntity, LoadableEntityState } from 'lib/store'; + +export interface CollectibleDetails { + listing: { + /** In atoms */ + floorPrice: number; + currencyId: number; + }; +} + +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 e9323f5d6..8c4474098 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'; @@ -38,12 +40,13 @@ const baseReducer = rootStateReducer({ tokensMetadata: tokensMetadataReducer, abTesting: abTestingReducer, buyWithCreditCard: buyWithCreditCardReducer, + collectibles: collectiblesReducer, newsletter: newsletterReducers }); export type RootState = GetStateType; -const persistConfigBlacklist: (keyof RootState)[] = ['buyWithCreditCard']; +const persistConfigBlacklist: (keyof RootState)[] = ['buyWithCreditCard', 'collectibles']; const persistConfig: PersistConfig = { key: 'temple-root', @@ -62,7 +65,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 bdb443fb5..55ba4481f 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'; @@ -26,5 +27,6 @@ export const mockRootState: RootState = { tokensMetadata: mockTokensMetadataState, abTesting: mockABTestingState, buyWithCreditCard: mockBuyWithCreditCardState, + collectibles: mockCollectiblesState, newsletter: mockNewsletterState }; diff --git a/src/app/templates/IconifiedSelect/Menu.tsx b/src/app/templates/IconifiedSelect/Menu.tsx index ba2ca2ea2..9e064a499 100644 --- a/src/app/templates/IconifiedSelect/Menu.tsx +++ b/src/app/templates/IconifiedSelect/Menu.tsx @@ -52,12 +52,7 @@ export const IconifiedSelectMenu = (props: Props) => { return ( {options.length ? ( options.map(option => ( diff --git a/src/app/templates/PaymentProviderInput/PaymentProvidersMenu/index.tsx b/src/app/templates/PaymentProviderInput/PaymentProvidersMenu/index.tsx index c1ff04d96..7210b4433 100644 --- a/src/app/templates/PaymentProviderInput/PaymentProvidersMenu/index.tsx +++ b/src/app/templates/PaymentProviderInput/PaymentProvidersMenu/index.tsx @@ -1,7 +1,5 @@ import React, { FC, useEffect } from 'react'; -import classNames from 'clsx'; - import DropdownWrapper from 'app/atoms/DropdownWrapper'; import Spinner from 'app/atoms/Spinner/Spinner'; import { useAppEnvStyle } from 'app/hooks/use-app-env-style.hook'; @@ -10,7 +8,6 @@ import { PaymentProviderInterface } from 'lib/buy-with-credit-card/topup.interfa import { T } from 'lib/i18n'; import { PaymentProviderOption } from './PaymentProviderOption'; -import styles from './style.module.css'; interface Props extends TestIDProperty { value?: PaymentProviderInterface; @@ -50,13 +47,13 @@ export const PaymentProvidersMenu: FC = ({ return ( {(options.length === 0 || isLoading) && (
{isLoading ? ( - + ) : (

diff --git a/src/app/templates/PaymentProviderInput/PaymentProvidersMenu/style.module.css b/src/app/templates/PaymentProviderInput/PaymentProvidersMenu/style.module.css deleted file mode 100644 index 58a7d91b9..000000000 --- a/src/app/templates/PaymentProviderInput/PaymentProvidersMenu/style.module.css +++ /dev/null @@ -1,8 +0,0 @@ -.root { - margin-top: 0.25rem !important; - max-height: 15rem; -} - -.spinner { - width: 3rem; -} \ No newline at end of file diff --git a/src/app/templates/SendForm/ContactsDropdown.tsx b/src/app/templates/SendForm/ContactsDropdown.tsx index c72b8d32a..59c89840e 100644 --- a/src/app/templates/SendForm/ContactsDropdown.tsx +++ b/src/app/templates/SendForm/ContactsDropdown.tsx @@ -57,17 +57,9 @@ const ContactsDropdown = memo(({ contacts, opened, onSele scaleAnimation={false} opened={opened} className={classNames( - 'absolute left-0 right-0 p-2', - 'origin-top overflow-x-hidden overflow-y-auto', - 'z-50 overscroll-contain' + 'z-50 absolute left-0 right-0 top-full max-h-44', + 'origin-top overflow-x-hidden overflow-y-auto overscroll-contain' )} - style={{ - top: '100%', - maxHeight: '11rem', - backgroundColor: 'white', - borderColor: '#e2e8f0', - padding: 0 - }} > {filteredContacts.length > 0 ? ( filteredContacts.map(contact => ( diff --git a/src/app/templates/SwapForm/SwapFormInput/AssetsMenu/AssetsMenu.tsx b/src/app/templates/SwapForm/SwapFormInput/AssetsMenu/AssetsMenu.tsx index f6066dc0a..c8b0eb0a1 100644 --- a/src/app/templates/SwapForm/SwapFormInput/AssetsMenu/AssetsMenu.tsx +++ b/src/app/templates/SwapForm/SwapFormInput/AssetsMenu/AssetsMenu.tsx @@ -49,15 +49,7 @@ export const AssetsMenu: FC = ({ }; return ( - + {(options.length === 0 || isLoading) && (

{isLoading ? ( diff --git a/src/app/templates/TopUpInput/CurrenciesMenu/index.tsx b/src/app/templates/TopUpInput/CurrenciesMenu/index.tsx index 5594550ee..613fb43c9 100644 --- a/src/app/templates/TopUpInput/CurrenciesMenu/index.tsx +++ b/src/app/templates/TopUpInput/CurrenciesMenu/index.tsx @@ -55,16 +55,7 @@ export const CurrenciesMenu: FC = ({ : undefined; return ( - + {(options.length === 0 || isLoading) && (
{isLoading ? ( diff --git a/src/lib/apis/objkt/constants.ts b/src/lib/apis/objkt/constants.ts new file mode 100644 index 000000000..6ce9c69ee --- /dev/null +++ b/src/lib/apis/objkt/constants.ts @@ -0,0 +1,45 @@ +import { getApolloConfigurableClient } from '../apollo'; + +const OBJKT_API = 'https://data.objkt.com/v3/graphql/'; + +export const apolloObjktClient = getApolloConfigurableClient(OBJKT_API); + +interface ObjktCurrencyInfo { + symbol: string; + decimals: number; + contract: string | null; + id: string | null; +} + +export const objktCurrencies: Record = { + 2537: { + symbol: 'uUSD', + decimals: 12, + contract: 'KT1XRPEPXbZK25r3Htzp2o1x7xdMMmfocKNW', + id: '0' + }, + 2557: { + symbol: 'USDtz', + decimals: 6, + contract: 'KT1LN4LPSqTMS7Sd2CJw4bbDGRkMv2t68Fy9', + id: '0' + }, + 1: { + symbol: 'TEZ', + decimals: 6, + contract: null, + id: null + }, + 4: { + symbol: 'oXTZ', + decimals: 6, + contract: 'KT1TjnZYs5CGLbmV6yuW169P8Pnr9BiVwwjz', + id: '0' + }, + 3: { + symbol: 'USDtz', + decimals: 6, + contract: 'KT1LN4LPSqTMS7Sd2CJw4bbDGRkMv2t68Fy9', + id: '0' + } +}; diff --git a/src/lib/apis/objkt/index.ts b/src/lib/apis/objkt/index.ts new file mode 100644 index 000000000..e8458dee3 --- /dev/null +++ b/src/lib/apis/objkt/index.ts @@ -0,0 +1,37 @@ +import { fromFa2TokenSlug } from 'lib/assets/utils'; + +import { apolloObjktClient } from './constants'; +import { buildGetCollectiblesQuery } from './queries'; + +export { objktCurrencies } from './constants'; + +interface GetUserObjktCollectiblesResponse { + token: UserObjktCollectible[]; +} + +interface ObjktListing { + currency_id: number; + price: number; +} + +interface UserObjktCollectible { + fa_contract: string; + token_id: string; + listings_active: ObjktListing[]; +} + +export const fetchObjktCollectibles$ = (slugs: string[]) => { + const request = buildGetCollectiblesQuery(); + + const queryVariables = { + where: { + _or: slugs.map(slug => { + const { contract, id } = fromFa2TokenSlug(slug); + + return { fa_contract: { _eq: contract }, token_id: { _eq: String(id) } }; + }) + } + }; + + return apolloObjktClient.query(request, queryVariables); +}; diff --git a/src/lib/apis/objkt/queries.ts b/src/lib/apis/objkt/queries.ts new file mode 100644 index 000000000..d27250b69 --- /dev/null +++ b/src/lib/apis/objkt/queries.ts @@ -0,0 +1,14 @@ +import { gql } from '@apollo/client'; + +export const buildGetCollectiblesQuery = () => gql` + query MyQuery($where: token_bool_exp) { + token(where: $where) { + fa_contract + token_id + listings_active(order_by: { price_xtz: asc }) { + currency_id + price + } + } + } +`; 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; diff --git a/src/lib/i18n/numbers.ts b/src/lib/i18n/numbers.ts index 3c38eba81..fa4d038b9 100644 --- a/src/lib/i18n/numbers.ts +++ b/src/lib/i18n/numbers.ts @@ -76,19 +76,23 @@ export function toLocalFixed(value: BigNumber.Value, decimalPlaces?: number, rou export function toShortened(value: BigNumber.Value) { let bn = new BigNumber(value); - if (bn.abs().lt(1)) { - return toLocalFixed(bn.toPrecision(1)); - } + const target = bn.abs().decimalPlaces(2); + + if (target.lt(0.01)) return toLocalFixed(bn.toPrecision(2)); + + if (target.lt(10_000)) return toLocalFixed(bn, 2); + bn = bn.integerValue(); + const formats: TID[] = ['thousandFormat', 'millionFormat', 'billionFormat']; + let formatIndex = -1; while (bn.abs().gte(1000) && formatIndex < formats.length - 1) { formatIndex++; bn = bn.div(1000); } - bn = bn.decimalPlaces(1, BigNumber.ROUND_FLOOR); - if (formatIndex === -1) { - return toLocalFixed(bn); - } - return t(formats[formatIndex], toLocalFixed(bn)); + + if (formatIndex === -1) return toLocalFixed(bn, 2); + + return t(formats[formatIndex], toLocalFixed(bn, 0)); } diff --git a/tailwind.config.js b/tailwind.config.js index 73720a5ab..cedac0979 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -39,6 +39,7 @@ module.exports = { 600: '#718096', 700: '#4a5568', 800: '#2d3748', + 850: '#212e36', 900: '#1a202c', 910: '#1b262c' }, @@ -167,37 +168,6 @@ module.exports = { ...brandColors }; })(), - spacing: { - px: '1px', - 0: '0', - 1: '0.25rem', - 1.5: '0.375rem', - 2: '0.5rem', - 2.5: '0.625rem', - 3: '0.75rem', - 4: '1rem', - 5: '1.25rem', - 6: '1.5rem', - 7: '1.75rem', - 8: '2rem', - 9: '2.25rem', - 10: '2.5rem', - 12: '3rem', - 14: '3.5rem', - 15: '3.75rem', - 16: '4rem', - 20: '5rem', - 24: '6rem', - 25: '6.25rem', - 29: '7.25rem', - 32: '8rem', - 35: '8.75rem', - 40: '10rem', - 48: '12rem', - 56: '14rem', - 64: '16rem', - 96: '24rem' - }, backgroundColor: theme => theme('colors'), backgroundOpacity: theme => theme('opacity'), backgroundPosition: { @@ -311,6 +281,7 @@ module.exports = { }; })(), fontSize: { + '2xs': '0.625rem', xs: '0.75rem', sm: '0.875rem', base: '1rem', @@ -335,26 +306,6 @@ module.exports = { extrabold: '800', black: '900' }, - height: theme => ({ - auto: 'auto', - ...theme('spacing'), - 2.25: '2.25rem', - 12: '3rem', - 700: '700px', - 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', @@ -363,63 +314,11 @@ 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', decimal: 'decimal' }, - margin: (theme, { negative }) => ({ - auto: 'auto', - ...theme('spacing'), - ...negative(theme('spacing')) - }), - maxHeight: { - full: '100%', - '3/25': '52px', - screen: '100vh' - }, - maxWidth: (theme, { breakpoints }) => ({ - none: 'none', - 100: '6.25rem', - xs: '20rem', - sm: '24rem', - md: '28rem', - lg: '32rem', - xl: '36rem', - '2xl': '42rem', - '3xl': '48rem', - '4xl': '56rem', - '5xl': '64rem', - '6xl': '72rem', - '9/10': '90%', - full: '100%', - ...breakpoints(theme('screens')) - }), - minHeight: { - 0: '0', - full: '100%', - screen: '100vh' - }, - minWidth: { - 0: '0', - full: '100%' - }, objectPosition: { bottom: 'bottom', center: 'center', @@ -448,44 +347,8 @@ module.exports = { 11: '11', 12: '12' }, - padding: theme => ({ - ...theme('spacing'), - '1/2': '50%', - '1/3': '33.333333%', - '2/3': '66.666667%', - '1/4': '25%', - '2/4': '50%', - '3/4': '75%', - '1/5': '20%', - '2/5': '40%', - '3/5': '60%', - '4/5': '80%', - '1/6': '16.666667%', - '2/6': '33.333333%', - '3/6': '50%', - '4/6': '66.666667%', - '5/6': '83.333333%', - '1/12': '8.333333%', - '2/12': '16.666667%', - '3/12': '25%', - '4/12': '33.333333%', - '5/12': '41.666667%', - '6/12': '50%', - '7/12': '58.333333%', - '8/12': '66.666667%', - '9/12': '75%', - '10/12': '83.333333%', - '11/12': '91.666667%', - 3.5: '52px', - 4.5: '72px', - full: '100%' - }), placeholderColor: theme => theme('colors'), placeholderOpacity: theme => theme('opacity'), - space: (theme, { negative }) => ({ - ...theme('spacing'), - ...negative(theme('spacing')) - }), stroke: { current: 'currentColor', 'accent-orange': '#FF5B00', @@ -499,39 +362,6 @@ module.exports = { }, textColor: theme => theme('colors'), textOpacity: theme => theme('opacity'), - width: theme => ({ - auto: 'auto', - fit: 'fit-content', - ...theme('spacing'), - '1/2': '50%', - '1/3': '33.333333%', - '2/3': '66.666667%', - '1/4': '25%', - '2/4': '50%', - '3/4': '75%', - '1/5': '20%', - '2/5': '40%', - '3/5': '60%', - '4/5': '80%', - '1/6': '16.666667%', - '2/6': '33.333333%', - '3/6': '50%', - '4/6': '66.666667%', - '5/6': '83.333333%', - '1/12': '8.333333%', - '2/12': '16.666667%', - '3/12': '25%', - '4/12': '33.333333%', - '5/12': '41.666667%', - '6/12': '50%', - '7/12': '58.333333%', - '8/12': '66.666667%', - '9/12': '75%', - '10/12': '83.333333%', - '11/12': '91.666667%', - full: '100%', - screen: '100vw' - }), zIndex: { auto: 'auto', 0: '0', @@ -541,7 +371,6 @@ module.exports = { 40: '40', 50: '50' }, - gap: theme => theme('spacing'), gridTemplateColumns: { none: 'none', 1: 'repeat(1, minmax(0, 1fr))', @@ -756,6 +585,79 @@ module.exports = { animationTimingFunction: 'cubic-bezier(0,0,0.2,1)' } } + }, + extend: { + spacing: { + 13: '3.25rem', + 15: '3.75rem', + 18: '4.5rem', + 25: '6.25rem', + 26.5: '6.625rem', + 29: '7.25rem', + 31.25: '7.8125rem', + 35: '8.75rem', + 60.5: '15.125rem', + 63: '15.75rem' + }, + height: theme => theme('spacing'), + minHeight: theme => theme('height'), + maxHeight: theme => theme('height'), + width: theme => theme('spacing'), + minWidth: theme => theme('width'), + maxWidth: (theme, { breakpoints }) => ({ + ...theme('width'), + xs: '20rem', + sm: '24rem', + md: '28rem', + lg: '32rem', + xl: '36rem', + '2xl': '42rem', + '3xl': '48rem', + '4xl': '56rem', + '5xl': '64rem', + '6xl': '72rem', + '9/10': '90%', + ...breakpoints(theme('screens')) + }), + margin: (theme, { negative }) => ({ + ...theme('spacing'), + ...negative(theme('spacing')) + }), + padding: theme => ({ + ...theme('spacing'), + '1/2': '50%', + '1/3': '33.333333%', + '2/3': '66.666667%', + '1/4': '25%', + '2/4': '50%', + '3/4': '75%', + '1/5': '20%', + '2/5': '40%', + '3/5': '60%', + '4/5': '80%', + '1/6': '16.666667%', + '2/6': '33.333333%', + '3/6': '50%', + '4/6': '66.666667%', + '5/6': '83.333333%', + '1/12': '8.333333%', + '2/12': '16.666667%', + '3/12': '25%', + '4/12': '33.333333%', + '5/12': '41.666667%', + '6/12': '50%', + '7/12': '58.333333%', + '8/12': '66.666667%', + '9/12': '75%', + '10/12': '83.333333%', + '11/12': '91.666667%', + full: '100%' + }), + space: (theme, { negative }) => ({ + ...theme('spacing'), + ...negative(theme('spacing')) + }), + gap: theme => theme('spacing') } }, variants: { diff --git a/yarn.lock b/yarn.lock index 27228df5c..647df9d8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11099,6 +11099,11 @@ use-composed-ref@^1.0.0: dependencies: ts-essentials "^2.0.3" +use-custom-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/use-custom-compare/-/use-custom-compare-1.4.0.tgz#3b750ae0487d3c8e8c38ed2da8b633450981e4e9" + integrity sha512-3VyBy9aUZgOdHKMVvECAW+o5SXVF1Ly9plXIoXZNBKnkiBz395Z0unTg6Q3SdLWp2RZHyyoG9GEo3HP3obz5Cw== + use-debounce@7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-7.0.1.tgz#380e6191cc13ad29f8e2149a12b5c37cc2891190"