From 432e828c3e7257c3bfe8a7b1c26d8d02daf464d9 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 +- .../animate-height/animate-height.web.tsx | 2 +- .../animations/approver-animation.web.tsx | 2 +- .../components/approver-actions.web.tsx | 2 +- .../components/approver-advanced.web.tsx | 2 +- .../components/approver-section.web.tsx | 2 +- .../collectibles/collectibles.shared.tsx | 319 +++++++ .../components/collectibles/index.native.ts | 6 + .../src/components/collectibles/index.web.ts | 8 + .../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/hooks/use-pressable.web.tsx | 67 ++ ...s-children.tsx => has-children.shared.tsx} | 0 packages/ui/web.ts | 4 +- pnpm-lock.yaml | 841 +++++++----------- 59 files changed, 1764 insertions(+), 546 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-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 rename packages/ui/src/utils/{has-children.tsx => has-children.shared.tsx} (100%) 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 f557ee93..237e171c 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 6a5341d9..c4b29bff 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 a02c26ff..8e0c4f1b 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 3abf4a56..57f2c456 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 00000000..6d5a01ee --- /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 00000000..6204fa1d --- /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 00000000..0c6a9ad3 --- /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 00000000..f083d0d5 --- /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 CollectiblesWidgetProps { + balance?: React.ReactNode; + children: React.ReactNode; + header?: React.ReactNode; + sheetRef?: React.RefObject; +} + +export function CollectiblesWidgetLayout({ children, header }: CollectiblesWidgetProps) { + 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 00000000..02510219 --- /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 00000000..9f4a0ef3 --- /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 3066a8d2..197874af 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 00000000..8f68cf5c --- /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 d9c7f273..acea83d1 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 d3199790..098cfdc2 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 bc79f8c1..399ac18f 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 11291bab..4ba72990 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 7377d59f..dd07fcc2 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 5729fe75..0af23028 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 c62bb903..4f8492e2 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 3856b9ec..92fb0f7c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -44,6 +44,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", @@ -60,12 +61,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.0", @@ -88,6 +92,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/animate-height/animate-height.web.tsx b/packages/ui/src/components/animate-height/animate-height.web.tsx index ae281507..8df0746d 100644 --- a/packages/ui/src/components/animate-height/animate-height.web.tsx +++ b/packages/ui/src/components/animate-height/animate-height.web.tsx @@ -2,7 +2,7 @@ import { useRef, useState } from 'react'; import { motion } from 'framer-motion'; -import { HasChildren } from '../../utils/has-children'; +import { HasChildren } from '../../utils/has-children.shared'; import { useElementHeightListener } from '../../utils/use-element-height-listener.web'; // https://github.com/framer/motion/discussions/1884#discussioncomment-5861808 diff --git a/packages/ui/src/components/approver/animations/approver-animation.web.tsx b/packages/ui/src/components/approver/animations/approver-animation.web.tsx index 0c86e510..3c379f0e 100644 --- a/packages/ui/src/components/approver/animations/approver-animation.web.tsx +++ b/packages/ui/src/components/approver/animations/approver-animation.web.tsx @@ -1,6 +1,6 @@ import { motion, stagger, useAnimate } from 'framer-motion'; import { css } from 'leather-styles/css'; -import { HasChildren } from 'src/utils/has-children'; +import { HasChildren } from 'src/utils/has-children.shared'; import { useOnMount } from 'src/utils/use-on-mount'; const animationSelector = '& > *:not(.skip-animation)'; diff --git a/packages/ui/src/components/approver/components/approver-actions.web.tsx b/packages/ui/src/components/approver/components/approver-actions.web.tsx index 75f3ee96..eb36bd33 100644 --- a/packages/ui/src/components/approver/components/approver-actions.web.tsx +++ b/packages/ui/src/components/approver/components/approver-actions.web.tsx @@ -1,7 +1,7 @@ import { css } from 'leather-styles/css'; import { Flex, styled } from 'leather-styles/jsx'; -import type { HasChildren } from '../../../utils/has-children'; +import type { HasChildren } from '../../../utils/has-children.shared'; import { ApproverActionAnimation, ApproverActionsAnimationContainer, diff --git a/packages/ui/src/components/approver/components/approver-advanced.web.tsx b/packages/ui/src/components/approver/components/approver-advanced.web.tsx index bdb70d76..274e3761 100644 --- a/packages/ui/src/components/approver/components/approver-advanced.web.tsx +++ b/packages/ui/src/components/approver/components/approver-advanced.web.tsx @@ -6,7 +6,7 @@ import { AnimateChangeInHeight } from 'src/components/animate-height/animate-hei import { Button } from 'src/components/button/button.web'; import { Flag } from 'src/components/flag/flag.web'; import { ChevronDownIcon } from 'src/icons/chevron-down-icon.web'; -import { HasChildren } from 'src/utils/has-children'; +import { HasChildren } from 'src/utils/has-children.shared'; import { getScrollParent } from 'src/utils/utils.web'; import { delay } from '@leather.io/utils'; diff --git a/packages/ui/src/components/approver/components/approver-section.web.tsx b/packages/ui/src/components/approver/components/approver-section.web.tsx index 0d699114..efa4e01c 100644 --- a/packages/ui/src/components/approver/components/approver-section.web.tsx +++ b/packages/ui/src/components/approver/components/approver-section.web.tsx @@ -1,5 +1,5 @@ import { styled } from 'leather-styles/jsx'; -import { HasChildren } from 'src/utils/has-children'; +import { HasChildren } from 'src/utils/has-children.shared'; import { useRegisterApproverChild } from '../approver-context.shared'; 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 00000000..7376bbf9 --- /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 00000000..b7b2d41d --- /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 00000000..3b6b4433 --- /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-card-layout.native.tsx b/packages/ui/src/components/collectibles/native/collectible-card-layout.native.tsx new file mode 100644 index 00000000..7eb25500 --- /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 00000000..c7b12497 --- /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 00000000..bd8c9f0f --- /dev/null +++ b/packages/ui/src/components/collectibles/native/collectible-card.native.tsx @@ -0,0 +1,31 @@ +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) { + // TODO: add audio support + // 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 00000000..ee763c9f --- /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 00000000..c8aec4a5 --- /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 00000000..45b21940 --- /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 00000000..772b2f10 --- /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 00000000..c79f6468 --- /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 00000000..665b1dc7 --- /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 ( + +