From 31b60cc8c2cf8e2a02bb3231d5fdfcd11aad47fb Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Tue, 17 Jun 2025 20:45:08 +0530 Subject: [PATCH] [TOOL-4827] Move Nebula out of dashboard --- apps/dashboard/.env.example | 11 - apps/dashboard/package.json | 1 - .../dashboard/src/@/components/Responsive.tsx | 2 +- .../src/@/components/blocks/client-only.tsx | 42 ++ .../src/@/components/color-mode-toggle.tsx | 2 +- .../components/ui/Spinner/Spinner.module.css | 1 - apps/dashboard/src/@/components/ui/button.tsx | 13 +- .../src/@/components/ui/skeleton.tsx | 3 +- apps/dashboard/src/@/constants/public-envs.ts | 5 - apps/dashboard/src/@/constants/server-envs.ts | 10 - .../(chain)/[chain_id]/(chainPage)/layout.tsx | 48 +- .../[contractAddress]/shared-layout.tsx | 31 +- .../components/server}/icons/NebulaIcon.tsx | 0 .../(chain)/components/server/products.ts | 2 +- .../app/(app)/(dashboard)/support/page.tsx | 4 +- .../tools/unixtime-converter/page.tsx | 2 +- .../_components/DeployedContractsPage.tsx | 2 +- .../src/app/(app)/login/LoginPage.tsx | 2 +- .../(app)/team/[team_slug]/(team)/layout.tsx | 2 +- .../nft-collection-info-fieldset.tsx | 2 +- .../token/token-info/token-info-fieldset.tsx | 2 +- .../[project_slug]/(sidebar)/assets/page.tsx | 2 +- .../components/ProjectFTUX/ProjectFTUX.tsx | 2 +- .../components/ProjectSidebarLayout.tsx | 2 +- .../account-abstraction/factories/page.tsx | 2 +- .../explorer/components/engine-explorer.tsx | 2 +- .../[project_slug]/(sidebar)/layout.tsx | 2 +- .../components/FloatingChat/FloatingChat.tsx | 279 --------- .../FloatingChat/FloatingChatContent.tsx | 346 ----------- .../(app)/components/FloatingChat/actions.ts | 54 -- apps/dashboard/src/app/nebula-app/layout.tsx | 62 -- .../src/app/nebula-app/nebula-global.css | 4 - apps/dashboard/src/app/nebula-app/readme.md | 10 - .../ClientOnly/ClientOnly.module.css | 14 - .../src/components/CustomChat/ChatBar.tsx | 136 +++++ .../CustomChat/CustomChatButton.tsx | 0 .../CustomChat/CustomChatContent.tsx | 15 +- .../components/CustomChat/CustomChats.tsx | 2 +- .../CustomChat}/Reasoning.tsx | 0 .../src/components/CustomChat/types.ts | 36 ++ .../explore/contract-card/index.tsx | 2 +- apps/dashboard/src/middleware.ts | 40 -- apps/dashboard/src/stories/stubs.ts | 29 - apps/nebula/.env.example | 2 + apps/nebula/.eslintignore | 13 + apps/nebula/.eslintrc.js | 114 ++++ apps/nebula/.storybook/main.ts | 28 + apps/nebula/.storybook/preview.tsx | 109 ++++ apps/nebula/LICENSE.md | 201 ++++++ apps/nebula/README.md | 1 + apps/nebula/biome.json | 15 + apps/nebula/components.json | 17 + apps/nebula/knip.json | 8 + apps/nebula/lucide-react.d.ts | 3 + apps/nebula/next-sitemap.config.js | 38 ++ apps/nebula/next.config.ts | 70 +++ apps/nebula/package.json | 3 + apps/nebula/postcss.config.js | 6 + .../src/@/components/blocks/ChainIcon.tsx | 44 ++ .../@/components/blocks/FormFieldSetup.tsx | 44 ++ .../src/@/components/blocks/Img.stories.tsx | 112 ++++ apps/nebula/src/@/components/blocks/Img.tsx | 88 +++ .../markdown-renderer.stories.tsx | 164 +++++ .../MarkdownRenderer/markdown-renderer.tsx | 225 +++++++ .../blocks/MultiNetworkSelector.stories.tsx | 49 ++ .../@/components/blocks/NetworkSelectors.tsx | 256 ++++++++ .../blocks/SingleNetworkSelector.stories.tsx | 53 ++ .../src/@/components/blocks/auto-connect.tsx | 16 + .../blocks/buttons/MismatchButton.tsx | 571 ++++++++++++++++++ .../buttons/TransactionButton.stories.tsx | 210 +++++++ .../blocks/buttons/TransactionButton.tsx | 175 ++++++ .../src/@/components/blocks/client-only.tsx} | 18 +- .../blocks/multi-select.stories.tsx | 72 +++ .../src/@/components/blocks/multi-select.tsx | 375 ++++++++++++ .../blocks/select-with-search.stories.tsx | 62 ++ .../components/blocks/select-with-search.tsx | 221 +++++++ .../blocks/skeletons/GenericLoadingPage.tsx | 21 + .../@/components/blocks/wallet-address.tsx | 212 +++++++ .../src/@/components/color-mode-toggle.tsx | 32 + .../src/@/components/ui/CopyTextButton.tsx | 68 +++ .../src/@/components/ui/DynamicHeight.tsx | 59 ++ apps/nebula/src/@/components/ui/NavLink.tsx | 50 ++ .../ui/ScrollShadow/ScrollShadow.module.css | 36 ++ .../ui/ScrollShadow/ScrollShadow.tsx | 151 +++++ .../components/ui/Spinner/Spinner.module.css | 38 ++ .../src/@/components/ui/Spinner/Spinner.tsx | 21 + apps/nebula/src/@/components/ui/avatar.tsx | 50 ++ apps/nebula/src/@/components/ui/badge.tsx | 39 ++ .../src/@/components/ui/button.stories.tsx | 64 ++ apps/nebula/src/@/components/ui/button.tsx | 84 +++ .../components/ui/code/CodeBlockContainer.tsx | 61 ++ .../src/@/components/ui/code/RenderCode.tsx | 29 + .../src/@/components/ui/code/code.client.tsx | 71 +++ .../src/@/components/ui/code/code.stories.tsx | 108 ++++ .../src/@/components/ui/code/getCodeHtml.tsx | 52 ++ .../ui/code/plaintext-code.stories.tsx | 45 ++ .../@/components/ui/code/plaintext-code.tsx | 29 + .../src/@/components/ui/decimal-input.tsx | 39 ++ apps/nebula/src/@/components/ui/dialog.tsx | 140 +++++ apps/nebula/src/@/components/ui/form.tsx | 203 +++++++ .../nebula/src/@/components/ui/hover-card.tsx | 29 + .../@/components/ui/image-upload-button.tsx | 0 .../src/@/components/ui/inline-code.tsx | 17 + apps/nebula/src/@/components/ui/input.tsx | 25 + apps/nebula/src/@/components/ui/label.tsx | 26 + apps/nebula/src/@/components/ui/popover.tsx | 31 + .../src/@/components/ui/select.stories.tsx | 79 +++ apps/nebula/src/@/components/ui/select.tsx | 162 +++++ apps/nebula/src/@/components/ui/separator.tsx | 31 + apps/nebula/src/@/components/ui/sheet.tsx | 143 +++++ apps/nebula/src/@/components/ui/skeleton.tsx | 50 ++ apps/nebula/src/@/components/ui/sonner.tsx | 31 + apps/nebula/src/@/components/ui/switch.tsx | 29 + .../src/@/components/ui/table.stories.tsx | 143 +++++ apps/nebula/src/@/components/ui/table.tsx | 156 +++++ apps/nebula/src/@/components/ui/tabs.tsx | 210 +++++++ .../src/@/components/ui/text-shimmer.tsx | 23 + apps/nebula/src/@/components/ui/textarea.tsx | 48 ++ apps/nebula/src/@/components/ui/tooltip.tsx | 70 +++ .../src/@/config/nebula-aa.ts} | 0 .../src/@/config/sdk-component-theme.ts | 43 ++ .../src/@/constants/cookies.ts} | 0 apps/nebula/src/@/constants/env-utils.ts | 3 + apps/nebula/src/@/constants/local-node.ts | 6 + .../src/@/constants/nebula-client.ts} | 4 +- apps/nebula/src/@/constants/public-envs.ts | 14 + apps/nebula/src/@/constants/server-envs.ts | 29 + apps/nebula/src/@/constants/urls.ts | 24 + .../src/@/data/eth-sanctioned-addresses.ts | 150 +++++ apps/nebula/src/@/hooks/chains.ts | 119 ++++ apps/nebula/src/@/hooks/use-clipboard.tsx | 36 ++ apps/nebula/src/@/icons/NebulaIcon.tsx | 24 + apps/nebula/src/@/lib/DashboardRouter.tsx | 119 ++++ apps/nebula/src/@/lib/reactive.ts | 55 ++ .../@/lib/resolveSchemeWithErrorHandler.ts | 21 + .../src/@/lib/useIsomorphicLayoutEffect.ts | 4 + apps/nebula/src/@/lib/useShowMore.ts | 37 ++ apps/nebula/src/@/lib/utils.ts | 6 + apps/nebula/src/@/storybook/stubs.ts | 28 + apps/nebula/src/@/storybook/utils.tsx | 37 ++ apps/nebula/src/@/types/chain.ts | 26 + .../src/@/utils}/authToken.ts | 4 +- apps/nebula/src/@/utils/fetchChain.ts | 23 + .../src/@/utils}/isAuthTokenValid.ts | 0 .../src/@/utils}/isLoggedIntoNebula.ts | 0 apps/nebula/src/@/utils/loginRedirect.ts | 9 + apps/nebula/src/@/utils/map-chains.ts | 20 + apps/nebula/src/@/utils/nebula-chains.ts | 32 + apps/nebula/src/@/utils/parse-error.tsx | 111 ++++ apps/nebula/src/@/utils/vercel-utils.ts | 12 + .../src/@/utils/verifyTurnstileToken.ts | 71 +++ .../src/app}/(app)/api/chat.ts | 4 +- .../src/app}/(app)/api/feedback.ts | 2 +- .../src/app/(app)/api}/fetchWithAuthToken.ts | 0 .../src/app}/(app)/api/session.ts | 2 +- .../src/app}/(app)/api/types.ts | 7 + .../src/app}/(app)/chat/[session_id]/page.tsx | 8 +- .../chat/history/ChatHistoryPage.stories.tsx | 2 +- .../(app)/chat/history/ChatHistoryPage.tsx | 0 .../src/app}/(app)/chat/history/page.tsx | 4 +- .../src/app}/(app)/chat/page.tsx | 6 +- .../AssetsSection/AssetsSection.stories.tsx | 2 +- .../AssetsSection/AssetsSection.tsx | 7 +- .../src/app}/(app)/components/ChatBar.tsx | 4 +- .../app}/(app)/components/ChatPageContent.tsx | 4 +- .../components/ChatPageLayout.stories.tsx | 2 +- .../app}/(app)/components/ChatPageLayout.tsx | 6 +- .../src/app}/(app)/components/ChatSidebar.tsx | 6 +- .../app}/(app)/components/ChatSidebarLink.tsx | 0 .../app}/(app)/components/Chatbar.stories.tsx | 2 +- .../app}/(app)/components/Chats.stories.tsx | 4 +- .../src/app}/(app)/components/Chats.tsx | 17 +- .../EmptyStateChatPageContent.stories.tsx | 0 .../components/EmptyStateChatPageContent.tsx | 4 +- .../ExecuteTransactionCard.stories.tsx | 6 +- .../components/ExecuteTransactionCard.tsx | 10 +- .../app}/(app)/components/MessageActions.tsx | 0 .../(app)/components/NebulaConnectButton.tsx | 6 +- .../src/app}/(app)/components/NebulaImage.tsx | 0 .../app}/(app)/components/NebulaMobileNav.tsx | 0 .../Reasoning/Reasoning.stories.tsx | 2 +- .../(app)/components/Reasoning/Reasoning.tsx | 65 ++ .../components/Swap/SwapCards.stories.tsx | 2 +- .../app}/(app)/components/Swap/SwapCards.tsx | 12 +- .../src/app}/(app)/components/Swap/common.tsx | 45 +- .../TransactionsSection.stories.tsx | 2 +- .../TransactionsSection.tsx | 6 +- .../src/app}/(app)/data/examplePrompts.ts | 4 +- .../app}/(app)/hooks/useNewChatPageLink.ts | 0 .../hooks/useSessionsWithLocalOverrides.ts | 0 .../src/app}/(app)/layout.tsx | 4 +- .../src/app}/(app)/page.tsx | 6 +- .../src/app}/(app)/stores.ts | 0 .../src/app}/(app)/utils/getChainIds.ts | 2 +- apps/nebula/src/app/layout.tsx | 43 +- .../app}/login/NebulaConnectEmbedLogin.tsx | 10 +- .../src/app}/login/NebulaLoginPage.tsx | 2 +- .../src/app}/login/auth-actions.ts | 12 +- .../src/app}/login/page.tsx | 4 +- .../src/app}/move-funds/connect-button.tsx | 8 +- .../src/app/move-funds/dashboard-client.ts | 42 ++ .../src/app}/move-funds/move-funds.tsx | 16 +- .../src/app}/move-funds/page.tsx | 2 +- .../page.tsx => nebula/src/app/not-found.tsx} | 11 +- .../src/app}/opengraph-image.png | Bin apps/nebula/src/app/page.tsx | 3 - .../src/app}/providers.tsx | 13 +- apps/nebula/src/global.css | 214 +++++++ apps/nebula/tailwind.config.js | 124 ++++ apps/nebula/tsconfig.json | 28 +- apps/nebula/vercel.json | 5 + .../components/ui/Spinner/Spinner.module.css | 1 - .../components/ui/Spinner/Spinner.module.css | 1 - pnpm-lock.yaml | 3 - 214 files changed, 8324 insertions(+), 1166 deletions(-) create mode 100644 apps/dashboard/src/@/components/blocks/client-only.tsx rename apps/dashboard/src/app/{nebula-app/(app) => (app)/(dashboard)/(chain)/components/server}/icons/NebulaIcon.tsx (100%) delete mode 100644 apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChat.tsx delete mode 100644 apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChatContent.tsx delete mode 100644 apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/actions.ts delete mode 100644 apps/dashboard/src/app/nebula-app/layout.tsx delete mode 100644 apps/dashboard/src/app/nebula-app/nebula-global.css delete mode 100644 apps/dashboard/src/app/nebula-app/readme.md delete mode 100644 apps/dashboard/src/components/ClientOnly/ClientOnly.module.css create mode 100644 apps/dashboard/src/components/CustomChat/ChatBar.tsx rename apps/dashboard/src/{app/nebula-app/(app) => }/components/CustomChat/CustomChatButton.tsx (100%) rename apps/dashboard/src/{app/nebula-app/(app) => }/components/CustomChat/CustomChatContent.tsx (96%) rename apps/dashboard/src/{app/nebula-app/(app) => }/components/CustomChat/CustomChats.tsx (99%) rename apps/dashboard/src/{app/nebula-app/(app)/components/Reasoning => components/CustomChat}/Reasoning.tsx (100%) create mode 100644 apps/dashboard/src/components/CustomChat/types.ts create mode 100644 apps/nebula/.env.example create mode 100644 apps/nebula/.eslintignore create mode 100644 apps/nebula/.eslintrc.js create mode 100644 apps/nebula/.storybook/main.ts create mode 100644 apps/nebula/.storybook/preview.tsx create mode 100644 apps/nebula/LICENSE.md create mode 100644 apps/nebula/README.md create mode 100644 apps/nebula/biome.json create mode 100644 apps/nebula/components.json create mode 100644 apps/nebula/knip.json create mode 100644 apps/nebula/lucide-react.d.ts create mode 100644 apps/nebula/next-sitemap.config.js create mode 100644 apps/nebula/next.config.ts create mode 100644 apps/nebula/postcss.config.js create mode 100644 apps/nebula/src/@/components/blocks/ChainIcon.tsx create mode 100644 apps/nebula/src/@/components/blocks/FormFieldSetup.tsx create mode 100644 apps/nebula/src/@/components/blocks/Img.stories.tsx create mode 100644 apps/nebula/src/@/components/blocks/Img.tsx create mode 100644 apps/nebula/src/@/components/blocks/MarkdownRenderer/markdown-renderer.stories.tsx create mode 100644 apps/nebula/src/@/components/blocks/MarkdownRenderer/markdown-renderer.tsx create mode 100644 apps/nebula/src/@/components/blocks/MultiNetworkSelector.stories.tsx create mode 100644 apps/nebula/src/@/components/blocks/NetworkSelectors.tsx create mode 100644 apps/nebula/src/@/components/blocks/SingleNetworkSelector.stories.tsx create mode 100644 apps/nebula/src/@/components/blocks/auto-connect.tsx create mode 100644 apps/nebula/src/@/components/blocks/buttons/MismatchButton.tsx create mode 100644 apps/nebula/src/@/components/blocks/buttons/TransactionButton.stories.tsx create mode 100644 apps/nebula/src/@/components/blocks/buttons/TransactionButton.tsx rename apps/{dashboard/src/components/ClientOnly/ClientOnly.tsx => nebula/src/@/components/blocks/client-only.tsx} (58%) create mode 100644 apps/nebula/src/@/components/blocks/multi-select.stories.tsx create mode 100644 apps/nebula/src/@/components/blocks/multi-select.tsx create mode 100644 apps/nebula/src/@/components/blocks/select-with-search.stories.tsx create mode 100644 apps/nebula/src/@/components/blocks/select-with-search.tsx create mode 100644 apps/nebula/src/@/components/blocks/skeletons/GenericLoadingPage.tsx create mode 100644 apps/nebula/src/@/components/blocks/wallet-address.tsx create mode 100644 apps/nebula/src/@/components/color-mode-toggle.tsx create mode 100644 apps/nebula/src/@/components/ui/CopyTextButton.tsx create mode 100644 apps/nebula/src/@/components/ui/DynamicHeight.tsx create mode 100644 apps/nebula/src/@/components/ui/NavLink.tsx create mode 100644 apps/nebula/src/@/components/ui/ScrollShadow/ScrollShadow.module.css create mode 100644 apps/nebula/src/@/components/ui/ScrollShadow/ScrollShadow.tsx create mode 100644 apps/nebula/src/@/components/ui/Spinner/Spinner.module.css create mode 100644 apps/nebula/src/@/components/ui/Spinner/Spinner.tsx create mode 100644 apps/nebula/src/@/components/ui/avatar.tsx create mode 100644 apps/nebula/src/@/components/ui/badge.tsx create mode 100644 apps/nebula/src/@/components/ui/button.stories.tsx create mode 100644 apps/nebula/src/@/components/ui/button.tsx create mode 100644 apps/nebula/src/@/components/ui/code/CodeBlockContainer.tsx create mode 100644 apps/nebula/src/@/components/ui/code/RenderCode.tsx create mode 100644 apps/nebula/src/@/components/ui/code/code.client.tsx create mode 100644 apps/nebula/src/@/components/ui/code/code.stories.tsx create mode 100644 apps/nebula/src/@/components/ui/code/getCodeHtml.tsx create mode 100644 apps/nebula/src/@/components/ui/code/plaintext-code.stories.tsx create mode 100644 apps/nebula/src/@/components/ui/code/plaintext-code.tsx create mode 100644 apps/nebula/src/@/components/ui/decimal-input.tsx create mode 100644 apps/nebula/src/@/components/ui/dialog.tsx create mode 100644 apps/nebula/src/@/components/ui/form.tsx create mode 100644 apps/nebula/src/@/components/ui/hover-card.tsx rename apps/{dashboard => nebula}/src/@/components/ui/image-upload-button.tsx (100%) create mode 100644 apps/nebula/src/@/components/ui/inline-code.tsx create mode 100644 apps/nebula/src/@/components/ui/input.tsx create mode 100644 apps/nebula/src/@/components/ui/label.tsx create mode 100644 apps/nebula/src/@/components/ui/popover.tsx create mode 100644 apps/nebula/src/@/components/ui/select.stories.tsx create mode 100644 apps/nebula/src/@/components/ui/select.tsx create mode 100644 apps/nebula/src/@/components/ui/separator.tsx create mode 100644 apps/nebula/src/@/components/ui/sheet.tsx create mode 100644 apps/nebula/src/@/components/ui/skeleton.tsx create mode 100644 apps/nebula/src/@/components/ui/sonner.tsx create mode 100644 apps/nebula/src/@/components/ui/switch.tsx create mode 100644 apps/nebula/src/@/components/ui/table.stories.tsx create mode 100644 apps/nebula/src/@/components/ui/table.tsx create mode 100644 apps/nebula/src/@/components/ui/tabs.tsx create mode 100644 apps/nebula/src/@/components/ui/text-shimmer.tsx create mode 100644 apps/nebula/src/@/components/ui/textarea.tsx create mode 100644 apps/nebula/src/@/components/ui/tooltip.tsx rename apps/{dashboard/src/app/nebula-app/login/account-abstraction.ts => nebula/src/@/config/nebula-aa.ts} (100%) create mode 100644 apps/nebula/src/@/config/sdk-component-theme.ts rename apps/{dashboard/src/app/nebula-app/_utils/constants.ts => nebula/src/@/constants/cookies.ts} (100%) create mode 100644 apps/nebula/src/@/constants/env-utils.ts create mode 100644 apps/nebula/src/@/constants/local-node.ts rename apps/{dashboard/src/app/nebula-app/(app)/utils/nebulaThirdwebClient.ts => nebula/src/@/constants/nebula-client.ts} (93%) create mode 100644 apps/nebula/src/@/constants/public-envs.ts create mode 100644 apps/nebula/src/@/constants/server-envs.ts create mode 100644 apps/nebula/src/@/constants/urls.ts create mode 100644 apps/nebula/src/@/data/eth-sanctioned-addresses.ts create mode 100644 apps/nebula/src/@/hooks/chains.ts create mode 100644 apps/nebula/src/@/hooks/use-clipboard.tsx create mode 100644 apps/nebula/src/@/icons/NebulaIcon.tsx create mode 100644 apps/nebula/src/@/lib/DashboardRouter.tsx create mode 100644 apps/nebula/src/@/lib/reactive.ts create mode 100644 apps/nebula/src/@/lib/resolveSchemeWithErrorHandler.ts create mode 100644 apps/nebula/src/@/lib/useIsomorphicLayoutEffect.ts create mode 100644 apps/nebula/src/@/lib/useShowMore.ts create mode 100644 apps/nebula/src/@/lib/utils.ts create mode 100644 apps/nebula/src/@/storybook/stubs.ts create mode 100644 apps/nebula/src/@/storybook/utils.tsx create mode 100644 apps/nebula/src/@/types/chain.ts rename apps/{dashboard/src/app/nebula-app/_utils => nebula/src/@/utils}/authToken.ts (96%) create mode 100644 apps/nebula/src/@/utils/fetchChain.ts rename apps/{dashboard/src/app/nebula-app/_utils => nebula/src/@/utils}/isAuthTokenValid.ts (100%) rename apps/{dashboard/src/app/nebula-app/_utils => nebula/src/@/utils}/isLoggedIntoNebula.ts (100%) create mode 100644 apps/nebula/src/@/utils/loginRedirect.ts create mode 100644 apps/nebula/src/@/utils/map-chains.ts create mode 100644 apps/nebula/src/@/utils/nebula-chains.ts create mode 100644 apps/nebula/src/@/utils/parse-error.tsx create mode 100644 apps/nebula/src/@/utils/vercel-utils.ts create mode 100644 apps/nebula/src/@/utils/verifyTurnstileToken.ts rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/api/chat.ts (97%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/api/feedback.ts (90%) rename apps/{dashboard/src/utils => nebula/src/app/(app)/api}/fetchWithAuthToken.ts (100%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/api/session.ts (97%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/api/types.ts (93%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/chat/[session_id]/page.tsx (84%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/chat/history/ChatHistoryPage.stories.tsx (97%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/chat/history/ChatHistoryPage.tsx (100%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/chat/history/page.tsx (73%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/chat/page.tsx (77%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/AssetsSection/AssetsSection.stories.tsx (96%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/AssetsSection/AssetsSection.tsx (97%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/ChatBar.tsx (99%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/ChatPageContent.tsx (99%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/ChatPageLayout.stories.tsx (96%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/ChatPageLayout.tsx (82%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/ChatSidebar.tsx (97%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/ChatSidebarLink.tsx (100%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/Chatbar.stories.tsx (98%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/Chats.stories.tsx (97%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/Chats.tsx (97%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/EmptyStateChatPageContent.stories.tsx (100%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/EmptyStateChatPageContent.tsx (97%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/ExecuteTransactionCard.stories.tsx (98%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/ExecuteTransactionCard.tsx (94%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/MessageActions.tsx (100%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/NebulaConnectButton.tsx (93%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/NebulaImage.tsx (100%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/NebulaMobileNav.tsx (100%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/Reasoning/Reasoning.stories.tsx (92%) create mode 100644 apps/nebula/src/app/(app)/components/Reasoning/Reasoning.tsx rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/Swap/SwapCards.stories.tsx (97%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/Swap/SwapCards.tsx (95%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/Swap/common.tsx (85%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/TransactionsSection/TransactionsSection.stories.tsx (98%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/components/TransactionsSection/TransactionsSection.tsx (96%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/data/examplePrompts.ts (97%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/hooks/useNewChatPageLink.ts (100%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/hooks/useSessionsWithLocalOverrides.ts (100%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/layout.tsx (82%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/page.tsx (85%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/stores.ts (100%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/(app)/utils/getChainIds.ts (94%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/login/NebulaConnectEmbedLogin.tsx (94%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/login/NebulaLoginPage.tsx (98%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/login/auth-actions.ts (97%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/login/page.tsx (84%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/move-funds/connect-button.tsx (80%) create mode 100644 apps/nebula/src/app/move-funds/dashboard-client.ts rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/move-funds/move-funds.tsx (98%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/move-funds/page.tsx (95%) rename apps/{dashboard/src/app/nebula-app/[...not-found]/page.tsx => nebula/src/app/not-found.tsx} (85%) rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/opengraph-image.png (100%) delete mode 100644 apps/nebula/src/app/page.tsx rename apps/{dashboard/src/app/nebula-app => nebula/src/app}/providers.tsx (81%) create mode 100644 apps/nebula/src/global.css create mode 100644 apps/nebula/tailwind.config.js create mode 100644 apps/nebula/vercel.json diff --git a/apps/dashboard/.env.example b/apps/dashboard/.env.example index 3f719125270..e6ec6b3b632 100644 --- a/apps/dashboard/.env.example +++ b/apps/dashboard/.env.example @@ -44,9 +44,6 @@ NEXT_PUBLIC_STRIPE_KEY= NEXT_PUBLIC_STRIPE_PAYMENT_METHOD_CFG_ID= -# Needed for contract analytics / blockchain data information -CHAINSAW_API_KEY= - # # Private (server) # @@ -65,17 +62,11 @@ MORALIS_API_KEY= # - not required to build (unless testing wallet NFTs)> SSR_ALCHEMY_KEY= -# beehiiv.com API key (used for newsletter signups) -# - not required to build (unless testing newsletter signups)> -BEEHIIV_API_KEY= # Hubspot Access Token (used for contact us form) # - not required to build (unless testing contact us form)> HUBSPOT_ACCESS_TOKEN= -# Github API Token (used for /open-source) -GITHUB_API_TOKEN="ghp_..." - # Upload server url NEXT_PUBLIC_DASHBOARD_UPLOAD_SERVER="https://storage.thirdweb-preview.com" @@ -100,8 +91,6 @@ REDIS_URL="" ANALYTICS_SERVICE_URL="" -# Required for Nebula Chat -NEXT_PUBLIC_NEBULA_URL="" # required for billing parts of the dashboard (team -> settings -> billing / invoices) STRIPE_SECRET_KEY="" diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 57702d8e216..6cbd148d935 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -62,7 +62,6 @@ "compare-versions": "^6.1.0", "date-fns": "4.1.0", "fast-xml-parser": "^5.2.5", - "fetch-event-stream": "0.1.5", "flat": "^6.0.1", "framer-motion": "12.17.0", "fuse.js": "7.1.0", diff --git a/apps/dashboard/src/@/components/Responsive.tsx b/apps/dashboard/src/@/components/Responsive.tsx index b5a6a88d198..6b78fc76779 100644 --- a/apps/dashboard/src/@/components/Responsive.tsx +++ b/apps/dashboard/src/@/components/Responsive.tsx @@ -1,6 +1,6 @@ "use client"; +import { ClientOnly } from "@/components/blocks/client-only"; import { Suspense } from "react"; -import { ClientOnly } from "../../components/ClientOnly/ClientOnly"; import { useIsMobile } from "../hooks/use-mobile"; export function ResponsiveLayout(props: { diff --git a/apps/dashboard/src/@/components/blocks/client-only.tsx b/apps/dashboard/src/@/components/blocks/client-only.tsx new file mode 100644 index 00000000000..a9640e46b85 --- /dev/null +++ b/apps/dashboard/src/@/components/blocks/client-only.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { type ReactNode, useEffect, useState } from "react"; + +interface ClientOnlyProps { + /** + * Use this to server render a skeleton or loading state + */ + ssr: ReactNode; + className?: string; + children: ReactNode; +} + +export const ClientOnly: React.FC = ({ + children, + ssr, + className, +}) => { + const hasMounted = useIsClientMounted(); + + if (!hasMounted) { + return <> {ssr} ; + } + + return ( +
+ {children} +
+ ); +}; + +function useIsClientMounted() { + const [hasMounted, setHasMounted] = useState(false); + + // eslint-disable-next-line no-restricted-syntax + useEffect(() => { + setHasMounted(true); + }, []); + + return hasMounted; +} diff --git a/apps/dashboard/src/@/components/color-mode-toggle.tsx b/apps/dashboard/src/@/components/color-mode-toggle.tsx index 3b12f09de24..41dc2be94ba 100644 --- a/apps/dashboard/src/@/components/color-mode-toggle.tsx +++ b/apps/dashboard/src/@/components/color-mode-toggle.tsx @@ -1,7 +1,7 @@ "use client"; +import { ClientOnly } from "@/components/blocks/client-only"; import { Button } from "@/components/ui/button"; -import { ClientOnly } from "components/ClientOnly/ClientOnly"; import { MoonIcon, SunIcon } from "lucide-react"; import { useTheme } from "next-themes"; import { Skeleton } from "./ui/skeleton"; diff --git a/apps/dashboard/src/@/components/ui/Spinner/Spinner.module.css b/apps/dashboard/src/@/components/ui/Spinner/Spinner.module.css index 0adc57ba75e..622a43a8a79 100644 --- a/apps/dashboard/src/@/components/ui/Spinner/Spinner.module.css +++ b/apps/dashboard/src/@/components/ui/Spinner/Spinner.module.css @@ -12,7 +12,6 @@ inset: 0px; border-radius: 50%; border: 4px solid #fff; - animation: prixClipFix 2s linear infinite; stroke-linecap: round; animation: dash 1.5s ease-in-out infinite; } diff --git a/apps/dashboard/src/@/components/ui/button.tsx b/apps/dashboard/src/@/components/ui/button.tsx index 81b78577aab..cb4966e89df 100644 --- a/apps/dashboard/src/@/components/ui/button.tsx +++ b/apps/dashboard/src/@/components/ui/button.tsx @@ -9,17 +9,16 @@ const buttonVariants = cva( { variants: { variant: { - primary: - "bg-primary hover:bg-primary/90 text-semibold text-primary-foreground ", + primary: "bg-primary hover:bg-primary/90 text-primary-foreground ", default: "bg-foreground text-background hover:bg-foreground/90", destructive: - "bg-destructive hover:bg-destructive/90 text-semibold text-destructive-foreground ", + "bg-destructive hover:bg-destructive/90 text-destructive-foreground ", outline: - "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground text-semibold", + "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", secondary: - "bg-secondary hover:bg-secondary/80 text-semibold text-secondary-foreground ", - ghost: "hover:bg-accent text-semibold hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline text-semibold", + "bg-secondary hover:bg-secondary/80 text-secondary-foreground ", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "underline-offset-4 hover:underline", pink: "border border-nebula-pink-foreground !text-nebula-pink-foreground bg-[hsl(var(--nebula-pink-foreground)/5%)] hover:bg-nebula-pink-foreground/10 dark:!text-foreground dark:bg-nebula-pink-foreground/10 dark:hover:bg-nebula-pink-foreground/20", upsell: "bg-green-600 text-white hover:bg-green-700 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-200", diff --git a/apps/dashboard/src/@/components/ui/skeleton.tsx b/apps/dashboard/src/@/components/ui/skeleton.tsx index e4b94e893dd..17bd1f3ec02 100644 --- a/apps/dashboard/src/@/components/ui/skeleton.tsx +++ b/apps/dashboard/src/@/components/ui/skeleton.tsx @@ -16,7 +16,7 @@ function SkeletonContainer(props: { loadedData?: T; skeletonData: T; className?: string; - render: (data: T, isSkeleton: boolean) => React.ReactNode; + render: (data: T) => React.ReactNode; style?: React.CSSProperties; }) { const isLoading = props.loadedData === undefined; @@ -40,7 +40,6 @@ function SkeletonContainer(props: { props.loadedData === undefined ? props.skeletonData : props.loadedData, - !isLoading, )} diff --git a/apps/dashboard/src/@/constants/public-envs.ts b/apps/dashboard/src/@/constants/public-envs.ts index 58bbe701ddf..f90758c0122 100644 --- a/apps/dashboard/src/@/constants/public-envs.ts +++ b/apps/dashboard/src/@/constants/public-envs.ts @@ -1,9 +1,6 @@ export const NEXT_PUBLIC_DASHBOARD_CLIENT_ID = process.env.NEXT_PUBLIC_DASHBOARD_CLIENT_ID || ""; -export const NEXT_PUBLIC_NEBULA_APP_CLIENT_ID = - process.env.NEXT_PUBLIC_NEBULA_APP_CLIENT_ID || ""; - export const NEXT_PUBLIC_THIRDWEB_VAULT_URL = process.env.NEXT_PUBLIC_THIRDWEB_VAULT_URL || ""; @@ -30,7 +27,5 @@ export const NEXT_PUBLIC_TURNSTILE_SITE_KEY = export const NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET = process.env.NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET || ""; -export const NEXT_PUBLIC_NEBULA_URL = process.env.NEXT_PUBLIC_NEBULA_URL || ""; - export const NEXT_PUBLIC_DEMO_ENGINE_URL = process.env.NEXT_PUBLIC_DEMO_ENGINE_URL || ""; diff --git a/apps/dashboard/src/@/constants/server-envs.ts b/apps/dashboard/src/@/constants/server-envs.ts index 234c7b1c669..ee0e4e5f9fa 100644 --- a/apps/dashboard/src/@/constants/server-envs.ts +++ b/apps/dashboard/src/@/constants/server-envs.ts @@ -18,16 +18,6 @@ experimental_taintUniqueValue( DASHBOARD_THIRDWEB_SECRET_KEY, ); -export const NEBULA_APP_SECRET_KEY = process.env.NEBULA_APP_SECRET_KEY || ""; - -if (NEBULA_APP_SECRET_KEY) { - experimental_taintUniqueValue( - "Do not pass NEBULA_APP_SECRET_KEY to the client", - process, - NEBULA_APP_SECRET_KEY, - ); -} - export const API_SERVER_SECRET = process.env.API_SERVER_SECRET || ""; if (API_SERVER_SECRET) { diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx index 21a7d2b0f43..4b6fa6e9aab 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx @@ -14,16 +14,12 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; -import { - getAuthToken, - getAuthTokenWalletAddress, -} from "@app/api/lib/getAuthToken"; +import { getAuthToken } from "@app/api/lib/getAuthToken"; import { ChevronDownIcon, TicketCheckIcon } from "lucide-react"; import type { Metadata } from "next"; import Link from "next/link"; import { redirect } from "next/navigation"; import { mapV4ChainToV5Chain } from "../../../../../../contexts/map-chains"; -import { NebulaChatButton } from "../../../../../nebula-app/(app)/components/FloatingChat/FloatingChat"; import { TeamHeader } from "../../../../team/components/TeamHeader/team-header"; import { StarButton } from "../../components/client/star-button"; import { getChain, getChainMetadata } from "../../utils"; @@ -61,10 +57,9 @@ export default async function ChainPageLayout(props: { }) { const params = await props.params; const { children } = props; - const [chain, authToken, accountAddress] = await Promise.all([ + const [chain, authToken] = await Promise.all([ getChain(params.chain_id), getAuthToken(), - getAuthTokenWalletAddress(), ]); if (params.chain_id !== chain.slug) { @@ -74,49 +69,12 @@ export default async function ChainPageLayout(props: { const chainMetadata = await getChainMetadata(chain.chainId); const client = getClientThirdwebClient(undefined); - const chainPromptPrefix = `\ -You are assisting users exploring the chain ${chain.name} (Chain ID: ${chain.chainId}). Provide concise insights into the types of applications and activities prevalent on this chain, such as DeFi protocols, NFT marketplaces, or gaming platforms. Highlight notable projects or trends without delving into technical details like consensus mechanisms or gas fees. -Users may seek comparisons between ${chain.name} and other chains. Provide objective, succinct comparisons focusing on performance, fees, and ecosystem support. Refrain from transaction-specific advice unless requested. -Provide users with an understanding of the unique use cases and functionalities that ${chain.name} supports. Discuss how developers leverage this chain for specific applications, such as scalable dApps, low-cost transactions, or specialized token standards, focusing on practical implementations. -Users may be interested in utilizing thirdweb tools on ${chain.name}. Offer clear guidance on how thirdweb's SDKs, smart contract templates, and deployment tools integrate with this chain. Emphasize the functionalities enabled by thirdweb without discussing transaction execution unless prompted. -Avoid transaction-related actions to be executed by the user unless inquired about. - -The following is the user's message: - `; - - const examplePrompts: string[] = [ - "What are users doing on this chain?", - "What are the most active contracts?", - "Why would I use this chain over others?", - "Can I deploy thirdweb contracts to this chain?", - ]; - - if (chain.chainId !== 1) { - examplePrompts.push("Can I bridge assets from Ethereum to this chain?"); - } - return (
- ({ - title: prompt, - message: prompt, - }))} - /> +
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/shared-layout.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/shared-layout.tsx index 7ac143c125a..8cdb83288d4 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/shared-layout.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/shared-layout.tsx @@ -8,9 +8,6 @@ import { notFound } from "next/navigation"; import { getContractMetadata } from "thirdweb/extensions/common"; import { isAddress, isContractDeployed } from "thirdweb/utils"; import { shortenIfAddress } from "utils/usedapp-external"; -import { NebulaChatButton } from "../../../../../nebula-app/(app)/components/FloatingChat/FloatingChat"; -import { examplePrompts } from "../../../../../nebula-app/(app)/data/examplePrompts"; -import { getAuthTokenWalletAddress } from "../../../../api/lib/getAuthToken"; import type { ProjectMeta } from "../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types"; import { TeamHeader } from "../../../../team/components/TeamHeader/team-header"; import { ConfigureCustomChain } from "./_layout/ConfigureCustomChain"; @@ -34,13 +31,12 @@ export async function SharedContractLayout(props: { return notFound(); } - const [info, accountAddress, teamsAndProjects] = await Promise.all([ + const [info, teamsAndProjects] = await Promise.all([ getContractPageParamsInfo({ contractAddress: props.contractAddress, chainIdOrSlug: props.chainIdOrSlug, teamId: props.projectMeta?.teamId, }), - getAuthTokenWalletAddress(), getTeamsAndProjectsIfLoggedIn(), ]); @@ -109,17 +105,6 @@ export async function SharedContractLayout(props: { projectMeta: props.projectMeta, }); - const contractAddress = serverContract.address; - const chainName = chainMetadata.name; - const chainId = chainMetadata.chainId; - - const contractPromptPrefix = `A user is viewing the contract address ${contractAddress} on ${chainName} (Chain ID: ${chainId}). Provide a concise summary of this contract's functionalities, such as token minting, staking, or governance mechanisms. Focus on what the contract enables users to do, avoiding transaction execution details unless requested. -Users may be interested in how to interact with the contract. Outline common interaction patterns, such as claiming rewards, participating in governance, or transferring assets. Emphasize the contract's capabilities without guiding through transaction processes unless asked. -Provide insights into how the contract is being used. Share information on user engagement, transaction volumes, or integration with other dApps, focusing on the contract's role within the broader ecosystem. -Users may be considering integrating the contract into their applications. Discuss how this contract's functionalities can be leveraged within different types of dApps, highlighting potential use cases and benefits. - -The following is the user's message:`; - return ( - {props.children} diff --git a/apps/dashboard/src/app/nebula-app/(app)/icons/NebulaIcon.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/components/server/icons/NebulaIcon.tsx similarity index 100% rename from apps/dashboard/src/app/nebula-app/(app)/icons/NebulaIcon.tsx rename to apps/dashboard/src/app/(app)/(dashboard)/(chain)/components/server/icons/NebulaIcon.tsx diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/components/server/products.ts b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/components/server/products.ts index bbdae72b44d..ed3b9bc72c4 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/components/server/products.ts +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/components/server/products.ts @@ -1,9 +1,9 @@ -import { NebulaIcon } from "../../../../../nebula-app/(app)/icons/NebulaIcon"; import type { ChainSupportedService } from "../../types/chain"; import { ConnectSDKIcon } from "./icons/ConnectSDKIcon"; import { ContractIcon } from "./icons/ContractIcon"; import { EngineIcon } from "./icons/EngineIcon"; import { InsightIcon } from "./icons/InsightIcon"; +import { NebulaIcon } from "./icons/NebulaIcon"; import { PayIcon } from "./icons/PayIcon"; import { RPCIcon } from "./icons/RPCIcon"; import { SmartAccountIcon } from "./icons/SmartAccountIcon"; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx index 3e94eec24ee..b4545311141 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx @@ -5,11 +5,11 @@ import { BookOpenIcon, ChevronRightIcon } from "lucide-react"; import { HomeIcon, WalletIcon } from "lucide-react"; import type { Metadata } from "next"; import Link from "next/link"; +import { NebulaIcon } from "../(chain)/components/server/icons/NebulaIcon"; import { EngineIcon } from "../../(dashboard)/(chain)/components/server/icons/EngineIcon"; import { InsightIcon } from "../../(dashboard)/(chain)/components/server/icons/InsightIcon"; import { PayIcon } from "../../(dashboard)/(chain)/components/server/icons/PayIcon"; -import { CustomChatButton } from "../../../nebula-app/(app)/components/CustomChat/CustomChatButton"; -import { NebulaIcon } from "../../../nebula-app/(app)/icons/NebulaIcon"; +import { CustomChatButton } from "../../../../components/CustomChat/CustomChatButton"; import { getAuthToken, getAuthTokenWalletAddress, diff --git a/apps/dashboard/src/app/(app)/(dashboard)/tools/unixtime-converter/page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/tools/unixtime-converter/page.tsx index d9568e16f4e..2c148b4d162 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/tools/unixtime-converter/page.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/tools/unixtime-converter/page.tsx @@ -1,4 +1,4 @@ -import { ClientOnly } from "components/ClientOnly/ClientOnly"; +import { ClientOnly } from "@/components/blocks/client-only"; import type { Metadata } from "next"; import { UnixTimeConverter } from "./components/UnixTimeConverter"; diff --git a/apps/dashboard/src/app/(app)/account/contracts/_components/DeployedContractsPage.tsx b/apps/dashboard/src/app/(app)/account/contracts/_components/DeployedContractsPage.tsx index 140f0779ce5..8c344a524e3 100644 --- a/apps/dashboard/src/app/(app)/account/contracts/_components/DeployedContractsPage.tsx +++ b/apps/dashboard/src/app/(app)/account/contracts/_components/DeployedContractsPage.tsx @@ -1,5 +1,5 @@ +import { ClientOnly } from "@/components/blocks/client-only"; import { Spinner } from "@/components/ui/Spinner/Spinner"; -import { ClientOnly } from "components/ClientOnly/ClientOnly"; import { ContractTable } from "components/contract-components/tables/contract-table"; import { Suspense } from "react"; import type { ThirdwebClient } from "thirdweb"; diff --git a/apps/dashboard/src/app/(app)/login/LoginPage.tsx b/apps/dashboard/src/app/(app)/login/LoginPage.tsx index 6e4c60fd4b5..bcae0f110ea 100644 --- a/apps/dashboard/src/app/(app)/login/LoginPage.tsx +++ b/apps/dashboard/src/app/(app)/login/LoginPage.tsx @@ -1,6 +1,7 @@ "use client"; import { getRawAccountAction } from "@/actions/getAccount"; +import { ClientOnly } from "@/components/blocks/client-only"; import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage"; import { ToggleThemeButton } from "@/components/color-mode-toggle"; import { Spinner } from "@/components/ui/Spinner/Spinner"; @@ -8,7 +9,6 @@ import { NEXT_PUBLIC_TURNSTILE_SITE_KEY } from "@/constants/public-envs"; import { useDashboardRouter } from "@/lib/DashboardRouter"; import type { Account } from "@3rdweb-sdk/react/hooks/useApi"; import { Turnstile } from "@marsidev/react-turnstile"; -import { ClientOnly } from "components/ClientOnly/ClientOnly"; import { isVercel } from "lib/vercel-utils"; import { useTheme } from "next-themes"; import Link from "next/link"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx index 5adc8561115..7ce661fa034 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx @@ -8,7 +8,7 @@ import { AnnouncementBanner } from "components/notices/AnnouncementBanner"; import Link from "next/link"; import { redirect } from "next/navigation"; import { siwaExamplePrompts } from "../../../(dashboard)/support/page"; -import { CustomChatButton } from "../../../../nebula-app/(app)/components/CustomChat/CustomChatButton"; +import { CustomChatButton } from "../../../../../components/CustomChat/CustomChatButton"; import { getValidAccount } from "../../../account/settings/getAccount"; import { getAuthToken, diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/collection-info/nft-collection-info-fieldset.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/collection-info/nft-collection-info-fieldset.tsx index 99af8ef0047..47c63919f6d 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/collection-info/nft-collection-info-fieldset.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/collection-info/nft-collection-info-fieldset.tsx @@ -2,10 +2,10 @@ import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors"; +import { ClientOnly } from "@/components/blocks/client-only"; import { Form } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; -import { ClientOnly } from "components/ClientOnly/ClientOnly"; import { FileInput } from "components/shared/FileInput"; import type { UseFormReturn } from "react-hook-form"; import type { ThirdwebClient } from "thirdweb"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/token/token-info/token-info-fieldset.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/token/token-info/token-info-fieldset.tsx index 063e56d5511..c7860848239 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/token/token-info/token-info-fieldset.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/token/token-info/token-info-fieldset.tsx @@ -2,10 +2,10 @@ import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors"; +import { ClientOnly } from "@/components/blocks/client-only"; import { Form } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; -import { ClientOnly } from "components/ClientOnly/ClientOnly"; import { FileInput } from "components/shared/FileInput"; import type { ThirdwebClient } from "thirdweb"; import { SocialUrlsFieldset } from "../../_common/SocialUrls"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/page.tsx index 536c199d31d..f32490c1062 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/page.tsx @@ -1,11 +1,11 @@ import { getProject } from "@/api/projects"; import { getTeamBySlug } from "@/api/team"; +import { ClientOnly } from "@/components/blocks/client-only"; import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage"; import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; import { redirect } from "next/navigation"; import { Suspense } from "react"; import type { ThirdwebClient } from "thirdweb"; -import { ClientOnly } from "../../../../../../../components/ClientOnly/ClientOnly"; import { ContractTable } from "../../../../../../../components/contract-components/tables/contract-table"; import { getSortedDeployedContracts } from "../../../../../account/contracts/_components/getSortedDeployedContracts"; import { getAuthToken } from "../../../../../api/lib/getAuthToken"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx index 63fb3b35fe8..470ccf034cb 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx @@ -17,7 +17,7 @@ import { import { ContractIcon } from "../../../../../../(dashboard)/(chain)/components/server/icons/ContractIcon"; import { EngineIcon } from "../../../../../../(dashboard)/(chain)/components/server/icons/EngineIcon"; import { InsightIcon } from "../../../../../../(dashboard)/(chain)/components/server/icons/InsightIcon"; -import { NebulaIcon } from "../../../../../../../nebula-app/(app)/icons/NebulaIcon"; +import { NebulaIcon } from "../../../../../../(dashboard)/(chain)/components/server/icons/NebulaIcon"; import { ClientIDSection } from "./ClientIDSection"; import { IntegrateAPIKeyCodeTabs } from "./IntegrateAPIKeyCodeTabs"; import { SecretKeySection } from "./SecretKeySection"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx index f0393114cf6..f6a68864927 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx @@ -13,9 +13,9 @@ import { import { ContractIcon } from "../../../../../(dashboard)/(chain)/components/server/icons/ContractIcon"; import { EngineIcon } from "../../../../../(dashboard)/(chain)/components/server/icons/EngineIcon"; import { InsightIcon } from "../../../../../(dashboard)/(chain)/components/server/icons/InsightIcon"; +import { NebulaIcon } from "../../../../../(dashboard)/(chain)/components/server/icons/NebulaIcon"; import { PayIcon } from "../../../../../(dashboard)/(chain)/components/server/icons/PayIcon"; import { SmartAccountIcon } from "../../../../../(dashboard)/(chain)/components/server/icons/SmartAccountIcon"; -import { NebulaIcon } from "../../../../../../nebula-app/(app)/icons/NebulaIcon"; export function ProjectSidebarLayout(props: { layoutPath: string; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/connect/account-abstraction/factories/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/connect/account-abstraction/factories/page.tsx index 0c9e68b11a2..a5ffe4bb929 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/connect/account-abstraction/factories/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/connect/account-abstraction/factories/page.tsx @@ -1,12 +1,12 @@ import { getProject } from "@/api/projects"; import { getTeamBySlug } from "@/api/team"; +import { ClientOnly } from "@/components/blocks/client-only"; import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage"; import { UnderlineLink } from "@/components/ui/UnderlineLink"; import { Button } from "@/components/ui/button"; import { TrackedLinkTW } from "@/components/ui/tracked-link"; import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; import { serverThirdwebClient } from "@/constants/thirdweb-client.server"; -import { ClientOnly } from "components/ClientOnly/ClientOnly"; import { DefaultFactoriesSection } from "components/smart-wallets/AccountFactories"; import { FactoryContracts } from "components/smart-wallets/AccountFactories/factory-contracts"; import { PlusIcon } from "lucide-react"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/explorer/components/engine-explorer.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/explorer/components/engine-explorer.tsx index 8aedece201b..699736498cf 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/explorer/components/engine-explorer.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/explorer/components/engine-explorer.tsx @@ -1,8 +1,8 @@ "use client"; -import { ClientOnly } from "components/ClientOnly/ClientOnly"; import "./swagger-ui.css"; import "swagger-ui-react/swagger-ui.css"; +import { ClientOnly } from "@/components/blocks/client-only"; import dynamic from "next/dynamic"; const SwaggerUI = dynamic(() => import("swagger-ui-react"), { ssr: false }); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx index b1870470da2..8846c40c3c3 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx @@ -7,7 +7,7 @@ import { AnnouncementBanner } from "components/notices/AnnouncementBanner"; import Link from "next/link"; import { redirect } from "next/navigation"; import { siwaExamplePrompts } from "../../../../(dashboard)/support/page"; -import { CustomChatButton } from "../../../../../nebula-app/(app)/components/CustomChat/CustomChatButton"; +import { CustomChatButton } from "../../../../../../components/CustomChat/CustomChatButton"; import { getValidAccount } from "../../../../account/settings/getAccount"; import { getAuthToken, diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChat.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChat.tsx deleted file mode 100644 index 28595558596..00000000000 --- a/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChat.tsx +++ /dev/null @@ -1,279 +0,0 @@ -"use client"; - -import { Spinner } from "@/components/ui/Spinner/Spinner"; -import { Button } from "@/components/ui/button"; -import { ToolTipLabel } from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils"; -import { useQuery } from "@tanstack/react-query"; -import { useTrack } from "hooks/analytics/useTrack"; -import { ExternalLinkIcon, RefreshCcwIcon, XIcon } from "lucide-react"; -import Link from "next/link"; -import { - Suspense, - lazy, - useCallback, - useEffect, - useRef, - useState, -} from "react"; -import type { ThirdwebClient } from "thirdweb"; -import type { NebulaContext } from "../../api/chat"; -import type { ExamplePrompt } from "../../data/examplePrompts"; -import { NebulaIcon } from "../../icons/NebulaIcon"; -import { getNebulaAuthToken } from "./actions"; - -const LazyFloatingChatContent = lazy(() => import("./FloatingChatContent")); - -export function NebulaChatButton(props: { - pageType: "chain" | "contract" | "support"; - examplePrompts: ExamplePrompt[]; - networks: NebulaContext["networks"]; - isLoggedIn: boolean; - label: string; - client: ThirdwebClient; - isFloating: boolean; - nebulaParams: - | { - messagePrefix: string; - chainIds: number[]; - wallet: string | undefined; - } - | undefined; -}) { - const [isOpen, setIsOpen] = useState(false); - const [hasBeenOpened, setHasBeenOpened] = useState(false); - const closeModal = useCallback(() => setIsOpen(false), []); - const [isDismissed, setIsDismissed] = useState(false); - const trackEvent = useTrack(); - - if (isDismissed) { - return null; - } - - return ( - <> -
- - {props.isFloating && ( - - )} -
- - - - ); -} - -function NebulaChatUIContainer(props: { - onClose: () => void; - isOpen: boolean; - hasBeenOpened: boolean; - examplePrompts: ExamplePrompt[]; - pageType: "chain" | "contract" | "support"; - client: ThirdwebClient; - isLoggedIn: boolean; - networks: NebulaContext["networks"]; - nebulaParams: - | { - messagePrefix: string; - chainIds: number[]; - wallet: string | undefined; - } - | undefined; -}) { - const ref = useOutsideClick(props.onClose); - const shouldRenderChat = props.isOpen || props.hasBeenOpened; - const [nebulaSessionKey, setNebulaSessionKey] = useState(0); - const trackEvent = useTrack(); - - return ( -
-
- { - trackEvent({ - category: "floating_nebula", - action: "click", - label: "nebula-landing", - page: props.pageType, - }); - }} - > -

Nebula

- - - -
- - - - - - - -
-
- - {/* once opened keep the component mounted to preserve the states */} -
- {shouldRenderChat && ( - - )} -
-
- ); -} - -function ChatContent( - props: Omit< - React.ComponentProps, - "authToken" - > & { - sessionKey: number; - isLoggedIn: boolean; - }, -) { - const { sessionKey, isLoggedIn, ...restProps } = props; - const nebulaAuthTokenQuery = useNebulaAuthToken(isLoggedIn); - - if (nebulaAuthTokenQuery.isFetching) { - return ; - } - - return ( - }> - - - ); -} - -function LoadingScreen() { - return ( -
- -
- ); -} - -function useOutsideClick(onOutsideClick: () => void) { - const ref = useRef(null); - - // clicking outside the chat window should close it - // eslint-disable-next-line no-restricted-syntax - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (ref.current && !ref.current.contains(event.target as Node)) { - // if clicked on a dialog or popover - ignore - if ( - (event.target as HTMLElement).closest( - "[data-radix-popper-content-wrapper], [role='dialog'], [data-state='open']", - ) - ) { - return; - } - onOutsideClick(); - } - }; - - document.addEventListener("mousedown", handleClickOutside); - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, [onOutsideClick]); - - return ref; -} - -function useNebulaAuthToken(isLoggedInToDashboard: boolean) { - return useQuery({ - queryKey: ["nebula-auth-token", isLoggedInToDashboard], - queryFn: async () => { - const jwt = await getNebulaAuthToken(); - return jwt || null; - }, - retry: false, - refetchOnWindowFocus: false, - refetchOnMount: "always", - refetchOnReconnect: false, - }); -} diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChatContent.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChatContent.tsx deleted file mode 100644 index 63257bfb22c..00000000000 --- a/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChatContent.tsx +++ /dev/null @@ -1,346 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { useTrack } from "hooks/analytics/useTrack"; -import { ArrowRightIcon, ArrowUpRightIcon } from "lucide-react"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; -import { useCallback, useState } from "react"; -import type { ThirdwebClient } from "thirdweb"; -import { - useActiveWallet, - useActiveWalletConnectionStatus, -} from "thirdweb/react"; -import type { NebulaContext } from "../../api/chat"; -import { createSession } from "../../api/session"; -import type { NebulaUserMessage } from "../../api/types"; -import type { ExamplePrompt } from "../../data/examplePrompts"; -import { NebulaIcon } from "../../icons/NebulaIcon"; -import { ChatBar } from "../ChatBar"; -import { - handleNebulaPrompt, - handleNebulaPromptError, -} from "../ChatPageContent"; -import { Chats } from "../Chats"; -import type { ChatMessage } from "../Chats"; - -export default function FloatingChatContent(props: { - authToken: string | undefined; - client: ThirdwebClient; - examplePrompts: ExamplePrompt[]; - pageType: "chain" | "contract" | "support"; - networks: NebulaContext["networks"]; - nebulaParams: - | { - messagePrefix: string; - chainIds: number[]; - wallet: string | undefined; - } - | undefined; -}) { - if (!props.authToken) { - return ; - } - - return ( - - ); -} - -function FloatingChatContentLoggedIn(props: { - authToken: string; - client: ThirdwebClient; - pageType: "chain" | "contract" | "support"; - examplePrompts: ExamplePrompt[]; - networks: NebulaContext["networks"]; - nebulaParams: - | { - messagePrefix: string; - chainIds: number[]; - wallet: string | undefined; - } - | undefined; -}) { - const [userHasSubmittedMessage, setUserHasSubmittedMessage] = useState(false); - const [messages, setMessages] = useState>([]); - const [sessionId, setSessionId] = useState(undefined); - const [chatAbortController, setChatAbortController] = useState< - AbortController | undefined - >(); - const trackEvent = useTrack(); - const [isChatStreaming, setIsChatStreaming] = useState(false); - const [enableAutoScroll, setEnableAutoScroll] = useState(false); - const connectionStatus = useActiveWalletConnectionStatus(); - const activeWallet = useActiveWallet(); - - const [contextFilters, setContextFilters] = useState< - NebulaContext | undefined - >(() => { - return { - chainIds: - props.nebulaParams?.chainIds.map((chainId) => chainId.toString()) || - null, - walletAddress: props.nebulaParams?.wallet || null, - networks: props.networks, - }; - }); - - const initSession = useCallback(async () => { - const session = await createSession({ - authToken: props.authToken, - context: contextFilters, - }); - setSessionId(session.id); - return session; - }, [props.authToken, contextFilters]); - - const handleSendMessage = useCallback( - async (userMessage: NebulaUserMessage) => { - const abortController = new AbortController(); - setUserHasSubmittedMessage(true); - setIsChatStreaming(true); - setEnableAutoScroll(true); - - const textMessage = userMessage.content.find((x) => x.type === "text"); - - trackEvent({ - category: "floating_nebula", - action: "send", - label: "message", - message: textMessage?.text, - page: props.pageType, - sessionId: sessionId, - }); - - setMessages((prev) => [ - ...prev, - { - type: "user", - content: userMessage.content, - }, - // instant loading indicator feedback to user - { - type: "presence", - texts: [], - }, - ]); - - const messagePrefix = props.nebulaParams?.messagePrefix; - - // if this is first message, set the message prefix - // deep clone `userMessage` to avoid mutating the original message, its a pretty small object so JSON.parse is fine - const messageToSend = JSON.parse( - JSON.stringify(userMessage), - ) as NebulaUserMessage; - - // if this is first message, set the message prefix - if (messagePrefix && !userHasSubmittedMessage) { - const textMessage = messageToSend.content.find( - (x) => x.type === "text", - ); - if (textMessage) { - textMessage.text = `${messagePrefix}\n\n${textMessage.text}`; - } - } - - try { - // Ensure we have a session ID - let currentSessionId = sessionId; - if (!currentSessionId) { - const session = await initSession(); - currentSessionId = session.id; - } - - setChatAbortController(abortController); - await handleNebulaPrompt({ - abortController, - message: messageToSend, - sessionId: currentSessionId, - authToken: props.authToken, - setMessages, - contextFilters: contextFilters, - setContextFilters: setContextFilters, - }); - } catch (error) { - if (abortController.signal.aborted) { - return; - } - - handleNebulaPromptError({ - error, - setMessages, - }); - } finally { - setIsChatStreaming(false); - setEnableAutoScroll(false); - } - }, - [ - props.authToken, - contextFilters, - initSession, - sessionId, - props.nebulaParams?.messagePrefix, - userHasSubmittedMessage, - trackEvent, - props.pageType, - ], - ); - - const showEmptyState = !userHasSubmittedMessage && messages.length === 0; - return ( -
- {showEmptyState ? ( - - ) : ( - - )} - {}} - abortChatStream={() => { - chatAbortController?.abort(); - setChatAbortController(undefined); - setIsChatStreaming(false); - // if last message is presence, remove it - if (messages[messages.length - 1]?.type === "presence") { - setMessages((prev) => prev.slice(0, -1)); - } - }} - isChatStreaming={isChatStreaming} - prefillMessage={undefined} - sendMessage={handleSendMessage} - className="rounded-none border-x-0 border-b-0" - allowImageUpload={true} - /> -
- ); -} - -function LoggedOutStateChatContent() { - const pathname = usePathname(); - return ( -
-
-
-
- -
-
-
- -

- How can I help you
- onchain today? -

- -
-

- Sign in to use Nebula AI -

-
- - -
- ); -} - -function EmptyStateChatPageContent(props: { - sendMessage: (message: NebulaUserMessage) => void; - examplePrompts: ExamplePrompt[]; -}) { - return ( -
-
-
-
- -
-
-
- -

- How can I help you
- onchain today? -

- -
-
- {props.examplePrompts.map((prompt) => { - return ( - - props.sendMessage({ - role: "user", - content: [ - { - type: "text", - text: prompt.message, - }, - ], - }) - } - /> - ); - })} -
-
- ); -} - -function ExamplePromptButton(props: { label: string; onClick: () => void }) { - return ( - - ); -} diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/actions.ts b/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/actions.ts deleted file mode 100644 index a5cd4b69c43..00000000000 --- a/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/actions.ts +++ /dev/null @@ -1,54 +0,0 @@ -"use server"; - -import { - getAuthToken, - getAuthTokenWalletAddress, -} from "../../../../(app)/api/lib/getAuthToken"; -import { getNebulaLoginStatus } from "../../../_utils/isLoggedIntoNebula"; -import { - doNebulaLogin, - getNebulaLoginPayload, -} from "../../../login/auth-actions"; - -export async function getNebulaAuthToken() { - const [dashboardAuthToken, dashboardAuthTokenAddress] = await Promise.all([ - getAuthToken(), - getAuthTokenWalletAddress(), - ]); - - // if not logged in to dashboard - if (!dashboardAuthToken || !dashboardAuthTokenAddress) { - return undefined; - } - - const nebulaLoginStatus = await getNebulaLoginStatus(); - - // if already logged in to nebula - if (nebulaLoginStatus.isLoggedIn) { - return nebulaLoginStatus.authToken; - } - - // automatically login to nebula with the dashboard auth token --- - const loginPayload = await getNebulaLoginPayload({ - address: dashboardAuthTokenAddress, - chainId: 1, - }); - - if (!loginPayload) { - return undefined; - } - - const result = await doNebulaLogin({ - type: "floating-chat", - loginPayload: { - payload: loginPayload, - token: dashboardAuthToken, - }, - }); - - if (result.success) { - return result.token; - } - - return undefined; -} diff --git a/apps/dashboard/src/app/nebula-app/layout.tsx b/apps/dashboard/src/app/nebula-app/layout.tsx deleted file mode 100644 index 927bbc8a5eb..00000000000 --- a/apps/dashboard/src/app/nebula-app/layout.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import type { Metadata } from "next"; -import "../../global.css"; -import "./nebula-global.css"; -import { DashboardRouterTopProgressBar } from "@/lib/DashboardRouter"; -import { cn } from "@/lib/utils"; -import { PHProvider } from "lib/posthog/Posthog"; -import { PosthogHeadSetup } from "lib/posthog/PosthogHeadSetup"; -import { PostHogPageView } from "lib/posthog/PosthogPageView"; -import { Inter } from "next/font/google"; -import NextTopLoader from "nextjs-toploader"; -import { NebulaProviders } from "./providers"; - -const title = - "thirdweb Nebula: The Most powerful AI for interacting with the blockchain"; -const description = - "The most powerful AI for interacting with the blockchain, with real-time access to EVM chains and their data"; - -export const metadata: Metadata = { - title, - description, - openGraph: { - title, - description, - }, -}; - -const fontSans = Inter({ - subsets: ["latin"], - variable: "--font-sans", - display: "swap", -}); - -export default function Layout(props: { - children: React.ReactNode; -}) { - return ( - - - - - - - - - {props.children} - - - - - - ); -} diff --git a/apps/dashboard/src/app/nebula-app/nebula-global.css b/apps/dashboard/src/app/nebula-app/nebula-global.css deleted file mode 100644 index 345dfcb1578..00000000000 --- a/apps/dashboard/src/app/nebula-app/nebula-global.css +++ /dev/null @@ -1,4 +0,0 @@ -::selection { - background-color: hsl(var(--inverted)); - color: hsl(var(--inverted-foreground)); -} diff --git a/apps/dashboard/src/app/nebula-app/readme.md b/apps/dashboard/src/app/nebula-app/readme.md deleted file mode 100644 index 7b1f684db10..00000000000 --- a/apps/dashboard/src/app/nebula-app/readme.md +++ /dev/null @@ -1,10 +0,0 @@ -# Nebula App - -- This section of dashboard is only accessible via `nebula` subdomain and not from the `/nebula-app` route. This is done using `middleware.ts` -- Treat this section of dashboard as a completely separate app from the rest of the dashboard. -- For example: redirecting to `/login` inside this layout will render `nebula-app/(app)/login` page and not the global login page because this layout is only rendered at top level from `nebula` subdomain. - -## Development - -- Start dev server - `pnpm run dashboard` -- Go to `http://nebula.localhost:3000` to test the nebula app. diff --git a/apps/dashboard/src/components/ClientOnly/ClientOnly.module.css b/apps/dashboard/src/components/ClientOnly/ClientOnly.module.css deleted file mode 100644 index 32cb7923ae0..00000000000 --- a/apps/dashboard/src/components/ClientOnly/ClientOnly.module.css +++ /dev/null @@ -1,14 +0,0 @@ -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -.fadeIn { - animation-name: fadeIn; - animation-timing-function: ease; - animation-fill-mode: forwards; -} diff --git a/apps/dashboard/src/components/CustomChat/ChatBar.tsx b/apps/dashboard/src/components/CustomChat/ChatBar.tsx new file mode 100644 index 00000000000..e41d6ff875c --- /dev/null +++ b/apps/dashboard/src/components/CustomChat/ChatBar.tsx @@ -0,0 +1,136 @@ +"use client"; +import { DynamicHeight } from "@/components/ui/DynamicHeight"; +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { AutoResizeTextarea } from "@/components/ui/textarea"; +import { ToolTipLabel } from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import { ArrowUpIcon, CircleStopIcon, PaperclipIcon } from "lucide-react"; +import { useState } from "react"; +import type { ThirdwebClient } from "thirdweb"; +import type { NebulaUserMessage } from "./types"; + +export function ChatBar(props: { + sendMessage: (message: NebulaUserMessage) => void; + isChatStreaming: boolean; + abortChatStream: () => void; + prefillMessage: string | undefined; + className?: string; + client: ThirdwebClient; + isConnectingWallet: boolean; + onLoginClick: undefined | (() => void); + placeholder: string; +}) { + const [message, setMessage] = useState(props.prefillMessage || ""); + + function handleSubmit(message: string) { + const userMessage: NebulaUserMessage = { + role: "user", + content: [{ type: "text", text: message }], + }; + + props.sendMessage(userMessage); + setMessage(""); + } + + return ( + +
+
+
+ setMessage(e.target.value)} + onKeyDown={(e) => { + // ignore if shift key is pressed to allow entering new lines + if (e.shiftKey) { + return; + } + if (e.key === "Enter" && !props.isChatStreaming) { + e.preventDefault(); + handleSubmit(message); + } + }} + className="min-h-[60px] resize-none border-none bg-transparent pt-2 leading-relaxed focus-visible:ring-0 focus-visible:ring-offset-0" + disabled={props.isChatStreaming} + /> +
+ +
+ {/* left */} +
+ + {/* right */} +
+ {props.onLoginClick ? ( + + + + + +
+

+ Get access to image uploads by signing in to Nebula +

+ +
+
+
+ ) : null} + + {/* Send / Stop */} + {props.isChatStreaming ? ( + + ) : ( + + )} +
+
+
+
+ + ); +} diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx b/apps/dashboard/src/components/CustomChat/CustomChatButton.tsx similarity index 100% rename from apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx rename to apps/dashboard/src/components/CustomChat/CustomChatButton.tsx diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx b/apps/dashboard/src/components/CustomChat/CustomChatContent.tsx similarity index 96% rename from apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx rename to apps/dashboard/src/components/CustomChat/CustomChatContent.tsx index bb294ed6793..16c341fdc08 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx +++ b/apps/dashboard/src/components/CustomChat/CustomChatContent.tsx @@ -7,12 +7,11 @@ import { usePathname } from "next/navigation"; import { useCallback, useState } from "react"; import type { ThirdwebClient } from "thirdweb"; import { useActiveWalletConnectionStatus } from "thirdweb/react"; -import type { NebulaContext } from "../../api/chat"; -import type { ExamplePrompt } from "../../data/examplePrompts"; -import { NebulaIcon } from "../../icons/NebulaIcon"; -import { ChatBar } from "../ChatBar"; +import { NebulaIcon } from "../../app/(app)/(dashboard)/(chain)/components/server/icons/NebulaIcon"; +import { ChatBar } from "./ChatBar"; import { type CustomChatMessage, CustomChats } from "./CustomChats"; import type { UserMessage, UserMessageContent } from "./CustomChats"; +import type { ExamplePrompt, NebulaContext } from "./types"; export default function CustomChatContent(props: { authToken: string | undefined; @@ -29,7 +28,6 @@ export default function CustomChatContent(props: { return ( >([]); @@ -242,11 +239,6 @@ function CustomChatContentLoggedIn(props: { onLoginClick={undefined} client={props.client} isConnectingWallet={connectionStatus === "connecting"} - context={undefined} - setContext={() => {}} - showContextSelector={false} - connectedWallets={[]} - setActiveWallet={() => {}} abortChatStream={() => { chatAbortController?.abort(); setChatAbortController(undefined); @@ -268,7 +260,6 @@ function CustomChatContentLoggedIn(props: { handleSendMessage(userMessage); }} className="rounded-none border-x-0 border-b-0" - allowImageUpload={false} />
); diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChats.tsx b/apps/dashboard/src/components/CustomChat/CustomChats.tsx similarity index 99% rename from apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChats.tsx rename to apps/dashboard/src/components/CustomChat/CustomChats.tsx index 172c03d2d30..0d84b01e0ef 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChats.tsx +++ b/apps/dashboard/src/components/CustomChat/CustomChats.tsx @@ -9,7 +9,7 @@ import { } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import type { ThirdwebClient } from "thirdweb"; -import { Reasoning } from "../Reasoning/Reasoning"; +import { Reasoning } from "./Reasoning"; // Define local types export type UserMessageContent = { type: "text"; text: string }; diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/Reasoning/Reasoning.tsx b/apps/dashboard/src/components/CustomChat/Reasoning.tsx similarity index 100% rename from apps/dashboard/src/app/nebula-app/(app)/components/Reasoning/Reasoning.tsx rename to apps/dashboard/src/components/CustomChat/Reasoning.tsx diff --git a/apps/dashboard/src/components/CustomChat/types.ts b/apps/dashboard/src/components/CustomChat/types.ts new file mode 100644 index 00000000000..ba1aaaa0d2b --- /dev/null +++ b/apps/dashboard/src/components/CustomChat/types.ts @@ -0,0 +1,36 @@ +export type ExamplePrompt = { + title: string; + message: string; + interceptedReply?: string; +}; + +// TODO - remove "Nebula" wording and simplify types + +export type NebulaContext = { + chainIds: string[] | null; + walletAddress: string | null; + networks: "mainnet" | "testnet" | "all" | null; +}; + +type NebulaUserMessageContentItem = + | { + type: "image"; + image_url: string | null; + b64: string | null; + } + | { + type: "text"; + text: string; + } + | { + type: "transaction"; + transaction_hash: string; + chain_id: number; + }; + +type NebulaUserMessageContent = NebulaUserMessageContentItem[]; + +export type NebulaUserMessage = { + role: "user"; + content: NebulaUserMessageContent; +}; diff --git a/apps/dashboard/src/components/explore/contract-card/index.tsx b/apps/dashboard/src/components/explore/contract-card/index.tsx index 52283bd04be..1d09547672a 100644 --- a/apps/dashboard/src/components/explore/contract-card/index.tsx +++ b/apps/dashboard/src/components/explore/contract-card/index.tsx @@ -1,3 +1,4 @@ +import { ClientOnly } from "@/components/blocks/client-only"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; @@ -10,7 +11,6 @@ import { moduleToBase64 } from "app/(app)/(dashboard)/published-contract/utils/m import { replaceDeployerAddress } from "lib/publisher-utils"; import { RocketIcon, ShieldCheckIcon } from "lucide-react"; import Link from "next/link"; -import { ClientOnly } from "../../ClientOnly/ClientOnly"; import { fetchPublishedContractVersion } from "../../contract-components/fetch-contracts-with-versions"; import { ContractPublisher } from "../publisher"; diff --git a/apps/dashboard/src/middleware.ts b/apps/dashboard/src/middleware.ts index c512b0f10ec..c2fa6cd94e4 100644 --- a/apps/dashboard/src/middleware.ts +++ b/apps/dashboard/src/middleware.ts @@ -5,10 +5,6 @@ import { getAddress } from "thirdweb"; import { getChainMetadata } from "thirdweb/chains"; import { isValidENSName } from "thirdweb/utils"; import { LAST_VISITED_TEAM_PAGE_PATH } from "./app/(app)/team/components/last-visited-page/consts"; -import { - NEBULA_COOKIE_ACTIVE_ACCOUNT, - NEBULA_COOKIE_PREFIX_TOKEN, -} from "./app/nebula-app/_utils/constants"; import { defineDashboardChain } from "./lib/defineDashboardChain"; // ignore assets, api - only intercept page routes @@ -31,44 +27,8 @@ export async function middleware(request: NextRequest) { const { pathname } = request.nextUrl; // nebula subdomain handling - const host = request.headers.get("host"); - const subdomain = host?.split(".")[0]; const paths = pathname.slice(1).split("/"); - const nebulaActiveAccount = request.cookies.get( - NEBULA_COOKIE_ACTIVE_ACCOUNT, - )?.value; - - const nebulaAuthCookie = nebulaActiveAccount - ? request.cookies.get( - NEBULA_COOKIE_PREFIX_TOKEN + getAddress(nebulaActiveAccount), - ) - : null; - - // nebula.thirdweb.com -> render page at app/nebula-app - // on vercel preview, the format is nebula---thirdweb-www-git-.thirdweb-preview.com - if ( - subdomain && - (subdomain === "nebula" || subdomain.startsWith("nebula---")) - ) { - // preserve search params when redirecting to /login page - if ( - !nebulaAuthCookie && - paths[0] !== "login" && - paths[0] !== "move-funds" - ) { - return redirect(request, "/login", { - searchParams: request.nextUrl.searchParams.toString(), - }); - } - - const newPaths = ["nebula-app", ...paths]; - - return rewrite(request, `/${newPaths.join("/")}`, { - searchParams: request.nextUrl.searchParams.toString(), - }); - } - let cookiesToSet: Record | undefined = undefined; const activeAccount = request.cookies.get(COOKIE_ACTIVE_ACCOUNT)?.value; diff --git a/apps/dashboard/src/stories/stubs.ts b/apps/dashboard/src/stories/stubs.ts index 37d29fc69e4..6a42f07be30 100644 --- a/apps/dashboard/src/stories/stubs.ts +++ b/apps/dashboard/src/stories/stubs.ts @@ -305,35 +305,6 @@ export function teamSubscriptionsStub( ]; } -export function randomLorem(length: number) { - const loremWords = [ - "lorem", - "ipsum", - "dolor", - "sit", - "amet", - "consectetur", - "adipiscing", - "elit", - "sed", - "do", - "eiusmod", - "tempor", - "incididunt", - "ut", - "labore", - "et", - "dolore", - "magna", - "aliqua", - ]; - - return Array.from({ length }, () => { - const randomIndex = Math.floor(Math.random() * loremWords.length); - return loremWords[randomIndex]; - }).join(" "); -} - export function newAccountStub(overrides?: Partial): Account { return { email: undefined, diff --git a/apps/nebula/.env.example b/apps/nebula/.env.example new file mode 100644 index 00000000000..09b24feff90 --- /dev/null +++ b/apps/nebula/.env.example @@ -0,0 +1,2 @@ +NEXT_PUBLIC_IPFS_GATEWAY_URL="https://{clientId}.thirdwebstorage-dev.com/ipfs/{cid}/{path}" +NEXT_PUBLIC_NEBULA_URL="https://nebula-api.thirdweb-dev.com" diff --git a/apps/nebula/.eslintignore b/apps/nebula/.eslintignore new file mode 100644 index 00000000000..9d60596ddc2 --- /dev/null +++ b/apps/nebula/.eslintignore @@ -0,0 +1,13 @@ +# folders +artifacts/ +build/ +cache/ +coverage/ +dist/ +node_modules/ +typechain/ +graphql/ + +# files +.solcover.js +coverage.json diff --git a/apps/nebula/.eslintrc.js b/apps/nebula/.eslintrc.js new file mode 100644 index 00000000000..39095430532 --- /dev/null +++ b/apps/nebula/.eslintrc.js @@ -0,0 +1,114 @@ +module.exports = { + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@next/next/recommended", + "plugin:storybook/recommended", + ], + rules: { + "react-compiler/react-compiler": "error", + "no-restricted-syntax": [ + "error", + { + selector: "CallExpression[callee.name='useEffect']", + message: + 'Are you *sure* you need to use "useEffect" here? If you loading any async function prefer using "useQuery".', + }, + { + selector: "CallExpression[callee.name='createContext']", + message: + 'Are you *sure* you need to use a "Context"? In almost all cases you should prefer passing props directly.', + }, + { + selector: "CallExpression[callee.name='resolveScheme']", + message: + "resolveScheme can throw error if resolution fails. Either catch the error and ignore the lint warning or Use `resolveSchemeWithErrorHandler` / `replaceIpfsUrl` utility in dashboard instead", + }, + ], + "no-restricted-imports": [ + "error", + { + paths: [ + { + name: "next/navigation", + importNames: ["useRouter"], + message: + 'Use `import { useDashboardRouter } from "@/lib/DashboardRouter";` instead', + }, + { + name: "lucide-react", + importNames: ["Link", "Table", "Sidebar"], + message: + 'This is likely a mistake. If you really want to import this - postfix the imported name with Icon. Example - "LinkIcon"', + }, + ], + }, + ], + }, + parser: "@typescript-eslint/parser", + plugins: ["@typescript-eslint", "react-compiler"], + parserOptions: { + ecmaVersion: 2019, + ecmaFeatures: { + impliedStrict: true, + jsx: true, + }, + warnOnUnsupportedTypeScriptVersion: true, + }, + settings: { + react: { + createClass: "createReactClass", + pragma: "React", + version: "detect", + }, + }, + overrides: [ + // enable rule specifically for TypeScript files + { + files: ["*.ts", "*.tsx"], + rules: { + "@typescript-eslint/explicit-module-boundary-types": ["off"], + }, + }, + + // in test files, allow null assertions and anys and eslint is sometimes weird about the react-scope thing + { + files: ["*test.ts?(x)"], + rules: { + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-explicit-any": "off", + + "react/display-name": "off", + }, + }, + // allow requires in non-transpiled JS files and logical key ordering in config files + { + files: [ + "babel-node.js", + "*babel.config.js", + "env.config.js", + "next.config.js", + "webpack.config.js", + "packages/mobile-web/package-builder/**", + ], + rules: {}, + }, + + // setupTests can have separated imports for logical grouping + { + files: ["setupTests.ts"], + rules: { + "import/newline-after-import": "off", + }, + }, + // THIS NEEDS TO GO LAST! + { + files: ["*.ts", "*.js", "*.tsx", "*.jsx"], + extends: ["biome"], + }, + ], + env: { + browser: true, + node: true, + }, +}; diff --git a/apps/nebula/.storybook/main.ts b/apps/nebula/.storybook/main.ts new file mode 100644 index 00000000000..ca8b9ca3d78 --- /dev/null +++ b/apps/nebula/.storybook/main.ts @@ -0,0 +1,28 @@ +import { dirname, join } from "node:path"; +import type { StorybookConfig } from "@storybook/nextjs"; + +/** + * This function is used to resolve the absolute path of a package. + * It is needed in projects that use Yarn PnP or are set up within a monorepo. + */ +function getAbsolutePath(value: string): string { + return dirname(require.resolve(join(value, "package.json"))); +} +const config: StorybookConfig = { + stories: ["../src/**/*.stories.tsx"], + addons: [ + getAbsolutePath("@storybook/addon-onboarding"), + getAbsolutePath("@storybook/addon-links"), + getAbsolutePath("@chromatic-com/storybook"), + getAbsolutePath("@storybook/addon-docs"), + ], + framework: { + name: getAbsolutePath("@storybook/nextjs"), + options: {}, + }, + staticDirs: ["../public"], + features: { + experimentalRSC: true, + }, +}; +export default config; diff --git a/apps/nebula/.storybook/preview.tsx b/apps/nebula/.storybook/preview.tsx new file mode 100644 index 00000000000..7b1658d6275 --- /dev/null +++ b/apps/nebula/.storybook/preview.tsx @@ -0,0 +1,109 @@ +import type { Preview } from "@storybook/nextjs"; +import "../src/global.css"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { MoonIcon, SunIcon } from "lucide-react"; +import { ThemeProvider, useTheme } from "next-themes"; +import { Inter as interFont } from "next/font/google"; +// biome-ignore lint/style/useImportType: +import React from "react"; +import { useEffect } from "react"; +import { Toaster } from "sonner"; +import { Button } from "../src/@/components/ui/button"; + +const queryClient = new QueryClient(); + +const fontSans = interFont({ + subsets: ["latin"], + variable: "--font-sans", +}); + +const customViewports = { + xs: { + // Regular sized phones (iphone 15 / 15 pro) + name: "iPhone", + styles: { + width: "390px", + height: "844px", + }, + }, + sm: { + // Larger phones (iphone 15 plus / 15 pro max) + name: "iPhone Plus", + styles: { + width: "430px", + height: "932px", + }, + }, +}; + +const preview: Preview = { + parameters: { + viewport: { + viewports: customViewports, + }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, + + decorators: [ + (Story) => { + return ( + + + + + + ); + }, + ], +}; + +export default preview; + +function StoryLayout(props: { + children: React.ReactNode; +}) { + const { setTheme, theme } = useTheme(); + + useEffect(() => { + document.body.className = `font-sans antialiased ${fontSans.variable}`; + }, []); + + return ( + +
+
+ +
+ +
{props.children}
+ +
+
+ ); +} + +function ToasterSetup() { + const { theme } = useTheme(); + return ; +} diff --git a/apps/nebula/LICENSE.md b/apps/nebula/LICENSE.md new file mode 100644 index 00000000000..88e8b41ee6d --- /dev/null +++ b/apps/nebula/LICENSE.md @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2021 Non-Fungible Labs, Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/apps/nebula/README.md b/apps/nebula/README.md new file mode 100644 index 00000000000..1b1bd7ce204 --- /dev/null +++ b/apps/nebula/README.md @@ -0,0 +1 @@ +# nebula.thirdweb.com \ No newline at end of file diff --git a/apps/nebula/biome.json b/apps/nebula/biome.json new file mode 100644 index 00000000000..d879ec77ec7 --- /dev/null +++ b/apps/nebula/biome.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.2/schema.json", + "extends": ["../../biome.json"], + "overrides": [ + { + "linter": { + "rules": { + "suspicious": { + "noImportantInKeyframe": "off" + } + } + } + } + ] +} diff --git a/apps/nebula/components.json b/apps/nebula/components.json new file mode 100644 index 00000000000..b1dc07eb643 --- /dev/null +++ b/apps/nebula/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/global.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/apps/nebula/knip.json b/apps/nebula/knip.json new file mode 100644 index 00000000000..c6e1afb1913 --- /dev/null +++ b/apps/nebula/knip.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://unpkg.com/knip@5/schema.json", + "next": true, + "ignore": ["src/@/components/ui/**"], + "project": ["src/**"], + "ignoreBinaries": ["only-allow", "biome"], + "ignoreDependencies": ["thirdweb"] +} diff --git a/apps/nebula/lucide-react.d.ts b/apps/nebula/lucide-react.d.ts new file mode 100644 index 00000000000..93f9156d593 --- /dev/null +++ b/apps/nebula/lucide-react.d.ts @@ -0,0 +1,3 @@ +declare module "lucide-react" { + export * from "lucide-react/dist/lucide-react.suffixed"; +} diff --git a/apps/nebula/next-sitemap.config.js b/apps/nebula/next-sitemap.config.js new file mode 100644 index 00000000000..fd2986798da --- /dev/null +++ b/apps/nebula/next-sitemap.config.js @@ -0,0 +1,38 @@ +// @ts-check + +/** @type {import('next-sitemap').IConfig} */ +module.exports = { + siteUrl: process.env.SITE_URL || "https://nebula.thirdweb.com", + generateRobotsTxt: true, + robotsTxtOptions: { + policies: [ + { + userAgent: "*", + // allow all if production + allow: process.env.VERCEL_ENV === "production" ? ["/"] : [], + // disallow all if not production + disallow: ["/move-funds"], + }, + ], + }, + transform: async (config, path) => { + // ignore og image paths + if (path.includes("_og") || path.includes("opengraph-image")) { + return null; + } + + // disallow /move-funds + if (path.includes("/move-funds")) { + return null; + } + + return { + // => this will be exported as http(s):/// + loc: path, + changefreq: config.changefreq, + priority: config.priority, + lastmod: config.autoLastmod ? new Date().toISOString() : undefined, + alternateRefs: config.alternateRefs ?? [], + }; + }, +}; diff --git a/apps/nebula/next.config.ts b/apps/nebula/next.config.ts new file mode 100644 index 00000000000..320093bb080 --- /dev/null +++ b/apps/nebula/next.config.ts @@ -0,0 +1,70 @@ +import type { NextConfig } from "next"; + +const ContentSecurityPolicy = ` + default-src 'self'; + img-src * data: blob:; + media-src * data: blob:; + object-src 'none'; + style-src 'self' 'unsafe-inline' vercel.live; + font-src 'self' vercel.live assets.vercel.com fonts.gstatic.com; + frame-src * data:; + script-src 'self' 'unsafe-eval' 'unsafe-inline' 'wasm-unsafe-eval' 'inline-speculation-rules' *.thirdweb.com *.thirdweb-dev.com vercel.live challenges.cloudflare.com googletagmanager.com us-assets.i.posthog.com; + connect-src * data: blob:; + worker-src 'self' blob:; + block-all-mixed-content; +`; + +const securityHeaders = [ + { + key: "X-DNS-Prefetch-Control", + value: "on", + }, + { + key: "X-XSS-Protection", + value: "1; mode=block", + }, + { + key: "X-Frame-Options", + value: "SAMEORIGIN", + }, + { + key: "Referrer-Policy", + value: "origin-when-cross-origin", + }, + { + key: "Content-Security-Policy", + value: ContentSecurityPolicy.replace(/\s{2,}/g, " ").trim(), + }, +]; + +const baseNextConfig: NextConfig = { + eslint: { + ignoreDuringBuilds: true, + }, + productionBrowserSourceMaps: false, + experimental: { + webpackBuildWorker: true, + webpackMemoryOptimizations: true, + serverSourceMaps: false, + taint: true, + }, + serverExternalPackages: ["pino-pretty"], + async headers() { + return [ + { + // Apply these headers to all routes in your application. + source: "/(.*)", + headers: [ + ...securityHeaders, + { + key: "accept-ch", + value: "sec-ch-viewport-width", + }, + ], + }, + ]; + }, + reactStrictMode: true, +}; + +export default baseNextConfig; diff --git a/apps/nebula/package.json b/apps/nebula/package.json index c62e07e1a83..b42a51b561d 100644 --- a/apps/nebula/package.json +++ b/apps/nebula/package.json @@ -7,8 +7,11 @@ "dev": "next dev --turbopack", "build": "NODE_OPTIONS=--max-old-space-size=6144 next build", "start": "next start", + "format": "biome format ./src --write", + "lint": "biome check ./src && knip && eslint ./src", "fix": "biome check ./src --fix && eslint ./src --fix", "typecheck": "tsc --noEmit", + "postbuild": "next-sitemap", "build:analyze": "ANALYZE=true pnpm run build", "knip": "knip", "storybook": "storybook dev -p 6006", diff --git a/apps/nebula/postcss.config.js b/apps/nebula/postcss.config.js new file mode 100644 index 00000000000..12a703d900d --- /dev/null +++ b/apps/nebula/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/nebula/src/@/components/blocks/ChainIcon.tsx b/apps/nebula/src/@/components/blocks/ChainIcon.tsx new file mode 100644 index 00000000000..334e647e913 --- /dev/null +++ b/apps/nebula/src/@/components/blocks/ChainIcon.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { Img } from "@/components/blocks/Img"; +import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler"; +import { cn } from "@/lib/utils"; +import type { ThirdwebClient } from "thirdweb"; + +type ImageProps = React.ComponentProps<"img">; + +type ChainIconProps = Omit & { + client: ThirdwebClient; + src?: string; +}; + +const fallbackChainIcon = + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOTYiIGhlaWdodD0iOTYiIHZpZXdCb3g9IjAgMCA5NiA5NiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTY4LjE1MTkgNzUuNzM3MkM2Mi4yOTQzIDc5Ljk5MyA1NS4yMzk3IDgyLjI4NTIgNDcuOTk5MyA4Mi4yODUyQzQwLjc1ODkgODIuMjg1MiAzMy43MDQzIDc5Ljk5MyAyNy44NDY2IDc1LjczNzJNNjMuMDI5MSAxNy4xODM3QzY5LjUzNjggMjAuMzU3NyA3NC44NzI2IDI1LjUxMDQgNzguMjcxOCAzMS45MDMzQzgxLjY3MDkgMzguMjk2MiA4Mi45NTkgNDUuNjAxMiA4MS45NTEzIDUyLjc3MTFNMTQuMDQ3NiA1Mi43NzA4QzEzLjAzOTkgNDUuNjAwOCAxNC4zMjggMzguMjk1OSAxNy43MjcxIDMxLjkwM0MyMS4xMjYzIDI1LjUxMDEgMjYuNDYyMSAyMC4zNTczIDMyLjk2OTggMTcuMTgzM000Ni4wNTk4IDI5LjM2NzVMMjkuMzY3MyA0Ni4wNkMyOC42ODg1IDQ2LjczODkgMjguMzQ5IDQ3LjA3ODMgMjguMjIxOCA0Ny40Njk3QzI4LjExIDQ3LjgxNCAyOC4xMSA0OC4xODQ5IDI4LjIyMTggNDguNTI5MkMyOC4zNDkgNDguOTIwNiAyOC42ODg1IDQ5LjI2MDEgMjkuMzY3MyA0OS45MzlMNDYuMDU5OCA2Ni42MzE0QzQ2LjczODcgNjcuMzEwMyA0Ny4wNzgxIDY3LjY0OTcgNDcuNDY5NSA2Ny43NzY5QzQ3LjgxMzggNjcuODg4OCA0OC4xODQ3IDY3Ljg4ODggNDguNTI5IDY3Ljc3NjlDNDguOTIwNCA2Ny42NDk3IDQ5LjI1OTkgNjcuMzEwMyA0OS45Mzg4IDY2LjYzMTRMNjYuNjMxMiA0OS45MzlDNjcuMzEwMSA0OS4yNjAxIDY3LjY0OTUgNDguOTIwNiA2Ny43NzY3IDQ4LjUyOTJDNjcuODg4NiA0OC4xODQ5IDY3Ljg4ODYgNDcuODE0IDY3Ljc3NjcgNDcuNDY5N0M2Ny42NDk1IDQ3LjA3ODMgNjcuMzEwMSA0Ni43Mzg5IDY2LjYzMTIgNDYuMDZMNDkuOTM4OCAyOS4zNjc1QzQ5LjI1OTkgMjguNjg4NyA0OC45MjA0IDI4LjM0OTIgNDguNTI5IDI4LjIyMkM0OC4xODQ3IDI4LjExMDIgNDcuODEzOCAyOC4xMTAyIDQ3LjQ2OTUgMjguMjIyQzQ3LjA3ODEgMjguMzQ5MiA0Ni43Mzg3IDI4LjY4ODcgNDYuMDU5OCAyOS4zNjc1WiIgc3Ryb2tlPSIjNDA0MDQwIiBzdHJva2Utd2lkdGg9IjYuODU3MTQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K"; + +export const ChainIconClient = ({ + client, + src, + ...restProps +}: ChainIconProps) => { + const resolvedSrc = src + ? resolveSchemeWithErrorHandler({ + client, + uri: src, + }) + : fallbackChainIcon; + + return ( + {restProps.alt}} + skeleton={
} + /> + ); +}; diff --git a/apps/nebula/src/@/components/blocks/FormFieldSetup.tsx b/apps/nebula/src/@/components/blocks/FormFieldSetup.tsx new file mode 100644 index 00000000000..346310925a9 --- /dev/null +++ b/apps/nebula/src/@/components/blocks/FormFieldSetup.tsx @@ -0,0 +1,44 @@ +import { Label } from "@/components/ui/label"; +import { ToolTipLabel } from "@/components/ui/tooltip"; +import { AsteriskIcon, InfoIcon } from "lucide-react"; +import type React from "react"; + +export function FormFieldSetup(props: { + htmlFor?: string; + label: React.ReactNode; + errorMessage: React.ReactNode | undefined; + children: React.ReactNode; + tooltip?: React.ReactNode; + isRequired: boolean; + helperText?: React.ReactNode; + className?: string; +}) { + return ( +
+
+ + + {props.isRequired && ( + + )} + + {props.tooltip && ( + + + + )} +
+ {props.children} + + {props.helperText && ( +

{props.helperText}

+ )} + + {props.errorMessage && ( +

+ {props.errorMessage} +

+ )} +
+ ); +} diff --git a/apps/nebula/src/@/components/blocks/Img.stories.tsx b/apps/nebula/src/@/components/blocks/Img.stories.tsx new file mode 100644 index 00000000000..8a074ca74a8 --- /dev/null +++ b/apps/nebula/src/@/components/blocks/Img.stories.tsx @@ -0,0 +1,112 @@ +import { BadgeContainer } from "@/storybook/utils"; +import type { Meta, StoryObj } from "@storybook/nextjs"; +import { ImageIcon } from "lucide-react"; +import { useState } from "react"; +import { Spinner } from "../ui/Spinner/Spinner"; +import { Button } from "../ui/button"; +import { Img } from "./Img"; + +const meta = { + title: "blocks/Img", + component: Story, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Variants: Story = { + args: {}, +}; + +function Story() { + return ( +
+

All images below are set with size-20 className

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ } + className="size-20" + /> + + + + + +
+ } + className="size-20" + src="invalid-src" + /> + +
+ ); +} + +function ToggleTest() { + const [src, setSrc] = useState(undefined); + + return ( +
+ + +

Src is {src ? "set" : "not set"}

+ + + + + + + + +
+ ); +} diff --git a/apps/nebula/src/@/components/blocks/Img.tsx b/apps/nebula/src/@/components/blocks/Img.tsx new file mode 100644 index 00000000000..ac94e7c966e --- /dev/null +++ b/apps/nebula/src/@/components/blocks/Img.tsx @@ -0,0 +1,88 @@ +/* eslint-disable @next/next/no-img-element */ +"use client"; +import { useRef, useState } from "react"; +import { useIsomorphicLayoutEffect } from "../../lib/useIsomorphicLayoutEffect"; +import { cn } from "../../lib/utils"; + +type imgElementProps = React.DetailedHTMLProps< + React.ImgHTMLAttributes, + HTMLImageElement +> & { + skeleton?: React.ReactNode; + fallback?: React.ReactNode; + src: string | undefined; + containerClassName?: string; +}; + +export function Img(props: imgElementProps) { + const [_status, setStatus] = useState<"pending" | "fallback" | "loaded">( + "pending", + ); + const status = + props.src === undefined + ? "pending" + : props.src === "" + ? "fallback" + : _status; + const { className, fallback, skeleton, containerClassName, ...restProps } = + props; + const defaultSkeleton =
; + const defaultFallback =
; + const imgRef = useRef(null); + + useIsomorphicLayoutEffect(() => { + const imgEl = imgRef.current; + if (!imgEl) { + return; + } + if (imgEl.complete) { + setStatus("loaded"); + } else { + function handleLoad() { + setStatus("loaded"); + } + imgEl.addEventListener("load", handleLoad); + return () => { + imgEl.removeEventListener("load", handleLoad); + }; + } + }, []); + + return ( +
+ { + restProps.onError?.(e); + setStatus("fallback"); + }} + style={{ + opacity: status === "loaded" ? 1 : 0, + ...restProps.style, + }} + alt={restProps.alt || ""} + className={cn( + "fade-in-0 object-cover transition-opacity duration-300", + className, + )} + decoding="async" + /> + + {status !== "loaded" && ( +
*]:h-full [&>*]:w-full", + className, + )} + > + {status === "pending" && (skeleton || defaultSkeleton)} + {status === "fallback" && (fallback || defaultFallback)} +
+ )} +
+ ); +} diff --git a/apps/nebula/src/@/components/blocks/MarkdownRenderer/markdown-renderer.stories.tsx b/apps/nebula/src/@/components/blocks/MarkdownRenderer/markdown-renderer.stories.tsx new file mode 100644 index 00000000000..91eaa0d0061 --- /dev/null +++ b/apps/nebula/src/@/components/blocks/MarkdownRenderer/markdown-renderer.stories.tsx @@ -0,0 +1,164 @@ +import type { Meta, StoryObj } from "@storybook/nextjs"; +import { MarkdownRenderer } from "./markdown-renderer"; + +const meta = { + title: "blocks/MarkdownRenderer", + component: Story, + parameters: { + nextjs: { + appDirectory: true, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: {}, +}; + +export const DisableCodeHighlight: Story = { + args: { + disableCodeHighlight: true, + }, +}; + +export const SkipHTML: Story = { + args: { + skipHtml: true, + }, +}; + +const markdownExample = `\ +# Heading 1 +## Heading 2 +### Heading 3 +#### Heading 4 +##### Heading 5 +###### Heading 6 + + +This a paragraph + +This is another paragraph + +This a very long paragraph lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec pur us. Donec euismod, nunc nec vehicula. +ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec pur us. Donec euismod, nunc nec vehicula. + + +## Empasis + +*Italic text* +_Also italic text_ + +**Bold text** +__Also bold text__ + +***Bold and italic*** +___Also bold and italic___ + +## Blockquote +> This is a blockquote. + +## Lists + +### Unordered list +- Item 1 +- Item 2 + - Nested Item 1 + - Nested Item 2 + +### Ordered list +1. First item +2. Second item + 1. Sub-item 1 + 2. Sub-item 2 + +### Mixed Nested lists + +- Item 1 +- Item 2 + 1. Sub-item 1 + 2. Sub-item 2 + + +1. First item +2. Second item + - Sub-item 1 + - Sub-item 2 + +### Code +This a a paragraph with some \`inlineCode()\` + +This a \`const longerCodeSnippet = "Example. This should be able to handle line breaks as well, it should not be overflowing the page";\` + +\`\`\`javascript +// Code block with syntax highlighting +function example() { + console.log("Hello, world!"); +} +\`\`\` + +### Links +[thirdweb](https://www.thirdweb.com) + +### Images +![Alt text](https://picsum.photos/2000/500) + + +### Horizontal Rule + + + +--- + + + +### Tables +| Header 1 | Header 2 | Header 3 | +|----------|----------|----------| +| Row 1 | Data | More Data| +| Row 2 | Data | More Data| + + +| Column 1 | Column 2 | Column 3 | Column 4 | Column 5 | +|--------------|------------------|--------------|------------------|--------------| +| Row 1 Cell 1 | Row 1 Cell 2 | Row 1 Cell 3 | Row 1 Cell 4 | Row 1 Cell 5 | +| Row 2 Cell 1 | Row 2 Cell 2 | Row 2 Cell 3 | Row 2 Cell 4 | Row 2 Cell 5 | +| Row 3 Cell 1 | Row 3 Cell 2 | Row 3 Cell 3 | Row 3 Cell 4 | Row 3 Cell 5 | +| Row 4 Cell 1 | Row 4 Cell 2 | Row 4 Cell 3 | Row 4 Cell 4 | Row 4 Cell 5 | +| Row 5 Cell 1 | Row 5 Cell 2 | Row 5 Cell 3 | Row 5 Cell 4 | Row 5 Cell 5 | + + +### Task List +- [ ] Task 1 +- [x] Task 2 (completed) + +### Escaping special characters +\\*This text is not italicized.\\* + +### Strikethrough +~~This is strikethrough text.~~ + +### HTML Elements + +
+`; + +function Story(props: { + disableCodeHighlight?: boolean; + skipHtml?: boolean; +}) { + return ( +
+ +
+ ); +} diff --git a/apps/nebula/src/@/components/blocks/MarkdownRenderer/markdown-renderer.tsx b/apps/nebula/src/@/components/blocks/MarkdownRenderer/markdown-renderer.tsx new file mode 100644 index 00000000000..13bf46701a2 --- /dev/null +++ b/apps/nebula/src/@/components/blocks/MarkdownRenderer/markdown-renderer.tsx @@ -0,0 +1,225 @@ +import { CodeClient } from "@/components/ui/code/code.client"; +import { PlainTextCodeBlock } from "@/components/ui/code/plaintext-code"; +import { InlineCode } from "@/components/ui/inline-code"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { cn } from "@/lib/utils"; +import Link from "next/link"; +import { onlyText } from "react-children-utilities"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; + +export const MarkdownRenderer: React.FC<{ + markdownText: string; + className?: string; + code?: { + disableCodeHighlight?: boolean; + ignoreFormattingErrors?: boolean; + className?: string; + }; + inlineCode?: { + className?: string; + }; + p?: { + className?: string; + }; + li?: { + className?: string; + }; + skipHtml?: boolean; +}> = (markdownProps) => { + const { markdownText, className, code } = markdownProps; + const commonHeadingClassName = "mb-2 leading-5 font-semibold tracking-tight"; + + return ( +
+ ( +

+ ), + + h2: (props) => ( +

+ ), + + h3: (props) => ( +

+ ), + + h4: (props) => ( +

+ ), + + h5: (props) => ( +
+ ), + + h6: (props) => ( +

+ ), + + a: (props) => ( + + ), + + hr: (props) => ( +


+ ), + + code: ({ ...props }) => { + const codeStr = onlyText(props.children); + + if (props?.className || codeStr.length > 100) { + if (code?.disableCodeHighlight || !props.className) { + return ( +
+ {/* @ts-expect-error - TODO: fix this */} + +
+ ); + } + const language = props.className.replace("language-", ""); + return ( +
+ +
+ ); + } + + return ( + + ); + }, + + p: (props) => ( +

+ ), + + table: (props) => ( +

+ + + + + ), + + th: ({ children: c, ...props }) => ( + + {c} + + ), + + td: (props) => ( + + ), + thead: (props) => , + tbody: (props) => , + tr: (props) => , + ul: (props) => { + return ( +
+ + + Invoice + Status + Method + Amount + + + + {invoices.map((invoice) => ( + + + + {invoice.invoice} + + + + {invoice.paymentStatus} + + {invoice.paymentMethod} + + {invoice.totalAmount} + + + ))} + + + {props.footer && ( + + + Total + $2,500.00 + + + )} +
+
+ ); +} diff --git a/apps/nebula/src/@/components/ui/table.tsx b/apps/nebula/src/@/components/ui/table.tsx new file mode 100644 index 00000000000..5c586685eaf --- /dev/null +++ b/apps/nebula/src/@/components/ui/table.tsx @@ -0,0 +1,156 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; +import { ScrollShadow } from "./ScrollShadow/ScrollShadow"; + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +Table.displayName = "Table"; + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); + +TableHeader.displayName = "TableHeader"; + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableBody.displayName = "TableBody"; + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className, + )} + {...props} + /> +)); +TableFooter.displayName = "TableFooter"; + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes & { + /** + * Contain the absolutely position elements inside the row with position:relative + transform:translate(0) + * transform:translate(0) is required because position:relative on tr element does not work on webkit + */ + linkBox?: boolean; + } +>(({ className, linkBox, ...props }, ref) => ( + +)); +TableRow.displayName = "TableRow"; + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableHead.displayName = "TableHead"; + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableCell.displayName = "TableCell"; + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableCaption.displayName = "TableCaption"; + +function TableContainer(props: { + children: React.ReactNode; + className?: string; + scrollableContainerClassName?: string; +}) { + return ( + + {props.children} + + ); +} + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, + TableContainer, +}; diff --git a/apps/nebula/src/@/components/ui/tabs.tsx b/apps/nebula/src/@/components/ui/tabs.tsx new file mode 100644 index 00000000000..0fe8f90b0cd --- /dev/null +++ b/apps/nebula/src/@/components/ui/tabs.tsx @@ -0,0 +1,210 @@ +"use client"; + +import { useIsomorphicLayoutEffect } from "@/lib/useIsomorphicLayoutEffect"; +import { cn } from "@/lib/utils"; +import Link from "next/link"; +import { useCallback, useRef, useState } from "react"; +import { ScrollShadow } from "./ScrollShadow/ScrollShadow"; +import { Button } from "./button"; +import { ToolTipLabel } from "./tooltip"; + +export type TabLink = { + name: React.ReactNode; + href: string; + isActive: boolean; + isDisabled?: boolean; +}; + +export function TabLinks(props: { + links: TabLink[]; + className?: string; + tabContainerClassName?: string; + shadowColor?: string; + scrollableClassName?: string; + bottomLineClassName?: string; +}) { + const { containerRef, lineRef, activeTabRef } = + useUnderline(); + + return ( +
+ {/* Bottom line */} +
+ + +
+ {props.links.map((tab) => { + return ( + + ); + })} +
+ + {/* Active line */} +
+ +
+ ); +} + +export function TabButtons(props: { + tabs: { + name: React.ReactNode; + onClick: () => void; + isActive: boolean; + isDisabled?: boolean; + icon?: React.FC<{ className?: string }>; + toolTip?: string; + }[]; + tabClassName?: string; + activeTabClassName?: string; + tabContainerClassName?: string; + containerClassName?: string; + shadowColor?: string; + tabIconClassName?: string; + hideBottomLine?: boolean; +}) { + const { containerRef, lineRef, activeTabRef } = + useUnderline(); + + return ( +
+ {/* Bottom line */} + {!props.hideBottomLine && ( +
+ )} + + +
+ {props.tabs.map((tab, index) => { + return ( + + + + ); + })} +
+ + {/* Active line */} +
+ +
+ ); +} + +function useUnderline() { + const containerRef = useRef(null); + const lineRef = useRef(null); + const [activeTabEl, setActiveTabEl] = useState(null); + + const activeTabRef = useCallback((el: El | null) => { + setActiveTabEl(el); + }, []); + + useIsomorphicLayoutEffect(() => { + function update() { + if (activeTabEl && containerRef.current && lineRef.current) { + const containerRect = containerRef.current.getBoundingClientRect(); + const lineEl = lineRef.current; + const rect = activeTabEl.getBoundingClientRect(); + const containerPaddingLeft = + containerRect.left - containerRef.current.offsetLeft; + lineEl.style.width = `${rect.width}px`; + lineEl.style.transform = `translateX(${ + rect.left - containerPaddingLeft + }px)`; + setTimeout(() => { + lineEl.style.transition = "transform 0.3s, width 0.3s"; + }, 0); + } else if (lineRef.current) { + lineRef.current.style.width = "0px"; + } + } + + update(); + let resizeObserver: ResizeObserver | undefined = undefined; + + if (containerRef.current) { + resizeObserver = new ResizeObserver(() => { + setTimeout(() => { + update(); + }, 100); + }); + resizeObserver.observe(containerRef.current); + } + + // add event listener for resize + window.addEventListener("resize", update); + return () => { + window.removeEventListener("resize", update); + resizeObserver?.disconnect(); + }; + }, [activeTabEl]); + + return { containerRef, lineRef, activeTabRef }; +} diff --git a/apps/nebula/src/@/components/ui/text-shimmer.tsx b/apps/nebula/src/@/components/ui/text-shimmer.tsx new file mode 100644 index 00000000000..7355cb8c18a --- /dev/null +++ b/apps/nebula/src/@/components/ui/text-shimmer.tsx @@ -0,0 +1,23 @@ +import { cn } from "../../lib/utils"; + +export function TextShimmer(props: { + text: string; + className?: string; +}) { + return ( +
+

+ {props.text} +

+
+ ); +} diff --git a/apps/nebula/src/@/components/ui/textarea.tsx b/apps/nebula/src/@/components/ui/textarea.tsx new file mode 100644 index 00000000000..f0b1229f281 --- /dev/null +++ b/apps/nebula/src/@/components/ui/textarea.tsx @@ -0,0 +1,48 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +export interface TextareaProps + extends React.TextareaHTMLAttributes {} + +const Textarea = React.forwardRef( + ({ className, ...props }, ref) => { + return ( +