├── .env.development ├── .env.test ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── comment-coverage.yml │ └── unit-tests.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .prettierrc.json ├── .storybook ├── main.js └── preview.js ├── CODEOWNERS ├── LICENSE.MD ├── README.md ├── __mocks__ ├── fileMock.js ├── next │ └── server.js └── styleMock.js ├── __tests__ ├── api │ ├── addresses │ │ └── [address] │ │ │ └── notifications │ │ │ └── emails │ │ │ └── [emailOrUuid].test.ts │ └── events │ │ ├── _middleware.test.ts │ │ ├── community │ │ └── nominationMessage.test.ts │ │ ├── consumers │ │ ├── communityNFTAwarder.test.ts │ │ ├── discord.test.ts │ │ ├── twitter.test.ts │ │ └── userNotifications.test.ts │ │ ├── cron │ │ ├── monitorEmailSends.test.ts │ │ ├── processNewOnchainEvents.test.ts │ │ └── processTimelyEvents.test.ts │ │ └── updateDiscordMetrics.test.ts ├── components │ ├── Banner.test.tsx │ ├── Button.test.tsx │ ├── CollateralInfo.test.tsx │ ├── CommunityActivity.test.tsx │ ├── CommunityInfo.test.tsx │ ├── CreatePageHeader.test.tsx │ ├── Disclosure.test.tsx │ ├── DisplayAddress.test.tsx │ ├── DisplayCurrency.test.tsx │ ├── ErrorBanners.test.tsx │ ├── Explainer.test.tsx │ ├── Form.test.tsx │ ├── GridListSelector.test.tsx │ ├── Input.test.tsx │ ├── LoanCard.test.tsx │ ├── LoanForm.test.tsx │ ├── LoanGalleryLoadMore.test.tsx │ ├── LoanHeader.test.tsx │ ├── LoanTable.test.tsx │ ├── LoanTickets.test.tsx │ ├── Marquee.test.tsx │ ├── Media.test.tsx │ ├── NFTCollateralPicker.test.tsx │ ├── NetworkSelector.test.tsx │ ├── NotificationsModal.test.tsx │ ├── PendingCommunityTransactions.test.tsx │ ├── ProfileActivity.test.tsx │ ├── Select.test.tsx │ └── Toggle.test.tsx ├── hooks │ ├── useBalance.test.ts │ ├── useCommunityGradient.test.tsx │ ├── useGlobalMessages.test.tsx │ ├── useLoanDetails.test.ts │ ├── useLoanUnderwriter.test.ts │ ├── useLoanViewerRole.test.ts │ ├── useNFTs.test.ts │ ├── useNetworkSpecificStyles.test.ts │ └── useTokenMetadata.test.ts └── lib │ ├── account.test.ts │ ├── community.test.ts │ ├── events │ ├── consumers │ │ ├── discord │ │ │ └── formatter.test.ts │ │ ├── twitter │ │ │ └── formatter.test.ts │ │ └── userNotifications │ │ │ ├── emails │ │ │ ├── emails.test.ts │ │ │ └── eventsFormatter.test.ts │ │ │ └── repository.test.ts │ ├── sqs │ │ └── consumer.test.ts │ └── timely │ │ └── timely.test.ts │ ├── getNFTInfo.test.ts │ ├── loans │ ├── loanById.test.ts │ ├── subgraph │ │ └── subgraphLoanById.test.ts │ └── utils.test.ts │ ├── metrics │ └── repository.test.ts │ └── nftCollectionStats │ └── reservoir.test.ts ├── abis ├── CommunityNFT.json ├── ERC20.json ├── ERC721.json ├── MockDAI.json ├── MockPUNK.json └── NFTLoanFacilitator.json ├── codegen ├── community.yml ├── eip721.yml └── nftLoans.yml ├── components ├── AdvancedSearch │ ├── AdvancedSearch.module.css │ ├── AdvancedSearch.tsx │ ├── CollateralInput.tsx │ ├── Header.tsx │ ├── LendabilityDropdown.tsx │ ├── LoanAssetDropdown.tsx │ ├── LoanNumericInput.tsx │ ├── LoanStatusButtons.tsx │ ├── SearchTextInput.tsx │ ├── SortDropdown.tsx │ └── index.ts ├── ApplicationProviders │ ├── ApplicationProviders.tsx │ └── index.ts ├── Banner │ ├── Banner.module.css │ ├── Banner.tsx │ ├── index.ts │ └── messages │ │ ├── WrongNetwork.tsx │ │ └── index.ts ├── Button │ ├── AllowButton.tsx │ ├── Button.module.css │ ├── Button.tsx │ ├── ButtonLink.tsx │ ├── CompletedButton.tsx │ ├── TextButton.tsx │ ├── TransactionButton.tsx │ └── index.ts ├── Carousel │ ├── Carousel.module.css │ ├── Carousel.tsx │ ├── index.ts │ └── slides.ts ├── CollateralInfo │ ├── CollateralInfo.module.css │ ├── CollateralInfo.tsx │ └── index.ts ├── CommunityActivity │ ├── CommunityActivity.module.css │ ├── CommunityActivity.tsx │ └── index.ts ├── CommunityHeader │ ├── CommunityHeader.module.css │ ├── CommunityHeader.tsx │ ├── PlaceholderBunn.tsx │ ├── index.ts │ ├── optimism-circle.png │ └── placeholder-bunn.svg ├── CommunityInfo │ ├── CommunityInfo.module.css │ ├── CommunityInfo.tsx │ ├── GeneratedSections.tsx │ ├── XPFieldset.tsx │ ├── bunns │ │ ├── alpha snake.svg │ │ ├── gold chain contributor.svg │ │ ├── gold key multisig.svg │ │ ├── pink protocol lei.svg │ │ ├── preserver of wisdom.svg │ │ ├── purple community scarf.svg │ │ ├── super chain contributor.svg │ │ ├── super community scarf.svg │ │ └── super protocol lei.svg │ └── index.ts ├── CommunityPageContent │ ├── CommunityPageContent.module.css │ ├── CommunityPageContent.tsx │ └── index.ts ├── ConnectWallet │ ├── ConnectWallet.module.css │ ├── ConnectWallet.tsx │ └── index.ts ├── CreatePageHeader │ ├── AuthorizeNFTButton.tsx │ ├── CreateFormData.ts │ ├── CreatePageForm.tsx │ ├── CreatePageHeader.module.css │ ├── CreatePageHeader.tsx │ ├── Explainer.tsx │ ├── SelectNFTButton.tsx │ ├── createPageFormMachine.ts │ ├── createPageFormSchema.ts │ └── index.ts ├── Custom404 │ ├── Custom404.module.css │ ├── Custom404.tsx │ └── index.ts ├── Custom500 │ ├── Custom500.module.css │ ├── Custom500.tsx │ └── index.ts ├── DescriptionList │ ├── DescriptionList.module.css │ ├── DescriptionList.tsx │ └── index.ts ├── Disclosure │ ├── Disclosure.module.css │ ├── Disclosure.tsx │ └── index.ts ├── DisplayAddress │ ├── DisplayAddress.tsx │ └── index.ts ├── DisplayCurrency │ ├── DisplayCurrency.tsx │ └── index.ts ├── ErrorBanners │ ├── ErrorBanners.module.css │ ├── ErrorBanners.tsx │ └── index.ts ├── EtherscanLink │ ├── EtherscanLink.tsx │ └── index.ts ├── Explainer │ ├── Explainer.module.css │ ├── Explainer.tsx │ └── index.ts ├── Fieldset │ ├── Fieldset.module.css │ ├── Fieldset.tsx │ └── index.ts ├── Footer │ ├── Footer.module.css │ ├── Footer.tsx │ └── index.ts ├── Form │ ├── Form.module.css │ ├── Form.tsx │ └── index.ts ├── GridListSelector │ ├── GridListSelector.module.css │ ├── GridListSelector.tsx │ └── index.ts ├── Header │ ├── Header.module.css │ ├── Header.tsx │ └── index.ts ├── HeaderInfo │ ├── HeaderInfo.module.css │ ├── HeaderInfo.tsx │ ├── borrower-bunny.png │ ├── index.ts │ ├── investigative-bunny.png │ └── lender-bunny.png ├── Icons │ ├── Arrow.tsx │ ├── Checkmark.tsx │ ├── Chevron.tsx │ ├── CoinbaseWallet.tsx │ ├── Cross.tsx │ ├── GridView.tsx │ ├── ListView.tsx │ ├── Metamask.tsx │ ├── ShimmerPlus.tsx │ └── WalletConnect.tsx ├── Input │ ├── Input.module.css │ ├── Input.tsx │ └── index.ts ├── LoanCard │ ├── LoanCard.module.css │ ├── LoanCard.tsx │ └── index.ts ├── LoanForm │ ├── Balance │ │ ├── Balance.tsx │ │ └── index.ts │ ├── LoanForm.module.css │ ├── LoanForm.tsx │ ├── LoanFormAwaiting │ │ ├── Explainer.tsx │ │ ├── LoanFormAwaiting.tsx │ │ ├── index.ts │ │ └── loanFormAwaitingMachine.ts │ ├── LoanFormBetterTerms │ │ ├── Explainer.tsx │ │ ├── LoanFormBetterTerms.tsx │ │ ├── index.ts │ │ └── loanFormBetterTermsMachine.ts │ ├── LoanFormData.ts │ ├── LoanFormDisclosure │ │ ├── LoanFormDisclosure.tsx │ │ └── index.ts │ ├── LoanFormEarlyClosure │ │ ├── LoanFormEarlyClosure.tsx │ │ └── index.ts │ ├── LoanFormRepay │ │ ├── Explainer.tsx │ │ ├── LoanFormRepay.tsx │ │ └── index.ts │ ├── LoanFormSeizeCollateral │ │ ├── Explainer.tsx │ │ ├── LoanFormSeizeCollateral.tsx │ │ └── index.ts │ ├── LoanOfferBetterTermsDisclosure │ │ ├── LoanOfferBetterTermsDisclosure.tsx │ │ └── index.ts │ ├── index.ts │ ├── loanPageFormSchema.ts │ └── strings.ts ├── LoanGalleryLoadMore │ ├── LoanGalleryLoadMore.module.css │ ├── LoanGalleryLoadMore.tsx │ └── index.ts ├── LoanHeader │ ├── Explainer.tsx │ ├── LoanHeader.module.css │ ├── LoanHeader.tsx │ └── index.ts ├── LoanInfo │ ├── LoanInfo.module.css │ ├── LoanInfo.tsx │ └── index.ts ├── LoanTable │ ├── LoanTable.module.css │ ├── LoanTable.tsx │ └── index.ts ├── LoanTermsDisclosure │ ├── LoanTermsDisclosure.module.css │ ├── LoanTermsDisclosure.tsx │ └── index.ts ├── LoanTickets │ ├── LoanTickets.module.css │ ├── LoanTickets.tsx │ └── index.ts ├── Logo │ ├── Logo.module.css │ ├── Logo.tsx │ ├── borked-bunny.png │ ├── index.ts │ └── pepe-bunny-line.png ├── Marquee │ ├── Marquee.module.css │ ├── Marquee.tsx │ └── index.ts ├── Media │ ├── Fallback.tsx │ ├── Media.module.css │ ├── Media.tsx │ ├── NFTMedia.tsx │ └── index.ts ├── Modal │ ├── Modal.module.css │ ├── Modal.tsx │ └── index.ts ├── NFTCollateralPicker │ ├── NFTCollateralPicker.module.css │ └── NFTCollateralPicker.tsx ├── NFTExchangeLink │ ├── NFTExchangeLink.tsx │ └── index.ts ├── NetworkSelector │ ├── NetworkSelector.module.css │ ├── NetworkSelector.tsx │ └── index.ts ├── NotificationsModal │ ├── NotificationsModal.module.css │ ├── NotificationsModal.tsx │ └── index.ts ├── OpenGraph │ ├── OpenGraph.tsx │ └── index.ts ├── PawnArt │ ├── PawnArt.tsx │ └── index.ts ├── PendingCommunityTransactions │ ├── PendingCommunityTransactions.module.css │ └── PendingCommunityTransactions.tsx ├── Profile │ ├── BorrowerLenderBubble.tsx │ ├── NextLoanDueCountdown.tsx │ ├── ProfileHeader.tsx │ ├── ProfileLoans.tsx │ ├── borrowerLenderBubble.module.css │ └── profile.module.css ├── ProfileActivity │ ├── ProfileActivity.module.css │ ├── ProfileActivity.tsx │ └── index.ts ├── RepaymentInfo │ ├── RepaymentInfo.module.css │ ├── RepaymentInfo.tsx │ └── index.ts ├── Select │ ├── Select.tsx │ └── index.ts ├── TicketHistory │ ├── ParsedEvent.tsx │ ├── TicketHistory.module.css │ ├── TicketHistory.tsx │ └── index.ts ├── Toggle │ ├── Toggle.module.css │ ├── Toggle.tsx │ └── index.ts └── layouts │ ├── AppWrapper.module.css │ ├── AppWrapper.tsx │ ├── FormWrapper.module.css │ ├── FormWrapper.tsx │ ├── ThreeColumn.module.css │ ├── ThreeColumn.tsx │ └── TwelveColumn │ ├── TwelveColumn.module.css │ ├── TwelveColumn.tsx │ └── index.ts ├── contracts └── NFTPawnShop.json ├── docker-compose.yml ├── global-setup.js ├── graphql ├── community │ ├── accessoriesQuery.graphql │ └── communityAccountQuery.graphql ├── eip721 │ └── queries │ │ └── useNFTquery.graphql ├── nftLoans │ ├── fragments │ │ ├── allLoanProperties.graphql │ │ └── eventProperties.graphql │ └── queries │ │ ├── allEventsQueries.graphql │ │ ├── allLoansQuery.graphql │ │ ├── eventsForAddress.graphql │ │ ├── eventsForLoan.graphql │ │ ├── homepageSearchQuery.graphql │ │ ├── homepageSearchQueryWithoutLender.graphql │ │ ├── loanById.graphql │ │ └── notificationEvents.graphql └── nftSales │ ├── fragments │ └── allSaleProperties.graphql │ └── queries │ └── salesByAddress.graphql ├── hooks ├── useBalance │ ├── index.ts │ └── useBalance.ts ├── useCachedRates │ └── useCachedRates.tsx ├── useCommunityGradient │ ├── index.ts │ └── useCommunityGradient.tsx ├── useConfig │ ├── index.ts │ └── useConfig.tsx ├── useGlobalMessages │ ├── index.ts │ └── useGlobalMessages.tsx ├── useHasCollapsedHeaderInfo │ ├── index.ts │ └── useHasCollapsedHeaderInfo.tsx ├── useKonami │ ├── index.ts │ └── useKonami.ts ├── useLTV │ ├── index.ts │ └── useLTV.ts ├── useLoanDetails │ ├── index.ts │ └── useLoanDetails.ts ├── useLoanUnderwriter │ ├── index.ts │ └── useLoanUnderwriter.tsx ├── useLoanViewerRole │ ├── index.ts │ └── useLoanViewerRole.ts ├── useNFTs │ ├── index.ts │ └── useNFTs.ts ├── useNetworkSpecificStyles │ ├── index.ts │ └── useNetworkSpecificStyles.ts ├── useOnClickOutside │ ├── index.ts │ └── useOnClickOutside.ts ├── useOnScreenRef.ts ├── usePaginatedLoans.ts ├── useTimestamp │ ├── index.ts │ └── useTimestamp.tsx └── useTokenMetadata │ ├── index.ts │ └── useTokenMetadata.ts ├── jest.config.js ├── jest.setup.js ├── lib ├── account.ts ├── authentication.ts ├── authorizations │ └── authorizeCurrency.ts ├── aws │ └── config.ts ├── coingecko.ts ├── community.ts ├── communityNFT │ └── multisig.ts ├── config.ts ├── constants.ts ├── contracts.ts ├── duration.ts ├── eip721Subraph.ts ├── erc20Helper.ts ├── eventTransformers.ts ├── events │ ├── consumers │ │ ├── attachmentsHelper.ts │ │ ├── discord │ │ │ ├── attachments.ts │ │ │ ├── bot.ts │ │ │ ├── formatter.ts │ │ │ └── shared.ts │ │ ├── formattingHelpers.ts │ │ ├── getNftInfoForAttachment.ts │ │ ├── twitter │ │ │ ├── api.ts │ │ │ ├── attachments.ts │ │ │ └── formatter.ts │ │ └── userNotifications │ │ │ ├── emails │ │ │ ├── emails.ts │ │ │ ├── eventsFormatter.ts │ │ │ ├── genericFormatter.ts │ │ │ ├── mjml.ts │ │ │ └── ses.ts │ │ │ ├── repository.ts │ │ │ └── shared.ts │ ├── sns │ │ ├── helpers.ts │ │ └── push.ts │ ├── sqs │ │ ├── consumer.ts │ │ └── helpers.ts │ └── timely │ │ └── timely.ts ├── eventsHelpers.ts ├── fetchWithTimeout.ts ├── getNFTInfo.ts ├── interest.ts ├── loanAssets.ts ├── loans │ ├── collateralSaleInfo.ts │ ├── loanById.ts │ ├── loans.ts │ ├── node │ │ ├── nodeLoanById.ts │ │ └── nodeLoanEventsById.ts │ ├── profileHeaderMethods.ts │ ├── subgraph │ │ ├── getAllLoansEventsForAddress.ts │ │ ├── subgraphLoanById.ts │ │ ├── subgraphLoanEventsById.ts │ │ └── subgraphLoans.ts │ └── utils.ts ├── metrics │ └── repository.ts ├── mockData.ts ├── mockSubgraphEventsData.ts ├── nftCollectionStats │ ├── index.ts │ ├── mockData.ts │ ├── quixotic.ts │ └── reservoir.ts ├── notifications │ └── script.ts ├── parseSerializedResponse.ts ├── pirsch.ts ├── signedMessages.ts ├── text.ts └── urql.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── 404.tsx ├── 500.tsx ├── _app.tsx ├── _document.tsx ├── _error.js ├── about.tsx ├── api │ ├── events │ │ ├── _middleware.ts │ │ ├── community │ │ │ └── nominationMessage.ts │ │ ├── consumers │ │ │ ├── communityNFTAwarder.ts │ │ │ ├── discord.ts │ │ │ ├── twitter.ts │ │ │ └── userNotifications.ts │ │ ├── cron │ │ │ ├── monitorEmailSends.ts │ │ │ ├── processNewOnchainEvents.ts │ │ │ └── processTimelyEvents.ts │ │ └── updateDiscordMetrics.ts │ ├── network │ │ └── [network] │ │ │ ├── addresses │ │ │ └── [address] │ │ │ │ ├── loans │ │ │ │ └── index.ts │ │ │ │ └── notifications │ │ │ │ └── emails │ │ │ │ └── [emailOrUuid].ts │ │ │ ├── events │ │ │ └── [event] │ │ │ │ └── all.tsx │ │ │ ├── loanAssets.ts │ │ │ ├── loans │ │ │ ├── [id].tsx │ │ │ ├── all.tsx │ │ │ ├── history │ │ │ │ └── [id].ts │ │ │ └── search.ts │ │ │ └── nftInfo │ │ │ └── [contractAddress] │ │ │ └── [tokenId].ts │ └── sharedTypes.ts ├── community │ ├── [address].tsx │ ├── index.tsx │ └── multisig.tsx └── network │ └── [network] │ ├── index.tsx │ ├── loans │ ├── [id].tsx │ ├── create.module.css │ └── create.tsx │ ├── profile │ ├── [address].module.css │ └── [address].tsx │ └── test.tsx ├── prisma ├── migrations │ ├── 20220209173148_init │ │ └── migration.sql │ ├── 20220210183028_varchar │ │ └── migration.sql │ ├── 20220210183615_eth_address_42 │ │ └── migration.sql │ ├── 20220223155259_timestamp_notifications │ │ └── migration.sql │ ├── 20220329220615_uuid │ │ └── migration.sql │ ├── 20220401180609_discord_metrics │ │ └── migration.sql │ ├── 20220419152427_backed_metrics │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── angelbunny-XL.png ├── cal-icon.svg ├── carousel-images │ ├── borrower.png │ ├── buy-out.png │ ├── continued-availability.png │ ├── contract.png │ ├── deposit-requested.png │ ├── duration-restart.png │ ├── funds-sent.png │ ├── loan-matures.png │ ├── mint-borrow.png │ ├── not-repay.png │ ├── other-lenders.png │ ├── perpetual-buyouts.png │ ├── potential-lenders.png │ ├── repay.png │ ├── to-buy-out.png │ └── transfer-loan-principal.png ├── favicon.ico ├── graph-square.png ├── legal │ ├── privacy-policy.pdf │ └── terms-of-service.pdf ├── loanAssets │ ├── local.json │ └── rinkeby.json ├── logo.svg └── logos │ ├── backed-bunny.png │ ├── opbunny.png │ └── pbunny.png ├── scripts └── build-nft-info.js ├── sentry.client.config.js ├── sentry.properties ├── sentry.server.config.js ├── stories ├── Banner.stories.tsx ├── Button.stories.tsx ├── Carousel.stories.tsx ├── DescriptionList.stories.tsx ├── Disclosure.stories.tsx ├── Fallback.stories.tsx ├── Fieldset.stories.tsx ├── Input.stories.tsx ├── LoanCard.stories.tsx ├── LoanHeader.stories.tsx ├── LoanInfo.stories.tsx ├── Marquee.stories.tsx ├── Modal.stories.tsx ├── NFTCollateralPicker.stories.tsx ├── NotificationsModal.stories.tsx ├── ParsedEvent.stories.tsx ├── Select.stories.tsx ├── ThreeColumn.stories.tsx ├── Toggle.stories.tsx ├── helpers.tsx └── urql.stories.tsx ├── styles ├── fonts-maru.css └── global.css ├── tsconfig.json ├── types ├── CollateralMedia.ts ├── Event.ts ├── Loan.ts ├── NFT.ts ├── RawEvent.ts ├── pinata.d.ts └── pirsch.d.ts └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: '14.x' 22 | cache: 'yarn' 23 | - name: Install dependencies 24 | run: yarn 25 | - name: Build app 26 | run: yarn run build 27 | - name: Spin up Postgres 28 | run: docker-compose up -d postgres && yarn run prisma-migrate:test 29 | - name: Run tests and calculate coverage 30 | run: yarn run test-coverage 31 | - uses: actions/upload-artifact@v2 32 | with: 33 | name: coverage-summary.json 34 | path: coverage/coverage-summary.json 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | /storybook-static 22 | tsconfig.tsbuildinfo 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env.local 31 | .env.development.local 32 | .env.test.local 33 | .env.production.local 34 | 35 | # vercel 36 | .vercel 37 | 38 | # gen 39 | /abis/types 40 | /types/generated 41 | 42 | # intellij 43 | .idea/ 44 | 45 | # Sentry 46 | .sentryclirc 47 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint 5 | npx lint-staged 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSameLine": true, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | stories: [ 5 | '../stories/**/*.stories.mdx', 6 | '../stories/**/*.stories.@(js|jsx|ts|tsx)', 7 | ], 8 | addons: [ 9 | '@storybook/addon-links', 10 | '@storybook/addon-essentials', 11 | 'storybook-css-modules-preset', 12 | 'storybook-addon-next-router', 13 | ], 14 | webpackFinal: async (config, { configType }) => { 15 | config.resolve.modules = [path.resolve(__dirname, '..'), 'node_modules']; 16 | 17 | return config; 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import '../styles/global.css'; 2 | import '../styles/fonts-maru.css'; 3 | import 'normalize.css'; 4 | import * as NextImage from 'next/image'; 5 | import { RouterContext } from 'next/dist/shared/lib/router-context'; 6 | 7 | const OriginalNextImage = NextImage.default; 8 | 9 | // If we don't do this, Storybook will break when rendering things that use Next/Image 10 | Object.defineProperty(NextImage, 'default', { 11 | configurable: true, 12 | value: (props) => , 13 | }); 14 | 15 | export const parameters = { 16 | actions: { argTypesRegex: '^on[A-Z].*' }, 17 | controls: { 18 | matchers: { 19 | color: /(background|color)$/i, 20 | date: /Date$/, 21 | }, 22 | }, 23 | nextRouter: { 24 | Provider: RouterContext.Provider, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @wilsoncusack @cnasc @adamgobes -------------------------------------------------------------------------------- /LICENSE.MD: -------------------------------------------------------------------------------- 1 | Copyright 2021 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # backed-interface 2 | 3 | ![backed](/public/logos/backed-bunny.png 'backed') 4 | 5 | ## Developing 6 | 7 | Install all deps by running `yarn` 8 | 9 | ### Running on rinkeby 10 | 11 | By default, just running `yarn dev` runs the frontend hooked up to the rinkeby testnet. Our app will allow you to mint NFTs and Rinkeby DAI at http://localhost:3000/test. You'll need some Rinkeby eth for gas, you can request a small amount from [Chainlink](https://faucets.chain.link/rinkeby). 12 | 13 | ### Running on other chains 14 | 15 | Coming soon after our deploy to mainnet. 16 | 17 | ### Setting up the notifications DB 18 | 19 | We use postgres + prisma ORM to keep track of which users have requested to receive notifications (email, telegram, etc.). In order to spin up prisma, run `docker-compose up -d postgres`, then, add the following to your `.env.local` 20 | 21 | ``` 22 | DATABASE_URL=postgresql://user:password@localhost:5432/notifications?schema=public 23 | ``` 24 | 25 | Finally, you'll need to apply all our DB migrations to your new local instance. Do this by running `yarn prisma-migrate:test`. 26 | 27 | ### Tests 28 | 29 | `yarn test` 30 | 31 | Note: you should expect to see `__tests/notifications/repository.ts` to fail if you do not have your local Docker postgres running 32 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /__mocks__/next/server.js: -------------------------------------------------------------------------------- 1 | // Jest gives an 'Unexpected token' error for direct imports of NextRequest 2 | // and NextResponse in test files because of the 'export' usage in them. 3 | // See: https://github.com/vercel/next.js/discussions/29750#discussioncomment-1804798 4 | import { NextRequest } from 'next/dist/server/web/spec-extension/request'; 5 | import { NextResponse } from 'next/dist/server/web/spec-extension/response'; 6 | 7 | export { NextRequest, NextResponse }; 8 | -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /__tests__/api/events/community/nominationMessage.test.ts: -------------------------------------------------------------------------------- 1 | import { createMocks } from 'node-mocks-http'; 2 | import { sendBotMessage } from 'lib/events/consumers/discord/bot'; 3 | import handler from 'pages/api/events/community/nominationMessage'; 4 | import { ethers } from 'ethers'; 5 | 6 | const address = ethers.Wallet.createRandom(); 7 | 8 | jest.mock('lib/events/consumers/discord/bot', () => ({ 9 | sendBotMessage: jest.fn(), 10 | })); 11 | 12 | const mockSendBotMessage = sendBotMessage as jest.MockedFunction< 13 | typeof sendBotMessage 14 | >; 15 | 16 | describe('Nomination Discord bot message', () => { 17 | beforeEach(() => { 18 | jest.resetAllMocks(); 19 | }); 20 | it('successfully calls discord bot method', async () => { 21 | mockSendBotMessage.mockResolvedValue(); 22 | 23 | const { req, res } = createMocks({ 24 | method: 'POST', 25 | body: { 26 | ethAddress: address, 27 | category: 'CONTRIBUTOR', 28 | value: 3, 29 | reason: 'Stellar code contribution', 30 | }, 31 | }); 32 | req.body = JSON.stringify(req.body); 33 | await handler(req, res); 34 | 35 | expect(mockSendBotMessage).toHaveBeenCalledTimes(1); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /__tests__/api/events/cron/processNewOnchainEvents.test.ts: -------------------------------------------------------------------------------- 1 | import { main } from 'lib/events/sqs/consumer'; 2 | import { createMocks } from 'node-mocks-http'; 3 | import handler from 'pages/api/events/cron/processNewOnchainEvents'; 4 | 5 | jest.mock('lib/events/sqs/consumer', () => ({ 6 | main: jest.fn(), 7 | })); 8 | 9 | const mockedSqsConsumerRun = main as jest.MockedFunction; 10 | 11 | describe('/api/events/cron/processNewOnchainEvents', () => { 12 | beforeEach(async () => { 13 | jest.clearAllMocks(); 14 | mockedSqsConsumerRun.mockResolvedValue(); 15 | }); 16 | 17 | describe('Calls main notification script and returns 200', () => { 18 | it('Returns 401 if caller is not authenitcated', async () => { 19 | const { req, res } = createMocks({ 20 | method: 'POST', 21 | }); 22 | 23 | await handler(req, res); 24 | 25 | expect(mockedSqsConsumerRun).toBeCalledTimes(1); 26 | expect(res._getStatusCode()).toBe(200); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /__tests__/components/CommunityInfo.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { CommunityInfo } from 'components/CommunityInfo'; 4 | 5 | // Mocking this because next/image doesn't play nicely with __mocks__/fileMock.js 6 | jest.mock( 7 | 'next/image', 8 | () => 9 | function Image() { 10 | return ; 11 | }, 12 | ); 13 | 14 | describe('CommunityInfo', () => { 15 | it('renders', () => { 16 | const { getByText } = render(); 17 | 18 | getByText('Automatically Awarded XP'); 19 | getByText('XP Awarded via Nomination Form'); 20 | getByText('Special Accessories'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /__tests__/components/Disclosure.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { Disclosure } from 'components/Disclosure'; 5 | import { DescriptionList } from 'components/DescriptionList'; 6 | 7 | const FullDisclosure = ({ subtitle }: { subtitle?: string }) => ( 8 | 9 | 10 |
Date
11 |
March 32, 1942
12 |
13 |
14 | ); 15 | 16 | describe('Disclosure', () => { 17 | it('renders in a collapsed state', () => { 18 | const { getByText } = render(); 19 | 20 | getByText('Underwrite Loan'); 21 | expect(getByText('Date')).not.toBeVisible(); 22 | }); 23 | 24 | it('expands on click to show full content, and contracts on further click', () => { 25 | const { getByText } = render(); 26 | 27 | const header = getByText('Underwrite Loan'); 28 | userEvent.click(header); 29 | expect(getByText('Date')).toBeVisible(); 30 | 31 | userEvent.click(header); 32 | expect(getByText('Date')).not.toBeVisible(); 33 | }); 34 | 35 | it('renders a subtitle when provided and the disclosure is closed', () => { 36 | const { getByText } = render(); 37 | 38 | getByText('Subtitle'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /__tests__/components/Explainer.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { Explainer } from 'components/Explainer'; 4 | 5 | describe('Explainer', () => { 6 | it('renders its children in normal mode', () => { 7 | const { getByText, container } = render( 8 | 9 | Hello! 10 | , 11 | ); 12 | 13 | expect(container.querySelector('.explainer')).not.toBeNull(); 14 | getByText('Hello!'); 15 | }); 16 | it('renders its children in error mode', () => { 17 | const { getByText, container } = render( 18 | 19 | Uh oh! 20 | , 21 | ); 22 | 23 | expect(container.querySelector('.explainer-error')).not.toBeNull(); 24 | getByText('Uh oh!'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /__tests__/components/Form.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { Form } from 'components/Form'; 5 | 6 | describe('Form', () => { 7 | it('renders', () => { 8 | // preventDefault to silence JSDom error log about submit not being implemented 9 | const onSubmit = jest.fn().mockImplementation((e) => e.preventDefault()); 10 | const { getByText, container } = render( 11 |
12 | 13 | 14 |
, 15 | ); 16 | 17 | expect(onSubmit).not.toHaveBeenCalled(); 18 | 19 | const button = getByText('Submit'); 20 | const form = container.querySelector('form'); 21 | expect(form).not.toBeNull(); 22 | 23 | userEvent.click(button); 24 | expect(onSubmit).toHaveBeenCalled(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /__tests__/components/GridListSelector.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { GridListSelector } from 'components/GridListSelector'; 5 | 6 | const handleChange = jest.fn(); 7 | 8 | describe('GridListSelector', () => { 9 | beforeEach(() => { 10 | jest.clearAllMocks(); 11 | }); 12 | it('renders', () => { 13 | const { getByRole } = render( 14 | , 15 | ); 16 | 17 | getByRole('checkbox'); 18 | }); 19 | 20 | it('calls handleChange when clicked', () => { 21 | const { getByRole } = render( 22 | , 23 | ); 24 | 25 | expect(handleChange).not.toHaveBeenCalled(); 26 | 27 | const selector = getByRole('checkbox'); 28 | 29 | userEvent.click(selector); 30 | expect(handleChange).toHaveBeenCalledWith(false); 31 | 32 | userEvent.click(selector); 33 | expect(handleChange).toHaveBeenCalledWith(true); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /__tests__/components/Input.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { Input } from 'components/Input'; 4 | 5 | describe('Input', () => { 6 | it('renders', () => { 7 | const { container } = render(); 8 | expect(container.querySelectorAll('input')).toHaveLength(1); 9 | }); 10 | it('renders with a unit', () => { 11 | const { container, getByText } = render(); 12 | expect(container.querySelectorAll('input')).toHaveLength(1); 13 | getByText('ETH'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /__tests__/components/LoanTickets.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { LoanTickets } from 'components/LoanTickets'; 4 | import { baseLoan, loanWithLenderAccruing } from 'lib/mockData'; 5 | import { addressToENS } from 'lib/account'; 6 | 7 | jest.mock('components/Media', () => ({ 8 | ...jest.requireActual('components/Media'), 9 | Media: () =>
, 10 | })); 11 | 12 | jest.mock('lib/account', () => ({ 13 | ...jest.requireActual('lib/account'), 14 | addressToENS: jest.fn(), 15 | })); 16 | 17 | jest.mock('lib/getNFTInfo'); 18 | 19 | const mockAddressToENS = addressToENS as jest.MockedFunction< 20 | typeof addressToENS 21 | >; 22 | 23 | describe('LoanTickets', () => { 24 | beforeEach(() => { 25 | mockAddressToENS.mockResolvedValue(null); 26 | }); 27 | it('renders tickets for a loan with no lender', () => { 28 | const { findByText } = render(); 29 | 30 | // borrower address 31 | findByText(baseLoan.borrower); 32 | 33 | findByText('No lender yet'); 34 | }); 35 | 36 | it('renders tickets for a loan with a lender', async () => { 37 | const { findByText } = render( 38 | , 39 | ); 40 | 41 | // borrower address 42 | findByText(loanWithLenderAccruing.borrower); 43 | 44 | findByText(loanWithLenderAccruing.lender as string); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /__tests__/components/Marquee.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { act, render } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { Marquee } from 'components/Marquee'; 5 | 6 | const messages = [ 7 | hello, 8 | another message, 9 | ]; 10 | 11 | describe('Marquee', () => { 12 | it('renders', () => { 13 | const { getAllByText } = render(); 14 | getAllByText('hello'); 15 | getAllByText('another message'); 16 | }); 17 | 18 | it('toggles scrolling on click', () => { 19 | const { getAllByText, container } = render( 20 | , 21 | ); 22 | const hellos = getAllByText('hello'); 23 | 24 | expect(container.querySelector('.paused')).toBeNull(); 25 | 26 | act(() => userEvent.click(hellos[0])); 27 | 28 | expect(container.querySelector('.paused')).not.toBeNull(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /__tests__/components/Media.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { fireEvent, render } from '@testing-library/react'; 3 | import { Media } from 'components/Media'; 4 | 5 | const media = 'some media'; 6 | const autoPlay = false; 7 | 8 | describe('Media', () => { 9 | it('renders a fallback when there is an error', () => { 10 | const { container } = render( 11 | , 12 | ); 13 | 14 | let fallback = container.querySelector('[data-testid=fallback]'); 15 | expect(fallback).toBeNull(); 16 | 17 | const audio = container.querySelector('audio'); 18 | expect(audio).not.toBeNull(); 19 | 20 | fireEvent.error(audio!); 21 | fallback = container.querySelector('[data-testid=fallback]'); 22 | expect(fallback).not.toBeNull(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /__tests__/components/PendingCommunityTransactions.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import { PendingCommunityTransactions } from 'components/PendingCommunityTransactions/PendingCommunityTransactions'; 3 | import { ethers } from 'ethers'; 4 | import { PendingChanges } from 'lib/communityNFT/multisig'; 5 | 6 | const address = ethers.Wallet.createRandom().address; 7 | 8 | const mockMultiSigChanges: { [key: number]: PendingChanges[] } = { 9 | 63: [ 10 | { 11 | account: address, 12 | id: 'COMMUNITY', 13 | ipfsLink: 'some-ipfs-link', 14 | value: 2, 15 | type: 'CATEGORY', 16 | }, 17 | ], 18 | 64: [ 19 | { 20 | account: address, 21 | id: '8', 22 | ipfsLink: 'some-ipfs-link', 23 | value: 1, 24 | type: 'ACCESSORY', 25 | }, 26 | ], 27 | }; 28 | 29 | describe('PendingCommunityTransactions', () => { 30 | it('renders pending txs from multi sig', async () => { 31 | const { getByText, getAllByText } = render( 32 | , 33 | ); 34 | 35 | getByText('Pending Transaction #63'); 36 | getByText('Pending Transaction #64'); 37 | getAllByText(`account: ${address}`); 38 | getByText('id: COMMUNITY'); 39 | getByText('id: 8'); 40 | getByText('value: 2'); 41 | getByText('value: 1'); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /__tests__/components/Toggle.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { Toggle } from 'components/Toggle'; 5 | 6 | const handleChange = jest.fn(); 7 | 8 | describe('Toggle', () => { 9 | beforeEach(() => { 10 | jest.clearAllMocks(); 11 | }); 12 | it('renders', () => { 13 | const { getByText } = render( 14 | , 15 | ); 16 | getByText('Left'); 17 | getByText('Right'); 18 | }); 19 | 20 | it('calls handleChange when clicked', () => { 21 | const { getByRole } = render( 22 | , 23 | ); 24 | 25 | expect(handleChange).not.toHaveBeenCalled(); 26 | 27 | const selector = getByRole('checkbox'); 28 | 29 | userEvent.click(selector); 30 | expect(handleChange).toHaveBeenCalledWith(false); 31 | 32 | userEvent.click(selector); 33 | expect(handleChange).toHaveBeenCalledWith(true); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /__tests__/hooks/useNetworkSpecificStyles.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import { useNetworkSpecificStyles } from 'hooks/useNetworkSpecificStyles'; 3 | import { STYLE_MAP } from 'hooks/useNetworkSpecificStyles/useNetworkSpecificStyles'; 4 | import { configs, SupportedNetwork } from 'lib/config'; 5 | 6 | const setPropertySpy = jest.spyOn( 7 | global.document.documentElement.style, 8 | 'setProperty', 9 | ); 10 | 11 | describe('useNetworkSpecificStyles', () => { 12 | beforeEach(() => { 13 | jest.clearAllMocks(); 14 | }); 15 | it.each(Object.keys(configs).map((name) => [name]))( 16 | 'calls setProperty the right number of times for %s', 17 | (network) => { 18 | expect(setPropertySpy).not.toHaveBeenCalled(); 19 | renderHook(() => useNetworkSpecificStyles(network as SupportedNetwork)); 20 | expect(setPropertySpy).toHaveBeenCalledTimes( 21 | STYLE_MAP[network as SupportedNetwork].length, 22 | ); 23 | }, 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /__tests__/lib/metrics/repository.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Metric, 3 | getBackedMetric, 4 | resetBackedMetrics, 5 | setBackedMetric, 6 | } from 'lib/metrics/repository'; 7 | 8 | describe('Backed metrics repository', () => { 9 | beforeEach(async () => { 10 | await resetBackedMetrics(); 11 | }); 12 | afterEach(async () => { 13 | await resetBackedMetrics(); 14 | }); 15 | 16 | describe('getter and setter methods', () => { 17 | it('increments emailsSentPastDay metric', async () => { 18 | expect(await getBackedMetric(Metric.EMAILS_PAST_DAY)).toEqual(0); 19 | await setBackedMetric(Metric.EMAILS_PAST_DAY, 1); 20 | expect(await getBackedMetric(Metric.EMAILS_PAST_DAY)).toEqual(1); 21 | }); 22 | }); 23 | 24 | describe('resetBackedMetrics', () => { 25 | it('resets backed metrics', async () => { 26 | await setBackedMetric(Metric.EMAILS_PAST_DAY, 1); 27 | 28 | await resetBackedMetrics(); 29 | 30 | expect(await getBackedMetric(Metric.EMAILS_PAST_DAY)).toEqual(0); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /__tests__/lib/nftCollectionStats/reservoir.test.ts: -------------------------------------------------------------------------------- 1 | import { collectionStatsEthMainnet } from 'lib/nftCollectionStats/reservoir'; 2 | import fetchMock from 'jest-fetch-mock'; 3 | 4 | describe('Reservoir API', () => { 5 | beforeEach(() => { 6 | jest.clearAllMocks(); 7 | }); 8 | 9 | it('makes one call to tokens/v4 and one call to collection/v2 for all NFTs', async () => { 10 | fetchMock.mockResponse( 11 | JSON.stringify({ 12 | floor: null, 13 | items: null, 14 | owners: null, 15 | volume: null, 16 | }), 17 | ).once; 18 | fetchMock.mockResponse( 19 | JSON.stringify({ 20 | tokens: [ 21 | { 22 | collection: { 23 | id: '0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270:74000000:74999999', 24 | }, 25 | }, 26 | ], 27 | }), 28 | ).once; 29 | 30 | await collectionStatsEthMainnet( 31 | '0xa7d8d9ef8D8Ce8992Df33D8b8CF4Aebabd5bD270', 32 | '147', 33 | ); 34 | 35 | expect(fetchMock).toHaveBeenCalledTimes(2); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /codegen/community.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: 'https://api.thegraph.com/subgraphs/name/adamgobes/backed-community-nft' 3 | documents: './graphql/community/**/*.graphql' 4 | generates: 5 | types/generated/graphql/communitysubgraph.ts: 6 | plugins: 7 | - 'typescript' 8 | - 'typescript-operations' 9 | - 'urql-introspection' 10 | - 'typed-document-node' 11 | -------------------------------------------------------------------------------- /codegen/eip721.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: 'https://api.thegraph.com/subgraphs/name/sunguru98/mainnet-erc721-subgraph' 3 | documents: './graphql/eip721/**/*.graphql' 4 | generates: 5 | types/generated/graphql/eip721subgraph.ts: 6 | plugins: 7 | - 'typescript' 8 | - 'typescript-operations' 9 | - 'urql-introspection' 10 | - 'typed-document-node' 11 | -------------------------------------------------------------------------------- /codegen/nftLoans.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: 'https://api.thegraph.com/subgraphs/name/with-backed/backed-protocol' 3 | documents: './graphql/nftLoans/**/*.graphql' 4 | generates: 5 | types/generated/graphql/nftLoans.ts: 6 | plugins: 7 | - 'typescript' 8 | - 'typescript-operations' 9 | - 'urql-introspection' 10 | - 'typed-document-node' 11 | -------------------------------------------------------------------------------- /components/AdvancedSearch/index.ts: -------------------------------------------------------------------------------- 1 | export { AdvancedSearch } from './AdvancedSearch'; 2 | export { SearchHeader } from './Header'; 3 | -------------------------------------------------------------------------------- /components/ApplicationProviders/index.ts: -------------------------------------------------------------------------------- 1 | export { ApplicationProviders } from './ApplicationProviders'; 2 | -------------------------------------------------------------------------------- /components/Banner/Banner.tsx: -------------------------------------------------------------------------------- 1 | import { Cross } from 'components/Icons/Cross'; 2 | import React from 'react'; 3 | import styles from './Banner.module.css'; 4 | 5 | export type BannerKind = 'error' | 'success' | 'info' | 'optimism' | 'polygon'; 6 | 7 | type BannerProps = { 8 | kind: BannerKind; 9 | close?: () => void; 10 | }; 11 | 12 | export function Banner({ 13 | children, 14 | close, 15 | kind, 16 | }: React.PropsWithChildren) { 17 | return ( 18 |
19 |
{children}
20 | {!!close && ( 21 | 27 | )} 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /components/Banner/index.ts: -------------------------------------------------------------------------------- 1 | export { Banner } from './Banner'; 2 | export type { BannerKind } from './Banner'; 3 | -------------------------------------------------------------------------------- /components/Banner/messages/index.ts: -------------------------------------------------------------------------------- 1 | export { WrongNetwork } from './WrongNetwork'; 2 | -------------------------------------------------------------------------------- /components/Button/ButtonLink.tsx: -------------------------------------------------------------------------------- 1 | import React, { ComponentProps } from 'react'; 2 | import Link from 'next/link'; 3 | import { ButtonKind } from './Button'; 4 | import styles from './Button.module.css'; 5 | 6 | interface ButtonLinkProps extends ComponentProps { 7 | kind: ButtonKind; 8 | } 9 | 10 | export function ButtonLink({ children, kind, ...props }: ButtonLinkProps) { 11 | const className = [styles[kind], styles['button-link']].join(' '); 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /components/Button/CompletedButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Checkmark } from 'components/Icons/Checkmark'; 3 | import styles from './Button.module.css'; 4 | 5 | interface CompletedButtonProps { 6 | buttonText: React.ReactNode; 7 | message?: React.ReactNode; 8 | success?: boolean; 9 | id?: string; 10 | } 11 | 12 | export function CompletedButton({ 13 | buttonText, 14 | message, 15 | success = false, 16 | id, 17 | }: CompletedButtonProps) { 18 | const className = [styles['button-completed'], styles.secondary].join(' '); 19 | return ( 20 |
21 | {buttonText} 22 | {!!message && ( 23 |
{message}
24 | )} 25 | {success && ( 26 |
27 | 28 |
29 | )} 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /components/Button/TextButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { ButtonHTMLAttributes } from 'react'; 2 | import styles from './Button.module.css'; 3 | 4 | export type ButtonKind = 5 | | 'neutral' 6 | | 'clickable' 7 | | 'visited' 8 | | 'active' 9 | | 'alert' 10 | | 'success'; 11 | 12 | export interface ButtonProps extends ButtonHTMLAttributes { 13 | kind?: ButtonKind; 14 | } 15 | 16 | export function TextButton({ 17 | children, 18 | kind = 'neutral', 19 | ...props 20 | }: ButtonProps) { 21 | const className = `text-button-${kind}`; 22 | return ( 23 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /components/Button/index.ts: -------------------------------------------------------------------------------- 1 | export { Button, DialogDisclosureButton, DisclosureButton } from './Button'; 2 | export { CompletedButton } from './CompletedButton'; 3 | export { ButtonLink } from './ButtonLink'; 4 | export { TransactionButton } from './TransactionButton'; 5 | export { AllowButton } from './AllowButton'; 6 | export { TextButton } from './TextButton'; 7 | -------------------------------------------------------------------------------- /components/Carousel/index.ts: -------------------------------------------------------------------------------- 1 | export { Carousel } from './Carousel'; 2 | -------------------------------------------------------------------------------- /components/CollateralInfo/CollateralInfo.module.css: -------------------------------------------------------------------------------- 1 | .collectionInfoElement { 2 | display: inline-block; 3 | width: 50%; 4 | } 5 | 6 | .label { 7 | text-transform: capitalize; 8 | } 9 | 10 | .stack { 11 | display: flex; 12 | flex-direction: column; 13 | line-height: 28px; 14 | padding-bottom: 5px; 15 | } 16 | 17 | .conversion { 18 | font-size: var(--font-small); 19 | line-height: var(--font-small); 20 | color: var(--highlight-success-100); 21 | } 22 | -------------------------------------------------------------------------------- /components/CollateralInfo/index.ts: -------------------------------------------------------------------------------- 1 | export { CollateralInfo } from './CollateralInfo'; 2 | -------------------------------------------------------------------------------- /components/CommunityActivity/CommunityActivity.module.css: -------------------------------------------------------------------------------- 1 | .wrapper > fieldset { 2 | border-radius: var(--border-radius-large); 3 | max-width: 715px; 4 | margin: 0 auto; 5 | } 6 | 7 | .list { 8 | list-style-type: none; 9 | padding: 0; 10 | font-size: var(--font-small); 11 | } 12 | 13 | .list > li { 14 | margin: 14px 0; 15 | } 16 | 17 | .circle { 18 | width: 8px; 19 | height: 8px; 20 | border-radius: 8px; 21 | display: inline-block; 22 | } 23 | 24 | .category { 25 | text-transform: uppercase; 26 | font-family: var(--sans); 27 | letter-spacing: 0.12rem; 28 | } 29 | 30 | .COMMUNITY { 31 | composes: circle; 32 | background-color: #4131ff; 33 | } 34 | 35 | .CONTRIBUTOR { 36 | composes: circle; 37 | background-color: #f8d270; 38 | } 39 | 40 | .ACTIVITY { 41 | composes: circle; 42 | background-color: #ff5386; 43 | } 44 | -------------------------------------------------------------------------------- /components/CommunityActivity/index.ts: -------------------------------------------------------------------------------- 1 | export { CommunityActivity } from './CommunityActivity'; 2 | -------------------------------------------------------------------------------- /components/CommunityHeader/PlaceholderBunn.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import placeholderBunn from './placeholder-bunn.svg'; 3 | export const PlaceholderBunn = () => ; 4 | -------------------------------------------------------------------------------- /components/CommunityHeader/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | CommunityHeader, 3 | CommunityHeaderDisconnected, 4 | CommunityHeaderManage, 5 | CommunityHeaderNotMinted, 6 | CommunityHeaderView, 7 | } from './CommunityHeader'; 8 | -------------------------------------------------------------------------------- /components/CommunityHeader/optimism-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/components/CommunityHeader/optimism-circle.png -------------------------------------------------------------------------------- /components/CommunityInfo/XPFieldset.tsx: -------------------------------------------------------------------------------- 1 | import { Fieldset } from 'components/Fieldset'; 2 | import { ShimmerPlus } from 'components/Icons/ShimmerPlus'; 3 | import React, { FunctionComponent } from 'react'; 4 | import styles from './CommunityInfo.module.css'; 5 | 6 | type XPKind = 'activity' | 'community' | 'contributor'; 7 | 8 | const titleMap = { 9 | activity: 'Activity XP', 10 | community: 'Community XP', 11 | contributor: 'Contributor XP', 12 | }; 13 | 14 | type LegendProps = { 15 | kind: XPKind; 16 | }; 17 | function Legend({ kind }: LegendProps) { 18 | return ( 19 |

20 | {titleMap[kind]} 21 |

22 | ); 23 | } 24 | 25 | type XPFieldsetProps = { 26 | kind: XPKind; 27 | }; 28 | export const XPFieldset: FunctionComponent = ({ 29 | kind, 30 | children, 31 | }) => { 32 | return ( 33 |
}> 34 |
{children}
35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /components/CommunityInfo/index.ts: -------------------------------------------------------------------------------- 1 | export { CommunityInfo } from './CommunityInfo'; 2 | -------------------------------------------------------------------------------- /components/CommunityPageContent/CommunityPageContent.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: '100%'; 3 | height: '100%'; 4 | display: 'flex'; 5 | flex-direction: 'column'; 6 | align-items: 'center'; 7 | flex-grow: 1; 8 | } 9 | -------------------------------------------------------------------------------- /components/CommunityPageContent/index.ts: -------------------------------------------------------------------------------- 1 | export { CommunityAddressPage, CommunityPage } from './CommunityPageContent'; 2 | -------------------------------------------------------------------------------- /components/ConnectWallet/ConnectWallet.module.css: -------------------------------------------------------------------------------- 1 | .address { 2 | width: auto; 3 | } 4 | -------------------------------------------------------------------------------- /components/ConnectWallet/ConnectWallet.tsx: -------------------------------------------------------------------------------- 1 | import { ConnectButton } from '@rainbow-me/rainbowkit'; 2 | import { Button, ButtonLink } from 'components/Button'; 3 | import { DisplayAddress } from 'components/DisplayAddress'; 4 | import { useConfig } from 'hooks/useConfig'; 5 | import { pirsch } from 'lib/pirsch'; 6 | import React from 'react'; 7 | import styles from './ConnectWallet.module.css'; 8 | 9 | export const ConnectWallet = () => { 10 | const { network } = useConfig(); 11 | return ( 12 | 13 | {({ account, openConnectModal }) => 14 | !account ? ( 15 | 23 | ) : ( 24 | 27 | 28 | 🔓 29 | 30 | 31 | ) 32 | } 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /components/ConnectWallet/index.ts: -------------------------------------------------------------------------------- 1 | export { ConnectWallet } from './ConnectWallet'; 2 | -------------------------------------------------------------------------------- /components/CreatePageHeader/CreateFormData.ts: -------------------------------------------------------------------------------- 1 | import { LoanAsset } from 'lib/loanAssets'; 2 | 3 | export type CreateFormData = { 4 | duration: string; 5 | interestRate: string; 6 | denomination: LoanAsset; 7 | loanAmount: string; 8 | acceptHigherLoanAmounts: boolean; 9 | }; 10 | -------------------------------------------------------------------------------- /components/CreatePageHeader/CreatePageHeader.module.css: -------------------------------------------------------------------------------- 1 | .create-page-header { 2 | display: flex; 3 | width: 100%; 4 | background: var(--background-white); 5 | justify-content: center; 6 | padding-bottom: 90px; 7 | padding-top: calc(var(--gap) / 2); 8 | } 9 | 10 | .create-page-header > div { 11 | max-width: var(--max-width); 12 | } 13 | 14 | .button-container { 15 | display: flex; 16 | flex-direction: column; 17 | gap: 1rem; 18 | } 19 | 20 | @media screen and (max-width: 700px) { 21 | .create-page-header { 22 | padding: 20px; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /components/CreatePageHeader/SelectNFTButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, DialogDisclosureButton } from 'components/Button'; 2 | import React from 'react'; 3 | import { DialogStateReturn } from 'reakit/Dialog'; 4 | 5 | const ID = 'selectNFT'; 6 | 7 | type SelectNFTButtonProps = { 8 | state: 'disabled' | 'active' | 'selected'; 9 | dialog: DialogStateReturn; 10 | }; 11 | export function SelectNFTButton({ dialog, state }: SelectNFTButtonProps) { 12 | const text = 'Select an NFT'; 13 | 14 | if (state === 'disabled') { 15 | return ( 16 | 19 | ); 20 | } 21 | 22 | return ( 23 | 27 | {text} 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /components/CreatePageHeader/createPageFormSchema.ts: -------------------------------------------------------------------------------- 1 | import { MIN_RATE } from 'lib/constants'; 2 | import * as Yup from 'yup'; 3 | 4 | export const createPageFormSchema = Yup.object({ 5 | denomination: Yup.object({ 6 | value: Yup.string(), 7 | address: Yup.string(), 8 | }).required(), 9 | loanAmount: Yup.number() 10 | .typeError('Loan amount must be a positive integer') 11 | .moreThan(0, 'Loan amount must be greater than zero.') 12 | .required(), 13 | interestRate: Yup.number() 14 | .typeError('Interest rate must be a positive integer') 15 | .min( 16 | MIN_RATE, 17 | `Interest rate must be greater than the minimum value of ${MIN_RATE}%`, 18 | ) 19 | .required(), 20 | duration: Yup.number() 21 | .typeError('Duration must be a positive integer') 22 | .moreThan(0, 'Duration must be greater than 0.') 23 | .required(), 24 | acceptHigherLoanAmounts: Yup.boolean(), 25 | }); 26 | -------------------------------------------------------------------------------- /components/CreatePageHeader/index.ts: -------------------------------------------------------------------------------- 1 | export { CreatePageHeader } from './CreatePageHeader'; 2 | -------------------------------------------------------------------------------- /components/Custom404/Custom404.module.css: -------------------------------------------------------------------------------- 1 | .div { 2 | padding: 40px 0; 3 | justify-content: center; 4 | text-align: center; 5 | } 6 | 7 | .link > a { 8 | color: var(--neutral-100); 9 | } -------------------------------------------------------------------------------- /components/Custom404/Custom404.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import styles from './Custom404.module.css'; 3 | import { DISCORD_ERROR_CHANNEL, DISCORD_URL } from 'lib/constants'; 4 | 5 | export function Custom404() { 6 | return ( 7 |
8 |

{"The URL you're looking for doesn't seem to exist."}

9 |

10 | {"If you think there's an error, let us know in "} 11 | 12 | 13 | 14 | {DISCORD_ERROR_CHANNEL} on Discord. 15 | 16 | 17 |

18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/Custom404/index.ts: -------------------------------------------------------------------------------- 1 | export { Custom404 } from './Custom404'; 2 | -------------------------------------------------------------------------------- /components/Custom500/Custom500.module.css: -------------------------------------------------------------------------------- 1 | .div { 2 | padding: 40px 0; 3 | justify-content: center; 4 | text-align: center; 5 | } 6 | 7 | .link > a { 8 | color: var(--neutral-100); 9 | } -------------------------------------------------------------------------------- /components/Custom500/Custom500.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import styles from './Custom500.module.css'; 3 | import { DISCORD_ERROR_CHANNEL, DISCORD_URL } from 'lib/constants'; 4 | 5 | export function Custom500() { 6 | return ( 7 |
8 |

{'We are having trouble loading this page.'}

9 |

10 | {'If refreshing does not work, let us know in '} 11 | 12 | 13 | 14 | {DISCORD_ERROR_CHANNEL} on Discord. 15 | 16 | 17 |

18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/Custom500/index.ts: -------------------------------------------------------------------------------- 1 | export { Custom500 } from './Custom500'; 2 | -------------------------------------------------------------------------------- /components/DescriptionList/DescriptionList.module.css: -------------------------------------------------------------------------------- 1 | /* Note: this styling is not compatible with "multiple terms, single definition". */ 2 | .horizontal { 3 | display: grid; 4 | grid-template-columns: var(--column-width) 1fr; 5 | column-gap: var(--gap); 6 | row-gap: calc(var(--gap) / 3); 7 | } 8 | 9 | .clamped { 10 | grid-template-columns: var(--column-width) 1fr; 11 | } 12 | 13 | .horizontal, 14 | .vertical { 15 | margin: 0; 16 | } 17 | 18 | .horizontal dd, 19 | .vertical dd { 20 | margin: 0; 21 | } 22 | 23 | .horizontal dt, 24 | .vertical dt { 25 | margin: 0; 26 | text-transform: uppercase; 27 | font-size: var(--font-small); 28 | font-family: var(--sans); 29 | letter-spacing: 0.12rem; 30 | } 31 | 32 | .vertical dt, 33 | .vertical dd { 34 | line-height: 32px; 35 | } 36 | 37 | .horizontal dt { 38 | text-align: right; 39 | line-height: 26px; 40 | } 41 | 42 | .vertical dt { 43 | margin-bottom: -0.75rem; 44 | } 45 | -------------------------------------------------------------------------------- /components/DescriptionList/DescriptionList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './DescriptionList.module.css'; 3 | 4 | interface DescriptionListProps 5 | extends React.DetailedHTMLProps< 6 | React.HTMLAttributes, 7 | HTMLDListElement 8 | > { 9 | orientation?: 'horizontal' | 'vertical'; 10 | clamped?: boolean; 11 | } 12 | 13 | export const DescriptionList = ({ 14 | children, 15 | orientation = 'vertical', 16 | clamped = false, 17 | }: DescriptionListProps) => { 18 | const className = [styles[orientation], clamped ? styles.clamped : ''].join( 19 | ' ', 20 | ); 21 | return
{children}
; 22 | }; 23 | -------------------------------------------------------------------------------- /components/DescriptionList/index.ts: -------------------------------------------------------------------------------- 1 | export { DescriptionList } from './DescriptionList'; 2 | -------------------------------------------------------------------------------- /components/Disclosure/Disclosure.module.css: -------------------------------------------------------------------------------- 1 | .disclosure { 2 | color: var(--highlight-clickable-100); 3 | cursor: pointer; 4 | display: flex; 5 | align-items: center; 6 | gap: 1rem; 7 | } 8 | 9 | .disclosure > svg { 10 | stroke: var(--highlight-clickable-100); 11 | transform: rotate(-90deg); 12 | } 13 | 14 | .opened { 15 | composes: disclosure; 16 | color: var(--highlight-visited-100); 17 | } 18 | 19 | .opened > svg { 20 | stroke: var(--highlight-visited-100); 21 | } 22 | 23 | .currently-open { 24 | composes: opened; 25 | } 26 | 27 | .currently-open > svg { 28 | transform: rotate(0deg); 29 | } 30 | -------------------------------------------------------------------------------- /components/Disclosure/index.ts: -------------------------------------------------------------------------------- 1 | export { Disclosure } from './Disclosure'; 2 | -------------------------------------------------------------------------------- /components/DisplayAddress/index.ts: -------------------------------------------------------------------------------- 1 | export { DisplayAddress } from './DisplayAddress'; 2 | -------------------------------------------------------------------------------- /components/DisplayCurrency/index.ts: -------------------------------------------------------------------------------- 1 | export { DisplayCurrency, DisplayEth } from './DisplayCurrency'; 2 | -------------------------------------------------------------------------------- /components/ErrorBanners/ErrorBanners.module.css: -------------------------------------------------------------------------------- 1 | .banners { 2 | position: sticky; 3 | top: 0; 4 | z-index: 100; 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /components/ErrorBanners/index.ts: -------------------------------------------------------------------------------- 1 | export { ErrorBanners } from './ErrorBanners'; 2 | -------------------------------------------------------------------------------- /components/EtherscanLink/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | EtherscanAddressLink, 3 | EtherscanTokenLink, 4 | EtherscanTransactionLink, 5 | } from './EtherscanLink'; 6 | -------------------------------------------------------------------------------- /components/Explainer/Explainer.module.css: -------------------------------------------------------------------------------- 1 | .explainer { 2 | position: absolute; 3 | border-radius: var(--border-radius-large); 4 | box-shadow: var(--box-shadow-blue); 5 | padding: 16px 21px; 6 | align-self: start; 7 | font-family: var(--sans); 8 | background: var(--background-white); 9 | transition: top 0.5s ease 0s; 10 | z-index: 2; 11 | } 12 | 13 | .explainer-error { 14 | composes: explainer; 15 | box-shadow: var(--box-shadow-red); 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | gap: 1rem; 20 | } 21 | 22 | .explainer-container { 23 | position: relative; 24 | } 25 | 26 | /* TODO: come up with a way to provide explainer content in mobile view. */ 27 | @media screen and (max-width: 700px) { 28 | .explainer { 29 | display: none; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /components/Explainer/Explainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Explainer.module.css'; 3 | 4 | interface ExplainerProps { 5 | top: number; 6 | children?: React.ReactNode; 7 | display?: 'normal' | 'error'; 8 | } 9 | 10 | export function Explainer({ 11 | children, 12 | display = 'normal', 13 | top, 14 | }: ExplainerProps) { 15 | return ( 16 |
17 |
22 | {children} 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /components/Explainer/index.ts: -------------------------------------------------------------------------------- 1 | export { Explainer } from './Explainer'; 2 | -------------------------------------------------------------------------------- /components/Fieldset/Fieldset.module.css: -------------------------------------------------------------------------------- 1 | .standard-fieldset { 2 | border: none; 3 | background: var(--background-fieldset); 4 | width: 100%; 5 | padding: var(--padding-container); 6 | word-break: normal; 7 | overflow-wrap: anywhere; 8 | } 9 | 10 | .legend { 11 | line-height: 0; 12 | font-size: var(--font-large); 13 | text-transform: capitalize; 14 | } 15 | -------------------------------------------------------------------------------- /components/Fieldset/Fieldset.tsx: -------------------------------------------------------------------------------- 1 | import React, { FieldsetHTMLAttributes } from 'react'; 2 | import styles from './Fieldset.module.css'; 3 | 4 | interface FieldsetProps extends FieldsetHTMLAttributes { 5 | legend: React.ReactNode; 6 | } 7 | export function Fieldset({ children, legend, ...props }: FieldsetProps) { 8 | return ( 9 |
10 | {legend} 11 | {children} 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /components/Fieldset/index.ts: -------------------------------------------------------------------------------- 1 | export { Fieldset } from './Fieldset'; 2 | -------------------------------------------------------------------------------- /components/Footer/Footer.module.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | width: 100%; 3 | min-height: 40px; 4 | display: flex; 5 | justify-content: center; 6 | background-color: var(--highlight-active-10); 7 | margin-top: auto; 8 | } 9 | 10 | .footer-links { 11 | width: 100%; 12 | max-width: var(--max-width); 13 | list-style-type: none; 14 | padding: 0; 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | flex-wrap: wrap; 19 | } 20 | 21 | .footer-links a { 22 | color: var(--neutral-100); 23 | margin: 0 0.25rem; 24 | } 25 | 26 | .footer-links a:hover { 27 | color: var(--highlight-clickable-100); 28 | } 29 | 30 | @media screen and (max-width: 900px) { 31 | .footer-links { 32 | flex-direction: column; 33 | align-items: flex-start; 34 | gap: 0.5em; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /components/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { DISCORD_URL, GITHUB_URL, TWITTER_URL } from 'lib/constants'; 2 | import Link from 'next/link'; 3 | import React from 'react'; 4 | import styles from './Footer.module.css'; 5 | 6 | type Link = { 7 | title: string; 8 | href: string; 9 | }; 10 | 11 | const LINKS: Link[] = [ 12 | { 13 | title: '🤝 Terms of Service', 14 | href: '/legal/terms-of-service.pdf', 15 | }, 16 | { 17 | title: '🔒 Privacy Policy', 18 | href: '/legal/privacy-policy.pdf', 19 | }, 20 | { 21 | title: '💼 Contract Audits', 22 | href: 'https://code4rena.com/reports/2022-04-backed/', 23 | }, 24 | { 25 | title: '⚙️ GitHub', 26 | href: GITHUB_URL, 27 | }, 28 | { 29 | title: '🐦 Twitter', 30 | href: TWITTER_URL, 31 | }, 32 | { 33 | title: '📣 Discord', 34 | href: DISCORD_URL, 35 | }, 36 | ]; 37 | 38 | export function Footer() { 39 | return ( 40 |
41 |
    42 | {LINKS.map((link) => { 43 | return ( 44 |
  • 45 | 46 | {link.title} 47 | 48 |
  • 49 | ); 50 | })} 51 |
  • 🥕
  • 52 |
53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /components/Footer/index.ts: -------------------------------------------------------------------------------- 1 | export { Footer } from './Footer'; 2 | -------------------------------------------------------------------------------- /components/Form/Form.module.css: -------------------------------------------------------------------------------- 1 | .form { 2 | display: flex; 3 | flex-direction: column; 4 | gap: calc(var(--gap) / 2); 5 | } 6 | 7 | .form > label { 8 | display: grid; 9 | grid-template-columns: var(--column-width) 1fr; 10 | column-gap: var(--gap); 11 | align-items: center; 12 | } 13 | 14 | .form > label > span { 15 | text-transform: uppercase; 16 | font-size: var(--font-small); 17 | letter-spacing: 0.12rem; 18 | text-align: right; 19 | } 20 | -------------------------------------------------------------------------------- /components/Form/Form.tsx: -------------------------------------------------------------------------------- 1 | import React, { FormHTMLAttributes } from 'react'; 2 | import styles from './Form.module.css'; 3 | 4 | export function Form({ 5 | children, 6 | autoComplete = 'off', 7 | ...props 8 | }: FormHTMLAttributes) { 9 | return ( 10 |
11 | {children} 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /components/Form/index.ts: -------------------------------------------------------------------------------- 1 | export { Form } from './Form'; 2 | -------------------------------------------------------------------------------- /components/GridListSelector/GridListSelector.module.css: -------------------------------------------------------------------------------- 1 | .selector { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: center; 5 | align-items: center; 6 | background: var(--background-white); 7 | border-radius: var(--border-radius-large); 8 | cursor: pointer; 9 | } 10 | 11 | .icon { 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | align-items: center; 16 | height: 40px; 17 | padding: 0.5rem 1rem; 18 | border-radius: var(--border-radius-large); 19 | } 20 | 21 | .grid { 22 | composes: icon; 23 | stroke: var(--neutral-50); 24 | } 25 | 26 | .list { 27 | composes: icon; 28 | fill: var(--neutral-50); 29 | } 30 | 31 | .selector[aria-checked='true'] > .grid { 32 | background-color: var(--highlight-active-10); 33 | stroke: var(--neutral-100); 34 | } 35 | 36 | .selector[aria-checked='false'] > .list { 37 | background-color: var(--highlight-active-10); 38 | fill: var(--neutral-100); 39 | } 40 | 41 | .selector[aria-checked='true'] > .grid:hover, 42 | .selector[aria-checked='false'] > .list:hover { 43 | background-color: var(--highlight-active-20); 44 | } 45 | 46 | .selector[aria-checked='true'] > .list:hover { 47 | fill: var(--neutral-100); 48 | } 49 | .selector[aria-checked='false'] > .grid:hover { 50 | stroke: var(--neutral-100); 51 | } 52 | -------------------------------------------------------------------------------- /components/GridListSelector/GridListSelector.tsx: -------------------------------------------------------------------------------- 1 | import { GridView } from 'components/Icons/GridView'; 2 | import { ListView } from 'components/Icons/ListView'; 3 | import React, { useCallback } from 'react'; 4 | import { Checkbox } from 'reakit/Checkbox'; 5 | import styles from './GridListSelector.module.css'; 6 | 7 | type GridListSelectorProps = { 8 | handleChange: (checked: boolean) => void; 9 | }; 10 | 11 | /** 12 | * This toggle is very similar to components/Toggle. Right now it has some 13 | * special-case CSS to handle the SVGs, but it may be worth consolidating 14 | * these in the future. 15 | */ 16 | export const GridListSelector = ({ handleChange }: GridListSelectorProps) => { 17 | const [checked, setChecked] = React.useState(true); 18 | const toggle = useCallback(() => { 19 | setChecked(!checked); 20 | handleChange(!checked); 21 | }, [checked, handleChange, setChecked]); 22 | return ( 23 | 29 |
30 | 31 |
32 |
33 | 34 |
35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /components/GridListSelector/index.ts: -------------------------------------------------------------------------------- 1 | export { GridListSelector } from './GridListSelector'; 2 | -------------------------------------------------------------------------------- /components/Header/index.ts: -------------------------------------------------------------------------------- 1 | export { Header } from './Header'; 2 | -------------------------------------------------------------------------------- /components/HeaderInfo/HeaderInfo.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | width: 100%; 3 | background: var(--background-white); 4 | position: relative; 5 | overflow: hidden; 6 | } 7 | 8 | .info { 9 | grid-column: span 4; 10 | display: grid; 11 | grid-template-columns: 70px 1fr; 12 | font-size: var(--font-small); 13 | margin: 1.2rem 0 2.2rem 0; 14 | } 15 | 16 | .info > p { 17 | line-height: 1.6rem; 18 | margin-left: 1rem; 19 | } 20 | 21 | .info > p > button { 22 | padding: 0; 23 | } 24 | 25 | @media screen and (max-width: 710px) { 26 | .info { 27 | grid-column: span 12; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /components/HeaderInfo/borrower-bunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/components/HeaderInfo/borrower-bunny.png -------------------------------------------------------------------------------- /components/HeaderInfo/index.ts: -------------------------------------------------------------------------------- 1 | export { HeaderInfo } from './HeaderInfo'; 2 | -------------------------------------------------------------------------------- /components/HeaderInfo/investigative-bunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/components/HeaderInfo/investigative-bunny.png -------------------------------------------------------------------------------- /components/HeaderInfo/lender-bunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/components/HeaderInfo/lender-bunny.png -------------------------------------------------------------------------------- /components/Icons/Arrow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Arrow = () => ( 4 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /components/Icons/Checkmark.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Checkmark = () => ( 4 | 5 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /components/Icons/Chevron.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Chevron = () => ( 4 | 5 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /components/Icons/Cross.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const Cross = () => ( 4 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /components/Icons/GridView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const GridView = () => ( 4 | 5 | 6 | 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /components/Icons/ListView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const ListView = () => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /components/Icons/WalletConnect.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const WalletConnect = () => ( 4 | 5 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /components/Input/Input.tsx: -------------------------------------------------------------------------------- 1 | import React, { InputHTMLAttributes } from 'react'; 2 | 3 | import styles from './Input.module.css'; 4 | 5 | interface InputProps extends InputHTMLAttributes { 6 | color?: 'light' | 'dark'; 7 | unit?: string; 8 | ignoreLastPass?: boolean; 9 | } 10 | 11 | export const Input = React.forwardRef( 12 | ({ color = 'light', ignoreLastPass = true, unit, ...props }, ref) => { 13 | const className = unit ? styles[`${color}-unit`] : styles[color]; 14 | 15 | if (unit) { 16 | return ( 17 |
18 | 24 | {unit} 25 |
26 | ); 27 | } 28 | return ( 29 | 35 | ); 36 | }, 37 | ); 38 | Input.displayName = 'Input'; 39 | -------------------------------------------------------------------------------- /components/Input/index.ts: -------------------------------------------------------------------------------- 1 | export { Input } from './Input'; 2 | -------------------------------------------------------------------------------- /components/LoanCard/index.ts: -------------------------------------------------------------------------------- 1 | export { LoanCard } from './LoanCard'; 2 | -------------------------------------------------------------------------------- /components/LoanForm/Balance/Balance.tsx: -------------------------------------------------------------------------------- 1 | import { DescriptionList } from 'components/DescriptionList'; 2 | import React from 'react'; 3 | import styles from '../LoanForm.module.css'; 4 | 5 | type BalanceProps = { 6 | balance: number; 7 | loanAmount: number; 8 | symbol: string; 9 | }; 10 | 11 | export const Balance = ({ balance, loanAmount, symbol }: BalanceProps) => { 12 | const insufficientFunds = loanAmount > balance; 13 | return ( 14 | <> 15 |
Current Balance
16 |
20 | {balance.toFixed(2)} {symbol} 21 |
22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /components/LoanForm/Balance/index.ts: -------------------------------------------------------------------------------- 1 | export { Balance } from './Balance'; 2 | -------------------------------------------------------------------------------- /components/LoanForm/LoanForm.module.css: -------------------------------------------------------------------------------- 1 | .base-wrapper { 2 | display: grid; 3 | grid-template-columns: 1fr 1fr; 4 | gap: var(--gap); 5 | } 6 | 7 | .wrapper { 8 | composes: base-wrapper; 9 | margin-top: var(--gap); 10 | } 11 | 12 | .form-wrapper { 13 | composes: base-wrapper; 14 | margin-top: calc(var(--gap) / 2); 15 | } 16 | 17 | .insufficient-funds { 18 | color: var(--highlight-alert-100); 19 | } 20 | 21 | @media screen and (max-width: 700px) { 22 | .base-wrapper { 23 | grid-template-columns: 1fr; 24 | } 25 | 26 | .disclosure-text-wrapper { 27 | width: 100%; 28 | } 29 | } 30 | 31 | .disclosure-text-wrapper { 32 | text-align: left; 33 | padding-top: 1.75rem; 34 | width: 50%; 35 | font-size: var(--font-medium); 36 | line-height: 2rem; 37 | } 38 | 39 | .disclosure-text { 40 | color: var(--highlight-clickable-100); 41 | cursor: pointer; 42 | } -------------------------------------------------------------------------------- /components/LoanForm/LoanFormAwaiting/index.ts: -------------------------------------------------------------------------------- 1 | export { LoanFormAwaiting } from './LoanFormAwaiting'; 2 | -------------------------------------------------------------------------------- /components/LoanForm/LoanFormBetterTerms/index.ts: -------------------------------------------------------------------------------- 1 | export { LoanFormBetterTerms } from './LoanFormBetterTerms'; 2 | -------------------------------------------------------------------------------- /components/LoanForm/LoanFormData.ts: -------------------------------------------------------------------------------- 1 | import { CreateFormData } from 'components/CreatePageHeader/CreateFormData'; 2 | 3 | export type LoanFormData = Omit; 4 | -------------------------------------------------------------------------------- /components/LoanForm/LoanFormDisclosure/LoanFormDisclosure.tsx: -------------------------------------------------------------------------------- 1 | import { DisclosureContent } from 'reakit/Disclosure'; 2 | import { useDisclosureState } from 'reakit/Disclosure'; 3 | import { DisclosureButton } from 'components/Button'; 4 | 5 | type LoanFormDisclosure = React.PropsWithChildren<{ 6 | title: string; 7 | className?: string; 8 | }>; 9 | 10 | export function LoanFormDisclosure({ 11 | title, 12 | children, 13 | className, 14 | }: LoanFormDisclosure) { 15 | const disclosure = useDisclosureState({ visible: false }); 16 | return ( 17 | <> 18 |
19 | 20 | {title} 21 | 22 |
23 | 24 | {(_props) => disclosure.visible && children} 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /components/LoanForm/LoanFormDisclosure/index.ts: -------------------------------------------------------------------------------- 1 | export { LoanFormDisclosure } from './LoanFormDisclosure'; 2 | -------------------------------------------------------------------------------- /components/LoanForm/LoanFormEarlyClosure/index.ts: -------------------------------------------------------------------------------- 1 | export { LoanFormEarlyClosure } from './LoanFormEarlyClosure'; 2 | -------------------------------------------------------------------------------- /components/LoanForm/LoanFormRepay/Explainer.tsx: -------------------------------------------------------------------------------- 1 | import { Explainer as ExplainerWrapper } from 'components/Explainer'; 2 | 3 | type ExplainerProps = { 4 | top: number; 5 | }; 6 | 7 | export const Explainer = ({ top }: ExplainerProps) => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | function Repay() { 16 | return ( 17 |
18 | ”Repay and claim” will pay this amount, close the loan, and transfer the 19 | collateral to your wallet. 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /components/LoanForm/LoanFormRepay/index.ts: -------------------------------------------------------------------------------- 1 | export { LoanFormRepay } from './LoanFormRepay'; 2 | -------------------------------------------------------------------------------- /components/LoanForm/LoanFormSeizeCollateral/Explainer.tsx: -------------------------------------------------------------------------------- 1 | import { Explainer as ExplainerWrapper } from 'components/Explainer'; 2 | 3 | type ExplainerProps = { 4 | top: number; 5 | }; 6 | 7 | export const Explainer = ({ top }: ExplainerProps) => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | function SeizeNFT() { 16 | return ( 17 |
18 | Because the Borrower has not repaid the loan, as the Lender you have the 19 | opportunity to seize the collateral NFT. If you choose to wait, the 20 | Borrower could still repay, and another Lender could still buy you out. 21 | Seizing the NFT will close the loan. 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /components/LoanForm/LoanFormSeizeCollateral/index.ts: -------------------------------------------------------------------------------- 1 | export { LoanFormSeizeCollateral } from './LoanFormSeizeCollateral'; 2 | -------------------------------------------------------------------------------- /components/LoanForm/LoanOfferBetterTermsDisclosure/LoanOfferBetterTermsDisclosure.tsx: -------------------------------------------------------------------------------- 1 | import { Disclosure, DisclosureContent } from 'reakit/Disclosure'; 2 | import { useDisclosureState } from 'reakit/Disclosure'; 3 | 4 | type LoanOfferBetterTermsDisclosure = React.PropsWithChildren<{ 5 | textWrapperClassName: string; 6 | disclosureTextClassName: string; 7 | className?: string; 8 | }>; 9 | 10 | export function LoanOfferBetterTermsDisclosure({ 11 | textWrapperClassName, 12 | disclosureTextClassName, 13 | children, 14 | }: LoanOfferBetterTermsDisclosure) { 15 | const disclosure = useDisclosureState({ visible: false }); 16 | return ( 17 | <> 18 |
19 | 🎉 You are the current lender!  You can still{' '} 20 | 25 | update the loan terms 26 | 27 | , which will reset the loan duration. 28 |
29 | 30 | {(_props) => disclosure.visible && children} 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /components/LoanForm/LoanOfferBetterTermsDisclosure/index.ts: -------------------------------------------------------------------------------- 1 | export { LoanOfferBetterTermsDisclosure } from './LoanOfferBetterTermsDisclosure'; 2 | -------------------------------------------------------------------------------- /components/LoanForm/index.ts: -------------------------------------------------------------------------------- 1 | export { LoanForm } from './LoanForm'; 2 | -------------------------------------------------------------------------------- /components/LoanForm/loanPageFormSchema.ts: -------------------------------------------------------------------------------- 1 | import { MIN_RATE } from 'lib/constants'; 2 | import * as Yup from 'yup'; 3 | 4 | type LoanPageFormSchemaParams = { 5 | duration: number; 6 | interestRate: number; 7 | loanAmount: number; 8 | }; 9 | export const loanPageFormSchema = ({ 10 | loanAmount, 11 | interestRate, 12 | duration, 13 | }: LoanPageFormSchemaParams) => 14 | Yup.object({ 15 | loanAmount: Yup.number() 16 | .min( 17 | loanAmount, 18 | `Loan amount must be at least the current term of ${loanAmount}.`, 19 | ) 20 | .required(), 21 | interestRate: Yup.number() 22 | .min(MIN_RATE) 23 | .max( 24 | interestRate, 25 | `Interest rate must be no greater than the current term of ${interestRate}.`, 26 | ) 27 | .required(), 28 | duration: Yup.number() 29 | .min( 30 | duration, 31 | `Duration must be at least the current term of ${duration} days.`, 32 | ) 33 | .required(), 34 | }); 35 | -------------------------------------------------------------------------------- /components/LoanForm/strings.ts: -------------------------------------------------------------------------------- 1 | export const BETTER_TERMS_LABEL = 'Lend at better terms'; 2 | export const LEND_LABEL = 'Lend'; 3 | -------------------------------------------------------------------------------- /components/LoanGalleryLoadMore/LoanGalleryLoadMore.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | grid-column: span 12; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | margin-bottom: var(--gap); 7 | } 8 | 9 | .container > button { 10 | max-width: 350px; 11 | } 12 | -------------------------------------------------------------------------------- /components/LoanGalleryLoadMore/LoanGalleryLoadMore.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'components/Button'; 2 | import React from 'react'; 3 | import styles from './LoanGalleryLoadMore.module.css'; 4 | 5 | type LoanGalleryLoadMoreProps = { 6 | isLoadingMore?: boolean; 7 | isReachingEnd?: boolean; 8 | loadMore: () => void; 9 | }; 10 | 11 | export function LoanGalleryLoadMore({ 12 | isLoadingMore, 13 | isReachingEnd, 14 | loadMore, 15 | }: LoanGalleryLoadMoreProps) { 16 | if (isLoadingMore) { 17 | return ( 18 |
19 | 20 |
21 | ); 22 | } 23 | 24 | if (isReachingEnd) { 25 | return ( 26 |
27 | 30 |
31 | ); 32 | } 33 | 34 | return ( 35 |
36 | 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /components/LoanGalleryLoadMore/index.ts: -------------------------------------------------------------------------------- 1 | export { LoanGalleryLoadMore } from './LoanGalleryLoadMore'; 2 | -------------------------------------------------------------------------------- /components/LoanHeader/LoanHeader.module.css: -------------------------------------------------------------------------------- 1 | .loan-header { 2 | display: flex; 3 | width: 100%; 4 | background: var(--background-white); 5 | justify-content: center; 6 | padding-bottom: 90px; 7 | padding-top: calc(var(--gap) / 2); 8 | } 9 | 10 | .loan-header > div { 11 | max-width: var(--max-width); 12 | } 13 | 14 | .stack { 15 | display: flex; 16 | flex-direction: column; 17 | line-height: 28px; 18 | padding-bottom: 5px; 19 | } 20 | 21 | .conversion { 22 | font-size: var(--font-small); 23 | line-height: var(--font-small); 24 | color: var(--highlight-success-100); 25 | } 26 | 27 | .red { 28 | color: var(--highlight-alert-100); 29 | } 30 | 31 | .media { 32 | grid-column: col-start / span 4; 33 | display: flex; 34 | align-items: flex-start; 35 | justify-content: center; 36 | } 37 | 38 | .form { 39 | grid-column: col-start 5 / -1; 40 | } 41 | 42 | @media screen and (max-width: 700px) { 43 | .media, 44 | .form { 45 | grid-column: span 12; 46 | } 47 | 48 | .loan-header { 49 | padding: calc(var(--gap) / 4) 0; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /components/LoanHeader/index.ts: -------------------------------------------------------------------------------- 1 | export { LoanHeader } from './LoanHeader'; 2 | -------------------------------------------------------------------------------- /components/LoanInfo/LoanInfo.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | justify-content: center; 4 | width: 100%; 5 | position: relative; 6 | padding: 40px 0; 7 | flex-grow: 1; 8 | } 9 | 10 | .wrapper > div:last-child { 11 | max-width: var(--max-width); 12 | z-index: 1; 13 | } 14 | 15 | .mask { 16 | background: var(--background-radial-gradient); 17 | opacity: 0.7; 18 | position: absolute; 19 | top: 0; 20 | bottom: 0; 21 | left: 0; 22 | right: 0; 23 | display: flex; 24 | z-index: 0; 25 | } 26 | 27 | .column { 28 | display: flex; 29 | flex-direction: column; 30 | width: 100%; 31 | gap: var(--gap); 32 | } 33 | 34 | .left-column { 35 | composes: column; 36 | grid-column: col-start / span 4; 37 | } 38 | 39 | .right-column { 40 | composes: column; 41 | grid-column: col-start 5 / -1; 42 | } 43 | 44 | @media screen and (max-width: 700px) { 45 | .left-column, 46 | .right-column { 47 | grid-column: span 12; 48 | gap: 20px; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /components/LoanInfo/index.ts: -------------------------------------------------------------------------------- 1 | export { LoanInfo } from './LoanInfo'; 2 | -------------------------------------------------------------------------------- /components/LoanTable/LoanTable.module.css: -------------------------------------------------------------------------------- 1 | .table { 2 | width: 100%; 3 | background: var(--background-white); 4 | border-collapse: collapse; 5 | grid-column: 1 / -1; 6 | box-shadow: var(--box-shadow); 7 | } 8 | 9 | .header th { 10 | text-align: left; 11 | font-weight: normal; 12 | text-transform: uppercase; 13 | font-size: var(--font-small); 14 | padding: 0.5rem 0; 15 | } 16 | 17 | .table tr { 18 | border: 10px solid #fff; 19 | } 20 | 21 | .table > tbody > tr:hover { 22 | background-color: var(--highlight-clickable-5); 23 | } 24 | 25 | .name-container { 26 | display: grid; 27 | grid-template-columns: 75px 1fr; 28 | grid-template-rows: 75px; 29 | gap: 1rem; 30 | color: var(--neutral-100); 31 | } 32 | 33 | .name-container:visited { 34 | color: var(--neutral-100); 35 | } 36 | 37 | .field-and-subfield { 38 | display: flex; 39 | flex-direction: column; 40 | justify-content: center; 41 | } 42 | 43 | .field-and-subfield > span:last-child { 44 | text-transform: uppercase; 45 | font-size: var(--font-small); 46 | } 47 | 48 | .right, 49 | th.right { 50 | text-align: right; 51 | } 52 | 53 | .rate, 54 | th.rate { 55 | text-align: right; 56 | padding-right: 1rem; 57 | } 58 | -------------------------------------------------------------------------------- /components/LoanTable/index.ts: -------------------------------------------------------------------------------- 1 | export { LoanTable } from './LoanTable'; 2 | -------------------------------------------------------------------------------- /components/LoanTermsDisclosure/LoanTermsDisclosure.module.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/components/LoanTermsDisclosure/LoanTermsDisclosure.module.css -------------------------------------------------------------------------------- /components/LoanTermsDisclosure/index.ts: -------------------------------------------------------------------------------- 1 | export { LoanTermsDisclosure } from './LoanTermsDisclosure'; 2 | -------------------------------------------------------------------------------- /components/LoanTickets/LoanTickets.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: grid; 3 | grid-auto-flow: column; 4 | grid-column-gap: var(--gap); 5 | grid-auto-columns: minmax(0, 1fr); 6 | } 7 | 8 | .column { 9 | display: flex; 10 | flex-direction: column; 11 | } 12 | 13 | .column > a { 14 | margin-top: 1rem; 15 | } 16 | -------------------------------------------------------------------------------- /components/LoanTickets/index.ts: -------------------------------------------------------------------------------- 1 | export { LoanTickets } from './LoanTickets'; 2 | -------------------------------------------------------------------------------- /components/Logo/Logo.module.css: -------------------------------------------------------------------------------- 1 | .image { 2 | height: 70px; 3 | width: 70px; 4 | image-rendering: pixelated; 5 | image-rendering: -moz-crisp-edges; 6 | image-rendering: crisp-edges; 7 | } 8 | -------------------------------------------------------------------------------- /components/Logo/borked-bunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/components/Logo/borked-bunny.png -------------------------------------------------------------------------------- /components/Logo/index.ts: -------------------------------------------------------------------------------- 1 | export { Logo } from './Logo'; 2 | -------------------------------------------------------------------------------- /components/Logo/pepe-bunny-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/components/Logo/pepe-bunny-line.png -------------------------------------------------------------------------------- /components/Marquee/Marquee.module.css: -------------------------------------------------------------------------------- 1 | /* https://reneroth.xyz/marquees-in-css/ */ 2 | .container { 3 | overflow: hidden; 4 | white-space: nowrap; 5 | align-items: stretch; 6 | position: absolute; 7 | left: 0; 8 | right: 0; 9 | } 10 | 11 | .scrolling { 12 | text-align: center; 13 | animation: marquee 15s linear infinite; 14 | display: inline-block; 15 | width: 100%; 16 | background: linear-gradient( 17 | 180deg, 18 | rgba(0, 0, 0, 0) calc(50% - 1px), 19 | var(--neutral-100) calc(50%), 20 | rgba(0, 0, 0, 0) calc(50% + 1px) 21 | ); 22 | } 23 | 24 | @keyframes marquee { 25 | from { 26 | transform: translateX(0); 27 | } 28 | to { 29 | transform: translateX(-100%); 30 | } 31 | } 32 | 33 | .paused { 34 | animation-play-state: paused; 35 | } 36 | 37 | .wrapper > * { 38 | background: var(--neutral-5); 39 | margin: 0 2rem; 40 | padding: 0 0.25rem; 41 | font-size: var(--font-large); 42 | } 43 | -------------------------------------------------------------------------------- /components/Marquee/index.ts: -------------------------------------------------------------------------------- 1 | export { Marquee } from './Marquee'; 2 | -------------------------------------------------------------------------------- /components/Media/Fallback.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from 'react'; 2 | import styles from './Media.module.css'; 3 | 4 | const PAWN_SHOP_ITEMS = ['🎸', '💸', '🔭', '🎥', '🛵', '🏺', '💎', '💍']; 5 | const getRandomItem = () => { 6 | return PAWN_SHOP_ITEMS[Math.floor(Math.random() * PAWN_SHOP_ITEMS.length)]; 7 | }; 8 | 9 | type FallbackProps = { 10 | small?: boolean; 11 | animated?: boolean; 12 | }; 13 | 14 | export const Fallback = ({ animated = true, small }: FallbackProps) => { 15 | const [item, setItem] = useState(''); 16 | useEffect(() => { 17 | setItem(getRandomItem()); 18 | }, [setItem]); 19 | 20 | const className = useMemo(() => { 21 | if (small) { 22 | return styles.smallback; 23 | } 24 | 25 | if (animated) { 26 | return styles['fallback-animated']; 27 | } 28 | 29 | return styles.fallback; 30 | }, [animated, small]); 31 | return ( 32 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /components/Media/Media.module.css: -------------------------------------------------------------------------------- 1 | .media-content { 2 | max-width: 100%; 3 | } 4 | 5 | .fallback { 6 | width: 100%; 7 | aspect-ratio: 1/1; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | font-size: min(5vw, calc(var(--max-width) / 20)); 12 | background: #f7f3f1; 13 | } 14 | 15 | .fallback-animated { 16 | composes: fallback; 17 | background-image: linear-gradient( 18 | 110deg, 19 | #f7f3f1 0%, 20 | #f7f3f1 30%, 21 | #fefaf8 50%, 22 | #f7f3f1 70%, 23 | #f7f3f1 100% 24 | ); 25 | background-repeat: no-repeat; 26 | background-size: 600px 600px; 27 | position: relative; 28 | animation-duration: 1600ms; 29 | animation-fill-mode: forwards; 30 | animation-iteration-count: infinite; 31 | animation-name: placeholderShimmer; 32 | animation-timing-function: linear; 33 | } 34 | 35 | .smallback { 36 | composes: fallback; 37 | font-size: calc(var(--max-width) / 40); 38 | } 39 | 40 | .fallback > span { 41 | opacity: 20%; 42 | } 43 | 44 | @keyframes placeholderShimmer { 45 | 0% { 46 | background-position: -500px 0; 47 | } 48 | 49 | 100% { 50 | background-position: 500px 0; 51 | } 52 | } 53 | 54 | .image { 55 | width: 100%; 56 | height: 100%; 57 | justify-self: center; 58 | object-fit: contain; 59 | object-position: top; 60 | } 61 | -------------------------------------------------------------------------------- /components/Media/NFTMedia.tsx: -------------------------------------------------------------------------------- 1 | import { Media } from 'components/Media'; 2 | import { Fallback } from './Fallback'; 3 | import { MaybeNFTMetadata } from 'hooks/useTokenMetadata'; 4 | 5 | interface NFTMediaProps { 6 | nftInfo: MaybeNFTMetadata; 7 | small?: boolean; 8 | } 9 | 10 | export function NFTMedia({ nftInfo, small = false }: NFTMediaProps) { 11 | const { isLoading, metadata } = nftInfo; 12 | 13 | if (!metadata) { 14 | return ; 15 | } 16 | 17 | return ( 18 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /components/Media/index.ts: -------------------------------------------------------------------------------- 1 | export { Media } from './Media'; 2 | -------------------------------------------------------------------------------- /components/Modal/index.ts: -------------------------------------------------------------------------------- 1 | export { Modal } from './Modal'; 2 | -------------------------------------------------------------------------------- /components/NFTCollateralPicker/NFTCollateralPicker.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | gap: 1rem; 6 | } 7 | 8 | .nft-group { 9 | display: flex; 10 | flex-direction: column; 11 | width: 100%; 12 | } 13 | 14 | .nft-group-header { 15 | display: flex; 16 | width: 100%; 17 | padding: 18px 22px; 18 | background: var(--neutral-5); 19 | border-radius: var(--border-radius-large); 20 | align-items: center; 21 | cursor: pointer; 22 | } 23 | 24 | .nft-group-header-active { 25 | composes: nft-group-header; 26 | background: var(--highlight-active-10); 27 | } 28 | 29 | .nft-count { 30 | margin-left: auto; 31 | } 32 | 33 | @keyframes append-animate { 34 | from { 35 | transform: scaleY(0); 36 | opacity: 0; 37 | } 38 | to { 39 | transform: scaleY(1); 40 | opacity: 1; 41 | } 42 | } 43 | 44 | .nft-list { 45 | display: grid; 46 | grid-template-columns: repeat(3, 1fr); 47 | gap: 1rem; 48 | margin-top: 1rem; 49 | animation: append-animate 0.3s linear; 50 | } 51 | -------------------------------------------------------------------------------- /components/NFTExchangeLink/index.ts: -------------------------------------------------------------------------------- 1 | export { NFTExchangeAddressLink } from './NFTExchangeLink'; 2 | -------------------------------------------------------------------------------- /components/NetworkSelector/NetworkSelector.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | z-index: 1000; 3 | } 4 | -------------------------------------------------------------------------------- /components/NetworkSelector/index.ts: -------------------------------------------------------------------------------- 1 | export { NetworkSelector } from './NetworkSelector'; 2 | -------------------------------------------------------------------------------- /components/NotificationsModal/NotificationsModal.module.css: -------------------------------------------------------------------------------- 1 | .button-row { 2 | display: flex; 3 | justify-content: space-between; 4 | } 5 | 6 | .button-row > button { 7 | width: auto; 8 | } 9 | 10 | .error-message { 11 | color: var(--highlight-alert-100); 12 | font-size: var(--font-small); 13 | } 14 | -------------------------------------------------------------------------------- /components/NotificationsModal/index.ts: -------------------------------------------------------------------------------- 1 | export { NotificationsModal } from './NotificationsModal'; 2 | -------------------------------------------------------------------------------- /components/OpenGraph/index.ts: -------------------------------------------------------------------------------- 1 | export { OpenGraph } from './OpenGraph'; 2 | -------------------------------------------------------------------------------- /components/PawnArt/index.ts: -------------------------------------------------------------------------------- 1 | export { PawnLoanArt, PawnTicketArt } from './PawnArt'; 2 | -------------------------------------------------------------------------------- /components/PendingCommunityTransactions/PendingCommunityTransactions.module.css: -------------------------------------------------------------------------------- 1 | .wrapper > fieldset { 2 | border-radius: var(--border-radius-large); 3 | max-width: 715px; 4 | margin: 30px auto; 5 | } 6 | 7 | .list { 8 | list-style-type: none; 9 | padding: 0; 10 | font-size: var(--font-small); 11 | } 12 | 13 | .list > li { 14 | margin: 14px 0; 15 | } 16 | 17 | .ipfsLink { 18 | max-width: 70%; 19 | white-space: nowrap; 20 | overflow: hidden; 21 | text-overflow: ellipsis; 22 | } -------------------------------------------------------------------------------- /components/Profile/BorrowerLenderBubble.tsx: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | import { useMemo } from 'react'; 3 | import { useAccount } from 'wagmi'; 4 | import styles from './borrowerLenderBubble.module.css'; 5 | 6 | type BubblesProps = { 7 | address: string; 8 | borrower: boolean; 9 | }; 10 | 11 | export function BorrowerLenderBubble({ address, borrower }: BubblesProps) { 12 | const { address: connectedAddress } = useAccount(); 13 | const isConnectedUser = useMemo( 14 | () => 15 | connectedAddress && connectedAddress === ethers.utils.getAddress(address), 16 | [connectedAddress, address], 17 | ); 18 | 19 | return ( 20 | 21 | {isConnectedUser && `You are the ${borrower ? 'borrower' : 'lender'}`} 22 | {!isConnectedUser && ( 23 | <> 24 | {borrower ? 'Borrower' : 'Lender'}{' '} 25 | {address.substring(0, 7)} 26 | 27 | )} 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /components/Profile/NextLoanDueCountdown.tsx: -------------------------------------------------------------------------------- 1 | import { formatTimeDigit, getDaysHoursMinutesSeconds } from 'lib/duration'; 2 | import { getNextLoanDue } from 'lib/loans/profileHeaderMethods'; 3 | import { useCallback, useEffect, useMemo, useState } from 'react'; 4 | import { Loan } from 'types/Loan'; 5 | 6 | export function NextLoanDueCountdown({ loans }: { loans: Loan[] }) { 7 | const [remainingSeconds, setRemainingSeconds] = useState( 8 | getNextLoanDue(loans), 9 | ); 10 | 11 | const refreshTimestamp = useCallback( 12 | (intervalId) => { 13 | if (remainingSeconds <= 0) { 14 | clearInterval(intervalId); 15 | return; 16 | } 17 | setRemainingSeconds((prev) => prev - 1); 18 | }, 19 | [remainingSeconds], 20 | ); 21 | 22 | useEffect(() => { 23 | const timeOutId: NodeJS.Timeout = setInterval( 24 | () => refreshTimestamp(timeOutId), 25 | 1000, 26 | ); 27 | return () => clearInterval(timeOutId); 28 | }, [refreshTimestamp]); 29 | 30 | const { days, hours, minutes, seconds } = useMemo( 31 | () => getDaysHoursMinutesSeconds(remainingSeconds), 32 | [remainingSeconds], 33 | ); 34 | 35 | return ( 36 |
37 | {days} days + {formatTimeDigit(hours)}:{formatTimeDigit(minutes)}: 38 | {formatTimeDigit(seconds)} 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /components/Profile/ProfileLoans.tsx: -------------------------------------------------------------------------------- 1 | import { TwelveColumn } from 'components/layouts/TwelveColumn'; 2 | import { LoanCard } from 'components/LoanCard'; 3 | import { Loan } from 'types/Loan'; 4 | 5 | type ProfileLoansProps = { 6 | address: string; 7 | loans: Loan[]; 8 | }; 9 | 10 | export function ProfileLoans({ address, loans }: ProfileLoansProps) { 11 | return ( 12 | 13 | {loans 14 | .filter((loan) => !loan.closed) 15 | .map((loan) => ( 16 | 21 | ))} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /components/Profile/borrowerLenderBubble.module.css: -------------------------------------------------------------------------------- 1 | .bubble { 2 | text-transform: uppercase; 3 | color: black; 4 | font-size: var(--font-small); 5 | padding: 4px 10px; 6 | border-radius: var(--border-radius-small); 7 | } 8 | 9 | .bubble > span { 10 | text-transform: lowercase; 11 | } 12 | 13 | .borrowerBubble { 14 | composes: bubble; 15 | background: var(--highlight-active-10); 16 | } 17 | 18 | .lenderBubble { 19 | composes: bubble; 20 | background: var(--neutral-30); 21 | } 22 | -------------------------------------------------------------------------------- /components/Profile/profile.module.css: -------------------------------------------------------------------------------- 1 | .profile-header-wrapper { 2 | background: var(--background-white); 3 | width: 100%; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | padding: var(--gap) 0; 8 | } 9 | 10 | .profile-header-wrapper > div { 11 | max-width: var(--max-width); 12 | } 13 | 14 | .profile-header-wrapper fieldset { 15 | grid-column: span 4; 16 | max-width: 100%; 17 | display: block; 18 | overflow: hidden; 19 | overflow-wrap: break-word; 20 | padding-left: 0; 21 | padding-right: 0; 22 | } 23 | 24 | .profile-header-wrapper fieldset button { 25 | padding: 0; 26 | text-align: left; 27 | } 28 | 29 | .container { 30 | display: grid; 31 | grid-template-columns: minmax(0, 1fr); 32 | gap: 1rem; 33 | } 34 | 35 | @media screen and (max-width: 700px) { 36 | .profile-header-wrapper { 37 | padding: 0; 38 | } 39 | .profile-header-wrapper fieldset { 40 | grid-column: span 12; 41 | padding: 20px 0; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /components/ProfileActivity/ProfileActivity.module.css: -------------------------------------------------------------------------------- 1 | .event-list { 2 | grid-column: span 12; 3 | list-style-type: none; 4 | background-color: var(--background-white); 5 | padding: var(--padding-loan-card); 6 | margin: 0; 7 | display: flex; 8 | flex-direction: column; 9 | gap: 1rem; 10 | } 11 | 12 | .event { 13 | display: grid; 14 | grid-template-columns: 0.35fr 70px 1fr; 15 | gap: var(--gap); 16 | align-items: center; 17 | justify-content: space-between; 18 | } 19 | 20 | .description-wrapper { 21 | display: flex; 22 | flex-direction: column; 23 | } 24 | 25 | .event-link { 26 | display: flex; 27 | flex-direction: column; 28 | } 29 | 30 | .event-link > span { 31 | font-size: var(--font-small); 32 | } 33 | 34 | .highlight-address { 35 | padding: 0.25rem; 36 | background-color: var(--highlight-active-10); 37 | } 38 | 39 | .description { 40 | font-size: var(--font-small); 41 | } 42 | 43 | @media screen and (max-width: 700px) { 44 | .event-list { 45 | gap: 2rem; 46 | } 47 | .event { 48 | grid-template-columns: 1fr; 49 | gap: 10px; 50 | } 51 | 52 | .event:last-of-type { 53 | border-bottom: none; 54 | padding-bottom: 0; 55 | } 56 | 57 | .event > *:nth-child(2) { 58 | display: none; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /components/ProfileActivity/index.ts: -------------------------------------------------------------------------------- 1 | export { ProfileActivity } from './ProfileActivity'; 2 | -------------------------------------------------------------------------------- /components/RepaymentInfo/RepaymentInfo.module.css: -------------------------------------------------------------------------------- 1 | .maturity-date > div { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | } 6 | 7 | .maturity-date img { 8 | cursor: pointer; 9 | margin: 0 10px; 10 | } 11 | 12 | .maturity-date a { 13 | display: flex; 14 | } -------------------------------------------------------------------------------- /components/RepaymentInfo/index.ts: -------------------------------------------------------------------------------- 1 | export { RepaymentInfo } from './RepaymentInfo'; 2 | -------------------------------------------------------------------------------- /components/Select/index.ts: -------------------------------------------------------------------------------- 1 | export { Select } from './Select'; 2 | -------------------------------------------------------------------------------- /components/TicketHistory/TicketHistory.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: grid; 3 | grid-template-columns: minmax(0, 1fr); 4 | gap: 1rem; 5 | } 6 | -------------------------------------------------------------------------------- /components/TicketHistory/TicketHistory.tsx: -------------------------------------------------------------------------------- 1 | import { Fieldset } from 'components/Fieldset'; 2 | import { ParsedEvent } from './ParsedEvent'; 3 | import { Loan } from 'types/Loan'; 4 | import styles from './TicketHistory.module.css'; 5 | import type { Event } from 'types/Event'; 6 | import useSWR from 'swr'; 7 | import { toObjectWithBigNumbers } from 'lib/parseSerializedResponse'; 8 | import { useMemo } from 'react'; 9 | import { useConfig } from 'hooks/useConfig'; 10 | 11 | interface TicketHistoryProps { 12 | loan: Loan; 13 | } 14 | 15 | const fetcher = (url: string) => fetch(url).then((res) => res.json()); 16 | 17 | export function TicketHistory({ loan }: TicketHistoryProps) { 18 | const { network } = useConfig(); 19 | const { data } = useSWR( 20 | `/api/network/${network}/loans/history/${loan.id}`, 21 | fetcher, 22 | ); 23 | 24 | const parsedData = useMemo( 25 | () => (data ? (data.map(toObjectWithBigNumbers) as Event[]) : []), 26 | [data], 27 | ); 28 | 29 | return ( 30 |
31 |
32 | {!!parsedData && 33 | parsedData.map((e) => ( 34 | 35 | ))} 36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /components/TicketHistory/index.ts: -------------------------------------------------------------------------------- 1 | export { TicketHistory } from './TicketHistory'; 2 | -------------------------------------------------------------------------------- /components/Toggle/Toggle.module.css: -------------------------------------------------------------------------------- 1 | .selector { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: center; 5 | align-items: center; 6 | background: var(--background-white); 7 | border-radius: var(--border-radius-large); 8 | cursor: pointer; 9 | } 10 | 11 | .child { 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | align-items: center; 16 | height: 40px; 17 | padding: 0.5rem 1rem; 18 | border-radius: var(--border-radius-large); 19 | color: var(--neutral-50); 20 | font-size: var(--font-large); 21 | } 22 | 23 | .left { 24 | composes: child; 25 | } 26 | 27 | .right { 28 | composes: child; 29 | } 30 | 31 | .selector[aria-checked='true'] > .left { 32 | background-color: var(--highlight-active-10); 33 | color: var(--neutral-100); 34 | } 35 | 36 | .selector[aria-checked='false'] > .right { 37 | background-color: var(--highlight-active-10); 38 | color: var(--neutral-100); 39 | } 40 | 41 | .selector[aria-checked='true'] > .left:hover, 42 | .selector[aria-checked='false'] > .right:hover { 43 | background-color: var(--highlight-active-20); 44 | } 45 | 46 | .selector[aria-checked='true'] > .right:hover { 47 | color: var(--neutral-100); 48 | } 49 | .selector[aria-checked='false'] > .left:hover { 50 | color: var(--neutral-100); 51 | } 52 | -------------------------------------------------------------------------------- /components/Toggle/Toggle.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { Checkbox } from 'reakit/Checkbox'; 3 | import styles from './Toggle.module.css'; 4 | 5 | type ToggleProps = { 6 | handleChange: (checked: boolean) => void; 7 | left: React.ReactNode; 8 | right: React.ReactNode; 9 | initialChecked?: boolean; 10 | }; 11 | 12 | export const Toggle = ({ 13 | handleChange, 14 | initialChecked = true, 15 | left, 16 | right, 17 | }: ToggleProps) => { 18 | const [checked, setChecked] = React.useState(initialChecked); 19 | const toggle = useCallback(() => { 20 | setChecked(!checked); 21 | handleChange(!checked); 22 | }, [checked, handleChange, setChecked]); 23 | return ( 24 | 29 |
{left}
30 |
{right}
31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /components/Toggle/index.ts: -------------------------------------------------------------------------------- 1 | export { Toggle } from './Toggle'; 2 | -------------------------------------------------------------------------------- /components/layouts/AppWrapper.module.css: -------------------------------------------------------------------------------- 1 | .app-wrapper { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | min-height: 100vh; 6 | align-items: center; 7 | background: var(--background-radial-gradient); 8 | } 9 | 10 | .app-wrapper-community { 11 | composes: app-wrapper; 12 | } 13 | 14 | .app-wrapper-community > nav { 15 | background-color: transparent; 16 | } 17 | -------------------------------------------------------------------------------- /components/layouts/AppWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { useCommunityGradient } from 'hooks/useCommunityGradient'; 2 | import { useRouter } from 'next/router'; 3 | import React, { FunctionComponent, useMemo } from 'react'; 4 | import styles from './AppWrapper.module.css'; 5 | 6 | function communityGradient(start: string, finish: string) { 7 | return `radial-gradient( 8 | 52.94% 52.36% at 28.53% 25.3%, 9 | ${start} 26.04%, 10 | ${finish} 100% 11 | )`; 12 | } 13 | 14 | export const AppWrapper: FunctionComponent = ({ children }) => { 15 | const { pathname } = useRouter(); 16 | const { from, to } = useCommunityGradient(); 17 | 18 | const isCommunityPage = useMemo( 19 | () => pathname.startsWith('/community'), 20 | [pathname], 21 | ); 22 | const computedStyles = useMemo(() => { 23 | if (isCommunityPage) { 24 | return { 25 | background: communityGradient(from, to), 26 | }; 27 | } 28 | 29 | return {}; 30 | }, [isCommunityPage, from, to]); 31 | 32 | return ( 33 |
40 | {children} 41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /components/layouts/FormWrapper.module.css: -------------------------------------------------------------------------------- 1 | .form-wrapper { 2 | display: grid; 3 | grid-template-columns: 1fr; 4 | row-gap: 0.75rem; 5 | } 6 | -------------------------------------------------------------------------------- /components/layouts/FormWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import styles from './FormWrapper.module.css'; 3 | 4 | export const FormWrapper: FunctionComponent = ({ children }) => { 5 | return
{children}
; 6 | }; 7 | -------------------------------------------------------------------------------- /components/layouts/ThreeColumn.module.css: -------------------------------------------------------------------------------- 1 | .grid { 2 | display: grid; 3 | grid-template-columns: repeat(3, 1fr); 4 | gap: var(--gap); 5 | width: 100%; 6 | } 7 | 8 | @media (max-width: 42em) { 9 | .grid { 10 | /* On mobile devices we only want one column. */ 11 | grid-template-columns: 1fr; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /components/layouts/ThreeColumn.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | 3 | import styles from './ThreeColumn.module.css'; 4 | 5 | export const ThreeColumn: FunctionComponent = ({ children }) => ( 6 |
{children}
7 | ); 8 | -------------------------------------------------------------------------------- /components/layouts/TwelveColumn/TwelveColumn.module.css: -------------------------------------------------------------------------------- 1 | .grid { 2 | display: grid; 3 | grid-template-columns: repeat(12, [col-start] 1fr); 4 | gap: var(--gap); 5 | margin: 0 auto; 6 | width: 100%; 7 | max-width: var(--max-width); 8 | } 9 | 10 | .padded-grid { 11 | composes: grid; 12 | padding: var(--gap) 0; 13 | } 14 | 15 | @media screen and (max-width: 700px) { 16 | .grid { 17 | gap: 0; 18 | row-gap: 20px; 19 | padding: 20px; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /components/layouts/TwelveColumn/TwelveColumn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './TwelveColumn.module.css'; 3 | 4 | type TwelveColumnProps = React.PropsWithChildren<{ 5 | padded?: boolean; 6 | }>; 7 | 8 | export const TwelveColumn = React.forwardRef( 9 | ({ children, padded }, ref) => { 10 | return ( 11 |
12 | {children} 13 |
14 | ); 15 | }, 16 | ); 17 | 18 | TwelveColumn.displayName = 'TwelveColumn'; 19 | -------------------------------------------------------------------------------- /components/layouts/TwelveColumn/index.ts: -------------------------------------------------------------------------------- 1 | export { TwelveColumn } from './TwelveColumn'; 2 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | postgres: 4 | image: 'postgres:12.8' 5 | environment: 6 | - POSTGRES_DB=notifications 7 | - POSTGRES_USER=user 8 | - POSTGRES_HOST=postgres 9 | - POSTGRES_PORT=5432 10 | - POSTGRES_PASSWORD=password 11 | volumes: 12 | - db:/var/lib/postgresql/data 13 | ports: 14 | - 5432:5432 15 | volumes: 16 | db: 17 | driver: local -------------------------------------------------------------------------------- /global-setup.js: -------------------------------------------------------------------------------- 1 | module.exports = async () => { 2 | process.env.TZ = 'America/New_York'; 3 | }; 4 | -------------------------------------------------------------------------------- /graphql/community/accessoriesQuery.graphql: -------------------------------------------------------------------------------- 1 | query accessories { 2 | accessories { 3 | id 4 | name 5 | contractAddress 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /graphql/community/communityAccountQuery.graphql: -------------------------------------------------------------------------------- 1 | query communityAccount($address: ID!) { 2 | account(id: $address) { 3 | id 4 | token { 5 | id 6 | uri 7 | mintedAt 8 | } 9 | categoryScoreChanges { 10 | id 11 | timestamp 12 | blockNumber 13 | category { 14 | id 15 | name 16 | } 17 | newScore 18 | oldScore 19 | ipfsEntryHash 20 | ipfsLink 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /graphql/eip721/queries/useNFTquery.graphql: -------------------------------------------------------------------------------- 1 | query Nfts($address: ID!) { 2 | account(id: $address) { 3 | id 4 | tokens { 5 | id 6 | identifier 7 | uri 8 | registry { 9 | name 10 | } 11 | approvals { 12 | id 13 | approved { 14 | id 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /graphql/nftLoans/fragments/allLoanProperties.graphql: -------------------------------------------------------------------------------- 1 | fragment allLoanProperties on Loan { 2 | id 3 | loanAssetContractAddress 4 | collateralContractAddress 5 | collateralTokenId 6 | perAnumInterestRate 7 | accumulatedInterest 8 | lastAccumulatedTimestamp 9 | durationSeconds 10 | loanAmount 11 | status 12 | closed 13 | loanAssetDecimal 14 | loanAssetSymbol 15 | lendTicketHolder 16 | borrowTicketHolder 17 | endDateTimestamp 18 | collateralTokenURI 19 | collateralName 20 | createdAtTimestamp 21 | lastUpdatedAtTimestamp 22 | numEvents 23 | allowLoanAmountIncrease 24 | } 25 | -------------------------------------------------------------------------------- /graphql/nftLoans/fragments/eventProperties.graphql: -------------------------------------------------------------------------------- 1 | fragment buyoutEventProperties on BuyoutEvent { 2 | id 3 | newLender 4 | lendTicketHolder 5 | loanAmount 6 | interestEarned 7 | timestamp 8 | blockNumber 9 | } 10 | 11 | fragment closeEventProperties on CloseEvent { 12 | id 13 | timestamp 14 | blockNumber 15 | closer 16 | } 17 | 18 | fragment collateralSeizureEventProperties on CollateralSeizureEvent { 19 | id 20 | timestamp 21 | blockNumber 22 | lendTicketHolder 23 | borrowTicketHolder 24 | } 25 | 26 | fragment createEventProperties on CreateEvent { 27 | id 28 | timestamp 29 | blockNumber 30 | creator 31 | maxPerAnumInterestRate 32 | minDurationSeconds 33 | minLoanAmount 34 | allowLoanAmountIncrease 35 | } 36 | 37 | fragment lendEventProperties on LendEvent { 38 | id 39 | lender 40 | loanAmount 41 | durationSeconds 42 | perAnumInterestRate 43 | timestamp 44 | blockNumber 45 | borrowTicketHolder 46 | } 47 | 48 | fragment repaymentEventProperties on RepaymentEvent { 49 | id 50 | repayer 51 | loanAmount 52 | interestEarned 53 | timestamp 54 | blockNumber 55 | lendTicketHolder 56 | borrowTicketHolder 57 | } 58 | -------------------------------------------------------------------------------- /graphql/nftLoans/queries/allEventsQueries.graphql: -------------------------------------------------------------------------------- 1 | query allCreateEvents($where: CreateEvent_filter) { 2 | createEvents(where: $where) { 3 | ...createEventProperties 4 | loan { 5 | ...allLoanProperties 6 | } 7 | } 8 | } 9 | 10 | query allBuyoutEvents($where: BuyoutEvent_filter) { 11 | buyoutEvents(where: $where) { 12 | ...buyoutEventProperties 13 | loan { 14 | ...allLoanProperties 15 | } 16 | } 17 | } 18 | 19 | query allLendEvents($where: LendEvent_filter) { 20 | lendEvents(where: $where) { 21 | ...lendEventProperties 22 | loan { 23 | ...allLoanProperties 24 | } 25 | } 26 | } 27 | 28 | query allRepaymentEvents($where: RepaymentEvent_filter) { 29 | repaymentEvents(where: $where) { 30 | ...repaymentEventProperties 31 | loan { 32 | ...allLoanProperties 33 | } 34 | } 35 | } 36 | 37 | query allCollateralSeizureEvents($where: CollateralSeizureEvent_filter) { 38 | collateralSeizureEvents(where: $where) { 39 | ...collateralSeizureEventProperties 40 | loan { 41 | ...allLoanProperties 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /graphql/nftLoans/queries/allLoansQuery.graphql: -------------------------------------------------------------------------------- 1 | query allLoans( 2 | $where: Loan_filter 3 | $first: Int 4 | $skip: Int 5 | $orderBy: Loan_orderBy 6 | $orderDirection: OrderDirection 7 | ) { 8 | loans( 9 | where: $where 10 | orderBy: $orderBy 11 | orderDirection: $orderDirection 12 | first: $first 13 | skip: $skip 14 | ) { 15 | ...allLoanProperties 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /graphql/nftLoans/queries/eventsForLoan.graphql: -------------------------------------------------------------------------------- 1 | query eventsForLoan($id: ID!) { 2 | loan(id: $id) { 3 | createEvent { 4 | ...createEventProperties 5 | } 6 | lendEvents { 7 | ...lendEventProperties 8 | } 9 | buyoutEvents { 10 | ...buyoutEventProperties 11 | } 12 | repaymentEvent { 13 | ...repaymentEventProperties 14 | } 15 | collateralSeizureEvent { 16 | ...collateralSeizureEventProperties 17 | } 18 | closeEvent { 19 | ...closeEventProperties 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /graphql/nftLoans/queries/homepageSearchQuery.graphql: -------------------------------------------------------------------------------- 1 | query homepageSearch( 2 | $statuses: [LoanStatus!] 3 | $collateralContractAddress: Bytes 4 | $collateralName: String 5 | $loanAssetSymbol: String 6 | $borrowTicketHolder: Bytes 7 | $lendTicketHolder: Bytes 8 | $loanAmountMin: BigInt 9 | $loanAmountMax: BigInt 10 | $perAnumInterestRateMin: BigInt 11 | $perAnumInterestRateMax: BigInt 12 | $durationSecondsMin: BigInt 13 | $durationSecondsMax: BigInt 14 | $selectedSort: Loan_orderBy 15 | $sortDirection: OrderDirection 16 | $first: Int 17 | $skip: Int 18 | ) { 19 | loans( 20 | where: { 21 | status_in: $statuses, 22 | collateralContractAddress_contains: $collateralContractAddress, 23 | collateralName_contains: $collateralName, 24 | loanAssetSymbol_contains: $loanAssetSymbol, 25 | borrowTicketHolder_contains: $borrowTicketHolder, 26 | lendTicketHolder_contains: $lendTicketHolder, 27 | loanAmount_gte: $loanAmountMin, 28 | loanAmount_lt: $loanAmountMax, 29 | perAnumInterestRate_gte: $perAnumInterestRateMin, 30 | perAnumInterestRate_lt: $perAnumInterestRateMax, 31 | durationSeconds_gte: $durationSecondsMin, 32 | durationSeconds_lt: $durationSecondsMax 33 | } 34 | orderBy: $selectedSort 35 | orderDirection: $sortDirection 36 | first: $first 37 | skip: $skip 38 | ) { 39 | ...allLoanProperties 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /graphql/nftLoans/queries/homepageSearchQueryWithoutLender.graphql: -------------------------------------------------------------------------------- 1 | query homepageSearchWithoutLender( 2 | $statuses: [LoanStatus!] 3 | $collateralContractAddress: Bytes 4 | $collateralName: String 5 | $loanAssetSymbol: String 6 | $borrowTicketHolder: Bytes 7 | $loanAmountMin: BigInt 8 | $loanAmountMax: BigInt 9 | $perAnumInterestRateMin: BigInt 10 | $perAnumInterestRateMax: BigInt 11 | $durationSecondsMin: BigInt 12 | $durationSecondsMax: BigInt 13 | $selectedSort: Loan_orderBy 14 | $sortDirection: OrderDirection 15 | $first: Int 16 | $skip: Int 17 | ) { 18 | loans( 19 | where: { 20 | status_in: $statuses, 21 | collateralContractAddress_contains: $collateralContractAddress, 22 | collateralName_contains: $collateralName, 23 | loanAssetSymbol_contains: $loanAssetSymbol, 24 | borrowTicketHolder_contains: $borrowTicketHolder, 25 | loanAmount_gte: $loanAmountMin, 26 | loanAmount_lt: $loanAmountMax, 27 | perAnumInterestRate_gte: $perAnumInterestRateMin, 28 | perAnumInterestRate_lt: $perAnumInterestRateMax, 29 | durationSeconds_gte: $durationSecondsMin, 30 | durationSeconds_lt: $durationSecondsMax 31 | } 32 | orderBy: $selectedSort 33 | orderDirection: $sortDirection 34 | first: $first 35 | skip: $skip 36 | ) { 37 | ...allLoanProperties 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /graphql/nftLoans/queries/loanById.graphql: -------------------------------------------------------------------------------- 1 | query loanById($id: ID!) { 2 | loan(id: $id) { 3 | ...allLoanProperties 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /graphql/nftLoans/queries/notificationEvents.graphql: -------------------------------------------------------------------------------- 1 | query createByTransactionHash($id: ID!) { 2 | createEvent(id: $id) { 3 | ...createEventProperties 4 | loan { 5 | ...allLoanProperties 6 | } 7 | } 8 | } 9 | 10 | query buyoutByTransactionHash($id: ID!) { 11 | buyoutEvent(id: $id) { 12 | ...buyoutEventProperties 13 | loan { 14 | ...allLoanProperties 15 | } 16 | } 17 | } 18 | 19 | query lendByTransactionHash($id: ID!) { 20 | lendEvent(id: $id) { 21 | ...lendEventProperties 22 | loan { 23 | ...allLoanProperties 24 | } 25 | } 26 | } 27 | 28 | query repaymentEventByTransactionHash($id: ID!) { 29 | repaymentEvent(id: $id) { 30 | ...repaymentEventProperties 31 | loan { 32 | ...allLoanProperties 33 | } 34 | } 35 | } 36 | 37 | query collateralSeizureEventByTransactionHash($id: ID!) { 38 | collateralSeizureEvent(id: $id) { 39 | ...collateralSeizureEventProperties 40 | loan { 41 | ...allLoanProperties 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /graphql/nftSales/fragments/allSaleProperties.graphql: -------------------------------------------------------------------------------- 1 | fragment allSaleProperties on Sale { 2 | id 3 | nftContractAddress 4 | nftTokenId 5 | saleType 6 | blockNumber 7 | timestamp 8 | seller 9 | buyer 10 | exchange 11 | paymentToken 12 | price 13 | } 14 | -------------------------------------------------------------------------------- /graphql/nftSales/queries/salesByAddress.graphql: -------------------------------------------------------------------------------- 1 | query salesByAddress( 2 | $nftContractAddress: Bytes 3 | $nftTokenId: String! 4 | $first: Int! 5 | $orderBy: Sale_orderBy 6 | $orderDirection: OrderDirection 7 | ) { 8 | sales( 9 | where: { nftContractAddress: $nftContractAddress, nftTokenId: $nftTokenId } 10 | first: $first 11 | orderBy: $orderBy 12 | orderDirection: $orderDirection 13 | ) { 14 | ...allSaleProperties 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /hooks/useBalance/index.ts: -------------------------------------------------------------------------------- 1 | export { useBalance } from './useBalance'; 2 | -------------------------------------------------------------------------------- /hooks/useCommunityGradient/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | CommunityGradientContext, 3 | CommunityGradientProvider, 4 | useCommunityGradient, 5 | } from './useCommunityGradient'; 6 | -------------------------------------------------------------------------------- /hooks/useConfig/index.ts: -------------------------------------------------------------------------------- 1 | export { ConfigProvider, useConfig } from './useConfig'; 2 | -------------------------------------------------------------------------------- /hooks/useConfig/useConfig.tsx: -------------------------------------------------------------------------------- 1 | import { Config, configs, SupportedNetwork } from 'lib/config'; 2 | import { useRouter } from 'next/router'; 3 | import { createContext, FunctionComponent, useContext, useMemo } from 'react'; 4 | 5 | const ConfigContext = createContext(null); 6 | 7 | type ConfigProviderProps = { 8 | network: SupportedNetwork; 9 | }; 10 | export const ConfigProvider: FunctionComponent = ({ 11 | children, 12 | network, 13 | }) => { 14 | return ( 15 | 16 | {children} 17 | 18 | ); 19 | }; 20 | 21 | export function useConfig(): Config { 22 | const config = useContext(ConfigContext); 23 | const { pathname } = useRouter(); 24 | const isCommunityPage = useMemo( 25 | () => pathname.startsWith('/community'), 26 | [pathname], 27 | ); 28 | 29 | if (isCommunityPage) { 30 | return configs.optimism; 31 | } 32 | 33 | return config!; 34 | } 35 | -------------------------------------------------------------------------------- /hooks/useGlobalMessages/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | useGlobalMessages, 3 | GlobalMessagingProvider, 4 | } from './useGlobalMessages'; 5 | 6 | export type { Message } from './useGlobalMessages'; 7 | -------------------------------------------------------------------------------- /hooks/useHasCollapsedHeaderInfo/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | useHasCollapsedHeaderInfo, 3 | HasCollapsedHeaderInfoProvider, 4 | } from './useHasCollapsedHeaderInfo'; 5 | -------------------------------------------------------------------------------- /hooks/useHasCollapsedHeaderInfo/useHasCollapsedHeaderInfo.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useCallback, useContext, useState } from 'react'; 2 | 3 | const hasCollapsedHeaderInfoContext = createContext<{ 4 | hasCollapsed: boolean; 5 | setHasCollapsed: (value: boolean) => void; 6 | }>({} as any); 7 | 8 | export function HasCollapsedHeaderInfoProvider({ 9 | children, 10 | }: React.PropsWithChildren<{}>) { 11 | const [hasCollapsed, setHasCollapsed] = useState(false); 12 | const { Provider } = hasCollapsedHeaderInfoContext; 13 | 14 | const value = { hasCollapsed, setHasCollapsed }; 15 | return {children}; 16 | } 17 | 18 | export function useHasCollapsedHeaderInfo() { 19 | const { hasCollapsed, setHasCollapsed } = useContext( 20 | hasCollapsedHeaderInfoContext, 21 | ); 22 | 23 | const onCollapse = useCallback( 24 | () => setHasCollapsed(true), 25 | [setHasCollapsed], 26 | ); 27 | 28 | return { hasCollapsed, onCollapse }; 29 | } 30 | -------------------------------------------------------------------------------- /hooks/useKonami/index.ts: -------------------------------------------------------------------------------- 1 | export { useKonami } from './useKonami'; 2 | -------------------------------------------------------------------------------- /hooks/useKonami/useKonami.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | const konami = [ 4 | 'ArrowUp', 5 | 'ArrowUp', 6 | 'ArrowDown', 7 | 'ArrowDown', 8 | 'ArrowLeft', 9 | 'ArrowRight', 10 | 'ArrowLeft', 11 | 'ArrowRight', 12 | 'KeyB', 13 | 'KeyA', 14 | 'Enter', 15 | ]; 16 | 17 | export function useKonami() { 18 | const [index, setIndex] = useState(0); 19 | const [codeActive, setCodeActive] = useState(false); 20 | 21 | useEffect(() => { 22 | function handleKeyDown(ev: KeyboardEvent) { 23 | setIndex((prev) => { 24 | if (ev.code === konami[prev]) { 25 | return ++prev; 26 | } 27 | return 0; 28 | }); 29 | } 30 | document.addEventListener('keydown', handleKeyDown); 31 | 32 | return () => document.removeEventListener('keydown', handleKeyDown); 33 | }, []); 34 | 35 | useEffect(() => { 36 | if (index >= konami.length) { 37 | setIndex(0); 38 | setCodeActive((prev) => !prev); 39 | } 40 | }, [index]); 41 | 42 | return codeActive; 43 | } 44 | -------------------------------------------------------------------------------- /hooks/useLTV/index.ts: -------------------------------------------------------------------------------- 1 | export { useLTV } from './useLTV'; 2 | -------------------------------------------------------------------------------- /hooks/useLoanDetails/index.ts: -------------------------------------------------------------------------------- 1 | export { useLoanDetails } from './useLoanDetails'; 2 | -------------------------------------------------------------------------------- /hooks/useLoanUnderwriter/index.ts: -------------------------------------------------------------------------------- 1 | export { useLoanUnderwriter } from './useLoanUnderwriter'; 2 | -------------------------------------------------------------------------------- /hooks/useLoanViewerRole/index.ts: -------------------------------------------------------------------------------- 1 | export { useLoanViewerRole } from './useLoanViewerRole'; 2 | -------------------------------------------------------------------------------- /hooks/useLoanViewerRole/useLoanViewerRole.ts: -------------------------------------------------------------------------------- 1 | import { Loan } from '../../types/Loan'; 2 | import { useMemo } from 'react'; 3 | 4 | export const useLoanViewerRole = (loan: Loan, account?: string | null) => 5 | useMemo(() => { 6 | if (!account) { 7 | return null; 8 | } else if (account.toUpperCase() === loan.lender?.toUpperCase()) { 9 | return 'lender'; 10 | } else if (account.toUpperCase() === loan.borrower.toUpperCase()) { 11 | return 'borrower'; 12 | } 13 | return null; 14 | }, [account, loan.lender, loan.borrower]); 15 | -------------------------------------------------------------------------------- /hooks/useNFTs/index.ts: -------------------------------------------------------------------------------- 1 | export { useNFTs } from './useNFTs'; 2 | -------------------------------------------------------------------------------- /hooks/useNFTs/useNFTs.ts: -------------------------------------------------------------------------------- 1 | import { captureException } from '@sentry/nextjs'; 2 | import { ethers } from 'ethers'; 3 | import { useEffect } from 'react'; 4 | import { 5 | NftsQuery, 6 | NftsDocument, 7 | } from 'types/generated/graphql/eip721subgraph'; 8 | import { NFTEntity } from 'types/NFT'; 9 | import { useQuery } from 'urql'; 10 | 11 | export const useNFTs = (address: string) => { 12 | const [result] = useQuery({ 13 | query: NftsDocument, 14 | variables: { 15 | address: address.toLowerCase(), 16 | }, 17 | }); 18 | 19 | const { data, fetching, error } = result; 20 | 21 | const rawNfts = data?.account?.tokens || []; 22 | 23 | const nfts: NFTEntity[] = rawNfts.map((nft) => ({ 24 | ...nft, 25 | identifier: ethers.BigNumber.from(nft.identifier), 26 | })); 27 | 28 | useEffect(() => { 29 | if (error) { 30 | captureException(error); 31 | } 32 | }, [error]); 33 | 34 | return { fetching, error, nfts }; 35 | }; 36 | -------------------------------------------------------------------------------- /hooks/useNetworkSpecificStyles/index.ts: -------------------------------------------------------------------------------- 1 | export { useNetworkSpecificStyles } from './useNetworkSpecificStyles'; 2 | -------------------------------------------------------------------------------- /hooks/useNetworkSpecificStyles/useNetworkSpecificStyles.ts: -------------------------------------------------------------------------------- 1 | import { SupportedNetwork } from 'lib/config'; 2 | import { useEffect } from 'react'; 3 | 4 | type StyleRule = [variableName: string, style: string]; 5 | 6 | const RADIAL_NAME = '--background-radial-gradient'; 7 | const RADIAL_DEFAULT = 'radial-gradient(#ffffff, #f6f2f0)'; 8 | const BASE_RADIAL: StyleRule = [RADIAL_NAME, RADIAL_DEFAULT]; 9 | 10 | export const STYLE_MAP: { [network in SupportedNetwork]: StyleRule[] } = { 11 | ethereum: [BASE_RADIAL], 12 | rinkeby: [BASE_RADIAL], 13 | optimism: [[RADIAL_NAME, 'var(--optimistic-red-10)']], 14 | polygon: [[RADIAL_NAME, 'var(--polygon-purple-10)']], 15 | }; 16 | 17 | export function useNetworkSpecificStyles(network: SupportedNetwork) { 18 | useEffect(() => { 19 | const rules = STYLE_MAP[network]; 20 | rules.forEach(([variableName, style]) => { 21 | document.documentElement.style.setProperty(variableName, style); 22 | }); 23 | }, [network]); 24 | } 25 | -------------------------------------------------------------------------------- /hooks/useOnClickOutside/index.ts: -------------------------------------------------------------------------------- 1 | export { useOnClickOutside } from './useOnClickOutside'; 2 | -------------------------------------------------------------------------------- /hooks/useOnClickOutside/useOnClickOutside.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect } from 'react'; 2 | 3 | type Handler = (event: MouseEvent) => void; 4 | 5 | export function useOnClickOutside( 6 | ref: RefObject, 7 | handler: Handler, 8 | mouseEvent: 'mousedown' | 'mouseup' = 'mousedown', 9 | ): void { 10 | useEffect(() => { 11 | const listener = (event: MouseEvent) => { 12 | if (!ref.current || ref.current.contains(event.target as Node)) { 13 | return; 14 | } 15 | handler(event); 16 | }; 17 | document.addEventListener(mouseEvent, listener); 18 | return () => { 19 | document.removeEventListener(mouseEvent, listener); 20 | }; 21 | }, [ref, handler, mouseEvent]); 22 | } 23 | 24 | export default useOnClickOutside; 25 | -------------------------------------------------------------------------------- /hooks/useOnScreenRef.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, RefObject } from 'react'; 2 | 3 | // Hook 4 | export function useOnScreen(ref: RefObject, rootMargin = '0px') { 5 | // State and setter for storing whether element is visible 6 | const [isIntersecting, setIntersecting] = useState(false); 7 | useEffect(() => { 8 | const current = ref.current; 9 | const observer = new IntersectionObserver( 10 | ([entry]) => { 11 | // Update our state when observer callback fires 12 | setIntersecting(entry.isIntersecting); 13 | }, 14 | { 15 | rootMargin, 16 | }, 17 | ); 18 | if (current) { 19 | observer.observe(current); 20 | } 21 | return () => { 22 | if (current) { 23 | observer.unobserve(current); 24 | } 25 | }; 26 | }, [ref, rootMargin]); 27 | return isIntersecting; 28 | } 29 | -------------------------------------------------------------------------------- /hooks/useTimestamp/index.ts: -------------------------------------------------------------------------------- 1 | export { useTimestamp } from './useTimestamp'; 2 | -------------------------------------------------------------------------------- /hooks/useTokenMetadata/index.ts: -------------------------------------------------------------------------------- 1 | export { useTokenMetadata } from './useTokenMetadata'; 2 | export type { MaybeNFTMetadata, CollateralSpec } from './useTokenMetadata'; 3 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const nextJest = require('next/jest'); 2 | 3 | const createJestConfig = nextJest({ 4 | dir: './', 5 | }); 6 | 7 | const customJestConfig = { 8 | collectCoverageFrom: [ 9 | 'components/**/*.{js,jsx,ts,tsx}', 10 | 'hooks/**/*.{js,jsx,ts,tsx}', 11 | 'lib/**/*.{js,jsx,ts,tsx}', 12 | '!**/index.ts', 13 | '!**/*.d.ts', 14 | '!**/node_modules/**', 15 | ], 16 | coverageReporters: ['json-summary', 'text'], 17 | coverageThreshold: { 18 | global: { 19 | branches: 31, 20 | functions: 37.5, 21 | lines: 46, 22 | statements: 48.5, 23 | }, 24 | }, 25 | moduleDirectories: ['node_modules', ''], 26 | setupFilesAfterEnv: ['/jest.setup.js'], 27 | testEnvironment: 'jsdom', 28 | testPathIgnorePatterns: ['/pages/'], 29 | globalSetup: '/global-setup.js', 30 | }; 31 | 32 | module.exports = createJestConfig(customJestConfig); 33 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import dontenv from 'dotenv'; 3 | import '@testing-library/jest-dom/extend-expect'; 4 | import fetchMock from 'jest-fetch-mock'; 5 | import { TextEncoder, TextDecoder } from 'util'; 6 | import { configs } from './lib/config'; 7 | 8 | // Minimize console output when testing failure cases 9 | global.console.error = jest.fn(); 10 | 11 | dontenv.config({ path: path.resolve(__dirname, './.env.test') }); 12 | 13 | global.fetch = fetchMock; 14 | global.TextEncoder = TextEncoder; 15 | global.TextDecoder = TextDecoder; 16 | 17 | jest.mock('./hooks/useConfig', () => ({ 18 | ...jest.requireActual('./hooks/useConfig'), 19 | useConfig: jest.fn(() => configs.rinkeby), 20 | })); 21 | -------------------------------------------------------------------------------- /lib/authorizations/authorizeCurrency.ts: -------------------------------------------------------------------------------- 1 | import { captureException } from '@sentry/nextjs'; 2 | import { ethers, Signer } from 'ethers'; 3 | import { SupportedNetwork } from 'lib/config'; 4 | import { contractDirectory, web3Erc20Contract } from 'lib/contracts'; 5 | 6 | type AllowParams = { 7 | callback: () => void; 8 | contractAddress: string; 9 | network: SupportedNetwork; 10 | signer: Signer; 11 | setTxHash: (value: string) => void; 12 | setWaitingForTx: (value: boolean) => void; 13 | }; 14 | export async function authorizeCurrency({ 15 | callback, 16 | contractAddress, 17 | network, 18 | signer, 19 | setTxHash, 20 | setWaitingForTx, 21 | }: AllowParams) { 22 | const contract = web3Erc20Contract(contractAddress, signer); 23 | const t = await contract.approve( 24 | contractDirectory[network].loanFacilitator, 25 | ethers.BigNumber.from(2).pow(256).sub(1), 26 | ); 27 | setWaitingForTx(true); 28 | setTxHash(t.hash); 29 | t.wait() 30 | .then(() => { 31 | callback(); 32 | setWaitingForTx(false); 33 | }) 34 | .catch((err) => { 35 | setWaitingForTx(false); 36 | captureException(err); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /lib/aws/config.ts: -------------------------------------------------------------------------------- 1 | export const awsConfig = { 2 | region: 'us-east-1', 3 | credentials: { 4 | accessKeyId: process.env.AMAZON_WEB_SERVICES_ACCESS_KEY!, 5 | secretAccessKey: process.env.AMAZON_WEB_SERVICES_SECRET_KEY!, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /lib/coingecko.ts: -------------------------------------------------------------------------------- 1 | import { captureException } from '@sentry/nextjs'; 2 | import { SupportedNetwork } from './config'; 3 | 4 | // based on output from https://api.coingecko.com/api/v3/asset_platforms 5 | const networkMap = { 6 | optimism: 'optimistic-ethereum', 7 | ethereum: 'ethereum', 8 | polygon: 'polygon-pos', 9 | }; 10 | 11 | export async function getUnitPriceForEth(toCurrency: string) { 12 | try { 13 | const res = await fetch( 14 | `https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=${toCurrency}`, 15 | ); 16 | const json = await res.json(); 17 | 18 | if (json) { 19 | return json.ethereum[toCurrency] as number | undefined; 20 | } 21 | } catch (e) { 22 | captureException(e); 23 | } 24 | return undefined; 25 | } 26 | 27 | export async function getUnitPriceForCoin( 28 | tokenAddress: string, 29 | toCurrency: string, 30 | network?: SupportedNetwork, 31 | ): Promise { 32 | if (network === 'rinkeby') { 33 | return 1.01; 34 | } 35 | 36 | if (network && networkMap[network]) { 37 | const statsRes = await fetch( 38 | `https://api.coingecko.com/api/v3/coins/${networkMap[network]}/contract/${tokenAddress}`, 39 | ); 40 | 41 | const json = await statsRes.json(); 42 | 43 | return json?.market_data?.current_price[toCurrency]; 44 | } 45 | 46 | // unhandled network 47 | return undefined; 48 | } 49 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | 3 | export const SECONDS_IN_A_DAY = 60 * 60 * 24; 4 | export const SECONDS_IN_AN_HOUR = 60 * 60; 5 | export const SECONDS_IN_A_YEAR = 31_536_000; 6 | export const INTEREST_RATE_PERCENT_DECIMALS = 3; 7 | export const MIN_RATE = 1 / 10 ** (INTEREST_RATE_PERCENT_DECIMALS - 2); 8 | 9 | export const SCALAR = ethers.BigNumber.from(10).pow( 10 | INTEREST_RATE_PERCENT_DECIMALS, 11 | ); 12 | 13 | export const DISCORD_URL = 'https://discord.gg/ZCxGuE6Ytk'; 14 | export const DISCORD_ERROR_CHANNEL = '#🪲bug-reports'; 15 | export const TWITTER_URL = 'https://twitter.com/backed_xyz'; 16 | export const GITHUB_URL = 'https://github.com/with-backed'; 17 | export const FAQ_URL = 18 | 'https://with-backed.notion.site/FAQ-df65a5002100406eb6c5211fb8e105cf'; 19 | export const BUNNY_IMG_URL_MAP = { 20 | ethereum: '/logos/backed-bunny.png', 21 | rinkeby: '/logos/backed-bunny.png', 22 | optimism: '/logos/opbunny.png', 23 | polygon: '/logos/pbunny.png', 24 | }; 25 | 26 | export const COMMUNITY_NFT_CONTRACT_ADDRESS = 27 | '0x63a9addF2327A0F4B71BcF9BFa757E333e1B7177'; 28 | export const COMMUNITY_NFT_SUBGRAPH = 29 | 'https://api.thegraph.com/subgraphs/name/with-backed/backed-community-nft'; 30 | -------------------------------------------------------------------------------- /lib/eip721Subraph.ts: -------------------------------------------------------------------------------- 1 | import { NFTEntity } from 'types/NFT'; 2 | import { contractDirectory } from 'lib/contracts'; 3 | import { SupportedNetwork } from './config'; 4 | 5 | export function hiddenNFTAddresses(network: SupportedNetwork) { 6 | const directory = contractDirectory[network]; 7 | return [directory.borrowTicket, directory.lendTicket].map((a) => 8 | a.toLowerCase(), 9 | ); 10 | } 11 | 12 | export function getNftContractAddress(nft: NFTEntity): string { 13 | return nft.id.substring(0, 42); 14 | } 15 | 16 | export function isNFTApprovedForCollateral( 17 | nft: NFTEntity, 18 | network: SupportedNetwork, 19 | ): boolean { 20 | return ( 21 | nft.approvals.filter( 22 | (approval: any) => 23 | approval.approved.id.toLowerCase() === 24 | contractDirectory[network].loanFacilitator.toLowerCase(), 25 | ).length > 0 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /lib/erc20Helper.ts: -------------------------------------------------------------------------------- 1 | export type ERC20Amount = { 2 | nominal: string; 3 | symbol: string; 4 | address: string; 5 | }; 6 | -------------------------------------------------------------------------------- /lib/events/consumers/attachmentsHelper.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export async function getPngBufferFromBase64SVG( 4 | base: string, 5 | contractAddress: string, 6 | ): Promise { 7 | const pngBufferRes = await axios.post(`${process.env.SVG_TO_PNG_URL!}`, { 8 | svg: base, 9 | contractAddress, 10 | }); 11 | return pngBufferRes.data.pngBuffer; 12 | } 13 | -------------------------------------------------------------------------------- /lib/events/consumers/discord/shared.ts: -------------------------------------------------------------------------------- 1 | export enum DiscordMetric { 2 | NUM_LOANS_CREATED = 'numLoansCreated', 3 | NUM_LOANS_LENT_TO = 'numLoansLentTo', 4 | DOLLAR_LOANS_LENT_TO = 'dollarLoansLentTo', 5 | } 6 | -------------------------------------------------------------------------------- /lib/events/consumers/getNftInfoForAttachment.ts: -------------------------------------------------------------------------------- 1 | import { SupportedNetwork } from 'lib/config'; 2 | import { getMedia } from 'lib/getNFTInfo'; 3 | import { NFTResponseData } from 'lib/getNFTInfo'; 4 | 5 | const JSON_PREFIX = 'data:application/json;base64,'; 6 | 7 | export async function getNFTInfoForAttachment( 8 | collateralContractAddress: string, 9 | collateralTokenId: string, 10 | siteUrl: string, 11 | network: SupportedNetwork, 12 | ): Promise { 13 | let NFTInfo: NFTResponseData; 14 | 15 | const tokenURIRes = await fetch( 16 | `${siteUrl}/api/network/${network}/nftInfo/${collateralContractAddress}/${collateralTokenId}`, 17 | ); 18 | NFTInfo = await tokenURIRes.json(); 19 | 20 | return NFTInfo; 21 | } 22 | -------------------------------------------------------------------------------- /lib/events/consumers/twitter/api.ts: -------------------------------------------------------------------------------- 1 | import { TwitterClient } from 'twitter-api-client'; 2 | 3 | export async function tweet( 4 | content: string, 5 | imageAttachment: string | undefined, 6 | ) { 7 | const client = new TwitterClient({ 8 | apiKey: process.env.TWITTER_API_KEY!, 9 | apiSecret: process.env.TWITTER_API_SECRET!, 10 | accessToken: process.env.TWITTER_ACCESS_TOKEN!, 11 | accessTokenSecret: process.env.TWITTER_ACCESS_TOKEN_SECRET!, 12 | }); 13 | 14 | const mediaIdRes = await client.media.mediaUpload({ 15 | media_data: imageAttachment, 16 | }); 17 | 18 | await client.tweetsV2.createTweet({ 19 | text: content, 20 | media: { 21 | media_ids: [mediaIdRes.media_id_string], 22 | }, 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /lib/events/consumers/twitter/attachments.ts: -------------------------------------------------------------------------------- 1 | import { NFTResponseData } from 'lib/getNFTInfo'; 2 | import fetch from 'node-fetch'; 3 | import { getPngBufferFromBase64SVG } from '../attachmentsHelper'; 4 | 5 | const SVG_PREFIX = 'data:image/svg+xml;base64,'; 6 | 7 | export async function nftResponseDataToImageBuffer( 8 | nftResponseData: NFTResponseData, 9 | contractAddress: string, 10 | ): Promise { 11 | if (nftResponseData?.image?.mediaUrl.startsWith(SVG_PREFIX)) { 12 | return await getPngBufferFromBase64SVG( 13 | nftResponseData!.image!.mediaUrl, 14 | contractAddress, 15 | ); 16 | } else { 17 | const imageUrlRes = await fetch(nftResponseData!.image!.mediaUrl); 18 | const arraybuffer = await imageUrlRes.arrayBuffer(); 19 | const outputBuffer = Buffer.from(arraybuffer); 20 | return outputBuffer.toString('base64'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/events/consumers/userNotifications/emails/genericFormatter.ts: -------------------------------------------------------------------------------- 1 | export enum GenericEmailType { 2 | CONFIRMATION = 'confirmation', 3 | } 4 | 5 | export type GenericEmailComponents = { 6 | mainMessage: string; 7 | footer: string; 8 | }; 9 | 10 | const throwInvalidGenericEmailType = (_: never): never => { 11 | throw new Error('Invalid generic email type passed'); 12 | }; 13 | 14 | export function getSubjectForGenericEmail(type: GenericEmailType): string { 15 | switch (type) { 16 | case GenericEmailType.CONFIRMATION: 17 | return 'Backed: Email request received'; 18 | default: 19 | return throwInvalidGenericEmailType(type); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/events/consumers/userNotifications/emails/ses.ts: -------------------------------------------------------------------------------- 1 | import aws from 'aws-sdk'; 2 | import { awsConfig } from 'lib/aws/config'; 3 | 4 | const baseParams: aws.SES.Types.SendEmailRequest = { 5 | Source: process.env.BACKED_NOTIFICATIONS_EMAIL_ADDRESS!, 6 | Destination: { 7 | ToAddresses: [], 8 | }, 9 | ReplyToAddresses: [process.env.BACKED_NOTIFICATIONS_EMAIL_ADDRESS!], 10 | Message: { 11 | Body: { 12 | Html: { 13 | Charset: 'UTF-8', 14 | Data: '', 15 | }, 16 | }, 17 | Subject: { 18 | Charset: 'UTF-8', 19 | Data: '', 20 | }, 21 | }, 22 | }; 23 | 24 | export async function executeEmailSendWithSes( 25 | html: string, 26 | subject: string, 27 | toAddress: string, 28 | ): Promise { 29 | const params: aws.SES.Types.SendEmailRequest = { 30 | ...baseParams, 31 | Destination: { 32 | ToAddresses: [toAddress], 33 | }, 34 | }; 35 | params.Message.Body.Html!.Data = html; 36 | params.Message.Subject.Data = subject; 37 | 38 | const res = await new aws.SES(awsConfig).sendEmail(params).promise(); 39 | 40 | return res.$response.error || null; 41 | } 42 | -------------------------------------------------------------------------------- /lib/events/consumers/userNotifications/shared.ts: -------------------------------------------------------------------------------- 1 | import { RawEventNameType } from 'types/RawEvent'; 2 | 3 | export enum NotificationMethod { 4 | EMAIL = 'email', 5 | } 6 | 7 | export type NotificationTriggerType = 8 | | RawEventNameType 9 | | 'LiquidationOccurring' 10 | | 'LiquidationOccurred' 11 | | 'All'; 12 | -------------------------------------------------------------------------------- /lib/events/sns/helpers.ts: -------------------------------------------------------------------------------- 1 | import { SupportedNetwork } from 'lib/config'; 2 | import { NextApiResponse } from 'next'; 3 | import { LendEvent as RawSubgraphLendEvent } from 'types/generated/graphql/nftLoans'; 4 | import { RawEventNameType, RawSubgraphEvent } from 'types/RawEvent'; 5 | 6 | export type EventsSNSMessage = { 7 | eventName: RawEventNameType; 8 | event: RawSubgraphEvent; 9 | network: SupportedNetwork; 10 | mostRecentTermsEvent?: RawSubgraphLendEvent; 11 | }; 12 | 13 | export async function confirmTopicSubscription( 14 | body: any, 15 | res: NextApiResponse, 16 | ): Promise { 17 | if ('SubscribeURL' in body) { 18 | try { 19 | await fetch(body['SubscribeURL'], { 20 | method: 'GET', 21 | }); 22 | res.status(200).send('subscription successful'); 23 | } catch (e) { 24 | res.status(400).send('subscription unsuccessful'); 25 | } 26 | return true; 27 | } else { 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/events/sns/push.ts: -------------------------------------------------------------------------------- 1 | import { SNS } from 'aws-sdk'; 2 | import { awsConfig } from 'lib/aws/config'; 3 | import { EventsSNSMessage } from 'lib/events/sns/helpers'; 4 | 5 | export async function pushEventForProcessing({ 6 | eventName, 7 | event, 8 | network, 9 | mostRecentTermsEvent, 10 | }: EventsSNSMessage): Promise { 11 | const sns = new SNS(awsConfig); 12 | 13 | const res = await sns 14 | .publish({ 15 | TopicArn: process.env.EVENTS_SNS_ARN!, 16 | Message: JSON.stringify({ 17 | eventName, 18 | event, 19 | network, 20 | mostRecentTermsEvent, 21 | }), 22 | }) 23 | .promise(); 24 | 25 | return !res.$response.error; 26 | } 27 | -------------------------------------------------------------------------------- /lib/events/sqs/helpers.ts: -------------------------------------------------------------------------------- 1 | import { SQS } from 'aws-sdk'; 2 | import { awsConfig } from 'lib/aws/config'; 3 | import { SupportedNetwork } from 'lib/config'; 4 | import { RawEventNameType } from 'types/RawEvent'; 5 | 6 | export type FormattedNotificationEventMessageType = { 7 | eventName: RawEventNameType; 8 | txHash: string; 9 | receiptHandle: string; 10 | network: SupportedNetwork; 11 | }; 12 | 13 | export async function receiveMessages(): Promise< 14 | FormattedNotificationEventMessageType[] | undefined 15 | > { 16 | const queueUrl = process.env.EVENTS_SQS_URL!; 17 | const sqs = new SQS(awsConfig); 18 | 19 | const response = await sqs.receiveMessage({ QueueUrl: queueUrl }).promise(); 20 | return response.Messages?.map((message) => { 21 | const messageBody: { 22 | txHash: string; 23 | eventName: RawEventNameType; 24 | network: SupportedNetwork; 25 | } = JSON.parse(message.Body!); 26 | return { 27 | ...messageBody, 28 | receiptHandle: message.ReceiptHandle!, 29 | }; 30 | }); 31 | } 32 | 33 | export function deleteMessage(receiptHandle: string) { 34 | const queueUrl = process.env.EVENTS_SQS_URL!; 35 | const sqs = new SQS(awsConfig); 36 | 37 | sqs.deleteMessage( 38 | { QueueUrl: queueUrl, ReceiptHandle: receiptHandle }, 39 | (err, _) => { 40 | if (err) { 41 | console.error(err); 42 | } 43 | }, 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /lib/fetchWithTimeout.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Wrapper for fetch that will timeout after a set amount of time. 3 | * @param RequestInit takes an optional `timeout` field (in ms) 4 | */ 5 | export const fetchWithTimeout: typeof fetch = async ( 6 | input: RequestInfo, 7 | init?: RequestInit & { timeout?: number }, 8 | ) => { 9 | const { timeout = 8000, ...options } = init || {}; 10 | 11 | const controller = new AbortController(); 12 | const id = setTimeout(() => controller.abort(), timeout); 13 | const response = await fetch(input, { 14 | ...options, 15 | signal: controller.signal, 16 | }); 17 | clearTimeout(id); 18 | return response; 19 | }; 20 | -------------------------------------------------------------------------------- /lib/interest.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | import { INTEREST_RATE_PERCENT_DECIMALS, SECONDS_IN_A_YEAR } from './constants'; 3 | 4 | export const formattedAnnualRate = (perAnumScaledRate: ethers.BigNumber) => { 5 | return ethers.utils.formatUnits( 6 | perAnumScaledRate, 7 | INTEREST_RATE_PERCENT_DECIMALS - 2, 8 | ); 9 | }; 10 | 11 | export const annualRateToPerSecond = (annualRate: number): string => { 12 | return Math.ceil( 13 | (annualRate / SECONDS_IN_A_YEAR) * Math.pow(10, 8), 14 | ).toString(); 15 | }; 16 | -------------------------------------------------------------------------------- /lib/loanAssets.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_ASSET_DECIMALS = 18; 2 | export interface LoanAsset { 3 | address: string; 4 | symbol: string; 5 | chainId: number; 6 | } 7 | -------------------------------------------------------------------------------- /lib/loans/collateralSaleInfo.ts: -------------------------------------------------------------------------------- 1 | import { SupportedNetwork } from 'lib/config'; 2 | import { 3 | CollectionStatistics, 4 | getCollectionStats, 5 | } from 'lib/nftCollectionStats'; 6 | 7 | export type CollateralSaleInfo = { 8 | recentSale: { 9 | paymentToken: string; 10 | price: number; 11 | } | null; 12 | collectionStats: CollectionStatistics; 13 | }; 14 | 15 | export async function getCollateralSaleInfo( 16 | nftContractAddress: string, 17 | tokenId: string, 18 | nftSalesSubgraph: string | null, 19 | network: SupportedNetwork, 20 | jsonRpcProvider: string, 21 | ): Promise { 22 | const recentSale = await getMostRecentSale( 23 | nftContractAddress, 24 | tokenId, 25 | nftSalesSubgraph, 26 | network, 27 | jsonRpcProvider, 28 | ); 29 | 30 | const collectionStats = await getCollectionStats( 31 | nftContractAddress, 32 | tokenId, 33 | network, 34 | ); 35 | 36 | return { 37 | collectionStats, 38 | recentSale, 39 | }; 40 | } 41 | 42 | async function getMostRecentSale( 43 | nftContractAddress: string, 44 | tokenId: string, 45 | _nftSalesSubgraph: string | null, // TODO: deprecate 46 | network: SupportedNetwork, 47 | jsonRpcProvider: string, 48 | ): Promise<{ paymentToken: string; price: number } | null> { 49 | // deprecating most recent sale, return null and UI will handle 50 | 51 | return null; 52 | } 53 | -------------------------------------------------------------------------------- /lib/loans/loans.ts: -------------------------------------------------------------------------------- 1 | import subgraphLoans from './subgraph/subgraphLoans'; 2 | import { parseSubgraphLoan } from './utils'; 3 | 4 | export async function loans(nftBackedLoansSubgraph: string) { 5 | const loans = await subgraphLoans(20, nftBackedLoansSubgraph); 6 | 7 | return loans.map((loan) => parseSubgraphLoan(loan)); 8 | } 9 | -------------------------------------------------------------------------------- /lib/loans/subgraph/subgraphLoanById.ts: -------------------------------------------------------------------------------- 1 | import { captureEvent } from '@sentry/nextjs'; 2 | import { clientFromUrl } from 'lib/urql'; 3 | import { 4 | LoanByIdDocument, 5 | LoanByIdQuery, 6 | } from 'types/generated/graphql/nftLoans'; 7 | 8 | export async function subgraphLoanById( 9 | id: string, 10 | nftBackedLoansSubgraph: string, 11 | ) { 12 | const nftBackedLoansClient = clientFromUrl(nftBackedLoansSubgraph); 13 | const { data, error } = await nftBackedLoansClient 14 | .query(LoanByIdDocument, { id }) 15 | .toPromise(); 16 | 17 | if (error) { 18 | captureEvent(error); 19 | } 20 | 21 | if (data?.loan) { 22 | return data.loan; 23 | } 24 | 25 | return null; 26 | } 27 | -------------------------------------------------------------------------------- /lib/nftCollectionStats/index.ts: -------------------------------------------------------------------------------- 1 | import { collectionStatsEthMainnet } from 'lib/nftCollectionStats/reservoir'; 2 | import { collectionStatsRinkeby } from 'lib/nftCollectionStats/mockData'; 3 | import { SupportedNetwork } from 'lib/config'; 4 | import { collectionStatsOptimism } from 'lib/nftCollectionStats/quixotic'; 5 | 6 | export type CollectionStatistics = { 7 | floor: number | null; 8 | items: number | null; 9 | owners: number | null; 10 | volume: number | null; 11 | }; 12 | 13 | export async function getCollectionStats( 14 | contractAddress: string, 15 | tokenId: string, 16 | network: SupportedNetwork, 17 | ): Promise { 18 | switch (network) { 19 | case 'ethereum': 20 | return collectionStatsEthMainnet(contractAddress, tokenId); 21 | case 'optimism': 22 | // quixotic api broke 23 | return nullCollectionStats; 24 | case 'polygon': 25 | return nullCollectionStats; 26 | case 'rinkeby': 27 | return collectionStatsRinkeby(); 28 | default: 29 | return nullCollectionStats; 30 | } 31 | } 32 | 33 | const nullCollectionStats: CollectionStatistics = { 34 | floor: null, 35 | items: null, 36 | owners: null, 37 | volume: null, 38 | }; 39 | -------------------------------------------------------------------------------- /lib/nftCollectionStats/mockData.ts: -------------------------------------------------------------------------------- 1 | export function collectionStatsRinkeby() { 2 | const [items, owners] = getFakeItemsAndOwners(); 3 | return { 4 | floor: getFakeFloor(), 5 | items, 6 | owners, 7 | volume: getFakeVolume(), 8 | }; 9 | } 10 | 11 | // MOCK METHODS TO GENERATE FAKE STATS FOR RINKEBY 12 | export function getFakeFloor(): number { 13 | return Math.floor(Math.random() * (20 + 1)); 14 | } 15 | 16 | export function getFakeItemsAndOwners(): [number, number] { 17 | const items = Math.floor(Math.random() * 800); 18 | const owners = Math.floor(Math.random() * (items - 1)); 19 | 20 | return [items, owners]; 21 | } 22 | 23 | export function getFakeVolume(): number { 24 | return Math.floor(Math.random() * 2000); 25 | } 26 | -------------------------------------------------------------------------------- /lib/nftCollectionStats/quixotic.ts: -------------------------------------------------------------------------------- 1 | import { CollectionStatistics } from 'lib/nftCollectionStats'; 2 | 3 | export async function collectionStatsOptimism( 4 | contractAddress: string, 5 | ): Promise { 6 | const quixoticAssetReq = await fetch( 7 | `https://api.quixotic.io/api/v1/opt/collection/${contractAddress}/stats`, 8 | { 9 | headers: new Headers({ 10 | Accept: 'application/json', 11 | 'x-api-key': process.env.QUIXOTIC_API_KEY!, 12 | }), 13 | }, 14 | ); 15 | 16 | const json = (await quixoticAssetReq.json()) as any; 17 | 18 | return { 19 | floor: json?.stats?.floor_price || null, 20 | items: json?.stats?.total_supply || null, 21 | owners: json?.stats?.num_owners || null, 22 | volume: json?.stats?.total_volume || null, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /lib/nftCollectionStats/reservoir.ts: -------------------------------------------------------------------------------- 1 | import { CollectionStatistics } from 'lib/nftCollectionStats'; 2 | 3 | const reservoirResToStats = (json: any): CollectionStatistics => ({ 4 | floor: json?.collection?.floorAsk.price || null, 5 | items: json?.collection?.tokenCount || null, 6 | owners: json?.collection?.ownerCount || null, 7 | volume: json?.collection?.volume.allTime || null, 8 | }); 9 | 10 | export async function collectionStatsEthMainnet( 11 | contractAddress: string, 12 | tokenId: string, 13 | ): Promise { 14 | const tokensV4Req = await fetch( 15 | `https://api.reservoir.tools/tokens/v4?tokens=${contractAddress}:${tokenId}`, 16 | { 17 | headers: new Headers({ 18 | Accept: 'application/json', 19 | 'x-api-key': process.env.RESERVOIR_API_KEY!, 20 | }), 21 | }, 22 | ); 23 | 24 | const collectionId = (await tokensV4Req.json())?.tokens[0]?.collection?.id; 25 | 26 | const collectionV2Req = await fetch( 27 | `https://api.reservoir.tools/collection/v2?id=${collectionId}`, 28 | { 29 | headers: new Headers({ 30 | Accept: 'application/json', 31 | 'x-api-key': process.env.RESERVOIR_API_KEY!, 32 | }), 33 | }, 34 | ); 35 | 36 | return reservoirResToStats(await collectionV2Req.json()); 37 | } 38 | -------------------------------------------------------------------------------- /lib/notifications/script.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | const prisma = new PrismaClient(); 4 | 5 | /* 6 | This file can be used as a script to run DB operations should we ever need to in the future (proceed with caution) 7 | run with: npx dotenv -e .env.development -- npx ts-node --skip-project notifications/index.ts 8 | */ 9 | 10 | async function main() { 11 | // await prisma.notificationRequest.count; // returns count of all notification requests 12 | } 13 | 14 | main() 15 | .catch((e) => { 16 | throw e; 17 | }) 18 | .finally(async () => { 19 | await prisma.$disconnect(); 20 | }); 21 | -------------------------------------------------------------------------------- /lib/parseSerializedResponse.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | 3 | export function parseSerializedResponse(jsonString: string) { 4 | const parsed = JSON.parse(jsonString); 5 | if (Array.isArray(parsed)) { 6 | return parsed.map(toObjectWithBigNumbers); 7 | } 8 | 9 | return toObjectWithBigNumbers(parsed); 10 | } 11 | 12 | /** 13 | * Given an object, return a new object with the same values, except all serialized BigNumbers are instantiated. 14 | * @param obj 15 | */ 16 | export function toObjectWithBigNumbers(obj: { [key: string]: any }) { 17 | const result = Object.assign({}, obj); 18 | Object.keys(result).forEach((key) => { 19 | if (result[key] && result[key].hex) { 20 | result[key] = ethers.BigNumber.from(result[key]); 21 | } 22 | }); 23 | return result; 24 | } 25 | -------------------------------------------------------------------------------- /lib/pirsch.ts: -------------------------------------------------------------------------------- 1 | export const pirsch: typeof window.pirsch = (...args) => { 2 | // Pirsch may be blocked by extensions; only try to send if it really exists 3 | if (window.pirsch) { 4 | window.pirsch(...args); 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /lib/signedMessages.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | 3 | export function generateAddressFromSignedMessage( 4 | signedMessage: string, 5 | ): string | null { 6 | try { 7 | const address = ethers.utils.verifyMessage( 8 | process.env.NEXT_PUBLIC_NOTIFICATION_REQ_MESSAGE!, 9 | signedMessage, 10 | ); 11 | return address; 12 | } catch (_e) { 13 | return null; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/text.ts: -------------------------------------------------------------------------------- 1 | import { Event } from 'types/Event'; 2 | 3 | type EventName = Pick['typename']; 4 | 5 | const eventNames: { 6 | [key in EventName]: string; 7 | } = { 8 | BuyoutEvent: 'Buyout Event', 9 | CloseEvent: 'Close Loan', 10 | CreateEvent: 'Create Loan', 11 | RepaymentEvent: 'Repay Loan', 12 | CollateralSeizureEvent: 'Seize Collateral', 13 | LendEvent: 'Lend Event', 14 | }; 15 | 16 | export function renderEventName(name: EventName) { 17 | return eventNames[name]; 18 | } 19 | -------------------------------------------------------------------------------- /lib/urql.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@urql/core'; 2 | 3 | // The local document cache is not very useful in our use case, because it 4 | // caches everything and only invalidates when there's a mutation. We never 5 | // perform mutations on data in the subgraphs, so our local cache would never 6 | // be invalidated. 7 | const requestPolicy = 'network-only'; 8 | 9 | export function clientFromUrl(url: string) { 10 | return createClient({ 11 | url, 12 | requestPolicy, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { Custom404 } from 'components/Custom404'; 2 | import Head from 'next/head'; 3 | 4 | export default function Page() { 5 | return ( 6 | <> 7 | 8 | Backed | 404 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /pages/500.tsx: -------------------------------------------------------------------------------- 1 | import { Custom500 } from 'components/Custom500'; 2 | import Head from 'next/head'; 3 | 4 | export default function Page() { 5 | return ( 6 | <> 7 | 8 | Backed | 500 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /pages/about.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { OpenGraph } from 'components/OpenGraph'; 3 | import { BUNNY_IMG_URL_MAP } from 'lib/constants'; 4 | import { HeaderInfo } from 'components/HeaderInfo'; 5 | 6 | export default function About() { 7 | return ( 8 | <> 9 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /pages/api/events/_middleware.ts: -------------------------------------------------------------------------------- 1 | import { authenticateRequest, AUTH_STATUS } from 'lib/authentication'; 2 | import { NextFetchEvent, NextRequest, NextResponse } from 'next/server'; 3 | 4 | export default function middleware(req: NextRequest, _ev?: NextFetchEvent) { 5 | try { 6 | const authStatus = authenticateRequest(req); 7 | if (authStatus == AUTH_STATUS.ok) { 8 | return NextResponse.next(); 9 | } else { 10 | return new NextResponse(undefined, { 11 | status: authStatus, 12 | headers: { 13 | 'WWW-Authenticate': 'Basic realm', 14 | }, 15 | }); 16 | } 17 | } catch (e) { 18 | console.error(e); 19 | return new NextResponse(undefined, { status: 500 }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pages/api/events/community/nominationMessage.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { captureException, withSentry } from '@sentry/nextjs'; 3 | import { sendBotMessage } from 'lib/events/consumers/discord/bot'; 4 | 5 | async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method != 'POST') { 7 | res.status(405).send('Only POST requests allowed'); 8 | return; 9 | } 10 | 11 | try { 12 | const { ethAddress, category, reason, value } = req.body; 13 | 14 | const message = `**New Form Nomination for ${ethAddress}** 15 | Category: ${category} 16 | Value: ${value} XP 17 | Reason: ${reason} 18 | `; 19 | 20 | await sendBotMessage(message, process.env.NOMINATION_CHANNEL_ID!); 21 | 22 | res.status(200).json(`discord nomination bot message successfully sent`); 23 | } catch (e) { 24 | captureException(e); 25 | res.status(404); 26 | } 27 | } 28 | 29 | export default withSentry(handler); 30 | -------------------------------------------------------------------------------- /pages/api/events/consumers/discord.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { 3 | confirmTopicSubscription, 4 | EventsSNSMessage, 5 | } from 'lib/events/sns/helpers'; 6 | import { sendBotUpdateForTriggerAndEntity } from 'lib/events/consumers/discord/formatter'; 7 | import { captureException, withSentry } from '@sentry/nextjs'; 8 | import { configs } from 'lib/config'; 9 | 10 | async function handler(req: NextApiRequest, res: NextApiResponse) { 11 | if (req.method != 'POST') { 12 | res.status(405).send('Only POST requests allowed'); 13 | return; 14 | } 15 | 16 | const parsedBody = JSON.parse(req.body); 17 | 18 | const isConfirmingSubscribe = await confirmTopicSubscription(parsedBody, res); 19 | if (isConfirmingSubscribe) { 20 | return; 21 | } 22 | 23 | try { 24 | const { eventName, event, mostRecentTermsEvent, network } = JSON.parse( 25 | parsedBody['Message'], 26 | ) as EventsSNSMessage; 27 | 28 | const now = Math.floor(new Date().getTime() / 1000); 29 | await sendBotUpdateForTriggerAndEntity( 30 | eventName, 31 | event, 32 | configs[network], 33 | mostRecentTermsEvent, 34 | ); 35 | 36 | res.status(200).json(`discord bot messages successfully sent`); 37 | } catch (e) { 38 | captureException(e); 39 | res.status(404); 40 | } 41 | } 42 | 43 | export default withSentry(handler); 44 | -------------------------------------------------------------------------------- /pages/api/events/consumers/twitter.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { 3 | confirmTopicSubscription, 4 | EventsSNSMessage, 5 | } from 'lib/events/sns/helpers'; 6 | import { sendTweetForTriggerAndEntity } from 'lib/events/consumers/twitter/formatter'; 7 | import { captureException, withSentry } from '@sentry/nextjs'; 8 | import { configs } from 'lib/config'; 9 | 10 | async function handler(req: NextApiRequest, res: NextApiResponse) { 11 | if (req.method != 'POST') { 12 | res.status(405).send('Only POST requests allowed'); 13 | return; 14 | } 15 | 16 | const parsedBody = JSON.parse(req.body); 17 | 18 | const isConfirmingSubscribe = await confirmTopicSubscription(parsedBody, res); 19 | if (isConfirmingSubscribe) { 20 | return; 21 | } 22 | 23 | try { 24 | const { eventName, event, mostRecentTermsEvent, network } = JSON.parse( 25 | parsedBody['Message'], 26 | ) as EventsSNSMessage; 27 | 28 | await sendTweetForTriggerAndEntity( 29 | eventName, 30 | event, 31 | configs[network], 32 | mostRecentTermsEvent, 33 | ); 34 | 35 | res.status(200).json(`tweet successfully sent`); 36 | } catch (e) { 37 | captureException(e); 38 | res.status(404); 39 | } 40 | } 41 | 42 | export default withSentry(handler); 43 | -------------------------------------------------------------------------------- /pages/api/events/consumers/userNotifications.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { sendEmailsForTriggerAndEntity } from 'lib/events/consumers/userNotifications/emails/emails'; 3 | import { 4 | confirmTopicSubscription, 5 | EventsSNSMessage, 6 | } from 'lib/events/sns/helpers'; 7 | import { captureException, withSentry } from '@sentry/nextjs'; 8 | import { configs } from 'lib/config'; 9 | 10 | async function handler(req: NextApiRequest, res: NextApiResponse) { 11 | if (req.method != 'POST') { 12 | res.status(405).send('Only POST requests allowed'); 13 | return; 14 | } 15 | 16 | const parsedBody = JSON.parse(req.body); 17 | 18 | const isConfirmingSubscribe = await confirmTopicSubscription(parsedBody, res); 19 | if (isConfirmingSubscribe) { 20 | return; 21 | } 22 | 23 | try { 24 | const { eventName, event, mostRecentTermsEvent, network } = JSON.parse( 25 | parsedBody['Message'], 26 | ) as EventsSNSMessage; 27 | 28 | const now = Math.floor(new Date().getTime() / 1000); 29 | await sendEmailsForTriggerAndEntity( 30 | eventName, 31 | event, 32 | now, 33 | configs[network], 34 | mostRecentTermsEvent, 35 | ); 36 | 37 | res.status(200).json(`notifications successfully sent`); 38 | } catch (e) { 39 | captureException(e); 40 | res.status(404); 41 | } 42 | } 43 | 44 | export default withSentry(handler); 45 | -------------------------------------------------------------------------------- /pages/api/events/cron/processNewOnchainEvents.ts: -------------------------------------------------------------------------------- 1 | import { captureException, withSentry } from '@sentry/nextjs'; 2 | import { main } from 'lib/events/sqs/consumer'; 3 | import { NextApiRequest, NextApiResponse } from 'next'; 4 | 5 | async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method != 'POST') { 7 | res.status(405).send('Only POST requests allowed'); 8 | return; 9 | } 10 | 11 | try { 12 | await main(); 13 | res.status(200).json({ success: true }); 14 | } catch (e) { 15 | captureException(e); 16 | res.status(404); 17 | } 18 | } 19 | 20 | export default withSentry(handler); 21 | -------------------------------------------------------------------------------- /pages/api/network/[network]/addresses/[address]/loans/index.ts: -------------------------------------------------------------------------------- 1 | import { getAllLoansForAddress } from 'lib/loans/subgraph/getAllLoansEventsForAddress'; 2 | import { Loan as SubgraphLoan } from 'types/generated/graphql/nftLoans'; 3 | import { NextApiRequest, NextApiResponse } from 'next'; 4 | import { captureException, withSentry } from '@sentry/nextjs'; 5 | import { configs, SupportedNetwork, validateNetwork } from 'lib/config'; 6 | 7 | async function handler( 8 | req: NextApiRequest, 9 | res: NextApiResponse, 10 | ) { 11 | try { 12 | validateNetwork(req.query); 13 | const { address, network } = req.query; 14 | const config = configs[network as SupportedNetwork]; 15 | const loans = await getAllLoansForAddress( 16 | address as string, 17 | config.nftBackedLoansSubgraph, 18 | ); 19 | res.status(200).json(loans); 20 | } catch (e) { 21 | captureException(e); 22 | res.status(404); 23 | } 24 | } 25 | 26 | export default withSentry(handler); 27 | -------------------------------------------------------------------------------- /pages/api/network/[network]/loans/[id].tsx: -------------------------------------------------------------------------------- 1 | import { subgraphLoanById } from 'lib/loans/subgraph/subgraphLoanById'; 2 | import { LoanByIdQuery } from 'types/generated/graphql/nftLoans'; 3 | import { NextApiRequest, NextApiResponse } from 'next'; 4 | import { captureException, withSentry } from '@sentry/nextjs'; 5 | import { configs, SupportedNetwork, validateNetwork } from 'lib/config'; 6 | 7 | // TODO: Should probably not just relying on 8 | // the subgraph, but fall back to the node, if the call didn't work 9 | // TODO: is this route used? 10 | async function handler( 11 | req: NextApiRequest, 12 | res: NextApiResponse, 13 | ) { 14 | try { 15 | validateNetwork(req.query); 16 | const { id, network } = req.query; 17 | const config = configs[network as SupportedNetwork]; 18 | const idString: string = Array.isArray(id) ? id[0] : id; 19 | 20 | const loan = await subgraphLoanById( 21 | idString, 22 | config.nftBackedLoansSubgraph, 23 | ); 24 | res.status(200).json(loan); 25 | } catch (e) { 26 | captureException(e); 27 | res.status(404); 28 | } 29 | } 30 | 31 | export default withSentry(handler); 32 | -------------------------------------------------------------------------------- /pages/api/network/[network]/loans/all.tsx: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import subgraphLoans from 'lib/loans/subgraph/subgraphLoans'; 3 | import { 4 | Loan, 5 | Loan_OrderBy, 6 | OrderDirection, 7 | } from 'types/generated/graphql/nftLoans'; 8 | import { captureException, withSentry } from '@sentry/nextjs'; 9 | import { configs, SupportedNetwork, validateNetwork } from 'lib/config'; 10 | 11 | async function handler( 12 | req: NextApiRequest, 13 | res: NextApiResponse, 14 | ) { 15 | try { 16 | validateNetwork(req.query); 17 | const { page, limit, sort, sortDirection, network } = req.query; 18 | const config = configs[network as SupportedNetwork]; 19 | 20 | const loans = await subgraphLoans( 21 | parseInt(limit as string), 22 | config.nftBackedLoansSubgraph, 23 | parseInt(page as string), 24 | sort as Loan_OrderBy, 25 | sortDirection as OrderDirection, 26 | ); 27 | 28 | res.status(200).json(loans); 29 | } catch (e) { 30 | captureException(e); 31 | res.status(404); 32 | } 33 | } 34 | 35 | export default withSentry(handler); 36 | -------------------------------------------------------------------------------- /pages/api/network/[network]/loans/history/[id].ts: -------------------------------------------------------------------------------- 1 | import { Event } from 'types/Event'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import { nodeLoanEventsById } from 'lib/loans/node/nodeLoanEventsById'; 4 | import { captureException, withSentry } from '@sentry/nextjs'; 5 | import { configs, SupportedNetwork, validateNetwork } from 'lib/config'; 6 | 7 | async function handler( 8 | req: NextApiRequest, 9 | res: NextApiResponse, 10 | ) { 11 | try { 12 | validateNetwork(req.query); 13 | const { id, network } = req.query as { 14 | id: string; 15 | network: SupportedNetwork; 16 | }; 17 | const idString: string = Array.isArray(id) ? id[0] : id; 18 | const config = configs[network]; 19 | 20 | const events = await nodeLoanEventsById( 21 | idString, 22 | config.jsonRpcProvider, 23 | config.facilitatorStartBlock, 24 | network, 25 | ); 26 | 27 | res.status(200).json(events); 28 | } catch (e) { 29 | captureException(e); 30 | res.status(404); 31 | } 32 | } 33 | 34 | export default withSentry(handler); 35 | -------------------------------------------------------------------------------- /pages/api/sharedTypes.ts: -------------------------------------------------------------------------------- 1 | export type APIErrorMessage = { 2 | message: string; 3 | }; 4 | -------------------------------------------------------------------------------- /pages/community/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { CommunityPage } from 'components/CommunityPageContent'; 3 | import { useAccount } from 'wagmi'; 4 | import { useRouter } from 'next/router'; 5 | 6 | export default function Community() { 7 | const { address } = useAccount(); 8 | const router = useRouter(); 9 | 10 | useEffect(() => { 11 | if (address) { 12 | router.push(`/community/${address}`); 13 | } 14 | }, [address, router]); 15 | return ; 16 | } 17 | -------------------------------------------------------------------------------- /pages/community/multisig.tsx: -------------------------------------------------------------------------------- 1 | import { PendingCommunityTransactions } from 'components/PendingCommunityTransactions/PendingCommunityTransactions'; 2 | import { getPendingMultiSigChanges } from 'lib/communityNFT/multisig'; 3 | import { GetServerSideProps } from 'next'; 4 | import { PendingChanges } from 'lib/communityNFT/multisig'; 5 | 6 | export type MultiSigProps = { 7 | multiSigChanges: string; 8 | }; 9 | 10 | export const getServerSideProps: GetServerSideProps = async ( 11 | context, 12 | ) => { 13 | return { 14 | props: { 15 | multiSigChanges: JSON.stringify(await getPendingMultiSigChanges()), 16 | }, 17 | }; 18 | }; 19 | 20 | export default function MultiSig({ multiSigChanges }: MultiSigProps) { 21 | return ( 22 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /pages/network/[network]/loans/create.module.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/pages/network/[network]/loans/create.module.css -------------------------------------------------------------------------------- /pages/network/[network]/loans/create.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CreatePageHeader } from 'components/CreatePageHeader'; 3 | import { GetServerSideProps } from 'next'; 4 | import { SupportedNetwork, validateNetwork } from 'lib/config'; 5 | import { captureException } from '@sentry/nextjs'; 6 | import { OpenGraph } from 'components/OpenGraph'; 7 | import { BUNNY_IMG_URL_MAP } from 'lib/constants'; 8 | import { useConfig } from 'hooks/useConfig'; 9 | import capitalize from 'lodash/capitalize'; 10 | 11 | export const getServerSideProps: GetServerSideProps<{}> = async (context) => { 12 | try { 13 | validateNetwork(context.params!); 14 | } catch (e) { 15 | captureException(e); 16 | return { 17 | notFound: true, 18 | }; 19 | } 20 | 21 | return { 22 | props: {}, 23 | }; 24 | }; 25 | 26 | export default function Create() { 27 | const { network } = useConfig(); 28 | return ( 29 | <> 30 | 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /pages/network/[network]/profile/[address].module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | margin: var(--gap) 0; 3 | display: flex; 4 | flex-direction: column; 5 | gap: var(--gap); 6 | width: 100%; 7 | max-width: var(--max-width); 8 | } 9 | 10 | .wrapper > div[role='checkbox'] { 11 | align-self: flex-start; 12 | } 13 | 14 | @media screen and (max-width: 700px) { 15 | .wrapper { 16 | margin: calc(var(--gap) / 2) 0; 17 | gap: 0; 18 | } 19 | 20 | .wrapper > div[role='checkbox'] { 21 | align-self: center; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /prisma/migrations/20220209173148_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "NotificationRequest" ( 3 | "id" SERIAL NOT NULL, 4 | "ethAddress" TEXT NOT NULL, 5 | "event" TEXT NOT NULL, 6 | "deliveryMethod" TEXT NOT NULL, 7 | "deliveryDestination" TEXT NOT NULL, 8 | 9 | CONSTRAINT "NotificationRequest_pkey" PRIMARY KEY ("id") 10 | ); 11 | -------------------------------------------------------------------------------- /prisma/migrations/20220210183028_varchar/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to alter the column `ethAddress` on the `NotificationRequest` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(40)`. 5 | - You are about to alter the column `event` on the `NotificationRequest` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(40)`. 6 | - You are about to alter the column `deliveryMethod` on the `NotificationRequest` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(40)`. 7 | - You are about to alter the column `deliveryDestination` on the `NotificationRequest` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(40)`. 8 | 9 | */ 10 | -- AlterTable 11 | ALTER TABLE "NotificationRequest" ALTER COLUMN "ethAddress" SET DATA TYPE VARCHAR(40), 12 | ALTER COLUMN "event" SET DATA TYPE VARCHAR(40), 13 | ALTER COLUMN "deliveryMethod" SET DATA TYPE VARCHAR(40), 14 | ALTER COLUMN "deliveryDestination" SET DATA TYPE VARCHAR(40); 15 | -------------------------------------------------------------------------------- /prisma/migrations/20220210183615_eth_address_42/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "NotificationRequest" ALTER COLUMN "ethAddress" SET DATA TYPE VARCHAR(42); 3 | -------------------------------------------------------------------------------- /prisma/migrations/20220223155259_timestamp_notifications/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "LastTimestampForNotifications" ( 3 | "id" SERIAL NOT NULL, 4 | "lastWrittenTimestamp" INTEGER NOT NULL, 5 | 6 | CONSTRAINT "LastTimestampForNotifications_pkey" PRIMARY KEY ("id") 7 | ); 8 | -------------------------------------------------------------------------------- /prisma/migrations/20220329220615_uuid/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The primary key for the `NotificationRequest` table will be changed. If it partially fails, the table could be left without primary key constraint. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "NotificationRequest" DROP CONSTRAINT "NotificationRequest_pkey", 9 | ALTER COLUMN "id" DROP DEFAULT, 10 | ALTER COLUMN "id" SET DATA TYPE TEXT, 11 | ADD CONSTRAINT "NotificationRequest_pkey" PRIMARY KEY ("id"); 12 | DROP SEQUENCE "NotificationRequest_id_seq"; 13 | -------------------------------------------------------------------------------- /prisma/migrations/20220401180609_discord_metrics/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "DiscordMetrics" ( 3 | "id" SERIAL NOT NULL, 4 | "numLoansCreated" INTEGER NOT NULL, 5 | "numLoansLentTo" INTEGER NOT NULL, 6 | "dollarLoansLentTo" INTEGER NOT NULL, 7 | 8 | CONSTRAINT "DiscordMetrics_pkey" PRIMARY KEY ("id") 9 | ); 10 | -------------------------------------------------------------------------------- /prisma/migrations/20220419152427_backed_metrics/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `DiscordMetrics` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropTable 8 | DROP TABLE "DiscordMetrics"; 9 | 10 | -- CreateTable 11 | CREATE TABLE "BackedMetrics" ( 12 | "id" SERIAL NOT NULL, 13 | "emailsSentPastDay" INTEGER NOT NULL, 14 | 15 | CONSTRAINT "BackedMetrics_pkey" PRIMARY KEY ("id") 16 | ); 17 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | shadowDatabaseUrl = env("SHADOW_DATABASE_URL") 12 | } 13 | 14 | model NotificationRequest { 15 | id String @id @default(uuid()) 16 | ethAddress String @db.VarChar(42) 17 | event String @db.VarChar(40) 18 | deliveryMethod String @db.VarChar(40) 19 | deliveryDestination String @db.VarChar(40) 20 | } 21 | 22 | model LastTimestampForNotifications { 23 | id Int @id @default(autoincrement()) 24 | lastWrittenTimestamp Int 25 | } 26 | 27 | model BackedMetrics { 28 | id Int @id @default(autoincrement()) 29 | emailsSentPastDay Int 30 | } 31 | -------------------------------------------------------------------------------- /public/angelbunny-XL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/angelbunny-XL.png -------------------------------------------------------------------------------- /public/carousel-images/borrower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/borrower.png -------------------------------------------------------------------------------- /public/carousel-images/buy-out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/buy-out.png -------------------------------------------------------------------------------- /public/carousel-images/continued-availability.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/continued-availability.png -------------------------------------------------------------------------------- /public/carousel-images/contract.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/contract.png -------------------------------------------------------------------------------- /public/carousel-images/deposit-requested.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/deposit-requested.png -------------------------------------------------------------------------------- /public/carousel-images/duration-restart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/duration-restart.png -------------------------------------------------------------------------------- /public/carousel-images/funds-sent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/funds-sent.png -------------------------------------------------------------------------------- /public/carousel-images/loan-matures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/loan-matures.png -------------------------------------------------------------------------------- /public/carousel-images/mint-borrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/mint-borrow.png -------------------------------------------------------------------------------- /public/carousel-images/not-repay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/not-repay.png -------------------------------------------------------------------------------- /public/carousel-images/other-lenders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/other-lenders.png -------------------------------------------------------------------------------- /public/carousel-images/perpetual-buyouts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/perpetual-buyouts.png -------------------------------------------------------------------------------- /public/carousel-images/potential-lenders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/potential-lenders.png -------------------------------------------------------------------------------- /public/carousel-images/repay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/repay.png -------------------------------------------------------------------------------- /public/carousel-images/to-buy-out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/to-buy-out.png -------------------------------------------------------------------------------- /public/carousel-images/transfer-loan-principal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/carousel-images/transfer-loan-principal.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/favicon.ico -------------------------------------------------------------------------------- /public/graph-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/graph-square.png -------------------------------------------------------------------------------- /public/legal/privacy-policy.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/legal/privacy-policy.pdf -------------------------------------------------------------------------------- /public/legal/terms-of-service.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/legal/terms-of-service.pdf -------------------------------------------------------------------------------- /public/loanAssets/local.json: -------------------------------------------------------------------------------- 1 | { 2 | "tokens": [ 3 | { 4 | "address": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82", 5 | "symbol": "DAI" 6 | }, 7 | { 8 | "address": "0x", 9 | "symbol": "USDC" 10 | }, 11 | { 12 | "address": "0x", 13 | "symbol": "WETH" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /public/loanAssets/rinkeby.json: -------------------------------------------------------------------------------- 1 | { 2 | "tokens": [ 3 | { 4 | "address": "0x6916577695D0774171De3ED95d03A3239139Eddb", 5 | "symbol": "DAI" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/logos/backed-bunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/logos/backed-bunny.png -------------------------------------------------------------------------------- /public/logos/opbunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/logos/opbunny.png -------------------------------------------------------------------------------- /public/logos/pbunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/with-backed/backed-interface/d8772efdee841c6c3d2ab24571e6a6a6b5ec4951/public/logos/pbunny.png -------------------------------------------------------------------------------- /sentry.client.config.js: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the browser. 2 | // The config you add here will be used whenever a page is visited. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from '@sentry/nextjs'; 6 | 7 | const shouldInitialize = !process.env.GITHUB_ACTIONS; 8 | const SENTRY_DSN = process.env.SENTRY_DSN; 9 | 10 | shouldInitialize && 11 | Sentry.init({ 12 | dsn: 13 | SENTRY_DSN || 14 | 'https://7e7528c337a44263b8b757289e80e7fc@o1195560.ingest.sentry.io/6318688', 15 | // Adjust this value in production, or use tracesSampler for greater control 16 | tracesSampleRate: 0.2, 17 | environment: process.env.NEXT_PUBLIC_CHAIN_ID, 18 | // ... 19 | // Note: if you want to override the automatic release value, do not set a 20 | // `release` value here - use the environment variable `SENTRY_RELEASE`, so 21 | // that it will also get attached to your source maps 22 | }); 23 | -------------------------------------------------------------------------------- /sentry.properties: -------------------------------------------------------------------------------- 1 | defaults.url=https://sentry.io/ 2 | defaults.org=non-fungible-finance 3 | defaults.project=backed 4 | cli.executable=../../.npm/_npx/a8388072043b4cbc/node_modules/@sentry/cli/bin/sentry-cli 5 | -------------------------------------------------------------------------------- /sentry.server.config.js: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the server. 2 | // The config you add here will be used whenever the server handles a request. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from '@sentry/nextjs'; 6 | 7 | const shouldInitialize = !process.env.GITHUB_ACTIONS; 8 | const SENTRY_DSN = process.env.SENTRY_DSN; 9 | 10 | shouldInitialize && 11 | Sentry.init({ 12 | dsn: 13 | SENTRY_DSN || 14 | 'https://7e7528c337a44263b8b757289e80e7fc@o1195560.ingest.sentry.io/6318688', 15 | // Adjust this value in production, or use tracesSampler for greater control 16 | tracesSampleRate: 0.2, 17 | environment: process.env.NEXT_PUBLIC_CHAIN_ID, 18 | // ... 19 | // Note: if you want to override the automatic release value, do not set a 20 | // `release` value here - use the environment variable `SENTRY_RELEASE`, so 21 | // that it will also get attached to your source maps 22 | }); 23 | -------------------------------------------------------------------------------- /stories/Carousel.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Carousel } from 'components/Carousel'; 3 | 4 | export default { 5 | title: 'components/Carousel', 6 | component: Carousel, 7 | }; 8 | 9 | export const CarouselStyles = () => { 10 | return ; 11 | }; 12 | -------------------------------------------------------------------------------- /stories/Disclosure.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ThreeColumn } from 'components/layouts/ThreeColumn'; 3 | import { Fieldset } from 'components/Fieldset'; 4 | import { FormWrapper } from 'components/layouts/FormWrapper'; 5 | import { Disclosure } from 'components/Disclosure'; 6 | 7 | export default { 8 | title: 'Components/Disclosure', 9 | component: Disclosure, 10 | }; 11 | 12 | export const Disclosures = () => { 13 | return ( 14 | 15 |
16 | 17 | 18 |
    19 |
  • Peanut Butter
  • 20 |
  • Jam
  • 21 |
  • Pickles
  • 22 |
23 |
24 | 27 |
    28 |
  • Basket
  • 29 |
  • Blanket
  • 30 |
  • Umbrella
  • 31 |
32 |
33 |
34 |
35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /stories/Fallback.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Fallback } from 'components/Media/Fallback'; 3 | import { ThreeColumn } from 'components/layouts/ThreeColumn'; 4 | import { Fieldset } from 'components/Fieldset'; 5 | 6 | export default { 7 | title: 'components/Media/Fallback', 8 | component: Fallback, 9 | }; 10 | 11 | export const FieldsetStyles = () => { 12 | return ( 13 | 14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /stories/Fieldset.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Fieldset } from 'components/Fieldset'; 3 | import { Input } from 'components/Input'; 4 | import { Button } from 'components/Button'; 5 | import { FormWrapper } from 'components/layouts/FormWrapper'; 6 | 7 | export default { 8 | title: 'components/Fieldset', 9 | component: Fieldset, 10 | }; 11 | 12 | export const FieldsetStyles = () => { 13 | return ( 14 |
15 | 16 | {}} /> 17 | {}} /> 18 | 19 | 20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /stories/Input.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent, HTMLAttributes } from 'react'; 2 | 3 | import { Input } from 'components/Input'; 4 | 5 | export default { 6 | title: 'Components/Input', 7 | component: Input, 8 | }; 9 | 10 | export const InputStyles = () => { 11 | return ( 12 | <> 13 |
21 | 22 | 23 | 24 | 25 |
26 |
34 | 35 | 36 | 37 | 38 |
39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /stories/LoanCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TwelveColumn } from 'components/layouts/TwelveColumn'; 3 | import { LoanCard } from 'components/LoanCard'; 4 | import { 5 | LoanCardLoaded, 6 | LoanCardLoading, 7 | ExpandedAttributes, 8 | Relationship, 9 | } from 'components/LoanCard/LoanCard'; 10 | import { GetNFTInfoResponse } from 'lib/getNFTInfo'; 11 | import { baseLoan } from 'lib/mockData'; 12 | 13 | export default { 14 | title: 'components/LoanCard', 15 | component: LoanCard, 16 | }; 17 | 18 | export const LoanCards = () => { 19 | return ( 20 | 21 | 22 | borrower 23 | 24 | 25 | 36 | borrower 37 | 38 | 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /stories/LoanInfo.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LoanInfo } from 'components/LoanInfo'; 3 | import { baseLoan, loanWithLenderAccruing } from 'lib/mockData'; 4 | import { CollateralSaleInfo } from 'lib/loans/collateralSaleInfo'; 5 | import { 6 | getFakeFloor, 7 | getFakeItemsAndOwners, 8 | getFakeVolume, 9 | } from 'lib/nftCollectionStats/mockData'; 10 | 11 | export default { 12 | title: 'components/LoanInfo', 13 | component: LoanInfo, 14 | }; 15 | 16 | const [items, owners] = getFakeItemsAndOwners(); 17 | const collectionStats = { 18 | floor: getFakeFloor(), 19 | items, 20 | owners, 21 | volume: getFakeVolume(), 22 | }; 23 | 24 | const saleInfo: CollateralSaleInfo = { 25 | recentSale: null, 26 | collectionStats, 27 | }; 28 | 29 | export function LoanInfoNoLender() { 30 | return ; 31 | } 32 | 33 | export function LoanInfoWithLender() { 34 | return ( 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /stories/Marquee.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Marquee } from 'components/Marquee'; 4 | 5 | export default { 6 | title: 'Components/Marquee', 7 | component: Marquee, 8 | }; 9 | 10 | const messages = [ 11 | 'this is some text', 12 | 'so is this', 13 | 'here is somewhat longer text for testing purposes', 14 | ]; 15 | 16 | export const MarqueeStyles = () => { 17 | return ; 18 | }; 19 | -------------------------------------------------------------------------------- /stories/Modal.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Modal } from 'components/Modal'; 3 | import { useDialogState, DialogDisclosure } from 'reakit/Dialog'; 4 | 5 | export default { 6 | title: 'Components/Modal', 7 | component: Modal, 8 | }; 9 | 10 | export const ModalStyles = () => { 11 | const dialog = useDialogState({ visible: true }); 12 | return ( 13 |
14 | relaunch modal 15 | 16 |

yote

17 |

yote

18 |

yote

19 |

yote

20 |

yote

21 |

yote

22 |

yote

23 |

yote

24 |

yote

25 |

yote

26 |

yote

27 |

yote

28 |

yote

29 |

yote

30 |

yote

31 |

yote

32 |

yote

33 |

yote

34 |

yote

35 |

yote

36 |

yote

37 |

yote

38 |

yote

39 |

yote

40 |
41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /stories/NFTCollateralPicker.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { NFTCollateralPicker } from 'components/NFTCollateralPicker/NFTCollateralPicker'; 4 | import { Provider } from 'urql'; 5 | import { clientFromUrl } from 'lib/urql'; 6 | import { noop } from 'lodash'; 7 | import { useDialogState, DialogDisclosure } from 'reakit/Dialog'; 8 | import { configs } from 'lib/config'; 9 | 10 | const NFTCollateralPickerStory = () => { 11 | const dialog = useDialogState({ visible: true }); 12 | return ( 13 |
14 | relaunch modal 15 | 20 |
21 | ); 22 | }; 23 | 24 | export const Wrapper = () => { 25 | return ( 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default { 33 | title: 'Components/NFTCollateralPicker', 34 | component: Wrapper, 35 | }; 36 | -------------------------------------------------------------------------------- /stories/NotificationsModal.stories.tsx: -------------------------------------------------------------------------------- 1 | import { NotificationsModal } from 'components/NotificationsModal'; 2 | import React from 'react'; 3 | import { useDialogState, DialogDisclosure } from 'reakit/Dialog'; 4 | 5 | export const NotificationsModalStyles = () => { 6 | const dialog = useDialogState({ visible: true }); 7 | return ( 8 |
9 | relaunch modal 10 | 11 |
12 | ); 13 | }; 14 | 15 | export default { 16 | title: 'Components/NotificationsModal', 17 | component: NotificationsModal, 18 | }; 19 | -------------------------------------------------------------------------------- /stories/ParsedEvent.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ParsedEvent } from 'components/TicketHistory/ParsedEvent'; 3 | import { Fieldset } from 'components/Fieldset'; 4 | import { ThreeColumn } from 'components/layouts/ThreeColumn'; 5 | import { baseLoan, events } from 'lib/mockData'; 6 | 7 | export default { 8 | title: 'Components/TicketHistory/ParsedEvent', 9 | component: ParsedEvent, 10 | }; 11 | 12 | const loan = baseLoan; 13 | 14 | export const ParsedEvents = () => { 15 | return ( 16 | 17 |
18 | {events.map((e, i) => ( 19 | 20 | ))} 21 |
22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /stories/Select.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Select } from 'components/Select'; 4 | 5 | export default { 6 | title: 'Components/Select', 7 | component: Select, 8 | }; 9 | 10 | const options = [ 11 | { value: 'chocolate', label: 'Chocolate' }, 12 | { value: 'strawberry', label: 'Strawberry' }, 13 | { value: 'vanilla', label: 'Vanilla' }, 14 | ]; 15 | 16 | export const SelectStyles = () => { 17 | return