├── .bin └── gatsby-transformer-video │ └── darwin-x64 │ ├── ffmpeg │ └── ffprobe ├── .cache-video └── videos │ ├── 3d_toys_playground-5a319b2b041635649dbefd017a5619f5-cd2d2d1c8620b4b9bf8529b5962aa8f4-h264.mp4 │ ├── 3d_toys_playground-bfbf5d170a47f2d2e02ab54877dcff39-cd2d2d1c8620b4b9bf8529b5962aa8f4-h264.mp4 │ └── pexels-olya-kobruseva-5236593-4371a36bd10c54527439c433f6cdab63-cd2d2d1c8620b4b9bf8529b5962aa8f4-h264.mp4 ├── .gitattributes ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .graphqlrc.yml ├── .prettierignore ├── .prettierrc ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── config ├── i18n │ ├── README.md │ ├── en.json │ ├── es.json │ └── zh.json ├── images │ ├── discord.svg │ ├── example.png │ ├── favicon.png │ ├── tenk-logo.png │ └── twitter.svg ├── settings.json ├── styles.scss └── videos │ ├── .gitkeep │ └── 3d_toys_playground.mp4 ├── gatsby-config.ts ├── gatsby-node.ts ├── lib └── locales │ ├── Locale.ts │ ├── Locale.validator.ts │ ├── index.ts │ └── runtimeUtils.ts ├── package.json ├── src ├── components │ ├── background-image.tsx │ ├── banner │ │ ├── banner.module.css │ │ ├── banner.module.css.d.ts │ │ └── index.tsx │ ├── dropdown │ │ ├── dropdown.module.css │ │ ├── dropdown.module.css.d.ts │ │ └── index.tsx │ ├── footer │ │ ├── footer.module.css │ │ ├── footer.module.css.d.ts │ │ └── index.tsx │ ├── hero │ │ ├── hero.module.css │ │ ├── hero.module.css.d.ts │ │ └── index.tsx │ ├── image.tsx │ ├── lang-picker │ │ ├── index.tsx │ │ ├── lang-picker.module.css │ │ └── lang-picker.module.css.d.ts │ ├── layout │ │ ├── a11y.css │ │ ├── atcb.css │ │ ├── index.tsx │ │ ├── layout.module.css │ │ ├── layout.module.css.d.ts │ │ └── layout.scss │ ├── markdown.tsx │ ├── my-nfts │ │ ├── index.tsx │ │ ├── my-nfts.module.css │ │ └── my-nfts.module.css.d.ts │ ├── nav │ │ ├── index.tsx │ │ ├── nav.module.css │ │ └── nav.module.css.d.ts │ ├── section │ │ ├── index.tsx │ │ ├── section.module.css │ │ └── section.module.css.d.ts │ ├── seo.tsx │ ├── slider │ │ ├── index.tsx │ │ ├── slider.module.css │ │ └── slider.module.css.d.ts │ └── video │ │ └── index.tsx ├── hooks │ ├── useHeroStatuses.ts │ ├── useImageData.ts │ ├── useLocales.ts │ ├── useTenk.ts │ └── useVideoData.ts ├── near │ ├── contracts │ │ ├── index.ts │ │ └── tenk.ts │ └── index.ts ├── pages │ ├── 404.js │ └── index.tsx └── templates │ └── [locale].tsx ├── stale-data-from-build-time.json ├── tsconfig.json └── yarn.lock /.bin/gatsby-transformer-video/darwin-x64/ffmpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TENK-DAO/frontend-starter/4c5e78745c4db8ced3e074d08c6041f24c357abe/.bin/gatsby-transformer-video/darwin-x64/ffmpeg -------------------------------------------------------------------------------- /.bin/gatsby-transformer-video/darwin-x64/ffprobe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TENK-DAO/frontend-starter/4c5e78745c4db8ced3e074d08c6041f24c357abe/.bin/gatsby-transformer-video/darwin-x64/ffprobe -------------------------------------------------------------------------------- /.cache-video/videos/3d_toys_playground-5a319b2b041635649dbefd017a5619f5-cd2d2d1c8620b4b9bf8529b5962aa8f4-h264.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TENK-DAO/frontend-starter/4c5e78745c4db8ced3e074d08c6041f24c357abe/.cache-video/videos/3d_toys_playground-5a319b2b041635649dbefd017a5619f5-cd2d2d1c8620b4b9bf8529b5962aa8f4-h264.mp4 -------------------------------------------------------------------------------- /.cache-video/videos/3d_toys_playground-bfbf5d170a47f2d2e02ab54877dcff39-cd2d2d1c8620b4b9bf8529b5962aa8f4-h264.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TENK-DAO/frontend-starter/4c5e78745c4db8ced3e074d08c6041f24c357abe/.cache-video/videos/3d_toys_playground-bfbf5d170a47f2d2e02ab54877dcff39-cd2d2d1c8620b4b9bf8529b5962aa8f4-h264.mp4 -------------------------------------------------------------------------------- /.cache-video/videos/pexels-olya-kobruseva-5236593-4371a36bd10c54527439c433f6cdab63-cd2d2d1c8620b4b9bf8529b5962aa8f4-h264.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TENK-DAO/frontend-starter/4c5e78745c4db8ced3e074d08c6041f24c357abe/.cache-video/videos/pexels-olya-kobruseva-5236593-4371a36bd10c54527439c433f6cdab63-cd2d2d1c8620b4b9bf8529b5962aa8f4-h264.mp4 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | lib/locales/Locale.validator.ts linguist-generated=true -diff 2 | src/near/contracts/tenk.ts linguist-generated=true -diff 3 | src/hooks/stale-data-from-build-time.json linguist-generated=true -diff 4 | yarn.lock linguist-generated=true -diff 5 | **/docs/**/* linguist-generated=true -diff 6 | .cache-video/** linguist-generated=true -diff -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | build-and-deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 🛎️ 11 | uses: actions/checkout@v2.3.1 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly. 12 | with: 13 | persist-credentials: false 14 | - uses: FedericoCarboni/setup-ffmpeg@v1 15 | id: setup-ffmpeg 16 | - name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built. 17 | run: | 18 | yarn install --frozen-lockfile 19 | yarn build 20 | env: 21 | PREFIX_PATHS: true 22 | - name: Deploy 🚀 23 | uses: JamesIves/github-pages-deploy-action@3.7.1 24 | with: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | BRANCH: gh-pages # The branch the action should deploy to. 27 | FOLDER: public # The folder the action should deploy. 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variable files 55 | .env* 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # Auto-generated by gatsby-plugin-graphql-codegen 72 | graphql-types.ts 73 | 74 | # Auto-generated by gatsby-plugin-extract-schema 75 | schema.graphql -------------------------------------------------------------------------------- /.graphqlrc.yml: -------------------------------------------------------------------------------- 1 | # Used by https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql 2 | schemaPath: "schema.graphql" 3 | extensions: 4 | endpoints: 5 | default: 6 | url: "http://localhost:8000/___graphql" 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | package.json 3 | package-lock.json 4 | public 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | // List of extensions which should be recommended for users of this workspace. 5 | "recommendations": [ 6 | "GraphQL.vscode-graphql", 7 | "syler.sass-indented" 8 | ], 9 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 10 | "unwantedRecommendations": [] 11 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TENK NFT Launch Landing Page Template 2 | 3 | Welcome to your new TENK NFT Launch Landing Page! This codebase gives you everything you need to make a simple-but-powerful landing page for your NFT project with near-zero custom code. Out-of-the-box, you get: 4 | 5 | * Super fast page loads using a Static Site Generator called [Gatsby]. 6 | * Internationalization: offer your website in multiple languages for the global NFT market. If you need help translating your page, contact [the TENK DAO team][TENK DAO]. 7 | * Good SEO and social media sharing support. 8 | * Simple customization via the handful of files in the [`config`](./config/) folder. 9 | 10 | If you need even more customization, you can rest easy knowing that the whole site uses fully-typed TypeScript and a well-considered React architecture with few extensions or add-ons. 11 | 12 | [Gatsby]: https://www.gatsbyjs.com 13 | [TENK DAO]: https://tenkbay.com/ 14 | 15 | # Run it 16 | 17 | To run this site locally, you will need [NodeJS] and [Yarn] installed. Then, change into the project directory using your command line and: 18 | 19 | * `yarn install` 20 | * `yarn start` (New to NodeJS? This runs the "start" script listed in the "scripts" section of the [package.json](./package.json) file.) 21 | 22 | Go ahead and do some stretching while that starts; it takes a while. 23 | 24 | # Tweak it 25 | 26 | Install [Virtual Studio Code][VSCode] (often called VS Code) if you don't have it already. Then open this project folder with it. This project includes a `.vscode` folder which will recommend extensions to improve your experience. Install them! 27 | 28 | You only need to edit files in the [`config`](./config/) folder: 29 | 30 | * `styles.scss`: This is a [Sass] file used by the site to customize your colors. If you install the recommended Sass extension for VS Code, you will see color squares next to each value. 31 | * `settings.json`: This contains non-internationalized data for your site, including the address of your smart contract. 32 | * `images/`: The images in this folder are referenced later by your internationalization data. 33 | * `i18n/`: Your internationalization data! (There are 18 letters between the "i" and "n" in "internationalization".) There's a JSON file for each language you want to include. These will become routes on your site: `example.com/en`, `example.com/es`, etc. JSON is a finicky specification, throwing errors if you forget quotes or include trailing commas. VS Code will yell at you about these problems, which saves you from needing to restart your Gatsby server to notice them. And yes, when you make changes to these files, you'll need to restart your development Gatsby server to see them. For more info about the shape of these files, see the readme [in the `i18n` folder](./config/i18n/). 34 | 35 | Make changes, commit them to git, and push. The included [GitHub Action] will [automatically deploy your pushed changes to GitHub Pages](./.github/workflows/deploy.yml). If you want to deploy somewhere else, you can! Static sites contain nothing but HTML, CSS, and JavaScript, making them the easiest sites to deploy. You could host these files on [IPFS], an AWS S3 bucket, or on many other hosting providers. You could automate deploys to one of these other networks/platforms by customizing the [`deploy.yml`](./.github/workflows/deploy.yml) GitHub Action. There are many GitHub Actions available to help you on [GitHub's marketplace](https://github.com/marketplace?category=deployment&type=actions). 36 | 37 | [NodeJS]: https://nodejs.dev/learn/how-to-install-nodejs 38 | [Yarn]: https://yarnpkg.com/ 39 | [VSCode]: https://code.visualstudio.com/ 40 | [Sass]: https://sass-lang.com/ 41 | [IPFS]: https://ipfs.io/ 42 | [GitHub Action]: https://github.com/features/actions 43 | 44 | # Grok it 45 | 46 | Start by understanding the Gatsby files: 47 | 48 | * [`gatsby-config.js`](./gatsby-config.js) bootstraps TypeScript for Gatsby 49 | * [`gatsby-config.ts`](./gatsby-config.ts) contains the main configuration for Gatsby, including a hefty stack of [plugins](https://www.gatsbyjs.com/plugins) 50 | * [`gatsby-node.ts`](./gatsby-node.ts) is a NodeJS script run by Gatsby at build time (when you run `yarn build`, which happens automatically during the deploy GitHub Action) or when you first start your development server (with `yarn start`). 51 | 52 | This last one is interesting. Reading through it, you'll see that it creates a new page for each JSON file in `config/i18n`. It grabs these files using a `locales` utility located at [`lib/locales`](./lib/locales/index.ts) (you can option-click the import line in VS Code to open the imported file directly) and renders them using a React file at [`src/templates/[locale].tsx`](./src/templates/[locale].tsx). Let's understand each of these next: 53 | 54 | * [`lib/locales`](./lib/locales/index.ts): the main export here is down at the bottom: `export const locales`. Everything above that is TypeScript stuff and data validation. This data validation will ensure that each locale is well-formatted with only expected fields. This is great! This means that you can edit your internationalization data confidently, knowing that it will be verified at build time, way before you have a chance to deploy a broken site and have users report bugs. 55 | * [`src/templates/[locale].tsx`](./src/templates/[locale].tsx): The filename here is not special, it's just a naming convention borrowed from NextJS, a competitor to Gatsby. (Square brackets are valid in filenames!) It is a React [JSX](https://reactjs.org/docs/introducing-jsx.html) file that uses TypeScript, hence the `.tsx` extension. As we already saw, this is the file that will be used by `gatsby-node.ts` to create each of the main routes in your page: `/en`, `/es`, etc. 56 | 57 | The contents of this last file almost look too simple! The import statements take up about as many lines as the main export. At this point you're about ready to go explore the code directly, option-clicking into various files to figure out what they do. Before you do, it's worth knowing about just a few more interesting bits: 58 | 59 | * [`src/near`](./src/near/) contains NEAR bootstrapping logic and a TypeScript wrapper around the TENK smart contract. This is amazing! This means you can use TypeScript-powered type-ahead to see what methods are available on your contract as you write your frontend code. If you want to test certain UI states that rely on certain data being returned from your contract, you can return spoof data in `src/near/contracts/tenk.ts` (just remember to change it back without committing it to git!) 60 | * [`src/hooks/useTenk.ts`](./src/hooks/useTenk.ts) contains a [custom React hook](https://reactjs.org/docs/hooks-custom.html) that makes RPC calls to your TENK smart contract once at page load. All React components that use this hook then make use of this data, without requiring new RPC calls. 61 | * [`src/hooks/useLocales.ts`](./src/hooks/useLocales.ts) is another custom hook that wraps the complex Gatsby logic needed to use your internationalization data in files outside of `[locale].tsx`. Gatsby forces all data, even simple JSON files, through a Rube Goldberg-like GraphQL pipeline ([GraphQL](https://graphql.org/) is great; Gatsby's overuse of it is tiring). Luckily, this project has automatic TypeScript typing on these GraphQL queries, improving the situation slightly. Note that the locales returned by `useLocales` differ slightly from the ones passed to `[locale].tsx`: they **do not contain `hero` or `extraSections` fields**. (This is due to a limitation of the `gatsby-transformer-json` plugin, which expects all JSON files to have exactly matching fields, while the locale format used by this project is more flexible.) 62 | * [`src/pages/index.tsx`](./src/pages/index.tsx) is the index page of the site. You'll see that it just lists all available locales, and then uses the [`useEffect` React hook](https://reactjs.org/docs/hooks-effect.html) to automatically redirect users to their current locale, if found. -------------------------------------------------------------------------------- /config/i18n/README.md: -------------------------------------------------------------------------------- 1 | # Internationalize your site 2 | 3 | This folder contains a JSON file for each locale you want to include. Add a new one and it will automatically be incorporated into your site next time you build/deploy or restart your development server. 4 | 5 | At build time (when you run `yarn build` as during a deploy, or when you restart your `yarn start` development server) the data in these files will be validated. This means you can be confident that you included all necessary fields, didn't misspell optional fields, and that every possible `hero` variant is well-formed. (The data validation does not and cannot validate that you included reasonable translations in each file, though!) 6 | 7 | # About that hero 8 | 9 | The most important part of your site is the hero section. This is the above-the-fold, right-at-the-top part that people see first. 10 | 11 | This site is designed so that everything important happens here. Before your NFT launch, it tells people when the launch will happen and encourages them to add it to their calendars. During the launch, it's where they buy NFTs. Afterwards, the hero section links to the secondary market on [Paras](https://paras.id/). 12 | 13 | In the example files contained here, you'll see that the `hero` i18n data is designed to be as compact as possible, minimizing duplication and thus your chance to make mistakes. Let's look at an example: 14 | 15 | ```js 16 | "hero": { 17 | "backgroundImage": "hero-bg.svg", 18 | "image": "hero.png", 19 | "title": "The first fleet of the \n*Metaverse*", 20 | "body": "Join an Exclusive Community of NEAR early adopters and BUIDLers.", 21 | "ps": "Misfits drop at SALE_START!", 22 | "cta": "Add to Calendar", 23 | "action": "ADD_TO_CALENDAR(SALE_START)", 24 | "saleClosed": { 25 | "signedIn": { 26 | "ps": "* Minting starts at: SALE_START\n\n* Pre-mint starts at: PRESALE_START\n\nGet in on the pre-mint! [Join our Discord](https://discord.com/invite/UY9Xf2k) and request an invite." 27 | }, 28 | "vip": { 29 | "ps": "Welcome, CURRENT_USER! Pre-mint starts at PRESALE_START", 30 | "action": "ADD_TO_CALENDAR(PRESALE_START)" 31 | } 32 | }, 33 | "presale": { 34 | "vip": { 35 | "ps": "Welcome, CURRENT_USER! Pre-mint started. Public minting starts at SALE_START.\n\nYour remaining pre-mint allowance: MINT_LIMIT", 36 | "cta": "Mint One!", 37 | "action": "MINT_ONE" 38 | } 39 | }, 40 | "saleOpen": { 41 | "ps": "Misfit Minting Has Begun!", 42 | "cta": "Mint One!", 43 | "action": "MINT_ONE", 44 | "signedOut": { 45 | "action": "SIGN_IN" 46 | } 47 | }, 48 | "allSold": { 49 | "title": "All INITIAL_COUNT minted!", 50 | "body": "The Misfits have all been created", 51 | "ps": "", 52 | "cta": "Buy Pre-Owned", 53 | "action": "GO_TO_PARAS" 54 | } 55 | } 56 | ``` 57 | 58 | As you may have guessed, a more specific setting will override a more general one. So here, before the sale has started (`saleClosed`): 59 | 60 | * Someone who is signed out will see the `ps` included in the root: "Misfits drop at SALE_START!" 61 | * While someone who is signed in will see both the sale start time and the presale start time, with a link to the project's Discord to become a VIP 62 | * And someone who is already a VIP will see only the presale start time, and their Call To Action button (CTA) will add the presale time to their calendar, rather than the sale time. 63 | 64 | And then, after the public sale has started (`saleOpen`), almost everyone sees the same thing. "Minting has begun! Mint One!" The only exception here is that people who are not yet signed in will get signed in, when they click the Mint One button, rather than immediately minting one. 65 | 66 | The `image` and `backgroundImage` values need to have corresponding images in [`config/images`](../images/). 67 | 68 | The list of valid `action`s is given by the keys of `const actions` in [`lib/locales/runtimeUtils.ts`](../../lib/locales/runtimeUtils.ts), and the list of valid placeholder strings like `CURRENT_USER` and `SALE_START` is given by the keys of `const replacers` in the same file. If you use an invalid action, the data will not validate and your build will fail, so you're protected from deploying a broken site. If you use an invalid placeholder string, you will only see a warning at build time, but the site will still build! This is because it's impossible to know at build time if you purposely included UPPERCASE_TEXT_WITH_UNDERSCORES_IN_BETWEEN. Maybe this is a stylistic choice that your NFT project made on purpose! So remember to double-check those warnings during `yarn start`. And... 69 | 70 | # Test your hero variants 71 | 72 | When you're ready to test your hero variants, start your development server, load your site, and then manually add `?hero=0` to the end of the URL in your browser. This will add a banner to the site showing what settings you're testing (`saleClosed` and `signedOut`), and allow you to quickly click through all twelve variants. 73 | 74 | Note that this hidden testing feature is included in your deployed site as well. This makes it easier to collaborate with your larger team of copywriters and translators. You can send these non-technical teammates URLs to your deployed site like `example.com/zh?hero=0` and have them click through and check that everything looks good, without them needing to run the site on their own computers. -------------------------------------------------------------------------------- /config/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "viewIn": "View in English", 3 | "langPicker": "English", 4 | "title": "Cool NFT Project", 5 | "description": "This is the minting page for Cool NFT Project", 6 | "calendarEvent": "Go Mint an NFT!", 7 | "connectWallet": "Login with NEAR", 8 | "signOut": "Sign Out", 9 | "new": "New!", 10 | "myNFTs": "My NFTs", 11 | "nextNFT": "Next NFT", 12 | "prevNFT": "Previous NFT", 13 | "close": "Close", 14 | "zoomIn": "Zoom In", 15 | "zoomOut": "Zoom Out", 16 | "hero": { 17 | "video": "3d_toys_playground.mp4", 18 | "title": "NFTs drop at SALE_START!", 19 | "body": "Tagline goes here.", 20 | "remaining": "REMAINING_COUNT / INITIAL_COUNT left", 21 | "cta": "Add to Calendar", 22 | "action": "ADD_TO_CALENDAR(SALE_START)", 23 | "saleClosed": { 24 | "ps": "Want early access? Sign in.", 25 | "signedIn": { 26 | "ps": "Pre-mint start: PRESALE_START\n\nJoin the whitelist! [Join our Discord](https://discord.com/invite/UY9Xf2k) and request an invite." 27 | }, 28 | "vip": { 29 | "title": "Welcome, CURRENT_USER!", 30 | "ps": "Pre-mint start: PRESALE_START", 31 | "action": "ADD_TO_CALENDAR(PRESALE_START)" 32 | } 33 | }, 34 | "presale": { 35 | "ps": "Pre-mint started!\n\nGet in on it! [Join our Discord](https://discord.com/invite/UY9Xf2k) and request an invite.", 36 | "vip": { 37 | "title": "Pre-mint started!", 38 | "body": "Public minting starts at SALE_START", 39 | "ps": "Your remaining pre-mint allowance: MINT_LIMIT", 40 | "setNumber": "Number to mint", 41 | "cta": "Mint for MINT_PRICE", 42 | "action": "MINT" 43 | } 44 | }, 45 | "saleOpen": { 46 | "title": "Minting has begun!", 47 | "ps": "", 48 | "setNumber": "Number to mint", 49 | "cta": "Mint for MINT_PRICE", 50 | "action": "MINT", 51 | "signedOut": { 52 | "action": "SIGN_IN" 53 | } 54 | }, 55 | "allSold": { 56 | "title": "All INITIAL_COUNT minted!", 57 | "body": "The NFTs have all been created", 58 | "ps": "", 59 | "cta": "Buy on Marketplace", 60 | "action": "GO_TO_PARAS" 61 | } 62 | }, 63 | "extraSections": [ 64 | { 65 | "className": "this-class-is-set-in-i18n-files", 66 | "text": "# Introducing Cool NFT Project\n\nThis is a longer description of the project. About a paragraph long. Some [filler text](https://placehodler.shapelabs.co/) to demonstrate: Token standard surrendered some burned mnemonic phrase until some wallet because blockchain detected many pre-mine. It based on many automated token generation event, and when Tether formed some trustless in the FUD, Dogecoin slept on lots of anti-money laundering of many stale block!\n\nSolidity limited lots of reinvested distributed denial of service attack! ICO specialises in lots of lightning fast private chain since Zilliqa surrendered few reinvested cryptocurrency, however, because someone limited few provably fiat until the vanity address, Augur stuck many burned pre-mine. They sharded many technical analysis, or NFT specialises in the FOMO in few wash trade.", 67 | "image": "example.png" 68 | } 69 | ] 70 | } -------------------------------------------------------------------------------- /config/i18n/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "viewIn": "Ver en Español", 3 | "langPicker": "Español", 4 | "title": "Cool NFT Project", 5 | "description": "Los primeros NFTs de tipo \"PFP\" de NEAR", 6 | "calendarEvent": "¡Crear un NFT!", 7 | "connectWallet": "Iniciar Sesión con NEAR", 8 | "signOut": "Desconectar", 9 | "new": "¡Nuevo!", 10 | "myNFTs": "Mi NFTs", 11 | "nextNFT": "Siguiente NFT", 12 | "prevNFT": "Anterior NFT", 13 | "close": "Cerrar", 14 | "zoomIn": "Dar un golpe de zoom", 15 | "zoomOut": "Disminuir el zoom", 16 | "hero": { 17 | "video": "3d_toys_playground.mp4", 18 | "title": "El Eslogan Va Aquí", 19 | "body": "Una descripción de una línea va aquí.", 20 | "ps": "¡Puedes crear tu primero a las SALE_START!", 21 | "remaining": "NFTs que quedan: REMAINING_COUNT", 22 | "cta": "Añadir a Calendario", 23 | "action": "ADD_TO_CALENDAR(SALE_START)", 24 | "saleClosed": { 25 | "signedIn": { 26 | "ps": "* Crear tu primero: SALE_START\n\n* Si estás en el listo \"VIP\": PRESALE_START\n\nQuedate un/a VIP! [Joino nuestro Discord](https://discord.com/invite/UY9Xf2k)." 27 | }, 28 | "vip": { 29 | "title": "¡Bienvenid@, CURRENT_USER!", 30 | "ps": "Puedes crear un NFT a las PRESALE_START", 31 | "action": "ADD_TO_CALENDAR(PRESALE_START)" 32 | } 33 | }, 34 | "presale": { 35 | "vip": { 36 | "title": "La creación temprano ha comenzado!", 37 | "ps": "El creación publico empezar a las SALE_START.\n\nTu VIP asignación restante: MINT_LIMIT", 38 | "setNumber": "Crear quantidad de", 39 | "cta": "Crear para MINT_PRICE", 40 | "action": "MINT" 41 | } 42 | }, 43 | "saleOpen": { 44 | "title": "Minting has begun!", 45 | "ps": "", 46 | "setNumber": "Crear quantidad de", 47 | "cta": "Crear para MINT_PRICE", 48 | "action": "MINT", 49 | "signedOut": { 50 | "action": "SIGN_IN" 51 | } 52 | }, 53 | "allSold": { 54 | "title": "¡Todos INITIAL_COUNT han estado creado!", 55 | "body": "", 56 | "ps": "", 57 | "cta": "Compra en el Mercado", 58 | "action": "GO_TO_PARAS" 59 | } 60 | }, 61 | "extraSections": [ 62 | { 63 | "className": "this-class-is-set-in-i18n-files", 64 | "text": "# Introducing Cool NFT Project\n\nThis is a longer description of the project. About a paragraph long. Some [filler text](https://placehodler.shapelabs.co/) to demonstrate: Token standard surrendered some burned mnemonic phrase until some wallet because blockchain detected many pre-mine. It based on many automated token generation event, and when Tether formed some trustless in the FUD, Dogecoin slept on lots of anti-money laundering of many stale block!\n\nSolidity limited lots of reinvested distributed denial of service attack! ICO specialises in lots of lightning fast private chain since Zilliqa surrendered few reinvested cryptocurrency, however, because someone limited few provably fiat until the vanity address, Augur stuck many burned pre-mine. They sharded many technical analysis, or NFT specialises in the FOMO in few wash trade.", 65 | "image": "example.png" 66 | } 67 | ] 68 | } -------------------------------------------------------------------------------- /config/i18n/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "viewIn": "中文查看", 3 | "langPicker": "中文", 4 | "title": "酷NFT项目", 5 | "description": "NEAR生态上首个头像类NFT", 6 | "calendarEvent": "立即铸造!", 7 | "connectWallet": "使用 NEAR 登录", 8 | "signOut": "登出", 9 | "new": "新的!", 10 | "myNFTs": "我的NFT", 11 | "nextNFT": "下一个NFT", 12 | "prevNFT": "上一个NFT", 13 | "close": "关闭", 14 | "zoomIn": "放大", 15 | "zoomOut": "缩小", 16 | "hero": { 17 | "video": "3d_toys_playground.mp4", 18 | "title": "标语在这里", 19 | "body": "一行描述在这里", 20 | "ps": "首发日期 SALE_START!", 21 | "remaining": "剩余NFT:REMAINING_COUNT", 22 | "cta": "添加到日历", 23 | "action": "ADD_TO_CALENDAR(SALE_START)", 24 | "saleClosed": { 25 | "signedIn": { 26 | "ps": "* 首发单价: SALE_START\n\n* 预售单价: PRESALE_START\n\n加入预售! [加入社区 Discord](https://discord.com/invite/UY9Xf2k) 参与白名单活动" 27 | }, 28 | "vip": { 29 | "title": "欢迎, CURRENT_USER!", 30 | "ps": "预售单价: PRESALE_START", 31 | "action": "ADD_TO_CALENDAR(PRESALE_START)" 32 | } 33 | }, 34 | "presale": { 35 | "vip": { 36 | "title": "早期创作已经开始", 37 | "ps": "公开销售时间为 SALE_START.\n\n 剩余购买额度: MINT_LIMIT", 38 | "setNumber": "购买数量", 39 | "cta": "支付金额: MINT_PRICE", 40 | "action": "MINT" 41 | } 42 | }, 43 | "saleOpen": { 44 | "title": "铸币开始了!", 45 | "ps": "", 46 | "setNumber": "购买数量", 47 | "cta": "支付金额: MINT_PRICE", 48 | "action": "MINT", 49 | "signedOut": { 50 | "action": "SIGN_IN" 51 | } 52 | }, 53 | "allSold": { 54 | "title": "All INITIAL_COUNT 售罄!", 55 | "body": "The NFTs 已经全部售罄", 56 | "ps": "", 57 | "cta": "在市场上购买", 58 | "action": "GO_TO_PARAS" 59 | } 60 | }, 61 | "extraSections": [ 62 | { 63 | "className": "this-class-is-set-in-i18n-files", 64 | "text": "# Introducing Cool NFT Project\n\nThis is a longer description of the project. About a paragraph long. Some [filler text](https://placehodler.shapelabs.co/) to demonstrate: Token standard surrendered some burned mnemonic phrase until some wallet because blockchain detected many pre-mine. It based on many automated token generation event, and when Tether formed some trustless in the FUD, Dogecoin slept on lots of anti-money laundering of many stale block!\n\nSolidity limited lots of reinvested distributed denial of service attack! ICO specialises in lots of lightning fast private chain since Zilliqa surrendered few reinvested cryptocurrency, however, because someone limited few provably fiat until the vanity address, Augur stuck many burned pre-mine. They sharded many technical analysis, or NFT specialises in the FOMO in few wash trade.", 65 | "image": "example.png" 66 | } 67 | ] 68 | } -------------------------------------------------------------------------------- /config/images/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | Discord 3 | 4 | -------------------------------------------------------------------------------- /config/images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TENK-DAO/frontend-starter/4c5e78745c4db8ced3e074d08c6041f24c357abe/config/images/example.png -------------------------------------------------------------------------------- /config/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TENK-DAO/frontend-starter/4c5e78745c4db8ced3e074d08c6041f24c357abe/config/images/favicon.png -------------------------------------------------------------------------------- /config/images/tenk-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TENK-DAO/frontend-starter/4c5e78745c4db8ced3e074d08c6041f24c357abe/config/images/tenk-logo.png -------------------------------------------------------------------------------- /config/images/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | Twitter 3 | 4 | -------------------------------------------------------------------------------- /config/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "@TenkDAO", 3 | "siteUrl": "https://tenk-dao.github.io/frontend-starter", 4 | "contractName": "v2.tenk.testnet", 5 | "image": "tenk-logo.png", 6 | "social": [ 7 | { 8 | "href": "https://discord.com/invite/UY9Xf2k", 9 | "img": "discord.svg", 10 | "alt": "Discord" 11 | }, 12 | { 13 | "href": "https://twitter.com/TenkDAO", 14 | "img": "twitter.svg", 15 | "alt": "Twitter" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /config/styles.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Make sure these look good in light mode and dark mode! 3 | * If you omit any values, defaults will be used. 4 | */ 5 | $white: #fdfdfd; 6 | $black: #262626; 7 | $accent: #e73b93; 8 | $secondary: adjust-color($accent, $hue: 90deg); 9 | 10 | .this-class-is-set-in-i18n-files h1 { 11 | /** 12 | * You could add other styles like this 13 | */ 14 | } -------------------------------------------------------------------------------- /config/videos/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TENK-DAO/frontend-starter/4c5e78745c4db8ced3e074d08c6041f24c357abe/config/videos/.gitkeep -------------------------------------------------------------------------------- /config/videos/3d_toys_playground.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TENK-DAO/frontend-starter/4c5e78745c4db8ced3e074d08c6041f24c357abe/config/videos/3d_toys_playground.mp4 -------------------------------------------------------------------------------- /gatsby-config.ts: -------------------------------------------------------------------------------- 1 | import { GatsbyConfig } from "gatsby" 2 | import settings from "./config/settings.json" 3 | import pkg from "./package.json" 4 | 5 | const config: GatsbyConfig = { 6 | // Build with env var PREFIX_PATHS=true to prefix all links & image paths with pathPrefix 7 | // see https://www.gatsbyjs.com/docs/how-to/previews-deploys-hosting/path-prefix/ 8 | pathPrefix: pkg.name, 9 | siteMetadata: settings, 10 | plugins: [ 11 | `gatsby-plugin-graphql-codegen`, 12 | // generates `./schema.graphql`, which is referenced by `.graphqlrc.yml`, used by VS Code plugin GraphQL.vscode-graphql 13 | `gatsby-plugin-extract-schema`, 14 | // types for CSS modules 15 | `gatsby-plugin-dts-css-modules`, 16 | `gatsby-plugin-react-helmet`, 17 | `gatsby-transformer-json`, 18 | { 19 | resolve: `gatsby-source-filesystem`, 20 | options: { 21 | name: `i18n`, 22 | path: `config/i18n`, 23 | }, 24 | }, 25 | `gatsby-transformer-inline-svg`, 26 | { 27 | resolve: `gatsby-source-filesystem`, 28 | options: { 29 | name: `images`, 30 | path: `config/images`, 31 | }, 32 | }, 33 | { 34 | // See https://www.gatsbyjs.com/plugins/gatsby-plugin-image/ 35 | resolve: `gatsby-plugin-sharp`, 36 | options: { 37 | defaults: { 38 | formats: [`auto`, `avif`, `webp`], 39 | placeholder: `blurred`, 40 | }, 41 | }, 42 | }, 43 | `gatsby-transformer-video`, 44 | { 45 | resolve: `gatsby-source-filesystem`, 46 | options: { 47 | name: `videos`, 48 | path: `config/videos`, 49 | }, 50 | }, 51 | `gatsby-transformer-sharp`, 52 | `gatsby-plugin-image`, 53 | { 54 | resolve: `gatsby-plugin-manifest`, 55 | options: { 56 | name: pkg.name, 57 | short_name: pkg.name, 58 | start_url: `/`, 59 | background_color: `#663399`, 60 | // This will impact how browsers show your PWA/website 61 | // https://css-tricks.com/meta-theme-color-and-trickery/ 62 | // theme_color: `#663399`, 63 | display: `minimal-ui`, 64 | icon: `config/images/favicon.png`, // This path is relative to the root of the site. 65 | }, 66 | }, 67 | `gatsby-plugin-sass`, 68 | `gatsby-plugin-portal`, 69 | // this (optional) plugin enables Progressive Web App + Offline functionality 70 | // To learn more, visit: https://gatsby.dev/offline 71 | // `gatsby-plugin-offline`, 72 | ] 73 | } 74 | 75 | export default config -------------------------------------------------------------------------------- /gatsby-node.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs" 2 | import path from "path" 3 | import { GatsbyNode } from "gatsby" 4 | import { locales } from "./lib/locales" 5 | import { rpcData } from "./src/hooks/useTenk" 6 | 7 | export const onPreInit: GatsbyNode["onPreInit"] = async () => { 8 | const data = await rpcData() 9 | fs.writeFileSync( 10 | path.resolve('stale-data-from-build-time.json'), 11 | JSON.stringify(data) 12 | ) 13 | } 14 | 15 | export const createPages: GatsbyNode["createPages"] = async ({ actions }) => { 16 | locales.forEach(locale => { 17 | actions.createPage({ 18 | path: locale.id, 19 | component: path.resolve("src/templates/[locale].tsx"), 20 | context: { locale }, 21 | }) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /lib/locales/Locale.ts: -------------------------------------------------------------------------------- 1 | // This file specifies the exact shape of data required in the locales files 2 | // located in `config/locales/*.json` 3 | // 4 | // We then use `create-validator-ts` to generate a JSON validator based on 5 | // these types, which lives in `./Locale.validator.ts` 6 | 7 | /** 8 | * Commonmark-formatted markdown. @see https://remarkjs.github.io/react-markdown/ 9 | */ 10 | type Markdown = string 11 | 12 | // TODO: should be able to import Action from ./runtimeUtils, but currently breaks with typescript-json-validator 13 | // import type { Action } from './runtimeUtils' 14 | type Action = "ADD_TO_CALENDAR(SALE_START)" | "ADD_TO_CALENDAR(PRESALE_START)" | "SIGN_IN" | "MINT" | "GO_TO_PARAS" 15 | 16 | export const requiredHeroFields = [ 17 | 'title', 18 | 'body', 19 | 'cta', 20 | 'action', 21 | 'remaining', 22 | ] as const 23 | 24 | export const optionalHeroFields = [ 25 | 'backgroundImage', 26 | 'backgroundColor', 27 | 'image', 28 | 'video', 29 | 'ps', 30 | 'setNumber', 31 | ] as const 32 | 33 | export type Hero = { 34 | [K in typeof requiredHeroFields[number]]: K extends 'action' ? Action : Markdown 35 | } & { 36 | [K in typeof optionalHeroFields[number]]?: string 37 | } 38 | 39 | export const userStatuses = ['signedOut', 'signedIn', 'vip'] as const 40 | 41 | type HeroSaleStatus = Partial & { 42 | [K in typeof userStatuses[number]]?: Partial 43 | } 44 | 45 | export const saleStatuses = ['saleClosed', 'presale', 'saleOpen', 'allSold'] as const 46 | 47 | export type RawHeroTree = Partial & { 48 | [K in typeof saleStatuses[number]]?: HeroSaleStatus 49 | } 50 | 51 | export interface SectionI18n { 52 | text: Markdown 53 | image?: string 54 | video?: string 55 | backgroundImage?: string 56 | backgroundColor?: string 57 | className?: string 58 | blocks?: { 59 | image?: string 60 | text?: string 61 | linkTo?: string 62 | }[] 63 | } 64 | 65 | export interface Locale { 66 | viewIn: string 67 | langPicker: string 68 | title: string 69 | description: string 70 | calendarEvent: string 71 | connectWallet: string 72 | signOut: string 73 | new: string 74 | myNFTs: string 75 | nextNFT: string 76 | prevNFT: string 77 | close: string 78 | zoomIn: string 79 | zoomOut: string 80 | hero: RawHeroTree 81 | extraSections?: SectionI18n[] 82 | } -------------------------------------------------------------------------------- /lib/locales/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs" 2 | import path from "path" 3 | import { execSync } from "child_process" 4 | import { Locale, requiredHeroFields, optionalHeroFields, saleStatuses, userStatuses } from "./Locale" 5 | import { placeholderStrings } from './runtimeUtils' 6 | import type { Hero, RawHeroTree } from "./Locale" 7 | 8 | // re-create `Locale.validator.ts` based of current contents of `Locale.ts` 9 | execSync(`yarn create-validator-ts --skipTypeCheck ${__dirname}/Locale.ts`) 10 | 11 | // now that we re-created the file we can import the latest version 12 | const validate: (x: unknown) => Locale = require('./Locale.validator').validateLocale 13 | 14 | class LocaleError extends Error {} 15 | 16 | interface HeroSaleState { 17 | signedOut: Hero 18 | signedIn: Hero 19 | vip: Hero 20 | } 21 | 22 | export interface ExpandedHeroTree { 23 | saleClosed: HeroSaleState 24 | presale: HeroSaleState 25 | saleOpen: HeroSaleState 26 | allSold: HeroSaleState 27 | } 28 | 29 | export interface DecoratedLocale extends Locale { 30 | id: string 31 | hero: ExpandedHeroTree 32 | } 33 | 34 | function computeField({ rawHeroTree, saleStatus, userStatus, required }: { 35 | rawHeroTree: RawHeroTree 36 | saleStatus: keyof ExpandedHeroTree 37 | userStatus: keyof HeroSaleState 38 | required: boolean 39 | }) { 40 | return (hero: Partial, field: keyof Hero) => { 41 | // @ts-expect-error Type 'string | undefined' is not assignable to type 'Action' 42 | hero[field] = 43 | rawHeroTree[saleStatus]?.[userStatus]?.[field] ?? 44 | rawHeroTree[saleStatus]?.[field] ?? 45 | rawHeroTree[field] 46 | if (required && typeof hero[field] === "undefined") { 47 | throw new LocaleError( 48 | `"hero" must include computable "${field}" in each branch; please include at least one of:\n` + 49 | ` • "hero.${field}"\n` + 50 | ` • "hero.${saleStatus}.${field}"\n` + 51 | ` • "hero.${saleStatus}.${userStatus}.${field}"\n` + 52 | `(if set in more than one of these, a more specific setting overrides a more general)` 53 | ) 54 | } 55 | // warn if it looks like there might be an unknown placeholder string 56 | // ('action' field values are validated as part of the schema, so we can skip them here) 57 | const allCapsSubStrings = field !== 'action' && hero[field]?.matchAll(/\b[A-Z_]+\b/g) 58 | Array.from(allCapsSubStrings || []).forEach(([possiblePlaceholder]) => { 59 | // TODO: update above regex to only match strings with underscores in them to avoid `.match('_')` 60 | if (possiblePlaceholder.match('_') && !placeholderStrings.includes(possiblePlaceholder)) { 61 | console.warn( 62 | `"hero" field "${field}" contains what looks like a placeholder string "${possiblePlaceholder}", ` + 63 | `but no substitution is available for this string. Did you mean to include one of the following?\n\n` + 64 | ` • ${placeholderStrings.join('\n • ')}\n\n` + 65 | `The full text given for this field was:\n\n ${hero[field]}\n\n` 66 | ) 67 | } 68 | }) 69 | return hero 70 | } 71 | } 72 | 73 | function hoistHeroFields(rawHeroTree: RawHeroTree): ExpandedHeroTree { 74 | return saleStatuses.reduce((a, saleStatus) => ({ ...a, 75 | [saleStatus]: userStatuses.reduce((b, userStatus) => ({ ...b, 76 | [userStatus]: { 77 | ...requiredHeroFields.reduce(computeField({ rawHeroTree, saleStatus, userStatus, required: true }), {}), 78 | ...optionalHeroFields.reduce(computeField({ rawHeroTree, saleStatus, userStatus, required: false }), {}), 79 | } as Hero 80 | }), {} as HeroSaleState) 81 | }), {} as ExpandedHeroTree) 82 | } 83 | 84 | // for use with `sort` 85 | function alphabeticOrder({ id: a }: DecoratedLocale, { id: b }: DecoratedLocale): -1 | 0 | 1 { 86 | if (a < b) { 87 | return -1 88 | } else if (a > b) { 89 | return 1 90 | } else { 91 | return 0 92 | } 93 | } 94 | 95 | const localesDirectory = path.resolve(process.cwd(), "config/i18n") 96 | 97 | let fileNames: string[] 98 | try { 99 | fileNames = fs.readdirSync(localesDirectory) 100 | } catch { 101 | fileNames = [] 102 | } 103 | 104 | const IS_JSON = /.json$/ 105 | 106 | export const locales: DecoratedLocale[] = fileNames.filter(f => IS_JSON.test(f)) 107 | .map(fileName => { 108 | // Remove ".json" from file name to get id 109 | // TODO: validate that `id` is valid according to https://www.npmjs.com/package/iso-639-1 110 | const id = fileName.replace(/\.json$/, "") 111 | const fullPath = path.join(localesDirectory, fileName) 112 | const fileContents: unknown = JSON.parse(fs.readFileSync(fullPath, "utf8")) 113 | const i18n = validate(fileContents) 114 | 115 | let hero: ExpandedHeroTree 116 | try { 117 | hero = hoistHeroFields(i18n.hero) 118 | } catch (e: unknown) { 119 | if (e instanceof LocaleError) { 120 | throw new Error(`Error parsing ${fileName}:\n\n${e.message}`) 121 | } 122 | throw e 123 | } 124 | 125 | // Combine the data with the id 126 | return { 127 | id, 128 | ...i18n, 129 | hero, 130 | } 131 | }) 132 | .sort(alphabeticOrder) 133 | -------------------------------------------------------------------------------- /lib/locales/runtimeUtils.ts: -------------------------------------------------------------------------------- 1 | import { Gas, NEAR } from 'near-units' 2 | import { atcb_action as addToCalendar } from 'add-to-calendar-button' 3 | import settings from '../../config/settings.json' 4 | import { signIn } from '../../src/near' 5 | import { TENK } from '../../src/near/contracts' 6 | import { TenkData } from "../../src/hooks/useTenk" 7 | import { saleStatuses, userStatuses } from './Locale' 8 | import { Locale } from '../../src/hooks/useLocales' 9 | 10 | type Timestamp = number 11 | 12 | type Data = TenkData & { 13 | currentUser: string 14 | locale: Locale 15 | numberToMint?: number 16 | saleStatus: typeof saleStatuses[number] 17 | userStatus: typeof userStatuses[number] 18 | } 19 | 20 | function formatNumber( 21 | num: number | string, 22 | 23 | /** 24 | * `undefined` will default to browser's locale (may not work correctly in Node during build) 25 | */ 26 | locale?: string, 27 | ) { 28 | 29 | return new Intl.NumberFormat(locale, { 30 | maximumSignificantDigits: 3, 31 | }).format(Number(num)) 32 | } 33 | 34 | function formatCurrency( 35 | num: number | string, 36 | currency: string = 'NEAR', 37 | 38 | /** 39 | * `undefined` will default to browser's locale (may not work correctly in Node during build) 40 | */ 41 | locale?: string, 42 | ) { 43 | return `${formatNumber(num, locale)} ${currency}` 44 | } 45 | 46 | function formatDate( 47 | d: Timestamp | Date, 48 | 49 | /** 50 | * `undefined` will default to browser's locale (may not work correctly in Node during build) 51 | */ 52 | locale?: string, 53 | options: Intl.DateTimeFormatOptions = {} 54 | ): string { 55 | const date = typeof d === "number" ? new Date(d) : d 56 | 57 | return new Intl.DateTimeFormat(locale, { 58 | dateStyle: 'short', 59 | timeStyle: 'short', 60 | ...options, 61 | }).format(date) 62 | } 63 | 64 | const replacers = { 65 | CURRENT_USER: (d: Data) => d.currentUser, 66 | PRESALE_START: (d: Data) => formatDate(d.saleInfo.presale_start), 67 | SALE_START: (d: Data) => formatDate(d.saleInfo.sale_start), 68 | MINT_LIMIT: (d: Data) => d.remainingAllowance ?? 0, 69 | MINT_PRICE: (d: Data) => formatCurrency( 70 | NEAR.from(d.saleInfo.price).mul(NEAR.from('' + (d.numberToMint ?? 1))).toHuman().split(' ')[0] 71 | ), 72 | MINT_RATE_LIMIT: (d: Data) => d.mintRateLimit, 73 | INITIAL_COUNT: (d: Data) => formatNumber(d.saleInfo.token_final_supply), 74 | REMAINING_COUNT: (d: Data) => formatNumber(d.tokensLeft), 75 | } as const 76 | 77 | export const placeholderStrings = Object.keys(replacers) 78 | 79 | export type PlaceholderString = keyof typeof replacers 80 | 81 | const placeholderRegex = new RegExp(`(${placeholderStrings.join('|')})`, 'gm') 82 | 83 | export function fill(text: string, data: Data): string { 84 | return text.replace(placeholderRegex, (match) => { 85 | return String(replacers[match as PlaceholderString](data)) 86 | }) 87 | } 88 | 89 | // add-to-calendar-button has strange strict requirements on time format 90 | function formatDatesForAtcb(d: Timestamp) { 91 | let [start, end] = new Date(d).toISOString().split('T') 92 | return [ 93 | start, 94 | end.replace(/:\d\d\..*$/, '') // strip seconds, ms, & TZ 95 | ] 96 | } 97 | 98 | // add-to-calendar-button doesn't allow passing simple ISO strings for start/end 99 | function getStartAndEnd(d: Timestamp) { 100 | const [startDate, startTime] = formatDatesForAtcb(d) 101 | const [endDate, endTime] = formatDatesForAtcb(d + 3600000) 102 | return { startDate, startTime, endDate, endTime } 103 | } 104 | 105 | const actions = { 106 | 'ADD_TO_CALENDAR(SALE_START)': (d: Data) => addToCalendar({ 107 | name: d.locale.calendarEvent!, 108 | ...getStartAndEnd(d.saleInfo.sale_start), 109 | options: ['Google', 'iCal', 'Apple', 'Microsoft365', 'MicrosoftTeams', 'Outlook.com', 'Yahoo'], 110 | timeZone: "UTC", 111 | trigger: 'click', 112 | }), 113 | 'ADD_TO_CALENDAR(PRESALE_START)': (d: Data) => addToCalendar({ 114 | name: d.locale.calendarEvent!, 115 | ...getStartAndEnd(d.saleInfo.presale_start), 116 | options: ['Google', 'iCal', 'Apple', 'Microsoft365', 'MicrosoftTeams', 'Outlook.com', 'Yahoo'], 117 | timeZone: "UTC", 118 | trigger: 'click', 119 | }), 120 | 'SIGN_IN': signIn, 121 | 'MINT': (d: Data) => TENK.nft_mint_many({ num: d.numberToMint ?? 1 }, { 122 | gas: Gas.parse('40 Tgas').mul(Gas.from('' + d.numberToMint)), 123 | attachedDeposit: NEAR.from(d.saleInfo.price).mul(NEAR.from('' + d.numberToMint)), 124 | }), 125 | 'GO_TO_PARAS': () => window.open(`https://paras.id/search?q=${settings.contractName}&sort=priceasc&pmin=.01&is_verified=true`), 126 | } 127 | 128 | export type Action = keyof typeof actions 129 | 130 | export function act(action: Action, data: Data): void { 131 | actions[action](data) 132 | } 133 | 134 | export function can(action: Action, data: Data): boolean { 135 | if (action === 'MINT') { 136 | return Boolean(data.currentUser) && ( 137 | (data.saleStatus === 'presale' && 138 | data.remainingAllowance !== undefined && 139 | data.remainingAllowance > 0 140 | ) || 141 | (data.saleStatus === 'saleOpen' && ( 142 | // users are added to the whitelist as they mint during saleOpen; 143 | // undefined means they haven't minted yet 144 | data.remainingAllowance === undefined || 145 | data.remainingAllowance > 0 146 | )) 147 | ) 148 | } 149 | return true 150 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend-starter", 3 | "private": true, 4 | "description": "Template for NFT Launches on TenK", 5 | "version": "0.1.0", 6 | "author": "Chad Ostrowski ", 7 | "dependencies": { 8 | "@radix-ui/react-dropdown-menu": "0.1.6", 9 | "@radix-ui/react-slider": "^0.1.4", 10 | "add-to-calendar-button": "1.7.8", 11 | "bn.js": "^5.2.0", 12 | "gatsby-background-image": "^1.6.0", 13 | "gatsby-transformer-video": "^0.6.0", 14 | "gbimage-bridge": "^0.2.1", 15 | "near-api-js": "AhaLabs/near-api-js#1.0.1", 16 | "near-units": "^0.1.9", 17 | "react": "17.0.2", 18 | "react-dom": "17.0.2", 19 | "react-helmet": "^6.1.0", 20 | "react-image-lightbox": "^5.1.4", 21 | "react-markdown": "^8.0.0" 22 | }, 23 | "devDependencies": { 24 | "@types/bn.js": "^5.1.0", 25 | "@types/react": "17", 26 | "@types/react-dom": "17", 27 | "@types/react-helmet": "^6.1.5", 28 | "create-validator-ts": "^3.0.1", 29 | "gatsby": "^4.12.1", 30 | "gatsby-plugin-dts-css-modules": "^2.2.0", 31 | "gatsby-plugin-extract-schema": "^0.2.1", 32 | "gatsby-plugin-gatsby-cloud": "^4.5.2", 33 | "gatsby-plugin-graphql-codegen": "^3.1.1", 34 | "gatsby-plugin-image": "^2.5.2", 35 | "gatsby-plugin-manifest": "^4.5.2", 36 | "gatsby-plugin-offline": "^5.5.2", 37 | "gatsby-plugin-portal": "^1.0.7", 38 | "gatsby-plugin-react-helmet": "^5.5.0", 39 | "gatsby-plugin-sass": "^5.7.0", 40 | "gatsby-plugin-sharp": "^4.5.2", 41 | "gatsby-source-filesystem": "^4.5.2", 42 | "gatsby-transformer-inline-svg": "^1.1.0", 43 | "gatsby-transformer-json": "^4.5.0", 44 | "gatsby-transformer-sharp": "^4.5.0", 45 | "node-sass": "6", 46 | "prettier": "^2.5.1", 47 | "typescript": "^4.5.4" 48 | }, 49 | "keywords": [ 50 | "gatsby" 51 | ], 52 | "license": "LGPL-3.0", 53 | "scripts": { 54 | "build": "gatsby build", 55 | "develop": "gatsby develop", 56 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"", 57 | "start": "gatsby develop", 58 | "serve": "gatsby serve", 59 | "clean": "gatsby clean", 60 | "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1" 61 | }, 62 | "repository": { 63 | "type": "git", 64 | "url": "https://github.com/gatsbyjs/gatsby-starter-default" 65 | }, 66 | "bugs": { 67 | "url": "https://github.com/gatsbyjs/gatsby/issues" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/components/background-image.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { convertToBgImage } from "gbimage-bridge" 3 | import GatsbyBackgroundImage, { IBackgroundImageProps } from 'gatsby-background-image' 4 | import useImageData from "../hooks/useImageData" 5 | 6 | type Props = IBackgroundImageProps & JSX.IntrinsicElements['div'] & { 7 | src?: string 8 | /** 9 | * Could add more; gatsby-background-image allows any of JSX.IntrinsicElements, but that doesn't type-check well 10 | */ 11 | Tag: 'section' | 'div' | 'header' | 'footer' | 'aside' 12 | } 13 | 14 | /** 15 | * Same as `Props`, but `src` must be present 16 | */ 17 | type StrictProps = Omit & { 18 | src: string 19 | } 20 | 21 | /** 22 | * Simple background images for Gatsby! Pass in the name of a file in 23 | * `config/images`, and this will render it correctly. 24 | * 25 | * Uses gatsby-transformer-inline-svg plugin to render SVG backgrounds. 26 | * Uses gatsby-background-image plugin to render all other backgrounds. 27 | * 28 | * @param Tag HTML Element to use. @default "div" 29 | * @param src filename of an image in `config/images`. If undefined, `Tag` will be rendered with all children and other props directly, with no background-image styling. 30 | */ 31 | const BackgroundImage: React.FC = ({ src, Tag = 'div', ...props }) => { 32 | if (!src) return 33 | 34 | return 35 | } 36 | 37 | export default BackgroundImage 38 | 39 | const StrictBackgroundImage: React.FC = ({ src, Tag, style, ...props }) => { 40 | const { svg, image } = useImageData(src) 41 | 42 | if (svg) { 43 | return ( 44 | 54 | ) 55 | } 56 | 57 | const bgImage = convertToBgImage(image.childImageSharp?.gatsbyImageData) 58 | 59 | return ( 60 | // @ts-expect-error type woes; gatsby-background-image does not make it easy to work with their types 61 | 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /src/components/banner/banner.module.css: -------------------------------------------------------------------------------- 1 | .banner { 2 | align-items: center; 3 | background-color: var(--accent); 4 | text-align: center; 5 | padding: var(--spacing-m); 6 | position: relative; 7 | } 8 | 9 | .banner a, 10 | .banner a:after { 11 | border-style: solid; 12 | border-color: transparent; 13 | position: absolute; 14 | top: 50%; 15 | transform: translateY(-50%); 16 | } 17 | .banner a { 18 | border-width: var(--spacing-m) var(--spacing-s); 19 | } 20 | .banner a:after { 21 | content: " "; 22 | border-width: var(--spacing-s) var(--spacing-xs); 23 | } 24 | 25 | .banner a:first-child, 26 | .banner a:first-child:after { 27 | border-left-width: 0; 28 | } 29 | .banner a:first-child { 30 | border-right-color: var(--fg); 31 | left: var(--spacing-m); 32 | top: 50%; 33 | } 34 | .banner a:first-child:after { 35 | border-right-color: var(--accent); 36 | left: var(--spacing-xs); 37 | } 38 | 39 | .banner a:last-child, 40 | .banner a:last-child:after { 41 | border-right-width: 0; 42 | } 43 | .banner a:last-child { 44 | border-left-color: var(--fg); 45 | right: var(--spacing-m); 46 | top: 50%; 47 | } 48 | .banner a:last-child:after { 49 | border-left-color: var(--accent); 50 | right: var(--spacing-xs); 51 | } -------------------------------------------------------------------------------- /src/components/banner/banner.module.css.d.ts: -------------------------------------------------------------------------------- 1 | // This file is automatically generated. Do not modify this file manually -- YOUR CHANGES WILL BE ERASED! 2 | export const banner: string; 3 | -------------------------------------------------------------------------------- /src/components/banner/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Link } from "gatsby" 3 | import useHeroStatuses from '../../hooks/useHeroStatuses' 4 | import * as css from './banner.module.css' 5 | 6 | export default function () { 7 | const { saleStatus, userStatus, heroParam, overrides } = useHeroStatuses() 8 | if (heroParam === undefined) return null 9 | 10 | const prevOverride = overrides[heroParam - 1] 11 | const nextOverride = overrides[heroParam + 1] 12 | 13 | return ( 14 | 37 | ) 38 | } -------------------------------------------------------------------------------- /src/components/dropdown/dropdown.module.css: -------------------------------------------------------------------------------- 1 | .trigger { 2 | background-image: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAwMCAxMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICA8cGF0aCBmaWxsPSJncmF5IiBkPSJNMTAsMjMyLjdsNDkwLDUzNC41bDQ5MC01MzQuNUgxMHoiIC8+DQo8L3N2Zz4=); 3 | background-repeat: no-repeat; 4 | background-position: bottom 50% right var(--spacing-s); 5 | background-size: var(--spacing-s); 6 | padding-right: var(--spacing-l); 7 | } 8 | 9 | .item { 10 | color: inherit; 11 | cursor: pointer; 12 | font-weight: var(--fw-regular); 13 | padding: 0 var(--spacing-s); 14 | } 15 | 16 | .item:focus, 17 | .item:hover { 18 | box-shadow: none; 19 | outline: none; 20 | } 21 | .item:focus, 22 | .item:hover { 23 | background-color: var(--gray-1); 24 | } 25 | 26 | .trigger:active, 27 | .item:active { 28 | top: 0; 29 | border-color: var(--gray-4); 30 | } 31 | 32 | .item { 33 | border: none; 34 | } 35 | 36 | .arrow { 37 | fill: var(--gray-4); 38 | } 39 | 40 | .content { 41 | background-color: var(--bg); 42 | border-radius: var(--br-base); 43 | border: 1px solid var(--gray-4); 44 | box-shadow: rgba(22, 23, 24, 0.35) 0px 10px 38px -10px, rgba(22, 23, 24, 0.2) 0px 10px 20px -15px; 45 | padding: var(--spacing-xs); 46 | } -------------------------------------------------------------------------------- /src/components/dropdown/dropdown.module.css.d.ts: -------------------------------------------------------------------------------- 1 | // This file is automatically generated. Do not modify this file manually -- YOUR CHANGES WILL BE ERASED! 2 | export const trigger: string; 3 | export const item: string; 4 | export const arrow: string; 5 | export const content: string; 6 | -------------------------------------------------------------------------------- /src/components/dropdown/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import * as DropdownMenu from "@radix-ui/react-dropdown-menu" 3 | import * as css from "./dropdown.module.css" 4 | 5 | const Dropdown: React.FC<{ 6 | trigger: React.ReactChild 7 | items: DropdownMenu.MenuItemProps[] 8 | }> = ({ trigger, items }) => { 9 | return ( 10 | 11 | 12 | {trigger} 13 | 14 | 15 | 16 | {items.map(({ className, ...props }, i) => ( 17 | 22 | ))} 23 | 24 | 25 | 26 | ) 27 | } 28 | 29 | export default Dropdown 30 | -------------------------------------------------------------------------------- /src/components/footer/footer.module.css: -------------------------------------------------------------------------------- 1 | /* This is a CSS Module; see https://create-react-app.dev/docs/adding-a-css-modules-stylesheet/ */ 2 | .footer { 3 | align-items: center; 4 | display: flex; 5 | flex-direction: row-reverse; 6 | flex-wrap: wrap; 7 | gap: var(--spacing-m); 8 | justify-content: space-between; 9 | padding-bottom: var(--spacing-l); 10 | width: 100%; 11 | } 12 | 13 | .launchPartner { 14 | align-items: center; 15 | display: flex; 16 | gap: var(--spacing-m); 17 | } 18 | 19 | /* When launchPartner is first child, there is only one language to choose from */ 20 | .launchPartner:first-child { 21 | width: 100%; 22 | justify-content: center; 23 | } -------------------------------------------------------------------------------- /src/components/footer/footer.module.css.d.ts: -------------------------------------------------------------------------------- 1 | // This file is automatically generated. Do not modify this file manually -- YOUR CHANGES WILL BE ERASED! 2 | export const footer: string; 3 | export const launchPartner: string; 4 | -------------------------------------------------------------------------------- /src/components/footer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { StaticImage } from "gatsby-plugin-image" 3 | import * as css from "./footer.module.css" 4 | import LangPicker from "../lang-picker" 5 | import useLocales from "../../hooks/useLocales" 6 | 7 | export default function Footer() { 8 | const { locale } = useLocales() 9 | if (!locale) return null 10 | return ( 11 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/components/hero/hero.module.css: -------------------------------------------------------------------------------- 1 | .content { 2 | display: flex; 3 | flex-direction: column; 4 | gap: var(--spacing-m); 5 | } 6 | 7 | .content p:last-child { 8 | margin-bottom: 0; 9 | } 10 | .content h1 { 11 | font-weight: var(--fw-black); 12 | font-size: 4rem; 13 | line-height: 0.9; 14 | margin-top: var(--spacing-l); 15 | } 16 | .content h2 { 17 | color: var(--accent); 18 | margin-top: var(--spacing-m); 19 | margin-bottom: calc(-1 * var(--spacing-m)); 20 | } 21 | 22 | .lead, 23 | .remaining { 24 | color: var(--gray-6); 25 | } 26 | 27 | @media (min-width: 500px) { 28 | .content { 29 | flex-direction: column-reverse; 30 | } 31 | .content h1 { 32 | margin-top: 0; 33 | } 34 | } 35 | 36 | .cta { 37 | margin-top: var(--spacing-s); 38 | width: 100%; 39 | } 40 | 41 | .labelWrap { 42 | align-items: center; 43 | display: flex; 44 | flex-wrap: wrap; 45 | gap: var(--spacing-xs) var(--spacing-m); 46 | justify-content: space-between; 47 | } 48 | 49 | .label { 50 | display: block; 51 | flex: 1 1 auto; 52 | font-weight: var(--fw-bold); 53 | } -------------------------------------------------------------------------------- /src/components/hero/hero.module.css.d.ts: -------------------------------------------------------------------------------- 1 | // This file is automatically generated. Do not modify this file manually -- YOUR CHANGES WILL BE ERASED! 2 | export const content: string; 3 | export const lead: string; 4 | export const remaining: string; 5 | export const cta: string; 6 | export const labelWrap: string; 7 | export const label: string; 8 | -------------------------------------------------------------------------------- /src/components/hero/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { ExpandedHeroTree } from '../../../lib/locales' 3 | import { act, can, fill } from '../../../lib/locales/runtimeUtils' 4 | import { wallet } from "../../near" 5 | import Slider from '../slider' 6 | import Section from '../section' 7 | import Markdown from "../markdown" 8 | import useHeroStatuses from '../../hooks/useHeroStatuses' 9 | import useTenk from '../../hooks/useTenk' 10 | import useLocales from '../../hooks/useLocales' 11 | import * as css from './hero.module.css' 12 | 13 | const currentUser = wallet.getAccountId() 14 | 15 | const Hero: React.FC<{ heroTree: ExpandedHeroTree }> = ({ heroTree }) => { 16 | const { locale } = useLocales() 17 | const tenkData = useTenk() 18 | const { saleStatus, userStatus } = useHeroStatuses() 19 | const [numberToMint, setNumberToMint] = React.useState(1) 20 | const hero = heroTree[saleStatus][userStatus] 21 | 22 | if (!locale) return null 23 | 24 | const data = { 25 | ...tenkData, 26 | currentUser, 27 | locale, 28 | saleStatus, 29 | userStatus, 30 | } 31 | 32 | return ( 33 |
47 |
48 | {can(hero.action, data) && ( 49 |
{ 50 | e.preventDefault() 51 | act(hero.action, { ...data, numberToMint }) 52 | }}> 53 | {hero.setNumber && ( 54 | <> 55 |
56 | 59 |
60 | {fill(hero.remaining, data)} 61 |
62 |
63 | {tenkData.mintRateLimit > 1 && ( 64 | setNumberToMint(v)} 72 | value={[numberToMint]} 73 | /> 74 | )} 75 | 76 | )} 77 | 80 | 81 | )} 82 |
83 |

{locale.title}

84 | 85 |
86 | 87 |
88 | {hero.ps && } 89 |
90 |
91 |
92 | ) 93 | } 94 | 95 | export default Hero -------------------------------------------------------------------------------- /src/components/image.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { GatsbyImage } from "gatsby-plugin-image" 3 | import type { GatsbyImageProps } from "gatsby-plugin-image" 4 | import useImageData from "../hooks/useImageData" 5 | 6 | export type ImageProps = Omit & { 7 | src: string 8 | } 9 | 10 | /** 11 | * Simple image handling for Gatsby! Pass in the name of a file in 12 | * `config/images`, and this will render it correctly. It will render it either 13 | * as an SVG using the gatsby-transformer-inline-svg plugin, or using 14 | * GatsbyImage. 15 | */ 16 | export default function ({ src, ...props }: ImageProps) { 17 | const { svg, image } = useImageData(src) 18 | 19 | if (svg) { 20 | if (svg.svg?.content) { 21 | // Inlined SVGs 22 | return
23 | } 24 | // SVGs that can/should not be inlined 25 | return ( 26 |
27 | {props.alt} 28 |
29 | ) 30 | } 31 | 32 | const imageData = image.childImageSharp?.gatsbyImageData 33 | 34 | if (imageData) { 35 | return ( 36 | 37 | ) 38 | } 39 | 40 | return ( 41 |
42 | 43 |
44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /src/components/lang-picker/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { navigate } from "gatsby" 3 | import useLocales from "../../hooks/useLocales" 4 | import * as css from "./lang-picker.module.css" 5 | 6 | export default function LangPicker() { 7 | const { locales, locale } = useLocales() 8 | if (!locale || locales.length <= 1) return null 9 | return ( 10 |
11 |
12 | 13 | 14 | 15 | 16 | {locales.map(l => l.viewIn).join(" | ")} 17 | 18 |
19 | 30 |
31 | ) 32 | } -------------------------------------------------------------------------------- /src/components/lang-picker/lang-picker.module.css: -------------------------------------------------------------------------------- 1 | .wrap { 2 | position: relative; 3 | display: inline-block; 4 | } 5 | 6 | .globe { 7 | position: absolute; 8 | left: var(--spacing-s); 9 | top: 50%; 10 | transform: translateY(-50%); 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: space-around; 14 | } 15 | 16 | .globe svg { 17 | height: var(--spacing-m); 18 | width: var(--spacing-m); 19 | } 20 | 21 | .select { 22 | border: 1px solid transparent; 23 | padding-left: calc(4px + var(--spacing-l)); 24 | text-align: center; 25 | } 26 | .select:hover, 27 | .select:focus { 28 | border-color: var(--gray-3); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/lang-picker/lang-picker.module.css.d.ts: -------------------------------------------------------------------------------- 1 | // This file is automatically generated. Do not modify this file manually -- YOUR CHANGES WILL BE ERASED! 2 | export const wrap: string; 3 | export const globe: string; 4 | export const select: string; 5 | -------------------------------------------------------------------------------- /src/components/layout/a11y.css: -------------------------------------------------------------------------------- 1 | /* hide from screen but leave visible to screen readers and findable with cmd/ctrl-F searches */ 2 | .visuallyHidden { 3 | border: 0; 4 | clip: rect(0 0 0 0); 5 | height: auto; 6 | margin: 0; 7 | overflow: hidden; 8 | padding: 0; 9 | position: absolute; 10 | width: 1px; 11 | white-space: nowrap; 12 | } -------------------------------------------------------------------------------- /src/components/layout/atcb.css: -------------------------------------------------------------------------------- 1 | /** 2 | * styles needed for add-to-calendar-button plugin 3 | */ 4 | .atcb_icon { 5 | height: 16px; 6 | display: inline-flex; 7 | margin-bottom: 4px; 8 | margin-right: 10px; 9 | vertical-align: middle; 10 | } 11 | .atcb_icon svg { 12 | height: 100%; 13 | color: rgb(51, 51, 51); 14 | width: auto; 15 | } 16 | 17 | .atcb_list { 18 | box-sizing: border-box; 19 | color: rgb(51, 51, 51); 20 | display: block; 21 | max-width: 100%; 22 | position: absolute; 23 | padding: 0 3px; 24 | -webkit-user-select: none; 25 | -moz-user-select: none; 26 | -ms-user-select: none; 27 | user-select: none; 28 | width: 100%; 29 | min-width: 12em; 30 | z-index: 80; 31 | } 32 | 33 | .atcb_list.atcb_modal { 34 | position: fixed; 35 | width: 12em; 36 | left: 50%; 37 | top: 50%; 38 | transform: translateY(-50%) translateX(-50%); 39 | } 40 | 41 | .atcb_list_item { 42 | background: rgb(250, 250, 250); 43 | border: 1px solid rgb(210, 210, 210); 44 | border-top: 0; 45 | -webkit-box-shadow: 1px 2px 8px 0px rgba(0,0,0,.3); 46 | box-shadow: 1px 2px 8px 0px rgba(0,0,0,.3); 47 | box-sizing: border-box; 48 | cursor: pointer; 49 | font-size: 16px; 50 | left: 50%; 51 | position: relative; 52 | padding: 12px 18px; 53 | text-align: left; 54 | transform: translate(-50%); 55 | touch-action: manipulation; 56 | -webkit-tap-highlight-color: transparent; 57 | } 58 | .atcb_list_item:focus, 59 | .atcb_list_item:hover { 60 | background: rgb(255, 255, 255); 61 | -webkit-box-shadow: 1px 2px 10px 0px rgba(0,0,0,.4); 62 | box-shadow: 1px 2px 10px 0px rgba(0,0,0,.4); 63 | color: rgb(0, 0, 0); 64 | } 65 | @media only screen and (max-width: 575px) { 66 | .atcb_list_item { 67 | font-size: 14px; 68 | } 69 | } 70 | 71 | .atcb_list:not(.atcb_modal) .atcb_list_item:first-child { 72 | padding-top: 25px; 73 | } 74 | 75 | .atcb_list.atcb_modal .atcb_list_item:first-child { 76 | border-radius: 6px 6px 0 0; 77 | } 78 | 79 | .atcb_list_item:last-child { 80 | border-radius: 0 0 6px 6px; 81 | } 82 | 83 | .atcb_list_item .atcb_icon { 84 | margin-right: 8px; 85 | } 86 | 87 | .atcb_bgoverlay { 88 | background: rgba(20,20,20,.2); 89 | backdrop-filter: blur(2px); 90 | height: 100%; 91 | left: 0; 92 | position: fixed; 93 | top: 0; 94 | width: 100%; 95 | z-index: 70; 96 | } 97 | 98 | -------------------------------------------------------------------------------- /src/components/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import "./layout.scss" 3 | import Banner from "../banner" 4 | import Nav from "../nav" 5 | import Footer from "../footer" 6 | import * as css from "./layout.module.css" 7 | 8 | const Layout: React.FC<{ style?: React.CSSProperties }> = ({ style, children }) => { 9 | return ( 10 |
11 |
12 | 13 |
16 |
17 |
18 | ) 19 | } 20 | 21 | export default Layout 22 | -------------------------------------------------------------------------------- /src/components/layout/layout.module.css: -------------------------------------------------------------------------------- 1 | .wrap { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | min-height: 100vh; 6 | min-height: -webkit-fill-available; 7 | } -------------------------------------------------------------------------------- /src/components/layout/layout.module.css.d.ts: -------------------------------------------------------------------------------- 1 | // This file is automatically generated. Do not modify this file manually -- YOUR CHANGES WILL BE ERASED! 2 | export const wrap: string; 3 | -------------------------------------------------------------------------------- /src/components/layout/layout.scss: -------------------------------------------------------------------------------- 1 | @import "./a11y.css"; 2 | @import "./atcb.css"; 3 | @import "../../../config/styles.scss"; 4 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;900&display=swap'); 5 | 6 | $white: white !default; 7 | $black: black !default; 8 | $accent: #e73b93 !default; 9 | $accent: adjust-color($accent, $hue: 90deg) !default; 10 | 11 | :root { 12 | /* Typographic Scale */ 13 | --base-font-size: 14px; 14 | @media (min-width: 415px) { 15 | --base-font-size: 16px; 16 | } 17 | @media (min-width: 600px) { 18 | --base-font-size: 18px; 19 | } 20 | 21 | /* Font Family */ 22 | --sans: Montserrat, sans-serif; 23 | --monospace: "Source Code Pro", monospace; 24 | 25 | /* Font Weights */ 26 | --fw-regular: 400; 27 | --fw-bold: 600; 28 | --fw-black: 900; 29 | 30 | /* Line Heights */ 31 | --lh-headings: 1.1; 32 | --lh-copy: 1.5; 33 | 34 | /* Spacing Scale */ 35 | --base-spacing: 0.5rem; 36 | --spacing-xs: calc(var(--base-spacing) / 2); 37 | --spacing-s: var(--base-spacing); 38 | --spacing-m: calc(var(--base-spacing) * 2); 39 | --spacing-l: calc(var(--base-spacing) * 3); 40 | --spacing-xl: calc(var(--base-spacing) * 4); 41 | --spacing-xxl: calc(var(--base-spacing) * 5); 42 | 43 | /* Sizes */ 44 | --header-height: 80px; 45 | --input-height: 48px; 46 | --input-height--small: 40px; 47 | 48 | /* Border Radius */ 49 | --br-base: 4px; 50 | 51 | /* Colors */ 52 | /* TODO: move these base colors to settings.json or similar */ 53 | --white: #{$white}; 54 | --black: #{$black}; 55 | --accent: #{$accent}; 56 | --secondary: #{$secondary}; 57 | 58 | --fg: var(--black); 59 | --bg: var(--white); 60 | 61 | --gray-1: #{adjust-color($black, $alpha: -0.9)}; 62 | --gray-2: #{adjust-color($black, $alpha: -0.8)}; 63 | --gray-3: #{adjust-color($black, $alpha: -0.7)}; 64 | --gray-4: #{adjust-color($black, $alpha: -0.6)}; 65 | --gray-5: #{adjust-color($black, $alpha: -0.5)}; 66 | --gray-6: #{adjust-color($black, $alpha: -0.4)}; 67 | --gray-7: #{adjust-color($black, $alpha: -0.3)}; 68 | --gray-8: #{adjust-color($black, $alpha: -0.2)}; 69 | --gray-9: #{adjust-color($black, $alpha: -0.1)}; 70 | 71 | --red: #fc5b5b; 72 | @media (prefers-color-scheme: dark) { 73 | --bg: var(--black); 74 | --fg: var(--white); 75 | 76 | --gray-1: #{adjust-color($white, $alpha: -0.9)}; 77 | --gray-2: #{adjust-color($white, $alpha: -0.8)}; 78 | --gray-3: #{adjust-color($white, $alpha: -0.7)}; 79 | --gray-4: #{adjust-color($white, $alpha: -0.6)}; 80 | --gray-5: #{adjust-color($white, $alpha: -0.5)}; 81 | --gray-6: #{adjust-color($white, $alpha: -0.4)}; 82 | --gray-7: #{adjust-color($white, $alpha: -0.3)}; 83 | --gray-8: #{adjust-color($white, $alpha: -0.2)}; 84 | --gray-9: #{adjust-color($white, $alpha: -0.1)}; 85 | 86 | --red: #ff8588; 87 | } 88 | } 89 | 90 | @function light($color) { 91 | @return adjust-color($color, $saturation: 53%, $lightness: 15%); 92 | } 93 | 94 | @function soft($color) { 95 | @return adjust-color($color, $alpha: -0.85); 96 | } 97 | 98 | * { 99 | box-sizing: border-box; 100 | } 101 | 102 | html, 103 | body { 104 | height: 100%; 105 | } 106 | 107 | html { 108 | background-color: var(--bg); 109 | color: var(--fg); 110 | line-height: var(--lh-copy); 111 | font-family: var(--sans); 112 | font-size: var(--base-font-size); 113 | font-weight: var(--fw-regular); 114 | } 115 | 116 | body { 117 | margin: 0 auto; 118 | } 119 | 120 | h1, 121 | h2, 122 | h3, 123 | h4, 124 | h5, 125 | h6 { 126 | font-weight: var(--fw-bold); 127 | margin: 0; 128 | line-height: var(--lh-headings); 129 | } 130 | 131 | img, 132 | picture, 133 | video { 134 | display: block; 135 | max-width: 100%; 136 | } 137 | 138 | a, 139 | .link { 140 | color: var(--accent); 141 | padding: 0; 142 | text-decoration: none; 143 | position: relative; 144 | } 145 | a:hover, 146 | a:focus, 147 | .link:hover, 148 | .link:focus { 149 | text-decoration: underline; 150 | } 151 | a:active, 152 | .link:active { 153 | top: 1px; 154 | } 155 | 156 | strong { 157 | font-weight: var(--fw-bold); 158 | } 159 | 160 | em { 161 | color: var(--accent); 162 | font-style: inherit; 163 | font-weight: inherit; 164 | } 165 | 166 | label { 167 | display: block; 168 | } 169 | 170 | code { 171 | color: var(--fg); 172 | padding: var(--spacing-xs) var(--spacing-s); 173 | margin: 0; 174 | font-size: 85%; 175 | font-family: var(--monospace); 176 | background-color: var(--gray-2); 177 | border-radius: 6px; 178 | } 179 | 180 | button, 181 | .button, 182 | input, 183 | select { 184 | font: inherit; 185 | outline: none; 186 | } 187 | 188 | button, 189 | .button { 190 | background: linear-gradient(to right, var(--secondary), var(--accent) 66%); 191 | border-radius: var(--br-base); 192 | border: none; 193 | color: var(--bg); 194 | cursor: pointer; 195 | display: inline-block; 196 | font-size: inherit; 197 | font-weight: var(--fw-regular); 198 | position: relative; 199 | } 200 | 201 | input, 202 | select { 203 | background-color: var(--bg); 204 | border: 1px solid var(--gray-3); 205 | border-radius: var(--br-base); 206 | color: inherit; 207 | } 208 | 209 | input, 210 | select, 211 | button, 212 | .button { 213 | padding: var(--spacing-s) var(--spacing-m); 214 | } 215 | 216 | button:hover, 217 | .button:hover, 218 | button:focus, 219 | .button:focus { 220 | background-color: var(--gray-9); 221 | text-decoration: none; 222 | } 223 | button:active, 224 | .button:active { 225 | top: 1px; 226 | } 227 | button.secondary, 228 | .button.secondary { 229 | background-color: transparent; 230 | border: 1px solid var(--gray-3); 231 | color: var(--fg); 232 | } 233 | button.secondary:focus, 234 | button.secondary:hover, 235 | .button.secondary:focus, 236 | .button.secondary:hover { 237 | background-color: transparent; 238 | border-color: var(--gray-5); 239 | } 240 | 241 | button.link, 242 | .button.link { 243 | background: none; 244 | box-shadow: none; 245 | display: inline; 246 | } 247 | [disabled] button, 248 | button[disabled], 249 | .button[disabled] { 250 | box-shadow: none; 251 | background-color: var(--gray-3); 252 | color: var(--gray-5); 253 | cursor: not-allowed; 254 | transform: none; 255 | } 256 | 257 | input:hover, 258 | input:focus, 259 | input:active, 260 | select:hover, 261 | select:focus, 262 | select:active { 263 | border-color: var(--gray-5); 264 | } 265 | 266 | select { 267 | appearance: none; 268 | background-image: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAwMCAxMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICA8cGF0aCBmaWxsPSJncmF5IiBkPSJNMTAsMjMyLjdsNDkwLDUzNC41bDQ5MC01MzQuNUgxMHoiIC8+DQo8L3N2Zz4=); 269 | background-repeat: no-repeat; 270 | background-position: bottom 50% right var(--spacing-s); 271 | background-size: var(--spacing-s); 272 | cursor: pointer; 273 | padding-right: var(--spacing-l); 274 | } 275 | 276 | input.error, 277 | select.error { 278 | border-color: var(--red); 279 | } 280 | 281 | input { 282 | caret-color: var(--accent); 283 | } 284 | 285 | input::selection { 286 | background-color: var(--accent); 287 | color: var(--bg); 288 | } 289 | 290 | .container { 291 | margin-left: auto; 292 | margin-right: auto; 293 | max-width: calc(55 * var(--spacing-m)); 294 | padding-left: var(--spacing-l); 295 | padding-right: var(--spacing-l); 296 | } 297 | 298 | .grid { 299 | display: grid; 300 | gap: var(--spacing-m); 301 | grid-template-columns: repeat(auto-fit, minmax(13em, 1fr)); 302 | margin: var(--spacing-m) 0; 303 | 304 | &>* { 305 | display: flex; 306 | flex-direction: column; 307 | gap: var(--spacing-m); 308 | } 309 | 310 | &>a { 311 | color: var(--fg); 312 | transform: scale(100%); 313 | transition: transform 75ms; 314 | 315 | &:hover, 316 | &:focus { 317 | text-decoration: none; 318 | transform: scale(103%); 319 | } 320 | } 321 | } -------------------------------------------------------------------------------- /src/components/markdown.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactMarkdown from "react-markdown" 3 | import { ReactMarkdownOptions } from "react-markdown/lib/react-markdown" 4 | 5 | const Markdown: React.FC = (props) => ( 6 | 7 | ) 8 | 9 | export default Markdown -------------------------------------------------------------------------------- /src/components/my-nfts/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useRef, useState } from "react" 2 | import { createPortal } from 'react-dom' 3 | import Lightbox from 'react-image-lightbox'; 4 | import { wallet } from "../../near" 5 | import useLocales from '../../hooks/useLocales' 6 | import useTenk from '../../hooks/useTenk' 7 | import * as css from './my-nfts.module.css' 8 | import 'react-image-lightbox/style.css'; 9 | 10 | const portalRoot: undefined | HTMLElement = typeof document !== 'undefined' 11 | ? document.getElementById("portal")! 12 | : undefined 13 | 14 | const MyNFTs: React.FC<{ 15 | highlight?: string[], 16 | onClose: () => void, 17 | }> = ({ highlight, onClose }) => { 18 | const currentUser = wallet.getAccountId() 19 | const { locale } = useLocales() 20 | const { contractMetadata, nfts } = useTenk() 21 | const [photoIndex, setPhotoIndex] = useState(0) 22 | const [lightboxOpen, setLightboxOpen] = useState(false) 23 | const portalElement = useRef(document.createElement("div")) 24 | const containerElement = useRef(null) 25 | 26 | const onClick = useMemo(() => function onCloseRaw(this: Document, event: MouseEvent) { 27 | if (!lightboxOpen && event.target && !containerElement.current?.contains(event.target as Node)) { 28 | onClose() 29 | } 30 | }, [lightboxOpen, onClose]) 31 | 32 | useEffect(() => { 33 | portalRoot?.appendChild(portalElement.current) 34 | const bgContent: HTMLDivElement = document.querySelector('#___gatsby')! 35 | bgContent.style.filter = 'blur(4px)' 36 | bgContent.style.overflow = 'hidden' 37 | document.addEventListener('click', onClick) 38 | return function onUnmount() { 39 | bgContent.style.filter = '' 40 | bgContent.style.overflow = '' 41 | portalRoot?.removeChild(portalElement.current) 42 | document.removeEventListener('click', onClick) 43 | } 44 | }, [onClick, portalRoot]) 45 | 46 | if ( 47 | !portalRoot || 48 | !locale || 49 | !currentUser || 50 | !contractMetadata || 51 | nfts.length === 0 52 | ) { 53 | return null 54 | } 55 | 56 | const nextSrc = nfts.length > 1 ? nfts[(photoIndex + 1) % nfts.length].media : undefined 57 | const prevSrc = nfts.length > 1 ? nfts[(photoIndex + nfts.length - 1) % nfts.length].media : undefined 58 | 59 | return createPortal( 60 | <> 61 |
62 |
63 |
64 |

{locale.myNFTs}

65 | 68 |
69 |
70 | {nfts.map((nft, index) => ( 71 | 89 | ))} 90 |
91 |
92 |
93 | {lightboxOpen && ( 94 | setLightboxOpen(false)} 106 | onMovePrevRequest={() => 107 | setPhotoIndex((photoIndex + nfts.length - 1) % nfts.length) 108 | } 109 | onMoveNextRequest={() => 110 | setPhotoIndex((photoIndex + 1) % nfts.length) 111 | } 112 | /> 113 | )} 114 | , 115 | portalElement.current 116 | ) 117 | } 118 | 119 | export default MyNFTs -------------------------------------------------------------------------------- /src/components/my-nfts/my-nfts.module.css: -------------------------------------------------------------------------------- 1 | .myNfts { 2 | background-color: rgba(0, 0, 0, 0.7); 3 | bottom: 0; 4 | color: var(--white); 5 | filter: invert(blur(4px)); 6 | left: 0; 7 | overflow: scroll; 8 | position: fixed; 9 | right: 0; 10 | top: 0; 11 | z-index: 1; 12 | padding-top: var(--spacing-m); 13 | } 14 | 15 | .myNfts header { 16 | align-items: center; 17 | display: flex; 18 | justify-content: space-between; 19 | } 20 | 21 | .close { 22 | background-color: transparent; 23 | border: none; 24 | cursor: pointer; 25 | color: var(--white); 26 | font-size: 3em; 27 | line-height: 1; 28 | padding: 0; 29 | } 30 | 31 | .close:focus, 32 | .close:hover { 33 | background-color: transparent; 34 | color: var(--accent); 35 | } 36 | 37 | .grid { 38 | display: grid; 39 | gap: var(--spacing-m); 40 | grid-template-columns: repeat(auto-fill, minmax(10em, 1fr)); 41 | margin: var(--spacing-m) 0; 42 | } 43 | 44 | .myNfts button { 45 | background: transparent; 46 | } 47 | 48 | .myNfts button:hover, 49 | .myNfts button:focus { 50 | box-shadow: none; 51 | background: transparent; 52 | } 53 | 54 | .nft { 55 | color: inherit; 56 | padding: 0; 57 | margin: 0; 58 | border: none; 59 | display: flex; 60 | flex-direction: column; 61 | justify-content: space-between; 62 | text-align: left; 63 | transition: transform 75ms; 64 | transform: scale(100%); 65 | } 66 | 67 | .nft:hover, 68 | .nft:focus { 69 | transform: scale(105%); 70 | } 71 | 72 | .nft footer { 73 | display: flex; 74 | justify-content: space-between; 75 | } 76 | 77 | .highlight { 78 | color: var(--accent); 79 | font-weight: var(--fw-bold); 80 | } 81 | 82 | .chip { 83 | color: var(--white); 84 | background: rgba(0, 0, 0, 0.5); 85 | padding: 0.5em 0.5em 0.5em; 86 | box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37); 87 | backdrop-filter: blur(19.5px); 88 | -webkit-backdrop-filter: blur(19.5px); 89 | border-radius: 10px; 90 | border: 1px solid rgba(255, 255, 255, 0.10); 91 | } 92 | 93 | .nft>div:first-child { 94 | --aspect-ratio: 1 / 1; 95 | background-color: var(--gray-1); 96 | border-radius: var(--br-small); 97 | padding-bottom: calc(var(--aspect-ratio, 1 / 1) * 100%); 98 | background-repeat: no-repeat; 99 | background-position: center; 100 | background-size: cover; 101 | } -------------------------------------------------------------------------------- /src/components/my-nfts/my-nfts.module.css.d.ts: -------------------------------------------------------------------------------- 1 | // This file is automatically generated. Do not modify this file manually -- YOUR CHANGES WILL BE ERASED! 2 | export const myNfts: string; 3 | export const close: string; 4 | export const grid: string; 5 | export const nft: string; 6 | export const highlight: string; 7 | export const chip: string; 8 | -------------------------------------------------------------------------------- /src/components/nav/index.tsx: -------------------------------------------------------------------------------- 1 | import settings from "../../../config/settings.json" 2 | import React, { useState } from "react" 3 | import { signIn, wallet } from "../../near" 4 | import * as css from "./nav.module.css" 5 | import useLocales from "../../hooks/useLocales" 6 | import useTenk from "../../hooks/useTenk" 7 | import Dropdown from "../../components/dropdown" 8 | import MyNFTs from "../../components/my-nfts" 9 | import Image from "../image" 10 | 11 | function signOut() { 12 | wallet.signOut() 13 | window.location.replace(window.location.origin + window.location.pathname) 14 | } 15 | 16 | export default function Nav() { 17 | const currentUser = wallet.getAccountId() 18 | const { locale } = useLocales() 19 | const { nfts } = useTenk() 20 | const [showNFTs, setShowNFTs] = useState(false) 21 | 22 | if (!locale) return null 23 | 24 | return ( 25 | <> 26 | 58 | {showNFTs && setShowNFTs(false)} />} 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/components/nav/nav.module.css: -------------------------------------------------------------------------------- 1 | /* This is a CSS Module; see https://create-react-app.dev/docs/adding-a-css-modules-stylesheet/ */ 2 | .bg { 3 | background-color: var(--gray-1); 4 | padding: var(--spacing-m) 0; 5 | margin-bottom: var(--spacing-l); 6 | } 7 | 8 | .content { 9 | align-items: center; 10 | display: flex; 11 | flex-wrap: wrap; 12 | gap: var(--spacing-m); 13 | justify-content: space-between; 14 | } 15 | 16 | .content > * { 17 | flex: 1 1 auto; 18 | } 19 | 20 | .social { 21 | display: flex; 22 | gap: var(--spacing-s); 23 | } 24 | 25 | .social svg { 26 | height: var(--spacing-xl); 27 | width: var(--spacing-xl); 28 | vertical-align: middle; 29 | } 30 | .social svg path { 31 | fill: var(--accent); 32 | } 33 | 34 | .actions { 35 | display: flex; 36 | gap: var(--spacing-m); 37 | flex-wrap: wrap; 38 | justify-content: flex-end; 39 | } 40 | 41 | .button { 42 | font-size: var(--spacing-l); 43 | font-weight: var(--fw-black); 44 | } -------------------------------------------------------------------------------- /src/components/nav/nav.module.css.d.ts: -------------------------------------------------------------------------------- 1 | // This file is automatically generated. Do not modify this file manually -- YOUR CHANGES WILL BE ERASED! 2 | export const bg: string; 3 | export const content: string; 4 | export const social: string; 5 | export const actions: string; 6 | export const button: string; 7 | -------------------------------------------------------------------------------- /src/components/section/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import Image, { ImageProps } from "../image" 3 | import Video, { VideoProps } from "../video" 4 | import BackgroundImage from "../background-image" 5 | 6 | import * as css from "./section.module.css" 7 | 8 | const Section: React.FC<{ 9 | backgroundColor?: string, 10 | backgroundImage?: string, 11 | className?: string, 12 | image?: string | ImageProps 13 | video?: string | VideoProps 14 | }> = ({ 15 | backgroundColor, backgroundImage, className, image, video, children 16 | }) => ( 17 | 23 |
24 |
25 |
26 | {children} 27 |
28 | {video &&
{ 29 | // wrap in div for proper styling 30 | typeof video === 'string' 31 | ?
} 34 | {image && (typeof image === 'string' 35 | ? 36 | : 37 | )} 38 |
39 |
40 |
41 | ) 42 | 43 | export default Section -------------------------------------------------------------------------------- /src/components/section/section.module.css: -------------------------------------------------------------------------------- 1 | .section { 2 | padding-bottom: var(--spacing-xxl); 3 | } 4 | 5 | .section:nth-of-type(n+2) .content h1 { 6 | color: var(--accent); 7 | } 8 | .content { 9 | align-items: flex-start; 10 | display: flex; 11 | flex-direction: column; 12 | gap: var(--spacing-xl); 13 | width: 100%; 14 | } 15 | 16 | .content > * { 17 | flex: 1 1; 18 | width: 100%; 19 | } 20 | 21 | .section:nth-of-type(1) .content.hasMedia>* { 22 | flex: 1 1 50%; 23 | } 24 | 25 | .section:nth-of-type(n+2) .content>*:first-child { 26 | flex: 2 1; 27 | } 28 | @media (min-width: 500px) { 29 | .content.hasMedia { 30 | flex-direction: row; 31 | } 32 | 33 | .section:nth-of-type(2n) .content { 34 | flex-direction: row-reverse; 35 | } 36 | } -------------------------------------------------------------------------------- /src/components/section/section.module.css.d.ts: -------------------------------------------------------------------------------- 1 | // This file is automatically generated. Do not modify this file manually -- YOUR CHANGES WILL BE ERASED! 2 | export const section: string; 3 | export const content: string; 4 | export const hasMedia: string; 5 | -------------------------------------------------------------------------------- /src/components/seo.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * SEO component that queries for data with 3 | * Gatsby's useStaticQuery React hook 4 | * 5 | * See: https://www.gatsbyjs.com/docs/use-static-query/ 6 | */ 7 | 8 | import * as React from "react" 9 | import { Helmet } from "react-helmet" 10 | import { useStaticQuery, graphql } from "gatsby" 11 | import settings from "../../config/settings.json" 12 | 13 | type MetaTag = 14 | | { 15 | name: string 16 | content: string 17 | } 18 | | { 19 | property: string 20 | content: string 21 | } 22 | 23 | interface Props { 24 | description: string 25 | lang: string 26 | title: string 27 | meta?: MetaTag[] 28 | favicon?: string 29 | image?: string 30 | } 31 | 32 | function Seo({ 33 | description, 34 | lang, 35 | meta = [], 36 | title, 37 | favicon, 38 | image, 39 | }: Props) { 40 | const { site } = useStaticQuery( 41 | graphql` 42 | query { 43 | site { 44 | siteMetadata { 45 | author 46 | siteUrl 47 | } 48 | } 49 | } 50 | ` 51 | ) 52 | 53 | return ( 54 | 111 | ) 112 | } 113 | export default Seo 114 | -------------------------------------------------------------------------------- /src/components/slider/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Root, Track, Range, Thumb, SliderProps } from "@radix-ui/react-slider" 3 | import * as css from "./slider.module.css" 4 | 5 | const Dropdown: React.FC = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | {props.value} 13 | 14 | 15 | ) 16 | } 17 | 18 | export default Dropdown 19 | -------------------------------------------------------------------------------- /src/components/slider/slider.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | align-items: center; 3 | display: flex; 4 | padding-bottom: var(--spacing-xl); 5 | padding-top: var(--spacing-xl); 6 | position: relative; 7 | touch-action: none; 8 | user-select: none; 9 | width: 100%; 10 | } 11 | .root[data-orientation="horizontal"] { 12 | height: 20px; 13 | } 14 | .root[data-orientation="vertical"] { 15 | flex-direction: column; 16 | height: 100px; 17 | width: 20px; 18 | } 19 | 20 | .track { 21 | background-color: var(--fg); 22 | border-radius: 9999px; 23 | flex-grow: 1; 24 | position: relative; 25 | } 26 | .track[data-orientation="horizontal"] { 27 | height: 3px; 28 | } 29 | .track[data-orientation="vertical"] { 30 | width: 3px; 31 | } 32 | 33 | .range { 34 | background-color: var(--accent); 35 | border-radius: 9999px; 36 | height: 100%; 37 | position: absolute; 38 | } 39 | 40 | .thumb { 41 | all: unset; 42 | background-color: var(--accent); 43 | border-radius: 1000px; 44 | box-shadow: 0 0 10px var(--gray-2); 45 | color: var(--white); 46 | cursor: col-resize; 47 | display: flex; 48 | flex-direction: column; 49 | height: var(--spacing-xl); 50 | justify-content: space-around; 51 | text-align: center; 52 | width: var(--spacing-xl); 53 | } 54 | .thumb:hover { 55 | box-shadow: 56 | 0 0 10px var(--gray-2), 57 | 0 0 100px var(--gray-1) inset; 58 | } 59 | .thumb:focus { 60 | box-shadow: 0 0 0 3px var(--gray-3); 61 | } -------------------------------------------------------------------------------- /src/components/slider/slider.module.css.d.ts: -------------------------------------------------------------------------------- 1 | // This file is automatically generated. Do not modify this file manually -- YOUR CHANGES WILL BE ERASED! 2 | export const root: string; 3 | export const track: string; 4 | export const range: string; 5 | export const thumb: string; 6 | -------------------------------------------------------------------------------- /src/components/video/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { withPrefix } from 'gatsby' 3 | import useVideoData from "../../hooks/useVideoData" 4 | 5 | export type VideoProps = React.DetailedHTMLProps< 6 | React.VideoHTMLAttributes, 7 | HTMLVideoElement 8 | > & { 9 | src: string 10 | } 11 | 12 | const Video: React.FC = ({ src, ...props }) => { 13 | const { videoH264 } = useVideoData(src) 14 | return ( 15 | 18 | ) 19 | } 20 | 21 | export default Video 22 | -------------------------------------------------------------------------------- /src/hooks/useHeroStatuses.ts: -------------------------------------------------------------------------------- 1 | import { useLocation } from "@reach/router" 2 | import { wallet } from "../near" 3 | import { Status } from "../near/contracts/tenk" 4 | import useTenk from './useTenk' 5 | 6 | const overrides = [ 7 | { saleStatus: 'saleClosed', userStatus: 'signedOut' }, 8 | { saleStatus: 'saleClosed', userStatus: 'signedIn' }, 9 | { saleStatus: 'saleClosed', userStatus: 'vip' }, 10 | { saleStatus: 'presale', userStatus: 'signedOut' }, 11 | { saleStatus: 'presale', userStatus: 'signedIn' }, 12 | { saleStatus: 'presale', userStatus: 'vip' }, 13 | { saleStatus: 'saleOpen', userStatus: 'signedOut' }, 14 | { saleStatus: 'saleOpen', userStatus: 'signedIn' }, 15 | { saleStatus: 'saleOpen', userStatus: 'vip' }, 16 | { saleStatus: 'allSold', userStatus: 'signedOut' }, 17 | { saleStatus: 'allSold', userStatus: 'signedIn' }, 18 | { saleStatus: 'allSold', userStatus: 'vip' }, 19 | ] as const 20 | 21 | const saleStatuses = { 22 | [Status.Closed]: 'saleClosed', 23 | [Status.Presale]: 'presale', 24 | [Status.Open]: 'saleOpen', 25 | [Status.SoldOut]: 'allSold', 26 | } as const 27 | 28 | export default function useHeroStatuses() { 29 | const { saleInfo, vip } = useTenk() 30 | const heroParamStr = new URLSearchParams(useLocation().search).get('hero') 31 | const heroParam = heroParamStr ? parseInt(heroParamStr) : undefined 32 | const override = overrides[heroParam ?? -1] 33 | 34 | return { 35 | heroParam, 36 | overrides, 37 | saleStatus: override?.saleStatus ?? saleStatuses[saleInfo.status], 38 | userStatus: override?.userStatus ?? 39 | (vip ? 'vip' : wallet.getAccountId() ? 'signedIn' : 'signedOut'), 40 | } 41 | } -------------------------------------------------------------------------------- /src/hooks/useImageData.ts: -------------------------------------------------------------------------------- 1 | import { useStaticQuery, graphql } from "gatsby" 2 | import type { AllImagesQuery } from "../../graphql-types" 3 | 4 | type SvgOrImage = { 5 | svg: AllImagesQuery['svg']['nodes'][number] 6 | image: undefined 7 | } | { 8 | svg: undefined 9 | image: AllImagesQuery['nonSvg']['nodes'][number] 10 | } 11 | 12 | /** 13 | * Get image data for use with an inline/data-URL SVG or with GatsbyImage. 14 | * 15 | * @param src filename of an image in `config/images`. A helpful error will be thrown if no image found. 16 | * @returns an object with either an `svg` key or an `image` key 17 | */ 18 | export default function useImageData(src: string): SvgOrImage { 19 | const { 20 | svg: { nodes: svgs }, 21 | nonSvg: { nodes: images }, 22 | }: AllImagesQuery = useStaticQuery( 23 | graphql` 24 | query AllImages { 25 | svg: allFile(filter: { sourceInstanceName: { eq: "images" }, extension: { eq: "svg" } }) { 26 | nodes { 27 | relativePath 28 | svg { 29 | content 30 | dataURI 31 | } 32 | } 33 | } 34 | nonSvg: allFile(filter: { sourceInstanceName: { eq: "images" }, extension: { ne: "svg" } }) { 35 | nodes { 36 | relativePath 37 | publicURL 38 | childImageSharp { 39 | gatsbyImageData 40 | } 41 | } 42 | } 43 | } 44 | ` 45 | ) 46 | 47 | const svg = svgs.find(s => s.relativePath === src) 48 | const image = images.find(i => i.relativePath === src) 49 | 50 | if (!svg && !image) { 51 | console.error( 52 | new Error( 53 | `No image "${src}" in PROJECT_ROOT/config/images. Set "src" to one of the following:\n • ${images 54 | .map(i => i.relativePath) 55 | .join("\n • ")}` 56 | ) 57 | ) 58 | } 59 | 60 | return { svg, image } as SvgOrImage 61 | } -------------------------------------------------------------------------------- /src/hooks/useLocales.ts: -------------------------------------------------------------------------------- 1 | import { useStaticQuery, graphql } from "gatsby" 2 | import { useLocation } from "@reach/router" 3 | import type { AllLocalesQuery } from "../../graphql-types" 4 | 5 | type I18n = NonNullable 6 | 7 | export type Locale = { 8 | id: string 9 | } & { 10 | [K in keyof I18n]: NonNullable 11 | } 12 | 13 | /** 14 | * Gatsby really wants to push everything, even simple stuff like JSON files in 15 | * a project folder, through a complicated GraphQL pipeline. This hook hides the 16 | * details of looking up the locale files in the `i18n` folder and makes them 17 | * easily accessible to any component that needs them. 18 | * 19 | * @returns the list of all `locales`, as well as the current `locale` given by the URL 20 | */ 21 | export default function useLocales(): { locales: Locale[]; locale?: Locale } { 22 | const { allFile }: AllLocalesQuery = useStaticQuery( 23 | graphql` 24 | query AllLocales { 25 | allFile(filter: { sourceInstanceName: { eq: "i18n" }, extension: {eq: "json"}}) { 26 | nodes { 27 | name 28 | childI18NJson { 29 | viewIn 30 | langPicker 31 | title 32 | description 33 | calendarEvent 34 | connectWallet 35 | signOut 36 | new 37 | myNFTs 38 | nextNFT 39 | prevNFT 40 | close 41 | zoomIn 42 | zoomOut 43 | } 44 | } 45 | } 46 | } 47 | ` 48 | ) 49 | const { pathname } = useLocation() 50 | 51 | const locales = allFile.nodes.map(node => ({ 52 | id: node.name, 53 | ...node.childI18NJson!, 54 | }) as Locale) // type coercion removes the `| null` that GraphQL includes 55 | 56 | const locale = locales.find(l => new RegExp(`/${l.id}`).test(pathname)) 57 | 58 | return { locales, locale } 59 | } 60 | -------------------------------------------------------------------------------- /src/hooks/useTenk.ts: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { NftContractMetadata, SaleInfo, Token as RawToken } from "../near/contracts/tenk" 3 | import { TENK } from "../near/contracts" 4 | import { wallet } from "../near" 5 | import staleData from "../../stale-data-from-build-time.json" 6 | 7 | const account_id = wallet.getAccountId() 8 | 9 | type Token = RawToken & { 10 | media: string 11 | } 12 | 13 | export interface TenkData { 14 | contractMetadata?: NftContractMetadata 15 | remainingAllowance?: number 16 | mintRateLimit: number 17 | nfts: Token[] 18 | saleInfo: SaleInfo 19 | tokensLeft: number 20 | vip: boolean 21 | } 22 | 23 | interface ReturnedData extends TenkData { 24 | stale: boolean 25 | } 26 | 27 | // initialize calls at root of file so that first evaluation of this file causes 28 | // calls to start, and subsequent imports of this file just use those same calls 29 | const rpcCalls = Promise.all([ 30 | TENK.get_sale_info(), 31 | TENK.nft_metadata(), 32 | TENK.tokens_left(), 33 | !account_id ? undefined : TENK.whitelisted({ account_id }), 34 | !account_id ? undefined : TENK.remaining_allowance({ account_id }), 35 | !account_id ? undefined : TENK.nft_tokens_for_owner({ account_id }), 36 | !account_id ? undefined : TENK.mint_rate_limit({ account_id }), 37 | ]) 38 | 39 | // Export utility to get data in object form, rather than array form. 40 | // Used by gatsby-node.ts to create the stale data JSON file. 41 | export async function rpcData(): Promise { 42 | const [ 43 | saleInfo, 44 | contractMetadata, 45 | tokensLeft, 46 | vip, 47 | remainingAllowance, 48 | nfts, 49 | mintRateLimit 50 | ] = await rpcCalls 51 | return { 52 | saleInfo, 53 | contractMetadata, 54 | tokensLeft, 55 | vip: vip ?? false, 56 | remainingAllowance: remainingAllowance ?? undefined, 57 | nfts: nfts?.map(nft => ({ ...nft, 58 | media: new URL(nft.metadata?.media ?? '', contractMetadata.base_uri ?? '').href 59 | })) ?? [], 60 | mintRateLimit: mintRateLimit ?? 10, 61 | } 62 | } 63 | 64 | export default function useTenk(): ReturnedData { 65 | const [data, setData] = React.useState({ 66 | ...staleData as unknown as TenkData, 67 | stale: true 68 | }) 69 | 70 | React.useEffect(() => { 71 | rpcData().then(d => setData({ ...d, stale: false })) 72 | }, []) 73 | 74 | return data 75 | } -------------------------------------------------------------------------------- /src/hooks/useVideoData.ts: -------------------------------------------------------------------------------- 1 | import { useStaticQuery, graphql } from "gatsby" 2 | import type { AllVideosQuery } from "../../graphql-types" 3 | 4 | type VideoQuery = AllVideosQuery['allFile']['videos'][number] 5 | type VideoH264 = NonNullable 6 | 7 | type H264 = { 8 | [K in keyof VideoH264]: NonNullable 9 | } 10 | 11 | type VideoData = { 12 | videoH264: H264 13 | } 14 | 15 | /** 16 | * Get video data for use with the Video or BackgroundVideo components. 17 | * 18 | * @param src filename of an image in `config/videos`. A helpful error will be thrown if no image found. 19 | * @returns ... 20 | */ 21 | export default function useImageData(src: string): VideoData { 22 | const { allFile: { videos } }: AllVideosQuery = useStaticQuery( 23 | graphql` 24 | query AllVideos { 25 | allFile(filter: {sourceInstanceName: {eq: "videos"}}) { 26 | videos: nodes { 27 | relativePath 28 | videoH264 { 29 | name 30 | duration 31 | aspectRatio 32 | absolutePath 33 | bitRate 34 | ext 35 | formatName 36 | height 37 | path 38 | size 39 | width 40 | startTime 41 | } 42 | } 43 | } 44 | } 45 | ` 46 | ) 47 | 48 | const video = videos.find(v => v.relativePath === src) 49 | 50 | if (!video) { 51 | throw new Error( 52 | `No video "${src}" in PROJECT_ROOT/config/videos. Set "src" to one of the following:\n • ${videos 53 | .map(v => v.relativePath) 54 | .join("\n • ")}` 55 | ) 56 | } 57 | 58 | if (!video.videoH264) { 59 | throw new Error( 60 | `Video "PROJECT_ROOT/config/videos/${src}" returned bad data; expected videoH264 to both be present, got: ${JSON.stringify(video)}` 61 | ) 62 | } 63 | 64 | return { 65 | videoH264: video.videoH264 as H264, 66 | } 67 | } -------------------------------------------------------------------------------- /src/near/contracts/index.ts: -------------------------------------------------------------------------------- 1 | import settings from "../../../config/settings.json" 2 | import { wallet } from ".." 3 | import { Contract } from "./tenk" 4 | 5 | export const TENK = new Contract(wallet.account(), settings.contractName) 6 | -------------------------------------------------------------------------------- /src/near/contracts/tenk.ts: -------------------------------------------------------------------------------- 1 | import { Account, transactions, providers, DEFAULT_FUNCTION_CALL_GAS } from 'near-api-js'; 2 | 3 | 4 | import BN from 'bn.js'; 5 | export interface ChangeMethodOptions { 6 | gas?: BN; 7 | attachedDeposit?: BN; 8 | walletMeta?: string; 9 | walletCallbackUrl?: string; 10 | } 11 | export interface ViewFunctionOptions { 12 | parse?: (response: Uint8Array) => any; 13 | stringify?: (input: any) => any; 14 | } 15 | 16 | /** 64 bit unsigned integer less than 2^53 -1 */ 17 | type u64 = number; 18 | /** 64 bit signed integer less than 2^53 -1 */ 19 | type i64 = number; 20 | /** 21 | * StorageUsage is used to count the amount of storage used by a contract. 22 | */ 23 | export type StorageUsage = u64; 24 | /** 25 | * Balance is a type for storing amounts of tokens, specified in yoctoNEAR. 26 | */ 27 | export type Balance = U128; 28 | /** 29 | * Represents the amount of NEAR tokens in "gas units" which are used to fund transactions. 30 | */ 31 | export type Gas = u64; 32 | /** 33 | * base64 string. 34 | */ 35 | export type Base64VecU8 = string; 36 | /** 37 | * Raw type for duration in nanoseconds 38 | */ 39 | export type Duration = u64; 40 | export type U128 = string; 41 | /** 42 | * Public key in a binary format with base58 string serialization with human-readable curve. 43 | * The key types currently supported are `secp256k1` and `ed25519`. 44 | * 45 | * Ed25519 public keys accepted are 32 bytes and secp256k1 keys are the uncompressed 64 format. 46 | */ 47 | export type PublicKey = string; 48 | export type AccountId = string; 49 | /** 50 | * Raw type for timestamp in nanoseconds 51 | */ 52 | export type Timestamp = u64; 53 | export interface StorageBalanceBounds { 54 | min: U128, 55 | max?: U128, 56 | } 57 | export interface FungibleTokenMetadata { 58 | spec: string, 59 | name: string, 60 | symbol: string, 61 | icon?: string, 62 | reference?: string, 63 | reference_hash?: Base64VecU8, 64 | decimals: number, 65 | } 66 | /** 67 | * In this implementation, the Token struct takes two extensions standards (metadata and approval) as optional fields, as they are frequently used in modern NFTs. 68 | */ 69 | export interface Token { 70 | token_id: TokenId, 71 | owner_id: AccountId, 72 | metadata?: TokenMetadata, 73 | approved_account_ids?: Record, 74 | } 75 | /** 76 | * Note that token IDs for NFTs are strings on NEAR. It's still fine to use autoincrementing numbers as unique IDs if desired, but they should be stringified. This is to make IDs more future-proof as chain-agnostic conventions and standards arise, and allows for more flexibility with considerations like bridging NFTs across chains, etc. 77 | */ 78 | export type TokenId = string; 79 | export interface StorageBalance { 80 | total: U128, 81 | available: U128, 82 | } 83 | export type WrappedDuration = string; 84 | /** 85 | * Metadata on the individual token level. 86 | */ 87 | export interface TokenMetadata { 88 | title?: string, 89 | description?: string, 90 | media?: string, 91 | media_hash?: Base64VecU8, 92 | copies?: u64, 93 | issued_at?: string, 94 | expires_at?: string, 95 | starts_at?: string, 96 | updated_at?: string, 97 | extra?: string, 98 | reference?: string, 99 | reference_hash?: Base64VecU8, 100 | } 101 | /** 102 | * Metadata for the NFT contract itself. 103 | */ 104 | export interface NftContractMetadata { 105 | spec: string, 106 | name: string, 107 | symbol: string, 108 | icon?: string, 109 | base_uri?: string, 110 | reference?: string, 111 | reference_hash?: Base64VecU8, 112 | } 113 | /** 114 | * Current state of contract 115 | */ 116 | export enum Status { 117 | /** 118 | * Not open for any sales 119 | */ 120 | Closed = "Closed", 121 | /** 122 | * VIP accounts can mint 123 | */ 124 | Presale = "Presale", 125 | /** 126 | * Any account can mint 127 | */ 128 | Open = "Open", 129 | /** 130 | * No more tokens to be minted 131 | */ 132 | SoldOut = "SoldOut", 133 | } 134 | export interface InitialMetadata { 135 | name: string, 136 | symbol: string, 137 | uri: string, 138 | icon?: string, 139 | spec?: string, 140 | reference?: string, 141 | reference_hash?: Base64VecU8, 142 | } 143 | /** 144 | * Information about the current sale 145 | */ 146 | export interface SaleInfo { 147 | /** 148 | * Current state of contract 149 | */ 150 | status: Status, 151 | /** 152 | * Start of the VIP sale 153 | */ 154 | presale_start: Duration, 155 | /** 156 | * Start of public sale 157 | */ 158 | sale_start: Duration, 159 | /** 160 | * Total tokens that could be minted 161 | */ 162 | token_final_supply: u64, 163 | /** 164 | * Current price for one token 165 | */ 166 | price: U128, 167 | } 168 | export interface Sale { 169 | royalties?: Royalties, 170 | initial_royalties?: Royalties, 171 | presale_start?: Duration, 172 | public_sale_start?: Duration, 173 | allowance?: number, 174 | presale_price?: U128, 175 | price: U128, 176 | mint_rate_limit?: number, 177 | } 178 | export type BasisPoint = number; 179 | /** 180 | * Information about the current sale from user perspective 181 | */ 182 | export interface UserSaleInfo { 183 | sale_info: SaleInfo, 184 | is_vip: boolean, 185 | remaining_allowance?: number, 186 | } 187 | /** 188 | * Copied from https://github.com/near/NEPs/blob/6170aba1c6f4cd4804e9ad442caeae9dc47e7d44/specs/Standards/NonFungibleToken/Payout.md#reference-level-explanation 189 | * A mapping of NEAR accounts to the amount each should be paid out, in 190 | * the event of a token-sale. The payout mapping MUST be shorter than the 191 | * maximum length specified by the financial contract obtaining this 192 | * payout data. Any mapping of length 10 or less MUST be accepted by 193 | * financial contracts, so 10 is a safe upper limit. 194 | * This currently deviates from the standard but is in the process of updating to use this type 195 | */ 196 | export interface Payout { 197 | payout: Record, 198 | } 199 | export interface Royalties { 200 | accounts: Record, 201 | percent: BasisPoint, 202 | } 203 | 204 | export class Contract { 205 | 206 | constructor(public account: Account, public readonly contractId: string){} 207 | 208 | check_key(args: { 209 | public_key: PublicKey, 210 | }, options?: ViewFunctionOptions): Promise { 211 | return this.account.viewFunction(this.contractId, "check_key", args, options); 212 | } 213 | async update_allowance(args: { 214 | allowance: number, 215 | }, options?: ChangeMethodOptions): Promise { 216 | return providers.getTransactionLastResult(await this.update_allowanceRaw(args, options)); 217 | } 218 | update_allowanceRaw(args: { 219 | allowance: number, 220 | }, options?: ChangeMethodOptions): Promise { 221 | return this.account.functionCall({contractId: this.contractId, methodName: "update_allowance", args, ...options}); 222 | } 223 | update_allowanceTx(args: { 224 | allowance: number, 225 | }, options?: ChangeMethodOptions): transactions.Action { 226 | return transactions.functionCall("update_allowance", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 227 | } 228 | whitelisted(args: { 229 | account_id: AccountId, 230 | }, options?: ViewFunctionOptions): Promise { 231 | return this.account.viewFunction(this.contractId, "whitelisted", args, options); 232 | } 233 | get_sale_info(args: { 234 | } = {}, options?: ViewFunctionOptions): Promise { 235 | return this.account.viewFunction(this.contractId, "get_sale_info", args, options); 236 | } 237 | cost_per_token(args: { 238 | minter: AccountId, 239 | }, options?: ViewFunctionOptions): Promise { 240 | return this.account.viewFunction(this.contractId, "cost_per_token", args, options); 241 | } 242 | async transfer_ownership(args: { 243 | new_owner: AccountId, 244 | }, options?: ChangeMethodOptions): Promise { 245 | return providers.getTransactionLastResult(await this.transfer_ownershipRaw(args, options)); 246 | } 247 | transfer_ownershipRaw(args: { 248 | new_owner: AccountId, 249 | }, options?: ChangeMethodOptions): Promise { 250 | return this.account.functionCall({contractId: this.contractId, methodName: "transfer_ownership", args, ...options}); 251 | } 252 | transfer_ownershipTx(args: { 253 | new_owner: AccountId, 254 | }, options?: ChangeMethodOptions): transactions.Action { 255 | return transactions.functionCall("transfer_ownership", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 256 | } 257 | nft_total_supply(args: { 258 | } = {}, options?: ViewFunctionOptions): Promise { 259 | return this.account.viewFunction(this.contractId, "nft_total_supply", args, options); 260 | } 261 | nft_tokens(args: { 262 | from_index?: U128, 263 | limit?: u64, 264 | }, options?: ViewFunctionOptions): Promise { 265 | return this.account.viewFunction(this.contractId, "nft_tokens", args, options); 266 | } 267 | nft_token(args: { 268 | token_id: TokenId, 269 | }, options?: ViewFunctionOptions): Promise { 270 | return this.account.viewFunction(this.contractId, "nft_token", args, options); 271 | } 272 | async close_contract(args: { 273 | } = {}, options?: ChangeMethodOptions): Promise { 274 | return providers.getTransactionLastResult(await this.close_contractRaw(args, options)); 275 | } 276 | close_contractRaw(args: { 277 | } = {}, options?: ChangeMethodOptions): Promise { 278 | return this.account.functionCall({contractId: this.contractId, methodName: "close_contract", args, ...options}); 279 | } 280 | close_contractTx(args: { 281 | } = {}, options?: ChangeMethodOptions): transactions.Action { 282 | return transactions.functionCall("close_contract", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 283 | } 284 | async nft_approve(args: { 285 | token_id: TokenId, 286 | account_id: AccountId, 287 | msg?: string, 288 | }, options?: ChangeMethodOptions): Promise { 289 | return providers.getTransactionLastResult(await this.nft_approveRaw(args, options)); 290 | } 291 | nft_approveRaw(args: { 292 | token_id: TokenId, 293 | account_id: AccountId, 294 | msg?: string, 295 | }, options?: ChangeMethodOptions): Promise { 296 | return this.account.functionCall({contractId: this.contractId, methodName: "nft_approve", args, ...options}); 297 | } 298 | nft_approveTx(args: { 299 | token_id: TokenId, 300 | account_id: AccountId, 301 | msg?: string, 302 | }, options?: ChangeMethodOptions): transactions.Action { 303 | return transactions.functionCall("nft_approve", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 304 | } 305 | async start_sale(args: { 306 | price?: U128, 307 | }, options?: ChangeMethodOptions): Promise { 308 | return providers.getTransactionLastResult(await this.start_saleRaw(args, options)); 309 | } 310 | start_saleRaw(args: { 311 | price?: U128, 312 | }, options?: ChangeMethodOptions): Promise { 313 | return this.account.functionCall({contractId: this.contractId, methodName: "start_sale", args, ...options}); 314 | } 315 | start_saleTx(args: { 316 | price?: U128, 317 | }, options?: ChangeMethodOptions): transactions.Action { 318 | return transactions.functionCall("start_sale", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 319 | } 320 | async nft_mint_many(args: { 321 | num: number, 322 | }, options?: ChangeMethodOptions): Promise { 323 | return providers.getTransactionLastResult(await this.nft_mint_manyRaw(args, options)); 324 | } 325 | nft_mint_manyRaw(args: { 326 | num: number, 327 | }, options?: ChangeMethodOptions): Promise { 328 | return this.account.functionCall({contractId: this.contractId, methodName: "nft_mint_many", args, ...options}); 329 | } 330 | nft_mint_manyTx(args: { 331 | num: number, 332 | }, options?: ChangeMethodOptions): transactions.Action { 333 | return transactions.functionCall("nft_mint_many", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 334 | } 335 | async update_uri(args: { 336 | uri: string, 337 | }, options?: ChangeMethodOptions): Promise { 338 | return providers.getTransactionLastResult(await this.update_uriRaw(args, options)); 339 | } 340 | update_uriRaw(args: { 341 | uri: string, 342 | }, options?: ChangeMethodOptions): Promise { 343 | return this.account.functionCall({contractId: this.contractId, methodName: "update_uri", args, ...options}); 344 | } 345 | update_uriTx(args: { 346 | uri: string, 347 | }, options?: ChangeMethodOptions): transactions.Action { 348 | return transactions.functionCall("update_uri", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 349 | } 350 | async nft_transfer_call(args: { 351 | receiver_id: AccountId, 352 | token_id: TokenId, 353 | approval_id?: u64, 354 | memo?: string, 355 | msg: string, 356 | }, options?: ChangeMethodOptions): Promise { 357 | return providers.getTransactionLastResult(await this.nft_transfer_callRaw(args, options)); 358 | } 359 | nft_transfer_callRaw(args: { 360 | receiver_id: AccountId, 361 | token_id: TokenId, 362 | approval_id?: u64, 363 | memo?: string, 364 | msg: string, 365 | }, options?: ChangeMethodOptions): Promise { 366 | return this.account.functionCall({contractId: this.contractId, methodName: "nft_transfer_call", args, ...options}); 367 | } 368 | nft_transfer_callTx(args: { 369 | receiver_id: AccountId, 370 | token_id: TokenId, 371 | approval_id?: u64, 372 | memo?: string, 373 | msg: string, 374 | }, options?: ChangeMethodOptions): transactions.Action { 375 | return transactions.functionCall("nft_transfer_call", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 376 | } 377 | nft_payout(args: { 378 | token_id: string, 379 | balance: U128, 380 | max_len_payout?: number, 381 | }, options?: ViewFunctionOptions): Promise { 382 | return this.account.viewFunction(this.contractId, "nft_payout", args, options); 383 | } 384 | async nft_transfer_payout(args: { 385 | receiver_id: AccountId, 386 | token_id: string, 387 | approval_id?: u64, 388 | memo?: string, 389 | balance: U128, 390 | max_len_payout?: number, 391 | }, options?: ChangeMethodOptions): Promise { 392 | return providers.getTransactionLastResult(await this.nft_transfer_payoutRaw(args, options)); 393 | } 394 | nft_transfer_payoutRaw(args: { 395 | receiver_id: AccountId, 396 | token_id: string, 397 | approval_id?: u64, 398 | memo?: string, 399 | balance: U128, 400 | max_len_payout?: number, 401 | }, options?: ChangeMethodOptions): Promise { 402 | return this.account.functionCall({contractId: this.contractId, methodName: "nft_transfer_payout", args, ...options}); 403 | } 404 | nft_transfer_payoutTx(args: { 405 | receiver_id: AccountId, 406 | token_id: string, 407 | approval_id?: u64, 408 | memo?: string, 409 | balance: U128, 410 | max_len_payout?: number, 411 | }, options?: ChangeMethodOptions): transactions.Action { 412 | return transactions.functionCall("nft_transfer_payout", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 413 | } 414 | /** 415 | * Returns the balance associated with given key. 416 | */ 417 | get_key_balance(args: { 418 | } = {}, options?: ViewFunctionOptions): Promise { 419 | return this.account.viewFunction(this.contractId, "get_key_balance", args, options); 420 | } 421 | /** 422 | * Create a pending token that can be claimed with corresponding private key 423 | */ 424 | async create_linkdrop(args: { 425 | public_key: PublicKey, 426 | }, options?: ChangeMethodOptions): Promise { 427 | return providers.getTransactionLastResult(await this.create_linkdropRaw(args, options)); 428 | } 429 | /** 430 | * Create a pending token that can be claimed with corresponding private key 431 | */ 432 | create_linkdropRaw(args: { 433 | public_key: PublicKey, 434 | }, options?: ChangeMethodOptions): Promise { 435 | return this.account.functionCall({contractId: this.contractId, methodName: "create_linkdrop", args, ...options}); 436 | } 437 | /** 438 | * Create a pending token that can be claimed with corresponding private key 439 | */ 440 | create_linkdropTx(args: { 441 | public_key: PublicKey, 442 | }, options?: ChangeMethodOptions): transactions.Action { 443 | return transactions.functionCall("create_linkdrop", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 444 | } 445 | async add_whitelist_accounts(args: { 446 | accounts: AccountId[], 447 | allowance?: number, 448 | }, options?: ChangeMethodOptions): Promise { 449 | return providers.getTransactionLastResult(await this.add_whitelist_accountsRaw(args, options)); 450 | } 451 | add_whitelist_accountsRaw(args: { 452 | accounts: AccountId[], 453 | allowance?: number, 454 | }, options?: ChangeMethodOptions): Promise { 455 | return this.account.functionCall({contractId: this.contractId, methodName: "add_whitelist_accounts", args, ...options}); 456 | } 457 | add_whitelist_accountsTx(args: { 458 | accounts: AccountId[], 459 | allowance?: number, 460 | }, options?: ChangeMethodOptions): transactions.Action { 461 | return transactions.functionCall("add_whitelist_accounts", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 462 | } 463 | async new(args: { 464 | owner_id: AccountId, 465 | metadata: NftContractMetadata, 466 | size: number, 467 | sale: Sale, 468 | }, options?: ChangeMethodOptions): Promise { 469 | return providers.getTransactionLastResult(await this.newRaw(args, options)); 470 | } 471 | newRaw(args: { 472 | owner_id: AccountId, 473 | metadata: NftContractMetadata, 474 | size: number, 475 | sale: Sale, 476 | }, options?: ChangeMethodOptions): Promise { 477 | return this.account.functionCall({contractId: this.contractId, methodName: "new", args, ...options}); 478 | } 479 | newTx(args: { 480 | owner_id: AccountId, 481 | metadata: NftContractMetadata, 482 | size: number, 483 | sale: Sale, 484 | }, options?: ChangeMethodOptions): transactions.Action { 485 | return transactions.functionCall("new", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 486 | } 487 | async start_presale(args: { 488 | public_sale_start?: Duration, 489 | presale_price?: U128, 490 | }, options?: ChangeMethodOptions): Promise { 491 | return providers.getTransactionLastResult(await this.start_presaleRaw(args, options)); 492 | } 493 | start_presaleRaw(args: { 494 | public_sale_start?: Duration, 495 | presale_price?: U128, 496 | }, options?: ChangeMethodOptions): Promise { 497 | return this.account.functionCall({contractId: this.contractId, methodName: "start_presale", args, ...options}); 498 | } 499 | start_presaleTx(args: { 500 | public_sale_start?: Duration, 501 | presale_price?: U128, 502 | }, options?: ChangeMethodOptions): transactions.Action { 503 | return transactions.functionCall("start_presale", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 504 | } 505 | token_storage_cost(args: { 506 | } = {}, options?: ViewFunctionOptions): Promise { 507 | return this.account.viewFunction(this.contractId, "token_storage_cost", args, options); 508 | } 509 | async nft_transfer(args: { 510 | receiver_id: AccountId, 511 | token_id: TokenId, 512 | approval_id?: u64, 513 | memo?: string, 514 | }, options?: ChangeMethodOptions): Promise { 515 | return providers.getTransactionLastResult(await this.nft_transferRaw(args, options)); 516 | } 517 | nft_transferRaw(args: { 518 | receiver_id: AccountId, 519 | token_id: TokenId, 520 | approval_id?: u64, 521 | memo?: string, 522 | }, options?: ChangeMethodOptions): Promise { 523 | return this.account.functionCall({contractId: this.contractId, methodName: "nft_transfer", args, ...options}); 524 | } 525 | nft_transferTx(args: { 526 | receiver_id: AccountId, 527 | token_id: TokenId, 528 | approval_id?: u64, 529 | memo?: string, 530 | }, options?: ChangeMethodOptions): transactions.Action { 531 | return transactions.functionCall("nft_transfer", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 532 | } 533 | async nft_revoke_all(args: { 534 | token_id: TokenId, 535 | }, options?: ChangeMethodOptions): Promise { 536 | return providers.getTransactionLastResult(await this.nft_revoke_allRaw(args, options)); 537 | } 538 | nft_revoke_allRaw(args: { 539 | token_id: TokenId, 540 | }, options?: ChangeMethodOptions): Promise { 541 | return this.account.functionCall({contractId: this.contractId, methodName: "nft_revoke_all", args, ...options}); 542 | } 543 | nft_revoke_allTx(args: { 544 | token_id: TokenId, 545 | }, options?: ChangeMethodOptions): transactions.Action { 546 | return transactions.functionCall("nft_revoke_all", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 547 | } 548 | cost_of_linkdrop(args: { 549 | minter: AccountId, 550 | }, options?: ViewFunctionOptions): Promise { 551 | return this.account.viewFunction(this.contractId, "cost_of_linkdrop", args, options); 552 | } 553 | total_cost(args: { 554 | num: number, 555 | minter: AccountId, 556 | }, options?: ViewFunctionOptions): Promise { 557 | return this.account.viewFunction(this.contractId, "total_cost", args, options); 558 | } 559 | get_linkdrop_contract(args: { 560 | } = {}, options?: ViewFunctionOptions): Promise { 561 | return this.account.viewFunction(this.contractId, "get_linkdrop_contract", args, options); 562 | } 563 | async new_default_meta(args: { 564 | owner_id: AccountId, 565 | metadata: InitialMetadata, 566 | size: number, 567 | sale?: Sale, 568 | }, options?: ChangeMethodOptions): Promise { 569 | return providers.getTransactionLastResult(await this.new_default_metaRaw(args, options)); 570 | } 571 | new_default_metaRaw(args: { 572 | owner_id: AccountId, 573 | metadata: InitialMetadata, 574 | size: number, 575 | sale?: Sale, 576 | }, options?: ChangeMethodOptions): Promise { 577 | return this.account.functionCall({contractId: this.contractId, methodName: "new_default_meta", args, ...options}); 578 | } 579 | new_default_metaTx(args: { 580 | owner_id: AccountId, 581 | metadata: InitialMetadata, 582 | size: number, 583 | sale?: Sale, 584 | }, options?: ChangeMethodOptions): transactions.Action { 585 | return transactions.functionCall("new_default_meta", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 586 | } 587 | async nft_revoke(args: { 588 | token_id: TokenId, 589 | account_id: AccountId, 590 | }, options?: ChangeMethodOptions): Promise { 591 | return providers.getTransactionLastResult(await this.nft_revokeRaw(args, options)); 592 | } 593 | nft_revokeRaw(args: { 594 | token_id: TokenId, 595 | account_id: AccountId, 596 | }, options?: ChangeMethodOptions): Promise { 597 | return this.account.functionCall({contractId: this.contractId, methodName: "nft_revoke", args, ...options}); 598 | } 599 | nft_revokeTx(args: { 600 | token_id: TokenId, 601 | account_id: AccountId, 602 | }, options?: ChangeMethodOptions): transactions.Action { 603 | return transactions.functionCall("nft_revoke", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 604 | } 605 | nft_metadata(args: { 606 | } = {}, options?: ViewFunctionOptions): Promise { 607 | return this.account.viewFunction(this.contractId, "nft_metadata", args, options); 608 | } 609 | mint_rate_limit(args: { 610 | } = {}, options?: ViewFunctionOptions): Promise { 611 | return this.account.viewFunction(this.contractId, "mint_rate_limit", args, options); 612 | } 613 | nft_is_approved(args: { 614 | token_id: TokenId, 615 | approved_account_id: AccountId, 616 | approval_id?: u64, 617 | }, options?: ViewFunctionOptions): Promise { 618 | return this.account.viewFunction(this.contractId, "nft_is_approved", args, options); 619 | } 620 | remaining_allowance(args: { 621 | account_id: AccountId, 622 | }, options?: ViewFunctionOptions): Promise { 623 | return this.account.viewFunction(this.contractId, "remaining_allowance", args, options); 624 | } 625 | async nft_mint(args: { 626 | token_id: TokenId, 627 | token_owner_id: AccountId, 628 | token_metadata: TokenMetadata, 629 | }, options?: ChangeMethodOptions): Promise { 630 | return providers.getTransactionLastResult(await this.nft_mintRaw(args, options)); 631 | } 632 | nft_mintRaw(args: { 633 | token_id: TokenId, 634 | token_owner_id: AccountId, 635 | token_metadata: TokenMetadata, 636 | }, options?: ChangeMethodOptions): Promise { 637 | return this.account.functionCall({contractId: this.contractId, methodName: "nft_mint", args, ...options}); 638 | } 639 | nft_mintTx(args: { 640 | token_id: TokenId, 641 | token_owner_id: AccountId, 642 | token_metadata: TokenMetadata, 643 | }, options?: ChangeMethodOptions): transactions.Action { 644 | return transactions.functionCall("nft_mint", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 645 | } 646 | get_user_sale_info(args: { 647 | account_id: AccountId, 648 | }, options?: ViewFunctionOptions): Promise { 649 | return this.account.viewFunction(this.contractId, "get_user_sale_info", args, options); 650 | } 651 | nft_tokens_for_owner(args: { 652 | account_id: AccountId, 653 | from_index?: U128, 654 | limit?: u64, 655 | }, options?: ViewFunctionOptions): Promise { 656 | return this.account.viewFunction(this.contractId, "nft_tokens_for_owner", args, options); 657 | } 658 | async add_whitelist_account_ungaurded(args: { 659 | account_id: AccountId, 660 | allowance: number, 661 | }, options?: ChangeMethodOptions): Promise { 662 | return providers.getTransactionLastResult(await this.add_whitelist_account_ungaurdedRaw(args, options)); 663 | } 664 | add_whitelist_account_ungaurdedRaw(args: { 665 | account_id: AccountId, 666 | allowance: number, 667 | }, options?: ChangeMethodOptions): Promise { 668 | return this.account.functionCall({contractId: this.contractId, methodName: "add_whitelist_account_ungaurded", args, ...options}); 669 | } 670 | add_whitelist_account_ungaurdedTx(args: { 671 | account_id: AccountId, 672 | allowance: number, 673 | }, options?: ChangeMethodOptions): transactions.Action { 674 | return transactions.functionCall("add_whitelist_account_ungaurded", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 675 | } 676 | tokens_left(args: { 677 | } = {}, options?: ViewFunctionOptions): Promise { 678 | return this.account.viewFunction(this.contractId, "tokens_left", args, options); 679 | } 680 | nft_supply_for_owner(args: { 681 | account_id: AccountId, 682 | }, options?: ViewFunctionOptions): Promise { 683 | return this.account.viewFunction(this.contractId, "nft_supply_for_owner", args, options); 684 | } 685 | async update_royalties(args: { 686 | royalties: Royalties, 687 | }, options?: ChangeMethodOptions): Promise { 688 | return providers.getTransactionLastResult(await this.update_royaltiesRaw(args, options)); 689 | } 690 | update_royaltiesRaw(args: { 691 | royalties: Royalties, 692 | }, options?: ChangeMethodOptions): Promise { 693 | return this.account.functionCall({contractId: this.contractId, methodName: "update_royalties", args, ...options}); 694 | } 695 | update_royaltiesTx(args: { 696 | royalties: Royalties, 697 | }, options?: ChangeMethodOptions): transactions.Action { 698 | return transactions.functionCall("update_royalties", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 699 | } 700 | async nft_mint_one(args: { 701 | } = {}, options?: ChangeMethodOptions): Promise { 702 | return providers.getTransactionLastResult(await this.nft_mint_oneRaw(args, options)); 703 | } 704 | nft_mint_oneRaw(args: { 705 | } = {}, options?: ChangeMethodOptions): Promise { 706 | return this.account.functionCall({contractId: this.contractId, methodName: "nft_mint_one", args, ...options}); 707 | } 708 | nft_mint_oneTx(args: { 709 | } = {}, options?: ChangeMethodOptions): transactions.Action { 710 | return transactions.functionCall("nft_mint_one", args, options?.gas ?? DEFAULT_FUNCTION_CALL_GAS, options?.attachedDeposit ?? new BN(0)) 711 | } 712 | } 713 | -------------------------------------------------------------------------------- /src/near/index.ts: -------------------------------------------------------------------------------- 1 | import * as naj from "near-api-js" 2 | import settings from "../../config/settings.json" 3 | 4 | const { contractName } = settings 5 | 6 | // TODO: remove pending https://github.com/near/near-api-js/issues/757 7 | import { Buffer } from "buffer" 8 | if (typeof window !== "undefined") window.Buffer = Buffer 9 | if (typeof global !== "undefined") global.Buffer = Buffer 10 | 11 | const nearConfig = /near$/.test(contractName) 12 | ? { 13 | networkId: "mainnet", 14 | nodeUrl: "https://rpc.mainnet.near.org", 15 | walletUrl: "https://wallet.near.org", 16 | helperUrl: "https://helper.mainnet.near.org", 17 | } 18 | : /testnet$/.test(contractName) 19 | ? { 20 | networkId: "testnet", 21 | nodeUrl: "https://rpc.testnet.near.org", 22 | walletUrl: "https://wallet.testnet.near.org", 23 | helperUrl: "https://helper.testnet.near.org", 24 | } 25 | : undefined 26 | 27 | if (!nearConfig) { 28 | throw new Error( 29 | `Don't know what network settings to use for contract "${contractName}"; expected name to end in 'testnet' or 'near'` 30 | ) 31 | } 32 | 33 | /** 34 | * NEAR Config object 35 | */ 36 | export const near = new naj.Near({ 37 | ...nearConfig, 38 | keyStore: typeof window === "undefined" 39 | ? new naj.keyStores.InMemoryKeyStore() 40 | : new naj.keyStores.BrowserLocalStorageKeyStore() 41 | }) 42 | 43 | /** 44 | * Interface to NEAR Wallet 45 | */ 46 | export const wallet = new naj.WalletConnection(near) 47 | 48 | export function signIn() { 49 | wallet.requestSignIn({ contractId: settings.contractName }) 50 | } -------------------------------------------------------------------------------- /src/pages/404.js: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import Layout from "../components/layout" 4 | import Seo from "../components/seo" 5 | 6 | const NotFoundPage = () => ( 7 | 8 | 9 |

404: Not Found

10 |

You just hit a route that doesn't exist... the sadness.

11 |
12 | ) 13 | 14 | export default NotFoundPage 15 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Link, navigate } from "gatsby" 3 | 4 | import useLocales from "../hooks/useLocales" 5 | import Layout from "../components/layout" 6 | 7 | const IndexPage = () => { 8 | const { locales } = useLocales() 9 | 10 | React.useEffect(() => { 11 | if (locales.length === 1) { 12 | navigate(`/${locales[0].id}/`) 13 | } 14 | 15 | const preferredLocale = window.navigator.language 16 | 17 | let matchingLocale = locales.find( 18 | l => l.id.replace("_", "-") === preferredLocale 19 | ) 20 | if (!matchingLocale) 21 | matchingLocale = locales.find( 22 | l => l.id === preferredLocale.replace(/-[A-Z]{2}/, "") 23 | ) 24 | 25 | if (matchingLocale) { 26 | navigate(`/${matchingLocale.id}/`) 27 | } 28 | }, []) 29 | 30 | return ( 31 | 32 | {locales.map(locale => ( 33 |

34 | {locale.viewIn} 35 |

36 | ))} 37 |
38 | ) 39 | } 40 | 41 | export default IndexPage 42 | -------------------------------------------------------------------------------- /src/templates/[locale].tsx: -------------------------------------------------------------------------------- 1 | import settings from "../../config/settings.json" 2 | import React, { useEffect, useState } from "react" 3 | import { PageProps, navigate } from "gatsby" 4 | import * as naj from "near-api-js" 5 | import { near, wallet } from "../near" 6 | 7 | import { fill } from '../../lib/locales/runtimeUtils' 8 | import Hero from "../components/hero" 9 | import MyNFTs from "../components/my-nfts" 10 | import Section from "../components/section" 11 | import Layout from "../components/layout" 12 | import Seo from "../components/seo" 13 | import Markdown from "../components/markdown" 14 | import Image from "../components/image" 15 | import type { DecoratedLocale } from "../../lib/locales" 16 | import useTenk from "../hooks/useTenk" 17 | import useImageData from "../hooks/useImageData" 18 | import useHeroStatuses from '../hooks/useHeroStatuses' 19 | import { Token } from "../near/contracts/tenk" 20 | 21 | type PageContext = { 22 | locale: DecoratedLocale 23 | } 24 | 25 | function hasSuccessValue(obj: {}): obj is { SuccessValue: string } { 26 | return 'SuccessValue' in obj 27 | } 28 | 29 | async function getTokenIDsForTxHash(txHash: string): Promise { 30 | const rpc = new naj.providers.JsonRpcProvider(near.config.nodeUrl) 31 | const tx = await rpc.txStatus(txHash, wallet.getAccountId()) 32 | if (!hasSuccessValue(tx.status)) return undefined 33 | const base64Result = tx.status.SuccessValue 34 | const result = atob(base64Result) 35 | const tokens = JSON.parse(result) as Token[] 36 | return tokens.map(token => token.token_id) 37 | } 38 | 39 | const currentUser = wallet.getAccountId() 40 | 41 | const Landing: React.FC> = ({ location, pageContext: { locale } }) => { 42 | 43 | const tenkData = useTenk() 44 | const { image } = useImageData(settings.image) 45 | 46 | const params = new URLSearchParams(location.search) 47 | const transactionHashes = params.get('transactionHashes') ?? undefined 48 | const [tokensMinted, setTokensMinted] = useState() 49 | const { saleStatus, userStatus } = useHeroStatuses() 50 | 51 | const data = { 52 | ...tenkData, 53 | currentUser, 54 | locale, 55 | saleStatus, 56 | userStatus, 57 | } 58 | 59 | useEffect(() => { 60 | if (!transactionHashes) return 61 | getTokenIDsForTxHash(transactionHashes).then(setTokensMinted) 62 | }, [transactionHashes]) 63 | 64 | return ( 65 | <> 66 | 67 | 74 | 75 | {locale.extraSections?.map((section, i) => ( 76 |
77 | 78 | {section.blocks && ( 79 |
80 | {section.blocks.map(({ linkTo, text, image }, j) => { 81 | const El = linkTo ? 'a' : 'div' 82 | const props = linkTo && { href: linkTo, target: '_blank' } 83 | return ( 84 | 85 | {image && ( 86 |
87 | 88 |
89 | )} 90 | {text && ( 91 |
92 | 93 |
94 | )} 95 |
96 | ) 97 | })} 98 |
99 | )} 100 |
101 | ))} 102 |
103 | {transactionHashes && ( 104 | navigate(`/${locale.id}`)} 106 | highlight={tokensMinted} 107 | /> 108 | )} 109 | 110 | ) 111 | } 112 | 113 | export default Landing 114 | -------------------------------------------------------------------------------- /stale-data-from-build-time.json: -------------------------------------------------------------------------------- 1 | {"saleInfo":{"status":"Presale","presale_start":1652212078574,"sale_start":8640000000000000,"token_final_supply":10000,"price":"2000000000000000000000000"},"contractMetadata":{"spec":"nft-1.0.0","name":"TENK NFT","symbol":"TENK","icon":"data:image/png;base64,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/LtrIX1UIAZtMKJYrOsNNPgfT46XzWv2Oy5kJnS1fIfxXyn8V8p/FfKfxXyn8UfamAidL1j9AiRRlVx9fntPqb+du+vuc47N0P7NOeFC0hrywfcee31RfBa/oPasvntPqb+Nu+ltnaHwhyGfLGms1T7xpvrPOfqcdL4DX7XdgiMFuJlz9tu379j/APlEyCZrX3V8dp9Xfy92pq0O1B0XoAgICkUgF8mSTWNUonJJk1cz7Atqc9T6pvjtfsWq3MAC7QU1IBmr+N/Vfxv6r+N/Vfxv6pVFZCkKabe5vntPqz+Fu+jjHIxDq30OruSvSZZC4mqfl6VazZ2+cqsY9mX1NzUrHj8TkkzPqO4mhF+B1Ks+YqlIPY0PBmo4uLubtZna/P25ZZZSgMMmGYald1NCL8Dqe3K3WxpgBfTKaRBmgzDUqyTaahnakkUuTHcCxof7WtBUz6DEwzP+J0f9KfAJLNbGCKz5ubbq0P8A8C//2gAMAwEAAgADAAAAEAQQQQQQQQQQQQQQQQQQQQQQQQTjDDjjjjjjjjjjjjjjjjjjjjjvfffffffff/OP9OP/AH33333333LLLLLLLedXzzzzyFvPLLLLLLI000000HTzzzzwHvTzkE000003333322XzzxzX78WwPz1333333DDDDqHzzzwXw7vLtjbxFbDDDAEEEerzzzzzSk+6dKfzzx4sEEH/AP8A5vPPPPPAp7Yx1QePPPGv/wD/ACyzY8888888G8Dq888888hzyyNNfc888888+NUB6d88888svNNCCX88884oizJ88Q5888888rCC99788440gaww8EF3888888199BBH888csc88rDXQG888888JBB88q18888888Byes988888xW88zzzL8888888rzzzy88888HzzzCCCW88888/YxCCCTgVD68qCCCNNNMJ84YWsNNNNNNNNNP/MNNNxxxxgRCxxxxxxxxxxxxxxxxxxAAAAAY00//wD/AMjz/iqhPmAAAAD/AP8A/wD/ANuU9oDDlH9onbU8/wD/AP8A/wDvvvvvqFPusMDVvqm2k+PvvvvjDDDDDHTTPTzvLDHrbz/DDDDDMcccccccccccccccccccccccccMMMMMMMMMMMMMMMMMMMMMMMMP/xAApEQADAAAEBAcAAwEAAAAAAAAAAREhMUFhIFFxkRCBobHB0fAwQOHx/9oACAEDAQE/EIQhCEIQhCEIQhCEIQhCEJ/aAAAAQhCEIQhBpBnl54e8Mzi6v6p/3n9GRR9H9wz28sfajSCEIQhCEIQhCEIQo43+7Ck8gvliqLXv34lEW/fuOXyD+GUcT/dyEIQhCEIQhCED5V9NhJiX7PidGRIqVtct3gxA0u73fg01L9kUPlV12IQhCEIQhCGGVsXPd7CSSi4Mj0+Wb7Dw0fWf7TXd8sn2Me3XzYqNEmWIZxp3E9W+j8Gk1GYpG5ct1sQhCEIQhDzHt3y+xEkXDLdvoPom5XLyECWbW8fl/wBHFTSndLD1WJrfuSQsbdN3HMSZNIXqi1kvg0mozAXlbPkQhCEIQzcWZEa4qlYZzN80PnyMNTN1WFuF93ywG1kWKxe0zGwNssGm5PgdYuN/1MQ9Dn04MmNiIcUQhCEIUaWWC+ePLZGMGef0cxCnVc++KKlcZPJ2+w3W3nkK3Yb1WA3xzXkxIcsj68EGlng/ghCEJ4OY8x6vH+B0Mq70WAtViP2WA1M3Odhjl+X/ALx855h1XghCENx2hcafPA3ADHZ4WK8hquaGccvjEUlcmrxM2PbIQhBLrv7cLaSrExJ5NCBcmRXqteo21XPHpxp/HyIQWmHGlR29kQhCGFOj4EjSa1EDWJMRCYr6I7bgJU1YJim3fwYGRI5NWu2Iohng+OW3REIQg9H7J8DGlZaCRNvnwPc6N6jbOg/swoqxaNz2wHoua43o/ZIhCENoGmLFfwMDsELtl5MTDWwzQmaEosnxNxG8DbIQhCFtqsO38DHc3kQhCVlrxW2rw7kIQhCFdsn78TKZ5G0JNQpN4QhCCBmIVyJj2YvU5e6iEqd8I6ZL3IQhCELeoSjVr44Db0F/cPa+pCEIQhCEHVaC7Ja9H9lNWQhOELhGXYGeG/6YAAEIQhB9a6mz6snp6s2vV/Zter+za9X9m16v7FCUaja9X9l/9MaVYIiTAgCtzdWQhCEIQhCEE/GwtaILdG4bglSfsD5PggsYUZpEwC8MEIQhCEIQhBMP7QTAQxKuTw24Irr/AME93wQTEKTobCxpooqyIQhCEIQhCDJBz1k8AbaNtDZzybSCiIYV1M8KaoSRpEIQhCEIQhCEIQhCEIQhCEIQhCf2gAAAH//EACoRAAMAAAIKAgMBAQEAAAAAAAABESExIEFRYXGBobHR8BCRMMHh8UBQ/9oACAECAQE/EKUpSlKUpSlKUpSlKUpSlL8Uv/PSlKUpSlEYmZccse1Mur5eT/MXkz6rl4MuOeHeCMQpSlKUpSlKUpSlMBJHWA/A+rX2+tJ9WLt9CnVLwYqyKUpSlKUpSlKXcXpXuws6vSXlYsizjfpD3N8WdGTcHpfuwpSlKUpSlKYhuOt7P72HjoaxFt1fYp3D7NUi26jh88kPcbaZCSpvodRi4/CcMA3HU9v97lKUpSlKUxhx9Ft8Dd0aBziLVEcM+ZjWSe6rmJTOifZvHpkYwjYc4X0CSrbNMcERbM/hOGMOHqtvkpSlKUo95KH3NekqTxyuSFpL2cDF5O3Y/wA5iQjPB5b6J6kkeNSv9EuCPd7DA3E2cdBdzULeSylKUpSS4p/rTz5RxLLLyLQ6nXwmXPWQba7zUncXqJb70HixEtTMpV5od011NCybxX7KUpSlOPeHBfgQ7OKcXj/o7cMWv2hDRLZ72Ib1++2ns7uPBlKUpTc2n+BrYaujBdxach4PmJfyUfJjzgxrWzTmnvfSKX5Ykrd3Wik24hZGs0Nh7WZNjInhfniBPz+hzLrx02Ou/u9HO4rQaacGsngaGRrmVTeJyOD1o3/XwlIrYsrWst+BZMsVp5XF6P0PutBKmjPWNtpLQSkVLUIm65eDG7i+hjd33x6FhsenJ+56O+VNDU/AgqrYyz5mBjaUa3yY1tmtJKm4VJaWzB4/f4FDUWL0Fdo4ppbMFj9ac9c120kmqtwmMsEXuwYo63oPOUxrM0JZT6G13DEYRI/i++b7aalbZMY98tXD4zE9ULeeFi0K7PxLYtEFtFrXgUqTJfgqWDWTNdn7uMll/wBC6yKbET/heBNy6F4PYl4PYl4PYl4PYl4KDsqXjRcD2JeBoz6F4HNYvPIpWulGz1ImFC14LwL8UEX0d0KEQiuQ3A3Aa0U/1GN8v7IjoD1cze+ZYW8+iEH8n2HdHWfCJxcLeXze+Fi/qM/L+/jpD18xsgbpHht3iNUPbpUpSlKNWQxe07x2lFWHHabx9PBvn08CQ9uP7H7bvHaUYprw2DStxe7BC9oYkVMRHE1w8FKUpf8A0//EACoQAQEBAAICAgIBBQEAAwEBAAERACExQWFRgTBxECCRobHwwUBQ8dHh/9oACAEBAAE/EPvfe+997733vvfe+997733vvfe+997733vvfe+997733vvfe+997733vvfe+997733vvfe+997733vvfe+997733vvfeuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuvvX3r7196+9fevvX3r7196+9fevvX3r7196+9fevvX3r7196+9fevvX3r7196+9fevvX3r7196+9fevvX3r7196+9fevvX3r7196+9fevvX3rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr/C666666/wv8Lrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrne9Vo+AOXGTSLD7qcf0tRWJ93Xp8fO8MMZn2Q/vhys8E/6t7+Pv4rlb5J/wBW8MMYn0R/vgbJv1VO3z8YyaQYfcbj+xne9Vo+EeTXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXdoqNG8nHaY0oDO8PhMRXZwicpxyj1rdEFUBUA5gc+tD8M0uiKqEoI8xefeVwkAF2cqvKccA9aIq5KM5eOkwrBC9666666666666666666666666666666666aBxGmjHiU65URpdHVni/WYHBHApLdJxvG+/xxezeNLxo6s8X7zE4A4MLJpoHUYYFeZDvkRCs111111111111111/MljJxQfpDh/fRXjEDAoUrah2ccq9jkmOuH/H5e29FCuo+B5OXvmDNzRJ1ogEnn4Quf1uXbaffIS/P0OK4fy9cv+MQMChGtoHbzwj2eAYLGXio/SHJ+umPH5rrrrrrrrrrrrk+xsdDyD23jwOe0ohaWHKO0eV4OXwB0BvvRvL+RTiwQaZY9pGOuFeCYtEbBJqq8qvnXDRKAU6IUgXwsCKn+MWfzG8OnvMrSCsF0hyPLyeFOlMG0IRaPAHTePB46WLrrrrrrrrrrrrrrrrrrrrrrriMcijwuz5jvo5rgEhGAiABwAePwr/D184/jr6aK0KHzDMSKwLB0C8wAD0GAkPPjic1UWfB2mGDs0EQAOADxj9/4z/H/dfy5CQjSRER4RPGuIxyKfC7fiuunmOXXXXXXXXXXX8fNI6cJPPovH0Oa4bEQUAcAHgxn+v5285H4DlegcuLCAPgGvk5V+t21HMfsr/Ga+kEm/vw/wAmBZBzyp3wV6nbf60AWy6Lzwk6HsL3iAteYhaPsn3luaDUKgArkUCi/HeX7n+IoWXwAHBwWVdKlky5hVwVQ+9WBLRk8lA9i/xCOhdr28sF7RpJ6JB8icJ/I5zsRBUDwieTcUjpys8ey8fY5j+Nddddddddd3R5APh7U76DnuCOm4cEQA8AG6/DwtRE3wBQCvEnrBghjk8VYHfT8c/NZxV+LIFIHlX5V5xYjbHGhRAvD8DvmWi59u50WVC4JSS8peUcChwicZCc0BUlnw9R5ORvCmqCSUdh4IJPhvxmr8MrSUedvL4cAc7U1jcK1OgegDXXFOoklfigH98XGAQuwadnnhjwne4uSGXoZ2TrtcPhWv8APeXTcOqIieRHdkeQD5ele+k57oXXXXXXXXXXXXXXXXXcx8WgSqHgBXh4MTAoOFB+wz54ADgN1+J587rTqAlxFglEewwyIr+qGl/YwHmV0QiI8hzgh5UAqePzXY6OGYxcVx+kR+nN1siCp7gUzqx7Cc8jDl1YPQPahlcaqleThQ8PAnPL0H6J5FB7OX/GdERGOACoXAUPn5bza0dQ/thOM3+jvIhUPQB/YL88ijwu5v4NQ7EfCInBw66666666666666667oKRFGkftwXLwdUcfjQplmA7XDMEO2DbHPEQOe68wiIqtVe8AMlBGtfHyj7yObkDAq2Ibyc8BazmdZnGg18ReUGuAFCc8BfXyfufrXGfmAT9vkvleXtw0sh6eXWEacN/wBgMMy3KOawAr5tAR5wd+pgPkTh/D2FYihCv25Dk4e6muuuuuuuv4ADFRW5AXijyuOxlHWACGnv8Y0gBUEBXyCPcXkUZ+T5qy6LCcKPfTrA8rU0E8gCvxwefOniIdICGbIBL41xFEi48euXl4cARyYK1S6A7V6A/QadRL1TwcPyT4JTpFNE5gX8IUBva9aFfrQOFK8BeAADEVnmtAo6QLKBwy/yWMYRwicfRKQCxMDh5RAdU45uLCcUKX+qe8gkecxUVuUV5p8rjtZT3+ABddddddcZECAoztlgrHp/IPJNFEHhejrVU5C9sAgBlDUaNfh508jq9K9kW4u8KFkXFoVwKjztNaFWvRXSq7r2997mDNto+RBYDXsMx2SkDr8grX1/bw8Ovg4Mg1woq/JUWhMYqOrxguAdBK8cOsjU/Pe6dV5cTu5hhXjPyvkSieRcFYjwYHYn9yhRGFxz3x/WREDEVJ2yxFh24uuuuuuv9aJh4BByheXQXjkYQD4/N9Gdj1QUpeeDinSn8dD6mHzCSCIcOiVUCbBSo/eMYClKDOF/5Mc/uZrjyj0js6St7cZ/9sooidibrPW4S9KRSPIQMsfI7p7+B46ClHm0HMMR1SkMpwxLBUKN6on9YomQOHIIeUDy7G88r+u666665sYDxAD1Ir93vf54+EkDzI988P17y8QRCGlIRUKSebmMiRape4ryvisIdBuQdz8D30maIpCLUXq5Z25hD0NxGvB5bE/U+spdZ0vASgHHB2/f5feDGQ8UB9QCfc666666/wBdUdH7R36B+jE/OU6PfyRf/LjJdAZTkGpwVjy4QlTTiDuPdk/Z/qNWnilwQv8Ag8mNOXmFyJ/uSxigkTRU8HK38ty+JNDY37B9/wBd11113ahh2jf1T9bofmM4AycEnAyRo+CIJLMQAACQ8Y0MWGuAUPSHQLA4KKDGdqFVAhHfFyz11UEyd+B5xWtVLlkA3JTy684RLj8nR3ahh2Hf3D9666666+9fevvX3r7xQQ++Dwn2X6/PXZyFZ03ivJQTqvEkWd5QA8PZGgygyxkZeAFSyUwUFQg88B5AHvnZDs6fhEwTXScQwBxwWgIHeE7vMnbxen77eXQoyIFvz5dfc13CNgdQ+z+2s/KWFGzwHG/bfvX3r7196+9ff9VlQZPY9fUf3/GvF1LcJEIPyIsbB8PWIEGk2jj/ABBw8sBouYMUIBJZAaBhRQRBMbCgiTsFg+a+N/aUvffkl5mNKiCQFA+QIfCG4P1/IQh/Ez85p/AmvhFvyO4l7vFxEclCK1YnM/LZVWT0HX3X9v6rr7111wVnKXyg3/T8Xd/hwBl9gfeCPioalGj/AJHpEpn0MKsEErUCK3srzvHGAxzUiHPKgHyq0DAhI7qKFE67nPf6DIIDyHOcCh2JCeRImiHjJjKaC0UonCc1KOphZScgJ0R4rRE/oubaFJ5F0/cf2c+F4gQqDpeJ+/2vP5WjOUvhRn+2uuuuuuuuuuiRyB9Ev9v4agOFOvFIiNcHyCwAA9QHhRBBvIaHKKGInMkKHFAKQC8oFVq8qD0QpIh2wnPHL5klcw9zXsQaZWDBeN4roOAlOUj+xT4d4zcKS2icZWUynlKDHM757PYp9DPyqDwPdC1WL2FV/o5z3jECAtZdhXivccIf7FiaH4OOV4PO7Py1I4A+m3+jXXXXXXXXXXF3rPnwQ/3/AKkEEBQSvrnjNpbLWvJhFhiCWPI624/hm3wQmQ5OQnINEQRDCwAo8/I0043nfeTnICBq1Yi+Ly42Onn+fI5zOYMs+Q8CNXB7UEPSMGI9VVJ8gH48Z/CCB0VKfLIhAgLjWBOjpRF3aOR4GB9l/Kne8efBH/XXXXXXX3r7196+9feX9n+vYLNNJXwXxgEBC0ZBlA8adBw+CWJSKfPseSnmmAOif1PBdy7GFeffhRBheLoFALACfChocUYBA5mC9CAg88FH0D8JjU1JxpAg7UOKPOvvL3kHw5E9lzL7y5iaQn/mr+Vf2d33r7196+9feuuuuuAyT4w5fsP77+CKPP4mE3LrcIjwj8OIybUVL3u3uhHSgKajo0/yPyPI8O9aqJbPmAOVXwPz8Z9zVozFDyI8I42ETfOlNRaAimmXTwBL2Qf3T8rGQfGHL9p/bNdddddfevvX3r7196eZ+8g37en/AMB/X8ARoCqWD00AnoKCuYbqG+SZsHiiPXE5px//ANSkv9chPyNMfB+4EPvKQkySaqvKr53PfU4PRdOKU95ki19RVEfpH3j+sC9BQqvK5byv5b5n7wDfo7fevvX3r719666666ZmYKTUPihlxRTQ1D0iP/wD41wLGTjqy3qs8o4hzw/lv34GSXyvdwL9RtprFMq3XXXXXNzGKT8g+IKYGqegF1zMwQugfFXXXXXXXXXXXXQDIWkBKCdwF+bu/wA/nS4H9ZBljXLOhDymuuuuuuDcJA77cD2oZ7HyoDg4RDofMnP4vGgGQsIS0N6oL8zXXXXXXV1dXV1dXSMAJrhVAcFbVnQ+D/4FDUUvxJyHYD0eZ2O9XV1dXV1dXNfyG2MJIjhEQ8MLEvY+TV1dXV1dXXXXXXXPeBOqFKwRoonkUzqm8IphTwx6/HWmblLhT+1OwRhT+Fmu2xjWhwA5KAfkTKkI1VquuuuuuuuN/U4sZRvjOO+8A6KSl8Fu/sM3MVcFgVCr0JzrqY7zj9f0OqbwqOUPLDrPeJOKlKUVggHgA1111111111111wdMp+fAj2iq9OPxjPimUQVDnm9F+DG6ABmqAFTHgEHZyVWS3AG/o/3ZVMjv1G0BIaqHFLrrrrrrrrrrrrjqxG9+1FyiSGpF6Wv+XvPTrJ82t/Q6k95pPiCn47e1IJAAMqTmTsOzeP5DrBM68gPaCJ24y66666666666665UeHijCn0ieeeOdMnzUWu4KCVF4RPG4/p8Z/euPX/APMX0XcXzXHFQR1YiqQEVuuuuuuuuuuuuuuuuuuuuKAlKPSBTI4YBT95jhxok8gVw8R2Ap12NtQY6ogpAXlQ85WeHiBAH0AeeOeddddddddfevvX3r7196+9fevvM/MYsq3nzOAF6EObuP6GEgMndquA/e4zBnEUQQq4EUQkw89S2D4Vf3T9YBzQkiI9id8c+dfevvX3r7196+9fevvX3r7196+9fevvX3r7196+9fevvX3r7196+9EsY9wAuBAlwll5196+9fevvX3r7196+9ddddddddcgIMHBGAR/tHBDwBoRC3Vx7CPwjcqAvXnf8Ir6xbnYovk3H9Ia8uBQb2UYejXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/7O6666666667+xMm+YL4dnevCvjivj/MzVtW1bVtW1bVtWzIekh/hgyeddWDmUN5TlARFQ0i8wbpW1fzmb/pMgYOdvnJwh+7KThvXwZX4/wB/xQ8ZPPeuuQmIxVADlVYGGmCHJ8JEeUJeqc5vWOAD9FKsAKqgCuBBxHVeEASSgs+XvXXXXXXXXX8cOeDAfKuf+N/3v+l/9z/RvC5OrP04YLKEAO1d/wBr/wC7/kf/AHf8j/7v+R/93/I/+7/kf/d/yP8A7v8Akf8A3f8AI/8AuLD1JA7IO7hYxQLuIqo9g9gBAgbub4YfsXf8j/7v+R/9yoGEJV6Aun8fz/MG5VAqu5zwdIOh4Q8uwYmA7tNFVR4ABVcyIaw9FZ12A8xVi/jLrrrrrrrrrrrrnf0fwK7rrkxTDkgqQliM9n9Vbdu3bgTBTtCgQFgs9OlScnfHOflWKe1fLrrngTxnP4vo4Ze4hkPED4L2ux6PhvjfK62i4BOwcPSLyC111111111111111111111111zr+v8AAbuuu55+Hxp609aejT1pNq6/ph6111zwJ0Zz+D2OO/QuMGdzov7v2XpvrPOACJGIOkj5AHUZddddddddddddddddddddddddf5o3uuv8f+UZohboh6Xk5n9Nas2bJg4ASQoJH4+N30etddctmdZfweMpoaziIT4SV+QTi0EmBACAfGCqFxfgHZTr9J4cyPlBAfoC/pFERRhKBgr+AeB1Rj5SLdddddddddddddddddddddddd2/T+AfdddNa1LzIpOK57zHHG8RwiDvhOREESIgiJllQPRx/0a9evyrCJUOAc07tg9a6657Mz/gfo1KDqBXoOl+Xj0Q0P6qIwt7SuXm2IgEgn7cDD9goj5EfOWBTy6XgHk68h5EE74gJVXgOSPPYiIIhddddddddddddddddddddddc1/wDk74J5p3uqoBaxUBVCqGuuurl8HK6eHhEURoiiIpubpzHA5CkXhAAKv6ffffeuFxQOabvgt5y3/wCTvgnmne48t113PXTlWICsB4C40vQnsiHEZ1ySc75Jv8xlQe/Dn68ggh5LPkKsOBQn1iHLf6/RwHBruHEWwB6fgqxor2KDcGhMlaNHTFPhdObtPE7jfEDORjGRprrrrrrrrr/9nfevvX3r7196+9fevvX3r7196+9fevvX3r7196+9feuvvX3r7196+9fevvX3r7196+9fevvX3r7196+9fevvX3r7196+9fevvX3r7196+9feuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuurq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq7/2Q==","base_uri":"https://bafybeihmtke7glg2aec5oav5btzlv6ec4fxkbbh4xjre4x5ipaqdxroahe.ipfs.dweb.link","reference":null,"reference_hash":null},"tokensLeft":9905,"vip":false,"nfts":[],"mintRateLimit":10} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "module": "commonjs", 5 | "lib": [ "es2020", "ESNext", "DOM" ], 6 | "allowJs": true, 7 | "jsx": "react", 8 | "esModuleInterop": true, 9 | "resolveJsonModule": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "incremental": true, 12 | "declaration": true, 13 | "sourceMap": true, 14 | /* Strict Type-Checking Options */ 15 | "strict": true, /* Enable all strict type-checking options. */ 16 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 17 | "strictNullChecks": true, /* Enable strict null checks. */ 18 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 19 | } 20 | } --------------------------------------------------------------------------------