diff --git a/package.json b/package.json index 5da3d7e20..897904740 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@noble/hashes": "^1.3.2", "@rainbow-me/rainbowkit": "2.1.2", "@sentry/nextjs": "7.43.x", + "@splidejs/react-splide": "^0.7.12", "@svgr/webpack": "^8.1.0", "@tanstack/query-persist-client-core": "5.22.2", "@tanstack/query-sync-storage-persister": "5.22.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e8c72c86..094aaeed1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: '@sentry/nextjs': specifier: 7.43.x version: 7.43.0(encoding@0.1.13)(next@13.5.6(@babel/core@7.24.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.91.0(esbuild@0.17.19)) + '@splidejs/react-splide': + specifier: ^0.7.12 + version: 0.7.12 '@svgr/webpack': specifier: ^8.1.0 version: 8.1.0(typescript@5.4.5) @@ -293,7 +296,7 @@ importers: version: 0.3.9 eslint-plugin-import: specifier: ^2.28.1 - version: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0))(eslint@8.50.0) + version: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0) eslint-plugin-jsx-a11y: specifier: ^6.7.1 version: 6.8.0(eslint@8.50.0) @@ -2947,6 +2950,12 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@splidejs/react-splide@0.7.12': + resolution: {integrity: sha512-UfXH+j47jsMc4x5HA/aOwuuHPqn6y9+ZTNYPWDRD8iLKvIVMZlzq2unjUEvyDAU+TTVPZOXkG2Ojeoz0P4AkZw==} + + '@splidejs/splide@4.1.4': + resolution: {integrity: sha512-5I30evTJcAJQXt6vJ26g2xEkG+l1nXcpEw4xpKh0/FWQ8ozmAeTbtniVtVmz2sH1Es3vgfC4SS8B2X4o5JMptA==} + '@stablelib/aead@1.0.1': resolution: {integrity: sha512-q39ik6sxGHewqtO0nP4BuSe3db5G1fEJE8ukvngS2gLkBXyy6E7pLubhbYgnkDFv6V8cWaxcE4Xn0t6LWcJkyg==} @@ -13629,6 +13638,12 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} + '@splidejs/react-splide@0.7.12': + dependencies: + '@splidejs/splide': 4.1.4 + + '@splidejs/splide@4.1.4': {} + '@stablelib/aead@1.0.1': {} '@stablelib/binary@1.0.1': @@ -16637,7 +16652,7 @@ snapshots: dependencies: confusing-browser-globals: 1.0.11 eslint: 8.50.0 - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0))(eslint@8.50.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0) object.assign: 4.1.5 object.entries: 1.1.8 semver: 6.3.1 @@ -16648,13 +16663,13 @@ snapshots: '@typescript-eslint/parser': 6.21.0(eslint@8.50.0)(typescript@5.4.5) eslint: 8.50.0 eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0))(eslint@8.50.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0) eslint-config-airbnb@19.0.4(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint-plugin-jsx-a11y@6.8.0(eslint@8.50.0))(eslint-plugin-react-hooks@4.6.2(eslint@8.50.0))(eslint-plugin-react@7.34.1(eslint@8.50.0))(eslint@8.50.0): dependencies: eslint: 8.50.0 eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0))(eslint@8.50.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.50.0) eslint-plugin-react: 7.34.1(eslint@8.50.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.50.0) @@ -16668,8 +16683,8 @@ snapshots: '@typescript-eslint/parser': 6.21.0(eslint@8.50.0)(typescript@5.4.5) eslint: 8.50.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0))(eslint@8.50.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.50.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.50.0) eslint-plugin-react: 7.34.1(eslint@8.50.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.50.0) @@ -16691,13 +16706,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.50.0): dependencies: debug: 4.3.4(supports-color@5.5.0) enhanced-resolve: 5.16.1 eslint: 8.50.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0))(eslint@8.50.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0))(eslint@8.50.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.50.0))(eslint@8.50.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0) fast-glob: 3.3.2 get-tsconfig: 4.7.5 is-core-module: 2.13.1 @@ -16708,18 +16723,18 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0))(eslint@8.50.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.50.0))(eslint@8.50.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 6.21.0(eslint@8.50.0)(typescript@5.4.5) eslint: 8.50.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.50.0) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0))(eslint@8.50.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -16729,7 +16744,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.50.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0))(eslint@8.50.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.50.0))(eslint@8.50.0) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 diff --git a/public/confetti.png b/public/confetti.png new file mode 100644 index 000000000..eab8f092f Binary files /dev/null and b/public/confetti.png differ diff --git a/src/assets/DAO.svg b/src/assets/DAO.svg new file mode 100644 index 000000000..2d33444de --- /dev/null +++ b/src/assets/DAO.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/social/SocialX.svg b/src/assets/social/SocialX.svg index 6a7b7dfe7..134d9024f 100644 --- a/src/assets/social/SocialX.svg +++ b/src/assets/social/SocialX.svg @@ -1,3 +1,3 @@ - + diff --git a/src/components/pages/migrate/Carousel.tsx b/src/components/pages/migrate/Carousel.tsx new file mode 100644 index 000000000..4c56f4bcd --- /dev/null +++ b/src/components/pages/migrate/Carousel.tsx @@ -0,0 +1,20 @@ +import { Splide, SplideSlide } from '@splidejs/react-splide' +import { ReactNode } from 'react' + +export const Carousel = ({ children }: { children: ReactNode[] }) => { + return ( + + {children.map((child) => ( + {child} + ))} + + ) +} diff --git a/src/components/pages/migrate/MigrationNamesList.tsx b/src/components/pages/migrate/MigrationNamesList.tsx new file mode 100644 index 000000000..9b7f3c6e0 --- /dev/null +++ b/src/components/pages/migrate/MigrationNamesList.tsx @@ -0,0 +1,170 @@ +import { TFunction, useTranslation } from 'react-i18next' +import styled, { css } from 'styled-components' +import { useEnsAvatar } from 'wagmi' + +import { NameWithRelation } from '@ensdomains/ensjs/subgraph' +import { CheckCircleSVG, Colors, DisabledSVG, PlusCircleSVG } from '@ensdomains/thorin' + +import { ensAvatarConfig } from '@app/utils/query/ipfsGateway' +import { formatDurationOfDates } from '@app/utils/utils' + +const tabs = ['eligible', 'ineligible', 'approved'] as const + +type Tab = (typeof tabs)[number] + +const icons: Record = { + eligible: , + ineligible: , + approved: , +} + +const colors: Record = { + eligible: { + fg: 'bluePrimary', + bg: 'blueSurface', + hover: 'blueLight', + }, + ineligible: { + fg: 'redPrimary', + bg: 'redSurface', + hover: 'redLight', + }, + approved: { + fg: 'greenPrimary', + bg: 'greenSurface', + hover: 'greenLight', + }, +} + +const Container = styled.div( + ({ theme }) => css` + display: flex; + flex-direction: column; + gap: ${theme.space['2']}; + `, +) + +const TabsContainer = styled.div( + ({ theme }) => css` + display: flex; + flex-direction: row; + gap: ${theme.space['4']}; + padding: ${theme.space['0.5']}; + border-radius: ${theme.radii['2xLarge']}; + background-color: ${theme.colors.background}; + border: 4px solid ${theme.colors.background}; + `, +) + +const TabButton = styled.button<{ $isActive?: boolean; tab: Tab }>( + ({ theme, $isActive, tab }) => css` + width: ${theme.space.full}; + padding: 0 ${theme.space['4']}; + height: ${theme.space['12']}; + border-radius: ${theme.radii.large}; + color: ${$isActive ? theme.colors[colors[tab].fg] : theme.colors.textTertiary}; + background-color: ${$isActive ? theme.colors[colors[tab].bg] : theme.colors.background}; + cursor: pointer; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + font-weight: ${theme.fontWeights.bold}; + gap: ${theme.space['2']}; + + transition-property: background-color; + transition-duration: ${theme.transitionDuration['150']}; + transition-timing-function: ${theme.transitionTimingFunction.inOut}; + + &:hover { + background-color: ${$isActive + ? theme.colors[colors[tab].hover] + : theme.colors[colors[tab].bg]}; + } + `, +) + +const NamesGrid = styled.div( + ({ theme }) => css` + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: ${theme.space['2']}; + `, +) + +const NameCard = styled.div( + ({ theme }) => css` + background: ${theme.colors.background}; + border-radius: ${theme.radii['2xLarge']}; + padding: ${theme.space['8']}; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + + & > span:nth-of-type(1) { + font-weight: ${theme.fontWeights.bold}; + } + & > span:nth-of-type(2) { + color: ${theme.colors.textTertiary}; + font-size: ${theme.fontSizes.small}; + } + + & > img { + border-radius: ${theme.radii.full}; + } + `, +) + +const nameListTabs = ['eligible', 'ineligible', 'approved'] as const + +export type NameListTab = (typeof nameListTabs)[number] + +const MigrationName = ({ name, t }: { name: NameWithRelation; t: TFunction }) => { + const now = new Date() + const { data: avatar } = useEnsAvatar({ ...ensAvatarConfig, name: name.name! }) + + const expiresIn = formatDurationOfDates({ + startDate: now, + endDate: new Date(name.expiryDate?.date!), + t, + }).split(', ')[0] + + return ( + + {avatar && } + {name.name} + Expires in {expiresIn} + + ) +} + +export const MigrationNamesList = ({ + activeTab, + setTab, + names, +}: { + activeTab: NameListTab + names: NameWithRelation[] + setTab: (tab: NameListTab) => void +}) => { + const { t } = useTranslation('migrate') + + return ( + + + {tabs.map((tab) => ( + setTab(tab)}> + {icons[tab]} {t(`migration-list.${tab}`)} + + ))} + + + {names.map((name) => ( + + ))} + + + ) +} diff --git a/src/hooks/migration/useApprovedNamesForMigration.ts b/src/hooks/migration/useApprovedNamesForMigration.ts new file mode 100644 index 000000000..d9a6bfe8c --- /dev/null +++ b/src/hooks/migration/useApprovedNamesForMigration.ts @@ -0,0 +1,28 @@ +import { erc721Abi } from 'viem' +import { usePublicClient, useReadContracts } from 'wagmi' + +import { getChainContractAddress } from '@ensdomains/ensjs/contracts' +import { NameWithRelation } from '@ensdomains/ensjs/subgraph' + +export const useApprovedNamesForMigration = ({ + names, +}: { + names: NameWithRelation[] +}): NameWithRelation[] => { + const client = usePublicClient() + + const { data } = useReadContracts({ + contracts: names.map((name) => ({ + address: getChainContractAddress({ + client, + contract: name.wrappedOwner ? 'ensNameWrapper' : 'ensBaseRegistrarImplementation', + }), + abi: erc721Abi, + functionName: 'isApprovedForAll', + args: ['contract-go-here', true], + })), + multicallAddress: getChainContractAddress({ client, contract: 'multicall3' }), + }) + + return names.filter((d, i) => Boolean(data?.[i].result)) +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 3c5d9dfb9..294ed4587 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,6 +1,7 @@ import { lightTheme, RainbowKitProvider, Theme } from '@rainbow-me/rainbowkit' import '@rainbow-me/rainbowkit/styles.css' +import '@splidejs/react-splide/css' import { NextPage } from 'next' import type { AppProps } from 'next/app' diff --git a/src/pages/migrate.tsx b/src/pages/migrate.tsx new file mode 100644 index 000000000..38956a0ee --- /dev/null +++ b/src/pages/migrate.tsx @@ -0,0 +1,560 @@ +/* eslint-disable @next/next/no-img-element */ +import { useConnectModal } from '@rainbow-me/rainbowkit' +import Head from 'next/head' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled, { css } from 'styled-components' +import { match } from 'ts-pattern' +import { useAccount } from 'wagmi' + +import { GetNamesForAddressParameters } from '@ensdomains/ensjs/subgraph' +import { + Banner, + Button, + Card, + GasPumpSVG, + InfoCircleSVG, + KeySVG, + QuestionBubbleSVG, + QuestionCircleSVG, + RightArrowSVG, + SpannerAltSVG, + Typography, + UpCircleSVG, + WalletSVG, +} from '@ensdomains/thorin' + +import { Carousel } from '@app/components/pages/migrate/Carousel' +import { MigrationNamesList, NameListTab } from '@app/components/pages/migrate/MigrationNamesList' +import { useNamesForAddress } from '@app/hooks/ensjs/subgraph/useNamesForAddress' +import { useApprovedNamesForMigration } from '@app/hooks/migration/useApprovedNamesForMigration' +import { useQueryParameterState } from '@app/hooks/useQueryParameterState' +import { makeIntroItem } from '@app/transaction-flow/intro' +import { createTransactionItem, TransactionData } from '@app/transaction-flow/transaction' +import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' + +import DAOSVG from '../assets/DAO.svg' +import SocialX from '../assets/social/SocialX.svg' + +const Main = styled.main( + ({ theme }) => css` + display: flex; + flex-direction: column; + gap: ${theme.space['16']}; + `, +) + +const Header = styled.header<{ $isLanding: boolean }>( + ({ theme, $isLanding }) => css` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: ${theme.space['4']}; + gap: ${theme.space['4']}; + min-height: ${$isLanding ? '530px' : 'unset'}; + `, +) + +const Heading = styled.h1( + ({ theme }) => css` + font-size: 52px; + font-size: 850; + color: ${theme.colors.textPrimary}; + text-align: center; + line-height: 104%; + & > span { + font-weight: inherit; + font-size: inherit; + line-height: inherit; + background: linear-gradient(90deg, #199c75 2.87%, #9b9ba7 97.95%); + background-clip: text; + -webkit-text-fill-color: transparent; + } + @media (min-width: 360px) { + font-size: 60px; + } + @media (min-width: 640px) { + font-size: 76px; + } + `, +) + +const ButtonContainer = styled.div( + ({ theme }) => css` + display: flex; + gap: ${theme.space['2']}; + flex-direction: column; + + @media (min-width: 360px) { + flex-direction: row; + } + `, +) + +const Caption = styled(Typography)` + text-align: center; + max-width: 538px; +` + +const PartnershipAnnouncement = styled.div( + ({ theme }) => css` + width: ${theme.space.full}; + padding: ${theme.space['4']}; + background-color: ${theme.colors.backgroundPrimary}; + border-radius: ${theme.radii['4xLarge']}; + font-size: ${theme.fontSizes.body}; + font-weight: ${theme.fontWeights.bold}; + display: flex; + justify-content: space-between; + & > a { + color: ${theme.colors.greenDim}; + cursor: pointer; + } + & > a:hover { + color: ${theme.colors.green}; + } + @media (min-width: 640px) { + border-radius: ${theme.radii['3xLarge']}; + } + `, +) + +const CenteredCard = styled(Card)` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +` + +const CardWithEmoji = styled(CenteredCard)` + padding-top: 83px; + position: relative; + grid-column: 1 / -1; + + img { + position: absolute; + top: -72px; + } +` + +const CardHeader = styled.h3( + ({ theme }) => css` + display: flex; + flex-direction: column; + font-size: ${theme.fontSizes.extraLarge}; + color: ${theme.colors.greenDim}; + font-weight: ${theme.fontWeights.bold}; + gap: ${theme.space['2']}; + align-items: center; + `, +) + +const GridOneToThree = styled.div( + ({ theme }) => css` + display: grid; + grid-template-rows: auto; + gap: ${theme.space['4']}; + text-align: center; + grid-template-columns: 1fr; + + @media (min-width: 640px) { + grid-template-columns: repeat(3, 1fr); + } + `, +) + +const Section = styled.div( + ({ theme }) => css` + display: flex; + flex-direction: column; + gap: ${theme.space['6']}; + h3 { + text-align: center; + } + & > div { + display: flex; + flex-direction: column; + gap: ${theme.space['4']}; + } + & > div > div { + width: 100%; + display: flex; + align-items: center; + } + @media (min-width: 360px) { + & > div { + flex-direction: row; + } + } + `, +) +const Footer = styled(Section)( + ({ theme }) => css` + span { + display: flex; + flex-direction: row; + align-items: center; + gap: ${theme.space['2']}; + color: ${theme.colors.green}; + } + `, +) + +const GetStarted = styled(Section)( + ({ theme }) => css` + & > div button span { + display: flex; + flex-direction: row; + align-items: center; + gap: ${theme.space['2']}; + } + `, +) + +const AnnouncementSlide = ({ title, text }: { title: string; text: string }) => ( + + {text} + +) + +const SlideshowContainer = styled.div( + ({ theme }) => css` + display: flex; + flex-direction: column; + gap: ${theme.space['6']}; + h3 { + text-align: center; + } + `, +) + +const TopNav = styled.div( + ({ theme }) => css` + display: flex; + flex-direction: column; + gap: ${theme.space['6']}; + align-items: center; + `, +) + +const TabManager = styled.div( + ({ theme }) => css` + display: flex; + flex-direction: row; + gap: ${theme.space['4']}; + `, +) + +const tabs = ['ensv2', 'migrations', 'extension'] as const + +const filter: Record = { + eligible: { owner: false, wrappedOwner: true, registrant: true, resolvedAddress: false }, + ineligible: { owner: true, wrappedOwner: false, registrant: false, resolvedAddress: false }, + approved: { owner: false, wrappedOwner: true, registrant: true, resolvedAddress: false }, +} + +type Tab = (typeof tabs)[number] + +export default function Page() { + const { t } = useTranslation('migrate') + + const { isConnected, address } = useAccount() + + const { openConnectModal } = useConnectModal() + + const [currentTab, setTab] = useQueryParameterState('tab', 'ensv2') + + const { createTransactionFlow } = useTransactionFlow() + + const [activeNameListTab, setNameListTab] = useState('eligible') + + const { infiniteData } = useNamesForAddress({ + address, + pageSize: 20, + filter: filter[activeNameListTab], + }) + + const names = infiniteData.filter( + (name) => + name.parentName === 'eth' && + (activeNameListTab === 'ineligible' ? name.registrant !== name.owner : true), + ) + + const approvedNames = useApprovedNamesForMigration({ names }) + + return ( + <> +
+ + + {t('partnership.text')} + + {t('partnership.watch')} + + + + {tabs.map((tab) => ( + + ))} + + +
+ {match(currentTab) + .with('ensv2', () => ( + <> + + {t('title.landing')} + + {t('title.landing')} + {/** @ts-expect-error styled-components don't know how to inherit types */} + {t('caption.ensv2')} + + )) + .with('migrations', () => ( + <> + + {t('title.migration')} + + {t('title.migration')} + {/** @ts-expect-error styled-components don't know how to inherit types */} + {t('caption.migration')} + + )) + .with('extension', () => ( + <> + + + {t('title.extension.part1')} {t('title.extension.part2')} + + + + {t('title.extension.part1')} {t('title.extension.part2')} + + + )) + .exhaustive()} + + {match(currentTab) + .with('ensv2', () => ( + + )) + .with('migrations', () => ( + + )) + .with('extension', () => ( + + )) + .exhaustive()} + + + +
+ {match(currentTab) + .with('ensv2', () => ( + <> + + + 🎉 + + {t('accessible.title')} + + {t('accessible.caption')} + + + + + + {t('accessible.gas.title')} + + {t('accessible.gas.text')} + + + + + {t('accessible.control.title')} + + {t('accessible.control.text')} + + + + + {t('accessible.multichain.title')} + + {t('accessible.multichain.text')} + + + + + {t('get-started.title')} + +
+ + + + {t('get-started.upgrade.title')} + + {t('get-started.upgrade.caption')} + + +
+
+ + + {t('announcement.title')} + + + + + + + + + )) + .with('migrations', () => ( + <> + {isConnected ? ( + + ) : null} +
+ + {t('approval-benefits.title')} + +
+ + + + {t('approval-benefits.migration.title')} + + {t('approval-benefits.migration.caption')} + + + + + {t('approval-benefits.no-gas-cost.title')} + + {t('approval-benefits.no-gas-cost.caption')} + +
+
+ + )) + .otherwise(() => ( +

bloop

+ ))} + +
+ + {t('footer.title')} + +
+ + + {t('footer.learn.faq')} + + + {t('footer.learn.plan')} + + + {t('footer.learn.base')} + + + + + {t('footer.support.ticket')} + + + {t('footer.support.twitter')} + + + {t('footer.support.dao')} + + +
+
+
+ + ) +} diff --git a/src/transaction-flow/transaction/approveNameWrapperForMigration.ts b/src/transaction-flow/transaction/approveNameWrapperForMigration.ts new file mode 100644 index 000000000..5e3e85bd9 --- /dev/null +++ b/src/transaction-flow/transaction/approveNameWrapperForMigration.ts @@ -0,0 +1,52 @@ +import type { TFunction } from 'react-i18next' +import { Address, encodeFunctionData } from 'viem' + +import { + getChainContractAddress, + registrySetApprovalForAllSnippet, +} from '@ensdomains/ensjs/contracts' + +import { Transaction, TransactionDisplayItem, TransactionFunctionParameters } from '@app/types' + +type Data = { address: Address } + +const displayItems = ( + { address }: Data, + t: TFunction<'translation', undefined>, +): TransactionDisplayItem[] => [ + { + label: 'address', + value: address, + type: 'address', + }, + { + label: 'action', + value: t('transaction.description.approveNameWrapper'), + }, + { + label: 'info', + value: t('transaction.info.approveNameWrapper'), + }, +] + +const transaction = async ({ client }: TransactionFunctionParameters) => { + return { + to: getChainContractAddress({ + client, + contract: 'ensNameWrapper', + }), + data: encodeFunctionData({ + abi: registrySetApprovalForAllSnippet, + functionName: 'setApprovalForAll', + args: [ + getChainContractAddress({ + client, + contract: 'contract-address-will-go-here', + }), + true, + ], + }), + } +} + +export default { displayItems, transaction } satisfies Transaction diff --git a/src/transaction-flow/transaction/approveRegistrarForMigration.ts b/src/transaction-flow/transaction/approveRegistrarForMigration.ts new file mode 100644 index 000000000..9ffee6155 --- /dev/null +++ b/src/transaction-flow/transaction/approveRegistrarForMigration.ts @@ -0,0 +1,53 @@ +import { TFunction } from 'react-i18next' +import { Address } from 'viem/accounts' +import { encodeFunctionData } from 'viem/utils' + +import { + getChainContractAddress, + registrySetApprovalForAllSnippet, +} from '@ensdomains/ensjs/contracts' + +import type { Transaction, TransactionDisplayItem, TransactionFunctionParameters } from '@app/types' + +type Data = { address: Address } + +const displayItems = ( + { address }: Data, + t: TFunction<'translation', undefined>, +): TransactionDisplayItem[] => [ + { + label: 'address', + value: address, + type: 'address', + }, + { + label: 'action', + value: t('transaction.description.approveEthRegistrar'), + }, + { + label: 'info', + value: t('transaction.info.approveEthRegistrar'), + }, +] + +const transaction = async ({ client }: TransactionFunctionParameters) => { + return { + to: getChainContractAddress({ + client, + contract: 'ensBaseRegistrarImplementation', + }), + data: encodeFunctionData({ + abi: registrySetApprovalForAllSnippet, + functionName: 'setApprovalForAll', + args: [ + getChainContractAddress({ + client, + contract: 'address-will-go-here', + }), + true, + ], + }), + } +} + +export default { displayItems, transaction } satisfies Transaction diff --git a/src/transaction-flow/transaction/index.ts b/src/transaction-flow/transaction/index.ts index 55211a5e5..96a04cc5d 100644 --- a/src/transaction-flow/transaction/index.ts +++ b/src/transaction-flow/transaction/index.ts @@ -1,5 +1,7 @@ import approveDnsRegistrar from './approveDnsRegistrar' import approveNameWrapper from './approveNameWrapper' +import approveNameWrapperForMigration from './approveNameWrapperForMigration' +import approveRegistrarForMigration from './approveRegistrarForMigration' import burnFuses from './burnFuses' import changePermissions from './changePermissions' import claimDnsName from './claimDnsName' @@ -31,6 +33,8 @@ import wrapName from './wrapName' export const transactions = { approveDnsRegistrar, + approveNameWrapperForMigration, + approveRegistrarForMigration, approveNameWrapper, burnFuses, changePermissions,