From 58c3b6929797ef0ae7829f8702e3e90e93f5ac6f Mon Sep 17 00:00:00 2001 From: Pete Watters <2938440+pete-watters@users.noreply.github.com> Date: Tue, 17 Sep 2024 11:11:20 +0100 Subject: [PATCH] feat: add collectibles widget, ref leather-io/issues#222 --- .../add-wallet/add-wallet-list-item.tsx | 2 - apps/mobile/src/components/home/home.tsx | 18 +- .../widgets/accounts/accounts-header.tsx | 2 - .../accounts/accounts-widget.layout.tsx | 1 - .../collectibles/collectibles-header.tsx | 30 ++ .../collectibles-serializer.spec.ts | 60 ++++ .../collectibles/collectibles-serializer.ts | 21 ++ .../collectibles-widget.layout.tsx | 30 ++ .../collectibles/collectibles-widget.tsx | 33 ++ .../components/widgets/collectibles/index.ts | 3 + .../components/balance/token-balance.tsx | 2 - .../src/components/widgets/tokens/index.ts | 2 + .../widgets/tokens/tokens-header.tsx | 2 - .../widgets/tokens/tokens-widget.layout.tsx | 1 - .../widgets/tokens/tokens.mocks.tsx | 8 - apps/mobile/src/components/widgets/widget.tsx | 2 - apps/mobile/src/locales/en/messages.po | 20 +- .../src/locales/pseudo-locale/messages.po | 20 +- packages/ui/native.ts | 1 + packages/ui/package.json | 7 +- .../approver/stories/approver.web.stories.tsx | 2 +- .../collectibles/collectibles.shared.tsx | 319 ++++++++++++++++++ .../components/collectibles/index.native.ts | 6 + .../src/components/collectibles/index.web.ts | 8 + .../native/collectible-audio.native.tsx | 10 + .../native/collectible-card-layout.native.tsx | 14 + .../collectible-card.native.stories.tsx | 64 ++++ .../native/collectible-card.native.tsx | 31 ++ .../native/collectible-html.native.tsx | 15 + .../native/collectible-image.native.tsx | 15 + .../native/collectible-text.native.tsx | 42 +++ .../web/collectible-audio.web.tsx | 18 + .../web/collectible-hover.web.tsx | 58 ++++ .../web/collectible-iframe.web.tsx | 34 ++ .../web/collectible-image.web.tsx | 48 +++ .../web/collectible-item.layout.web.tsx | 119 +++++++ .../web/collectible-other.web.tsx | 23 ++ .../collectible-placeholder.layout.web.tsx | 19 ++ .../web/collectible-text.layout.web.tsx | 31 ++ .../collectibles/web/collectible-text.web.tsx | 15 + .../collectibles/web/iframe.web.tsx | 34 ++ .../web/image-unavailable.web.tsx | 15 + .../components/inscription-image.web.tsx | 11 + .../components/inscription-metadata.web.tsx | 31 ++ .../inscription-preview.layout.web.tsx | 19 ++ .../inscription-text.layout.web.tsx | 31 ++ .../web/inscription-preview-card/index.web.ts | 4 + .../inscription-preview-card.web.tsx | 39 +++ .../collectibles/web/inscription-text.web.tsx | 31 ++ packages/ui/src/components/spinner/index.ts | 2 + .../spinner/loading-spinner.web.tsx | 27 ++ .../components/{ => spinner}/spinner.web.tsx | 0 packages/ui/src/exports.web.ts | 4 +- packages/ui/src/hooks/use-pressable.web.tsx | 67 ++++ packages/ui/src/icons/headset-icon.native.tsx | 17 + packages/ui/src/icons/icon/icon.native.tsx | 2 +- pnpm-lock.yaml | 319 ++++++++++++++++-- 57 files changed, 1739 insertions(+), 70 deletions(-) create mode 100644 apps/mobile/src/components/widgets/collectibles/collectibles-header.tsx create mode 100644 apps/mobile/src/components/widgets/collectibles/collectibles-serializer.spec.ts create mode 100644 apps/mobile/src/components/widgets/collectibles/collectibles-serializer.ts create mode 100644 apps/mobile/src/components/widgets/collectibles/collectibles-widget.layout.tsx create mode 100644 apps/mobile/src/components/widgets/collectibles/collectibles-widget.tsx create mode 100644 apps/mobile/src/components/widgets/collectibles/index.ts create mode 100644 apps/mobile/src/components/widgets/tokens/index.ts create mode 100644 packages/ui/src/components/collectibles/collectibles.shared.tsx create mode 100644 packages/ui/src/components/collectibles/index.native.ts create mode 100644 packages/ui/src/components/collectibles/index.web.ts create mode 100644 packages/ui/src/components/collectibles/native/collectible-audio.native.tsx create mode 100644 packages/ui/src/components/collectibles/native/collectible-card-layout.native.tsx create mode 100644 packages/ui/src/components/collectibles/native/collectible-card.native.stories.tsx create mode 100644 packages/ui/src/components/collectibles/native/collectible-card.native.tsx create mode 100644 packages/ui/src/components/collectibles/native/collectible-html.native.tsx create mode 100644 packages/ui/src/components/collectibles/native/collectible-image.native.tsx create mode 100644 packages/ui/src/components/collectibles/native/collectible-text.native.tsx create mode 100644 packages/ui/src/components/collectibles/web/collectible-audio.web.tsx create mode 100644 packages/ui/src/components/collectibles/web/collectible-hover.web.tsx create mode 100644 packages/ui/src/components/collectibles/web/collectible-iframe.web.tsx create mode 100644 packages/ui/src/components/collectibles/web/collectible-image.web.tsx create mode 100644 packages/ui/src/components/collectibles/web/collectible-item.layout.web.tsx create mode 100644 packages/ui/src/components/collectibles/web/collectible-other.web.tsx create mode 100644 packages/ui/src/components/collectibles/web/collectible-placeholder.layout.web.tsx create mode 100644 packages/ui/src/components/collectibles/web/collectible-text.layout.web.tsx create mode 100644 packages/ui/src/components/collectibles/web/collectible-text.web.tsx create mode 100644 packages/ui/src/components/collectibles/web/iframe.web.tsx create mode 100644 packages/ui/src/components/collectibles/web/image-unavailable.web.tsx create mode 100644 packages/ui/src/components/collectibles/web/inscription-preview-card/components/inscription-image.web.tsx create mode 100644 packages/ui/src/components/collectibles/web/inscription-preview-card/components/inscription-metadata.web.tsx create mode 100644 packages/ui/src/components/collectibles/web/inscription-preview-card/components/inscription-preview.layout.web.tsx create mode 100644 packages/ui/src/components/collectibles/web/inscription-preview-card/components/inscription-text.layout.web.tsx create mode 100644 packages/ui/src/components/collectibles/web/inscription-preview-card/index.web.ts create mode 100644 packages/ui/src/components/collectibles/web/inscription-preview-card/inscription-preview-card.web.tsx create mode 100644 packages/ui/src/components/collectibles/web/inscription-text.web.tsx create mode 100644 packages/ui/src/components/spinner/index.ts create mode 100644 packages/ui/src/components/spinner/loading-spinner.web.tsx rename packages/ui/src/components/{ => spinner}/spinner.web.tsx (100%) create mode 100644 packages/ui/src/hooks/use-pressable.web.tsx create mode 100644 packages/ui/src/icons/headset-icon.native.tsx diff --git a/apps/mobile/src/components/add-wallet/add-wallet-list-item.tsx b/apps/mobile/src/components/add-wallet/add-wallet-list-item.tsx index f557ee93f..237e171c3 100644 --- a/apps/mobile/src/components/add-wallet/add-wallet-list-item.tsx +++ b/apps/mobile/src/components/add-wallet/add-wallet-list-item.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - import { Box, Text, TouchableOpacity } from '@leather.io/ui/native'; interface AddWalletListItemProps { diff --git a/apps/mobile/src/components/home/home.tsx b/apps/mobile/src/components/home/home.tsx index 6a5341d92..c4b29bff2 100644 --- a/apps/mobile/src/components/home/home.tsx +++ b/apps/mobile/src/components/home/home.tsx @@ -1,12 +1,22 @@ import { AccountsWidget } from '@/components/widgets/accounts/accounts-widget'; -import { TokensWidget } from '@/components/widgets/tokens/tokens-widget'; -import { getMockTokens, mockTotalBalance } from '@/components/widgets/tokens/tokens.mocks'; +import { + CollectiblesWidget, + mockCollectibles, + serializeCollectibles, +} from '@/components/widgets/collectibles'; +import { TokensWidget, getMockTokens } from '@/components/widgets/tokens'; import { useAccounts } from '@/store/accounts/accounts.read'; import { useWallets } from '@/store/wallets/wallets.read'; import { useLingui } from '@lingui/react'; import { HomeLayout } from './home.layout'; +const mockTotalBalance = { + totalUsdBalance: '$126.74', + totalBtcBalance: '0.00215005', + totalStxBalance: '0.0024', +}; + export function Home() { useLingui(); const wallets = useWallets(); @@ -16,6 +26,10 @@ export function Home() { + ); } diff --git a/apps/mobile/src/components/widgets/accounts/accounts-header.tsx b/apps/mobile/src/components/widgets/accounts/accounts-header.tsx index a02c26ffa..8e0c4f1b1 100644 --- a/apps/mobile/src/components/widgets/accounts/accounts-header.tsx +++ b/apps/mobile/src/components/widgets/accounts/accounts-header.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - import { AccountSelectorSheet } from '@/features/account-selector-sheet'; import { t } from '@lingui/macro'; diff --git a/apps/mobile/src/components/widgets/accounts/accounts-widget.layout.tsx b/apps/mobile/src/components/widgets/accounts/accounts-widget.layout.tsx index 3abf4a56c..57f2c4567 100644 --- a/apps/mobile/src/components/widgets/accounts/accounts-widget.layout.tsx +++ b/apps/mobile/src/components/widgets/accounts/accounts-widget.layout.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { ScrollView } from 'react-native-gesture-handler'; import { useTheme } from '@shopify/restyle'; diff --git a/apps/mobile/src/components/widgets/collectibles/collectibles-header.tsx b/apps/mobile/src/components/widgets/collectibles/collectibles-header.tsx new file mode 100644 index 000000000..6d5a01ee6 --- /dev/null +++ b/apps/mobile/src/components/widgets/collectibles/collectibles-header.tsx @@ -0,0 +1,30 @@ +import { t } from '@lingui/macro'; + +import { Box, ChevronRightIcon, Chip, SheetRef, Text } from '@leather.io/ui/native'; + +import { FiatBalance } from '../components/balance/fiat-balance'; + +interface CollectiblesHeaderProps { + collectibleCount: number; + sheetRef: React.RefObject; + totalBalance: string; +} + +function CollectiblesHeaderText() { + return {t`My collectibles`}; +} + +export function CollectiblesHeader({ collectibleCount, totalBalance }: CollectiblesHeaderProps) { + const hasCollectibles = collectibleCount > 0; + if (!hasCollectibles) return ; + return ( + + + + + + + + + ); +} diff --git a/apps/mobile/src/components/widgets/collectibles/collectibles-serializer.spec.ts b/apps/mobile/src/components/widgets/collectibles/collectibles-serializer.spec.ts new file mode 100644 index 000000000..6204fa1db --- /dev/null +++ b/apps/mobile/src/components/widgets/collectibles/collectibles-serializer.spec.ts @@ -0,0 +1,60 @@ +import { mockCollectibles } from '@leather.io/ui/native'; + +import { serializeCollectibles } from './collectibles-serializer'; + +describe('serializeCollectibles', () => { + it('should correctly serialize collectibles', () => { + const serializedCollectibles = serializeCollectibles(mockCollectibles); + + expect(serializedCollectibles).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + title: expect.any(String), + imageUrl: expect.any(String), + collection: expect.any(String), + type: expect.stringMatching(/^(ordinal|stacks)$/), + }), + ]) + ); + }); + + it('should handle empty input', () => { + const serializedCollectibles = serializeCollectibles([]); + expect(serializedCollectibles).toEqual([]); + }); + + it('should correctly serialize Ordinals', () => { + const ordinals = mockCollectibles.filter(c => 'name' in c && 'mimeType' in c); + const serializedOrdinals = serializeCollectibles(ordinals); + + serializedOrdinals.forEach(ordinal => { + expect(ordinal).toEqual( + expect.objectContaining({ + type: 'inscription', + id: expect.any(String), + title: expect.any(String), + imageUrl: expect.any(String), + collection: expect.any(String), + }) + ); + }); + }); + + it('should correctly serialize StacksNfts', () => { + const stacksNfts = mockCollectibles.filter(c => 'metadata' in c); + const serializedStacksNfts = serializeCollectibles(stacksNfts); + + serializedStacksNfts.forEach(nft => { + expect(nft).toEqual( + expect.objectContaining({ + type: 'stacks', + id: expect.any(String), + title: expect.any(String), + imageUrl: expect.any(String), + collection: expect.any(String), + }) + ); + }); + }); +}); diff --git a/apps/mobile/src/components/widgets/collectibles/collectibles-serializer.ts b/apps/mobile/src/components/widgets/collectibles/collectibles-serializer.ts new file mode 100644 index 000000000..0c6a9ad32 --- /dev/null +++ b/apps/mobile/src/components/widgets/collectibles/collectibles-serializer.ts @@ -0,0 +1,21 @@ +import { type Collectible, type CollectibleCardProps } from '@leather.io/ui/native'; + +export function serializeCollectibles(collectibles: Collectible[]): CollectibleCardProps[] { + return collectibles.map(collectible => { + const isOrdinal = 'name' in collectible && 'mimeType' in collectible; + if (isOrdinal) { + return { + type: 'inscription', + name: collectible.title, + src: collectible.src, + mimeType: collectible.mimeType, + }; + } + return { + type: 'stacks', + name: collectible.metadata.name, + src: collectible.metadata.cached_image, + mimeType: null, + }; + }); +} diff --git a/apps/mobile/src/components/widgets/collectibles/collectibles-widget.layout.tsx b/apps/mobile/src/components/widgets/collectibles/collectibles-widget.layout.tsx new file mode 100644 index 000000000..cdf50d3a1 --- /dev/null +++ b/apps/mobile/src/components/widgets/collectibles/collectibles-widget.layout.tsx @@ -0,0 +1,30 @@ +import { ScrollView } from 'react-native-gesture-handler'; + +import { useTheme } from '@shopify/restyle'; + +import { Box, SheetRef, Theme } from '@leather.io/ui/native'; + +import { Widget } from '../widget'; + +interface CollectiblesWidgetLayoutProps { + balance?: React.ReactNode; + children: React.ReactNode; + header?: React.ReactNode; + sheetRef?: React.RefObject; +} + +export function CollectiblesWidgetLayout({ children, header }: CollectiblesWidgetLayoutProps) { + const theme = useTheme(); + return ( + + {header} + + {children} + + + ); +} diff --git a/apps/mobile/src/components/widgets/collectibles/collectibles-widget.tsx b/apps/mobile/src/components/widgets/collectibles/collectibles-widget.tsx new file mode 100644 index 000000000..025102197 --- /dev/null +++ b/apps/mobile/src/components/widgets/collectibles/collectibles-widget.tsx @@ -0,0 +1,33 @@ +import React, { useRef } from 'react'; + +import { CollectibleCard, CollectibleCardProps, SheetRef } from '@leather.io/ui/native'; + +import { TokenBalance } from '../components/balance/token-balance'; +import { CollectiblesHeader } from './collectibles-header'; +import { CollectiblesWidgetLayout } from './collectibles-widget.layout'; + +interface CollectiblesWidgetProps { + collectibles: CollectibleCardProps[]; + totalBalance: string; +} + +export function CollectiblesWidget({ collectibles, totalBalance }: CollectiblesWidgetProps) { + const sheetRef = useRef(null); + + return ( + + } + balance={collectibles.length > 0 && } + > + {collectibles.map((collectible: CollectibleCardProps, index) => ( + + ))} + + ); +} diff --git a/apps/mobile/src/components/widgets/collectibles/index.ts b/apps/mobile/src/components/widgets/collectibles/index.ts new file mode 100644 index 000000000..9f4a0ef31 --- /dev/null +++ b/apps/mobile/src/components/widgets/collectibles/index.ts @@ -0,0 +1,3 @@ +export { mockCollectibles } from '@leather.io/ui/native'; +export { CollectiblesWidget } from './collectibles-widget'; +export { serializeCollectibles } from './collectibles-serializer'; diff --git a/apps/mobile/src/components/widgets/components/balance/token-balance.tsx b/apps/mobile/src/components/widgets/components/balance/token-balance.tsx index 3066a8d22..197874aff 100644 --- a/apps/mobile/src/components/widgets/components/balance/token-balance.tsx +++ b/apps/mobile/src/components/widgets/components/balance/token-balance.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - import { t } from '@lingui/macro'; import { BulletSeparator, Text } from '@leather.io/ui/native'; diff --git a/apps/mobile/src/components/widgets/tokens/index.ts b/apps/mobile/src/components/widgets/tokens/index.ts new file mode 100644 index 000000000..8f68cf5ca --- /dev/null +++ b/apps/mobile/src/components/widgets/tokens/index.ts @@ -0,0 +1,2 @@ +export { TokensWidget } from './tokens-widget'; +export { getMockTokens } from './tokens.mocks'; diff --git a/apps/mobile/src/components/widgets/tokens/tokens-header.tsx b/apps/mobile/src/components/widgets/tokens/tokens-header.tsx index d9c7f273e..acea83d17 100644 --- a/apps/mobile/src/components/widgets/tokens/tokens-header.tsx +++ b/apps/mobile/src/components/widgets/tokens/tokens-header.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - import { t } from '@lingui/macro'; import { Box, ChevronRightIcon, Chip, SheetRef, Text } from '@leather.io/ui/native'; diff --git a/apps/mobile/src/components/widgets/tokens/tokens-widget.layout.tsx b/apps/mobile/src/components/widgets/tokens/tokens-widget.layout.tsx index d3199790a..098cfdc2a 100644 --- a/apps/mobile/src/components/widgets/tokens/tokens-widget.layout.tsx +++ b/apps/mobile/src/components/widgets/tokens/tokens-widget.layout.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { ScrollView } from 'react-native-gesture-handler'; import { useTheme } from '@shopify/restyle'; diff --git a/apps/mobile/src/components/widgets/tokens/tokens.mocks.tsx b/apps/mobile/src/components/widgets/tokens/tokens.mocks.tsx index bc79f8c16..399ac18f7 100644 --- a/apps/mobile/src/components/widgets/tokens/tokens.mocks.tsx +++ b/apps/mobile/src/components/widgets/tokens/tokens.mocks.tsx @@ -4,14 +4,6 @@ import BigNumber from 'bignumber.js'; import { Money } from '@leather.io/models'; import { BtcAvatarIcon, StxAvatarIcon } from '@leather.io/ui/native'; -// provided by useTotalBalance hook in extension/src/app/common/hooks/balance/use-total-balance.tsx - -export const mockTotalBalance = { - totalUsdBalance: '$126.74', - totalBtcBalance: '0.00215005', - totalStxBalance: '0.0024', -}; - export interface Token { availableBalance: Record; formattedBalance: { isAbbreviated: boolean; value: string }; diff --git a/apps/mobile/src/components/widgets/widget.tsx b/apps/mobile/src/components/widgets/widget.tsx index 11291babb..4ba72990c 100644 --- a/apps/mobile/src/components/widgets/widget.tsx +++ b/apps/mobile/src/components/widgets/widget.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - import { Box } from '@leather.io/ui/native'; interface WidgetProps { diff --git a/apps/mobile/src/locales/en/messages.po b/apps/mobile/src/locales/en/messages.po index 7377d59fe..dd07fcc2e 100644 --- a/apps/mobile/src/locales/en/messages.po +++ b/apps/mobile/src/locales/en/messages.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"POT-Creation-Date: 2024-09-23 11:25-0400\n" +"POT-Creation-Date: 2024-09-25 06:31+0100\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -194,11 +194,11 @@ msgstr "Awaiting verification" msgid "BIP39 passphrase" msgstr "BIP39 passphrase" -#: src/components/widgets/tokens/tokens.mocks.tsx:50 +#: src/components/widgets/tokens/tokens.mocks.tsx:42 msgid "Bitcoin" msgstr "Bitcoin" -#: src/components/widgets/tokens/tokens.mocks.tsx:27 +#: src/components/widgets/tokens/tokens.mocks.tsx:19 msgid "Bitcoin blockchain" msgstr "Bitcoin blockchain" @@ -490,7 +490,7 @@ msgid "Lock app" msgstr "Lock app" #: src/components/widgets/components/balance/fiat-balance.tsx:45 -#: src/components/widgets/components/balance/token-balance.tsx:21 +#: src/components/widgets/components/balance/token-balance.tsx:19 msgid "locked" msgstr "locked" @@ -507,11 +507,15 @@ msgstr "More options" msgid "Must use physical device for Push Notifications" msgstr "Must use physical device for Push Notifications" -#: src/components/widgets/accounts/accounts-header.tsx:14 +#: src/components/widgets/accounts/accounts-header.tsx:12 msgid "My accounts" msgstr "My accounts" -#: src/components/widgets/tokens/tokens-header.tsx:18 +#: src/components/widgets/collectibles/collectibles-header.tsx:14 +msgid "My collectibles" +msgstr "My collectibles" + +#: src/components/widgets/tokens/tokens-header.tsx:16 msgid "My tokens" msgstr "My tokens" @@ -704,11 +708,11 @@ msgstr "Something is wrong!" msgid "Something went wrong" msgstr "Something went wrong" -#: src/components/widgets/tokens/tokens.mocks.tsx:113 +#: src/components/widgets/tokens/tokens.mocks.tsx:105 msgid "Stacks" msgstr "Stacks" -#: src/components/widgets/tokens/tokens.mocks.tsx:59 +#: src/components/widgets/tokens/tokens.mocks.tsx:51 msgid "Stacks blockchain" msgstr "Stacks blockchain" diff --git a/apps/mobile/src/locales/pseudo-locale/messages.po b/apps/mobile/src/locales/pseudo-locale/messages.po index 5729fe75a..0af230288 100644 --- a/apps/mobile/src/locales/pseudo-locale/messages.po +++ b/apps/mobile/src/locales/pseudo-locale/messages.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"POT-Creation-Date: 2024-09-23 11:25-0400\n" +"POT-Creation-Date: 2024-09-25 06:31+0100\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -194,11 +194,11 @@ msgstr "" msgid "BIP39 passphrase" msgstr "" -#: src/components/widgets/tokens/tokens.mocks.tsx:50 +#: src/components/widgets/tokens/tokens.mocks.tsx:42 msgid "Bitcoin" msgstr "" -#: src/components/widgets/tokens/tokens.mocks.tsx:27 +#: src/components/widgets/tokens/tokens.mocks.tsx:19 msgid "Bitcoin blockchain" msgstr "" @@ -490,7 +490,7 @@ msgid "Lock app" msgstr "" #: src/components/widgets/components/balance/fiat-balance.tsx:45 -#: src/components/widgets/components/balance/token-balance.tsx:21 +#: src/components/widgets/components/balance/token-balance.tsx:19 msgid "locked" msgstr "" @@ -507,11 +507,15 @@ msgstr "" msgid "Must use physical device for Push Notifications" msgstr "" -#: src/components/widgets/accounts/accounts-header.tsx:14 +#: src/components/widgets/accounts/accounts-header.tsx:12 msgid "My accounts" msgstr "" -#: src/components/widgets/tokens/tokens-header.tsx:18 +#: src/components/widgets/collectibles/collectibles-header.tsx:14 +msgid "My collectibles" +msgstr "" + +#: src/components/widgets/tokens/tokens-header.tsx:16 msgid "My tokens" msgstr "" @@ -704,11 +708,11 @@ msgstr "" msgid "Something went wrong" msgstr "" -#: src/components/widgets/tokens/tokens.mocks.tsx:113 +#: src/components/widgets/tokens/tokens.mocks.tsx:105 msgid "Stacks" msgstr "" -#: src/components/widgets/tokens/tokens.mocks.tsx:59 +#: src/components/widgets/tokens/tokens.mocks.tsx:51 msgid "Stacks blockchain" msgstr "" diff --git a/packages/ui/native.ts b/packages/ui/native.ts index c62bb9039..4f8492e28 100644 --- a/packages/ui/native.ts +++ b/packages/ui/native.ts @@ -38,3 +38,4 @@ export { export { SheetHeader } from './src/components/sheet/components/sheet-header.native'; export { RadioButton } from './src/components/radio-button/radio-button.native'; export { Switch } from './src/components/switch/switch.native'; +export * from './src/components/collectibles/index.native'; diff --git a/packages/ui/package.json b/packages/ui/package.json index e43620a63..e47d1c8d1 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -43,6 +43,7 @@ "@rnx-kit/metro-config": "1.3.14", "@rnx-kit/metro-resolver-symlinks": "0.1.35", "@shopify/restyle": "2.4.2", + "dompurify": "3.1.4", "expo": "51.0.26", "expo-asset": "10.0.6", "expo-blur": "13.0.2", @@ -59,12 +60,15 @@ "react-native": "0.74.1", "react-native-reanimated": "3.10.1", "react-native-safe-area-context": "4.10.1", - "react-native-svg": "15.2.0" + "react-native-svg": "15.2.0", + "react-native-webview": "13.8.6", + "use-events": "1.4.2" }, "devDependencies": { "@babel/core": "7.24.6", "@babel/runtime": "7.25.0", "@leather.io/eslint-config": "workspace:*", + "@leather.io/models": "workspace:*", "@leather.io/panda-preset": "workspace:*", "@microsoft/api-extractor": "7.47.6", "@pandacss/dev": "0.46.1", @@ -87,6 +91,7 @@ "@storybook/test": "8.3.2", "@storybook/theming": "8.3.2", "@svgr/webpack": "8.1.0", + "@types/dompurify": "3.0.5", "@types/react": "18.2.79", "@types/react-dom": "18.2.25", "babel-preset-expo": "11.0.6", diff --git a/packages/ui/src/components/approver/stories/approver.web.stories.tsx b/packages/ui/src/components/approver/stories/approver.web.stories.tsx index a3848da33..8edb4c3fc 100644 --- a/packages/ui/src/components/approver/stories/approver.web.stories.tsx +++ b/packages/ui/src/components/approver/stories/approver.web.stories.tsx @@ -1,7 +1,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { Box, Circle, Flex } from 'leather-styles/jsx'; -import { ZapIcon } from 'src/icons/index.web'; +import { ZapIcon } from '../../../icons/zap-icon.web'; import { Button } from '../../button/button.web'; import { Flag } from '../../flag/flag.web'; import { ItemLayout } from '../../item-layout/item-layout.web'; diff --git a/packages/ui/src/components/collectibles/collectibles.shared.tsx b/packages/ui/src/components/collectibles/collectibles.shared.tsx new file mode 100644 index 000000000..7376bbf95 --- /dev/null +++ b/packages/ui/src/components/collectibles/collectibles.shared.tsx @@ -0,0 +1,319 @@ +/* eslint-disable */ + +export interface Ordinal { + id: string; + number: number; + output: string; + txid: string; + offset: string; + address: string; + preview: string; + title: string; + genesisBlockHeight: number; + genesisBlockHash: string; + genesisTimestamp: number; + value: string; + mimeType: string; + name: string; + src: string; +} + +const mockOrdinals: Ordinal[] = [ + { + id: 'a494e48bf7120c959239e8c544bc821ca4fb5a46e5fff79938943d434f252949i0', + number: 74703951, + output: '0', + txid: 'a494e48bf7120c959239e8c544bc821ca4fb5a46e5fff79938943d434f252949', + offset: '0', + address: 'bc1pwz9n62p9dhjpqcpdmfcrewdnz3nk8jcved242vd2lj9fgvtvwnwscvdyre', + preview: + 'https://ordinals.hiro.so/inscription/a494e48bf7120c959239e8c544bc821ca4fb5a46e5fff79938943d434f252949i0', + title: 'Inscription 74703951', + genesisBlockHeight: 857719, + genesisBlockHash: '00000000000000000002bc6789fc6742da4958d003d3abff740687a863613a46', + genesisTimestamp: 1724219117, + value: '546', + mimeType: 'html', + name: 'inscription', + src: 'https://ordinals.com/preview/a494e48bf7120c959239e8c544bc821ca4fb5a46e5fff79938943d434f252949i0', + }, + { + id: '335209b72c452f52199ae09e8ce586a451ce452c73326f01f958d8aa8417e062i0', + number: 73858867, + output: '0', + txid: '335209b72c452f52199ae09e8ce586a451ce452c73326f01f958d8aa8417e062', + offset: '0', + address: 'bc1pwz9n62p9dhjpqcpdmfcrewdnz3nk8jcved242vd2lj9fgvtvwnwscvdyre', + preview: + 'https://ordinals.hiro.so/inscription/335209b72c452f52199ae09e8ce586a451ce452c73326f01f958d8aa8417e062i0', + title: 'Inscription 73858867', + genesisBlockHeight: 855754, + genesisBlockHash: '000000000000000000021972c2000a8d347dbac1a2540112fadf81219b188796', + genesisTimestamp: 1723027746, + value: '546', + mimeType: 'text', + name: 'inscription', + src: 'https://bis-ord-content.fra1.cdn.digitaloceanspaces.com/ordinals/335209b72c452f52199ae09e8ce586a451ce452c73326f01f958d8aa8417e062i0', + }, + { + id: 'cd27e71f955e021dd0840aa0544067fc92c3608009f2191a405f9f4910712b78i0', + number: 55549412, + output: '0', + txid: 'cd27e71f955e021dd0840aa0544067fc92c3608009f2191a405f9f4910712b78', + offset: '0', + address: 'bc1pwz9n62p9dhjpqcpdmfcrewdnz3nk8jcved242vd2lj9fgvtvwnwscvdyre', + preview: + 'https://ordinals.hiro.so/inscription/cd27e71f955e021dd0840aa0544067fc92c3608009f2191a405f9f4910712b78i0', + title: 'Inscription 55549412', + genesisBlockHeight: 825933, + genesisBlockHash: '00000000000000000002f95317315f9d00b2299eb3499b0f499a707506ad6735', + genesisTimestamp: 1705356588, + value: '600', + mimeType: 'image', + name: 'inscription', + src: 'https://bis-ord-content.fra1.cdn.digitaloceanspaces.com/ordinals/cd27e71f955e021dd0840aa0544067fc92c3608009f2191a405f9f4910712b78i0', + }, + { + id: 'e59434da4436cbdcdcf6b7b31fb734d43b304e981a2e3b69092bd6ca83108009i1286', + number: 64484111, + output: '1287', + txid: 'e59434da4436cbdcdcf6b7b31fb734d43b304e981a2e3b69092bd6ca83108009', + offset: '0', + address: 'bc1pwz9n62p9dhjpqcpdmfcrewdnz3nk8jcved242vd2lj9fgvtvwnwscvdyre', + preview: + 'https://ordinals.hiro.so/inscription/e59434da4436cbdcdcf6b7b31fb734d43b304e981a2e3b69092bd6ca83108009i1286', + title: 'Inscription 64484111', + genesisBlockHeight: 834795, + genesisBlockHash: '00000000000000000000a3f2c9b0459df8eda99abca3c83f0e94a2a224badaba', + genesisTimestamp: 1710504509, + value: '546', + mimeType: 'gltf', + name: 'inscription', + src: 'https://ordinals.com/preview/e59434da4436cbdcdcf6b7b31fb734d43b304e981a2e3b69092bd6ca83108009i1286', + }, +]; + +export interface StacksNft { + token_uri: string; + metadata: { + sip: number; + name: string; + description: string; + image: string; + cached_image: string; + cached_thumbnail_image: string; + attributes?: Array<{ + trait_type: string; + value: string; + display_type?: string; + }>; + properties?: { + collection?: string; + collectionId?: string; + [key: string]: any; + }; + }; +} + +const mockStacksNfts: StacksNft[] = [ + { + token_uri: 'ipfs://ipfs/QmQ63rXC9F7GyLYoYNyqxeiYvbBUvmHmL36PrfYNxpw5sT/90.json', + metadata: { + sip: 16, + name: 'BlockSurvey #90', + description: 'Worlds First Software License as an NFT', + image: 'ipfs://QmZXkLMrN2ejpzGv1wk4HgcuL6XbyLVieW3Zm9wyAoDk18/90.png', + cached_image: + 'https://assets.hiro.so/api/mainnet/token-metadata-api/SPNWZ5V2TPWGQGVDR6T7B6RQ4XMGZ4PXTEE0VQ0S.blocksurvey/90.png', + cached_thumbnail_image: + 'https://assets.hiro.so/api/mainnet/token-metadata-api/SPNWZ5V2TPWGQGVDR6T7B6RQ4XMGZ4PXTEE0VQ0S.blocksurvey/90-thumb.png', + attributes: [ + { + trait_type: 'NightBackground', + value: 'MidnightMoss', + display_type: '', + }, + { + trait_type: 'NightLogo', + value: 'AtomicTangerine', + display_type: '', + }, + { + trait_type: 'SignatureWhite', + value: 'SignatureWhite', + display_type: '', + }, + ], + }, + }, + { + token_uri: 'ipfs://ipfs/QmWRQyaVxUjHGjBUoZqGcNjL37VN99jcFwmoB1wZnpjJEg/', + metadata: { + sip: 16, + name: 'Portals-ALEX-Anniversary-Series', + description: 'From proof-of-concept to bringing Bitcoin value to the multi-chain.\n', + image: 'ipfs://ipfs/QmUgHdbTy5LYi4wijf9YJgGs89SCAKNbXascvzuFgAsMB9', + cached_image: + 'https://assets.hiro.so/api/mainnet/token-metadata-api/SP3N7Y3K01Y24G9JC1XXA13RQXXCY721WAVBMMD38.alex-anniversary-series/1452.png', + cached_thumbnail_image: + 'https://assets.hiro.so/api/mainnet/token-metadata-api/SP3N7Y3K01Y24G9JC1XXA13RQXXCY721WAVBMMD38.alex-anniversary-series/1452-thumb.png', + properties: { + collection: 'ALEX Anniversary Series', + }, + }, + }, + { + token_uri: 'ipfs://QmYTX3u58v2Ero2drdtqhL6rPE5qnv51EJZ6WSu3LKqUBN/crashpunks-5559.json', + metadata: { + sip: 16, + name: 'Crash Punk 5559', + description: '', + image: 'ipfs://Qmb84UcaMr1MUwNbYBnXWHM3kEaDcYrKuPWwyRLVTNKELC/5559.png', + cached_image: + 'https://assets.hiro.so/api/mainnet/token-metadata-api/SP3QSAJQ4EA8WXEDSRRKMZZ29NH91VZ6C5X88FGZQ.crashpunks-v2/5559.png', + cached_thumbnail_image: + 'https://assets.hiro.so/api/mainnet/token-metadata-api/SP3QSAJQ4EA8WXEDSRRKMZZ29NH91VZ6C5X88FGZQ.crashpunks-v2/5559-thumb.png', + attributes: [ + { + trait_type: 'Background', + value: 'Blue', + display_type: 'string', + }, + { + trait_type: 'Outfit Back', + value: 'Stacks Hoodie Back', + display_type: 'string', + }, + { + trait_type: 'Neck', + value: 'Neck Metal', + display_type: 'string', + }, + { + trait_type: 'Outfit Front', + value: 'Stacks Hoodie', + display_type: 'string', + }, + { + trait_type: 'Head', + value: 'Head Tan', + display_type: 'string', + }, + { + trait_type: 'Piercings', + value: 'Piercings', + display_type: 'string', + }, + { + trait_type: 'Mouth', + value: 'Lips Bare', + display_type: 'string', + }, + { + trait_type: 'Eyes', + value: 'RoboEyes Blue', + display_type: 'string', + }, + { + trait_type: 'Hair', + value: 'Bob Silver', + display_type: 'string', + }, + ], + properties: { + collection: 'Crash Punks', + collectionId: 'grace.btc/crash_punks', + dna: '5c2f54662bb494b5e4ebc195070d9ce624c5a849', + total_supply: '9216', + external_url: + 'https://thisisnumberone.com/nfts/SP3QSAJQ4EA8WXEDSRRKMZZ29NH91VZ6C5X88FGZQ.crashpunks-v2/5559', + }, + }, + }, + { + token_uri: 'ipfs://ipfs/QmZYoSr94MKdarScJZSsyBYxBgMJchUQqqbtLxxxR86wZN/', + metadata: { + sip: 16, + name: 'WORRY - NFT - MUSIC', + description: + 'Musical NFT Collection \nWorry is a self-reflective song done by Brythreesixty also known as 3hunnatheartist. Worry is an emotional state of being anxious and troubled over actual or potential problems. The greatest weapon is positivity. Welcome to my Bullish state of Mind. This collection is a gift to the community. Enjoy \n\nhttps://gamma.io/3hunnatheartist.btc\nhttps://gamma.io/brythreesixty\n\nhttps://twitter.com/brythreesixty\nhttps://twitter.com/3hunnatheartist\n\nhttps://discord.gg/hRqeVRFG', + image: '', + cached_image: '', + cached_thumbnail_image: '', + }, + }, + { + token_uri: 'ipfs://QmbMdASbHZb5XHizZJsFPL9hdmuDgekUHH9Ya1DnuSxfHj/1547.json', + metadata: { + sip: 16, + name: 'StacksMFers #1547', + description: 'Just a bunch of mfers on stacks', + image: 'ipfs://QmUL7yELAmF1wnbqt6yaNLmCVbBa7BSbSNXYKijpku2r45/1547.png', + cached_image: + 'https://assets.hiro.so/api/mainnet/token-metadata-api/SP2N3BAG4GBF8NHRPH6AY4YYH1SP6NK5TGCY7RDFA.stacks-mfers/1547.png', + cached_thumbnail_image: + 'https://assets.hiro.so/api/mainnet/token-metadata-api/SP2N3BAG4GBF8NHRPH6AY4YYH1SP6NK5TGCY7RDFA.stacks-mfers/1547-thumb.png', + attributes: [ + { + trait_type: 'BG', + value: 'Pixels', + display_type: '', + }, + { + trait_type: 'Type', + value: 'Plain', + display_type: '', + }, + { + trait_type: 'Eyes', + value: 'Greenglasses', + display_type: '', + }, + { + trait_type: 'Mouth', + value: 'Smile', + display_type: '', + }, + { + trait_type: 'Beard', + value: 'None', + display_type: '', + }, + { + trait_type: 'Shirt', + value: 'GreenHoodie', + display_type: '', + }, + { + trait_type: 'Accessory', + value: 'None', + display_type: '', + }, + { + trait_type: 'LongHair', + value: 'LongBlue', + display_type: '', + }, + { + trait_type: 'Hat under', + value: 'RedBandana', + display_type: '', + }, + { + trait_type: 'Headphones', + value: 'Red', + display_type: '', + }, + { + trait_type: 'Smoke', + value: 'None', + display_type: '', + }, + ], + }, + }, +]; + +export type Collectible = Ordinal | StacksNft; + +export const mockCollectibles: (Ordinal | StacksNft)[] = [...mockOrdinals, ...mockStacksNfts]; diff --git a/packages/ui/src/components/collectibles/index.native.ts b/packages/ui/src/components/collectibles/index.native.ts new file mode 100644 index 000000000..b7b2d41da --- /dev/null +++ b/packages/ui/src/components/collectibles/index.native.ts @@ -0,0 +1,6 @@ +export * from './native/collectible-html.native'; +export * from './native/collectible-image.native'; +export * from './native/collectible-text.native'; +export * from './native/collectible-card.native'; +export * from './native/collectible-card-layout.native'; +export * from './collectibles.shared'; diff --git a/packages/ui/src/components/collectibles/index.web.ts b/packages/ui/src/components/collectibles/index.web.ts new file mode 100644 index 000000000..3b6b44330 --- /dev/null +++ b/packages/ui/src/components/collectibles/index.web.ts @@ -0,0 +1,8 @@ +export * from './web/collectible-audio.web'; +export * from './web/collectible-image.web'; +export * from './web/collectible-text.web'; +export * from './web/collectible-other.web'; +export * from './web/collectible-iframe.web'; +export * from './web/image-unavailable.web'; +export * from './web/inscription-preview-card/index.web'; +export * from './web/collectible-item.layout.web'; diff --git a/packages/ui/src/components/collectibles/native/collectible-audio.native.tsx b/packages/ui/src/components/collectibles/native/collectible-audio.native.tsx new file mode 100644 index 000000000..05e1e7f76 --- /dev/null +++ b/packages/ui/src/components/collectibles/native/collectible-audio.native.tsx @@ -0,0 +1,10 @@ +import { HeadsetIcon } from '../../../icons/headset-icon.native'; +import { CollectibleCardLayout } from './collectible-card-layout.native'; + +export function CollectibleAudio() { + return ( + + + + ); +} diff --git a/packages/ui/src/components/collectibles/native/collectible-card-layout.native.tsx b/packages/ui/src/components/collectibles/native/collectible-card-layout.native.tsx new file mode 100644 index 000000000..7eb255006 --- /dev/null +++ b/packages/ui/src/components/collectibles/native/collectible-card-layout.native.tsx @@ -0,0 +1,14 @@ +import { BaseTheme, BoxProps } from '@shopify/restyle'; + +import { Box, Theme } from '../../../../native'; +import { HasChildren } from '../../../utils/has-children.shared'; + +type CollectibleCardLayoutProps = BoxProps & HasChildren; + +export function CollectibleCardLayout({ children, ...props }: CollectibleCardLayoutProps) { + return ( + + {children} + + ); +} diff --git a/packages/ui/src/components/collectibles/native/collectible-card.native.stories.tsx b/packages/ui/src/components/collectibles/native/collectible-card.native.stories.tsx new file mode 100644 index 000000000..c7b124978 --- /dev/null +++ b/packages/ui/src/components/collectibles/native/collectible-card.native.stories.tsx @@ -0,0 +1,64 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { CollectibleCard } from './collectible-card.native'; + +const meta: Meta = { + title: 'Collectibles/CollectibleCard', + component: CollectibleCard, + tags: ['autodocs'], + argTypes: {}, + parameters: {}, + decorators: [Story => ], +}; + +export default meta; + +export const OrdinalHtmlCollectibleCardStory = { + args: { + mimeType: 'html', + name: 'Inscription 74703951', + src: 'https://ordinals.com/preview/a494e48bf7120c959239e8c544bc821ca4fb5a46e5fff79938943d434f252949i0', + type: 'inscription', + }, + argTypes: {}, +} satisfies StoryObj; + +export const OrdinalTextCollectibleCardStory = { + args: { + mimeType: 'text', + name: 'Inscription 73858867', + src: 'https://bis-ord-content.fra1.cdn.digitaloceanspaces.com/ordinals/335209b72c452f52199ae09e8ce586a451ce452c73326f01f958d8aa8417e062i0', + type: 'inscription', + }, + argTypes: {}, +} satisfies StoryObj; + +export const OrdinalImageCollectibleCardStory = { + args: { + mimeType: 'image', + name: 'Inscription 55549412', + src: 'https://bis-ord-content.fra1.cdn.digitaloceanspaces.com/ordinals/cd27e71f955e021dd0840aa0544067fc92c3608009f2191a405f9f4910712b78i0', + type: 'inscription', + }, + argTypes: {}, +} satisfies StoryObj; + +export const OrdinalGltfCollectibleCardStory = { + args: { + mimeType: 'gltf', + name: 'Inscription 64484111', + src: 'https://ordinals.com/preview/e59434da4436cbdcdcf6b7b31fb734d43b304e981a2e3b69092bd6ca83108009i1286', + type: 'inscription', + }, + argTypes: {}, +} satisfies StoryObj; + +export const StxNftCollectibleCardStory = { + args: { + mimeType: null, + name: 'BlockSurvey #90', + src: 'https://assets.hiro.so/api/mainnet/token-metadata-api/SPNWZ5V2TPWGQGVDR6T7B6RQ4XMGZ4PXTEE0VQ0S.blocksurvey/90.png', + type: 'stacks', + }, + argTypes: {}, +} satisfies StoryObj; diff --git a/packages/ui/src/components/collectibles/native/collectible-card.native.tsx b/packages/ui/src/components/collectibles/native/collectible-card.native.tsx new file mode 100644 index 000000000..abae1b73f --- /dev/null +++ b/packages/ui/src/components/collectibles/native/collectible-card.native.tsx @@ -0,0 +1,31 @@ +import { CollectibleAudio } from './collectible-audio.native'; +import { CollectibleHtml } from './collectible-html.native'; +import { CollectibleImage } from './collectible-image.native'; +import { CollectibleText } from './collectible-text.native'; + +export interface CollectibleCardProps { + name: string; + type: 'inscription' | 'stacks'; + src: string; + mimeType?: string | null; +} + +export function CollectibleCard({ name, type, src, mimeType }: CollectibleCardProps) { + const isOrdinal = type === 'inscription'; + + if (isOrdinal) { + switch (mimeType) { + case 'audio': + return ; + case 'text': + return ; + case 'html': + case 'gltf': + case 'svg': + case 'video': + return ; + } + } + + return ; +} diff --git a/packages/ui/src/components/collectibles/native/collectible-html.native.tsx b/packages/ui/src/components/collectibles/native/collectible-html.native.tsx new file mode 100644 index 000000000..ee763c9fa --- /dev/null +++ b/packages/ui/src/components/collectibles/native/collectible-html.native.tsx @@ -0,0 +1,15 @@ +import { WebView } from 'react-native-webview'; + +import { CollectibleCardLayout } from './collectible-card-layout.native'; + +interface CollectibleHtmlProps { + src: string; +} + +export function CollectibleHtml({ src }: CollectibleHtmlProps) { + return ( + + + + ); +} diff --git a/packages/ui/src/components/collectibles/native/collectible-image.native.tsx b/packages/ui/src/components/collectibles/native/collectible-image.native.tsx new file mode 100644 index 000000000..c8aec4a5e --- /dev/null +++ b/packages/ui/src/components/collectibles/native/collectible-image.native.tsx @@ -0,0 +1,15 @@ +import { Image } from 'react-native'; + +import { Box } from '../../../../native'; + +interface CollectibleImageProps { + alt: string; + source: string; +} +export function CollectibleImage({ alt, source }: CollectibleImageProps) { + return ( + + {alt} + + ); +} diff --git a/packages/ui/src/components/collectibles/native/collectible-text.native.tsx b/packages/ui/src/components/collectibles/native/collectible-text.native.tsx new file mode 100644 index 000000000..45b21940c --- /dev/null +++ b/packages/ui/src/components/collectibles/native/collectible-text.native.tsx @@ -0,0 +1,42 @@ +import { useEffect, useState } from 'react'; + +// import { sanitize } from 'dompurify'; +import { Text } from '../../../../native'; +import { CollectibleCardLayout } from './collectible-card-layout.native'; + +interface CollectibleTextProps { + src: string; +} + +export function CollectibleText({ src }: CollectibleTextProps) { + const [content, setContent] = useState(null); + + useEffect(() => { + const fetchContent = async () => { + try { + const response = await fetch(src); + if (!response.ok) { + throw new Error('Network response was not ok'); + } + const data = await response.json(); + setContent(JSON.stringify(data, null, 2)); + } catch (err) {} + }; + + void fetchContent(); + }, [src]); + + return ( + + + {/* FIXME + - implement alternative for dompurify in native + - I tried using jsdom as a polyfill but then hit an error Can't resolve 'vm' in jsdom + - maybe we should write our own sanitizer? + */} + {/* {content ? sanitize(content) : ''} */} + {content} + + + ); +} diff --git a/packages/ui/src/components/collectibles/web/collectible-audio.web.tsx b/packages/ui/src/components/collectibles/web/collectible-audio.web.tsx new file mode 100644 index 000000000..772b2f10e --- /dev/null +++ b/packages/ui/src/components/collectibles/web/collectible-audio.web.tsx @@ -0,0 +1,18 @@ +import { ReactNode } from 'react'; + +import { HeadsetIcon } from '../../../icons/headset-icon.web'; +import { CollectibleItemLayout, CollectibleItemLayoutProps } from './collectible-item.layout.web'; +import { CollectiblePlaceholderLayout } from './collectible-placeholder.layout.web'; + +interface CollectibleAudioProps extends Omit { + icon: ReactNode; +} +export function CollectibleAudio({ icon, ...props }: CollectibleAudioProps) { + return ( + + + + + + ); +} diff --git a/packages/ui/src/components/collectibles/web/collectible-hover.web.tsx b/packages/ui/src/components/collectibles/web/collectible-hover.web.tsx new file mode 100644 index 000000000..c79f64683 --- /dev/null +++ b/packages/ui/src/components/collectibles/web/collectible-hover.web.tsx @@ -0,0 +1,58 @@ +import { ReactNode } from 'react'; + +import { Box } from 'leather-styles/jsx'; + +import { ExternalLinkIcon } from '../../../icons/external-link-icon.web'; +import { IconButton } from '../../icon-button/icon-button.web'; + +interface CollectibleHoverProps { + collectibleTypeIcon?: ReactNode; + isHovered: boolean; + onClickCallToAction?(): void; +} +export function CollectibleHover({ + collectibleTypeIcon, + isHovered, + onClickCallToAction, +}: CollectibleHoverProps) { + return ( + + + {collectibleTypeIcon} + + {onClickCallToAction && ( + + } + onClick={e => { + e.stopPropagation(); + onClickCallToAction(); + }} + /> + + )} + + ); +} diff --git a/packages/ui/src/components/collectibles/web/collectible-iframe.web.tsx b/packages/ui/src/components/collectibles/web/collectible-iframe.web.tsx new file mode 100644 index 000000000..665b1dc70 --- /dev/null +++ b/packages/ui/src/components/collectibles/web/collectible-iframe.web.tsx @@ -0,0 +1,34 @@ +import { ReactNode, useState } from 'react'; + +import { CollectibleItemLayout, CollectibleItemLayoutProps } from './collectible-item.layout.web'; +import { Iframe } from './iframe.web'; +import { ImageUnavailable } from './image-unavailable.web'; + +interface CollectibleIframeProps extends Omit { + icon: ReactNode; + src: string; +} +export function CollectibleIframe({ icon, src, ...props }: CollectibleIframeProps) { + const [isError, setIsError] = useState(false); + + if (isError) + return ( + + + + ); + + return ( + +