├── public ├── favicon.png ├── logo@3x.png ├── icon-monsters.png ├── default-avatar.png ├── fonts │ ├── Gilroy-Light.otf │ ├── Gilroy-Medium.otf │ ├── Gilroy-ExtraBold.otf │ └── Gilroy-SemiBold.otf ├── icons-close.svg ├── icons-small-menu.svg ├── ellipsis.svg ├── close.svg ├── icon-light-chevron-down.svg ├── loading.svg ├── logo.svg ├── arrow.svg ├── down-arrow.svg ├── x.svg ├── launch.svg ├── vercel.svg └── placeholder-asset.svg ├── .dockerignore ├── .prettierrc.js ├── next-env.d.ts ├── components ├── SalesHistoryTable │ ├── SalesHistoryTable.styled.ts │ └── index.tsx ├── TableHeaderRow │ ├── TableHeader.styled.ts │ └── index.tsx ├── TableRow │ ├── TableRow.styled.ts │ └── index.tsx ├── Modal │ ├── index.tsx │ ├── ClaimBalanceModal.tsx │ ├── Modal.styled.ts │ ├── CancelSaleModal.tsx │ └── CreateSaleModal.tsx ├── LoadingPage │ ├── LoadingPage.styled.ts │ └── index.tsx ├── TableContentWraper │ ├── TableContentWrapper.styled.ts │ └── index.tsx ├── Provider │ ├── index.ts │ ├── ModalProvider.tsx │ └── AuthProvider.tsx ├── TableHeaderCell │ ├── index.tsx │ └── TableHeaderCell.styled.ts ├── TableDataCell │ ├── index.tsx │ └── TableDataCell.styled.ts ├── Spinner │ ├── index.tsx │ └── Spinner.styled.ts ├── SalesHistoryTableCell │ ├── SalesHistoryTabelCell.styled.ts │ └── index.tsx ├── Grid │ ├── Grid.styled.ts │ └── index.tsx ├── PriceInput │ ├── PriceInput.styled.ts │ └── index.tsx ├── Button │ ├── index.tsx │ └── Button.styled.ts ├── Banner │ ├── Banner.styled.ts │ └── index.tsx ├── PageLayout │ ├── PageLayout.styled.ts │ └── index.tsx ├── Tooltip │ ├── index.tsx │ └── Tooltip.styled.ts ├── PaginationButton │ ├── index.tsx │ └── PaginationButton.styled.ts ├── AssetFormTitle │ ├── AssetFormTitle.styled.ts │ └── index.tsx ├── Error │ ├── index.tsx │ └── Error.styled.ts ├── AssetFormBuy │ ├── AssetFormBuy.styled.ts │ └── index.tsx ├── Footer │ ├── index.tsx │ └── Footer.styled.ts ├── AssetFormSellPopupMenu │ ├── AssetFormSellPopupMenu.styled.ts │ └── index.tsx ├── DetailsLayout │ ├── DetailsLayout.styled.ts │ └── index.tsx ├── GridCard │ ├── GridCard.styled.ts │ └── index.tsx ├── AssetFormSell │ └── index.tsx └── NavBar │ ├── NavBar.styled.ts │ └── index.tsx ├── kustomization.yaml ├── types └── index.d.ts ├── styles ├── customprogress.css ├── FadeInImageContainer.styled.ts ├── Title.styled.ts ├── MaxWidth.styled.ts ├── globals.css ├── Breakpoints.ts └── reset.css ├── babel.config.json ├── .env.template ├── .gitlab-ci.yml ├── next.config.js ├── letsencrypt-ssl-cert-issuer.yaml ├── deployment.yaml ├── certificate-storage.yaml ├── .github └── workflows │ ├── merge-request.yml │ └── google.yml ├── .gitignore ├── utils ├── constants.ts ├── withCache.ts ├── gtag.ts ├── browser-fetch.ts └── index.ts ├── tsconfig.json ├── ingress-tls.yaml ├── Dockerfile ├── pages ├── api │ ├── profile.ts │ └── upload.ts ├── _app.tsx ├── _document.js ├── index.tsx ├── my-nfts │ └── [chainAccount].tsx ├── my-templates │ └── [templateId].tsx └── [templateId].tsx ├── services ├── cache.ts ├── offers.ts ├── proton-rpc.ts ├── assets.ts ├── sales.ts ├── templates.ts └── proton.ts ├── package.json ├── hooks └── index.ts ├── .eslintrc.json └── README.md /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPRNetwork/nft-demo/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPRNetwork/nft-demo/HEAD/public/logo@3x.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile 4 | .dockerignore 5 | .git 6 | .gitignore -------------------------------------------------------------------------------- /public/icon-monsters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPRNetwork/nft-demo/HEAD/public/icon-monsters.png -------------------------------------------------------------------------------- /public/default-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPRNetwork/nft-demo/HEAD/public/default-avatar.png -------------------------------------------------------------------------------- /public/fonts/Gilroy-Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPRNetwork/nft-demo/HEAD/public/fonts/Gilroy-Light.otf -------------------------------------------------------------------------------- /public/fonts/Gilroy-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPRNetwork/nft-demo/HEAD/public/fonts/Gilroy-Medium.otf -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | jsxBracketSameLine: true, 4 | semi: true, 5 | }; 6 | -------------------------------------------------------------------------------- /public/fonts/Gilroy-ExtraBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPRNetwork/nft-demo/HEAD/public/fonts/Gilroy-ExtraBold.otf -------------------------------------------------------------------------------- /public/fonts/Gilroy-SemiBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPRNetwork/nft-demo/HEAD/public/fonts/Gilroy-SemiBold.otf -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /components/SalesHistoryTable/SalesHistoryTable.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StyledTable = styled.table` 4 | width: 100%; 5 | `; 6 | -------------------------------------------------------------------------------- /kustomization.yaml: -------------------------------------------------------------------------------- 1 | commonLabels: 2 | app: nft-demo 3 | resources: 4 | - deployment.yaml 5 | - certificate-storage.yaml 6 | - ingress-tls.yaml 7 | - letsencrypt-ssl-cert-issuer.yaml -------------------------------------------------------------------------------- /components/TableHeaderRow/TableHeader.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StyledTableHeaderRow = styled.tr` 4 | background-color: #f6f7fe; 5 | `; 6 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | import React = require('react'); 3 | export const ReactComponent: React.FC>; 4 | const src: string; 5 | export default src; 6 | } 7 | -------------------------------------------------------------------------------- /components/TableRow/TableRow.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StyledTableRow = styled.tr` 4 | &:nth-child(even) { 5 | background-color: #f6f7fe; 6 | } 7 | height: 64px; 8 | `; 9 | -------------------------------------------------------------------------------- /components/Modal/index.tsx: -------------------------------------------------------------------------------- 1 | export { ClaimBalanceModal } from './ClaimBalanceModal'; 2 | export { CancelSaleModal, CancelMultipleSalesModal } from './CancelSaleModal'; 3 | export { CreateSaleModal, CreateMultipleSalesModal } from './CreateSaleModal'; 4 | -------------------------------------------------------------------------------- /public/icons-close.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/icons-small-menu.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/ellipsis.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /styles/customprogress.css: -------------------------------------------------------------------------------- 1 | #nprogress { 2 | pointer-events: none; 3 | } 4 | 5 | #nprogress .bar { 6 | background: #8a9ef5; 7 | 8 | position: fixed; 9 | z-index: 1031; 10 | top: 0; 11 | left: 0; 12 | 13 | width: 100%; 14 | height: 4px; 15 | } -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "next/babel" 4 | ], 5 | "plugins": [ 6 | [ 7 | "styled-components", 8 | { 9 | "ssr": true, 10 | "displayName": true, 11 | "preprocess": false 12 | } 13 | ] 14 | ] 15 | } -------------------------------------------------------------------------------- /public/close.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/icon-light-chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/LoadingPage/LoadingPage.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.section` 4 | width: 100%; 5 | margin: 25vh 0; 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | align-items: center; 10 | `; 11 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CHAIN_ID='71ee83bcf52142d61019d95f9cc5427ba6a0d7ff8accd9e2088ae2abeaf3d3dd' 2 | NEXT_PUBLIC_CHAIN_ENDPOINT='https://testnet.protonchain.com' 3 | NEXT_PUBLIC_BLOCK_EXPLORER='https://proton-test.bloks.io/block/' 4 | NEXT_PUBLIC_NFT_ENDPOINT='https://test.proton.api.atomicassets.io' 5 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: nikolaik/python-nodejs 2 | 3 | stages: 4 | - test 5 | 6 | test: 7 | image: nikolaik/python-nodejs 8 | stage: test 9 | only: 10 | - merge_requests 11 | script: 12 | - yarn 13 | - yarn format 14 | - yarn typecheck 15 | - yarn lint 16 | - yarn build 17 | -------------------------------------------------------------------------------- /components/LoadingPage/index.tsx: -------------------------------------------------------------------------------- 1 | import { Container } from './LoadingPage.styled'; 2 | import Spinner from '../Spinner'; 3 | 4 | const LoadingPage = (): JSX.Element => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default LoadingPage; 13 | -------------------------------------------------------------------------------- /components/TableContentWraper/TableContentWrapper.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const BlankRow = styled.tr` 4 | height: 200px; 5 | `; 6 | 7 | export const CenteredCell = styled.td` 8 | text-align: center; 9 | vertical-align: middle; 10 | color: #7578b5; 11 | `; 12 | -------------------------------------------------------------------------------- /public/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /styles/FadeInImageContainer.styled.ts: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | 3 | const fadeIn = keyframes` 4 | 0% { 5 | opacity: 0 6 | } 7 | 100% { 8 | opacity: 1 9 | } 10 | `; 11 | 12 | export const FadeInImageContainer = styled.div` 13 | animation: ${fadeIn} 0.5s linear; 14 | `; 15 | -------------------------------------------------------------------------------- /components/TableRow/index.tsx: -------------------------------------------------------------------------------- 1 | import { StyledTableRow } from './TableRow.styled'; 2 | import { ReactNode } from 'react'; 3 | 4 | type Props = { 5 | children: ReactNode; 6 | }; 7 | 8 | const TableRow = ({ children }: Props): JSX.Element => ( 9 | {children} 10 | ); 11 | export default TableRow; 12 | -------------------------------------------------------------------------------- /components/Provider/index.ts: -------------------------------------------------------------------------------- 1 | export { useAuthContext, AuthProvider } from './AuthProvider'; 2 | export { useModalContext, ModalProvider, MODAL_TYPES } from './ModalProvider'; 3 | export type { 4 | CancelSaleModalProps, 5 | CancelMultipleSalesModalProps, 6 | CreateSaleModalProps, 7 | CreateMultipleSalesModalProps, 8 | } from './ModalProvider'; 9 | -------------------------------------------------------------------------------- /components/TableHeaderCell/index.tsx: -------------------------------------------------------------------------------- 1 | import { StyledTableHeaderCell } from './TableHeaderCell.styled'; 2 | 3 | type Props = { 4 | children: string; 5 | }; 6 | 7 | const TableHeaderCell = ({ children }: Props): JSX.Element => ( 8 | {children} 9 | ); 10 | 11 | export default TableHeaderCell; 12 | -------------------------------------------------------------------------------- /public/arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /styles/Title.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { breakpoint } from './Breakpoints'; 3 | 4 | export const Title = styled.h1` 5 | font-family: GilroySemiBold; 6 | font-size: 28px; 7 | line-height: 32px; 8 | color: #0e103c; 9 | margin: 48px 0; 10 | 11 | ${breakpoint.tablet` 12 | margin: 32px 0; 13 | `} 14 | `; 15 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | images: { 3 | domains: ['cloudflare-ipfs.com'], 4 | }, 5 | webpack(config) { 6 | config.module.rules.push({ 7 | test: /\.svg$/, 8 | issuer: { 9 | test: /\.(js|ts)x?$/, 10 | }, 11 | use: ['@svgr/webpack', 'url-loader'], 12 | }); 13 | 14 | return config; 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /components/TableHeaderRow/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { StyledTableHeaderRow } from './TableHeader.styled'; 3 | 4 | type Props = { 5 | children: ReactNode; 6 | }; 7 | 8 | const TableHeaderRow = ({ children }: Props): JSX.Element => ( 9 | {children} 10 | ); 11 | export default TableHeaderRow; 12 | -------------------------------------------------------------------------------- /components/TableDataCell/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { StyledTableDataCell } from './TableDataCell.styled'; 3 | 4 | type Props = { 5 | children: ReactNode; 6 | }; 7 | 8 | const TableDataCell = ({ children }: Props): JSX.Element => { 9 | return {children}; 10 | }; 11 | 12 | export default TableDataCell; 13 | -------------------------------------------------------------------------------- /components/Spinner/index.tsx: -------------------------------------------------------------------------------- 1 | import StyledSpinner from './Spinner.styled'; 2 | 3 | const Spinner = (): JSX.Element => ( 4 | 5 | 13 | 14 | ); 15 | 16 | export default Spinner; 17 | -------------------------------------------------------------------------------- /public/down-arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/x.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /letsencrypt-ssl-cert-issuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: ClusterIssuer 3 | metadata: 4 | name: nft-demo-issuer 5 | spec: 6 | acme: 7 | server: https://acme-v02.api.letsencrypt.org/directory 8 | email: cindy@metalpay.co 9 | privateKeySecretRef: 10 | name: nft-demo-cert 11 | http01: {} 12 | solvers: 13 | - selector: {} 14 | http01: 15 | ingress: 16 | class: nginx 17 | 18 | -------------------------------------------------------------------------------- /deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: nft-demo 5 | labels: 6 | app: nft-demo 7 | spec: 8 | selector: 9 | matchLabels: 10 | app: nft-demo 11 | template: 12 | metadata: 13 | labels: 14 | app: nft-demo 15 | tier: web 16 | spec: 17 | containers: 18 | - name: nft-demo 19 | image: gcr.io/proton-wallet/nft-demo 20 | ports: 21 | - containerPort: 3000 -------------------------------------------------------------------------------- /styles/MaxWidth.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { breakpoint } from './Breakpoints'; 3 | 4 | export const MaxWidth = styled.div` 5 | max-width: 1128px; 6 | display: flex; 7 | justify-content: center; 8 | margin: 0px auto; 9 | 10 | ${breakpoint.laptop` 11 | max-width: 80%; 12 | `} 13 | 14 | ${breakpoint.tablet` 15 | max-width: 90%; 16 | `}; 17 | 18 | ${breakpoint.mobile` 19 | max-width: 90%; 20 | `}; 21 | `; 22 | -------------------------------------------------------------------------------- /certificate-storage.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: Certificate 3 | metadata: 4 | name: nft-demo-cert 5 | namespace: default 6 | spec: 7 | secretName: nft-demo-cert 8 | issuerRef: 9 | name: nft-demo-issuer 10 | kind: ClusterIssuer 11 | commonName: nft.protonchain.com 12 | dnsNames: 13 | - nft.protonchain.com 14 | acme: 15 | config: 16 | - http01: 17 | ingress: nft-demo-ssl-ingress 18 | domains: 19 | - nft.protonchain.com 20 | -------------------------------------------------------------------------------- /components/SalesHistoryTableCell/SalesHistoryTabelCell.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import Image from 'next/image'; 3 | import { breakpoint } from '../../styles/Breakpoints'; 4 | 5 | export const AvatarImage = styled(Image)` 6 | border-radius: 100%; 7 | `; 8 | 9 | export const ImageDataCell = styled.td` 10 | vertical-align: middle; 11 | text-align: ${({ align }) => (align ? align : ' center')}; 12 | 13 | ${breakpoint.mobile` 14 | padding: 0px 5px; 15 | `} 16 | `; 17 | -------------------------------------------------------------------------------- /components/TableDataCell/TableDataCell.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { breakpoint } from '../../styles/Breakpoints'; 3 | 4 | export const StyledTableDataCell = styled.td` 5 | display: table-cell; 6 | vertical-align: middle; 7 | font-size: 16px; 8 | font-weight: 500; 9 | font-stretch: normal; 10 | font-style: normal; 11 | line-height: 1.5; 12 | letter-spacing: normal; 13 | color: #0e103c; 14 | text-align: 'left'; 15 | 16 | ${breakpoint.mobile` 17 | padding: 0px 10px; 18 | `} 19 | `; 20 | -------------------------------------------------------------------------------- /.github/workflows/merge-request.yml: -------------------------------------------------------------------------------- 1 | name: merge-request 2 | on: 3 | pull_request: 4 | branches: [ master, develop ] 5 | jobs: 6 | typecheck: 7 | name: Run Prettier, Linter, and Typecheck 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v1 12 | - name: Install packages 13 | run: yarn 14 | - name: Prettier 15 | run: yarn format 16 | - name: Linter 17 | run: yarn lint 18 | - name: Typecheck 19 | run: yarn typecheck 20 | -------------------------------------------------------------------------------- /components/TableHeaderCell/TableHeaderCell.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { breakpoint } from '../../styles/Breakpoints'; 3 | 4 | export const StyledTableHeaderCell = styled.th` 5 | padding: 9px 0px; 6 | font-size: 12px; 7 | font-weight: 600; 8 | font-stretch: normal; 9 | font-style: normal; 10 | line-height: 2; 11 | letter-spacing: 1px; 12 | color: #7578b5; 13 | text-align: left; 14 | 15 | ${breakpoint.mobile` 16 | padding: 9px 10px; 17 | `} 18 | 19 | :first-of-type { 20 | width: 64px; 21 | } 22 | `; 23 | -------------------------------------------------------------------------------- /components/Grid/Grid.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { breakpoint } from '../../styles/Breakpoints'; 3 | 4 | export const Container = styled.section` 5 | width: 100%; 6 | display: inline-grid; 7 | grid-column-gap: 16px; 8 | grid-row-gap: 48px; 9 | grid-template-columns: repeat(5, minmax(0, 1fr)); 10 | 11 | ${breakpoint.laptop` 12 | grid-template-columns: repeat(3, minmax(0, 1fr)); 13 | `} 14 | 15 | ${breakpoint.tablet` 16 | grid-template-columns: repeat(2, minmax(0, 1fr)); 17 | grid-column-gap: 10px; 18 | `} 19 | `; 20 | -------------------------------------------------------------------------------- /components/PriceInput/PriceInput.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Input = styled.input` 4 | font-size: 14px; 5 | line-height: 24px; 6 | color: #7578b5; 7 | border-radius: 4px; 8 | padding: 8px; 9 | border: solid 1px #e8ecfd; 10 | margin-bottom: 12px; 11 | width: 100%; 12 | 13 | ::-webkit-inner-spin-button, 14 | ::-webkit-outer-spin-button { 15 | -webkit-appearance: none; 16 | -moz-appearance: none; 17 | appearance: none; 18 | margin: 0; 19 | } 20 | 21 | ::placeholder { 22 | color: #7578b5; 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /public/launch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.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 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | package-lock.json 27 | yarn.lock 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 | # jetbrains IDEs 39 | .idea 40 | -------------------------------------------------------------------------------- /components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import { StyledButton } from './Button.styled'; 2 | 3 | type Props = { 4 | children: string; 5 | onClick: () => void; 6 | filled?: boolean; 7 | rounded?: boolean; 8 | fullWidth?: boolean; 9 | disabled?: boolean; 10 | }; 11 | 12 | const Button = ({ 13 | children, 14 | onClick, 15 | filled, 16 | rounded, 17 | fullWidth, 18 | disabled, 19 | }: Props): JSX.Element => ( 20 | 26 | {children} 27 | 28 | ); 29 | 30 | export default Button; 31 | -------------------------------------------------------------------------------- /utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const EMPTY_BALANCE = '0 FOOBAR'; 2 | export const TOKEN_SYMBOL = 'FOOBAR'; 3 | export const TOKEN_CONTRACT = 'xtokens'; 4 | export const TOKEN_PRECISION = 6; 5 | export const SHORTENED_TOKEN_PRECISION = 2; 6 | export const DEFAULT_COLLECTION = 'monsters'; 7 | export const PAGINATION_LIMIT = 10; 8 | 9 | export interface QueryParams { 10 | collection_name?: string; 11 | owner?: string; 12 | state?: string; 13 | sender?: string; 14 | seller?: string; 15 | asset_id?: string; 16 | template_id?: string; 17 | limit?: string | number; 18 | sort?: string; 19 | order?: string; 20 | page?: number; 21 | symbol?: string; 22 | } 23 | -------------------------------------------------------------------------------- /components/Banner/Banner.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Background = styled.section` 4 | left: 0; 5 | width: 100%; 6 | height: 40px; 7 | background: #ebf4ee; 8 | z-index: 2; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | color: #75b587; 13 | position: fixed; 14 | font-size: 14px; 15 | font-family: GilroySemiBold; 16 | cursor: pointer; 17 | `; 18 | 19 | export const Spacer = styled.div` 20 | height: 40px; 21 | `; 22 | 23 | export const Content = styled.span` 24 | position: relative; 25 | color: #75b587; 26 | font-size: 14px; 27 | font-family: GilroySemiBold; 28 | `; 29 | -------------------------------------------------------------------------------- /components/PageLayout/PageLayout.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { MaxWidth } from '../../styles/MaxWidth.styled'; 3 | import { breakpoint } from '../../styles/Breakpoints'; 4 | 5 | export const Main = styled.main` 6 | position: relative; 7 | min-height: calc(100vh - 83px); 8 | padding-top: 64px; 9 | overflow: auto; 10 | 11 | ${breakpoint.tablet` 12 | min-height: calc(100vh - 83px); 13 | `} 14 | `; 15 | 16 | export const Container = styled(MaxWidth)` 17 | width: 100%; 18 | flex-direction: column; 19 | justify-content: flex-start; 20 | margin-bottom: 128px; 21 | 22 | ${breakpoint.tablet` 23 | margin-bottom: 64px; 24 | `} 25 | `; 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "downlevelIteration": true 21 | }, 22 | "include": [ 23 | "next-env.d.ts", 24 | "**/*.ts", 25 | "**/*.tsx", 26 | "next.config.js" 27 | ], 28 | "exclude": [ 29 | "node_modules" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /components/Grid/index.tsx: -------------------------------------------------------------------------------- 1 | import { TemplateCard } from '../GridCard'; 2 | import { Template } from '../../services/templates'; 3 | import { Container } from './Grid.styled'; 4 | 5 | type Props = { 6 | items: Template[]; 7 | isLoading?: boolean; 8 | isUsersTemplates?: boolean; 9 | }; 10 | 11 | const Grid = ({ isLoading, items, isUsersTemplates }: Props): JSX.Element => { 12 | return ( 13 | 14 | {items.map((item) => ( 15 | 21 | ))} 22 | 23 | ); 24 | }; 25 | 26 | export default Grid; 27 | -------------------------------------------------------------------------------- /components/Tooltip/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useState } from 'react'; 2 | import { Background, Header, Content } from './Tooltip.styled'; 3 | 4 | type Props = { 5 | children: ReactNode; 6 | content: string; 7 | }; 8 | 9 | const Tooltip = ({ children, content }: Props): JSX.Element => { 10 | const [active, setActive] = useState(false); 11 | 12 | return ( 13 | setActive(true)} 15 | onMouseLeave={() => setActive(false)}> 16 | {children} 17 | {active && ( 18 | 19 |
Attention!
20 | {content} 21 |
22 | )} 23 |
24 | ); 25 | }; 26 | 27 | export default Tooltip; 28 | -------------------------------------------------------------------------------- /components/PaginationButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from './PaginationButton.styled'; 2 | import { ReactComponent as Arrow } from '../../public/arrow.svg'; 3 | import { ReactComponent as Loading } from '../../public/loading.svg'; 4 | 5 | type Props = { 6 | onClick: () => Promise; 7 | disabled: boolean; 8 | isLoading: boolean; 9 | isHidden?: boolean; 10 | }; 11 | 12 | const PaginationButton = ({ 13 | onClick, 14 | disabled, 15 | isLoading, 16 | isHidden, 17 | }: Props): JSX.Element => ( 18 | 25 | ); 26 | 27 | export default PaginationButton; 28 | -------------------------------------------------------------------------------- /components/Spinner/Spinner.styled.ts: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | 3 | const rotate = keyframes` 4 | 100% { 5 | transform: rotate(360deg); 6 | } 7 | `; 8 | 9 | const dash = keyframes` 10 | 0% { 11 | stroke-dasharray: 1, 150; 12 | stroke-dashoffset: 0; 13 | } 14 | 50% { 15 | stroke-dasharray: 90, 150; 16 | stroke-dashoffset: -35; 17 | } 18 | 100% { 19 | stroke-dasharray: 90, 150; 20 | stroke-dashoffset: -124; 21 | } 22 | `; 23 | 24 | const StyledSpinner = styled.svg` 25 | animation: ${rotate} 2s linear infinite; 26 | width: 50px; 27 | height: 50px; 28 | 29 | & .path { 30 | stroke: rgba(117, 120, 181, 0.2); 31 | stroke-linecap: round; 32 | animation: ${dash} 1.5s ease-in-out infinite; 33 | } 34 | `; 35 | 36 | export default StyledSpinner; 37 | -------------------------------------------------------------------------------- /ingress-tls.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: nft-demo-backend 5 | labels: 6 | app: nft-demo 7 | spec: 8 | type: NodePort 9 | selector: 10 | app: nft-demo 11 | tier: web 12 | ports: 13 | - port: 3000 14 | targetPort: 3000 15 | --- 16 | apiVersion: extensions/v1beta1 17 | kind: Ingress 18 | metadata: 19 | name: nft-demo-ssl-ingress 20 | annotations: 21 | kubernetes.io/ingress.class: nginx 22 | cert-manager.io/cluster-issuer: nft-demo-issuer 23 | labels: 24 | app: nft-demo 25 | spec: 26 | tls: 27 | - secretName: nft-demo-cert 28 | hosts: 29 | - nft.protonchain.com 30 | rules: 31 | - host: nft.protonchain.com 32 | http: 33 | paths: 34 | - backend: 35 | serviceName: nft-demo-backend 36 | servicePort: 3000 37 | path: / 38 | -------------------------------------------------------------------------------- /components/AssetFormTitle/AssetFormTitle.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const NameContainer = styled.div` 4 | display: flex; 5 | justify-content: space-between; 6 | `; 7 | 8 | export const Name = styled.h1` 9 | font-size: 40px; 10 | line-height: 48px; 11 | font-family: GilroySemiBold; 12 | margin-bottom: 8px; 13 | `; 14 | 15 | export const General = styled.p` 16 | color: #7578b5; 17 | font-size: 14px; 18 | line-height: 24px; 19 | `; 20 | 21 | export const Title = styled(General)` 22 | margin-left: 8px; 23 | color: #0e103c; 24 | `; 25 | 26 | export const Author = styled(General).attrs({ as: 'a' })` 27 | color: #8a9ef5; 28 | cursor: pointer; 29 | `; 30 | 31 | export const CollectionIconContainer = styled.div` 32 | display: flex; 33 | align-items: center; 34 | margin-bottom: 24px; 35 | `; 36 | -------------------------------------------------------------------------------- /components/Error/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import Button from '../Button'; 3 | import { Container, QuestionIcon, Text } from './Error.styled'; 4 | 5 | type Props = { 6 | errorMessage: string; 7 | buttonText?: string; 8 | buttonOnClick?: () => void; 9 | }; 10 | 11 | const Error = ({ 12 | errorMessage, 13 | buttonText, 14 | buttonOnClick, 15 | }: Props): JSX.Element => { 16 | const router = useRouter(); 17 | const onClick = buttonOnClick ? buttonOnClick : () => router.push('/'); 18 | return ( 19 | 20 | 21 | {errorMessage} 22 | 25 | 26 | ); 27 | }; 28 | 29 | Error.defaultProps = { 30 | buttonText: 'Explore Monsters', 31 | }; 32 | 33 | export default Error; 34 | -------------------------------------------------------------------------------- /components/Error/Error.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.section` 4 | width: 100%; 5 | margin: 10% 0; 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | align-items: center; 10 | `; 11 | 12 | export const QuestionIcon = styled.div` 13 | font-family: GilroySemiBold; 14 | font-size: 50px; 15 | width: 94px; 16 | height: 94px; 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | box-sizing: border-box; 21 | color: #8a9ef5; 22 | background-color: #f1f3fe; 23 | border-radius: 35px; 24 | :before { 25 | content: '?'; 26 | } 27 | `; 28 | 29 | export const Text = styled.p` 30 | font-family: GilroySemiBold; 31 | font-size: 18px; 32 | line-height: 24px; 33 | color: #0e103c; 34 | max-width: 280px; 35 | text-align: center; 36 | margin: 32px 0 20px; 37 | `; 38 | -------------------------------------------------------------------------------- /components/PaginationButton/PaginationButton.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | type ButtonProps = { 4 | isHidden: boolean; 5 | disabled: boolean; 6 | }; 7 | 8 | export const Button = styled.button` 9 | ${({ isHidden }) => isHidden && `display: none;`} 10 | padding: 8px 16px; 11 | margin: 24px 0 12px; 12 | border-radius: 4px; 13 | border: 1px solid #e8ecfd; 14 | transition: 0.2s; 15 | width: 100%; 16 | transform: rotate(-180deg); 17 | -webkit-transform: rotate(-180deg); 18 | background-color: #ffffff; 19 | 20 | ${({ disabled }) => 21 | !disabled && 22 | ` 23 | cursor: pointer; 24 | :hover { 25 | opacity: 1; 26 | color: #ffffff; 27 | background-color: #8a9ef5; 28 | box-shadow: 0 8px 12px -4px rgba(130, 136, 148, 0.24), 29 | 0 0 4px 0 rgba(141, 141, 148, 0.16), 0 0 2px 0 rgba(141, 141, 148, 0.12); 30 | } 31 | `} 32 | `; 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine 2 | 3 | ENV PORT 3000 4 | 5 | RUN mkdir -p /usr/src/app 6 | WORKDIR /usr/src/app 7 | 8 | ARG NEXT_PUBLIC_GA_TRACKING_ID 9 | ARG NEXT_PUBLIC_CHAIN_ID 10 | ARG NEXT_PUBLIC_CHAIN_ENDPOINT 11 | ARG NEXT_PUBLIC_BLOCK_EXPLORER 12 | ARG NEXT_PUBLIC_NFT_ENDPOINT 13 | ARG XAUTH_PROTON_MARKET 14 | ARG PINATA_API_KEY 15 | ARG PINATA_SECRET 16 | ENV NEXT_PUBLIC_CHAIN_ID=$NEXT_PUBLIC_CHAIN_ID 17 | ENV NEXT_PUBLIC_CHAIN_ENDPOINT=$NEXT_PUBLIC_CHAIN_ENDPOINT 18 | ENV NEXT_PUBLIC_BLOCK_EXPLORER=$NEXT_PUBLIC_BLOCK_EXPLORER 19 | ENV NEXT_PUBLIC_GA_TRACKING_ID=$NEXT_PUBLIC_GA_TRACKING_ID 20 | ENV NEXT_PUBLIC_NFT_ENDPOINT=$NEXT_PUBLIC_NFT_ENDPOINT 21 | ENV XAUTH_PROTON_MARKET=$XAUTH_PROTON_MARKET 22 | ENV PINATA_API_KEY=$PINATA_API_KEY 23 | ENV PINATA_SECRET=$PINATA_SECRET 24 | 25 | COPY package*.json /usr/src/app/ 26 | RUN yarn install 27 | 28 | COPY . /usr/src/app 29 | 30 | RUN yarn build 31 | 32 | EXPOSE 3000 33 | 34 | CMD [ "yarn", "start" ] 35 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'GilroyLight'; 3 | src: url('/fonts/Gilroy-Light.otf') format('woff'); 4 | } 5 | 6 | @font-face { 7 | font-family: 'GilroyExtraBold'; 8 | src: url('/fonts/Gilroy-ExtraBold.otf') format('woff'); 9 | } 10 | 11 | @font-face { 12 | font-family: 'GilroyMedium'; 13 | src: url('/fonts/Gilroy-Medium.otf') format('woff'); 14 | } 15 | 16 | @font-face { 17 | font-family: 'GilroySemiBold'; 18 | src: url('/fonts/Gilroy-SemiBold.otf') format('woff'); 19 | } 20 | 21 | html, 22 | body { 23 | padding: 0; 24 | margin: 0; 25 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 26 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 27 | } 28 | 29 | a { 30 | color: inherit; 31 | text-decoration: none; 32 | } 33 | 34 | * { 35 | box-sizing: border-box; 36 | font-family: 'GilroyMedium'; 37 | } 38 | 39 | *:focus-visible { 40 | outline-color: #8a9ef5; 41 | } 42 | -------------------------------------------------------------------------------- /components/Tooltip/Tooltip.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Background = styled.div` 4 | position: fixed; 5 | left: 0; 6 | width: 100%; 7 | display: flex; 8 | justify-content: center; 9 | `; 10 | 11 | export const Header = styled.h1` 12 | font-size: 16px; 13 | margin-bottom: 8px; 14 | font-family: GilroyMedium; 15 | color: red; 16 | `; 17 | 18 | export const Content = styled.div` 19 | font-family: GilroyMedium; 20 | font-size: 12px; 21 | color: #7578b5; 22 | flex-direction: column; 23 | max-width: 350px; 24 | padding: 8px; 25 | background-color: white; 26 | border: 1px solid #7578b5; 27 | border-radius: 4px; 28 | margin-top: 48px; 29 | position: absolute; 30 | 31 | ::before { 32 | content: ''; 33 | position: absolute; 34 | left: 50%; 35 | border: solid transparent; 36 | top: -11px; 37 | border-width: 5px; 38 | border-bottom-color: #7578b5; 39 | } 40 | `; 41 | -------------------------------------------------------------------------------- /utils/withCache.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import proton from '../services/proton-rpc'; 3 | import cache, { Cache } from '../services/cache'; 4 | 5 | export interface MyAssetRequest extends NextApiRequest { 6 | query: { 7 | accounts: string[]; 8 | }; 9 | cache: Cache; 10 | } 11 | 12 | type Handler = (req: MyAssetRequest, res: NextApiResponse) => Promise; 13 | 14 | export const conditionallyUpdateCache = ( 15 | account: string, 16 | cache: Cache 17 | ): Promise => 18 | new Promise((resolve) => { 19 | if (!cache.has(account)) { 20 | proton.getProfileImage({ account }).then((avatar) => { 21 | cache.set(account, avatar); 22 | resolve(avatar); 23 | }); 24 | } else { 25 | resolve(account); 26 | } 27 | }); 28 | 29 | const withCache = (handler: Handler): Handler => { 30 | return async (req, res) => { 31 | req.cache = cache; 32 | return handler(req, res); 33 | }; 34 | }; 35 | 36 | export default withCache; 37 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /components/Button/Button.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export interface ButtonProps { 4 | filled?: boolean; 5 | rounded?: boolean; 6 | fullWidth?: boolean; 7 | } 8 | 9 | export const StyledButton = styled.button` 10 | padding: 8px 16px; 11 | margin: 12px 0; 12 | border-radius: ${({ rounded }) => (rounded ? '20px' : '4px')}; 13 | border: ${({ filled }) => (filled ? 'none' : ' 1px solid #e8ecfd')}; 14 | background-color: ${({ filled }) => (filled ? '#8a9ef5' : '#ffffff')}; 15 | color: ${({ filled }) => (filled ? '#ffffff' : '#8a9ef5')}; 16 | cursor: pointer; 17 | transition: 0.2s; 18 | height: auto; 19 | font-size: 16px; 20 | line-height: 24px; 21 | width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')}; 22 | 23 | :hover, 24 | :focus-visible { 25 | opacity: 1; 26 | color: #ffffff; 27 | background-color: ${({ filled }) => (filled ? '#4d5dc1' : '#8a9ef5')}; 28 | box-shadow: 0 8px 12px -4px rgba(130, 136, 148, 0.24), 29 | 0 0 4px 0 rgba(141, 141, 148, 0.16), 0 0 2px 0 rgba(141, 141, 148, 0.12); 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /utils/gtag.ts: -------------------------------------------------------------------------------- 1 | export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GA_TRACKING_ID; 2 | export const isProduction = process.env.NODE_ENV === 'production'; 3 | 4 | type Event = { 5 | event_category?: string; 6 | event_label?: string; 7 | page_path?: URL; 8 | value?: number; 9 | }; 10 | 11 | declare global { 12 | interface Window { 13 | gtag: (type: string, action: string, event: Event) => void; 14 | } 15 | } 16 | 17 | // https://developers.google.com/analytics/devguides/collection/gtagjs/pages 18 | export const pageview = (url: URL): void => { 19 | if (isProduction) { 20 | window.gtag('config', GA_TRACKING_ID, { 21 | page_path: url, 22 | }); 23 | } 24 | }; 25 | 26 | type GTagEvent = { 27 | action: string; 28 | category?: string; 29 | label?: string; 30 | value?: number; 31 | }; 32 | 33 | // https://developers.google.com/analytics/devguides/collection/gtagjs/events 34 | export const event = ({ action, category, label, value }: GTagEvent): void => { 35 | if (isProduction) { 36 | window.gtag('event', action, { 37 | event_category: category, 38 | event_label: label, 39 | value, 40 | }); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /styles/Breakpoints.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | 3 | interface MediaQueryProps { 4 | [key: string]: string; 5 | } 6 | 7 | interface MediaQueryValues { 8 | [key: string]: number; 9 | } 10 | 11 | const breakpoints: MediaQueryProps = { 12 | smallMobile: '400px', 13 | mobile: '600px', 14 | tablet: '970px', 15 | laptop: '1224px', 16 | }; 17 | 18 | export const breakpointValues: MediaQueryValues = { 19 | smallMobile: 400, 20 | mobile: 600, 21 | tablet: 970, 22 | laptop: 1224, 23 | }; 24 | 25 | export const breakpoint = Object.keys(breakpoints).reduce( 26 | (accumulator, label) => { 27 | accumulator[label] = (...args: Array) => { 28 | return css` 29 | @media (max-width: ${breakpoints[label]}) { 30 | ${css({}, ...args)}; 31 | } 32 | `; 33 | }; 34 | return accumulator; 35 | }, 36 | { 37 | smallMobile: undefined, 38 | mobile: undefined, 39 | tablet: undefined, 40 | laptop: undefined, 41 | } 42 | ); 43 | 44 | // How to use 45 | // export const ExampleComponent = styled.div` 46 | // background-color: lime; 47 | 48 | // ${breakpoint.mobile` 49 | // background-color: red; 50 | // `} 51 | // `; 52 | -------------------------------------------------------------------------------- /components/AssetFormBuy/AssetFormBuy.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const General = styled.p` 4 | color: #7578b5; 5 | font-size: 14px; 6 | line-height: 24px; 7 | `; 8 | 9 | export const Amount = styled.h3` 10 | font-size: 22px; 11 | font-weight: 600; 12 | line-height: 32px; 13 | margin-bottom: 28px; 14 | `; 15 | 16 | export const Row = styled.div` 17 | display: flex; 18 | justify-content: space-between; 19 | align-items: center; 20 | width: 100%; 21 | `; 22 | 23 | export const ErrorMessage = styled.p` 24 | color: #b57579; 25 | font-size: 16px; 26 | line-height: 24px; 27 | `; 28 | 29 | export const DropdownMenu = styled.select` 30 | font-size: 16px; 31 | margin: 4px 0 12px; 32 | padding: 0 16px; 33 | width: 100%; 34 | height: 40px; 35 | color: #0e103c; 36 | border: 1px solid #e8ecfd; 37 | border-radius: 4px; 38 | cursor: pointer; 39 | line-height: 24px; 40 | position: relative; 41 | -webkit-appearance: none; 42 | -moz-appearance: none; 43 | appearance: none; 44 | background: url('/down-arrow.svg'); 45 | background-repeat: no-repeat; 46 | background-position: top 2px right 15px; 47 | 48 | &:hover { 49 | border: 1px solid #aab2d5; 50 | } 51 | `; 52 | -------------------------------------------------------------------------------- /components/TableContentWraper/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import Spinner from '../Spinner'; 3 | import { BlankRow, CenteredCell } from './TableContentWrapper.styled'; 4 | import TableRow from '../TableRow'; 5 | 6 | type TableContentWrapperProps = { 7 | children: ReactNode; 8 | error?: string; 9 | loading: boolean; 10 | columns: number; 11 | noData: boolean; 12 | noDataMessage?: string; 13 | }; 14 | 15 | const TableContentWrapper = ({ 16 | children, 17 | error, 18 | loading, 19 | columns, 20 | noData, 21 | noDataMessage = 'No Data', 22 | }: TableContentWrapperProps): JSX.Element => { 23 | if (error) { 24 | return ( 25 | 26 | {error} 27 | 28 | ); 29 | } else if (loading) { 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | } else if (noData) { 38 | return ( 39 | 40 | {noDataMessage} 41 | 42 | ); 43 | } else { 44 | return <>{children}; 45 | } 46 | }; 47 | 48 | export default TableContentWrapper; 49 | -------------------------------------------------------------------------------- /pages/api/profile.ts: -------------------------------------------------------------------------------- 1 | import { NextApiResponse } from 'next'; 2 | import withCache, { 3 | MyAssetRequest, 4 | conditionallyUpdateCache, 5 | } from '../../utils/withCache'; 6 | 7 | const handler = async ( 8 | req: MyAssetRequest, 9 | res: NextApiResponse 10 | ): Promise => { 11 | const { 12 | method, 13 | query: { accounts }, 14 | } = req; 15 | switch (method) { 16 | case 'POST': 17 | break; 18 | case 'PUT': 19 | break; 20 | case 'PATCH': 21 | break; 22 | default: { 23 | try { 24 | const chainAccounts = 25 | typeof accounts === 'string' ? [accounts] : [...new Set(accounts)]; 26 | 27 | const promises = chainAccounts.map((account) => 28 | conditionallyUpdateCache(account, req.cache) 29 | ); 30 | await Promise.all(promises); 31 | 32 | const avatarsByChainAccount = req.cache.getValues(chainAccounts); 33 | res.status(200).send({ success: true, message: avatarsByChainAccount }); 34 | } catch (e) { 35 | res.status(500).send({ 36 | success: false, 37 | message: e.message || 'Error retrieving profile avatars', 38 | }); 39 | } 40 | break; 41 | } 42 | } 43 | }; 44 | 45 | export default withCache(handler); 46 | -------------------------------------------------------------------------------- /styles/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | vertical-align: baseline; 24 | } 25 | /* HTML5 display-role reset for older browsers */ 26 | article, aside, details, figcaption, figure, 27 | footer, header, hgroup, menu, nav, section { 28 | display: block; 29 | } 30 | body { 31 | line-height: 1; 32 | } 33 | ol, ul { 34 | list-style: none; 35 | } 36 | blockquote, q { 37 | quotes: none; 38 | } 39 | blockquote:before, blockquote:after, 40 | q:before, q:after { 41 | content: ''; 42 | content: none; 43 | } 44 | table { 45 | border-collapse: collapse; 46 | border-spacing: 0; 47 | } 48 | -------------------------------------------------------------------------------- /components/Banner/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useAuthContext, useModalContext } from '../Provider'; 3 | import Tooltip from '../Tooltip'; 4 | import { Background, Spacer, Content } from './Banner.styled'; 5 | 6 | type Props = { 7 | toolTipContent?: string; 8 | bannerContent: string; 9 | modalType: string; 10 | }; 11 | 12 | const Banner = ({ 13 | toolTipContent, 14 | bannerContent, 15 | modalType, 16 | }: Props): JSX.Element => { 17 | const { currentUser } = useAuthContext(); 18 | const { openModal } = useModalContext(); 19 | const [isBannerVisible, setIsBannerVisible] = useState(false); 20 | 21 | useEffect(() => { 22 | if (currentUser) { 23 | setIsBannerVisible(true); 24 | } else { 25 | setIsBannerVisible(false); 26 | } 27 | }, [currentUser]); 28 | 29 | const getContent = () => { 30 | return ( 31 | openModal(modalType)}> 32 | {bannerContent} 33 | 34 | ); 35 | }; 36 | 37 | if (!isBannerVisible) return null; 38 | 39 | return ( 40 | <> 41 | 42 | {toolTipContent ? ( 43 | {getContent()} 44 | ) : ( 45 | getContent() 46 | )} 47 | 48 | ); 49 | }; 50 | 51 | export default Banner; 52 | -------------------------------------------------------------------------------- /components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import { 3 | FooterBackground, 4 | ImageContainer, 5 | StyledFooter, 6 | Section, 7 | FooterLink, 8 | } from './Footer.styled'; 9 | 10 | const links = [ 11 | { 12 | name: 'Proton Blockchain', 13 | url: 'https://www.protonchain.com/', 14 | }, 15 | { 16 | name: 'Documentation', 17 | url: 'https://docs.protonchain.com/', 18 | }, 19 | { 20 | name: 'Wallet', 21 | url: 'https://www.protonchain.com/wallet', 22 | }, 23 | { 24 | name: 'Support', 25 | url: 'https://support.protonchain.com/support/tickets/new', 26 | }, 27 | ]; 28 | 29 | const Footer = (): JSX.Element => { 30 | return ( 31 | 32 | 33 | 34 | logo 42 | 43 |
44 | {links.map(({ name, url }) => ( 45 | 46 | {name} 47 | 48 | ))} 49 |
50 |
51 |
52 | ); 53 | }; 54 | 55 | export default Footer; 56 | -------------------------------------------------------------------------------- /components/Footer/Footer.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { MaxWidth } from '../../styles/MaxWidth.styled'; 3 | import { breakpoint } from '../../styles/Breakpoints'; 4 | import { FadeInImageContainer } from '../../styles/FadeInImageContainer.styled'; 5 | 6 | export const FooterBackground = styled.footer` 7 | width: 100%; 8 | background: #f6f7fe; 9 | border-bottom: 1px solid #f6f7fe; 10 | `; 11 | 12 | export const StyledFooter = styled(MaxWidth).attrs({ as: 'section' })` 13 | justify-content: space-between; 14 | align-items: center; 15 | width: 100%; 16 | 17 | ${breakpoint.tablet` 18 | flex-direction: column; 19 | justify-content: center; 20 | `} 21 | `; 22 | 23 | export const ImageContainer = styled(FadeInImageContainer)` 24 | margin: 24px 0; 25 | `; 26 | 27 | export const Section = styled.section` 28 | display: flex; 29 | justify-content: space-between; 30 | 31 | ${breakpoint.tablet` 32 | flex-direction: column; 33 | justify-content: center; 34 | `} 35 | `; 36 | 37 | export const FooterLink = styled.a` 38 | color: #7578b5; 39 | font-weight: 500; 40 | cursor: pointer; 41 | margin-right: 40px; 42 | padding: 20px 0; 43 | font-size: 16px; 44 | line-height: 24px; 45 | text-align: center; 46 | 47 | ${breakpoint.tablet` 48 | padding: 0; 49 | margin: 0 0 16px; 50 | 51 | &:last-of-type { 52 | margin-bottom: 24px; 53 | } 54 | `} 55 | `; 56 | -------------------------------------------------------------------------------- /utils/browser-fetch.ts: -------------------------------------------------------------------------------- 1 | export interface BrowserResponse { 2 | success: boolean; 3 | message: T; 4 | data?: T; 5 | error?: { 6 | message: string; 7 | }; 8 | } 9 | 10 | const request = ( 11 | url: string, 12 | body: P | null, 13 | headers?: HeadersInit | undefined, 14 | method?: string 15 | ): Promise> => { 16 | const isValidBody = body && Object.keys(body).length > 0; 17 | const isValidHeader = headers && Object.keys(headers).length > 0; 18 | const originalHeaders = { 19 | Accept: 'application/json', 20 | 'Content-Type': 'application/json', 21 | }; 22 | 23 | const options: RequestInit = { 24 | method: method || 'GET', 25 | body: isValidBody ? JSON.stringify(body) : null, 26 | headers: isValidHeader 27 | ? Object.assign(originalHeaders, headers) 28 | : originalHeaders, 29 | }; 30 | 31 | return fetch(url, options) 32 | .then((result) => { 33 | return result.json(); 34 | }) 35 | .catch((e) => { 36 | throw Error(e); 37 | }); 38 | }; 39 | 40 | export const sendToApi = ( 41 | method: string, 42 | url: string, 43 | body: P, 44 | header?: HeadersInit | undefined 45 | ): Promise> => { 46 | return request(url, body, header, method); 47 | }; 48 | 49 | export const getFromApi = ( 50 | url: string, 51 | header?: HeadersInit | undefined 52 | ): Promise> => { 53 | return request(url, null, header, 'GET'); 54 | }; 55 | -------------------------------------------------------------------------------- /components/AssetFormSellPopupMenu/AssetFormSellPopupMenu.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { breakpoint } from '../../styles/Breakpoints'; 3 | 4 | type MenuProps = { 5 | isOpen: boolean; 6 | }; 7 | 8 | export const MenuContainer = styled.div` 9 | position: relative; 10 | `; 11 | 12 | export const PopupMenuButton = styled.button` 13 | background: none; 14 | outline: none; 15 | border: 1px solid #e6e6e6; 16 | border-radius: 100%; 17 | width: 40px; 18 | height: 40px; 19 | text-align: center; 20 | cursor: pointer; 21 | transition: 0.2s; 22 | 23 | :hover, 24 | :focus-visible { 25 | background: #efefef; 26 | } 27 | `; 28 | 29 | export const Menu = styled.ul` 30 | display: ${({ isOpen }) => (isOpen ? 'flex' : 'none')}; 31 | flex-direction: column; 32 | position: absolute; 33 | top: 0; 34 | left: 50px; 35 | background: #ffffff; 36 | border-radius: 8px; 37 | box-shadow: 0 12px 20px -4px rgba(0, 0, 0, 0.1), 0 0 8px 0 rgba(0, 0, 0, 0.08); 38 | width: 224px; 39 | z-index: 2; 40 | 41 | ${breakpoint.tablet` 42 | left: -235px; 43 | `} 44 | `; 45 | 46 | export const MenuItem = styled.li` 47 | line-height: 24px; 48 | padding: 8px 16px; 49 | cursor: pointer; 50 | transition: 0.2s; 51 | color: #0e103c; 52 | font-size: 16px; 53 | 54 | :hover, 55 | :focus-visible { 56 | color: #8a9ef5; 57 | } 58 | 59 | :first-of-type { 60 | margin-top: 16px; 61 | } 62 | 63 | :last-of-type { 64 | margin-bottom: 16px; 65 | } 66 | `; 67 | -------------------------------------------------------------------------------- /components/SalesHistoryTableCell/index.tsx: -------------------------------------------------------------------------------- 1 | import TableDataCell from '../TableDataCell'; 2 | import Image from 'next/image'; 3 | import Link from 'next/link'; 4 | import { AvatarImage, ImageDataCell } from './SalesHistoryTabelCell.styled'; 5 | 6 | type Props = { 7 | id: string; 8 | content: string; 9 | }; 10 | 11 | const SalesHistoryTableCell = ({ id, content }: Props): JSX.Element => { 12 | switch (id) { 13 | case 'img': { 14 | return ( 15 | 16 | 26 | 27 | ); 28 | } 29 | case 'serial': { 30 | return #{content}; 31 | } 32 | case 'tx': { 33 | return ( 34 | 35 | 38 | 39 | 46 | 47 | 48 | 49 | ); 50 | } 51 | default: { 52 | return {content}; 53 | } 54 | } 55 | }; 56 | 57 | export default SalesHistoryTableCell; 58 | -------------------------------------------------------------------------------- /services/cache.ts: -------------------------------------------------------------------------------- 1 | interface CacheValue { 2 | value: string; 3 | updatedAt: number; 4 | } 5 | 6 | export class Cache { 7 | cache: Map; 8 | maxLength: number; 9 | length: number; 10 | 11 | constructor() { 12 | this.cache = new Map(); 13 | this.maxLength = 1000; 14 | this.length = 0; 15 | } 16 | 17 | has(key: string): boolean { 18 | return this.cache.has(key); 19 | } 20 | 21 | set(key: string, value: string): number { 22 | if (this.length >= this.maxLength) { 23 | const leastUsedKey = this.leastRecentlyUsed(); 24 | this.delete(leastUsedKey); 25 | } 26 | 27 | if (!this.has(key)) this.length += 1; 28 | this.cache.set(key, { 29 | value, 30 | updatedAt: Date.now(), 31 | }); 32 | 33 | return this.length; 34 | } 35 | 36 | getValue(key: string): string { 37 | const { value } = this.cache.get(key); 38 | this.set(key, value); 39 | return value; 40 | } 41 | 42 | delete(key: string): number { 43 | this.length -= 1; 44 | this.cache.delete(key); 45 | return this.length; 46 | } 47 | 48 | clear(): number { 49 | this.length = 0; 50 | this.cache.clear(); 51 | return this.length; 52 | } 53 | 54 | leastRecentlyUsed(): string { 55 | const leastUsedKey = Array.from(this.cache.keys()).sort( 56 | (a, b) => this.cache.get(a).updatedAt - this.cache.get(b).updatedAt 57 | )[0]; 58 | return leastUsedKey; 59 | } 60 | 61 | getValues(keys: string[]): { [key: string]: string } { 62 | const cacheValue = {}; 63 | 64 | for (const key of keys) { 65 | cacheValue[key] = this.getValue(key); 66 | } 67 | 68 | return cacheValue; 69 | } 70 | } 71 | 72 | export default new Cache(); 73 | -------------------------------------------------------------------------------- /components/AssetFormTitle/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import Image from 'next/image'; 3 | import { useRouter } from 'next/router'; 4 | import { 5 | NameContainer, 6 | Name, 7 | General, 8 | Title, 9 | Author, 10 | CollectionIconContainer, 11 | } from './AssetFormTitle.styled'; 12 | import AssetFormSellPopupMenu from '../AssetFormSellPopupMenu'; 13 | import { capitalize } from '../../utils'; 14 | 15 | type Props = { 16 | templateName: string; 17 | collectionName: string; 18 | collectionAuthor: string; 19 | }; 20 | 21 | const AssetFormTitle = ({ 22 | templateName, 23 | collectionName, 24 | collectionAuthor, 25 | }: Props): JSX.Element => { 26 | const router = useRouter(); 27 | const isMyTemplate = router.pathname.includes('my-templates'); 28 | const redirectToAuthor = () => router.push(`/my-nfts/${collectionAuthor}`); 29 | 30 | useEffect(() => { 31 | router.prefetch(`/my-nfts/${collectionAuthor}`); 32 | }, []); 33 | 34 | return ( 35 | <> 36 | 37 | Crypto Monsters icon 45 | Crypto {capitalize(collectionName)} 46 | 47 | 48 | {templateName} 49 | {isMyTemplate && } 50 | 51 | 52 | Created by{' '} 53 | 54 | {capitalize(collectionAuthor)} 55 | 56 | 57 | 58 | ); 59 | }; 60 | 61 | export default AssetFormTitle; 62 | -------------------------------------------------------------------------------- /components/PageLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import Head from 'next/head'; 3 | import Router from 'next/router'; 4 | import { Main, Container } from './PageLayout.styled'; 5 | import { useModalContext, MODAL_TYPES } from '../Provider'; 6 | import { 7 | ClaimBalanceModal, 8 | CreateSaleModal, 9 | CreateMultipleSalesModal, 10 | CancelSaleModal, 11 | CancelMultipleSalesModal, 12 | } from '../Modal'; 13 | import { useEscapeKeyClose } from '../../hooks'; 14 | 15 | type Props = { 16 | title: string; 17 | children: ReactNode; 18 | }; 19 | 20 | const PageLayout = ({ title, children }: Props): JSX.Element => { 21 | const { closeModal, modalType } = useModalContext(); 22 | useEscapeKeyClose(closeModal); 23 | 24 | Router.events.on('routeChangeComplete', () => { 25 | window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); 26 | }); 27 | 28 | const renderModal = () => { 29 | switch (modalType) { 30 | case MODAL_TYPES.CLAIM: 31 | return ; 32 | case MODAL_TYPES.CREATE_SALE: 33 | return ; 34 | case MODAL_TYPES.CREATE_MULTIPLE_SALES: 35 | return ; 36 | case MODAL_TYPES.CANCEL_SALE: 37 | return ; 38 | case MODAL_TYPES.CANCEL_MULTIPLE_SALES: 39 | return ; 40 | default: 41 | return null; 42 | } 43 | }; 44 | 45 | return ( 46 |
47 | 48 | {`${title} - NFT Demo`} 49 | 50 | 51 | {children} 52 | {renderModal()} 53 |
54 | ); 55 | }; 56 | 57 | export default PageLayout; 58 | -------------------------------------------------------------------------------- /services/offers.ts: -------------------------------------------------------------------------------- 1 | import { Asset } from './assets'; 2 | import { toQueryString } from '../utils'; 3 | import { getFromApi } from '../utils/browser-fetch'; 4 | 5 | export type Offer = { 6 | contract: string; 7 | offer_id: string; 8 | sender_name: string; 9 | recipient_name: string; 10 | memo: string; 11 | state: number; 12 | sender_assets: Asset[]; 13 | recipient_assets: Asset[]; 14 | is_sender_contract: boolean; 15 | is_recipient_contract: boolean; 16 | updated_at_block: string; 17 | updated_at_time: string; 18 | created_at_block: string; 19 | created_at_time: string; 20 | }; 21 | 22 | /** 23 | * Get list of assets that user has listed for sale 24 | * @param sender The account name of the owner of the assets to look up 25 | * @returns {Offer[]} 26 | */ 27 | 28 | export const getUserOffers = async (sender: string): Promise => { 29 | try { 30 | const limit = 100; 31 | let offers = []; 32 | let hasResults = true; 33 | let page = 1; 34 | 35 | while (hasResults) { 36 | const queryObject = { 37 | sender, 38 | state: '0', // Offer created and valid 39 | page, 40 | limit, 41 | }; 42 | const queryParams = toQueryString(queryObject); 43 | const result = await getFromApi( 44 | `${process.env.NEXT_PUBLIC_NFT_ENDPOINT}/atomicassets/v1/offers?${queryParams}` 45 | ); 46 | 47 | if (!result.success) { 48 | throw new Error((result.message as unknown) as string); 49 | } 50 | 51 | if (result.data.length < limit) { 52 | hasResults = false; 53 | } 54 | 55 | offers = offers.concat(result.data); 56 | page += 1; 57 | } 58 | 59 | return offers; 60 | } catch (e) { 61 | throw new Error(e); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import type { AppProps } from 'next/app'; 4 | import Router from 'next/router'; 5 | import NProgress from 'nprogress'; 6 | import '../styles/reset.css'; 7 | import '../styles/globals.css'; 8 | import NavBar from '../components/NavBar'; 9 | import Footer from '../components/Footer'; 10 | import { AuthProvider, ModalProvider } from '../components/Provider'; 11 | import '../styles/customprogress.css'; 12 | import * as gtag from '../utils/gtag'; 13 | 14 | NProgress.configure({ 15 | minimum: 0.3, 16 | easing: 'ease', 17 | speed: 800, 18 | showSpinner: false, 19 | }); 20 | 21 | function MyApp({ Component, pageProps }) { 22 | const start = () => NProgress.start(); 23 | const end = (url) => { 24 | NProgress.done(); 25 | gtag.pageview(url); 26 | }; 27 | 28 | /* 29 | useEffect(() => { 30 | if (process.env.NODE_ENV !== 'production') { 31 | // eslint-disable-next-line @typescript-eslint/no-var-requires 32 | const axe = require('@axe-core/react'); 33 | axe(React, ReactDOM, 1000); 34 | } 35 | }, []); 36 | */ 37 | 38 | useEffect(() => { 39 | Router.events.on('routeChangeStart', start); 40 | Router.events.on('routeChangeComplete', end); 41 | Router.events.on('routeChangeError', end); 42 | return () => { 43 | Router.events.off('routeChangeStart', start); 44 | Router.events.off('routeChangeComplete', end); 45 | Router.events.off('routeChangeError', end); 46 | }; 47 | }, []); 48 | 49 | return ( 50 | 51 | 52 | 53 | 54 |