copied from other repo

This commit is contained in:
Moon Man 2025-01-18 07:33:45 -05:00
commit a19f19a96c
102 changed files with 52148 additions and 0 deletions

3
.browserslistrc Normal file
View File

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

14
.eslintrc.js Normal file
View File

@ -0,0 +1,14 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: ["plugin:vue/essential", "eslint:recommended", "@vue/prettier"],
parserOptions: {
parser: "babel-eslint",
},
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
},
};

22
.gitignore vendored Normal file
View File

@ -0,0 +1,22 @@
.DS_Store
node_modules
/dist
*.bak
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

30
README.md Normal file
View File

@ -0,0 +1,30 @@
# nftfactory
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
Serve a rinkeby-specific version of the codebase (pulls vars from .env.test):
```
npm run serve --mode test
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

4
babel.config.js Normal file
View File

@ -0,0 +1,4 @@
module.exports = {
plugins: ["@babel/plugin-proposal-nullish-coalescing-operator", "@babel/plugin-proposal-optional-chaining"],
presets: ["@vue/cli-plugin-babel/preset"],
};

10
env.example Normal file
View File

@ -0,0 +1,10 @@
# Copy this to .env.{environment} with public info and tailor to need.
VUE_APP_FACTORY=0x80Fb340938514915825dF020332a30A922f8dB06
VUE_APP_TOKEN=0x0826180A4c981d5095Cb5c48BB2A098A44cf6f73
VUE_APP_API_BASE="https://token.gallery"
VUE_APP_WEB3_API="wss://rpc.token.gallery"
VUE_APP_IPFS_PREFIX="https://token.gallery/ipfs/"
VUE_APP_CHAIN_ID=8
VUE_APP_CHAIN_NAME=ubiq-fork
VUE_APP_SENTRY_DSN=
VUE_APP_GTAG_ID=

19
getabi.js Normal file
View File

@ -0,0 +1,19 @@
const fs = require("fs");
(async () => {
const storeStr = await fs.promises.readFile("public/ERC1155MoonStore.json");
const storeJson = JSON.parse(storeStr);
const storeAbi = storeJson.abi;
const storeOutFile = "public/store.abi.json";
console.log(`Writing store ABI to file: ${storeOutFile}`);
await fs.promises.writeFile(storeOutFile, JSON.stringify(storeAbi));
const factoryStr = await fs.promises.readFile("public/ERC1155ContractFactory.json");
const factoryJson = JSON.parse(factoryStr);
const factoryAbi = factoryJson.abi;
const factoryOutFile = "public/factory.abi.json";
console.log(`Writing factory abi to file: ${factoryOutFile}`);
await fs.promises.writeFile(factoryOutFile, JSON.stringify(factoryAbi));
})();

36555
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

53
package.json Normal file
View File

@ -0,0 +1,53 @@
{
"name": "nftfactory",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8",
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
"@metamask/detect-provider": "^1.2.0",
"@vue/cli": "^4.5.13",
"basiclightbox": "^5.0.3",
"bignumber.js": "^9.0.0",
"core-js": "^3.6.4",
"dayjs": "^1.8.36",
"idle-vue": "^2.0.5",
"localforage": "^1.9.0",
"lodash.debounce": "^4.0.8",
"micromodal": "^0.4.6",
"vue": "^2.6.14",
"vue-async-computed": "^3.9.0",
"vue-gtag": "^1.11.0",
"vue-localforage": "^0.2.5",
"vue-meta": "^2.4.0",
"vue-router": "^3.1.6",
"vuex": "^3.1.3",
"vuex-map-fields": "^1.4.1",
"vuex-persistedstate": "^4.0.0",
"web3": "^1.5.0",
"web3-eth": "^1.5.0",
"web3-eth-personal": "^1.5.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.5.13",
"@vue/cli-plugin-eslint": "^4.5.13",
"@vue/cli-plugin-router": "^4.5.13",
"@vue/cli-plugin-vuex": "^4.5.13",
"@vue/cli-service": "^4.5.13",
"@vue/eslint-config-prettier": "^6.0.0",
"babel-eslint": "^10.1.0",
"dotenv-webpack": "^2.0.0",
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-vue": "^6.2.2",
"prettier": "^1.19.1",
"vue-svg-loader": "^0.16.0",
"vue-template-compiler": "^2.6.14",
"webpack-chain": "^6.5.1"
}
}

8
prettier.config.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
printWidth: 180,
trailingComma: "es5",
tabWidth: 2,
semi: true,
jsxBracketSameLine: false,
arrowParens: "always",
};

370
public/10grans.abi.json Normal file
View File

@ -0,0 +1,370 @@
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "tokenOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"stateMutability": "nonpayable",
"type": "fallback"
},
{
"inputs": [],
"name": "_totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "contractOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "decimals",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "address",
"name": "newContractOwner",
"type": "address"
}
],
"name": "setContractOwner",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenOwner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "balance",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenOwner",
"type": "address"
},
{
"internalType": "address",
"name": "spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"internalType": "uint256",
"name": "remaining",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "approveAndCall",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "recipient",
"type": "address"
},
{
"name": "value",
"type": "uint256"
}
],
"name": "mintToken",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenAddress",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
}
],
"name": "transferAnyERC20Token",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]

BIN
public/10grans.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

79
public/18plus.svg Normal file
View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="162.59882mm"
height="162.59882mm"
viewBox="0 0 162.59882 162.59882"
version="1.1"
id="svg8"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="18plus.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="-199.57703"
inkscape:cy="308.87066"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1055"
inkscape:window-x="1920"
inkscape:window-y="1080"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-23.70059,-67.20059)">
<circle
style="fill:#ffffff;fill-opacity:1;stroke:#be0000;stroke-width:16.70000112;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;stroke-dashoffset:0"
id="path815"
cx="105"
cy="148.5"
r="72.949409" />
<g
aria-label="18+"
transform="matrix(0.48091573,0,0,0.48091573,570.92145,-59.714867)"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:136.06298828px;line-height:1.25;font-family:'Arial Black';-inkscape-font-specification:'Arial Black, Bold';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-opacity:1"
id="flowRoot817">
<path
sodipodi:type="inkscape:offset"
inkscape:radius="0"
inkscape:original="M -1061.2012 382.5957 C -1064.0802 388.70791 -1068.0667 393.84638 -1073.1602 398.00977 C -1078.2537 402.17315 -1085.4065 405.73756 -1094.6191 408.70508 L -1094.6191 430.89453 C -1088.374 428.99 -1083.1933 426.99771 -1079.0742 424.91602 C -1074.9108 422.83432 -1070.6147 420.11028 -1066.1855 416.74414 L -1066.1855 481.65234 L -1038.8125 481.65234 L -1038.8125 382.5957 L -1061.2012 382.5957 z M -971.04688 382.5957 C -980.87955 382.5957 -988.7863 384.588 -994.76562 388.57422 C -1002.428 393.75631 -1006.2578 400.42266 -1006.2578 408.57227 C -1006.2578 413.5329 -1004.929 417.91751 -1002.2715 421.72656 C -1000.3227 424.51692 -997.22268 427.04192 -992.9707 429.30078 C -998.50714 431.47105 -1002.6043 434.54894 -1005.2617 438.53516 C -1007.9192 442.47709 -1009.248 447.30595 -1009.248 453.01953 C -1009.248 458.86599 -1007.8308 464.15731 -1004.9961 468.89648 C -1002.1615 473.63565 -998.02012 477.22413 -992.57227 479.66016 C -987.12443 482.09618 -979.24176 483.31445 -968.92188 483.31445 C -963.42975 483.31445 -958.26917 482.75963 -953.44141 481.65234 C -948.56936 480.58935 -944.51647 478.90666 -941.2832 476.60352 C -938.00564 474.30036 -935.25949 471.04556 -933.04492 466.83789 C -930.78607 462.58592 -929.65625 458.0244 -929.65625 453.15234 C -929.65625 448.10314 -930.96299 443.49542 -933.57617 439.33203 C -936.14507 435.16864 -940.06527 431.82539 -945.33594 429.30078 C -941.34972 426.6433 -938.42665 423.89715 -936.56641 421.0625 C -934.08609 417.25345 -932.8457 413.15633 -932.8457 408.77148 C -932.8457 401.24196 -935.96781 394.99774 -942.21289 390.03711 C -948.41368 385.07648 -958.02522 382.5957 -971.04688 382.5957 z M -890.06055 396.67969 L -890.06055 422.25781 L -915.50586 422.25781 L -915.50586 443.78516 L -890.06055 443.78516 L -890.06055 469.23047 L -868.60156 469.23047 L -868.60156 443.78516 L -843.02344 443.78516 L -843.02344 422.25781 L -868.60156 422.25781 L -868.60156 396.67969 L -890.06055 396.67969 z M -969.71875 399.07227 C -966.48548 399.07227 -963.82779 400.13573 -961.74609 402.26172 C -959.6644 404.34341 -958.62305 407.06745 -958.62305 410.43359 C -958.62305 413.66686 -959.62017 416.30049 -961.61328 418.33789 C -963.60639 420.3753 -966.15351 421.39453 -969.25391 421.39453 C -972.75292 421.39453 -975.56542 420.3753 -977.69141 418.33789 C -979.7731 416.2562 -980.8125 413.53411 -980.8125 410.16797 C -980.8125 406.89041 -979.79521 404.23271 -977.75781 402.19531 C -975.67612 400.11362 -972.99631 399.07227 -969.71875 399.07227 z M -969.71875 437.33984 C -966.30832 437.33984 -963.3189 438.71292 -960.75 441.45898 C -958.13681 444.20505 -956.83008 447.72522 -956.83008 452.02148 C -956.83008 456.27345 -958.11469 459.79558 -960.68359 462.58594 C -963.25249 465.332 -966.15345 466.70508 -969.38672 466.70508 C -972.75286 466.70508 -975.74228 465.35411 -978.35547 462.65234 C -980.96866 459.90628 -982.27539 456.38415 -982.27539 452.08789 C -982.27539 447.57018 -981.035 443.98365 -978.55469 441.32617 C -976.03008 438.66869 -973.08489 437.33984 -969.71875 437.33984 z "
id="path877"
d="m -1061.2012,382.5957 c -2.879,6.11221 -6.8655,11.25068 -11.959,15.41407 -5.0935,4.16338 -12.2463,7.72779 -21.4589,10.69531 v 22.18945 c 6.2451,-1.90453 11.4258,-3.89682 15.5449,-5.97851 4.1634,-2.0817 8.4595,-4.80574 12.8887,-8.17188 v 64.9082 h 27.373 V 382.5957 Z m 90.15432,0 c -9.83267,0 -17.73942,1.9923 -23.71874,5.97852 -7.66238,5.18209 -11.49218,11.84844 -11.49218,19.99805 0,4.96063 1.3288,9.34524 3.9863,13.15429 1.9488,2.79036 5.04882,5.31536 9.3008,7.57422 -5.53644,2.17027 -9.6336,5.24816 -12.291,9.23438 -2.6575,3.94193 -3.9863,8.77079 -3.9863,14.48437 0,5.84646 1.4172,11.13778 4.2519,15.87695 2.8346,4.73917 6.97598,8.32765 12.42383,10.76368 5.44784,2.43602 13.33051,3.65429 23.65039,3.65429 5.49213,0 10.65271,-0.55482 15.48047,-1.66211 4.87205,-1.06299 8.92494,-2.74568 12.15821,-5.04882 3.27756,-2.30316 6.02371,-5.55796 8.23828,-9.76563 2.25885,-4.25197 3.38867,-8.81349 3.38867,-13.68555 0,-5.0492 -1.30674,-9.65692 -3.91992,-13.82031 -2.5689,-4.16339 -6.4891,-7.50664 -11.75977,-10.03125 3.98622,-2.65748 6.90929,-5.40363 8.76953,-8.23828 2.48032,-3.80905 3.72071,-7.90617 3.72071,-12.29102 0,-7.52952 -3.12211,-13.77374 -9.36719,-18.73437 -6.20079,-4.96063 -15.81233,-7.44141 -28.83399,-7.44141 z m 80.98633,14.08399 v 25.57812 h -25.44531 v 21.52735 h 25.44531 v 25.44531 h 21.45899 v -25.44531 h 25.57812 v -21.52735 h -25.57812 v -25.57812 z m -79.6582,2.39258 c 3.23327,0 5.89096,1.06346 7.97266,3.18945 2.08169,2.08169 3.12304,4.80573 3.12304,8.17187 0,3.23327 -0.99712,5.8669 -2.99023,7.9043 -1.99311,2.03741 -4.54023,3.05664 -7.64063,3.05664 -3.49901,0 -6.31151,-1.01923 -8.4375,-3.05664 -2.08169,-2.08169 -3.12109,-4.80378 -3.12109,-8.16992 0,-3.27756 1.01729,-5.93526 3.05469,-7.97266 2.08169,-2.08169 4.7615,-3.12304 8.03906,-3.12304 z m 0,38.26757 c 3.41043,0 6.39985,1.37308 8.96875,4.11914 2.61319,2.74607 3.91992,6.26624 3.91992,10.5625 0,4.25197 -1.28461,7.7741 -3.85351,10.56446 -2.5689,2.74606 -5.46986,4.11914 -8.70313,4.11914 -3.36614,0 -6.35556,-1.35097 -8.96875,-4.05274 -2.61319,-2.74606 -3.91992,-6.26819 -3.91992,-10.56445 0,-4.51771 1.24039,-8.10424 3.7207,-10.76172 2.52461,-2.65748 5.4698,-3.98633 8.83594,-3.98633 z" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
public/1x1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 B

BIN
public/about/collect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
public/about/create.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
public/about/header.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
public/about/sell.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
public/christian.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
public/deck.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
public/dropzone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
public/error.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

494
public/factory.abi.json Normal file
View File

@ -0,0 +1,494 @@
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "BridgeTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "ManagerTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "store",
"type": "address"
},
{
"indexed": true,
"internalType": "uint24",
"name": "id",
"type": "uint24"
}
],
"name": "NewStore",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "OracleTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "PayoutTransferred",
"type": "event"
},
{
"inputs": [],
"name": "bridge",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "diamondCut",
"outputs": [
{
"internalType": "address",
"name": "facetAddress",
"type": "address"
},
{
"internalType": "enum IDiamondCut.FacetCutAction",
"name": "action",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "initParams",
"outputs": [
{
"internalType": "address payable",
"name": "creator",
"type": "address"
},
{
"internalType": "contract IMotherShip",
"name": "mothership",
"type": "address"
},
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "bool",
"name": "isERC1363Token",
"type": "bool"
},
{
"internalType": "uint8",
"name": "fee",
"type": "uint8"
},
{
"internalType": "uint8",
"name": "royalty",
"type": "uint8"
},
{
"internalType": "string",
"name": "storeName",
"type": "string"
},
{
"internalType": "string",
"name": "storeSymbol",
"type": "string"
},
{
"internalType": "string",
"name": "baseURI",
"type": "string"
},
{
"internalType": "address",
"name": "proxyRegistryAddress",
"type": "address"
},
{
"internalType": "uint24",
"name": "storeId",
"type": "uint24"
},
{
"internalType": "bool",
"name": "sendRightAway",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "manager",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "oracle",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "payout",
"outputs": [
{
"internalType": "address payable",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "facetAddress",
"type": "address"
},
{
"internalType": "enum IDiamondCut.FacetCutAction",
"name": "action",
"type": "uint8"
},
{
"internalType": "bytes4[]",
"name": "functionSelectors",
"type": "bytes4[]"
}
],
"internalType": "struct IDiamondCut.FacetCut[]",
"name": "_diamondCut",
"type": "tuple[]"
},
{
"components": [
{
"internalType": "address payable",
"name": "creator",
"type": "address"
},
{
"internalType": "contract IMotherShip",
"name": "mothership",
"type": "address"
},
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "bool",
"name": "isERC1363Token",
"type": "bool"
},
{
"internalType": "uint8",
"name": "fee",
"type": "uint8"
},
{
"internalType": "uint8",
"name": "royalty",
"type": "uint8"
},
{
"internalType": "string",
"name": "storeName",
"type": "string"
},
{
"internalType": "string",
"name": "storeSymbol",
"type": "string"
},
{
"internalType": "string",
"name": "baseURI",
"type": "string"
},
{
"internalType": "address",
"name": "proxyRegistryAddress",
"type": "address"
},
{
"internalType": "uint24",
"name": "storeId",
"type": "uint24"
},
{
"internalType": "bool",
"name": "sendRightAway",
"type": "bool"
}
],
"internalType": "struct DiamondERC1155.InitializationParameters",
"name": "_initParams",
"type": "tuple"
}
],
"name": "initialize",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "_name",
"type": "string"
},
{
"internalType": "string",
"name": "_symbol",
"type": "string"
}
],
"name": "newStore",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "_name",
"type": "string"
},
{
"internalType": "string",
"name": "_symbol",
"type": "string"
},
{
"internalType": "string",
"name": "_metadataURI",
"type": "string"
},
{
"internalType": "address",
"name": "_customToken",
"type": "address"
},
{
"internalType": "bool",
"name": "_isERC1363",
"type": "bool"
}
],
"name": "newStore",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_manager",
"type": "address"
}
],
"name": "setManager",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_bridge",
"type": "address"
}
],
"name": "setBridge",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_oracle",
"type": "address"
}
],
"name": "setOracle",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address payable",
"name": "_payout",
"type": "address"
}
],
"name": "setPayout",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/flame.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
public/hourglass.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

28
public/index.html Normal file
View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="theme-color" content="#5F2EEA">
<meta name="title" content="token gallery">
<meta name="description" content="nfts and digital asset accessories">
<meta property="og:title" content="token gallery">
<meta property="og:description" content="nfts and digital asset accessories">
<meta property="og:image" content="https://token.gallery/logo-icon.png">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;800&display=swap" rel="stylesheet">
<title>token gallery</title>
</head>
<body>
<noscript>
<strong>token gallery doesn't work properly without javascript enabled. please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

BIN
public/logo-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
public/nft-bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 KiB

BIN
public/question.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

BIN
public/r18.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

1460
public/store.abi.json Normal file

File diff suppressed because it is too large Load Diff

BIN
public/think.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

344
public/token.abi.json Normal file
View File

@ -0,0 +1,344 @@
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "tokenOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"stateMutability": "nonpayable",
"type": "fallback"
},
{
"inputs": [],
"name": "_totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "contractOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newContractOwner",
"type": "address"
}
],
"name": "setContractOwner",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenOwner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "balance",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenOwner",
"type": "address"
},
{
"internalType": "address",
"name": "spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"internalType": "uint256",
"name": "remaining",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "approveAndCall",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenAddress",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
}
],
"name": "transferAnyERC20Token",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]

BIN
public/ubiq-bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 866 KiB

BIN
public/ubiq.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

843
src/App.vue Normal file
View File

@ -0,0 +1,843 @@
<template>
<div id="app" class="app">
<div v-if="showModal" id="overlay" />
<nav>
<p class="title button">
<router-link to="/">
<Logo alt="token.gallery" />
<Beta alt="beta" class="beta" />
</router-link>
</p>
<div class="nav-links">
<router-link to="/about" class="button button-round-plain">About</router-link>
<template v-if="userAuthenticated && metaMaskConnected && username">
<router-link to="/createnft" class="button button-round-accent">Create</router-link>
<button @click="toggleNotifications" class="notification-icon">
<BellIcon class="bell" alt="Show/Hide Notifications" />
<div v-if="newNotificationAvailable" class="red-circle" />
</button>
<div v-if="showNotifications" class="notifications-modal-container">
<aside class="notifications-modal">
<h1>Notifications</h1>
<Notifications class="light" />
</aside>
</div>
<router-link :to="{ name: 'Address', params: { address: username } }" class="button user-button">
<span class="user-letter">{{ username[0].toUpperCase() }}</span>
</router-link>
</template>
<span v-else>
<a href="#" @click="openModal" class="button button-round-outline">Connect Wallet</a>
</span>
</div>
</nav>
<main>
<div v-if="showModal" id="modal-login">
<div class="close-icon-container">
<BlackXIcon class="close-icon" alt="Close wallet connect modal" @click="closeModal(0)" />
</div>
<section class="modal-inner">
<h1>Connect your wallet</h1>
<div v-if="error">
<p class="error">{{ this.error }}</p>
<br />
</div>
<div v-if="!metaMaskPresent" class="flex">
<div class="message">
<p>Token Gallery uses the <strong>Ubiq</strong> blockchain.</p>
<p>
Install <a href="https://metamask.io/" target="_blank"><strong>Metamask</strong></a> or
<a href="https://ubiqsmart.com/sparrow" target="_blank"><strong>Sparrow</strong></a> wallet to log in.
</p>
<p class="metamask-warning">(Do not install Metamask and Sparrow in the same browser.)</p>
</div>
</div>
<div v-else-if="!chainIdCorrect" class="flex">
<p>Token Gallery uses the <strong>Ubiq</strong> blockchain.</p>
<div class="message">
<p>Click here to switch to the Ubiq chain:</p>
</div>
<button class="button-round-accent" @click="switchChain">Switch Network</button>
</div>
<div v-else-if="!metaMaskConnected" class="flex">
<div class="message">
<p>Click here to connect your wallet</p>
</div>
<button class="button-round-accent" @click="connect">Connect</button>
</div>
<div v-else-if="hasUser">
<div v-if="userAuthenticated" class="flex">
<div class="message">
<p>Logged in!</p>
</div>
</div>
<div v-else class="flex">
<div class="message">
<p>Welcome back!</p>
<p>Sign a message to log in.</p>
</div>
<button class="button-round-accent" id="login" @click="login()">Sign</button>
<button class="button-round-gray" id="login" @click="closeModal(0)">Cancel</button>
</div>
</div>
<div v-else class="user-register flex">
<div class="message">
<p>Welcome! Register your address {{ this.accounts[0].substring(0, 5) + "..." + this.accounts[0].substring(39) }}.</p>
<p>Enter a username:</p>
</div>
<form>
<input type="text" maxlength="40" id="username" v-model="requestedUsername" />
<button type="button" class="button-round-accent" @click="register">Register</button>
</form>
</div>
<div class="help">
<p>By registering, you agree to our <br /><router-link to="/privacy">Privacy Policy</router-link> and <router-link to="/terms">Terms of Service</router-link>.</p>
<p>Confused? Check out our <router-link @click.native="closeModal(0)" to="/about">about page</router-link></p>
</div>
</section>
</div>
<transition name="fade">
<router-view @connect-wallet="openModal" />
</transition>
</main>
<footer>
<div class="footer-left">
<p class="title"><router-link to="/">token.gallery</router-link></p>
<p><a href="https://twitter.com/tokengallery">Twitter</a></p>
<p><a href="https://discord.gg/2vq4WcD">Discord</a></p>
<p><a href="mailto:nfttokengallery@gmail.com">Support</a></p>
</div>
<div class="footer-right">
<p><router-link to="/terms">Terms of Service</router-link></p>
<p><router-link to="/privacy">Privacy</router-link></p>
<p>
<a href="https://github.com/robekworld/nft-license/" target="_blank">NFT License <ExternalLinkIcon class="external-link-icon"/></a>
</p>
<p><router-link to="/about">About</router-link></p>
</div>
</footer>
</div>
</template>
<style>
:root {
background-color: #000808;
}
/*
Optionally transition on every page?
.fade-enter-active, .fade-leave-active {
transition-property: opacity;
transition-duration: .125s;
}
.fade-enter-active {
transition-delay: .125s;
}
.fade-enter, .fade-leave-active {
opacity: 0
}
*/
.app {
color: var(--white);
font-family: Poppins, Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
display: flex;
flex-direction: column;
min-height: 100vh;
}
.app a {
color: var(--white);
}
.app a:active {
color: var(--white);
}
.modal {
text-shadow: none;
}
.app nav {
display: flex;
justify-content: space-between;
align-items: center;
min-height: 80px;
padding: 0 24px;
}
.app nav .title a {
display: inline-flex;
text-decoration: none;
}
.beta {
margin-left: 16px;
}
.app nav .notification-icon {
background-color: #292f2f;
font-weight: initial;
border-radius: 100%;
margin-right: 0;
}
.app nav .notification-icon .bell {
position: fixed;
transform: translate(-50%, -50%);
}
.app nav .notification-icon .red-circle {
background-color: var(--red);
width: 12px;
height: 12px;
border-radius: 100%;
position: fixed;
transform: translate(90%, -190%);
}
.app nav a.user-button {
background-color: #292f2f;
font-weight: initial;
border-radius: 100%;
margin-right: 0;
}
.app nav .nav-links {
display: flex;
}
.app nav .nav-links button,
.app nav .nav-links .button {
max-height: 48px;
}
.app nav .nav-links > * {
transform: scale(1); /* hack to get childrens position:fixed to treat this element as the parent */
z-index: 2;
}
.app nav .nav-links .user-button {
display: flex;
align-items: center;
padding: 20px;
height: 8px;
width: 8px;
}
.app nav .nav-links .user-letter {
position: relative;
left: -1px;
color: var(--white);
}
.app main {
width: 100%;
flex: 1;
}
#modal-login {
z-index: 20;
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
width: 530px;
max-width: 80%;
border-radius: 8px;
background: var(--white);
color: var(--card-background);
text-align: center;
}
#modal-login .flex {
display: flex;
flex-direction: column;
}
#modal-login .close-icon-container {
text-align: right;
margin-top: 32px;
margin-bottom: 40px;
margin-right: 32px;
}
#modal-login .close-icon {
cursor: pointer;
}
#modal-login section {
margin-bottom: 48px;
margin-left: 24px;
margin-right: 24px;
}
#modal-login .help {
margin-top: 32px;
font-size: 13px;
line-height: 18px;
}
#modal-login .message {
margin-bottom: 32px;
}
#modal-login .metamask-warning {
font-size: 13px;
font-style: italic;
}
#modal-login button {
margin: 0 auto;
width: 200px;
max-width: 100%;
}
#modal-login button:first-of-type {
margin-bottom: 16px;
}
#modal-login form {
margin-top: -16px;
display: flex;
flex-direction: column;
}
#modal-login input {
margin-bottom: 16px;
max-width: 100%;
background-color: revert;
color: revert;
}
#modal-login input:focus {
color: revert;
}
#modal-login a {
color: var(--accent);
text-decoration: none;
}
#modal-login h1 {
margin-bottom: 32px;
font-size: 32px;
line-height: 38px;
font-weight: 700;
color: var(--accent);
}
.app footer {
display: flex;
justify-content: space-between;
align-items: center;
border-top: var(--border-soft);
height: 80px;
margin: 60px 40px 0px 40px;
}
.app footer .title {
font-weight: bold;
}
.app footer .footer-left {
display: flex;
}
.app footer .footer-right {
display: flex;
}
.app footer p {
margin: 0 16px;
}
.app footer a {
text-decoration: none;
}
.external-link-icon {
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
}
.notifications-modal {
position: fixed;
right: 0;
top: 75px;
overflow-y: scroll;
border-radius: 8px;
background-color: var(--white);
padding: 32px;
width: 420px;
max-height: 720px;
}
.notifications-modal h1 {
text-transform: uppercase;
color: var(--offwhite);
margin-bottom: 24px;
font-weight: 600;
font-size: 16px;
line-height: 16px;
}
</style>
<script>
import Notifications from "@/components/Notifications.vue";
import Logo from "@/assets/images/logo.svg";
import Beta from "@/assets/images/beta.svg";
import BlackXIcon from "@/assets/images/black-x.svg";
import ExternalLinkIcon from "@/assets/images/external-ltr.svg";
import BellIcon from "@/assets/images/bell.svg";
import shared from "./shared";
import Web3 from "web3";
import detectEthereumProvider from "@metamask/detect-provider";
const providerOptions = {
reconnect: {
auto: true,
delay: 5000,
maxAttempts: 5,
onTimeout: false,
},
};
const web3Options = {
transactionConfirmationBlocks: 1,
};
const rinkebyParams = {
chainId: "0x4",
chainName: "Ethereum Testnet Rinkeby",
nativeCurrency: {
name: "Rinkeby Ether",
symbol: "RIN",
decimals: 18,
},
rpcUrls: ["https://rinkeby.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161", "wss://rinkeby.infura.io/ws/v3/9aa3d95b3bc440fa88ea12eaa4456161"],
blockExplorerUrls: ["https://rinkeby.etherscan.io"],
};
const ubiqParams = {
chainId: "0x8",
chainName: "Ubiq Network Mainnet",
nativeCurrency: {
name: "Ubiq Ether",
symbol: "UBQ",
decimals: 18,
},
rpcUrls: ["https://rpc.octano.dev"],
blockExplorerUrls: ["https://ubiqscan.io"],
};
const selectedChainParameters = process.env.VUE_APP_CHAIN_ID == 8 ? ubiqParams : rinkebyParams;
console.debug("selected chain: " + selectedChainParameters.chainName);
export default {
components: {
Notifications,
Logo,
Beta,
BlackXIcon,
ExternalLinkIcon,
BellIcon,
},
data() {
return {
expectedChainId: "0x" + process.env.VUE_APP_CHAIN_ID,
actualChainId: "",
errorStr: "",
requestedUsername: "",
showModal: false,
showNotifications: false,
setLoginStateMutex: false, // true if setting login state
};
},
async created() {
detectEthereumProvider(providerOptions).then(async (provider) => {
if (!provider) {
this.$root.web3 = new Web3(); // TODO set this to use https://rpc.token.gallery/
} else {
this.$root.web3 = new Web3(provider, web3Options);
// set up callback for network change
window.ethereum.on("chainChanged", (newChainId) => {
this.$data.actualChainId = newChainId;
// if not on the right chain, force a logout
if (!this.chainIdCorrect) {
// clear local session user
console.debug("clearing user from local state");
this.$store.commit("updateUser", null);
return;
} else {
this.setLoginState();
}
});
// set up callback for account change
window.ethereum.on("accountsChanged", this.setLoginState);
// determine if user is logged in
this.setLoginState();
// Poll the chainId for .7 second. In my metamask it takes some time for this value to become correct, strangely...
for (let count = 1; count < 8; count++) {
setTimeout(() => {
this.$data.actualChainId = window.ethereum.chainId;
}, 100 * count);
}
// after polling complete, force a logout if on the wrong chain
setTimeout(() => {
if (!this.chainIdCorrect) {
// clear local session user
console.debug("clearing user from local state");
this.$store.commit("updateUser", null);
return;
} else {
this.setLoginState();
}
}, 800);
}
});
window.addEventListener("keydown", (e) => {
if (e.key == "Escape") {
this.closeModal();
}
});
},
computed: {
error() {
if (!this.metaMaskPresent) return "Error: No wallet detected";
if (!this.chainIdCorrect) return "Error: Incorrect network";
if (this.$data.errorStr !== "") return this.$data.errorStr;
return "";
},
metaMaskPresent() {
return typeof window.ethereum !== "undefined";
},
metaMaskConnected() {
return this.accounts.length > 0;
},
accounts() {
return this.$store.state.accounts;
},
chainIdCorrect() {
if (this.$data.expectedChainId !== this.$data.actualChainId) {
console.warn(`Chain ID incorrect- expected ${this.$data.expectedChainId}, but found ${this.$data.actualChainId}`);
return false;
}
return true;
},
hasUser() {
return this.$store.state.user != null;
},
userAuthenticated() {
return this.$store.getters.isUserAuthenticated;
},
userAddress() {
if (this.$store.state.user) return this.$store.state.user.id;
return "";
},
username() {
if (this.$store.state.user) return this.$store.state.user.username;
return "";
},
lastNotificationDateLocal: {
get() {
return this.$store.state.lastNotificationCheckDate;
},
set(val) {
this.$store.commit("updateLastNotificationCheckDate", val);
},
},
newNotificationAvailable() {
if (!this.userAddress) return false;
if (!this.lastNotificationDateRemote) return false;
return this.lastNotificationDateRemote > this.lastNotificationDateLocal;
},
},
asyncComputed: {
lastNotificationDateRemote: {
async get() {
if (!this.userAddress) return "";
try {
const res = await shared.fetchJson(`${this.$apiBase}/api/notifications/${this.userAddress}/last`);
return res.last;
} catch (e) {
console.error("Unable to check last notification date", e);
return "";
}
},
},
},
methods: {
openModal() {
this.showModal = true;
},
closeModal(delay) {
setTimeout(() => {
this.showModal = false;
}, delay);
},
toggleNotifications() {
if (this.showNotifications) {
this.showNotifications = false;
} else {
this.showNotifications = true;
this.lastNotificationDateLocal = new Date().toISOString();
// debug:
//let d = new Date(); d.setMonth(d.getMonth() - 3); this.lastNotificationDateLocal = d.toISOString();
//this.lastNotificationDateLocal = "";
}
},
hideNotifications() {
this.showNotifications = false;
},
async connect() {
await this.$root.web3.eth.requestAccounts();
// update login state now that eth address is available
this.setLoginState();
},
async switchChain() {
// Try to change the chain ID to ubiq
try {
console.debug("chain id: " + JSON.stringify(selectedChainParameters.chainId));
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: selectedChainParameters.chainId }],
});
} catch (switchError) {
// This error code indicates that the chain has not been added to MetaMask.
if (switchError.code === 4902) {
console.error("received 4902");
try {
console.debug("chain params: " + JSON.stringify(selectedChainParameters));
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [selectedChainParameters],
});
} catch (addError) {
console.error("Failed to add chain in metamask");
throw addError;
}
} else {
console.error("Failed to switch chain in metamask");
throw switchError;
}
}
},
logout() {
// clear user from local state, then force a refresh
this.$store.commit("updateUser", null);
this.$router.push("/");
// update login state
this.setLoginState();
},
async setLoginState() {
if (this.setLoginStateMutex) return; // already in progress
try {
console.debug("Setting login state");
this.setLoginStateMutex = true;
const accounts = await this.$root.web3.eth.getAccounts();
this.$store.commit("updateAccounts", accounts);
if (this.accounts.length > 0) {
const walletAddress = this.$root.web3.utils.toChecksumAddress(this.accounts[0]);
console.debug("metamask selected account: " + walletAddress);
const localUser = this.$store.state.user;
console.debug("local user: " + JSON.stringify(localUser));
if (localUser == null) {
await this.setLocalUser(walletAddress);
} else {
// local session user found; does it match what metamask reports?
const sessionUserMatchesWallet = localUser.id == walletAddress;
console.debug(`user's id matches MetaMask address? ${sessionUserMatchesWallet}`);
if (!sessionUserMatchesWallet) {
// clear local session user
console.debug("clearing user from local state");
this.$store.commit("updateUser", null);
await this.setLocalUser(walletAddress);
} else {
// local session user matches metamask acct! great
// But do they exist on remote database?
const remoteUser = await shared.fetchJson(`${this.$apiBase}/api/user/${walletAddress}`);
console.debug("remote user: " + JSON.stringify(remoteUser));
if (!remoteUser) {
// no remote user found for local user- clearing local user.
this.$store.commit("updateUser", null);
} else {
// found remote user matching local- done.
console.debug("user already logged in.");
this.closeModal(1000);
}
}
}
}
} finally {
this.setLoginStateMutex = false;
}
},
async setLocalUser(walletAddress) {
// no local session user! check if the current address has been registered remotely
const remoteUser = await shared.fetchJson(`${this.$apiBase}/api/user/${walletAddress}`);
console.debug("remote user: " + JSON.stringify(remoteUser));
if (remoteUser) {
// same user exists remotely. Set this as local state
console.debug("found local user matching remote. setting local");
this.$store.commit("updateUser", remoteUser);
}
},
async register() {
let usernameError = this.validateUsername(this.requestedUsername);
this.$data.errorStr = usernameError;
if (usernameError != "") return false;
try {
const usernameAvailable = await this.usernameAvailable(this.requestedUsername);
if (!usernameAvailable) {
this.$data.errorStr = "Username already taken";
return false;
}
} catch (error) {
console.error(error);
this.$data.errorStr = "Error checking username availability; please contact support";
return false;
}
const registerSuccess = await this.registerUsername();
if (!registerSuccess) return false;
this.closeModal(1000);
return true;
},
async login() {
const now = new Date().toISOString();
const message = `${now} ${this.$store.state.user.id} login`;
const signature = await this.$root.web3.eth.personal.sign(this.$root.web3.utils.fromUtf8(message), this.$store.state.user.id);
const formData = new FormData();
formData.append("now", now);
formData.append("account", this.$store.state.user.id);
formData.append("signature", signature);
let data;
let loginError;
try {
const response = await fetch(`${this.$apiBase}/api/user/login`, { method: "POST", body: formData, dataType: "json", contentType: "application/json" });
data = await response.json();
if (data.error) {
loginError = data.error;
}
} catch (e) {
console.error("failed to submit form", e);
loginError = e.toString();
}
if (loginError) {
console.warn("something went wrong remotely");
this.$data.errorStr = loginError;
return false;
}
this.$store.commit("updateUser", data);
//await this.populateAmounts();
//await this.populateNfts();
//await this.populateStores();
this.$forceUpdate(); // TODO is this necessary?
this.closeModal(1000);
return true;
},
async registerUsername() {
// push a signed request and get back a UUID secret key
// ISO date, checksummed account, and requested username
const account = this.$root.web3.utils.toChecksumAddress(this.accounts[0]);
const now = new Date().toISOString();
const message = `${now} ${account} ${this.requestedUsername}`;
const signature = await this.$root.web3.eth.personal.sign(this.$root.web3.utils.fromUtf8(message), account);
const formData = new FormData();
formData.append("now", now);
formData.append("account", account);
formData.append("username", this.requestedUsername);
formData.append("signature", signature);
let data, registrationError;
try {
const response = await fetch(`${this.$apiBase}/api/user/register`, { method: "POST", body: formData, dataType: "json", contentType: "application/json" });
data = await response.json();
if (data.error) {
registrationError = data.error;
}
} catch (e) {
console.error("failed to submit form", e);
registrationError = e.toString();
}
if (registrationError) {
this.$data.errorStr = registrationError;
return false;
}
this.$store.commit("updateUser", data);
this.$forceUpdate();
return true;
},
async usernameAvailable(username) {
const availability = await shared.fetchJson(`${this.$apiBase}/api/user/check/${username}`);
if (availability.error) {
throw `Error fetching /api/user/check/${username}: ${availability.error}`;
}
return !availability.taken;
},
validateUsername(username) {
if (username == null || username.length == 0) {
return "Username required";
} else if (username.length < 3) {
return "Username must be at least three characters";
} else if (username.length > 20) {
return "Username may only be 20 characters";
} else if (!username.match(/^[a-z0-9_]+$/i)) {
return "Username must be Latin alphanumeric/underscore";
} else if (username[0].match(/^[0-9_]+$/)) {
return "Username must not start with a number or underscore";
} else {
return "";
}
},
},
};
</script>

307
src/assets/global.css Normal file
View File

@ -0,0 +1,307 @@
:root {
/* CSS Variables */
--actually-black: #000808;
--black: #121212;
--dark: #373737;
--white: #FCFCFC;
--offwhite: #A0A3BD;
--gray: #4E4B66;
--gray-label: #6E7191;
--accent: #5F2EEA;
--purple-text: #BCA4FF;
--red: #ED2E7E;
--green-success: #34EAB9;
--green: #00BA88;
--card-foreground: #D9DBE9;
--card-background: #141428;
--alt-background: #011313;
--alt-background-darker: #071A1A;
--purple-background: #262549;
--border-soft: 1px solid var(--gray);
--distance-from-nav: 85px;
--modal-border-radius: 8px;
font-size: 16px;
letter-spacing: 0.75px;
}
.hidden {
display: none;
}
.is-active {
display: revert !important;
}
.error {
color: var(--red) !important;
font-weight: bold;
line-height: 1.25em;
}
.loading {
max-width: 80px;
margin-left: auto;
margin-right: auto;
}
a {
text-decoration: none;
text-underline-offset: 20%;
}
strong {
font-weight: bold;
}
i, .i {
font-style: italic;
}
h1 {
font-size: 48px;
line-height: 58px;
font-weight: bold;
margin-bottom: .5em;
}
h2 {
font-size: 14pt;
font-weight: bold;
margin-bottom: 1em;
margin-top: .5em;
}
h2.subheading {
font-size: 24px;
line-height: 34px;
}
p {
margin-top: .25em;
margin-bottom: .25em;
}
.details-title {
text-transform: uppercase;
color: var(--gray-label);
margin-bottom: 8px;
}
.details-data {
margin-bottom: 32px;
}
.hoverable:hover, a:hover, a.button:hover, button:hover {
filter: brightness(0.8);
cursor: pointer;
}
.not-hoverable:hover {
filter: revert;
cursor: revert;
}
a.button, button {
display: inline-block;
margin: 12px 12px;
padding: 12px 24px;
border-radius: 48px;
border: 0;
color: var(--black);
text-decoration: none;
white-space: nowrap;
font-weight: 600;
font-size: 100%;
line-height: normal;
font-family: inherit;
cursor: pointer;
}
a.button.disabled, button.disabled {
background-color: var(--card-background) !important;
color: var(--gray) !important;
cursor: default !important;
pointer-events: none !important;
}
a.button-round-plain, button.button-round-plain {
background-color: initial;
padding: 12px 0px;
color: var(--white);
}
a.button-round-gray, button.button-round-gray {
background-color: var(--card-foreground);
padding: 12px 0px;
color: var(--card-background);
}
a.button-round-accent, button.button-round-accent {
background-color: var(--accent);
color: var(--white);
}
a.button-round-white, button.button-round-white {
background-color: var(--white);
color: var(--black);
}
a.button-round-darkgray, button.button-round-darkgray {
background-color: var(--gray);
color: var(--white);
}
a.button-round-outline, button.button-round-outline {
background-color: initial;
box-shadow: 0 0 0 1pt var(--white);
color: var(--white);
}
a.button-round-reset, button.button-round-reset {
background-color: var(--white);
color: var(--red);
}
a.button-square-accent, button.button-square-accent {
line-height: 16px;
padding: 24px 32px;
border-radius: 4px;
background-color: var(--accent);
color: var(--white);
}
a.button-square-white, button.button-square-white {
line-height: 16px;
padding: 24px 32px;
border-radius: 4px;
background-color: var(--white);
color: var(--black);
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, 361px);
column-gap: 32px;
row-gap: 40px;
justify-content: center;
}
.container {
width: calc(100% - 80px);
max-width: 1620px;
margin-top: var(--distance-from-nav);
margin-left: auto;
margin-right: auto;
padding: 0 40px;
}
/* cols-2 defines a container with two columns. the left is small, and the right is large */
.container .cols-2 {
display: flex;
flex-wrap: nowrap;
}
.container .cols-2 aside {
margin-right: 48px;
margin-top: 20px;
width: calc(25% - 48px);
}
.container .cols-2 section {
width: 75%;
}
table {
background-color: var(--dark);
}
table th {
background-color: var(--black);
font-style: italic;
}
td, th {
border: 1px solid white;
padding: 1em;
}
select, input, textarea {
font-size: 20px;
line-height: 28px;
padding: 16px 32px;
border: 1px solid var(--accent);
box-sizing: border-box;
border-radius: 8px;
font-family: inherit;
}
input, textarea {
background-color: var(--gray-label);
color: var(--card-foreground);
}
input::placeholder, textarea::placeholder {
color: var(--card-foreground);
}
input:focus, input:focus:hover, textarea:focus, textarea:focus:hover {
background-color: var(--white);
color: var(--card-background);
}
input:hover, textarea:hover {
color: var(--white);
border: 1px solid #FFA3FD;
}
input:invalid:focus, textarea:invalid:focus {
color: var(--red);
}
select, input[type="text"], input[type="number"] {
/* min-height: 75px; */
}
textarea {
min-height: 361px;
}
.checkbox-container {
display: flex;
font-size: 16px;
line-height: 24px;
}
.checkbox-container input {
margin-right: 8px;
height: 24px;
width: 24px;
}
.checkbox-container label {
margin-top: 3px;
}
#overlay {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
z-index: 15;
}
#overlay.buy-modal {
background-color: rgba(0, 0, 0, 0.95);
}
abbr {
text-decoration: none;
cursor: help;
}

View File

@ -0,0 +1,5 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32.8008 13.3335V50.6668" stroke="#5F2EEA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M50.9349 32L32.2682 50.6667L13.6016 32" stroke="#5F2EEA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 354 B

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 8C18 6.4087 17.3679 4.88258 16.2426 3.75736C15.1174 2.63214 13.5913 2 12 2C10.4087 2 8.88258 2.63214 7.75736 3.75736C6.63214 4.88258 6 6.4087 6 8C6 15 3 17 3 17H21C21 17 18 15 18 8Z" stroke="#FCFCFC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.7295 21C13.5537 21.3031 13.3014 21.5547 12.9978 21.7295C12.6941 21.9044 12.3499 21.9965 11.9995 21.9965C11.6492 21.9965 11.3049 21.9044 11.0013 21.7295C10.6977 21.5547 10.4453 21.3031 10.2695 21" stroke="#FCFCFC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 675 B

View File

@ -0,0 +1,5 @@
<svg width="77" height="32" viewBox="0 0 77 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.285645" width="76" height="32" rx="16" fill="#ED2E7E"/>
<path d="M24.3816 16.208C25.043 16.3573 25.5656 16.688 25.9496 17.2C26.3443 17.7013 26.5416 18.2773 26.5416 18.928C26.5416 19.888 26.211 20.64 25.5496 21.184C24.8883 21.728 23.9603 22 22.7656 22H17.1816V10.72H22.5896C23.7416 10.72 24.643 10.976 25.2936 11.488C25.955 12 26.2856 12.72 26.2856 13.648C26.2856 14.3093 26.1096 14.864 25.7576 15.312C25.4163 15.7493 24.9576 16.048 24.3816 16.208ZM20.3176 15.2H21.9016C22.691 15.2 23.0856 14.8747 23.0856 14.224C23.0856 13.552 22.691 13.216 21.9016 13.216H20.3176V15.2ZM22.1416 19.472C22.931 19.472 23.3256 19.1413 23.3256 18.48C23.3256 18.1387 23.219 17.8773 23.0056 17.696C22.803 17.5147 22.5096 17.424 22.1256 17.424H20.3176V19.472H22.1416ZM31.802 13.232V15.072H35.402V17.456H31.802V19.488H35.882V22H28.666V10.72H35.882V13.232H31.802ZM46.6776 10.72V13.216H43.6696V22H40.5336V13.216H37.5576V10.72H46.6776ZM55.7239 20.16H51.7239L51.1159 22H47.8199L51.9319 10.72H55.5479L59.6439 22H56.3319L55.7239 20.16ZM54.9399 17.76L53.7239 14.112L52.5239 17.76H54.9399Z" fill="#FCFCFC"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24 8L8 24" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 8L24 24" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@ -0,0 +1,6 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 20.5C16.2467 20.5 20.5 16.2467 20.5 11C20.5 5.75329 16.2467 1.5 11 1.5C5.75329 1.5 1.5 5.75329 1.5 11C1.5 16.2467 5.75329 20.5 11 20.5Z" fill="#6E7191" stroke="#262549" stroke-width="3"/>
<path d="M14 8L8 14" stroke="#EFF0F7" stroke-width="2"/>
<path d="M8 8L14 14" stroke="#EFF0F7" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 419 B

View File

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.0076 9.50001C18.0107 10.8199 17.726 12.1219 17.1766 13.3C16.5252 14.7118 15.5237 15.8992 14.2844 16.7293C13.0451 17.5594 11.6169 17.9994 10.1598 18C8.94116 18.0034 7.73904 17.6951 6.65132 17.1L1.38867 19L3.14289 13.3C2.59348 12.1219 2.30877 10.8199 2.31194 9.50001C2.31251 7.92177 2.71875 6.37487 3.48516 5.03257C4.25158 3.69027 5.3479 2.60559 6.65132 1.90003C7.73904 1.30496 8.94116 0.996587 10.1598 1.00003H10.6214C12.5458 1.11502 14.3635 1.99479 15.7263 3.47088C17.0891 4.94698 17.9014 6.91567 18.0076 9.00002V9.50001Z" stroke="#6E7191" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 723 B

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 9H11C9.89543 9 9 9.89543 9 11V20C9 21.1046 9.89543 22 11 22H20C21.1046 22 22 21.1046 22 20V11C22 9.89543 21.1046 9 20 9Z" stroke="#6E7191" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 15H4C3.46957 15 2.96086 14.7893 2.58579 14.4142C2.21071 14.0391 2 13.5304 2 13V4C2 3.46957 2.21071 2.96086 2.58579 2.58579C2.96086 2.21071 3.46957 2 4 2H13C13.5304 2 14.0391 2.21071 14.4142 2.58579C14.7893 2.96086 15 3.46957 15 4V5" stroke="#6E7191" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 649 B

View File

@ -0,0 +1,4 @@
<svg width="32" height="25" viewBox="0 0 32 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M27.0894 2.20743C25.0498 1.27159 22.8626 0.582101 20.5759 0.187203C20.5342 0.179582 20.4926 0.198628 20.4712 0.236722C20.1899 0.737009 19.8783 1.38967 19.6601 1.90266C17.2005 1.53444 14.7536 1.53444 12.3444 1.90266C12.1262 1.37827 11.8033 0.737009 11.5208 0.236722C11.4993 0.199899 11.4577 0.180853 11.4161 0.187203C9.13055 0.580839 6.94341 1.27033 4.90258 2.20743C4.88491 2.21505 4.86977 2.22776 4.85972 2.24425C0.711189 8.44207 -0.425267 14.4875 0.13224 20.4581C0.134763 20.4873 0.15116 20.5152 0.173864 20.533C2.91095 22.543 5.56228 23.7633 8.16437 24.5721C8.20602 24.5849 8.25014 24.5696 8.27664 24.5353C8.89217 23.6948 9.44086 22.8084 9.9113 21.8764C9.93906 21.8218 9.91256 21.757 9.85582 21.7355C8.98551 21.4053 8.1568 21.0028 7.35964 20.5457C7.29659 20.5089 7.29154 20.4187 7.34954 20.3755C7.5173 20.2498 7.68509 20.119 7.84527 19.9869C7.87425 19.9628 7.91464 19.9577 7.94871 19.973C13.1857 22.364 18.8554 22.364 24.0306 19.973C24.0647 19.9565 24.1051 19.9616 24.1353 19.9857C24.2955 20.1177 24.4633 20.2498 24.6323 20.3755C24.6903 20.4187 24.6865 20.5089 24.6235 20.5457C23.8263 21.0117 22.9976 21.4053 22.126 21.7342C22.0693 21.7558 22.044 21.8218 22.0718 21.8764C22.5523 22.8071 23.101 23.6935 23.7052 24.5341C23.7304 24.5696 23.7758 24.5849 23.8175 24.5721C26.4322 23.7633 29.0835 22.543 31.8206 20.533C31.8446 20.5152 31.8597 20.4885 31.8622 20.4593C32.5294 13.5568 30.7447 7.56085 27.131 2.24552C27.1221 2.22776 27.107 2.21505 27.0894 2.20743ZM10.6934 16.8226C9.11666 16.8226 7.81751 15.3751 7.81751 13.5974C7.81751 11.8197 9.09147 10.3722 10.6934 10.3722C12.3078 10.3722 13.5944 11.8324 13.5692 13.5974C13.5692 15.3751 12.2952 16.8226 10.6934 16.8226ZM21.3263 16.8226C19.7497 16.8226 18.4505 15.3751 18.4505 13.5974C18.4505 11.8197 19.7244 10.3722 21.3263 10.3722C22.9408 10.3722 24.2274 11.8324 24.2022 13.5974C24.2022 15.3751 22.9408 16.8226 21.3263 16.8226Z" fill="#FCFCFC"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13">
<path fill="#36b" d="M5.002 1.01h7v7l-2-2-3 2v-1l3-2.25 1 1V2.01h-3.75l1 1-2.25 3h-1l2-3z"/>
<path fill="#36b" d="M7.002 3.01h-5v8h8v-5h-1v4h-6v-6h4z"/>
<path fill="#15a5ea" d="M4.082 5.51c0-.621.621-.621.621-.621 1.864.621 3.107 1.864 3.728 3.728 0 0 0 .621-.62.621-1.245-1.864-1.866-2.485-3.73-3.728z"/>
</svg>

After

Width:  |  Height:  |  Size: 419 B

View File

@ -0,0 +1,5 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.12 14.62C13.8454 14.9148 13.5141 15.1512 13.1462 15.3151C12.7782 15.4791 12.3809 15.5673 11.9781 15.5744C11.5753 15.5815 11.1752 15.5074 10.8016 15.3565C10.4281 15.2056 10.0887 14.9811 9.80385 14.6962C9.51897 14.4113 9.29439 14.072 9.14351 13.6984C8.99262 13.3249 8.91853 12.9247 8.92563 12.5219C8.93274 12.1191 9.02091 11.7219 9.18488 11.3539C9.34884 10.9859 9.58525 10.6547 9.88 10.38M17.94 18.44C16.2306 19.743 14.1491 20.4649 12 20.5C5 20.5 1 12.5 1 12.5C2.24389 10.1819 3.96914 8.15663 6.06 6.56003L17.94 18.44ZM9.9 4.74002C10.5883 4.5789 11.2931 4.49836 12 4.50003C19 4.50003 23 12.5 23 12.5C22.393 13.6356 21.6691 14.7048 20.84 15.69L9.9 4.74002Z" stroke="#A0A3BD" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1 1.5L23 23.5" stroke="#A0A3BD" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 963 B

View File

@ -0,0 +1,5 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 12.5C1 12.5 5 4.5 12 4.5C19 4.5 23 12.5 23 12.5C23 12.5 19 20.5 12 20.5C5 20.5 1 12.5 1 12.5Z" stroke="#FCFCFC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 15.5C13.6569 15.5 15 14.1569 15 12.5C15 10.8431 13.6569 9.5 12 9.5C10.3431 9.5 9 10.8431 9 12.5C9 14.1569 10.3431 15.5 12 15.5Z" stroke="#FCFCFC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 519 B

View File

@ -0,0 +1,4 @@
<svg width="22" height="21" viewBox="0 0 22 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.8937 2.6193C18.4199 2.10593 17.8574 1.6987 17.2383 1.42085C16.6192 1.14301 15.9557 1 15.2855 1C14.6154 1 13.9518 1.14301 13.3327 1.42085C12.7136 1.6987 12.1511 2.10593 11.6773 2.6193L10.6941 3.68421L9.71092 2.6193C8.75397 1.58282 7.45607 1.00054 6.10274 1.00054C4.74941 1.00054 3.45151 1.58282 2.49456 2.6193C1.53761 3.65577 1 5.06153 1 6.52733C1 7.99313 1.53761 9.39889 2.49456 10.4354L3.47776 11.5003L10.6941 19.3163L17.9105 11.5003L18.8937 10.4354C19.3677 9.92224 19.7437 9.313 20.0002 8.64245C20.2567 7.97189 20.3887 7.25317 20.3887 6.52733C20.3887 5.80149 20.2567 5.08277 20.0002 4.41222C19.7437 3.74166 19.3677 3.13242 18.8937 2.6193V2.6193Z" stroke="#6E7191" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="#6E7191"/>
</svg>

After

Width:  |  Height:  |  Size: 864 B

View File

@ -0,0 +1,4 @@
<svg width="22" height="21" viewBox="0 0 22 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.8937 2.6193C18.4199 2.10593 17.8574 1.6987 17.2383 1.42085C16.6192 1.14301 15.9557 1 15.2855 1C14.6154 1 13.9518 1.14301 13.3327 1.42085C12.7136 1.6987 12.1511 2.10593 11.6773 2.6193L10.6941 3.68421L9.71092 2.6193C8.75397 1.58282 7.45607 1.00054 6.10274 1.00054C4.74941 1.00054 3.45151 1.58282 2.49456 2.6193C1.53761 3.65577 1 5.06153 1 6.52733C1 7.99313 1.53761 9.39889 2.49456 10.4354L3.47776 11.5003L10.6941 19.3163L17.9105 11.5003L18.8937 10.4354C19.3677 9.92224 19.7437 9.313 20.0002 8.64245C20.2567 7.97189 20.3887 7.25317 20.3887 6.52733C20.3887 5.80149 20.2567 5.08277 20.0002 4.41222C19.7437 3.74166 19.3677 3.13242 18.8937 2.6193V2.6193Z" stroke="#6E7191" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 849 B

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="#6E7191" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 17L12 22L22 17" stroke="#6E7191" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 12L12 17L22 12" stroke="#6E7191" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 445 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,7 @@
<svg width="39" height="18" viewBox="0 0 39 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="39" height="18" rx="9" fill="#6E7191"/>
<path d="M20 10C20.5523 10 21 9.55228 21 9C21 8.44772 20.5523 8 20 8C19.4477 8 19 8.44772 19 9C19 9.55228 19.4477 10 20 10Z" stroke="#FCFCFC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M27 10C27.5523 10 28 9.55228 28 9C28 8.44772 27.5523 8 27 8C26.4477 8 26 8.44772 26 9C26 9.55228 26.4477 10 27 10Z" stroke="#FCFCFC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 10C13.5523 10 14 9.55228 14 9C14 8.44772 13.5523 8 13 8C12.4477 8 12 8.44772 12 9C12 9.55228 12.4477 10 13 10Z" stroke="#FCFCFC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 781 B

View File

@ -0,0 +1,28 @@
<svg width="361" height="361" viewBox="0 0 361 361" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 7.99999C0 3.58172 3.58172 0 8 0H353C357.418 0 361 3.58172 361 8V361H0V7.99999Z" fill="#00BA88"/>
<rect x="193.843" y="97.1253" width="106.155" height="155.48" rx="3.5" transform="rotate(15 193.843 97.1253)" fill="#EFF0F7" stroke="#2A00A2"/>
<path d="M161.047 219.522L263.584 246.997L257.045 271.401C256.545 273.268 254.626 274.376 252.759 273.876L156.983 248.213C155.116 247.713 154.008 245.793 154.508 243.926L161.047 219.522Z" fill="#FCFCFC" stroke="#2A00A2"/>
<path d="M261.093 135.167L258.257 145.754L243.884 141.903L232.185 185.563L218.971 182.022L230.67 138.362L216.297 134.511L219.133 123.924L261.093 135.167Z" fill="#2A00A2"/>
<path d="M261.093 135.167L262.059 135.426L262.318 134.46L261.352 134.201L261.093 135.167ZM258.257 145.754L257.998 146.72L258.964 146.979L259.222 146.013L258.257 145.754ZM243.884 141.903L244.142 140.937L243.176 140.678L242.918 141.644L243.884 141.903ZM232.185 185.563L231.926 186.528L232.892 186.787L233.151 185.821L232.185 185.563ZM218.971 182.022L218.005 181.763L217.746 182.729L218.712 182.988L218.971 182.022ZM230.67 138.362L231.636 138.621L231.894 137.655L230.928 137.396L230.67 138.362ZM216.297 134.511L215.331 134.252L215.072 135.218L216.038 135.477L216.297 134.511ZM219.133 123.924L219.392 122.958L218.426 122.7L218.167 123.665L219.133 123.924ZM260.127 134.909L257.291 145.495L259.222 146.013L262.059 135.426L260.127 134.909ZM258.515 144.788L244.142 140.937L243.625 142.869L257.998 146.72L258.515 144.788ZM242.918 141.644L231.219 185.304L233.151 185.821L244.849 142.162L242.918 141.644ZM232.444 184.597L219.23 181.056L218.712 182.988L231.926 186.528L232.444 184.597ZM219.937 182.281L231.636 138.621L229.704 138.103L218.005 181.763L219.937 182.281ZM230.928 137.396L216.556 133.545L216.038 135.477L230.411 139.328L230.928 137.396ZM217.263 134.77L220.099 124.183L218.167 123.665L215.331 134.252L217.263 134.77ZM218.875 124.89L260.834 136.133L261.352 134.201L219.392 122.958L218.875 124.89Z" fill="#2A00A2"/>
<path d="M234.965 195.857C234.469 193.791 233.418 192.047 231.812 190.622C230.271 189.161 228.265 188.098 225.792 187.436C221.516 186.29 217.711 186.789 214.376 188.932C211.054 191.024 208.766 194.413 207.51 199.101C206.171 204.098 206.461 208.373 208.381 211.924C210.366 215.437 213.728 217.829 218.467 219.099C221.713 219.968 224.664 219.876 227.321 218.821C230.029 217.78 232.313 215.852 234.171 213.037L217.403 208.544L220.012 198.807L248.758 206.51L245.466 218.796C243.603 221.831 241.108 224.448 237.979 226.646C234.901 228.858 231.325 230.357 227.249 231.142C223.174 231.928 218.87 231.713 214.336 230.498C208.979 229.062 204.498 226.62 200.895 223.17C197.357 219.682 194.962 215.534 193.711 210.726C192.511 205.932 192.628 200.856 194.064 195.499C195.5 190.141 197.935 185.686 201.372 182.135C204.873 178.545 209.021 176.151 213.815 174.95C218.623 173.699 223.706 173.791 229.063 175.226C235.554 176.966 240.594 180 244.182 184.33C247.822 188.673 249.618 193.82 249.57 199.77L234.965 195.857Z" fill="#2A00A2"/>
<path d="M234.965 195.857L233.993 196.09L234.131 196.669L234.706 196.823L234.965 195.857ZM231.812 190.622L231.124 191.348L231.136 191.36L231.149 191.371L231.812 190.622ZM214.376 188.932L214.909 189.778L214.916 189.773L214.376 188.932ZM208.381 211.924L207.501 212.399L207.505 212.408L207.51 212.416L208.381 211.924ZM227.321 218.821L226.962 217.887L226.952 217.891L227.321 218.821ZM234.171 213.037L235.006 213.588L235.77 212.43L234.43 212.071L234.171 213.037ZM217.403 208.544L216.437 208.285L216.178 209.251L217.144 209.51L217.403 208.544ZM220.012 198.807L220.271 197.841L219.305 197.582L219.046 198.548L220.012 198.807ZM248.758 206.51L249.724 206.768L249.982 205.803L249.017 205.544L248.758 206.51ZM245.466 218.796L246.318 219.319L246.394 219.195L246.431 219.055L245.466 218.796ZM237.979 226.646L237.404 225.828L237.395 225.834L237.979 226.646ZM227.249 231.142L227.06 230.16L227.249 231.142ZM200.895 223.17L200.193 223.882L200.198 223.887L200.203 223.892L200.895 223.17ZM193.711 210.726L192.741 210.969L192.743 210.978L193.711 210.726ZM201.372 182.135L200.656 181.436L200.653 181.439L201.372 182.135ZM213.815 174.95L214.058 175.921L214.067 175.918L213.815 174.95ZM244.182 184.33L243.412 184.968L243.416 184.972L244.182 184.33ZM249.57 199.77L249.311 200.736L250.559 201.071L250.57 199.778L249.57 199.77ZM235.937 195.623C235.393 193.357 234.233 191.433 232.476 189.874L231.149 191.371C232.603 192.661 233.545 194.226 233.993 196.09L235.937 195.623ZM232.501 189.897C230.816 188.299 228.652 187.167 226.051 186.47L225.533 188.402C227.878 189.03 229.727 190.022 231.124 191.348L232.501 189.897ZM226.051 186.47C221.538 185.261 217.436 185.776 213.835 188.091L214.916 189.773C217.985 187.801 221.494 187.319 225.533 188.402L226.051 186.47ZM213.843 188.086C210.261 190.342 207.849 193.972 206.544 198.843L208.476 199.36C209.683 194.855 211.848 191.706 214.909 189.778L213.843 188.086ZM206.544 198.843C205.158 204.014 205.428 208.565 207.501 212.399L209.26 211.448C207.494 208.18 207.183 204.183 208.476 199.36L206.544 198.843ZM207.51 212.416C209.655 216.212 213.27 218.741 218.208 220.065L218.726 218.133C214.186 216.916 211.077 214.663 209.251 211.432L207.51 212.416ZM218.208 220.065C221.624 220.98 224.8 220.898 227.69 219.75L226.952 217.891C224.528 218.854 221.802 218.957 218.726 218.133L218.208 220.065ZM227.68 219.754C230.613 218.627 233.052 216.547 235.006 213.588L233.337 212.486C231.574 215.156 229.446 216.933 226.962 217.887L227.68 219.754ZM234.43 212.071L217.662 207.578L217.144 209.51L233.913 214.003L234.43 212.071ZM218.369 208.803L220.978 199.066L219.046 198.548L216.437 208.285L218.369 208.803ZM219.753 199.773L248.499 207.476L249.017 205.544L220.271 197.841L219.753 199.773ZM247.792 206.251L244.5 218.537L246.431 219.055L249.724 206.768L247.792 206.251ZM244.613 218.273C242.828 221.183 240.43 223.702 237.404 225.828L238.554 227.464C241.786 225.193 244.379 222.479 246.318 219.319L244.613 218.273ZM237.395 225.834C234.446 227.954 231.007 229.4 227.06 230.16L227.439 232.124C231.643 231.314 235.357 229.762 238.562 227.458L237.395 225.834ZM227.06 230.16C223.152 230.913 219.001 230.713 214.595 229.532L214.077 231.464C218.738 232.713 223.196 232.942 227.439 232.124L227.06 230.16ZM214.595 229.532C209.381 228.135 205.055 225.768 201.587 222.447L200.203 223.892C203.941 227.471 208.576 229.99 214.077 231.464L214.595 229.532ZM201.597 222.457C198.188 219.096 195.885 215.107 194.678 210.474L192.743 210.978C194.04 215.96 196.527 220.267 200.193 223.882L201.597 222.457ZM194.681 210.483C193.527 205.874 193.633 200.971 195.03 195.757L193.098 195.24C191.624 200.741 191.494 205.991 192.741 210.969L194.681 210.483ZM195.03 195.757C196.427 190.544 198.786 186.245 202.09 182.83L200.653 181.439C197.085 185.127 194.572 189.738 193.098 195.24L195.03 195.757ZM202.088 182.833C205.462 179.374 209.447 177.075 214.058 175.921L213.572 173.98C208.596 175.226 204.285 177.716 200.656 181.436L202.088 182.833ZM214.067 175.918C218.689 174.715 223.594 174.796 228.805 176.192L229.322 174.26C223.817 172.785 218.557 172.683 213.563 173.983L214.067 175.918ZM228.805 176.192C235.135 177.889 239.981 180.827 243.412 184.968L244.952 183.692C241.208 179.173 235.973 176.043 229.322 174.26L228.805 176.192ZM243.416 184.972C246.895 189.124 248.616 194.037 248.57 199.762L250.57 199.778C250.62 193.603 248.75 188.223 244.949 183.687L243.416 184.972ZM249.829 198.804L235.224 194.891L234.706 196.823L249.311 200.736L249.829 198.804Z" fill="#2A00A2"/>
<rect x="167.382" y="235.307" width="29.6156" height="7.50433" rx="3.5" transform="rotate(15 167.382 235.307)" fill="#EFF0F7" stroke="#2A00A2"/>
<rect x="241.314" y="255.117" width="7.50433" height="7.50433" rx="3.5" transform="rotate(15 241.314 255.117)" fill="#EFF0F7" stroke="#2A00A2"/>
<rect x="64.6903" y="145.168" width="106.155" height="155.48" rx="3.5" transform="rotate(-30 64.6903 145.168)" fill="#EFF0F7" stroke="#2A00A2"/>
<path d="M128.048 254.906L219.98 201.828L232.613 223.709C233.579 225.383 233.006 227.523 231.332 228.49L145.461 278.067C143.787 279.034 141.647 278.46 140.68 276.786L128.048 254.906Z" fill="#FCFCFC" stroke="#2A00A2"/>
<path d="M139.143 124.514L144.623 134.006L131.736 141.446L154.336 180.59L142.489 187.43L119.889 148.286L107.003 155.726L101.523 146.234L139.143 124.514Z" fill="#2A00A2"/>
<path d="M139.143 124.514L140.009 124.014L139.509 123.148L138.643 123.648L139.143 124.514ZM144.623 134.006L145.123 134.872L145.989 134.372L145.489 133.506L144.623 134.006ZM131.736 141.446L131.236 140.58L130.37 141.08L130.87 141.946L131.736 141.446ZM154.336 180.59L154.836 181.456L155.702 180.956L155.202 180.09L154.336 180.59ZM142.489 187.43L141.623 187.93L142.123 188.796L142.989 188.296L142.489 187.43ZM119.889 148.286L120.755 147.786L120.255 146.92L119.389 147.42L119.889 148.286ZM107.003 155.726L106.137 156.226L106.637 157.092L107.503 156.592L107.003 155.726ZM101.523 146.234L101.023 145.368L100.157 145.868L100.657 146.734L101.523 146.234ZM138.277 125.014L143.757 134.506L145.489 133.506L140.009 124.014L138.277 125.014ZM144.123 133.14L131.236 140.58L132.236 142.312L145.123 134.872L144.123 133.14ZM130.87 141.946L153.47 181.09L155.202 180.09L132.602 140.946L130.87 141.946ZM153.836 179.724L141.989 186.564L142.989 188.296L154.836 181.456L153.836 179.724ZM143.355 186.93L120.755 147.786L119.023 148.786L141.623 187.93L143.355 186.93ZM119.389 147.42L106.503 154.86L107.503 156.592L120.389 149.152L119.389 147.42ZM107.869 155.226L102.389 145.734L100.657 146.734L106.137 156.226L107.869 155.226ZM102.023 147.1L139.643 125.38L138.643 123.648L101.023 145.368L102.023 147.1Z" fill="#2A00A2"/>
<path d="M163.581 185.904C161.77 184.794 159.794 184.304 157.651 184.432C155.528 184.488 153.357 185.156 151.14 186.436C147.307 188.649 144.969 191.693 144.126 195.566C143.257 199.394 144.035 203.409 146.462 207.612C149.049 212.092 152.276 214.909 156.144 216.063C160.032 217.144 164.101 216.458 168.35 214.004C171.26 212.324 173.282 210.172 174.414 207.547C175.593 204.896 175.845 201.918 175.168 198.613L160.134 207.293L155.094 198.564L180.867 183.684L187.227 194.7C188.056 198.162 188.142 201.777 187.484 205.544C186.872 209.285 185.403 212.873 183.076 216.31C180.75 219.747 177.554 222.639 173.49 224.986C168.686 227.759 163.791 229.2 158.803 229.308C153.835 229.344 149.209 228.104 144.924 225.589C140.686 223.048 137.18 219.376 134.407 214.572C131.633 209.769 130.206 204.896 130.124 199.955C130.062 194.941 131.302 190.315 133.843 186.076C136.358 181.792 140.017 178.263 144.82 175.489C150.64 172.129 156.349 170.711 161.948 171.235C167.593 171.733 172.502 174.102 176.676 178.344L163.581 185.904Z" fill="#2A00A2"/>
<path d="M163.581 185.904L163.059 186.757L163.566 187.068L164.081 186.77L163.581 185.904ZM157.651 184.432L157.677 185.432L157.694 185.431L157.711 185.43L157.651 184.432ZM144.126 195.566L145.101 195.788L145.103 195.779L144.126 195.566ZM156.145 216.063L155.859 217.021L155.868 217.024L155.877 217.027L156.145 216.063ZM174.414 207.547L173.5 207.141L173.496 207.151L174.414 207.547ZM175.168 198.613L176.148 198.413L175.87 197.054L174.668 197.747L175.168 198.613ZM160.134 207.293L159.268 207.793L159.768 208.659L160.634 208.159L160.134 207.293ZM155.094 198.564L154.594 197.698L153.728 198.198L154.228 199.064L155.094 198.564ZM180.867 183.684L181.733 183.184L181.233 182.318L180.367 182.818L180.867 183.684ZM187.227 194.7L188.2 194.467L188.166 194.325L188.093 194.2L187.227 194.7ZM187.484 205.544L186.499 205.372L186.497 205.383L187.484 205.544ZM183.076 216.31L182.248 215.75L183.076 216.31ZM158.803 229.308L158.81 230.308L158.818 230.308L158.825 230.308L158.803 229.308ZM144.924 225.589L144.41 226.447L144.418 226.452L144.924 225.589ZM130.124 199.955L129.125 199.967L129.125 199.972L130.124 199.955ZM133.843 186.076L134.701 186.59L134.706 186.582L133.843 186.076ZM161.948 171.235L161.855 172.231L161.86 172.232L161.948 171.235ZM176.676 178.344L177.176 179.21L178.295 178.564L177.389 177.643L176.676 178.344ZM164.104 185.052C162.116 183.833 159.936 183.293 157.591 183.434L157.711 185.43C159.651 185.314 161.425 185.755 163.059 186.757L164.104 185.052ZM157.625 183.433C155.304 183.493 152.972 184.223 150.64 185.57L151.64 187.302C153.743 186.088 155.751 185.482 157.677 185.432L157.625 183.433ZM150.64 185.57C146.594 187.906 144.059 191.171 143.149 195.354L145.103 195.779C145.879 192.214 148.019 189.393 151.64 187.302L150.64 185.57ZM143.151 195.345C142.213 199.473 143.074 203.745 145.596 208.112L147.328 207.112C144.996 203.073 144.3 199.315 145.101 195.788L143.151 195.345ZM145.596 208.112C148.273 212.749 151.682 215.776 155.859 217.021L156.43 215.105C152.87 214.043 149.824 211.436 147.328 207.112L145.596 208.112ZM155.877 217.027C160.077 218.194 164.422 217.427 168.85 214.87L167.85 213.138C163.78 215.488 159.987 216.093 156.412 215.1L155.877 217.027ZM168.85 214.87C171.913 213.102 174.1 210.799 175.332 207.944L173.496 207.151C172.463 209.545 170.608 211.546 167.85 213.138L168.85 214.87ZM175.328 207.954C176.605 205.082 176.859 201.887 176.148 198.413L174.189 198.814C174.83 201.949 174.582 204.71 173.501 207.141L175.328 207.954ZM174.668 197.747L159.634 206.427L160.634 208.159L175.668 199.479L174.668 197.747ZM161 206.793L155.96 198.064L154.228 199.064L159.268 207.793L161 206.793ZM155.594 199.43L181.367 184.55L180.367 182.818L154.594 197.698L155.594 199.43ZM180.001 184.184L186.361 195.2L188.093 194.2L181.733 183.184L180.001 184.184ZM186.255 194.933C187.05 198.253 187.135 201.729 186.499 205.372L188.469 205.716C189.149 201.825 189.063 198.072 188.2 194.467L186.255 194.933ZM186.497 205.383C185.911 208.967 184.501 212.421 182.248 215.75L183.904 216.871C186.304 213.325 187.833 209.602 188.471 205.706L186.497 205.383ZM182.248 215.75C180.017 219.046 176.94 221.839 172.99 224.12L173.99 225.852C178.168 223.44 181.483 220.449 183.904 216.871L182.248 215.75ZM172.99 224.12C168.315 226.819 163.583 228.204 158.782 228.308L158.825 230.308C163.999 230.196 169.057 228.7 173.99 225.852L172.99 224.12ZM158.796 228.308C154.009 228.342 149.56 227.15 145.431 224.727L144.418 226.452C148.859 229.058 153.662 230.345 158.81 230.308L158.796 228.308ZM145.439 224.732C141.363 222.288 137.972 218.747 135.273 214.072L133.541 215.072C136.388 220.005 140.009 223.808 144.41 226.447L145.439 224.732ZM135.273 214.072C132.574 209.398 131.203 204.69 131.124 199.939L129.125 199.972C129.209 205.103 130.693 210.14 133.541 215.072L135.273 214.072ZM131.124 199.943C131.065 195.111 132.256 190.668 134.701 186.59L132.986 185.562C130.348 189.962 129.06 194.771 129.125 199.967L131.124 199.943ZM134.706 186.582C137.123 182.464 140.649 179.052 145.32 176.355L144.32 174.623C139.385 177.473 135.593 181.119 132.981 185.57L134.706 186.582ZM145.32 176.355C150.996 173.078 156.5 171.73 161.855 172.231L162.041 170.24C156.199 169.693 150.284 171.18 144.32 174.623L145.32 176.355ZM161.86 172.232C167.256 172.707 171.947 174.964 175.963 179.045L177.389 177.643C173.058 173.241 167.93 170.759 162.036 170.239L161.86 172.232ZM176.176 177.478L163.081 185.038L164.081 186.77L177.176 179.21L176.176 177.478Z" fill="#2A00A2"/>
<rect x="143.689" y="261.587" width="29.6156" height="7.50433" rx="3.5" transform="rotate(-30 143.689 261.587)" fill="#EFF0F7" stroke="#2A00A2"/>
<rect x="209.974" y="223.318" width="7.50433" height="7.50433" rx="3.5" transform="rotate(-30 209.974 223.318)" fill="#EFF0F7" stroke="#2A00A2"/>
<rect x="127.14" y="81" width="106.155" height="155.48" rx="3.5" fill="#EFF0F7" stroke="#2A00A2"/>
<path d="M127.14 207.715H233.295V232.98C233.295 234.913 231.728 236.48 229.795 236.48H130.64C128.707 236.48 127.14 234.913 127.14 232.98V207.715Z" fill="#FCFCFC" stroke="#2A00A2"/>
<path d="M199.229 98.36V107.95H186.209V147.5H174.239V107.95H161.219V98.36H199.229Z" fill="#2A00A2"/>
<path d="M199.229 98.36H200.229V97.36H199.229V98.36ZM199.229 107.95V108.95H200.229V107.95H199.229ZM186.209 107.95V106.95H185.209V107.95H186.209ZM186.209 147.5V148.5H187.209V147.5H186.209ZM174.239 147.5H173.239V148.5H174.239V147.5ZM174.239 107.95H175.239V106.95H174.239V107.95ZM161.219 107.95H160.219V108.95H161.219V107.95ZM161.219 98.36V97.36H160.219V98.36H161.219ZM198.229 98.36V107.95H200.229V98.36H198.229ZM199.229 106.95H186.209V108.95H199.229V106.95ZM185.209 107.95V147.5H187.209V107.95H185.209ZM186.209 146.5H174.239V148.5H186.209V146.5ZM175.239 147.5V107.95H173.239V147.5H175.239ZM174.239 106.95H161.219V108.95H174.239V106.95ZM162.219 107.95V98.36H160.219V107.95H162.219ZM161.219 99.36H199.229V97.36H161.219V99.36Z" fill="#2A00A2"/>
<path d="M190.943 161.524C190.056 159.891 188.773 158.654 187.093 157.814C185.459 156.927 183.523 156.484 181.283 156.484C177.409 156.484 174.306 157.767 171.973 160.334C169.639 162.854 168.473 166.237 168.473 170.484C168.473 175.011 169.686 178.557 172.113 181.124C174.586 183.644 177.969 184.904 182.263 184.904C185.203 184.904 187.676 184.157 189.683 182.664C191.736 181.171 193.229 179.024 194.163 176.224H178.973V167.404H205.013V178.534C204.126 181.521 202.609 184.297 200.463 186.864C198.363 189.431 195.679 191.507 192.413 193.094C189.146 194.681 185.459 195.474 181.353 195.474C176.499 195.474 172.159 194.424 168.333 192.324C164.553 190.177 161.589 187.214 159.443 183.434C157.343 179.654 156.293 175.337 156.293 170.484C156.293 165.631 157.343 161.314 159.443 157.534C161.589 153.707 164.553 150.744 168.333 148.644C172.113 146.497 176.429 145.424 181.283 145.424C187.163 145.424 192.109 146.847 196.123 149.694C200.183 152.541 202.866 156.484 204.173 161.524H190.943Z" fill="#2A00A2"/>
<path d="M190.943 161.524L190.064 162.001L190.348 162.524H190.943V161.524ZM187.093 157.814L186.616 158.693L186.63 158.701L186.645 158.708L187.093 157.814ZM171.973 160.334L172.706 161.013L172.713 161.007L171.973 160.334ZM172.113 181.124L171.386 181.811L171.392 181.818L171.399 181.824L172.113 181.124ZM189.683 182.664L189.094 181.855L189.086 181.862L189.683 182.664ZM194.163 176.224L195.111 176.54L195.55 175.224H194.163V176.224ZM178.973 176.224H177.973V177.224H178.973V176.224ZM178.973 167.404V166.404H177.973V167.404H178.973ZM205.013 167.404H206.013V166.404H205.013V167.404ZM205.013 178.534L205.971 178.819L206.013 178.679V178.534H205.013ZM200.463 186.864L199.696 186.222L199.689 186.231L200.463 186.864ZM192.413 193.094L191.976 192.195V192.195L192.413 193.094ZM168.333 192.324L167.839 193.194L167.845 193.197L167.852 193.201L168.333 192.324ZM159.443 183.434L158.568 183.92L158.573 183.928L159.443 183.434ZM159.443 157.534L158.57 157.045L158.568 157.048L159.443 157.534ZM168.333 148.644L168.818 149.518L168.826 149.514L168.333 148.644ZM196.123 149.694L195.544 150.51L195.549 150.513L196.123 149.694ZM204.173 161.524V162.524H205.465L205.141 161.273L204.173 161.524ZM191.821 161.047C190.836 159.232 189.401 157.85 187.54 156.92L186.645 158.708C188.144 159.458 189.276 160.55 190.064 162.001L191.821 161.047ZM187.57 156.935C185.763 155.954 183.655 155.484 181.283 155.484V157.484C183.39 157.484 185.156 157.9 186.616 158.693L187.57 156.935ZM181.283 155.484C177.164 155.484 173.779 156.861 171.233 159.661L172.713 161.007C174.833 158.674 177.655 157.484 181.283 157.484V155.484ZM171.239 159.655C168.696 162.401 167.473 166.048 167.473 170.484H169.473C169.473 166.427 170.582 163.307 172.706 161.013L171.239 159.655ZM167.473 170.484C167.473 175.191 168.738 179.011 171.386 181.811L172.839 180.437C170.634 178.104 169.473 174.83 169.473 170.484H167.473ZM171.399 181.824C174.1 184.576 177.763 185.904 182.263 185.904V183.904C178.176 183.904 175.072 182.712 172.826 180.424L171.399 181.824ZM182.263 185.904C185.379 185.904 188.072 185.109 190.28 183.466L189.086 181.862C187.28 183.205 185.027 183.904 182.263 183.904V185.904ZM190.271 183.473C192.519 181.838 194.123 179.505 195.111 176.54L193.214 175.908C192.335 178.543 190.953 180.504 189.094 181.855L190.271 183.473ZM194.163 175.224H178.973V177.224H194.163V175.224ZM179.973 176.224V167.404H177.973V176.224H179.973ZM178.973 168.404H205.013V166.404H178.973V168.404ZM204.013 167.404V178.534H206.013V167.404H204.013ZM204.054 178.249C203.209 181.096 201.761 183.753 199.696 186.222L201.23 187.506C203.457 184.842 205.043 181.946 205.971 178.819L204.054 178.249ZM199.689 186.231C197.689 188.675 195.125 190.665 191.976 192.195L192.85 193.994C196.234 192.35 199.036 190.186 201.237 187.497L199.689 186.231ZM191.976 192.195C188.862 193.707 185.328 194.474 181.353 194.474V196.474C185.591 196.474 189.43 195.655 192.85 193.994L191.976 192.195ZM181.353 194.474C176.648 194.474 172.477 193.458 168.814 191.447L167.852 193.201C171.842 195.39 176.35 196.474 181.353 196.474V194.474ZM168.826 191.454C165.203 189.397 162.37 186.563 160.312 182.94L158.573 183.928C160.809 187.865 163.902 190.958 167.839 193.194L168.826 191.454ZM160.317 182.948C158.309 179.334 157.293 175.189 157.293 170.484H155.293C155.293 175.486 156.376 179.974 158.568 183.92L160.317 182.948ZM157.293 170.484C157.293 165.779 158.309 161.634 160.317 158.02L158.568 157.048C156.376 160.994 155.293 165.482 155.293 170.484H157.293ZM160.315 158.023C162.372 154.356 165.203 151.527 168.818 149.518L167.847 147.77C163.902 149.961 160.806 153.059 158.57 157.045L160.315 158.023ZM168.826 149.514C172.439 147.462 176.582 146.424 181.283 146.424V144.424C176.277 144.424 171.786 145.533 167.839 147.774L168.826 149.514ZM181.283 146.424C186.997 146.424 191.731 147.805 195.544 150.51L196.701 148.878C192.488 145.89 187.329 144.424 181.283 144.424V146.424ZM195.549 150.513C199.404 153.216 201.954 156.953 203.205 161.775L205.141 161.273C203.778 156.015 200.962 151.866 196.697 148.875L195.549 150.513ZM204.173 160.524H190.943V162.524H204.173V160.524Z" fill="#2A00A2"/>
<rect x="137.345" y="221.321" width="29.6156" height="7.50433" rx="3.5" fill="#EFF0F7" stroke="#2A00A2"/>
<rect x="213.884" y="221.321" width="7.50433" height="7.50433" rx="3.5" fill="#EFF0F7" stroke="#2A00A2"/>
</svg>

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,15 @@
<svg width="361" height="361" viewBox="0 0 361 361" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 7.99999C0 3.58172 3.58172 0 8 0H353C357.418 0 361 3.58172 361 8V361H0V7.99999Z" fill="#00966D"/>
<path d="M180.5 60.4998V108.5" stroke="#FCFCFC" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M180.5 252.5V300.5" stroke="#FCFCFC" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M95.6611 95.6592L129.621 129.619" stroke="#FCFCFC" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M231.382 231.38L265.342 265.34" stroke="#FCFCFC" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M60.5 180.5H108.5" stroke="#FCFCFC" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M252.5 180.5H300.5" stroke="#FCFCFC" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M95.6611 265.34L129.621 231.38" stroke="#FCFCFC" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M231.382 129.619L265.342 95.6592" stroke="#FCFCFC" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M180.499 203.401C193.147 203.401 203.4 193.148 203.4 180.5C203.4 167.852 193.147 157.599 180.499 157.599C167.852 157.599 157.599 167.852 157.599 180.5C157.599 193.148 167.852 203.401 180.499 203.401Z" stroke="#FCFCFC" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M180.499 171.34V189.66" stroke="#FCFCFC" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M171.339 180.5H189.659" stroke="#FCFCFC" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,7 @@
<svg width="76" height="38" viewBox="0 0 76 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="19" height="19" fill="#5F2EEA"/>
<rect x="38" width="19" height="19" fill="#FCFCFC"/>
<rect x="19" y="19" width="19" height="19" fill="#FCFCFC"/>
<rect x="57" y="19" width="19" height="19" fill="#5F2EEA"/>
</svg>

After

Width:  |  Height:  |  Size: 323 B

View File

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="#BCA4FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 8V16" stroke="#BCA4FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 12H16" stroke="#BCA4FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 515 B

View File

@ -0,0 +1,12 @@
<svg width="361" height="361" viewBox="0 0 361 361" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 7.99999C0 3.58172 3.58172 0 8 0H353C357.418 0 361 3.58172 361 8V361H0V7.99999Z" fill="#5F2EEA"/>
<rect x="118" y="81" width="125" height="199" rx="3.5" fill="#EFF0F7" stroke="#2A00A2"/>
<path d="M118 242.957H243V276.5C243 278.433 241.433 280 239.5 280H121.5C119.567 280 118 278.433 118 276.5V242.957Z" fill="#FCFCFC" stroke="#2A00A2"/>
<path d="M202.227 105.34V116.3H187.347V161.5H173.667V116.3H158.787V105.34H202.227Z" fill="#2A00A2"/>
<path d="M202.227 105.34H203.227V104.34H202.227V105.34ZM202.227 116.3V117.3H203.227V116.3H202.227ZM187.347 116.3V115.3H186.347V116.3H187.347ZM187.347 161.5V162.5H188.347V161.5H187.347ZM173.667 161.5H172.667V162.5H173.667V161.5ZM173.667 116.3H174.667V115.3H173.667V116.3ZM158.787 116.3H157.787V117.3H158.787V116.3ZM158.787 105.34V104.34H157.787V105.34H158.787ZM201.227 105.34V116.3H203.227V105.34H201.227ZM202.227 115.3H187.347V117.3H202.227V115.3ZM186.347 116.3V161.5H188.347V116.3H186.347ZM187.347 160.5H173.667V162.5H187.347V160.5ZM174.667 161.5V116.3H172.667V161.5H174.667ZM173.667 115.3H158.787V117.3H173.667V115.3ZM159.787 116.3V105.34H157.787V116.3H159.787ZM158.787 106.34H202.227V104.34H158.787V106.34Z" fill="#2A00A2"/>
<path d="M192.771 178.97C191.758 177.103 190.291 175.69 188.371 174.73C186.505 173.716 184.291 173.21 181.731 173.21C177.305 173.21 173.758 174.676 171.091 177.61C168.425 180.49 167.091 184.356 167.091 189.21C167.091 194.383 168.478 198.436 171.251 201.37C174.078 204.25 177.945 205.69 182.851 205.69C186.211 205.69 189.038 204.836 191.331 203.13C193.678 201.423 195.385 198.97 196.451 195.77H179.091V185.69H208.851V198.41C207.838 201.823 206.105 204.996 203.651 207.93C201.251 210.863 198.185 213.236 194.451 215.05C190.718 216.863 186.505 217.77 181.811 217.77C176.265 217.77 171.305 216.57 166.931 214.17C162.611 211.716 159.225 208.33 156.771 204.01C154.371 199.69 153.171 194.756 153.171 189.21C153.171 183.663 154.371 178.73 156.771 174.41C159.225 170.036 162.611 166.65 166.931 164.25C171.251 161.796 176.185 160.57 181.731 160.57C188.451 160.57 194.105 162.196 198.691 165.45C203.331 168.703 206.398 173.21 207.891 178.97H192.771Z" fill="#2A00A2"/>
<path d="M192.771 178.97L191.892 179.447L192.176 179.97H192.771V178.97ZM188.371 174.73L187.894 175.608L187.909 175.617L187.924 175.624L188.371 174.73ZM171.091 177.61L171.825 178.289L171.831 178.282L171.091 177.61ZM171.251 201.37L170.525 202.057L170.531 202.063L170.538 202.07L171.251 201.37ZM191.331 203.13L190.743 202.321L190.734 202.327L191.331 203.13ZM196.451 195.77L197.4 196.086L197.839 194.77H196.451V195.77ZM179.091 195.77H178.091V196.77H179.091V195.77ZM179.091 185.69V184.69H178.091V185.69H179.091ZM208.851 185.69H209.851V184.69H208.851V185.69ZM208.851 198.41L209.81 198.694L209.851 198.555V198.41H208.851ZM203.651 207.93L202.884 207.288L202.877 207.296L203.651 207.93ZM194.451 215.05L194.014 214.15L194.451 215.05ZM166.931 214.17L166.437 215.039L166.444 215.043L166.45 215.046L166.931 214.17ZM156.771 204.01L155.897 204.495L155.902 204.503L156.771 204.01ZM156.771 174.41L155.899 173.92L155.897 173.924L156.771 174.41ZM166.931 164.25L167.417 165.124L167.425 165.119L166.931 164.25ZM198.691 165.45L198.113 166.265L198.117 166.268L198.691 165.45ZM207.891 178.97V179.97H209.184L208.859 178.719L207.891 178.97ZM193.65 178.493C192.538 176.444 190.92 174.886 188.818 173.835L187.924 175.624C189.663 176.493 190.978 177.762 191.892 179.447L193.65 178.493ZM188.848 173.851C186.808 172.743 184.424 172.21 181.731 172.21V174.21C184.159 174.21 186.201 174.689 187.894 175.608L188.848 173.851ZM181.731 172.21C177.059 172.21 173.231 173.77 170.351 176.937L171.831 178.282C174.285 175.583 177.55 174.21 181.731 174.21V172.21ZM170.357 176.93C167.482 180.036 166.091 184.167 166.091 189.21H168.091C168.091 184.546 169.368 180.943 171.825 178.289L170.357 176.93ZM166.091 189.21C166.091 194.564 167.53 198.89 170.525 202.057L171.978 200.683C169.425 197.983 168.091 194.202 168.091 189.21H166.091ZM170.538 202.07C173.592 205.182 177.738 206.69 182.851 206.69V204.69C178.151 204.69 174.564 203.317 171.965 200.669L170.538 202.07ZM182.851 206.69C186.387 206.69 189.434 205.788 191.928 203.932L190.734 202.327C188.642 203.884 186.035 204.69 182.851 204.69V206.69ZM191.919 203.938C194.461 202.09 196.278 199.45 197.4 196.086L195.503 195.453C194.491 198.489 192.895 200.756 190.743 202.321L191.919 203.938ZM196.451 194.77H179.091V196.77H196.451V194.77ZM180.091 195.77V185.69H178.091V195.77H180.091ZM179.091 186.69H208.851V184.69H179.091V186.69ZM207.851 185.69V198.41H209.851V185.69H207.851ZM207.893 198.125C206.921 201.398 205.257 204.452 202.884 207.288L204.418 208.571C206.953 205.541 208.755 202.248 209.81 198.694L207.893 198.125ZM202.877 207.296C200.577 210.107 197.63 212.394 194.014 214.15L194.888 215.949C198.739 214.079 201.925 211.619 204.425 208.563L202.877 207.296ZM194.014 214.15C190.434 215.889 186.373 216.77 181.811 216.77V218.77C186.636 218.77 191.002 217.837 194.888 215.949L194.014 214.15ZM181.811 216.77C176.414 216.77 171.622 215.603 167.412 213.293L166.45 215.046C170.987 217.536 176.116 218.77 181.811 218.77V216.77ZM167.425 213.3C163.262 210.936 160.005 207.679 157.641 203.516L155.902 204.503C158.444 208.98 161.961 212.497 166.437 215.039L167.425 213.3ZM157.645 203.524C155.338 199.37 154.171 194.608 154.171 189.21H152.171C152.171 194.905 153.405 200.009 155.897 204.495L157.645 203.524ZM154.171 189.21C154.171 183.812 155.338 179.049 157.645 174.895L155.897 173.924C153.405 178.41 152.171 183.514 152.171 189.21H154.171ZM157.643 174.899C160.007 170.685 163.261 167.432 167.417 165.124L166.446 163.375C161.961 165.867 158.442 169.388 155.899 173.92L157.643 174.899ZM167.425 165.119C171.578 162.761 176.337 161.57 181.731 161.57V159.57C176.032 159.57 170.925 160.832 166.437 163.38L167.425 165.119ZM181.731 161.57C188.285 161.57 193.726 163.154 198.113 166.265L199.27 164.634C194.483 161.239 188.617 159.57 181.731 159.57V161.57ZM198.117 166.268C202.552 169.378 205.486 173.678 206.923 179.221L208.859 178.719C207.31 172.741 204.11 168.028 199.265 164.631L198.117 166.268ZM207.891 177.97H192.771V179.97H207.891V177.97Z" fill="#2A00A2"/>
<rect x="130" y="260.348" width="35" height="9.86957" rx="3.5" fill="#EFF0F7" stroke="#2A00A2"/>
<rect x="220" y="260.348" width="9" height="9.86957" rx="3.5" fill="#EFF0F7" stroke="#2A00A2"/>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -0,0 +1,4 @@
<svg width="72" height="63" viewBox="0 0 72 63" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30.2996 4.86735L2.06625 52.0007C1.48414 53.0088 1.17614 54.1517 1.17288 55.3157C1.16962 56.4798 1.47122 57.6244 2.04767 58.6358C2.62412 59.6471 3.45533 60.4898 4.45861 61.0802C5.46188 61.6705 6.60225 61.9879 7.76625 62.0007H64.2329C65.3969 61.9879 66.5373 61.6705 67.5406 61.0802C68.5438 60.4898 69.375 59.6471 69.9515 58.6358C70.5279 57.6244 70.8295 56.4798 70.8263 55.3157C70.823 54.1517 70.515 53.0088 69.9329 52.0007L41.6996 4.86735C41.1053 3.8877 40.2687 3.07775 39.2702 2.51563C38.2718 1.95351 37.1454 1.6582 35.9996 1.6582C34.8538 1.6582 33.7274 1.95351 32.7289 2.51563C31.7305 3.07775 30.8938 3.8877 30.2996 4.86735V4.86735Z" stroke="#ED2E7E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 831 B

View File

@ -0,0 +1,6 @@
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M70 50V63.3333C70 65.1014 69.2976 66.7971 68.0474 68.0474C66.7971 69.2976 65.1014 70 63.3333 70H16.6667C14.8986 70 13.2029 69.2976 11.9526 68.0474C10.7024 66.7971 10 65.1014 10 63.3333V50" stroke="#5F2EEA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M23.333 33.3335L39.9997 50.0002L56.6663 33.3335" stroke="#5F2EEA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M40 50V10" stroke="#5F2EEA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 629 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><style>.cls-1{fill:#5f2eea;}.cls-2{fill:#fff;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M12.84.39l2.32,2.67a1.08,1.08,0,0,0,.92.38l3.54-.25a1.12,1.12,0,0,1,1.19,1.19l-.25,3.54a1.08,1.08,0,0,0,.38.92l2.67,2.32a1.1,1.1,0,0,1,0,1.68l-2.67,2.32a1.08,1.08,0,0,0-.38.92l.25,3.54a1.12,1.12,0,0,1-1.19,1.19l-3.54-.25a1.08,1.08,0,0,0-.92.38l-2.32,2.67a1.1,1.1,0,0,1-1.68,0L8.84,20.94a1.08,1.08,0,0,0-.92-.38l-3.54.25a1.12,1.12,0,0,1-1.19-1.19l.25-3.54a1.08,1.08,0,0,0-.38-.92L.39,12.84a1.1,1.1,0,0,1,0-1.68L3.06,8.84a1.08,1.08,0,0,0,.38-.92L3.19,4.38A1.12,1.12,0,0,1,4.38,3.19l3.54.25a1.08,1.08,0,0,0,.92-.38L11.16.39A1.1,1.1,0,0,1,12.84.39Z"/><path class="cls-2" d="M11,15.38,8.44,12.5a.75.75,0,1,1,1.12-1L11,13.12l3.44-3.87a.75.75,0,0,1,1.12,1Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 890 B

View File

@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24 8L8 24" stroke="#FCFCFC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 8L24 24" stroke="#FCFCFC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 312 B

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

160
src/assets/minimodal.css Normal file
View File

@ -0,0 +1,160 @@
/**************************\
Basic Modal Styles
\**************************/
.modal {
display: none;
}
.modal.is-open {
display: block;
}
.modal {
font-family: -apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica neue,helvetica,ubuntu,roboto,noto,segoe ui,arial,sans-serif;
}
.modal__overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.6);
display: flex;
justify-content: center;
align-items: center;
}
.modal__container {
background-color: #fff;
padding: 30px;
max-width: 500px;
max-height: 100vh;
border-radius: 4px;
overflow-y: auto;
box-sizing: border-box;
}
.modal__header {
display: flex;
justify-content: space-between;
align-items: center;
}
.modal__title {
margin-top: 0;
margin-bottom: 0;
font-weight: 600;
font-size: 1.25rem;
line-height: 1.25;
color: #00449e;
box-sizing: border-box;
}
.modal__close {
background: transparent;
border: 0;
}
.modal__header .modal__close:before { content: "\2715"; }
.modal__content {
margin-top: 2rem;
margin-bottom: 2rem;
line-height: 1.5;
color: rgba(0,0,0,.8);
}
.modal__btn {
font-size: .875rem;
padding-left: 1rem;
padding-right: 1rem;
padding-top: .5rem;
padding-bottom: .5rem;
background-color: #e6e6e6;
color: rgba(0,0,0,.8);
border-radius: .25rem;
border-style: none;
border-width: 0;
cursor: pointer;
-webkit-appearance: button;
text-transform: none;
overflow: visible;
line-height: 1.15;
margin: 0;
will-change: transform;
-moz-osx-font-smoothing: grayscale;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-transform: translateZ(0);
transform: translateZ(0);
transition: -webkit-transform .25s ease-out;
transition: transform .25s ease-out;
transition: transform .25s ease-out,-webkit-transform .25s ease-out;
}
.modal__btn:focus, .modal__btn:hover {
-webkit-transform: scale(1.05);
transform: scale(1.05);
}
.modal__btn-primary {
background-color: #00449e;
color: #fff;
}
/**************************\
Demo Animation Style
\**************************/
@keyframes mmfadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes mmfadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes mmslideIn {
from { transform: translateY(15%); }
to { transform: translateY(0); }
}
@keyframes mmslideOut {
from { transform: translateY(0); }
to { transform: translateY(-10%); }
}
.micromodal-slide {
display: none;
}
.micromodal-slide.is-open {
display: block;
}
.micromodal-slide[aria-hidden="false"] .modal__overlay {
animation: mmfadeIn .3s cubic-bezier(0.0, 0.0, 0.2, 1);
}
.micromodal-slide[aria-hidden="false"] .modal__container {
animation: mmslideIn .3s cubic-bezier(0, 0, .2, 1);
}
.micromodal-slide[aria-hidden="true"] .modal__overlay {
animation: mmfadeOut .3s cubic-bezier(0.0, 0.0, 0.2, 1);
}
.micromodal-slide[aria-hidden="true"] .modal__container {
animation: mmslideOut .3s cubic-bezier(0, 0, .2, 1);
}
.micromodal-slide .modal__container,
.micromodal-slide .modal__overlay {
will-change: transform;
}

48
src/assets/reset.css Normal file
View File

@ -0,0 +1,48 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1.05em;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}

View File

@ -0,0 +1,219 @@
<template>
<div class="batch-form">
<div class="form-swap-container">
<button class="swap-nav" v-if="index != 0" @click="swap(index, index - 1)" title="move up"></button>
<button class="swap-nav" v-if="index != setInfo.size - 1" @click="swap(index, index + 1)" title="move down"></button>
</div>
<div class="form-image-container">
<img
class="dropzone image-preview"
:src="blobUrl"
:id="'image-' + value.id"
@error="imageError"
@click="showImage"
@drop="handleImageDrop"
@dragover.prevent
@dragenter.prevent
/>
</div>
<div class="form-contents-container">
<div class="form-in-set" v-if="setInfo.inSet">{{ index + 1 }} of {{ setInfo.size }}</div>
<div class="form-is-adult">
Adult?
<input type="radio" :value="'false'" v-model="value.adult" />No <input type="radio" :value="'true'" v-model="value.adult" />Yes
</div>
<div class="form-name">
NFT Name:
<input type="text" :id="'name-' + value.id" v-model="value.name" maxlength="100" size="14" />
</div>
<div class="form-controls">
<button @click="removeSelf" v-if="!fix">Remove</button>
<!--<button v-if="index==0 && !fix" @click="copyToAll" title="Copies all fields to every new NFT.">Copy to ALL</button>-->
<button @click="duplicate" title="Duplicate settings into multiple new NFTs">Duplicate</button>
</div>
</div>
<div>
NFT Description:<br />
<textarea :id="'description-' + value.id" cols="60" rows="5" v-model="value.description" maxlength="1000" />
<br />
<ExtraProperties v-model="value.extra" />
<div class="errors">
<div v-for="error of errors" :key="error" class="error">
{{ error }}
</div>
</div>
</div>
</div>
</template>
<style scoped>
.batch-form {
display: flex;
flex-wrap: wrap;
margin: 0.5em;
border: 2px solid white;
}
.form-image-container {
width: 120px;
}
.image-preview {
max-width: 120px;
}
.image-preview:hover {
cursor: pointer;
}
.swap-nav {
display: block;
}
</style>
<script>
import * as basicLightbox from "basiclightbox";
import ExtraProperties from "./ExtraProperties.vue";
import { bus } from "../main";
export default {
data: function() {
return {
basicLightbox: null,
errors: [],
};
},
computed: {
blobUrl: {
get() {
return this.$store.state.simpleSetForms[this.index].blobUrl;
},
set(value) {
this.$store.commit("updateSimpleSetFormBlobUrl", { index: this.index, value });
},
},
},
components: {
ExtraProperties,
},
props: [
"value",
"index",
"setInfo", // { inSet: true, size: 5 }
"fix",
],
async created() {
if (!this.blobUrl) {
const imageId = this.$store.state.simpleSetForms[this.index].parentImageId ? this.$store.state.simpleSetForms[this.index].parentImageId : this.value.id;
console.log(`No blobUrl, attempting to get from localForage image/${imageId}`);
const imageBlob = await this.$getItem(`image/${imageId}`);
if (imageBlob == null) {
console.log(`No localForage image either for image/${this.value.id}, setting a default image.`);
this.blobUrl = "/question.png";
} else {
console.log(imageBlob);
console.log("Creating blob URL for extracted blob.");
this.blobUrl = URL.createObjectURL(imageBlob);
}
}
bus.$on("validate", this.validateHandler);
this.$once("hook:beforeDestroy", () => {
console.log("Destroying validate handler.");
bus.$off("validate", this.validateHandler);
});
bus.$on("clearform", () => {
this.errors = [];
});
},
async mounted() {},
methods: {
// this should only be called by the event bus, just use validate() for internal use.
validateHandler() {
console.log("Validating form on request from event bus.");
const isValidated = this.validate();
bus.$emit("validated", { index: this.index, validated: isValidated });
},
async imageError() {
console.log("Blob must have expired, refreshing it.");
const imageId = this.$store.state.simpleSetForms[this.index].parentImageId ? this.$store.state.simpleSetForms[this.index].parentImageId : this.value.id;
const imageBlob = await this.$getItem(`image/${imageId}`);
if (imageBlob == null) {
console.log(`No localForage image either for image/${this.value.id}`);
} else {
console.log("Found it, creating blob URL for extracted blob.");
this.blobUrl = URL.createObjectURL(imageBlob);
}
},
async handleImageDrop(e) {
console.log("handling drop on item preview image");
e.preventDefault();
const files = [...e.dataTransfer.files].filter((file) => ["image/png", "image/gif", "image/jpeg"].includes(file.type));
if (files.length == 0 || files.length > 1) {
this.errors.push(`Will only handle one file dragged, got ${files.length}, not changing`);
return;
} else {
if (files[0].size > 10 * 1024 * 1024) {
this.errors.push("File must be maximum 10 megabyte, not changing");
return;
}
if (this.blobUrl.startsWith("blob:")) {
console.log("Revoking old file object");
URL.revokeObjectURL(this.blobUrl);
}
this.blobUrl = URL.createObjectURL(files[0]);
console.log(`Created new URL: ${this.blobUrl}`);
this.basicLightbox = basicLightbox.create(`<img src="${this.blobUrl}"/>`);
console.log("Persisting file.");
this.$setItem(`image/${this.value.id}`, files[0]);
}
},
showImage() {
if (!this.basicLightbox) {
this.basicLightbox = basicLightbox.create(`<img src="${this.blobUrl}"/>`);
}
this.basicLightbox.show();
},
swap(index1, index2) {
this.$parent.swapSimpleSetForms(index1, index2);
},
removeSelf() {
this.$store.commit("removeSimpleSetForm", this.index);
},
copyToAll() {
this.$parent.copyToAll(this.index);
},
duplicate() {
this.$parent.duplicate(this.index);
},
validate() {
this.errors = this.validateNftForm(this.value);
return this.errors.length == 0;
},
validateNftForm(obj) {
const ARBITRARY_FIELD_MAX = 1000;
const NAME_MAX = 40;
const SET_MAX = 100;
const errors = [];
if (!obj.name) {
errors.push("Name is required");
}
if (obj.name && obj.name.length > NAME_MAX) {
errors.push(`Name may only be ${NAME_MAX} characters`);
}
if (!obj.description) {
errors.push("Description is required");
}
if (obj.description && obj.description.length > ARBITRARY_FIELD_MAX) {
errors.push(`Description may only be ${ARBITRARY_FIELD_MAX} characters`);
}
if (typeof obj.set !== "undefined" && obj.set.length > SET_MAX) {
errors.push(`Set may only be ${SET_MAX} characters`);
}
return errors;
},
},
};
</script>

View File

@ -0,0 +1,85 @@
<template>
<div class="collection-container">
<router-link v-if="clickAction == 'link'" class="collection" :to="{ name: 'Collection', params: { collection: address } }">
<div class="collection-top">
<NewCollection v-if="symbol == '_new_'" />
<h1 v-else>{{ symbol }}</h1>
</div>
<div class="collection-bottom">
<p class="collection-name">{{ name }}</p>
<p class="collection-type">ERC-1155</p>
</div>
</router-link>
<div v-else class="collection" @click="clickAction(address)">
<div class="collection-top">
<NewCollection v-if="symbol == '_new_'" />
<h1 v-else>{{ symbol }}</h1>
</div>
<div class="collection-bottom" :class="{ selected: selectedCollection == address && address != '_new_' }">
<p class="collection-name">{{ name }}</p>
<p class="collection-type">ERC-1155</p>
</div>
</div>
</div>
</template>
<style scoped>
.collection-container {
width: 361px;
cursor: pointer;
}
.collection-top {
height: 361px; /* consider reducing this? making it non-square */
border-radius: 8px 8px 0px 0px;
background-color: var(--accent);
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.collection-top h1 {
overflow-wrap: anywhere;
padding: 0 20px;
}
.collection-bottom {
height: 120px;
border-radius: 0px 0px 8px 8px;
padding: 0 20px;
background-color: var(--card-background);
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.collection-bottom.selected {
background-color: var(--accent);
}
.collection-name {
font-size: 20px;
line-height: 24px;
font-weight: bold;
}
.collection-type {
font-size: 16px;
line-height: 24px;
}
</style>
<script>
import NewCollection from "@/assets/images/new-collection.svg";
export default {
// clickAction can either be the string 'link' (link to collection) or a callback function
props: ["symbol", "name", "address", "clickAction", "selectedCollection"],
components: {
NewCollection,
},
};
</script>

View File

@ -0,0 +1,80 @@
<template>
<div class="create-nft-header">
<header>
<div class="header-left">
<h1>{{ this.h1 }}</h1>
<h2>{{ this.h2 }}</h2>
</div>
<div class="header-right">
<div class="header-right-inner">
<h4 v-if="this.done !== 'true'">Step {{ this.stepCurrent }} of {{ this.stepMax }}</h4>
<h4 v-else>Ready to mint!</h4>
<div class="progress-bar">
<div class="progress" :style="progressBarWidth" />
</div>
</div>
</div>
</header>
</div>
</template>
<style scoped>
header {
display: grid;
grid-template-columns: 75% 25%;
margin-bottom: 72px;
}
header h2 {
font-size: 24px;
line-height: 34px;
font-weight: normal;
color: var(--offwhite);
}
h4 {
font-size: 20px;
line-height: 30px;
font-weight: normal;
white-space: nowrap;
margin-right: 16px;
}
.header-right-inner {
margin-top: 10px;
display: flex;
align-items: center;
}
.progress-bar {
height: 9px;
width: 100%;
border-radius: 40px;
background-color: var(--gray);
}
.progress {
height: 100%;
width: var(--progress-bar-width);
border-radius: 40px;
background-color: var(--green-success);
}
</style>
<script>
export default {
props: ["h1", "h2", "stepCurrent", "stepMax", "done"],
computed: {
progressBarWidth() {
let progress = 0;
if (this.done) progress = 100;
else progress = (this.stepCurrent / this.stepMax) * 100;
return {
"--progress-bar-width": progress + "%",
};
},
},
};
</script>

View File

@ -0,0 +1,39 @@
<template>
<span v-if="isEllipsized()" class="ellipsized" v-bind:title="content"
><span class="ellipsized-content">{{ ellipsizedContent }}</span
><span class="ellipsis"></span></span
>
<span v-else>{{ content }}</span>
</template>
<style scoped></style>
<script>
export default {
data: function() {
return {};
},
computed: {
ellipsizedContent: function() {
if (this.isEllipsized()) {
return this.content.substring(0, this.maxLength - 3);
} else {
return this.content;
}
},
},
methods: {
isEllipsized: function() {
return this.content.length > this.maxLength;
},
},
props: {
maxLength: {
type: Number,
default: 14,
},
content: String, // regular string NOT html
},
async created() {},
};
</script>

View File

@ -0,0 +1,141 @@
<template>
<div class="form-extra">
<h3 class="extra-properties-header">Extra Properties</h3>
<div class="extra-properties-description">Arbitrary data about your NFT (like game stats!) can be added here.</div>
<table class="form-extra-properties">
<thead>
<tr>
<th>Property Name</th>
<th>Property Value</th>
<th>&nbsp;</th>
</tr>
<tr>
<td>
<input type="text" :id="'property-' + $parent.id + 'name'" v-model="propertyName" />
</td>
<td>
<input type="text" :id="'property-' + $parent.id + 'value'" v-model="propertyValue" />
</td>
<td>
<button @click="addProperty">Add</button>
</td>
</tr>
<tr>
<th colspan="3" class="error">{{ error }}</th>
</tr>
</thead>
<tbody v-if="value.length > 0">
<tr v-for="(p, i) in value" v-bind:key="p.name">
<td>{{ p.name }}</td>
<td>{{ p.value }}</td>
<td><button @click="removeProperty(i)">Remove</button></td>
</tr>
</tbody>
<tbody v-else>
<tr>
<td colspan="3"><span class="extra-properties-empty">No extra properties.</span></td>
</tr>
</tbody>
</table>
</div>
</template>
<style scoped>
table.form-extra-properties th {
color: white;
}
.extra-properties-description {
font-size: smaller;
font-style: italic;
}
.extra-properties-empty {
font-size: smaller;
font-style: italic;
}
</style>
<script>
import { bus } from "../main";
export default {
data: function() {
return {
error: "",
propertyName: "",
propertyValue: "",
};
},
props: ["value"],
async created() {
bus.$on("validate", this.validateHandler);
this.$once("hook:beforeDestroy", () => {
console.log("Destroying validate handler.");
bus.$off("validate", this.validateHandler);
});
bus.$on("clearform", () => {
this.error = "";
});
},
async mounted() {},
methods: {
// only event bus call this
validateHandler() {
const isValidated = this.validate();
bus.$emit("validated", { validated: isValidated });
},
validate() {
if (this.propertyName || this.propertyValue) {
this.error = "Did you forget to add a property?";
return false;
} else {
return true;
}
},
containsProperty(name) {
for (const property of this.value) {
if (property.name == name) return true;
}
return false;
},
addProperty() {
const BLACKLISTED_NAMES = ["id", "set", "number", "of", "adult", "name", "description", "extra", "blobUrl"];
const blacklisted = BLACKLISTED_NAMES.includes(this.propertyName.toLowerCase());
this.propertyName = this.propertyName.trim().toLowerCase();
this.propertyValue = this.propertyValue.trim();
if (blacklisted) {
this.error = "You can't use this property name.";
return;
} else if (!this.propertyName || !this.propertyValue) {
this.error = "Name and value are required.";
return;
} else if (this.propertyName.length > 40) {
this.error = "Property name must be only 40 characters.";
return;
} else if (this.propertyValue.length > 1000) {
this.error = "Property value must be less than 1000 characters.";
return;
} else if (this.containsProperty(this.propertyName)) {
this.error = "Property already exists.";
return;
} else {
let newValue = this.propertyValue;
if (isNaN(newValue)) {
console.log("Leaving value a string");
} else {
newValue = Number(newValue);
}
const index = typeof this.$parent.index === "undefined" ? 0 : this.$parent.index;
this.$store.commit("addSimpleSetFormProperty", { index, name: this.propertyName, value: newValue });
this.propertyName = "";
this.propertyValue = "";
this.error = "";
}
},
removeProperty(i) {
const index = typeof this.$parent.index === "undefined" ? 0 : this.$parent.index;
this.$store.commit("removeSimpleSetFormProperty", { index, subscript: i });
},
},
};
</script>

View File

@ -0,0 +1,163 @@
<template>
<div class="form-extra">
<h3 class="extra-properties-header">Extra Properties:</h3>
<div class="blurb">Arbitrary data about your NFT (like game stats!) can be added here.</div>
<table class="form-extra-properties">
<thead>
<tr>
<th>Property Name</th>
<th>Property Value</th>
<th>&nbsp;</th>
</tr>
<tr>
<td>
<input type="text" :id="'property-' + id + '-name'" v-model="name" />
</td>
<td>
<input type="text" :id="'property-' + id + '-value'" v-model="value" />
</td>
<td>
<button @click="add">Add</button>
</td>
</tr>
<tr>
<th colspan="3" class="error">{{ error }}</th>
</tr>
</thead>
<tbody v-if="properties.length">
<tr v-for="(p, i) in properties" v-bind:key="p.name">
<td>{{ p.name }}</td>
<td>{{ p.value }}</td>
<td><button @click="remove(i)">Remove</button></td>
</tr>
</tbody>
<tbody v-else>
<tr>
<td colspan="3"><span class="blurb">No extra properties defined yet</span></td>
</tr>
</tbody>
</table>
</div>
</template>
<style scoped>
table.form-extra-properties .error {
font-weight: bold;
}
table.form-extra-properties th {
/*color: white;*/
}
button {
background-color: var(--dark);
color: var(--white);
cursor: pointer;
margin-top: 0;
margin-bottom: 0;
padding-top: 0;
padding-bottom: 0;
}
.blurb {
font-size: smaller;
font-style: italic;
}
</style>
<script>
import { bus } from "../main";
const BLACKLISTED_NAMES = ["id", "set", "number", "of", "adult", "name", "description", "extra", "blobUrl", "tags"];
export default {
data() {
return {
error: "",
name: "",
value: "",
properties: [],
};
},
props: ["id", "preload"],
async created() {
if (this.preload && this.preload.length > 0) {
for (const p of this.preload) {
this.properties.push({ name: p.name, value: p.value });
}
}
bus.$on("ExtraPropertiesGet", this.get);
bus.$on("validate", this.handleValidation);
this.$once("hook:beforeDestroy", () => {
bus.$off("ExtraPropertiesGet", this.get);
bus.$off("validate", this.handleValidation);
});
},
methods: {
add() {
if (this.validated()) {
const value = isNaN(this.value) ? this.value : Number(this.value);
this.properties.push({ name: this.name, value: this.value });
bus.$emit("ExtraPropertiesAdd", { id: this.id, name: this.name, value });
this.error = "";
this.name = "";
this.value = "";
}
},
remove(subscript) {
this.$delete(this.properties, subscript);
bus.$emit("ExtraPropertiesRemove", { id: this.id, subscript });
},
get() {
bus.$emit("ExtraPropertiesReceive", { id: this.id, properties: this.properties });
},
contains(name) {
for (const property of this.properties) {
if (property.name == name) return true;
}
return false;
},
validated() {
this.error = "";
this.name = this.name.trim().toLowerCase();
this.value = this.value.trim();
if (BLACKLISTED_NAMES.includes(this.name)) {
this.error = "You can't use this property name.";
return false;
} else if (this.name.length == 0) {
this.error = "Property name cannot be empty.";
return false;
} else if (this.value.length == 0) {
this.error = "Property value cannot be empty.";
return false;
} else if (this.name.length > 40) {
this.error = "Property name must be only 40 characters.";
return false;
} else if (this.value.length > 1000) {
this.error = "Property value must be less than 1000 characters.";
return false;
} else if (this.contains(this.name)) {
this.error = "Property already exists.";
return false;
}
return true;
},
handleValidation(p) {
/* This should only be called by an event, it's different than the ordinary validate. */
if (!p || ((typeof p.component === "undefined" || p.component === "ExtraPropertiesV2") && (typeof p.id === "undefined" || p.id === this.id))) {
this.error = "";
let isValidated = true;
if (this.name.length || this.value.length) {
this.error = "Did you forget to add a property?";
isValidated = false;
}
bus.$emit("validated", { component: "ExtraPropertiesV2", id: this.id, validated: isValidated });
}
},
},
};
</script>

View File

@ -0,0 +1,201 @@
<template>
<section class="featured">
<router-link :to="link" class="image-link">
<img class="image" v-bind:src="image" />
</router-link>
<div class="featured-content">
<div class="featured-content-inner">
<UserLink :name="creatorName" :address="store.creator" :verified="verified" />
<router-link :to="link"
><h1>{{ title }}</h1></router-link
>
<p class="price-title">Current price</p>
<Price class="price-crypto" v-if="nft.ethForSale" :price="nft.ethPrice" symbol="UBQ" />
<Price class="price-crypto" v-else-if="nft.tokenForSale" :price="nft.tokenPrice" symbol="GRANS" />
<span class="price-crypto" v-else>Not for sale</span>
<p v-if="priceUsd" class="price-usd">${{ (priceUsd / 1e18).toFixed(2) }}</p>
<div class="button-container">
<router-link class="buy-button button-square-accent" :to="link" v-if="nft.ethForSale || nft.tokenForSale">Buy Now</router-link>
<router-link class="view-button button-square-white" :to="link">View Artwork</router-link>
</div>
</div>
</div>
</section>
</template>
<style scoped>
.featured {
display: flex;
max-width: 1000px;
margin: auto;
justify-content: center;
}
.featured .image-link {
margin-right: 40px;
}
.featured a.image-link {
display: flex;
align-items: center;
}
.featured img {
border-radius: 16px;
max-height: 500px;
max-width: 500px;
}
.featured .featured-content {
margin-top: 28px;
margin-left: 40px;
display: flex;
align-items: center;
}
.featured .featured-content-inner {
max-width: 100%;
}
.featured h1 {
margin-top: 20px;
margin-bottom: 40px;
overflow-wrap: break-word;
}
.featured .price-title {
margin-bottom: 8px;
font-size: 16px;
text-transform: uppercase;
}
.featured .price-crypto {
font-size: 32px;
line-height: 32px;
font-weight: 700;
}
.featured .price-usd {
margin-top: 14px;
color: var(--offwhite);
}
.featured .button-container {
margin-top: 40px;
text-transform: uppercase;
text-decoration: none;
display: flex;
font-weight: 600;
}
.featured .buy-button {
margin-left: 0;
margin-right: 24px;
}
</style>
<script>
import shared from "../shared";
import UserLink from "./UserLink.vue";
import Price from "./Price.vue";
export default {
props: ["nft"],
components: {
UserLink,
Price,
},
data() {
return {
ethPrice: 0,
tokenPrice: 0,
verified: false,
};
},
async created() {
const price = await shared.getPrice(this.$apiBase);
this.ethPrice = price.ubiqUsdRatio;
this.tokenPrice = price.ubiqUsdRatio / price.ubiqGransRatio;
},
computed: {
title() {
if (this.nft.metadata) return this.nft.metadata.name;
else return "untitled";
},
set() {
return this.nft !== null && this.nft.cnt > 1;
},
link() {
if (this.nft && this.nft.store) {
if (this.set) {
return { name: "NFT", params: { collection: this.nft.store, nft: this.nft.metaId } };
} else {
return { name: "NFT", params: { collection: this.nft.store, nft: this.nft.nftId } };
}
}
return null;
},
image() {
let src;
if (this.nft.metadata) {
if (this.nft.metadata.preview) {
src = this.nft.metadata.preview;
} else {
src = this.nft.metadata.image;
}
} else {
src = "/question.png";
}
if (src.startsWith("ipfs") || src.startsWith("Qm")) {
src = shared.formatIpfsLink(this.$ipfsPrefix, src);
}
return src;
},
priceUsd() {
if (this.nft.ethForSale) {
return this.ethPrice * this.nft.ethPrice;
} else if (this.nft.tokenForSale) {
return this.tokenPrice * this.nft.tokenPrice;
}
return null;
},
},
asyncComputed: {
store: {
async get() {
return await shared.fetchJson(`${this.$apiBase}/api/store/${this.nft.store}`);
},
default: {
address: "0x0000000000000000000000000000000000000000",
name: "Nothing",
symbol: "NTHNG",
creator: "0x0000000000000000000000000000000000000000",
},
},
creatorName: {
async get() {
if (this.store.creator == "0x0000000000000000000000000000000000000000") return null;
try {
const u = await shared.fetchJson(`${this.$apiBase}/api/user/${this.store.creator}`);
this.verified = u.verified;
return u ? u.username : null;
} catch {
return null;
}
},
default: "Loading...",
},
},
};
</script>

View File

@ -0,0 +1,88 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br />
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li>
<a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a>
</li>
<li>
<a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a>
</li>
<li>
<a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a>
</li>
<li>
<a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a>
</li>
</ul>
<h3>Essential Links</h3>
<ul>
<li>
<a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
</li>
<li>
<a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a>
</li>
<li>
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a>
</li>
<li>
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a>
</li>
<li>
<a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
</li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li>
<a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a>
</li>
<li>
<a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
</li>
<li>
<a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a>
</li>
<li>
<a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a>
</li>
<li>
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String,
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,103 @@
<template>
<div>
<p v-if="activity.name == 'TransferSingle'">
<span v-if="activity.from == burned" class="green">
Minted
</span>
<span v-else-if="activity.to == burned" class="red">
<span class="gray">{{ edition }}</span> burned by
<UserLink :name="activity.fromUser.username" :address="activity.fromUser.id" :verified="activity.fromUser.verified" :accent="true" />
</span>
<span v-else>
<span class="gray">{{ edition }}</span> Transferred from
<UserLink :name="activity.fromUser.username" :address="activity.fromUser.id" :verified="activity.fromUser.verified" :accent="true" /> to
<UserLink :name="activity.toUser.username" :address="activity.toUser.id" :verified="activity.toUser.verified" :accent="true" />
</span>
</p>
<p v-else-if="activity.name == 'BuySingleNft'">
<span>
<span class="gray">{{ edition }}</span> purchased for <Price :price="activity.price" symbol="UBQ" customFreeMessage="0.000 UBQ" /> by
<UserLink :name="activity.toUser.username" :address="activity.toUser.id" :verified="activity.toUser.verified" :accent="true" />
</span>
</p>
<p v-else-if="activity.name == 'TokenBuySingleNft'">
<span>
<span class="gray">{{ edition }}</span> purchased for <Price :price="activity.tokenPrice" symbol="GRANS" customFreeMessage="0.000 GRANS" /> by
<UserLink :name="activity.toUser.username" :address="activity.toUser.id" :verified="activity.toUser.verified" :accent="true" />
</span>
</p>
<p v-else-if="activity.name == 'PriceChange'">
<span v-if="activity.forSale && activity.tokenForSale">
<span class="gray">{{ edition }}</span> listed for sale by
<UserLink :name="activity.ownerUser.username" :address="activity.ownerUser.id" :verified="activity.ownerUser.verified" :accent="true" /> for
<Price :price="activity.price" symbol="UBQ" customFreeMessage="0.000 UBQ" /> and <Price :price="activity.tokenPrice" symbol="GRANS" customFreeMessage="0.000 GRANS" />
</span>
<span v-else-if="activity.forSale">
<span class="gray">{{ edition }}</span> listed for sale by
<UserLink :name="activity.ownerUser.username" :address="activity.ownerUser.id" :verified="activity.ownerUser.verified" :accent="true" /> for
<Price :price="activity.price" symbol="UBQ" customFreeMessage="0.000 UBQ" />
</span>
<span v-else-if="activity.tokenForSale">
<span class="gray">{{ edition }}</span> listed for sale by
<UserLink :name="activity.ownerUser.username" :address="activity.ownerUser.id" :verified="activity.ownerUser.verified" :accent="true" /> for
<Price :price="activity.tokenPrice" symbol="GRANS" customFreeMessage="0.000 GRANS" />
</span>
<span v-else-if="activity.ownerUser.id != burned">
<span class="gray">{{ edition }}</span> sale cancelled by
<UserLink :name="activity.ownerUser.username" :address="activity.ownerUser.id" :verified="activity.ownerUser.verified" :accent="true" />
</span>
</p>
<p v-else-if="activity.name == 'CreatorTransferred'" class="red">
<span>Collection ownership transferred</span>
</p>
</div>
</template>
<style scoped>
.green {
color: var(--green-success);
}
.red {
color: var(--red);
}
.gray {
color: var(--offwhite);
}
.red .gray {
/* red overwrites gray */
color: var(--red);
}
</style>
<script>
import UserLink from "@/components/UserLink.vue";
import Price from "@/components/Price.vue";
export default {
components: {
UserLink,
Price,
},
data() {
return {
burned: "0x0000000000000000000000000000000000000000",
};
},
props: ["activity", "totalCount"],
computed: {
edition() {
if (this.totalCount > 1) {
return `Edition ${this.activity.metaId}/${this.totalCount}`;
}
return null;
},
},
};
</script>

374
src/components/NftCard.vue Normal file
View File

@ -0,0 +1,374 @@
<template>
<div v-bind:class="['nft-card', burned ? 'burned' : '']">
<router-link :to="link">
<img class="image" v-bind:src="image" />
</router-link>
<div class="top-half">
<router-link :to="link">
<div class="nft-name" v-if="nft.metadata && nft.metadata.name" :title="nft.metadata.name">{{ nft.metadata.name }}</div>
<div class="nft-name" v-else>Unknown</div>
</router-link>
<div class="creator-wrapper">
<UserLink :name="creatorName" :address="creatorAddress" :verified="verified" />
<div v-if="qty > 1" class="quantity-wrapper">
<LayersIcon alt="Quantity" />
<span class="qty">x{{ qty }}</span>
</div>
</div>
</div>
<hr />
<div class="bottom-half">
<div class="nft-price">
<p>Price</p>
<div class="nft-price-value" v-if="nft.ethForSale">
<Price :price="nft.ethPrice" symbol="UBQ" />
</div>
<div class="nft-price-value" v-else-if="nft.tokenForSale">
<Price :price="nft.tokenPrice" symbol="GRANS" />
</div>
<div class="nft-price-value" v-else>
<span>Not for sale</span>
</div>
</div>
<div class="icons-container">
<div class="favorites">
<Heart alt="Favorites" id="favorite" />
<span class="num">{{ favoritesCount || " " }}</span>
</div>
<div class="comments">
<Comment alt="Comments" id="comment" />
<span class="num">{{ commentsCount || " " }}</span>
</div>
</div>
</div>
</div>
</template>
<style>
.tooltip > img {
cursor: help;
}
.nft-card {
text-shadow: none;
max-width: 8em;
}
.nft-card .price-icons img {
width: 14px;
height: 14px;
padding: 0;
margin: none;
position: relative;
}
.nft-card.burned {
background-color: lightpink;
}
.tooltip {
position: relative;
display: inline-block;
}
.tooltip .tooltiptext {
visibility: hidden;
width: 120px;
background-color: black;
color: #fff;
text-align: center;
padding: 5px;
border-radius: 6px;
/* Position the tooltip text - see examples below! */
position: absolute;
z-index: 1;
}
/* Show the tooltip text when you mouse over the tooltip container */
.tooltip:hover .tooltiptext {
visibility: visible;
}
.price-icons {
height: 18px;
}
.nft-card {
display: inline-block;
border-radius: 8px;
min-width: 361px;
max-width: 361px;
height: 648px;
}
.nft-card .image {
display: flex;
border-radius: 8px 8px 0px 0px;
width: 100%;
height: 400px;
object-fit: cover;
}
.nft-card .top-half {
height: 152px;
background-color: var(--card-background);
}
.nft-card .nft-name {
display: block;
color: var(--card-foreground);
font-weight: 600;
font-size: 20px;
line-height: 24px;
padding: 24px 40px;
overflow-wrap: break-word;
overflow: hidden;
height: 48px;
}
.nft-card .nft-user .user-name {
font-size: 14px;
}
.nft-card .creator-wrapper {
padding: 0 40px;
display: flex;
justify-content: space-between;
align-items: center;
}
.nft-card .quantity-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
}
.nft-card .quantity-wrapper svg {
margin: 0 8px;
}
.nft-card .quantity-wrapper .qty {
color: var(--gray-label);
font-size: 14px;
line-height: 14px;
font-weight: 600;
}
.nft-card hr {
border: 1px solid var(--gray);
margin: 0;
}
.nft-card .bottom-half {
display: flex;
justify-content: space-between;
padding: 24px 40px;
color: var(--card-foreground);
background-color: var(--card-background);
}
.nft-card .nft-price {
display: flex;
padding-right: 24px;
flex-direction: column;
text-transform: uppercase;
}
.nft-card .nft-price .nft-price-value {
font-weight: bold;
}
.nft-card .icons-container {
display: flex;
align-items: center;
}
.nft-card .favorites {
display: flex;
align-items: center;
margin-right: 12px;
}
.nft-card .comments {
display: flex;
align-items: center;
margin-left: 12px;
}
.num {
color: var(--gray-label);
margin-left: 8px;
}
#app .nft-card a {
font-weight: normal;
margin: 0;
padding: 0;
border-radius: none;
}
</style>
<script>
import shared from "../shared";
import UserLink from "./UserLink.vue";
import Price from "./Price.vue";
import LayersIcon from "@/assets/images/layers.svg";
import Heart from "@/assets/images/heart.svg";
import Comment from "@/assets/images/comment.svg";
export default {
components: {
UserLink,
Price,
LayersIcon,
Heart,
Comment,
},
props: ["nft", "quantityOverride", "creatorNameOverride", "favoritesOverride"],
async created() {
if (this.nft.metadata == null) {
console.debug("No metadata :-/");
if (this.nft.metadataUri == null) {
console.warn("Encountered broken NFT (no metadata URI)");
} else {
console.debug("But we can have an URI so we can query it :-)");
this.nft.metadata = await this.fetchJson(this.nft.metadataUri);
}
}
},
data() {
return {
verified: false,
};
},
asyncComputed: {
store: {
async get() {
if (this.nft.store) {
return await shared.fetchJson(`${this.$apiBase}/api/store/${this.nft.store}`);
} else {
// return default
return {
address: "0x0000000000000000000000000000000000000000",
name: "Nothing",
symbol: "NTHNG",
creator: "0x0000000000000000000000000000000000000000",
};
}
},
default: {
address: "0x0000000000000000000000000000000000000000",
name: "Nothing",
symbol: "NTHNG",
creator: "0x0000000000000000000000000000000000000000",
},
},
creatorName: {
async get() {
if (this.creatorNameOverride) return this.creatorNameOverride;
if (this.store.creator == "0x0000000000000000000000000000000000000000") return null;
try {
const u = await shared.fetchJson(`${this.$apiBase}/api/user/${this.store.creator}`);
this.verified = u.verified;
return u ? u.username : null;
} catch (e) {
console.warn(`Error loading user by address ${this.store.creator}`, e);
return null;
}
},
default: "Loading...",
},
creatorAddress() {
if (this.store) return this.store.creator;
else return "0x0000000000000000000000000000000000000000"; // FIXME
},
burned() {
return this.nft.owner == "0x0000000000000000000000000000000000000000";
},
adult() {
const val = this?.nft?.metadata?.properties?.adult;
return typeof val === "undefined" ? false : val;
},
set() {
return this.nft !== null && this.nft.cnt > 1;
},
qty() {
if (this.quantityOverride) return this.quantityOverride;
else if (this.nft.cnt) return this.nft.cnt;
else return 1;
},
link: {
async get() {
if (this.nft && this.nft.store) {
if (this.nft.metaId) {
return { name: "NFT", params: { collection: this.nft.store, nft: this.nft.metaId } };
} else {
return { name: "NFT", params: { collection: this.nft.store, nft: this.nft.nftId } };
}
}
return "#";
},
default: "#",
},
image() {
let src;
if (this.nft.metadata) {
if (this.nft.metadata.preview) {
src = this.nft.metadata.preview;
} else {
src = this.nft.metadata.image;
}
} else {
src = "/question.png";
}
if (src.startsWith("ipfs") || src.startsWith("Qm")) {
src = shared.formatIpfsLink(this.$ipfsPrefix, src);
}
return src;
},
favoritesCount: {
async get() {
if (this.favoritesOverride) return this.favoritesOverride;
if (this.nft.store && this.nft.metaId) {
const response = await shared.fetchJson(`${this.$apiBase}/api/store/${this.nft.store}/meta/${this.nft.metaId}/favorites/count`);
return response.count || "0";
}
return null;
},
default: null,
},
commentsCount: {
async get() {
if (this.commentsOverride) return this.commentsOverride;
if (this.nft.store && this.nft.metaId) {
const response = await shared.fetchJson(`${this.$apiBase}/api/store/${this.nft.store}/meta/${this.nft.metaId}/comments/count`);
return response.count || "0";
}
return null;
},
default: null,
},
},
async mounted() {},
methods: {
async fetchJson(url) {
return await (await fetch(url)).json();
},
},
};
</script>

View File

@ -0,0 +1,257 @@
<template>
<div>
<!--<h2>August 2021</h2>-->
<h3 v-if="altMessage" class="alt-message">{{ altMessage }}</h3>
<article v-for="(entry, index) of entries" :key="index" class="notifications-page-entry">
{{ /*entry*/ }}
<div v-if="entry.type == 'Comment'" class="content-container">
<abbr class="time" :title="absoluteTime(+new Date(entry.date))">{{ relativeTime(+new Date(entry.date)) }}</abbr>
<h3>
<UserLink :name="userData[entry.event.author].name" :address="entry.event.author" :verified="userData[entry.event.author].verified" :accent="true" /> said "<Ellipsis
:maxLength="40"
:content="entry.event.content"
/>"
</h3>
</div>
<div v-else-if="entry.type == 'BuySingleNft' || entry.type == 'TokenBuySingleNft'" class="content-container">
<abbr class="time" :title="absoluteTime(+new Date(entry.date))">{{ relativeTime(+new Date(entry.date)) }}</abbr>
<h3>{{ entry.metadata.name }}</h3>
<p class="entry-description">Sold for <Price :price="entry.event.price" :symbol="entry.type == 'BuySingleNft' ? 'UBQ' : 'GRANS'" customFreeMessage="0 (free!)" /></p>
</div>
<div v-else-if="entry.type == 'Resale' || entry.type == 'TokenResale'" class="content-container">
<abbr class="time" :title="absoluteTime(+new Date(entry.date))">{{ relativeTime(+new Date(entry.date)) }}</abbr>
<h3>{{ entry.metadata.name }}</h3>
<p class="entry-description">
Resold by <UserLink :name="userData[entry.event.from].name" :address="entry.event.from" :verified="userData[entry.event.from].verified" :accent="true" /> for
<Price :price="entry.event.price" :symbol="entry.type == 'Resale' ? 'UBQ' : 'GRANS'" customFreeMessage="0 (free!)" />
</p>
</div>
<div class="button-container">
<button v-if="entry.event.id" @click="goToNft({ collection: entry.store, nft: entry.event.id })" class="button button-round-outline view-nft">View</button>
<button v-else @click="goToNft({ collection: entry.store, nft: entry.event.meta_id })" class="button button-round-outline view-nft">View</button>
</div>
</article>
<div v-if="!page">
<button @click="goToNotificationsPage" class="button button-round-accent view-more">View all notifications</button>
</div>
<div class="pagination" v-else>
<button :class="{ disabled: page <= 1 }" @click="decrementPage" class="button button-round-accent view-more"> Prev Page</button>
<button :class="{ disabled: !nextPageAvailable }" @click="incrementPage" class="button button-round-accent view-more">Next Page </button>
</div>
</div>
</template>
<style scoped>
h2 {
color: var(--offwhite);
font-size: 24px;
line-height: 34px;
font-weight: 400;
padding-bottom: 16px;
border-bottom: var(--border-soft);
margin-bottom: 24px;
}
h3.alt-message {
font-weight: 600;
color: var(--white);
}
.light h3.alt-message {
color: var(--black);
}
.notifications-page-entry {
display: flex;
justify-content: space-between;
padding-top: 24px;
padding-bottom: 24px;
border-bottom: 1px solid #eff0f7;
}
.notifications-page-entry:first-child {
padding-top: 0;
}
.notifications-page-entry:last-child {
padding-bottom: 0;
}
.content-container {
display: flex;
flex-direction: column;
}
h3 {
margin-top: 8px;
margin-bottom: 8px;
font-weight: 700;
}
.time {
text-transform: uppercase;
color: var(--gray-label);
}
.entry-description {
text-transform: uppercase;
margin: 0;
}
.view-more {
margin-top: 24px;
margin-bottom: 0;
}
.pagination {
display: flex;
justify-content: space-between;
}
.light .view-nft:hover {
background-color: #ffffff;
}
.view-nft:hover {
background-color: #303030;
}
.light {
color: var(--black);
}
.light .entry-description {
color: var(--card-background);
}
.light .button.button-round-outline {
color: var(--black);
box-shadow: 0 0 0 1pt var(--black);
}
</style>
<script>
import shared from "@/shared";
import Price from "@/components/Price.vue";
import Ellipsis from "@/components/Ellipsis.vue";
import UserLink from "@/components/UserLink.vue";
export default {
components: {
Price,
Ellipsis,
UserLink,
},
props: ["page"],
data() {
return {
altMessage: "",
nextPageAvailable: false,
userCache: {},
userData: {},
};
},
computed: {
user() {
return this.$store.state.user;
},
},
asyncComputed: {
entries: {
async get() {
this.altMessage = "Loading...";
const resultsPerPage = 20;
let pageClause = `page=1`;
if (this.page) {
pageClause = `page=${this.page}&resultsPerPage=${resultsPerPage}`;
}
let res;
try {
res = await shared.fetchJson(`${this.$apiBase}/api/notifications/${this.user.id}?${pageClause}`);
} catch (err) {
console.error("Unable to fetch notifications", err);
this.altMessage = "Error: Unable to fetch notifications.";
return [];
}
// for comments and resales, look up author's name, populate this.userData
for (let i = 0; i < res.length; i++) {
let addr;
if (res[i].type == "Comment") {
addr = res[i].event.author;
} else if (res[i].type == "Resale" || res[i].type == "TokenResale") {
addr = res[i].event.from;
}
if (addr) {
const user = await this.fetchUser(addr);
this.userData[addr] = {
name: user.username,
verified: user.verified,
};
}
}
// asynchronously fire off a lookahead query to see if there is another page available
if (this.page) {
(async () => {
const nextPageClause = `page=${this.page + 1}&resultsPerPage=${resultsPerPage}`;
const next = await shared.fetchJson(`${this.$apiBase}/api/notifications/${this.user.id}?${nextPageClause}`);
this.nextPageAvailable = next.length > 0;
})();
}
//console.log("fetched notifications: " + JSON.stringify(res, null, 2));
if (res.length == 0) {
this.altMessage = "No notifications to show.";
} else {
this.altMessage = "";
}
return res;
},
default: [],
},
},
methods: {
goToNft(params) {
if (this.$parent.hideNotifications) this.$parent.hideNotifications();
this.$router.push({ name: "NFT", params: params });
},
goToNotificationsPage() {
if (this.$parent.hideNotifications) this.$parent.hideNotifications();
this.$router.push("/notifications");
},
decrementPage() {
this.$router.push(`/notifications/page/${this.page - 1}`);
},
incrementPage() {
this.$router.push(`/notifications/page/${this.page + 1}`);
},
relativeTime(timestamp) {
return shared.relativeTime(timestamp);
},
absoluteTime(timestamp) {
return shared.absoluteTime(timestamp);
},
async fetchUser(address) {
if (!(address in this.userCache)) {
const userFetched = await shared.fetchJson(`${this.$apiBase}/api/user/${address}`);
this.userCache[address] = userFetched;
}
return this.userCache[address];
},
},
};
</script>

24
src/components/Price.vue Normal file
View File

@ -0,0 +1,24 @@
<template>
<span>
{{ prettyPrice }}
</span>
</template>
<style scoped></style>
<script>
import shared from "@/shared";
export default {
props: ["price", "symbol", "customFreeMessage"],
computed: {
prettyPrice() {
if (this.price == 0) {
return this.customFreeMessage || "Free!";
} else {
return shared.formatPrice(this.price) + " " + this.symbol;
}
},
},
};
</script>

View File

@ -0,0 +1,79 @@
<template>
<div class="nft-user">
<router-link v-if="addressComputed !== burned" :to="{ name: 'Address', params: { address: nameComputed } }" class="user-name" :class="{ accent: accent }">
<span>@{{ nameComputed }}</span>
<VerifiedIcon class="verified-user" v-if="verified" />
</router-link>
<span v-else class="user-name">
{{ nameComputed }}
</span>
</div>
</template>
<style scoped>
.nft-user {
display: inline-flex;
max-width: 100%;
white-space: nowrap;
vertical-align: middle;
}
.nft-user img {
border-radius: 100%;
width: 26px;
margin-right: 10px;
}
.nft-user .user-name {
display: flex;
align-items: center;
text-decoration: none;
color: var(--white);
max-width: 100%;
overflow-x: hidden;
text-overflow: ellipsis;
line-height: 23px;
}
.nft-user .user-name.accent {
color: var(--accent);
font-weight: 600;
}
.nft-user .user-name .verified-user {
max-height: 16px;
margin-left: 4px;
min-width: 16px;
}
</style>
<script>
import VerifiedIcon from "@/assets/images/verified.svg";
export default {
components: {
VerifiedIcon,
},
props: ["name", "address", "accent", "verified"],
data() {
return {
burned: "0x0000000000000000000000000000000000000000",
};
},
computed: {
addressComputed() {
if (!this.address) {
return "0x0000000000000000000000000000000000000000";
}
return this.address;
},
nameComputed() {
if (!this.name) {
// if empty name provided, the user is probably not registered. show address instead
return this.addressComputed;
}
return this.name;
},
},
};
</script>

61
src/main.js Normal file
View File

@ -0,0 +1,61 @@
import Vue from "vue";
import VueMeta from "vue-meta";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import shared from "./shared";
import AsyncComputed from "vue-async-computed";
import "./../node_modules/basiclightbox/dist/basicLightbox.min.css";
import "./assets/reset.css";
import "./assets/global.css";
import "./assets/minimodal.css";
import VueGtag from "vue-gtag";
import localForage from "localforage";
import VueLocalForage from "vue-localforage";
Vue.use(VueLocalForage);
Vue.prototype.$storageConfig({
driver: localForage.INDEXEDDB,
name: "nft-store-web",
});
Vue.use(AsyncComputed);
Vue.use(VueMeta);
Vue.use(VueGtag, {
config: {
id: process.env.VUE_APP_GTAG_ID,
params: {
send_page_view: true,
},
},
});
export const bus = new Vue();
Vue.config.productionTip = false;
Vue.prototype.$appLoaded = false;
Vue.prototype.$factoryAddress = process.env.VUE_APP_FACTORY;
Vue.prototype.$tokenAddress = process.env.VUE_APP_TOKEN;
Vue.prototype.$tokenDecimals = 18;
Vue.prototype.$apiBase = process.env.VUE_APP_API_BASE;
Vue.prototype.$ipfsPrefix = process.env.VUE_APP_IPFS_PREFIX;
// load contract ABIs to memory
Vue.prototype.$storeAbiPromise = shared.fetchJson("/store.abi.json");
Vue.prototype.$factoryAbiPromise = shared.fetchJson("/factory.abi.json");
Vue.prototype.$tokenAbiPromise = shared.fetchJson("/token.abi.json");
Vue.prototype.$userSetupLoaded = false;
window.vm = new Vue({
router,
store,
data: {
web3: null,
},
render: (h) => h(App),
}).$mount("#app");

216
src/router/index.js Normal file
View File

@ -0,0 +1,216 @@
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";
import About from "../views/About.vue";
import Terms from "../views/Terms.vue";
import PrivacyPolicy from "../views/Privacy.vue";
import Collection from "../views/Collection.vue";
import Nft from "../views/NftV3.vue";
import User from "../views/Settings.vue";
import CreateNftGettingStarted from "../views/CreateNft/GettingStarted.vue";
import CreateNftUpload from "../views/CreateNft/Upload.vue";
import CreateNftDetails from "../views/CreateNft/Details.vue";
import CreateNftSetPrice from "../views/CreateNft/SetPrice.vue";
import CreateNftSelectCollection from "../views/CreateNft/SelectCollection.vue";
import CreateNftConfirm from "../views/CreateNft/Confirm.vue";
import CreateNftMint from "../views/CreateNft/Mint.vue";
import Address from "../views/Address.vue";
import NotificationsView from "../views/NotificationsView.vue";
import PageNotFound from "../views/PageNotFound";
Vue.use(VueRouter);
const routes = [
{
path: "*",
component: PageNotFound,
meta: {
title: "404 page not found",
},
},
{
path: "/",
name: "Home",
component: Home,
meta: {
title: "token gallery",
},
},
{
path: "/page/:page",
name: "Home",
component: Home,
meta: {
title: "token gallery",
},
},
{
path: "/about",
name: "About",
component: About,
meta: {
title: "token gallery",
},
},
{
path: "/privacy",
name: "PrivacyPolicy",
component: PrivacyPolicy,
meta: {
title: "token gallery privacy policy",
},
},
{
path: "/terms",
name: "TermsOfService",
component: Terms,
meta: {
title: "token gallery terms of service",
},
},
{
name: "Collection",
path: "/collection/:collection",
component: Collection,
meta: {
title: "token gallery collection",
},
},
{
name: "Collection",
path: "/collection/:collection/page/:page",
component: Collection,
meta: {
title: "token gallery - collection",
},
},
{
name: "NFT",
path: "/collection/:collection/nft/:nft",
component: Nft,
meta: {
title: "token gallery nft",
},
},
{
name: "Settings",
path: "/settings",
component: User,
meta: {
title: "token gallery user settings",
},
},
{
name: "Create NFT",
path: "/createnft",
component: CreateNftGettingStarted,
meta: {
title: "token gallery create nft",
},
},
{
path: "/createnft/upload",
component: CreateNftUpload,
meta: {
title: "token gallery create nft Upload",
},
},
{
path: "/createnft/details",
component: CreateNftDetails,
meta: {
title: "token gallery create nft Enter Details",
},
},
{
path: "/createnft/setprice",
component: CreateNftSetPrice,
meta: {
title: "token gallery create nft Set Price",
},
},
{
path: "/createnft/selectcollection",
component: CreateNftSelectCollection,
meta: {
title: "token gallery create nft Select a Collection",
},
},
{
path: "/createnft/confirm",
component: CreateNftConfirm,
meta: {
title: "token gallery create nft Confirm Creation",
},
},
{
path: "/createnft/mint",
component: CreateNftMint,
meta: {
title: "token gallery create nft Minting NFT...",
},
},
{
name: "Address",
path: "/address/:address",
component: Address,
meta: {
title: "token gallery - user",
},
},
{
name: "Address",
path: "/address/:address/tab/:tab",
component: Address,
meta: {
title: "token gallery - user",
},
},
{
name: "Address",
path: "/address/:address/tab/:tab/page/:page",
component: Address,
meta: {
title: "token gallery - user",
},
},
{
name: "Notifications",
path: "/notifications",
component: NotificationsView,
meta: {
title: "token gallery - notifications",
},
},
{
name: "Notifications",
path: "/notifications/page/:page",
component: NotificationsView,
meta: {
title: "token gallery - notifications",
},
},
];
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes,
scrollBehavior() {
return { x: 0, y: 0 };
},
});
router.beforeEach((to, from, next) => {
const nearestWithTitle = to.matched
.slice()
.reverse()
.find((r) => r.meta && r.meta.title);
if (nearestWithTitle) document.title = nearestWithTitle.meta.title;
next();
});
export default router;

267
src/shared.js Normal file
View File

@ -0,0 +1,267 @@
import { bus } from "./main";
const BigNumber = require("bignumber.js");
BigNumber.set({ DECIMAL_PLACES: 18, ROUNDING_MODE: BigNumber.ROUND_FLOOR });
export default {
propertyNameBlacklist: ["id", "set", "number", "of", "adult", "name", "description", "extra", "blobUrl", "tags"],
async fetchJson(url) {
return await (await fetch(url)).json();
},
formatIpfsLink(prefix, ipfsStr) {
if (ipfsStr.startsWith("ipfs://")) {
ipfsStr = ipfsStr.substring(7);
}
return prefix + ipfsStr;
},
validateNftForm() {
if (this.nftName == null || this.nftName.length == 0) {
this.error = "Name is required";
return false;
} else if (this.nftName.length > 40) {
this.error = "Name may only be 40 characters";
return false;
} else if (this.nftDescription == null || this.nftDescription.length == 0) {
this.error = "Description is required";
return false;
} else if (this.nftDescription.length > 1000) {
this.error = "Description must only be 1000 characters";
return false;
} else {
return true;
}
},
// Eventually will replace the other one.
newValidateNftForm(obj) {
const ARBITRARY_FIELD_MAX = 1000;
const NAME_MAX = 40;
const SET_MAX = 100;
const errors = [];
if (obj.name == null || obj.name.length == 0) {
errors.push("Name is required");
}
if (obj.name && obj.name.length > NAME_MAX) {
errors.push(`Name may only be ${NAME_MAX} characters`);
}
if (obj.description && obj.description.length > ARBITRARY_FIELD_MAX) {
errors.push(`Description may only be ${ARBITRARY_FIELD_MAX} characters`);
}
if (typeof obj.set !== "undefined" && obj.set.length > SET_MAX) {
errors.push(`Set may only be ${SET_MAX} characters`);
}
return errors;
},
blobToDataUrl: (blob) => {
const reader = new FileReader();
reader.readAsDataURL(blob);
return new Promise((resolve) => {
reader.onloadend = () => {
resolve(reader.result);
};
});
},
dataUrlToBlob: (data) => {
return fetch(data).then((res) => res.blob());
},
async getBaseNft(storeAddress) {
const bases = await (await fetch(`${this.$apiBase}/api/store/${storeAddress}/base`)).json();
return bases.length == 0 ? null : bases[0].nftId;
},
async uploadForm(url, form, method, hashString_suffix, account, token, web3Utils, expectResponse = true) {
const path = new URL(url).pathname;
const now = new Date().toISOString();
account = web3Utils.toChecksumAddress(account);
let hashString = `${now} ${account} ${token} ${path}`;
if (hashString_suffix) {
hashString = hashString + ` ${hashString_suffix}`;
}
console.debug(`Sending hash of message '${hashString}'`);
const hash = web3Utils.keccak256(hashString); // keccak doesn't need HMAC
const responsePromise = await fetch(url, {
method: method,
body: form,
headers: {
Authorization: `Bearer ${hash}`,
"X-NftStore-Account": account,
"X-NftStore-Now": now,
},
}).then(async (response) => {
let json;
try {
json = await response.json();
} catch (error) {
// If we weren't expecting a response, just return empty
// If we were, then something went wrong during json parsing- throw
if (!expectResponse) {
return;
} else {
throw error;
}
}
if (!response.ok) {
if (json.error) {
throw new Error(`Backend error ${response.status}: ${json.error}`);
} else {
throw new Error(`Backend returned unspecified error (status ${response.status}`);
}
}
// no error- return response as normal
return json;
});
return await responsePromise;
},
handleTransactionError(error) {
// this function should be used in the .catch handler for transactions
// returns an object like { errorMessage: "Stuff went wrong", errorMessageSecondary: "Stuff went wrong for X reason" }
console.error("error executing transaction; ", error);
let retval = {
errorMessage: "",
errorMessageSecondary: "",
};
if (error.code == 4001) {
retval.errorMessage = "User canceled the request";
} else if (typeof error.code !== "undefined") {
retval.errorMessage = "Error " + error.code;
retval.errorMessageSecondary = error.message;
} else if (error.message.startsWith("Transaction has been reverted by the EVM")) {
retval.errorMessage = "Transaction was reverted";
retval.errorMessageSecondary = "An error was thrown from the smart contract";
} else {
retval.errorMessage = "Unspecified error";
retval.errorMessageSecondary = error.message;
}
return retval;
},
apiPing() {
return new Promise((resolve) => {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 1000);
try {
fetch(`${this.$apiBase}/api/ping`, { signal: controller.signal })
.then((response) => {
clearTimeout(id);
resolve(response.status == 200);
})
.catch((e) => {
console.error("ping failed 1", e);
resolve(false);
});
} catch (e) {
console.error("ping failed 2", e);
resolve(false);
}
});
},
getExtraProperties(id) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
bus.$off("ExtraPropertiesReceive", handle);
reject("No properties by that id");
}, 1000);
function handle(p) {
if (p.id == id) {
clearTimeout(timeoutId);
bus.$off("ExtraPropertiesReceive", handle);
resolve(p.properties);
}
}
bus.$on("ExtraPropertiesReceive", handle);
bus.$emit("ExtraPropertiesGet");
});
},
clone(obj) {
if (null == obj || "object" != typeof obj) {
console.debug(`I can't clone this: ${typeof obj}`);
return obj;
}
const isArray = Array.isArray(obj);
const copy = isArray ? [] : {}; // does this even matter
const attrs = Object.getOwnPropertyNames(obj);
for (let attr of attrs) {
if (attr === "__ob__") {
console.debug("Ignoring circular reference.");
} else if (isArray && attr === "length") {
console.debug("Ignoring array length");
} else if (Object.prototype.hasOwnProperty.call(obj, attr)) {
if (obj[attr] instanceof Object) {
copy[attr] = this.clone(obj[attr]);
} else {
copy[attr] = obj[attr];
}
}
}
return copy;
},
formatPrice(price) {
if (price === null) return null;
// This is a terrible hack, and i feel bad about it
// but it's too much work to figure out the proper way
let rv;
rv = BigNumber(price)
.dividedBy(1e18)
.toString();
return rv;
},
async getPrice(apiBase) {
try {
const price = await this.fetchJson(`${apiBase}/api/money`);
if (!price.ubiqUsdRatio) {
console.error("Received empty ubiq price response");
return { ubiqUsdRatio: 0, ubiqGransRatio: 1 };
}
return price;
} catch (e) {
console.error("Error fetching ubiq/grans prices", e);
return { ubiqUsdRatio: 0, ubiqGransRatio: 1 };
}
},
relativeTime(timestamp, millis = false) {
// taken from https://stackoverflow.com/a/53800501/5924962
var units = {
year: 24 * 60 * 60 * 1000 * 365,
month: (24 * 60 * 60 * 1000 * 365) / 12,
day: 24 * 60 * 60 * 1000,
hour: 60 * 60 * 1000,
minute: 60 * 1000,
second: 1000,
};
var rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
var getRelativeTime = (date) => {
var elapsed = date - new Date();
// "Math.abs" accounts for both "past" & "future" scenarios
for (var u in units) if (Math.abs(elapsed) > units[u] || u == "second") return rtf.format(Math.round(elapsed / units[u]), u);
};
let multiplier = 1;
if (millis) multiplier = 1000;
const date = new Date(timestamp * multiplier);
return getRelativeTime(date);
},
absoluteTime(timestamp, millis = false) {
let multiplier = 1;
if (millis) multiplier = 1000;
const date = new Date(timestamp * multiplier);
return date.toString();
},
};

238
src/store/index.js Normal file
View File

@ -0,0 +1,238 @@
import Vue from "vue";
import Vuex from "vuex";
import createPersistedState from "vuex-persistedstate";
import { getField, updateField } from "vuex-map-fields";
import { v4 as uuidv4 } from "uuid";
Vue.use(Vuex);
const clone = (obj) => {
if (null == obj || "object" != typeof obj) {
console.log(`I can't clone this: ${typeof obj}`);
return obj;
}
const isArray = Array.isArray(obj);
const copy = isArray ? [] : {}; // does this even matter
const attrs = Object.getOwnPropertyNames(obj);
console.log(`Object has attributes: ${attrs}`);
for (let attr of attrs) {
if (attr === "__ob__") {
console.log("Ignoring circular reference.");
} else if (isArray && attr === "length") {
console.log("Ignoring array length");
} else if (Object.prototype.hasOwnProperty.call(obj, attr)) {
if (obj[attr] instanceof Object) {
copy[attr] = clone(obj[attr]);
} else {
console.log(`copying ${attr}`);
copy[attr] = obj[attr];
}
}
}
return copy;
};
export default new Vuex.Store({
state: {
user: null,
accounts: [],
showUnverified: false,
showAdult: false,
batchCreateNftForm: [],
batchCreateNftImages: {},
simpleSetForms: [],
simpleSetIsCollection: "false",
simpleSetName: "",
latestForSale: false,
showSiteMessage: true,
latestCurrency: "any",
ethPrice: 0,
ethForSale: false,
tokenPrice: 0,
tokenForSale: false,
showCount: null,
lastNotificationCheckDate: "",
},
mutations: {
clearPrices(state) {
state.ethPrice = null;
state.ethForSale = false;
state.tokenPrice = null;
state.tokenForSale = false;
},
updateUbiqPrice(state, price) {
state.ethPrice = price;
},
updateUbiqForSale(state, forSale) {
state.ethForSale = forSale;
},
updateTokenPrice(state, price) {
state.tokenPrice = price;
},
updateTokenForSale(state, forSale) {
state.tokenForSale = forSale;
},
updateUser(state, user) {
state.user = user;
},
clearUser(state) {
state.user = null;
},
updateAccounts(state, accounts) {
state.accounts = accounts;
},
updateLatestForSale(state, showOnlyForSale) {
state.latestForSale = showOnlyForSale;
},
updateLastNotificationCheckDate(state, date) {
state.lastNotificationCheckDate = date;
},
updateShowSiteMessage(state, showSiteMessage) {
state.showSiteMessage = showSiteMessage;
},
updateLatestCurrency(state, currency) {
state.latestCurrency = currency;
},
updateShowUnverified(state, showUnverified) {
state.showUnverified = showUnverified;
},
updateShowAdult(state, showAdult) {
state.showAdult = showAdult;
},
updateField,
updateSimpleSetName(state, name) {
state.simpleSetName = name;
},
updateSimpleSetIsCollection(state, isCollection) {
state.simpleSetIsCollection = isCollection;
},
swapSimpleSetForms(state, p) {
const form1 = state.simpleSetForms[p.index1];
const id1 = form1.id;
const newId1 = uuidv4(); // temporary new id so no clash on insert
const form2 = state.simpleSetForms[p.index2];
const id2 = form2.id;
const newId2 = uuidv4(); // temporary new id so no clash on insert
console.log(`Swapping ${p.index1} and ${p.index2}.`);
const clone1 = clone(form1);
clone1.id = newId1;
const clone2 = clone(form2);
clone2.id = newId2;
p.newId1 = newId1;
p.newId2 = newId2;
Vue.set(state.simpleSetForms, p.index2, clone1);
Vue.set(state.simpleSetForms, p.index1, clone2);
// put the old ids back.
clone1.id = id1;
clone2.id = id2;
},
updateSimpleSetFormBlobUrl(state, p) {
Vue.set(state.simpleSetForms[p.index], "blobUrl", p.value);
},
updateSimpleSetFormField(state, payload) {
Vue.set(state.simpleSetForms[payload.index], payload.field, payload.value);
},
addSimpleSetForm(state, p) {
p.newId = uuidv4();
state.simpleSetForms.push({
id: p.newId,
name: "",
description: "",
adult: false,
count: "",
collection: null,
extra: [], // this is merged into the regular form's properties before upload
prices: {
ethPrice: "",
ethForSale: false,
tokenPrice: "",
tokenForSale: false,
},
});
},
removeSimpleSetForm(state, index) {
const imageId = `image/${state.simpleSetForms[index].id}`;
Vue.prototype.$removeItem(imageId).catch();
Vue.delete(state.simpleSetForms, index);
},
clearSimpleSetForms(state) {
for (let i = state.simpleSetForms.length - 1; i >= 0; --i) {
const imageId = `image/${state.simpleSetForms[i].id}`;
Vue.prototype.$removeItem(imageId).catch();
Vue.delete(state.simpleSetForms, i);
}
state.simpleSetIsCollection = false;
state.simpleSetName = "";
},
addSimpleSetFormProperty(state, p) {
const index = p.index; // index of the form not the item
const name = p.name;
const value = p.value;
//const extraLength = state.simpleSetForms[index].extra.length;
// FIXME: reject if already exists
//Vue.set(state.simpleSetForms[index].extra, extraLength, { name, value });
state.simpleSetForms[index].extra.push({ name, value });
},
removeSimpleSetFormProperty(state, p) {
const index = p.index; // index of form, not item in form
const subscript = p.subscript; // index of property in form
Vue.delete(state.simpleSetForms[index].extra, subscript);
},
/*
addBatchNft(state) {
state.batchCreateNftForm.push({
id: uuidv4(),
name: "",
description: "",
adult: false,
});
},
*/
clearBatchCreateNftForm(state) {
state.batchCreateNftForm = [];
},
clearBatchImages(state) {
state.batchCreateNftImages = {};
},
deleteBatchNft(state, index) {
state.batchCreateNftForm.splice(index, 1);
},
addBatchImage(state, obj) {
Vue.set(state.batchCreateNftImages, obj.id, { name: obj.name, base64: obj.base64 });
},
deleteBatchImage(state, id) {
delete state.batchCreateNftImages[id];
},
setShowCount(state, value) {
state.showCount = !!value;
},
},
getters: {
getUser: (state) => {
return state.user;
},
getAccounts: (state) => {
return state.accounts;
},
isUserAuthenticated: (state) => {
return state.user != null && state.user.token != null;
},
getField,
batchFormEmpty: (state) => {
return state.batchCreateNftForm.length == 0;
},
lastSimpleSetForm: (state) => {
return state.simpleSetForms.length == 0 ? null : state.simpleSetForms[state.simpleSetForms.length - 1];
},
firstSimpleSetForm: (state) => {
return state.simpleSetForms.length == 0 ? null : state.simpleSetForms[0];
},
},
plugins: [createPersistedState()],
});

595
src/views/About.vue Normal file
View File

@ -0,0 +1,595 @@
<template>
<div class="container about">
<header>
<!--<h1>
<img class="token-image" src="/about/header.gif" alt="TOKEN" />
</h1>-->
<h1>welcome to token.gallery take a look around.</h1>
<div class="icons-container">
<div class="icon">
<img src="/about/create.png" />
<p>create</p>
</div>
<div class="icon">
<img src="/about/sell.png" />
<p>sell</p>
</div>
<div class="icon">
<img src="/about/collect.png" />
<p>collect</p>
</div>
</div>
<ArrowDown class="arrow-down" alt="⬇" />
</header>
<div id="about-main">
<section>
<h3 class="title">What</h3>
<p class="what">
Token.gallery is an nft marketplace that supports and showcases creative work. You can create ("mint"), sell, buy, and collect NFTs that live on the blockchain.
</p>
<PageBreak class="page-break" />
</section>
<aside>
<h3 class="title">Who</h3>
<p>
We are developers, artists, and collectors working together to make cool s#!t that explores what blockchain can do.
</p>
</aside>
<section class="main-text">
<h2>Things to know</h2>
<ul class="faq">
<li id="faq-1">
<div class="li-header" @click="toggleItem">
<h3 class="hoverable">How do I create an NFT?</h3>
<EyeClosed class="open-close-icon hoverable" alt="Hide answer" v-if="showFaq['faq-1']" />
<EyeOpen class="open-close-icon hoverable" alt="Show answer" v-else />
</div>
<div class="li-content" v-if="showFaq['faq-1']">
<p>
Each NFT is a unique, ownable, and collectable piece of the digital world just like real meat-world physical collectibles such as Pokémon cards, Magic the
Gathering, or sports memorabilia.
</p>
<h4>Follow this 4-step guide, friend:</h4>
<ol>
<li>
Connect your wallet. (Either <a class="link-accent" href="https://metamask.io/">Metamask</a> or
<a class="link-accent" href="https://ubiqsmart.com/sparrow">Sparrow</a> wallet)
</li>
<li>Click the create button at the top.</li>
<li>Go through our cool ass 5-step minting process.</li>
<li>Boom! Your NFT will be minted onto the Blockchain.</li>
<li>From there you can sell it, share it, or whatever.</li>
</ol>
</div>
</li>
<li id="faq-2">
<div class="li-header" @click="toggleItem">
<h3 class="hoverable">Buying and collecting NFTs</h3>
<EyeClosed class="open-close-icon hoverable" alt="Hide answer" v-if="showFaq['faq-2']" />
<EyeOpen class="open-close-icon hoverable" alt="Show answer" v-else />
</div>
<div class="li-content" v-if="showFaq['faq-2']">
<p>
There are all kinds of people buying NFTs today. Some people buy to support their friends, others are art enthusiasts who simply love supporting artists, and then
there's collectors. Collectors are those who find talent and bring it forward for the world to see.
</p>
<h4>Follow these steps to buy and collect NFTs:</h4>
<ol>
<li>
Connect your wallet. (Either <a class="link-accent" href="https://metamask.io/">Metamask</a> or
<a class="link-accent" href="https://ubiqsmart.com/sparrow">Sparrow</a> wallet)
</li>
<li>Find an NFT you like by searching, filtering, sorting, etc.</li>
<li>Click the buy button.</li>
<li>Make sure you have some UBQ or GRANS in your wallet.</li>
</ol>
</div>
</li>
<li id="faq-3">
<div class="li-header" @click="toggleItem">
<h3 class="hoverable">Fees and Royalties</h3>
<EyeClosed class="open-close-icon hoverable" alt="Hide answer" v-if="showFaq['faq-3']" />
<EyeOpen class="open-close-icon hoverable" alt="Show answer" v-else />
</div>
<div class="li-content" v-if="showFaq['faq-3']">
<p>
There are two types of fees on token.gallery:
</p>
<ul>
<li>Artist royalty fee: <strong>8%</strong></li>
<li>Marketplace fee: <strong>2%</strong></li>
</ul>
<p>
Fees are automatically deducted from each purchase, <strong>including resales</strong>.
<br />
The 8% royalty fee will automatically show up in the creator's wallet.
</p>
</div>
</li>
<li id="faq-4">
<div class="li-header" @click="toggleItem">
<h3 class="hoverable">Token.gallery's unique technical implementation</h3>
<EyeClosed class="open-close-icon hoverable" alt="Hide answer" v-if="showFaq['faq-4']" />
<EyeOpen class="open-close-icon hoverable" alt="Show answer" v-else />
</div>
<div class="li-content" v-if="showFaq['faq-4']">
<p>
Token.gallery is different from other NFT stores. When you mint on a typical ethereum-based NFT service like
<a class="link-accent" href="https://foundation.app/">Foundation</a> or <a class="link-accent" href="https://rarible.com/">Rarible</a>, you're creating a new entry
in their ERC-1155 NFT contract.
</p>
<p>
Token.gallery instead introduces the concept of a <strong>collection-</strong> each collection is an independent
<a class="link-accent" href="https://eips.ethereum.org/EIPS/eip-1155">ERC-1155 contract</a>. Users can create multiple collections, and then mint NFTs independently
within those collections.
</p>
<h4>Advantages</h4>
<p>
Having an independent contract per collection gives users greater control over their NFT creations. Users can transfer entire contract ownership, giving another
person the royalty income from future sales.
</p>
<p>
We've also made it easier to create NFT <strong>sets</strong>. A set is comprised of multiple <strong>editions</strong> of the same artwork. Each edition is
nonfungible, marked with a number (1/10, 2/10, etc), and can be priced/sold/transferred independently.
</p>
<p>
This allows artists to easily create multiple editions of the same artwork at once, instead of tediously creating each NFT in a separate transaction.
</p>
<h4>Why don't other services do this?</h4>
<p>
Creating contracts is a gas-expensive operation. This is cheap on the Ubiq blockchain, but would be prohibitively expensive on Ethereum. Ethereum services instead
opt for one single ERC-1155 contract, or a less versatile ERC-721 contract.
</p>
</div>
</li>
<li id="faq-5">
<div class="li-header" @click="toggleItem">
<h3 class="hoverable">What is verification?</h3>
<EyeClosed class="open-close-icon hoverable" alt="Hide answer" v-if="showFaq['faq-5']" />
<EyeOpen class="open-close-icon hoverable" alt="Show answer" v-else />
</div>
<div class="li-content" v-if="showFaq['faq-5']">
<p>
Verification helps the community build trust between artists and collectors. Token.Gallery is an open platform that allows anyone to mint, buy, and trade NFTs on
the Ubiq Network. However, because of the open nature of blockchain, it can be hard to validate the authenticity of new participants. Verification helps shine light
on artists creating their own work and gives new users a sense of security. There are many content licenses for remixes, reworks, and adaptations and as
Token.Gallery operates out of the United States - the platform must take these licenses seriously. All of these considerations combined is what goes into
verification.
</p>
<p>Simply put, verification is proof that a person has engaged with the community, built trust, and is a known contributor to the platform.</p>
</div>
</li>
<li id="faq-6">
<div class="li-header" @click="toggleItem">
<h3 class="hoverable">How do I get verified on token.gallery?</h3>
<EyeClosed class="open-close-icon hoverable" alt="Hide answer" v-if="showFaq['faq-6']" />
<EyeOpen class="open-close-icon hoverable" alt="Show answer" v-else />
</div>
<div class="li-content" v-if="showFaq['faq-6']">
<p>
Both artists and collectors can be verified. The new way to get verified is to
<a class="link-accent" href="https://rwtb18kz14y.typeform.com/to/HkYq4Xt1">fill out this form</a>.
</p>
<br />
<ol>
<li>
<strong>Were looking for multiple minted (or purchased - for collectors) pieces on token.gallery under your profile.</strong>
<br />
Minting allows us to see what youre planning on showcasing or collecting on token.gallery and helps us verify that the artwork is original and owned by you.
Minting copyrighted work that you do not own or have rights to sell will disqualify you from being verified. If you are using secondary assets in your work please
provide the licenses in your application.
</li>
<li>
<strong>Link us 1 or 2 profiles you have online (other marketplaces, portfolios, or social media)</strong>
<br />
If you have anywhere you showcase your artwork, share it. If youre a collector, show us your collection on other platforms. If youve never collected before,
check out the frontpage!
</li>
<li>
<strong>Share a short artist / collector statement.</strong>
<br />
Write a 2-5 sentence statement of your intent as an artist or collector. What are you exploring? What got you into NFTs? What do you want to share with the
community? Tell us about your art and why you make it. This can be anything youd like, so long as youre helping us understand your intentions.
</li>
<li>
<strong>Get involved in the token.gallery community.</strong>
<br />
Join our Discord or talk with us on Twitter. This helps a ton. The community behind token.gallery is half the fun of NFTs. Getting to know you is key to getting
verified.
</li>
<li>
<strong>Dont message the devs/mods directly about your verification.</strong>
<br />
If youve filled out the form linked above, you can trust that we are reviewing your profile. Messaging us directly about your verification status will not speed
up the process. If you have not been verified within 2 weeks, your verification was denied and you should re-read the rules and requirements and re-apply.
Spamming multiple applications will automatically disqualify you from being verified and potentially lead to account suspension.
</li>
</ol>
</div>
</li>
<li id="faq-7">
<div class="li-header" @click="toggleItem">
<h3 class="hoverable">Why wasnt my profile verified?</h3>
<EyeClosed class="open-close-icon hoverable" alt="Hide answer" v-if="showFaq['faq-7']" />
<EyeOpen class="open-close-icon hoverable" alt="Show answer" v-else />
</div>
<div class="li-content" v-if="showFaq['faq-7']">
<p>
There are multiple reasons why your profile might not be verified. Well list the major ones below.
</p>
<br />
<ol>
<li><strong>You havent joined our Discord, and havent said anything, so we dont know anything about you.</strong></li>
<li><strong>You didnt fill out the verification form completely.</strong></li>
<li><strong>Youve just recently joined and dont have enough of a history with token.gallery for us to review and verify you with.</strong></li>
<li><strong>Youve minted copyrighted (without rights to resell) images in the past.</strong></li>
<li>
<strong
>Youve minted low effort spammy NFTs onto the gallery in a short amount of time. (This includes low effort meme images and low effort images with cheap
filters from image editing apps). The key phrase is low effort.</strong
>
</li>
<li>
<strong
>Youve intentionally tried to deceive, trick, or manipulate collectors by minting copycat NFTs from other users on token.gallery or other marketplaces.</strong
>
</li>
<li>
<strong
>We may not feel that your profile meets our standards of verification based on quality, history, and other factors at the moment. You can always apply again
later.</strong
>
</li>
<li>
<strong
>Youve messaged the mods directly about your verification. This actually slows us down. As more people ask, the more time we spend chatting instead of
reviewing and verifying.</strong
>
</li>
</ol>
</div>
</li>
<li id="faq-8">
<div class="li-header" @click="toggleItem">
<h3 class="hoverable">How do I prevent my NFT from being delisted from token.gallery?</h3>
<EyeClosed class="open-close-icon hoverable" alt="Hide answer" v-if="showFaq['faq-8']" />
<EyeOpen class="open-close-icon hoverable" alt="Show answer" v-else />
</div>
<div class="li-content" v-if="showFaq['faq-8']">
<p>This section is not legal advice. These are general tips to help you learn about posting on token.gallery successfully.</p>
<br />
<ol>
<li>
<strong
>Free images and assets from the web are not automatically okay to sell. You must read the associated Terms of Use for any asset you find online or in the
meat-world. Free assets are often attached to licensing that does not allow resale.</strong
>
</li>
<li><strong>If youre using purchased stock assets, always read the license related to resales.</strong></li>
<li>
<strong
>If youve commissioned art from an artist for resale as an NFT, get their permission upfront. We also encourage you to credit the artist in the description of
your NFT when minting.</strong
>
</li>
<li>
<strong
>Never download images from blogs, websites, or other marketplaces and repost them on token.gallery if you dont own the rights to resell them. You may face
legal repercussions from the owners for minting NFTs with copyrighted materials or IP.</strong
>
</li>
<li>
<strong
>As a collector, perform your due diligence and look into the artist, the description of the NFT, and do a quick reverse image search on your preferred search
engine.</strong
>
</li>
</ol>
<br />
<p>Any NFTs minted on token.gallery are subject to immediate removal from our website if legal claims are made against them by external parties.</p>
<br />
<p>Even if you follow these tips, you never know if someone will make a claim. Play it safe.</p>
<br />
<p>
<strong>Learn more about copyrights.</strong>
<a class="link-accent" href="https://edu.gcfglobal.org/en/useinformationcorrectly/copyright-and-fair-use/1/">
https://edu.gcfglobal.org/en/useinformationcorrectly/copyright-and-fair-use/1/
</a>
</p>
</div>
</li>
</ul>
</section>
<aside class="main-text">
<a href="https://discord.gg/2vq4WcD" class="button button-square-accent join-our-discord">
<DiscordIcon class="discord-icon" />
<span>Join our discord</span>
</a>
<h3 class="title">Quicklinks</h3>
<div class="quicklinks">
<p><a href="https://ubiqsmart.com/en/what-is-ubiq">What is the Ubiq Blockchain? </a></p>
<p><a href="https://10grans.cash">What is 10grans? </a></p>
<p><a href="https://metamask.io/">Install Metamask </a></p>
<p><a href="https://ubiqsmart.com/sparrow">Install Sparrow </a></p>
</div>
<h3 class="title">Who made this?</h3>
<div class="creators">
<p>
<a target="_blank" href="https://twitter.com/TheMoonOfficia2"><span class="blue">Moon / </span>backend, frontend, solidity dev </a>
</p>
<p>
<a target="_blank" href="https://🆒🆕🎨.y.at/"><span class="red">robek.world / </span>research, ideas, leader </a>
</p>
<p>
<a target="_blank" href="https://github.com/fafrd"><span class="green">Kian / </span>frontend, backend dev </a>
</p>
<p>
<a target="_blank" href="https://twitter.com/Wesker44803474"><span class="purple">Wǝsker / </span>design, product </a>
</p>
</div>
</aside>
</div>
</div>
</template>
<style scoped>
.about.container {
margin-top: 0;
}
header {
text-align: center;
background-image: url("/about/header.gif");
background-position: top;
background-repeat: no-repeat;
}
h1 {
padding-top: 640px;
margin-bottom: 64px;
font-size: 32px;
line-height: 38.4px;
font-weight: 700;
}
#about-main {
display: flex;
flex-wrap: wrap;
padding-bottom: 100px;
}
#about-main section {
width: calc(60% - 160px);
margin-right: 160px;
}
#about-main aside {
width: 30%;
}
.icons-container {
display: flex;
justify-content: center;
}
.icons-container .icon {
margin: 80px;
}
.icons-container p {
margin-top: 32px;
color: var(--offwhite);
font-size: 24px;
line-height: 28.8px;
}
h3.title {
margin-bottom: 32px;
font-size: 16px;
line-height: 16px;
font-weight: 400;
letter-spacing: 2.5px;
text-transform: uppercase;
color: var(--accent);
}
h3 {
font-size: 24px;
line-height: 28.8px;
font-weight: 700;
margin-top: 0;
margin-bottom: 32px;
}
h4 {
font-weight: 700;
font-size: 20px;
margin-top: 24px;
margin-bottom: 24px;
}
p.what {
font-size: 32px;
line-height: 48px;
font-weight: 700;
}
ul.faq > li {
padding-bottom: 32px;
border-bottom: var(--border-soft);
list-style-type: none;
font-size: revert;
line-height: revert;
font-weight: revert;
}
ul.faq > li:last-child {
padding-bottom: 0;
border-bottom: initial;
}
p,
ol > li,
ul > li {
font-size: 21px;
line-height: 31.5px;
font-weight: 400;
}
ol {
list-style-type: decimal;
margin-left: 32px;
}
ul {
list-style-type: disc;
margin-left: 32px;
}
.li-header {
display: flex;
justify-content: space-between;
cursor: pointer;
margin-top: 24px;
margin-bottom: 8px;
padding-top: 24px;
padding-bottom: 24px;
}
.li-header h3 {
margin: 0;
margin-right: 32px;
}
.page-break {
margin-top: 178px;
margin-bottom: 178px;
}
section.main-text h2 {
font-size: 56px;
line-height: 67px;
font-weight: 700;
color: var(--accent);
margin-top: 0;
margin-bottom: 0;
padding-bottom: 32px;
}
aside.main-text h3.title {
margin-top: 64px;
margin-bottom: 24px;
}
aside.main-text p {
line-height: 24px;
margin-top: 0;
margin-bottom: 27px;
}
.join-our-discord .discord-icon {
margin-right: 16px;
}
.blue {
color: #1cc8ee;
}
.red {
color: var(--red);
}
.green {
color: var(--green);
}
.purple {
color: var(--purple-text);
}
.join-our-discord {
display: flex;
align-items: center;
max-width: min-content;
font-size: 16px;
line-height: 16px;
font-weight: 600;
text-transform: uppercase;
margin-left: 0;
margin-top: 0;
margin-bottom: 64px;
}
.link-accent {
color: var(--accent);
}
.open-close-icon {
overflow: visible;
}
li::marker {
font-weight: 700;
}
ol > li {
margin-bottom: 16px;
}
</style>
<script>
import Vue from "vue";
import ArrowDown from "@/assets/images/arrow-down.svg";
import PageBreak from "@/assets/images/page-break.svg";
import DiscordIcon from "@/assets/images/discord.svg";
import EyeClosed from "@/assets/images/eye-closed.svg";
import EyeOpen from "@/assets/images/eye-open.svg";
export default {
components: {
ArrowDown,
PageBreak,
DiscordIcon,
EyeClosed,
EyeOpen,
},
data() {
return {
showFaq: {}, // faq-1: true, faq-2: false...
};
},
metaInfo() {
return {
meta: [{ description: "token.gallery is an NFT marketplace that supports and showcases creative work. create and collect artwork that lives on the blockchain." }],
};
},
methods: {
toggleItem(element) {
// find parent <li>
const parent = element.target.closest("ul.faq > li");
if (this.showFaq[parent.id]) {
Vue.set(this.showFaq, parent.id, false);
} else {
Vue.set(this.showFaq, parent.id, true);
}
},
},
};
</script>

628
src/views/Address.vue Normal file
View File

@ -0,0 +1,628 @@
<template>
<div class="container">
<div v-if="amAdmin" id="admin">
<h2>Administrator controls</h2>
<button class="danger" @click="disableAccount">Disable account</button>
<button v-if="verified" @click="unverifyAccount">Remove Verfication</button>
<button v-else @click="verifyAccount">Verify Account </button>
</div>
<aside class="user-controls" v-if="myUser !== null && userAuthenticated && myUser.id == address">
<h1>Welcome, {{ myUser.username }}!</h1>
<div class="user-controls-buttons">
<router-link to="/settings" class="button button-round-accent">Settings</router-link>
<button @click="logout()">Log out</button>
</div>
</aside>
<div class="cols-2">
<aside class="user-pane">
<div class="username">
<h1 class="username" v-if="finishedLoading.username">{{ username }}</h1>
<h1 class="username" v-else>Loading...</h1>
<VerifiedIcon :title="`${username} is a verified user!`" :alt="`${username} is a verified user!`" class="verified-user" v-if="finishedLoading.username && verified" />
</div>
<div class="wallet">
<div class="addr-container">
<p class="details-title">WALLET</p>
<p class="addr">{{ address }}</p>
</div>
<CopyIcon class="copy-icon hoverable" @click="copyAddress" />
</div>
<div v-if="amAdmin && walletBalance > 0">
<p class="details-title">Ubiq balance</p>
<p class="details-data">{{ walletBalance }} UBQ</p>
</div>
<div v-if="amAdmin && wallet10GransBalance > 0">
<p class="details-title">10grans balance</p>
<p class="details-data">{{ wallet10GransBalance }} GRANS</p>
</div>
</aside>
<section class="right-half">
<nav>
<button :class="{ 'active-tab': activeTab == 'owned' }" @click="activeTab = 'owned'">Owned</button>
<button :class="{ 'active-tab': activeTab == 'for-sale' }" @click="activeTab = 'for-sale'">For Sale</button>
<button :class="{ 'active-tab': activeTab == 'created' }" @click="activeTab = 'created'">Created</button>
<!--<button :class="{ 'active-tab': activeTab == 'favorites' }" @click="activeTab = 'favorites'">Favorites</button>-->
<button :class="{ 'active-tab': activeTab == 'collections' }" @click="activeTab = 'collections'">Collections</button>
</nav>
<article class="tab" :class="{ hidden: activeTab !== 'owned' }">
<div class="sad-message" v-if="!finishedLoading.ownedNfts">
<h2>Loading...</h2>
</div>
<div class="card-grid" v-else-if="this.ownedNfts && this.ownedNfts.length > 0">
<NftCard v-for="(nft, index) of ownedNfts" :key="index" :nft="nft" />
</div>
<div class="sad-message" v-else>
<h2>{{ username }} doesn't own any NFTs.</h2>
</div>
<div class="paging">
<button class="button-square-accent" @click="currentPage--" :class="{ disabled: currentPage <= 1 }"> Prev Page</button>
<span class="current-page">{{ currentPage }}</span>
<button class="button-square-accent" @click="currentPage++" :class="{ disabled: !nextPageAvailable['owned'] }">Next Page </button>
</div>
</article>
<article class="tab" :class="{ hidden: activeTab !== 'for-sale' }">
<div class="sad-message" v-if="!finishedLoading.ownedNfts">
<h2>Loading...</h2>
</div>
<div class="card-grid" v-else-if="this.forSaleNfts && this.forSaleNfts.length > 0">
<NftCard v-for="(nft, index) of forSaleNfts" :key="index" :nft="nft" />
</div>
<div class="sad-message" v-else>
<h2>{{ username }} has no NFTs for sale.</h2>
</div>
<div class="paging">
<button class="button-square-accent" @click="currentPage--" :class="{ disabled: currentPage <= 1 }"> Prev Page</button>
<span class="current-page">{{ currentPage }}</span>
<button class="button-square-accent" @click="currentPage++" :class="{ disabled: !nextPageAvailable['for-sale'] }">Next Page </button>
</div>
</article>
<article class="tab" :class="{ hidden: activeTab !== 'created' }">
<div class="sad-message" v-if="!finishedLoading.createdNfts || !finishedLoading.collections">
<h2>Loading...</h2>
</div>
<div class="card-grid" v-else-if="this.createdNfts && this.createdNfts.length > 0">
<NftCard v-for="(nft, index) of createdNfts" :key="index" :nft="nft" />
</div>
<div class="sad-message" v-else>
<h2>{{ username }} has not created any NFTs.</h2>
</div>
<div class="paging">
<button class="button-square-accent" @click="currentPage--" :class="{ disabled: currentPage <= 1 }"> Prev Page</button>
<span class="current-page">{{ currentPage }}</span>
<button class="button-square-accent" @click="currentPage++" :class="{ disabled: !nextPageAvailable['created'] }">Next Page </button>
</div>
</article>
<!--
<article class="tab" :class="{ hidden: activeTab !== 'favorites' }">
<div class="sad-message" v-if="!finishedLoading.favoriteNfts">
<h2>Loading...</h2>
</div>
<div class="card-grid" v-else-if="this.favoriteNfts && this.favoriteNfts.length > 0">
<NftCard v-for="(nft, index) of favoriteNfts" :key="index" :nft="nft" />
</div>
<div class="sad-message" v-else>
<h2>{{ username }} has not favorited any NFTs.</h2>
</div>
</article>
-->
<article class="tab" :class="{ hidden: activeTab !== 'collections' }">
<div class="sad-message" v-if="!finishedLoading.createdNfts || !finishedLoading.collections">
<h2>Loading...</h2>
</div>
<div class="card-grid" v-else-if="collections.length > 0">
<div v-for="c in collections" :key="c.address" class="collection">
<CollectionCard :symbol="c.symbol" :name="c.name" :address="c.address" clickAction="link" />
</div>
</div>
<div class="sad-message" v-else>
<h2>{{ username }} does not have any collections.</h2>
</div>
</article>
</section>
</div>
</div>
</template>
<style scoped>
aside.user-controls {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
box-sizing: border-box;
width: 600px;
max-width: 100%;
margin-left: auto;
margin-right: auto;
margin-bottom: var(--distance-from-nav);
padding: 32px;
border-radius: 16px;
box-shadow: 0 0 0 1pt var(--white);
background-color: var(--card-background);
}
aside.user-controls .user-controls-buttons {
display: flex;
justify-content: center;
align-items: center;
}
h1 {
font-size: 32px;
line-height: 32px;
font-weight: 700;
margin-bottom: 32px;
}
aside.user-pane .user-balance {
color: var(--offwhite);
margin-bottom: 8px;
}
aside.user-pane .wallet {
display: flex;
align-items: center;
background-color: var(--card-background);
color: var(--card-foreground);
margin: 24px 0;
padding: 16px 24px;
border-radius: 4px;
}
aside.user-pane .wallet .details-title {
color: var(--card-foreground);
margin: 0;
}
aside.user-pane .wallet .addr-container {
display: flex;
flex-direction: column;
max-width: 80%;
}
aside.user-pane .wallet .addr {
font-weight: 600;
margin-top: 8px;
overflow: hidden;
text-overflow: ellipsis;
}
aside.user-pane .wallet .addr {
margin-top: 8px;
overflow: hidden;
text-overflow: ellipsis;
}
aside.user-pane .wallet .copy-icon {
padding: 8px 20px;
min-width: 24px;
cursor: pointer;
}
aside.user-pane .username {
display: flex;
align-items: baseline;
}
aside.user-pane .username .verified-user {
max-height: 24px;
margin-left: 8px;
min-width: 24px;
}
#admin {
background-color: #500000;
border: 1px solid white;
padding: 1em;
margin-bottom: 2em;
}
.sad-message {
text-align: center;
}
.right-half nav {
display: flex;
flex-wrap: wrap;
justify-content: center;
border: none;
margin-bottom: 0;
}
.right-half nav button {
background-color: var(--gray);
color: var(--white);
font-weight: 400;
}
.right-half nav button.active-tab {
background-color: var(--white);
color: var(--black);
}
.right-half .tab {
padding-top: 28px;
padding-bottom: 40px;
}
.right-half .tab.hidden {
display: none !important;
}
.paging {
margin-top: 64px;
text-align: center;
}
.paging p {
display: inline;
font-weight: bold;
margin: 0 16px;
}
.danger {
background: black;
color: red;
border: 2px solid white;
}
.danger:hover {
background: red;
color: black;
}
.paging {
display: flex;
justify-content: center;
align-items: center;
margin-top: 64px;
text-align: center;
}
.current-page {
margin: 0 32px;
font-weight: 700;
font-size: 38px;
}
</style>
<script>
import shared from "@/shared";
import NftCard from "@/components/NftCard.vue";
import CollectionCard from "@/components/CollectionCard.vue";
import VerifiedIcon from "@/assets/images/verified.svg";
import CopyIcon from "@/assets/images/copy.svg";
export default {
components: {
NftCard,
CollectionCard,
VerifiedIcon,
CopyIcon,
},
data() {
return {
verified: false,
address: "",
ethPrice: 0,
tokenPrice: 0,
nextPageAvailable: {
owned: false,
"for-sale": false,
created: false,
},
finishedLoading: {
username: false,
ownedNfts: false,
createdNfts: true, // assume true... we first load collections, then IF there are collections, we load createdNfts
collections: false,
favoriteNfts: false,
},
};
},
metaInfo() {
if (this.username == "Unregistered user" && this.address) {
return {
meta: [{ description: `nft collector ${this.address}` }],
};
} else if (this.username) {
return {
meta: [{ description: `nft collector ${this.username}` }],
};
}
return {};
},
async created() {
shared.getPrice(this.$apiBase).then((price) => {
this.ethPrice = price.ubiqUsdRatio;
this.tokenPrice = price.ubiqUsdRatio / price.ubiqGransRatio;
});
},
computed: {
myUser() {
return this.$store.state.user;
},
userAuthenticated() {
return this.$store.getters.isUserAuthenticated;
},
amAdmin() {
return this.myUser !== null && this.$store.getters.isUserAuthenticated && this.myUser.admin;
},
activeTab: {
get() {
return this.$route.params.tab || "owned";
},
set(val) {
if (!this.address) return;
this.$router.push(`/address/${this.$route.params.address}/tab/${val}`);
},
},
currentPage: {
get() {
if (this.$route.params.page) {
return Number(this.$route.params.page);
} else {
return 1;
}
},
set(val) {
if (!this.address) return;
if (val > 1) {
this.$router.push(`/address/${this.$route.params.address}/tab/${this.activeTab}/page/${val}`);
} else {
this.$router.push(`/address/${this.$route.params.address}/tab/${this.activeTab}`);
}
},
},
page: {
get() {
return {
owned: this.activeTab == "owned" ? this.currentPage : 1,
"for-sale": this.activeTab == "for-sale" ? this.currentPage : 1,
created: this.activeTab == "created" ? this.currentPage : 1,
};
},
},
},
asyncComputed: {
ownedNfts: {
async get() {
if (this.address) {
const adultClause = this.$store.state.showAdult ? "adult=true" : "";
const ownerClause = `owner=${this.address}`;
const pageClause = `page=${this.page["owned"]}`;
let res = [];
try {
this.finishedLoading.ownedNfts = false;
res = await shared.fetchJson(`${this.$apiBase}/api/nfts/latest?${adultClause}&${ownerClause}&${pageClause}&showunverified=true`);
} finally {
this.finishedLoading.ownedNfts = true;
}
// asynchronously fire off a lookahead query to see if there is another page available
(async () => {
const nextPageClause = `page=${this.page["owned"] + 1}`;
const next = await shared.fetchJson(`${this.$apiBase}/api/nfts/latest?${adultClause}&${ownerClause}&${nextPageClause}&showunverified=true`);
this.nextPageAvailable["owned"] = next.length > 0;
})();
return res;
}
return [];
},
default: [],
},
forSaleNfts: {
async get() {
if (this.address) {
const adultClause = this.$store.state.showAdult ? "adult=true" : "";
const ownerClause = `owner=${this.address}`;
const latestCurrency = "any"; // TODO provide grans/ubq currency filtering similar to front page
const forSaleClause = `forsale=${latestCurrency}`;
const pageClause = `page=${this.page["for-sale"]}`;
let res = [];
try {
this.finishedLoading.ownedNfts = false;
res = await shared.fetchJson(`${this.$apiBase}/api/nfts/latest?${adultClause}&${ownerClause}&${forSaleClause}&${pageClause}&showunverified=true`);
} finally {
this.finishedLoading.ownedNfts = true;
}
// asynchronously fire off a lookahead query to see if there is another page available
(async () => {
const nextPageClause = `page=${this.page["for-sale"] + 1}`;
const next = await shared.fetchJson(`${this.$apiBase}/api/nfts/latest?${adultClause}&${ownerClause}&${forSaleClause}&${nextPageClause}&showunverified=true`);
this.nextPageAvailable["for-sale"] = next.length > 0;
})();
return res;
}
return [];
},
default: [],
},
/*
favoriteNfts: {
// TODO BROKEN NEEDS TESTING
async get() {
if (!this.address) return [];
const adultClause = this.$store.state.showAdult ? "adult=true" : "";
const res = await shared.fetchJson(`${this.$apiBase}/api/account/${this.address}/favorites?${adultClause}`);
return res.nfts.reverse(); // most recently favorited first
},
default: [],
},
*/
createdNfts: {
async get() {
if (this.collections.length > 0) {
const adultClause = this.$store.state.showAdult ? "adult=true" : "";
const storeAddresses = this.collections.map((e) => e.address);
const storeClause = "store=" + storeAddresses.reduce((acc, curr) => `${acc}&store=${curr}`);
const pageClause = `page=${this.page["created"]}`;
let res = [];
try {
this.finishedLoading.createdNfts = false;
res = await shared.fetchJson(`${this.$apiBase}/api/nfts/latest?${adultClause}&${storeClause}&${pageClause}&showunverified=true`);
} finally {
this.finishedLoading.createdNfts = true;
}
// asynchronously fire off a lookahead query to see if there is another page available
(async () => {
const nextPageClause = `page=${this.page["created"] + 1}`;
const next = await shared.fetchJson(`${this.$apiBase}/api/nfts/latest?${adultClause}&${storeClause}&${nextPageClause}&showunverified=true`);
this.nextPageAvailable["created"] = next.length > 0;
})();
return res;
}
return [];
},
default: [],
},
collections: {
async get() {
if (!this.address) return [];
try {
this.finishedLoading.collections = false;
return await shared.fetchJson(`${this.$apiBase}/api/stores/${this.address}`);
} finally {
this.finishedLoading.collections = true;
}
},
default: [],
},
username: {
// also sets this.address
async get() {
try {
const accountUser = await shared.fetchJson(`${this.$apiBase}/api/user/${this.$route.params.address}`);
if (accountUser) {
this.address = accountUser.id;
this.verified = accountUser.verified;
document.title = accountUser.username;
return accountUser.username;
} else {
return "Unregistered user";
}
} finally {
this.finishedLoading.username = true;
}
},
default: "User",
},
walletBalance: {
async get() {
if (!this.address) return 0;
if (this.$root.web3) {
const balance = await this.$root.web3.eth.getBalance(this.address);
return shared.formatPrice(balance);
}
return null;
},
default: 0,
},
wallet10GransBalance: {
async get() {
if (!this.address) return 0;
if (this.$root.web3) {
const tokenAbi = await this.$tokenAbiPromise;
this.tokenContract = new this.$root.web3.eth.Contract(tokenAbi, this.$tokenAddress);
const tenGransWei = await this.tokenContract.methods.balanceOf(this.address).call();
return shared.formatPrice(tenGransWei);
}
return null;
},
default: 0,
},
},
methods: {
copyAddress() {
navigator.clipboard.writeText(this.address);
},
logout() {
this.$parent.logout();
},
async disableAccount() {
let confirmation = confirm("This will disable the user's account, preventing them logging in and making API calls. Their NFTs will still exist and be visible.\n\nProceed?");
if (!confirmation) return;
confirmation = confirm("Are you really sure you want to DISABLE THIS USER?");
if (!confirmation) return;
try {
const url = `${this.$apiBase}/api/admin/account/${this.address}`;
const form = new FormData();
form.append("disabled", true);
await shared.uploadForm(url, form, "POST", "disabled=true", this.myUser.id, this.myUser.token, this.$root.web3.utils);
alert("Successfully disabled user");
} catch (error) {
alert(`disableAccount(); received error while disabling user: ${error.message}`);
throw Error(`disableAccount(); received error while disabling user: ${error.message}`);
}
},
async verifyAccount() {
let confirmation = confirm(`Are you sure you want to verify user '${this.username}' at address '${this.address}'?`);
if (!confirmation) return;
try {
const url = `${this.$apiBase}/api/admin/account/${this.address}/verified`;
const form = new FormData();
form.append("verified", true);
await shared.uploadForm(url, form, "POST", "", this.myUser.id, this.myUser.token, this.$root.web3.utils, false);
alert("Successfully verified user");
} catch (error) {
alert(`verifyAccount(); received error while verifying user: ${error.message}`);
throw Error(`verifyAccount(); received error while verifying user: ${error.message}`);
}
},
async unverifyAccount() {
let confirmation = confirm(`Are you sure you want to REMOVE verification from ${this.username}?`);
if (!confirmation) return;
try {
const url = `${this.$apiBase}/api/admin/account/${this.address}/verified`;
const form = new FormData();
form.append("verified", false);
await shared.uploadForm(url, form, "POST", "", this.myUser.id, this.myUser.token, this.$root.web3.utils, false);
alert("Successfully un-verified user");
} catch (error) {
alert(`verifyAccount(); received error while un-verifying user: ${error.message}`);
throw Error(`verifyAccount(); received error while un-verifying user: ${error.message}`);
}
},
},
};
</script>

269
src/views/Collection.vue Normal file
View File

@ -0,0 +1,269 @@
<template>
<div class="container">
<h1>{{ name ? name : "Loading..." }}</h1>
<div class="cols-2">
<aside>
<div class="creator-block">
<div class="details-title">Creator</div>
<div class="details-data">
<router-link :to="{ name: 'Address', params: { address: storeCreatorName } }">{{ storeCreatorName }}</router-link>
</div>
</div>
<div>
<div class="desc-symbol details-title">Symbol</div>
<div class="details-data">{{ symbol }}</div>
</div>
<div class="contract-balance">
<div class="desc-balance details-title">Contract Balance</div>
<div class="details-data">{{ contractBalance }} UBQ</div>
</div>
<div v-if="myBalance != '0'">
<div class="desc-stored details-title">Stored Balance</div>
<div class="details-data">{{ myBalance }} UBQ</div>
</div>
<div v-if="my10GransBalance != '0'">
<div class="desc-10grans details-title">Stored 10grans Balance</div>
<div class="details-data">{{ my10GransBalance }} GRANS</div>
</div>
<button v-if="hasBalance" @click="withdraw()">Withdraw</button>
<div v-if="isStoreCreator" class="create-new">
<router-link :to="{ name: 'Create NFT', params: { store: $route.params.collection } }" class="button button-round-accent">Create New NFTs</router-link>
</div>
<div id="withdraw-notification" class="notification is-success is-hidden">
<button class="delete" @click="deleteNotif()"></button>
You will have your funds when the transaction clears.
</div>
</aside>
<section>
<div v-if="nfts.length > 0" class="card-grid">
<NftCard v-for="(nft, index) of nfts" :key="index" :nft="nft" />
</div>
<div v-else class="nfts">
<h2 class="no-nfts-yet">{{ finishedLoading ? "No NFTs in this store yet." : "Loading..." }}</h2>
</div>
<div class="paging">
<button class="button-square-accent" @click="currentPage--" :class="{ disabled: currentPage <= 1 }"> Prev Page</button>
<span class="current-page">{{ currentPage }}</span>
<button class="button-square-accent" @click="currentPage++" :class="{ disabled: !nextPageAvailable }">Next Page </button>
</div>
</section>
</div>
</div>
</template>
<style scoped>
.create-new,
.no-nfts-yet {
margin-top: 3em;
text-align: center;
}
.notification {
width: 30%;
}
.contract-balance {
display: none;
}
.is-hidden {
display: none;
}
.paging {
display: flex;
justify-content: center;
align-items: center;
margin-top: 64px;
text-align: center;
}
.current-page {
margin: 0 32px;
font-weight: 700;
font-size: 38px;
}
</style>
<script>
const BigNumber = require("bignumber.js");
import NftCard from "../components/NftCard.vue";
import shared from "../shared";
export default {
data: function() {
return {
name: "",
symbol: "",
nfts: [],
contractBalance: "0",
my10GransBalance: "0",
myBalance: "0",
storeContract: null,
isStoreCreator: false,
storeCreator: null,
storeCreatorName: "",
finishedLoading: false,
ethPrice: 0,
tokenPrice: 0,
nextPageAvailable: false,
};
},
components: {
NftCard,
},
metaInfo() {
if (this.name && this.storeCreatorName) {
return {
meta: [{ description: `nft collection - ${this.name}` }, { author: this.storeCreatorName }],
};
}
return {};
},
async created() {
try {
shared.getPrice(this.$apiBase).then((price) => {
this.ethPrice = price.ubiqUsdRatio;
this.tokenPrice = price.ubiqUsdRatio / price.ubiqGransRatio;
});
const dbStore = await shared.fetchJson(`${this.$apiBase}/api/store/${this.$route.params.collection}`);
if (dbStore == null) {
console.error("Store not found.");
return;
}
this.name = dbStore.name;
document.title = this.name;
this.symbol = dbStore.symbol;
this.storeCreator = dbStore.creator;
const storeCreatorUser = await shared.fetchJson(`${this.$apiBase}/api/user/${this.storeCreator}`);
if (this.isStoreCreator) {
this.storeCreatorName = "You";
} else if (storeCreatorUser) {
this.storeCreatorName = storeCreatorUser.username;
} else {
this.storeCreatorName = this.storeCreator;
}
if (this.$store.getters.isUserAuthenticated) {
const storeAbi = await this.$storeAbiPromise;
this.storeContract = new this.$root.web3.eth.Contract(storeAbi, this.$route.params.collection);
this.contractBalance = this.$root.web3.utils.fromWei(await this.$root.web3.eth.getBalance(this.$route.params.collection), "ether");
this.isStoreCreator = this.user.id == this.storeCreator;
await this.getBalances();
}
} finally {
this.finishedLoading = true;
}
},
computed: {
hasBalance() {
return this.myBalance != "0" || this.my10GransBalance != "0";
},
user() {
return this.$store.state.user;
},
currentPage: {
get() {
if (this.$route.params.page) {
return Number(this.$route.params.page);
} else {
return 1;
}
},
set(val) {
if (!this.$route.params.collection) return;
if (val > 1) {
this.$router.push(`/collection/${this.$route.params.collection}/page/${val}`);
} else {
this.$router.push(`/collection/${this.$route.params.collection}`);
}
},
},
},
asyncComputed: {
nfts: {
async get() {
if (this.$route.params.collection) {
const adultClause = this.$store.state.showAdult ? "adult=true" : "";
const pageClause = `page=${this.currentPage}`;
const storeClause = `store=${this.$route.params.collection}`;
let res = [];
try {
this.finishedLoading = false;
res = await shared.fetchJson(`${this.$apiBase}/api/nfts/latest?${adultClause}&${pageClause}&${storeClause}&showunverified=true`);
} finally {
this.finishedLoading = true;
}
// asynchronously fire off a lookahead query to see if there is another page available
(async () => {
const nextPageClause = `page=${this.currentPage + 1}`;
const next = await shared.fetchJson(`${this.$apiBase}/api/nfts/latest?${adultClause}&${nextPageClause}&${storeClause}&showunverified=true`);
this.nextPageAvailable = next.length > 0;
})();
return res;
}
return [];
},
default: [],
},
},
methods: {
async asyncFilter(arr, predicate) {
const results = await Promise.all(arr.map(predicate));
return arr.filter((_v, index) => results[index]);
},
async getBalances() {
try {
var wei = await this.storeContract.methods.ownerEthBalances(this.user.id).call();
} catch (e1) {
console.error("failed to get eth balance", e1);
return;
}
this.myBalance = this.$root.web3.utils.fromWei(wei, "ether");
try {
var tenGransWei = await this.storeContract.methods.ownerTokenBalances(this.user.id).call();
} catch (e2) {
console.error("failed to get token balance");
return;
}
this.my10GransBalance = new BigNumber(tenGransWei).div(new BigNumber(10).pow(this.$tokenDecimals));
},
async withdraw() {
await this.getBalances();
if (this.hasBalance()) {
//don't wait.
try {
this.storeContract.methods.withdraw().send({ from: this.user.id });
} catch (e) {
console.error("Failed to withdraw", e);
return;
}
this.myBalance = "0";
this.my10GransBalance = "0";
document.getElementById("withdraw-notification").classList.remove("is-hidden");
}
},
deleteNotif: function() {
const notification = document.getElementById("withdraw-notification");
notification.parentNode.removeChild(notification);
},
},
};
</script>

View File

@ -0,0 +1,272 @@
<template>
<div class="container">
<CreateNftHeader h1="Nice, lets mint this thing." h2="Double check that all of the information is correct — then click the mint button." done="true" />
<div class="confirm-container">
<aside class="preview">
<h2 class="subheading">Preview</h2>
<div class="nft-card-wrapper" @click="showImage">
<NftCard :nft="nft" :creator-name-override="username" :favorites-override="'1'" :quantity-override="count" />
</div>
</aside>
<section class="details">
<h2 class="subheading">Your NFT's info</h2>
<h3>Title</h3>
<p class="title" v-if="nft.metadata.name">{{ nft.metadata.name }}</p>
<p class="title none" v-else>no title</p>
<h3>Description</h3>
<p class="description" v-if="nft.metadata.description">{{ nft.metadata.description }}</p>
<p class="description none" v-else>no description</p>
<h3>Tags</h3>
<p v-if="nft.metadata.tags">{{ tags }}</p>
<p v-else class="none">no tags</p>
<h3>Optional data</h3>
<div v-if="Object.keys(properties).length >= 1">
<p v-for="prop in properties" :key="prop.name">{{ prop.name }}: {{ prop.value }}</p>
</div>
<p v-else class="none">no optional properties set</p>
<h3>Currency</h3>
<p v-if="currency">{{ currency }}</p>
<p v-else class="none">n/a</p>
<h3>Price</h3>
<p v-if="priceFormatted">{{ priceFormatted }}</p>
<p v-else class="none">not for sale</p>
<h3>Number of Copies</h3>
<p>{{ count || 1 }}</p>
<h3>Collection</h3>
<p>{{ collection.name }} ({{ collection.symbol }})</p>
</section>
<section class="confirm">
<h2 class="subheading">Ready to mint?</h2>
<div class="confirm-box">
<h3>Everything looks good? Lets go.</h3>
<router-link to="/createnft/mint" class="button button-round-accent" id="mint" @click="mint">Mint NFT</router-link>
</div>
</section>
</div>
</div>
</template>
<style scoped>
.confirm-container {
display: flex;
column-gap: 24px;
}
.confirm-container > * {
flex-basis: 0;
}
.preview {
flex-grow: 4;
}
.details {
flex-grow: 5;
}
.confirm {
flex-grow: 3;
}
.nft-card-wrapper {
cursor: pointer;
}
.nft-card {
pointer-events: none;
}
.details h3 {
font-size: 16px;
line-height: 16px;
font-weight: normal;
text-transform: uppercase;
margin-top: 32px;
margin-bottom: 8px;
color: var(--gray-label);
}
.details p {
font-size: 16px;
line-height: 24px;
font-weight: 600;
margin-bottom: 8px;
color: var(--card-foreground);
}
.details p.none {
font-style: italic;
color: var(--offwhite);
}
.details p.description {
white-space: pre-wrap;
}
.confirm-box {
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
min-height: 238px;
background-color: var(--card-background);
padding: 32px;
}
.confirm-box h3 {
font-size: 32px;
line-height: 38px;
font-weight: bold;
}
.confirm-box button {
width: max-content;
margin-top: 32px;
margin-left: auto;
margin-right: auto;
font-size: 24px;
line-height: 34px;
font-weight: 600;
padding: 16px 40px;
}
</style>
<script>
import shared from "@/shared";
import * as basicLightbox from "basiclightbox";
import CreateNftHeader from "@/components/CreateNftHeader.vue";
import NftCard from "@/components/NftCard.vue";
export default {
components: {
CreateNftHeader,
NftCard,
},
created() {
// Perform some cleanup.
// remove empty properties
let invalidProps = [];
for (const idx in this.properties) {
if (this.properties[idx].name == "" || this.properties[idx].value == "") {
invalidProps.push(idx);
}
}
for (const i in invalidProps) {
this.properties.splice(invalidProps[i]);
}
// trim strings
this.name = this.name.trim();
this.description = this.description.trim();
for (const idx in this.properties) {
this.properties[idx].name = this.properties[idx].name.trim();
this.properties[idx].value = this.properties[idx].value.trim();
}
},
computed: {
form() {
return this.$store.state.simpleSetForms[0];
},
nft() {
return {
ethForSale: this.form.prices.ethForSale,
ethPrice: this.form.prices.ethPrice * 1e18,
tokenForSale: this.form.prices.tokenForSale,
tokenPrice: this.form.prices.tokenPrice * 1e18,
metadata: {
name: this.form.name,
description: this.form.description,
image: this.form.blobUrl,
tags: this.form.tags,
},
};
},
username() {
if (this.$store.state.user) return this.$store.state.user.username;
return "";
},
name: {
get() {
return this.form.name;
},
set(value) {
this.$store.commit("updateSimpleSetFormField", { index: 0, field: "name", value });
},
},
description: {
get() {
return this.form.description;
},
set(value) {
this.$store.commit("updateSimpleSetFormField", { index: 0, field: "description", value });
},
},
tags() {
let tags = "";
for (let i = 0; i < this.form.tags.length; i++) {
tags += this.form.tags[i];
if (i != this.form.tags.length - 1) tags += ", ";
}
return tags;
},
properties: {
get() {
return this.$store.state.simpleSetForms[0].extra;
},
set(value) {
this.$store.commit("updateSimpleSetFormField", { index: 0, field: "extra", value });
},
},
currency() {
if (this.nft.ethForSale) {
return "Ubiq";
} else if (this.nft.tokenForSale) {
return "10grans";
} else {
return null;
}
},
priceFormatted() {
if (this.nft.ethForSale) {
const formattedPrice = shared.formatPrice(this.nft.ethPrice);
return formattedPrice + " UBQ";
} else if (this.nft.tokenForSale) {
const formattedPrice = shared.formatPrice(this.nft.tokenPrice);
return formattedPrice + " GRANS";
} else {
return null;
}
},
count() {
return this.form.count;
},
collection() {
return this.form.collection;
},
},
methods: {
mint() {
console.log("Performing NFT mint");
},
showImage() {
this.lightbox = basicLightbox.create(`<img id="full-image" src="${this.form.blobUrl}"/>`);
this.lightbox.show();
},
},
};
</script>

View File

@ -0,0 +1,381 @@
<template>
<div class="container">
<CreateNftHeader h1="Write the lore for your NFT." h2="After minting you wont be able to edit this information. Make sure its right." :step-current="3" :step-max="5" />
<div class="details-container">
<form class="details-form">
<h2 class="subheading">Title, description, tags, and extras</h2>
<input type="text" id="input-title" name="title" v-model="name" placeholder="Title" maxlength="40" @focus="displayNameError = true" />
<div class="field-info">
<span v-if="displayNameError" class="field-error">{{ nameError }}</span>
<span class="field-maxlen">{{ this.name.length }}/40</span>
</div>
<textarea
id="input-desc"
name="description"
v-model="description"
placeholder="Description - Ex. “One time I knew this duck named gary and let me tell you something. Gary knew a thing or two about working a beak...”"
maxlength="1000"
@focus="displayDescError = true"
/>
<div class="field-info">
<span v-if="displayDescError" class="field-error">{{ descError }}</span>
<span class="field-maxlen">{{ this.description.length }}/1000</span>
</div>
<input type="text" id="input-tags" name="tags" v-model="tags" placeholder="Tags - Ex. “anime, sketch, 3d, yellow”" maxlength="1000" @focus="displayTagsError = true" />
<div class="field-info">
<span v-if="displayTagsError" class="field-error">{{ tagsError }}</span>
<span class="field-maxlen">limit of 15</span>
</div>
<section class="extra-properties">
<h3><strong>Optional</strong> stats, properties, or data</h3>
<div v-for="(prop, idx) in properties" :key="idx" class="key-value-pair">
<div class="key">
<input
type="text"
:id="'extra-prop-' + idx + '-key'"
:name="'extra-prop-' + idx + '-key'"
:placeholder="'Stat ' + (idx + 1) + ' name'"
v-model="prop.name"
maxlength="40"
/>
<div class="field-info">
<span class="field-error">{{ propNameError[idx] }}</span>
<span class="field-maxlen">{{ prop.name.length }}/40</span>
</div>
</div>
<div class="value">
<input
type="text"
:id="'extra-prop-' + idx + '-value'"
:name="'extra-prop-' + idx + '-value'"
:placeholder="'Stat ' + (idx + 1) + ' value'"
v-model="prop.value"
maxlength="80"
/>
<div class="field-info">
<span class="field-maxlen">{{ prop.value.length }}/80</span>
</div>
</div>
<div class="x-icon hoverable" @click="removeProperty(idx)">
<WhiteXIcon :alt="'Remove property ' + (idx + 1)" />
</div>
</div>
<div @click="appendProperty" class="add-another-stat hoverable">
<PlusIcon class="plus-icon" alt="+" />
<span class="add-another-stat-text">Add another stat</span>
</div>
</section>
<div class="checkbox-container">
<input type="checkbox" id="adult" name="adult" v-model="adult" />
<label for="adult">Does this NFT contain adult content?</label>
</div>
<button type="button" class="button button-round-accent" :class="{ disabled: formError }" id="continue-button" @click="continueButton">Continue</button>
<span v-if="displayFormError" class="field-error center">{{ formError }}</span>
</form>
<aside class="details-preview">
<h2 class="subheading">Preview</h2>
<div class="nft-card-wrapper" @click="showImage">
<NftCard :nft="nft" :creator-name-override="username" :favorites-override="'1'" />
</div>
</aside>
</div>
</div>
</template>
<style scoped>
.details-container {
display: flex;
}
.details-form {
width: 42%;
display: flex;
flex-direction: column;
}
.details-preview {
width: 25%;
margin: 0 auto;
}
.field-info {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
margin-top: 8px;
margin-bottom: 16px;
font-size: 16px;
line-height: 24px;
}
.field-error {
color: var(--red);
font-size: 14px;
}
.field-maxlen {
flex-grow: 1;
margin-left: 16px;
text-align: right;
color: var(--card-foreground);
white-space: nowrap;
}
.nft-card-wrapper {
cursor: pointer;
}
.nft-card {
pointer-events: none;
}
.extra-properties h3 {
margin-bottom: 16px;
font-size: 16px;
line-height: 24px;
}
.key-value-pair {
display: flex;
}
.key-value-pair .key {
display: flex;
flex-direction: column;
width: 48%;
margin-right: 2%;
}
.key-value-pair .value {
display: flex;
flex-direction: column;
width: 50%;
}
.key-value-pair .x-icon {
cursor: pointer;
margin-left: 8px;
/*margin-top: 21.5px;*/
}
.add-another-stat {
display: flex;
cursor: pointer;
padding-bottom: 16px;
}
.plus-icon {
margin-right: 8px;
}
.add-another-stat-text {
color: var(--purple-text);
font-size: 16px;
line-height: 24px;
}
#continue-button {
margin-top: 56px;
text-align: center;
font-size: 24px;
line-height: 34px;
}
.checkbox-container {
margin-top: 42px;
}
</style>
<script>
import shared from "@/shared";
import * as basicLightbox from "basiclightbox";
import CreateNftHeader from "@/components/CreateNftHeader.vue";
import NftCard from "@/components/NftCard.vue";
import PlusIcon from "@/assets/images/plus.svg";
import WhiteXIcon from "@/assets/images/white-x.svg";
export default {
components: {
CreateNftHeader,
NftCard,
PlusIcon,
WhiteXIcon,
},
data() {
return {
tagsRawString: "",
displayNameError: false,
displayDescError: false,
displayTagsError: false,
};
},
computed: {
form() {
return this.$store.state.simpleSetForms[0];
},
nft() {
let name = "Title";
if (this.form.name) {
name = this.form.name;
}
return {
metadata: {
name: name,
image: this.form.blobUrl,
},
ethForSale: true,
ethPrice: 1e18,
};
},
username() {
if (this.$store.state.user) return this.$store.state.user.username;
return "";
},
displayFormError() {
const extraPropertiesError = this.propNameError.filter((x) => x).length > 0;
return extraPropertiesError || this.displayNameError || this.displayDescError || this.displayTagsError;
},
formError() {
const extraPropertiesError = this.propNameError.filter((x) => x).length > 0;
if ((this.displayNameError && this.nameError) || (this.displayDescError && this.descError) || (this.displayTagsError && this.tagsError) || extraPropertiesError) {
return "Fix errors in the form before continuing.";
}
return null;
},
nameError() {
if (this.name == "") return "Name cannot be empty";
return null;
},
descError() {
if (this.description == "") return "Description cannot be empty";
return null;
},
tagsError() {
if (this.tagsArray) {
const tagsWithSpaces = this.tagsArray.filter((t) => t.includes(" "));
if (tagsWithSpaces.length > 0) return "Tags cannot have spaces";
if (this.tagsArray.length > 15) return "Too many tags";
}
return null;
},
propNameError() {
let propNameError = new Array(this.properties.length);
const names = this.properties.map((x) => x.name);
for (let i = 0; i < this.properties.length; i++) {
if (shared.propertyNameBlacklist.includes(this.properties[i].name)) {
propNameError[i] = `Name '${this.properties[i].name}' not permitted`;
}
if (names.filter((x) => x == this.properties[i].name).length > 1) {
if (this.properties[i].name) propNameError[i] = `Name already exists`;
}
if (this.properties.length > 100) {
propNameError[i] = `Too many properties! (max 100)`;
}
}
return propNameError;
},
name: {
get() {
return this.form.name;
},
set(value) {
this.$store.commit("updateSimpleSetFormField", { index: 0, field: "name", value });
},
},
description: {
get() {
return this.form.description;
},
set(value) {
this.$store.commit("updateSimpleSetFormField", { index: 0, field: "description", value });
},
},
tags: {
get() {
return this.tagsRawString;
},
set(value) {
// first set tagsRawString
this.tagsRawString = value;
// then clean and set tagsArray
let _tagsArray = this.tags.split(",");
_tagsArray = _tagsArray.map((t) => t.trim());
_tagsArray = _tagsArray.filter((t) => t); // remove empty
this.tagsArray = _tagsArray;
},
},
tagsArray: {
get() {
return this.form.tags;
},
set(value) {
this.$store.commit("updateSimpleSetFormField", { index: 0, field: "tags", value });
},
},
properties: {
get() {
return this.$store.state.simpleSetForms[0].extra;
},
set(value) {
//console.log("comitting updated value: " + JSON.stringify(value));
this.$store.commit("updateSimpleSetFormField", { index: 0, field: "extra", value });
},
},
adult: {
get() {
return this.form.adult;
},
set(value) {
this.$store.commit("updateSimpleSetFormField", { index: 0, field: "adult", value });
},
},
},
methods: {
appendProperty() {
this.$store.commit("addSimpleSetFormProperty", { index: 0, name: "", value: "" });
},
removeProperty(idx) {
this.properties.splice(idx, 1);
},
continueButton() {
const notValid = this.nameError || this.descError || this.tagsError;
if (notValid) {
// they tried to click continue but the form isn't ready- show all the errors
this.displayNameError = true;
this.displayDescError = true;
this.displayTagsError = true;
} else {
this.$router.push("/createnft/setprice");
}
},
showImage() {
this.lightbox = basicLightbox.create(`<img id="full-image" src="${this.form.blobUrl}"/>`);
this.lightbox.show();
},
},
};
</script>

View File

@ -0,0 +1,82 @@
<template>
<div class="container">
<CreateNftHeader h1="Let's get started." h2="Do you want to sell this NFT one time or multiple times?" :step-current="1" :step-max="5" />
<h2 class="subheading">Choose your creation method.</h2>
<section class="creation-method">
<div class="methods">
<router-link to="/createnft/upload" @click.native="setShowCountFalse" class="single">
<!-- <router-link :to="{ name: '/createnft/upload', params: {creationType: 'single' }}" class="single"> -->
<Single />
<div class="card-text">
<h3>Create a single</h3>
<p>Sell a "one of a kind" NFT</p>
</div>
</router-link>
<router-link to="/createnft/upload" @click.native="setShowCountTrue" class="multiple">
<Multiple />
<div class="card-text">
<h3>Create multiples</h3>
<p>Sell multiple copies of one NFT</p>
</div>
</router-link>
</div>
</section>
</div>
</template>
<style scoped>
.methods {
display: flex;
}
.methods > * {
margin-right: 32px;
background-color: var(--card-background);
border-radius: 0px 0px 8px 8px;
text-decoration: none;
}
.methods .card-text {
display: flex;
flex-direction: column;
}
.methods h3 {
font-size: 20px;
line-height: 24px;
font-weight: normal;
text-align: center;
margin-top: 32px;
margin-bottom: 16px;
}
.methods p {
text-align: center;
margin-bottom: 32px;
}
</style>
<script>
import CreateNftHeader from "@/components/CreateNftHeader.vue";
import Single from "@/assets/images/single.svg";
import Multiple from "@/assets/images/multiple.svg";
export default {
components: {
CreateNftHeader,
Single,
Multiple,
},
created() {},
methods: {
setShowCountTrue() {
this.$store.commit("setShowCount", true);
},
setShowCountFalse() {
this.$store.commit("setShowCount", false);
},
},
};
</script>

View File

@ -0,0 +1,336 @@
<template>
<div class="container">
<div v-if="mintingState == 'waiting-for-upload'">
<h1>Waiting for NFTs to upload... {{ uploadProgress }}</h1>
<h2>Your NFT metadata is being uploaded to IPFS.</h2>
</div>
<div v-if="mintingState == 'waiting-for-signature'">
<h1>Waiting for your signature...</h1>
<h2>You should see a prompt from your app / extension to sign for this transaction.</h2>
</div>
<div v-if="mintingState == 'waiting-for-confirmation'">
<h1>Your NFT is being minted.</h1>
<h2 v-if="confirmedOnce"><span class="success">Received one blockchain confirmation.</span> Waiting for one more...</h2>
<h2 v-else><span class="error">Do not close this window.</span> This should take between 10-120 seconds.</h2>
<img class="loading" src="/hourglass.gif" />
</div>
<div v-if="mintingState == 'error'">
<h1 class="error">{{ errorMinting }}</h1>
<h1 class="sub-error">{{ errorMinting_secondary }}</h1>
<h2>Please contact support, or retry your transaction</h2>
<button class="button button-round-accent" id="retry-button" @click="retryMint">Retry</button>
</div>
<div v-if="mintingState == 'complete'">
<h1>Mint complete!</h1>
<h2>Taking you to your collection page in 2 seconds.</h2>
</div>
</div>
</template>
<style scoped>
.container {
text-align: center;
}
h1 {
color: var(--green-success);
font-size: 48px;
line-height: 58px;
font-weight: bold;
margin-bottom: 24px;
}
h1.sub-error {
color: var(--red);
font-size: 24px;
line-height: 34px;
font-weight: bold;
margin-bottom: 24px;
}
h2 {
color: var(--offwhite);
font-size: 24px;
line-height: 34px;
font-weight: normal;
margin-bottom: 24px;
}
button {
font-size: 16px;
line-height: 16px;
font-weight: 600;
padding: 16px 24px;
}
.success {
color: var(--green-success);
}
</style>
<script>
import shared from "@/shared";
const BigNumber = require("bignumber.js");
BigNumber.set({ DECIMAL_PLACES: 18, ROUNDING_MODE: BigNumber.ROUND_FLOOR });
export default {
data() {
return {
state: "waiting-for-upload",
confirmedOnce: false,
uploadProgress: "",
errorMinting: "",
errorMinting_secondary: "",
};
},
async created() {
const storeAbi = await this.$storeAbiPromise;
this.storeContract = new this.$root.web3.eth.Contract(storeAbi, this.form.collection.address);
this.mint();
},
computed: {
mintingState: {
get() {
return this.state;
},
set(value) {
this.state = value;
},
},
form() {
return this.$store.state.simpleSetForms[0];
},
apiToken() {
return this.$store.state.user.token;
},
user() {
return this.$store.state.user;
},
isForSale() {
if (this.form.prices.ethForSale) {
return true;
} else if (this.form.prices.tokenForSale) {
return true;
} else {
return false;
}
},
count() {
if (this.form.count == 0) return 1;
return this.form.count;
},
},
methods: {
async retryMint() {
this.mint();
},
async mint() {
// create metadata
console.log("form: " + JSON.stringify(this.form));
console.debug("Uploading metadata...");
this.mintingState = "waiting-for-upload";
const metadatas = this.makeMetadatas();
const imageBlob = await this.getImageBlob();
// upload metadata
const ipfsHashes = await this.putMetadataIpfs_sameImage(
this.storeContract._address,
metadatas,
imageBlob,
this.user.id,
this.apiToken,
`${this.$apiBase}/api/nft/upload`,
this.$root.web3.utils
).catch((error) => {
this.mintingState = "error";
this.errorMinting = "Error uploading NFT metadata";
this.errorMinting_secondary = error.message;
throw Error(`mint(); received error: ${error.message}`);
});
console.debug("Upload complete. received IPFS hashes: ", ipfsHashes);
if (ipfsHashes.length !== metadatas.length) {
// something went terribly wrong
this.mintingState = "error";
this.errorMinting = "Metadata count mismatch";
}
// Mint NFT
this.mintingState = "waiting-for-signature";
// Calculate prices
let ethForSale = 1; // 1 means not for sale, 0 means for sale
let tokenForSale = 1;
let ethPrice = 0;
let tokenPrice = 0;
try {
if (this.form.prices.ethForSale) {
ethForSale = 0;
ethPrice = new BigNumber(this.form.prices.ethPrice).times("1e18").toString(10);
} else if (this.form.prices.tokenForSale) {
tokenForSale = 0;
tokenPrice = new BigNumber(this.form.prices.tokenPrice).times("1e18").toString(10);
}
} catch (error) {
console.error("error calculating prices; ", error);
this.mintingState = "error";
this.errorSettingPrice = "Error calculating prices";
this.errorSettingPrice_secondary = error.message;
return;
}
// Construct NFT object
let nfts = [];
for (let i = 0; i < ipfsHashes.length; i++) {
nfts.push({
owner: this.user.id,
metadataIpfsHash: ipfsHashes[i].substring(7),
price: ethPrice,
forSale: ethForSale,
tokenPrice: tokenPrice,
tokenForSale: tokenForSale,
dataLocked: true,
});
}
console.log("nfts: ");
console.log(JSON.stringify(nfts));
this.$gtag.event("mint", { count: this.count });
await this.storeContract.methods
// 'true' here determines whether they all get the same meta id, and therefore are part of a set
.mintNonFungible(true, nfts)
.send({ from: this.user.id })
.on("transactionHash", () => {
this.mintingState = "waiting-for-confirmation";
this.confirmedOnce = false;
})
.on("confirmation", (confirmationNumber) => {
if (confirmationNumber == 0) {
console.debug(`Received ${confirmationNumber} blockchain confirmations`);
this.mintingState = "waiting-for-confirmation";
this.confirmedOnce = true;
} else if (confirmationNumber == 1) {
console.debug(`Received ${confirmationNumber} blockchain confirmations`);
// Done! now redirect to collection page.
// (wait to prevent this.mintingState race condition)
setTimeout(() => this.handleComplete(), 250);
}
})
.catch((error) => {
this.mintingState = "error";
const msg = shared.handleTransactionError(error);
this.errorMinting = msg.errorMessage;
this.errorMinting_secondary = msg.errorMessageSecondary;
});
},
async handleComplete() {
this.mintingState = "complete";
setTimeout(() => {
this.$router.push("/collection/" + this.form.collection.address);
}, 2000);
},
async putMetadataIpfs_sameImage(store, metadatas, imageFile) {
// This function handles the upload of the same image one or more times in a set.
// When we implement batch upload of different images, make a new function like 'putMetadataIpfs_diffImages'
let ret = []; // return array of IPFS hashes representing metadatas
if (metadatas.length > 1) {
this.uploadProgress = `(uploading image)`;
}
// Upload first metadata differently from the rest
const initialFormData = new FormData();
initialFormData.append("storeAddress", store);
initialFormData.append("file", imageFile);
const initialMetadata = JSON.stringify(metadatas[0]); // Stringified because order in json can't be guaranteed and we need to hash it.
initialFormData.append("metadata", initialMetadata);
const initialHashString = `${store} ${initialMetadata}`;
const initialResponse = await shared.uploadForm(
`${this.$apiBase}/api/nft/upload`,
initialFormData,
"POST",
initialHashString,
this.user.id,
this.apiToken,
this.$root.web3.utils
);
ret.push(initialResponse.metadata);
for (let i = 1; i < metadatas.length; i++) {
this.uploadProgress = `(uploading metadata ${i + 1}/${metadatas.length})`;
// pull image/preview hashes from response and use in subsequent metadatas
metadatas[i].image = initialResponse.image;
if (initialResponse.preview) metadatas[i].preview = initialResponse.preview;
// upload metadata
const formData = new FormData();
formData.append("storeAddress", store);
const metadata = JSON.stringify(metadatas[i]); // Stringified because order in json can't be guaranteed and we need to hash it.
formData.append("metadata", metadata);
const hashString = `${store} ${metadata}`;
const response = await shared.uploadForm(`${this.$apiBase}/api/nft/upload`, formData, "POST", hashString, this.user.id, this.apiToken, this.$root.web3.utils);
ret.push(response.metadata);
}
return ret;
},
makeMetadatas() {
const metadatas = [];
for (let i = 0; i < this.count; ++i) {
let metadata = {
name: this.form.name,
description: this.form.description,
properties: {
adult: this.form.adult,
},
};
if (this.form.tags && this.form.tags.length > 0) {
metadata.properties.tags = this.form.tags;
}
if (this.count > 1) {
metadata.properties.set = {
name: this.form.name,
number: i + 1,
of: this.count,
};
}
for (const r of this.form.extra) {
metadata.properties[r.name] = r.value;
}
metadatas.push(metadata);
}
return metadatas;
},
async getImageBlob() {
const image = await this.$getItem(`image/${this.form.id}`);
if (image) {
return image;
} else {
console.error("Failed to get image!");
throw new Error("Unable to get image");
}
},
},
};
</script>

View File

@ -0,0 +1,360 @@
<template>
<div class="container">
<CreateNftHeader h1="Put your NFT in a collection." h2="Every NFT needs to live inside of a collection. Where will yours live?" :step-current="5" :step-max="5" />
<h2 class="subheading">Create a collection, or choose an existing one.</h2>
<div id="collection-grid" class="card-grid" v-if="collections.length > 0">
<!--<div v-for="c in collections" :key="c.address" @click="selectCollection(i)" class="collection">-->
<div v-for="c in collections" :key="c.address" class="collection">
<CollectionCard class="hoverable" :symbol="c.symbol" :name="c.name" :address="c.address" :clickAction="selectCollection" :selectedCollection="selected" />
</div>
</div>
<div v-else>
<h3>Loading...</h3>
</div>
<router-link to="/createnft/confirm" class="button button-round-accent disabled" id="continue-button">Continue</router-link>
<div v-if="createCollectionState" id="overlay" />
<aside v-if="createCollectionState" id="create-collection">
<form class="create-collection-inner" v-if="createCollectionState == 'create-store-form'" id="create-store-form">
<div class="heading-container">
<WhiteXIcon class="close-collection-icon" alt="Close collection form" @click="closeCreateCollectionPane" />
<h2>Create a collection</h2>
</div>
<h3>Required - you wont be able to edit the name or symbol after creation.</h3>
<h4>Each of your collections must have a unique name and symbol.</h4>
<input type="text" id="input-name" name="name" v-model="name" placeholder="Collection name" maxlength="40" />
<div class="field-info">
<span v-if="displayNameError" class="field-error">{{ nameError }}</span>
<span class="field-maxlen">{{ name.length }}/40</span>
</div>
<input type="text" id="input-symbol" name="symbol" v-model="symbol" placeholder="Symbol - EX. GIGL. FOX, ANIME" maxlength="10" pattern="[a-zA-Z0-9]*" />
<div class="field-info">
<span v-if="displaySymbolError" class="field-error">{{ symbolError }}</span>
<span class="field-maxlen">{{ symbol.length }}/10</span>
</div>
<p class="fee-message">Creating a collection has a <strong>one-time network fee.</strong> <br />Afterwards, you can put any NFTs into it.</p>
<button type="button" class="button button-round-accent" id="create-collection-button" :class="{ disabled: formError }" @click="createCollection">Create collection</button>
</form>
<section class="waiting create-collection-inner" v-if="createCollectionState == 'waiting-for-signature'" id="waiting-for-signature">
<h2>Waiting for your signature...</h2>
<p>Check your wallet app of choice</p>
<button class="button button-round-accent" id="retry-button" @click="openCreateCollectionPane">Retry</button>
</section>
<section class="waiting create-collection-inner" v-if="createCollectionState == 'waiting-for-confirmation'" id="waiting-for-confirmation">
<h2 id="creating-message">Creating your collection on the blockchain...</h2>
<p>This should take between 10-120 seconds</p>
<img class="loading" src="/hourglass.gif" />
</section>
<section class="waiting create-collection-inner" v-if="createCollectionState == 'error'" id="transaction-error">
<h2 id="creating-message" class="error">{{ error }}</h2>
<h2 class="sub-error error">{{ error_secondary }}</h2>
<p>Please contact support, or retry your transaction</p>
<button class="button button-round-accent" id="retry-button" @click="openCreateCollectionPane">Retry</button>
</section>
</aside>
</div>
</template>
<style scoped>
#collection-grid.card-grid {
justify-content: left;
}
#continue-button {
margin-top: 40px;
font-size: 24px;
line-height: 34px;
font-weight: 600;
}
#create-collection {
position: fixed;
width: 42%;
min-width: 815px;
height: 100%;
top: 0;
right: 0;
bottom: 0;
z-index: 20;
background-color: #011313;
}
.create-collection-inner {
display: flex;
flex-direction: column;
margin: 0 10%;
}
#create-collection .heading-container {
padding-top: 32px;
}
#create-collection .heading-container h2 {
font-size: 32px;
line-height: 38px;
font-weight: bold;
margin-top: 16px;
margin-bottom: 0;
}
.close-collection-icon {
display: block;
margin-left: auto;
cursor: pointer;
}
#create-collection h3 {
font-size: 16px;
line-height: 24px;
font-weight: bold;
margin-top: 32px;
}
#create-collection h4 {
font-size: 16px;
line-height: 24px;
margin-bottom: 8px;
}
#create-collection .fee-message {
color: var(--green-success);
font-size: 20px;
line-height: 26px;
}
#create-collection form button {
margin-top: 32px;
margin-left: 0;
margin-right: 0;
}
.field-info {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
margin-top: 8px;
margin-bottom: 16px;
font-size: 16px;
line-height: 24px;
}
.field-error {
color: var(--red);
font-size: 14px;
}
.field-maxlen {
flex-grow: 1;
margin-left: 16px;
text-align: right;
color: var(--card-foreground);
white-space: nowrap;
}
#input-symbol {
text-transform: uppercase;
}
.waiting {
height: 100%;
justify-content: center;
text-align: center;
}
.waiting h2 {
color: var(--green-success);
margin: 0 auto;
}
.waiting p {
color: var(--offwhite);
text-align: center;
font-size: 16px;
line-height: 30px;
margin-top: 16px;
margin-bottom: 16px;
}
#retry-button {
width: max-content;
margin-left: auto;
margin-right: auto;
}
.sub-error {
margin-top: 16px !important;
font-size: 0.8em;
}
</style>
<script>
import shared from "@/shared";
import CreateNftHeader from "@/components/CreateNftHeader.vue";
import CollectionCard from "@/components/CollectionCard.vue";
import WhiteXIcon from "@/assets/images/white-x.svg";
export default {
components: {
CreateNftHeader,
CollectionCard,
WhiteXIcon,
},
data: function() {
return {
collections: [],
selected: null,
name: "",
symbol: "",
factory: null,
error: "",
createCollectionState: "",
displayNameError: false,
displaySymbolError: false,
};
},
async created() {
// It takes a moment for the collections to update on the backend... let's make two attempts at refreshing
setTimeout(() => {
this.refreshCollections();
}, 500);
setTimeout(() => {
this.refreshCollections();
}, 2000);
},
computed: {
user() {
return this.$store.state.user;
},
nameError() {
console.log("nameError");
if (this.name == "") {
return "Name cannot be empty";
}
const existingCollections = this.collections.find((c) => c.name == this.name);
if (existingCollections) {
return "You already have a collection with this name";
}
return null;
},
symbolError() {
if (this.symbol == "") {
return "Symbol cannot be empty";
}
const existingCollections = this.collections.find((c) => c.symbol == this.symbol.toUpperCase());
if (existingCollections) {
return "You already have a collection with this symbol";
}
return null;
},
formError() {
return (this.displayNameError && this.nameError) || (this.displaySymbolError && this.symbolError);
},
},
methods: {
selectCollection(address) {
console.log("selectCollection called with " + address);
this.selected = address;
// if clicking 'create new collection...'
if (address == "_new_") {
this.openCreateCollectionPane();
document.getElementById("continue-button").classList.add("disabled");
} else {
document.getElementById("continue-button").classList.remove("disabled");
// Set the desired collection on the form
const selectedCollectionObj = this.collections.find((c) => c.address == address);
this.$store.commit("updateSimpleSetFormField", { index: 0, field: "collection", value: selectedCollectionObj });
}
},
openCreateCollectionPane() {
this.name = "";
this.symbol = "";
this.createCollectionState = "create-store-form";
},
closeCreateCollectionPane() {
this.createCollectionState = "";
},
async refreshCollections() {
const apiFetch = await shared.fetchJson(`${this.$apiBase}/api/stores/${this.user.id}`);
const createNew = {
address: "_new_",
name: "Create new collection",
symbol: "_new_",
creator: "",
};
this.collections = [createNew].concat(apiFetch);
},
async createCollection() {
console.debug("Validating collection details...");
const notValid = this.nameError || this.symbolError;
if (notValid) {
this.displayNameError = true;
this.displaySymbolError = true;
return;
}
console.debug("Creating a new collection");
this.createCollectionState = "waiting-for-signature";
this.symbol = this.symbol.toUpperCase();
const factoryAbi = await this.$factoryAbiPromise;
this.factory = new this.$root.web3.eth.Contract(factoryAbi, this.$factoryAddress);
try {
// note that using newstore automatically appends the store contract address to metadata uri.
window.newStoreTransaction = await this.factory.methods
.newStore(this.name, this.symbol)
.send({ from: this.user.id })
.on("transactionHash", (hash) => {
console.debug(`Setting transaction: ${hash}`);
window.transaction = hash;
this.createCollectionState = "waiting-for-confirmation";
})
.on("receipt", (receipt) => {
console.debug("Received receipt of blockchain confirmation: " + JSON.stringify(receipt));
})
.on("confirmation", (confirmationNumber) => {
if (confirmationNumber == 0) {
console.debug(`Received ${confirmationNumber} blockchain confirmations`);
document.getElementById("creating-message").innerText = "Received one blockchain confirmation. Waiting for one more...";
} else if (confirmationNumber == 1) {
console.debug(`Received ${confirmationNumber} blockchain confirmations`);
this.createCollectionState = ""; // done
return;
}
})
.catch((error) => {
this.createCollectionState = "error";
const msg = shared.handleTransactionError(error);
this.error = msg.errorMessage;
this.error_secondary = msg.errorMessageSecondary;
});
} finally {
this.refreshCollections();
}
},
},
};
</script>

View File

@ -0,0 +1,306 @@
<template>
<div class="container">
<CreateNftHeader h1="Set a price for your NFT." h2="Choose which currency you prefer — then set a price that collectors will find enticing." :step-current="4" :step-max="5" />
<div class="details-container">
<div class="details-form">
<label for="input-currency"><h2 class="subheading">What currency are you accepting?</h2></label>
<select id="input-currency" name="currency" v-model="currency">
<option value="Ubiq" selected>Ubiq</option>
<option value="10grans">10grans</option>
</select>
<label for="input-price"><h2 class="subheading">Price</h2></label>
<input type="number" step="any" id="input-price" name="price" v-model="price" placeholder="How much will one copy sell for?" />
<div class="current-rate">
<p class="current-rate" v-if="currency == '10grans'">Current exchange rate: 1 GRANS = ${{ roundTo2Decimals(tokenPrice) }}</p>
<p class="current-rate" v-else>Current exchange rate: 1 UBQ = ${{ roundTo2Decimals(ethPrice) }}</p>
<p>
When one copy sells you'll receive <strong> {{ priceFormatted }} {{ currencySymbol }} (${{ priceUSD }}).</strong>
</p>
<p class="current-rate-royalty">You will also earn an <strong>8% royalty</strong> on all resales.</p>
</div>
<div v-if="showCount">
<label for="input-count"><h2 class="subheading">Number of copies</h2></label>
<input
type="number"
min="1"
max="50"
step="1"
id="input-count"
name="count"
v-model="count"
placeholder="How many will be available to buy?"
@focus="displayFormError = true"
/>
<p class="count-description">Up to 50 copies</p>
</div>
<div class="checkbox-container">
<input type="checkbox" id="for-sale" name="for-sale" v-model="forSale" />
<label for="for-sale">Put this NFT on sale immediately?</label>
</div>
<button type="button" class="button button-round-accent" :class="{ disabled: displayFormError && countError }" id="continue-button" @click="continueButton">
Continue
</button>
<span v-if="displayFormError && countError" class="field-error">Set the number of copies before continuing.</span>
</div>
<aside class="details-preview">
<h2 class="subheading">Preview</h2>
<div class="nft-card-wrapper" @click="showImage">
<NftCard :nft="nft" :creator-name-override="username" :favorites-override="'1'" :quantity-override="count" />
</div>
</aside>
</div>
</div>
</template>
<style scoped>
.details-container {
display: flex;
}
.details-form {
width: 42%;
display: flex;
flex-direction: column;
}
.details-preview {
width: 25%;
margin: 0 auto;
}
select {
margin-bottom: 40px;
}
input {
width: 100%;
}
.current-rate {
margin-top: 16px;
margin-bottom: 30px;
}
.current-rate-royalty {
margin-top: 12px;
}
.count-description {
margin-top: 16px;
}
.checkbox-container {
margin-top: 58px;
}
#continue-button {
margin-top: 56px;
text-align: center;
font-size: 24px;
line-height: 34px;
}
.nft-card-wrapper {
cursor: pointer;
}
.nft-card {
pointer-events: none;
}
.field-error {
color: var(--red);
text-align: center;
}
</style>
<script>
import shared from "@/shared";
import * as basicLightbox from "basiclightbox";
import CreateNftHeader from "@/components/CreateNftHeader.vue";
import NftCard from "@/components/NftCard.vue";
export default {
components: {
CreateNftHeader,
NftCard,
},
data: function() {
return {
displayFormError: false,
selectedCurrency: "Ubiq",
ethPrice: 0,
tokenPrice: 0,
rawPriceStr: "",
};
},
async created() {
const price = await shared.getPrice(this.$apiBase);
this.ethPrice = price.ubiqUsdRatio;
this.tokenPrice = price.ubiqUsdRatio / price.ubiqGransRatio;
if (!this.showCount) {
this.count = 1;
}
},
computed: {
form() {
return this.$store.state.simpleSetForms[0];
},
nft() {
let nft = {
metadata: {
name: this.form.name,
image: this.form.blobUrl,
},
};
if (this.currency == "Ubiq") {
nft.ethPrice = this.price * 10e17;
nft.ethForSale = this.forSale;
nft.tokenPrice = 0;
nft.tokenForSale = false;
} else if (this.currency == "10grans") {
nft.ethPrice = 0;
nft.ethForSale = false;
nft.tokenPrice = this.price * 10e17;
nft.tokenForSale = this.forSale;
}
return nft;
},
username() {
if (this.$store.state.user) return this.$store.state.user.username;
return "";
},
currency: {
get() {
return this.selectedCurrency;
},
set(value) {
// swap price values when changing currency
this.swapPrices();
// swap state of checkbox
if (this.forSale) {
let payload = this.form.prices;
payload.ethForSale = false;
payload.tokenForSale = false;
if (value == "Ubiq") {
payload.ethForSale = true;
} else if (value == "10grans") {
payload.tokenForSale = true;
}
this.$store.commit("updateSimpleSetFormField", { index: 0, field: "prices", value: payload });
}
this.selectedCurrency = value;
},
},
currencySymbol: {
get() {
if (this.currency == "10grans") return "GRANS";
return "UBQ";
},
},
priceFormatted: {
get() {
if (!this.price) {
return 0;
}
return shared.formatPrice(this.price * 1e18);
},
},
priceUSD: {
get() {
if (this.currency == "10grans") {
return this.roundTo2Decimals(this.price * this.tokenPrice);
}
return this.roundTo2Decimals(this.price * this.ethPrice);
},
},
price: {
get() {
return this.rawPriceStr;
},
set(value) {
this.rawPriceStr = value;
this.forSale = true; // set forSale=true when setting the price
let payload = this.form.prices;
if (this.currency == "Ubiq") payload.ethPrice = Number(value);
else if (this.currency == "10grans") payload.tokenPrice = Number(value);
this.$store.commit("updateSimpleSetFormField", { index: 0, field: "prices", value: payload });
},
},
forSale: {
get() {
if (this.currency == "Ubiq") return this.form.prices.ethForSale;
else if (this.currency == "10grans") return this.form.prices.tokenForSale;
return null;
},
set(value) {
let payload = this.form.prices;
payload.ethForSale = false;
payload.tokenForSale = false;
if (this.currency == "Ubiq") {
payload.ethForSale = value;
} else if (this.currency == "10grans") {
payload.tokenForSale = value;
}
this.$store.commit("updateSimpleSetFormField", { index: 0, field: "prices", value: payload });
},
},
showCount: {
get() {
return !!this.$store.state.showCount;
},
},
countError() {
return !Number.isInteger(this.count) || this.count < 1 || this.count > 50;
},
count: {
get() {
return this.form.count;
},
set(value) {
this.$store.commit("updateSimpleSetFormField", { index: 0, field: "count", value: Number(value) });
},
},
},
methods: {
swapPrices() {
let payload = this.form.prices;
const tmp = payload.tokenPrice;
payload.tokenPrice = payload.ethPrice;
payload.ethPrice = tmp;
this.$store.commit("updateSimpleSetFormField", { index: 0, field: "prices", value: payload });
},
roundTo2Decimals(value) {
return (Math.round(value * 100) / 100).toFixed(2);
},
continueButton() {
const notValid = this.countError;
if (notValid) {
// they tried to click continue but the form isn't ready- show all the errors
this.displayFormError = true;
} else {
this.$router.push("/createnft/selectcollection");
}
},
showImage() {
this.lightbox = basicLightbox.create(`<img id="full-image" src="${this.form.blobUrl}"/>`);
this.lightbox.show();
},
},
};
</script>

View File

@ -0,0 +1,257 @@
<template>
<div class="container">
<CreateNftHeader h1="What's it look like?" h2="We support JPG, PNG, and GIF formats — for now." :step-current="2" :step-max="5" />
<form>
<h2 class="subheading">First, upload that beautiful artwork.</h2>
<label for="file-picker">
<section class="upload-area hoverable" @drop="drop" @dragover.prevent @dragenter.prevent>
<div class="upload-state" id="upload-state-default">
<UploadIcon class="upload-icon" alt="Click here to upload an image" />
<h2>Drag and drop your file here or <span class="accent">browse your files</span></h2>
<p>{{ this.sizeLimit }}MB size limit</p>
</div>
<div class="upload-state hidden" id="upload-state-error">
<UploadErrorIcon class="upload-icon" alt="Error" />
<h2 id="error-message" class="error">That file is too big or it's the wrong type.</h2>
<p>Make sure your file is smaller than {{ this.sizeLimit }}MB and either JPG, PNG, or GIF.</p>
</div>
<div class="upload-state hidden" id="upload-state-pending">
<h2>Processing image...</h2>
</div>
<div class="upload-state hidden" id="upload-state-complete">
<img src="" alt="Previewed NFT image" id="nft-preview-image" />
</div>
</section>
</label>
<input id="file-picker" accept="image/png,image/gif,image/jpeg" type="file" @change="drop" />
<!-- TODO- if multiple upload, add 'multiple' attribute -->
</form>
<section class="upload-buttons">
<router-link to="/createnft/details" class="button button-round-accent disabled" id="upload-button">Upload</router-link>
<button @click="reset" class="button button-round-reset disabled" id="reset-button">Reset</button>
</section>
</div>
</template>
<style scoped>
.upload-area {
background-color: var(--card-background);
border: 1px solid var(--accent);
border-radius: 8px;
box-sizing: border-box;
height: 500px;
display: flex;
flex-direction: column;
text-decoration: none;
cursor: pointer;
}
.upload-state {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.upload-state.hidden {
display: none;
}
.upload-area .upload-icon {
display: block;
margin: 0 auto 26px auto;
}
.upload-area h2 {
text-align: center;
font-weight: normal;
}
.upload-area h2 .accent {
color: var(--accent);
font-weight: bold;
}
.upload-area p {
text-align: center;
}
.upload-buttons {
display: flex;
justify-content: center;
}
.upload-buttons .button {
font-size: 24px;
line-height: 34px;
padding: 16px 40px;
margin-top: 24px;
}
#file-picker {
display: none;
}
#nft-preview-image {
display: block;
margin: 0 auto;
max-height: 80%;
max-width: 80%;
}
</style>
<script>
import CreateNftHeader from "@/components/CreateNftHeader.vue";
import UploadIcon from "@/assets/images/upload.svg";
import UploadErrorIcon from "@/assets/images/upload-error.svg";
export default {
components: {
CreateNftHeader,
UploadIcon,
UploadErrorIcon,
},
computed: {
sizeLimit() {
return 200; // filesize limit in megabytes
},
forms() {
return this.$store.state.simpleSetForms;
},
},
methods: {
async drop(e) {
console.debug("handling image drop...");
e.preventDefault(); // prevent default browser drag behavior
const errorMessages = {
"no-file": "Unexpected error during file upload; could not parse file.",
"too-big": "That file is too big.",
"wrong-type": "That file is the wrong type.",
};
try {
this.setFormState("pending");
try {
let files;
if (e.dataTransfer) {
files = [...e.dataTransfer.files];
} else {
const fileElem = document.getElementById("file-picker");
if (fileElem.files) {
files = [...fileElem.files];
} else {
throw errorMessages["no-file"];
}
}
// check type
const originalCount = files.length;
files = files.filter((file) => ["image/png", "image/gif", "image/jpeg"].includes(file.type));
if (originalCount != files.length) {
throw errorMessages["wrong-type"];
}
// check size
files = files.filter((file) => file.size < this.sizeLimit * 1024 * 1024);
if (originalCount != files.length) {
throw errorMessages["too-big"];
}
// clear all previous batch form entries
this.$store.commit("clearSimpleSetForms");
// handle multiple? todo?
//for (const file of files) {
// const blobUrl = URL.createObjectURL(file);
// await this.$setItem(`image/${ret.newId}`, file);
// todo- set form with image
//}
// handle single
const ret = {};
this.$store.commit("addSimpleSetForm", ret);
const blobUrl = URL.createObjectURL(files[0]);
await this.$setItem(`image/${ret.newId}`, files[0]);
this.$store.commit("updateSimpleSetFormBlobUrl", { index: 0, value: blobUrl });
// prepopulate extra props with an empty value
this.$store.commit("addSimpleSetFormProperty", { index: 0, name: "", value: "" });
// Set image with ID
document.getElementById("nft-preview-image").src = blobUrl;
console.debug("Received files: ");
console.debug(files);
console.debug("blob:");
let res = await this.$getItem(`image/${this.$store.state.simpleSetForms[0].id}`);
console.debug(res);
} finally {
// We don't actually need to wait to show the 'Uploading image...' dialogue. Let's fake it with a half-second timeout
await new Promise((resolve) => setTimeout(resolve, 500));
}
} catch (e) {
if (e instanceof Error) {
// unexpected error- hide real message from user
console.error(e);
this.setError("Unexpected error while processing file! Please contact support.");
this.setFormState("error");
} else {
// anticipated error- show message to user
console.error(e);
this.setError(e);
this.setFormState("error");
}
return;
}
// done
this.setFormState("complete");
},
setFormState(state) {
document.getElementById("upload-state-default").classList.add("hidden");
document.getElementById("upload-state-error").classList.add("hidden");
document.getElementById("upload-state-pending").classList.add("hidden");
document.getElementById("upload-state-complete").classList.add("hidden");
document.getElementById("upload-button").classList.add("disabled");
document.getElementById("reset-button").classList.add("disabled");
// state should be one of "default", "error", "pending", "complete"
switch (state) {
case "default":
document.getElementById("upload-state-default").classList.remove("hidden");
break;
case "error":
document.getElementById("upload-state-error").classList.remove("hidden");
document.getElementById("reset-button").classList.remove("disabled");
break;
case "pending":
document.getElementById("upload-state-pending").classList.remove("hidden");
break;
case "complete":
document.getElementById("upload-state-complete").classList.remove("hidden");
document.getElementById("upload-button").classList.remove("disabled");
document.getElementById("reset-button").classList.remove("disabled");
break;
default:
document.getElementById("upload-state-error").classList.remove("hidden");
document.getElementById("reset-button").classList.remove("disabled");
this.setError("Unexpected form state! Please contact support.");
}
},
setError(err) {
document.getElementById("error-message").innerText = err;
},
reset() {
this.setFormState("default");
},
},
};
</script>

370
src/views/Home.vue Normal file
View File

@ -0,0 +1,370 @@
<template>
<div class="container homepage">
<p class="error">{{ error }}</p>
<div v-if="showSiteMessage" class="site-message">
<div class="dot" />
<p>Notice: if you're having trouble minting, please join our <a href="https://discord.gg/2vq4WcD">Discord</a></p>
<CloseModal alt="close message" class="close-message" @click="showSiteMessage = false" />
</div>
<div class="homepage-top" v-if="nftsFinishedLoading && this.nfts.length > 0">
<FeaturedCard :nft="this.featuredNft" />
</div>
<div class="homepage-top" v-else-if="backendFailure">
<h1 class="error">Token.gallery backend down.<br />Please try later</h1>
</div>
<div class="homepage-top" v-else-if="!nftsFinishedLoading">
<h1>Loading...</h1>
</div>
<div class="homepage-top" v-else>
<h1>Nothing to show</h1>
<h3><router-link to="/" class="button button-round-accent">Homepage</router-link></h3>
</div>
<div class="filter-switches">
<div class="left">
<button class="button-round-white">Newest Mints</button>
<!--
<button class="button-round-darkgray">Most Activity</button>
<button class="button-round-darkgray">Random</button>
-->
</div>
<div class="right">
<div class="switch">
<input class="switch__input" type="checkbox" id="verified-only" name="verified-only" v-model="showVerified" />
<label class="switch__label" for="verified-only">Verified Only</label>
</div>
<div class="switch">
<input class="switch__input" type="checkbox" id="for-sale" name="for-sale" v-model="latestForSale" />
<label class="switch__label" for="for-sale">For Sale Only</label>
</div>
</div>
</div>
<div v-if="nftsFinishedLoading && this.nfts.length > 0" id="main-grid" class="card-grid">
<NftCard v-for="(nft, index) of nfts.slice(1)" :key="index" :nft="nft" />
</div>
<div class="paging">
<router-link class="button button-square-accent" :to="prevPageLink || '#'" :class="{ disabled: !prevPageLink }"> Prev Page </router-link>
<span class="current-page">{{ currentPage }}</span>
<router-link class="button button-square-accent" :to="nextPageLink || '#'" :class="{ disabled: !nextPageLink }">Next Page </router-link>
</div>
</div>
</template>
<style scoped>
.container.homepage {
margin-top: 0;
}
.newest-nfts {
margin-top: 80px;
text-align: center;
}
.latest-options {
margin-bottom: 32px;
text-align: center;
}
.latest-options p {
line-height: 24px;
}
h1 {
text-align: center;
}
h3 {
text-align: center;
}
.site-message {
position: absolute;
right: 0;
max-width: max-content;
margin-right: 24px;
padding: 14px 21px;
margin-top: 8px;
border-radius: 100px;
display: flex;
align-items: center;
background-color: var(--purple-background);
color: var(--offwhite);
font-size: 13px;
line-height: 120%;
letter-spacing: 0.75px;
font-weight: 600;
text-transform: uppercase;
}
.site-message .dot {
background-color: #f4b740;
border-radius: 100%;
width: 8px;
height: 8px;
border: 1px solid #14142b;
margin-right: 8px;
}
.site-message h2 {
text-align: center;
font-size: 32px;
}
.site-message a {
font-weight: 700;
color: var(--accent);
}
.site-message .close-message {
position: absolute;
right: -2px;
top: -2px;
cursor: pointer;
}
.homepage-top {
padding-top: 150px;
}
.paging {
display: flex;
justify-content: center;
align-items: center;
margin-top: 64px;
text-align: center;
}
.current-page {
margin: 0 32px;
font-weight: 700;
font-size: 38px;
}
.filter-switches {
position: relative;
z-index: 0;
display: flex;
align-items: center;
justify-content: space-between;
margin-left: 32px;
margin-right: 32px;
margin-top: 150px;
margin-bottom: 40px;
}
.filter-switches .left,
.filter-switches .right {
display: flex;
align-items: center;
justify-content: space-between;
user-select: none;
}
.filter-switches button {
font-weight: 400;
}
.switch:first-child {
margin-right: 24px;
}
.switch {
display: inline-block;
position: relative;
margin: 0 0 10px;
font-size: 16px;
line-height: 24px;
}
.switch__input {
position: absolute;
top: 0;
left: 0;
width: 36px;
height: 20px;
opacity: 0;
z-index: 0;
}
.switch__label {
display: block;
padding: 0 0 0 44px;
cursor: pointer;
margin-left: 12px;
font-weight: 600;
}
.switch__label:before {
content: "";
position: absolute;
left: 0;
width: 48px;
height: 24px;
background-color: var(--gray);
border-radius: 40px;
z-index: 1;
transition: background-color 0.28s cubic-bezier(0.4, 0, 0.2, 1);
}
.switch__label:after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 24px;
height: 24px;
background-color: white;
border-radius: 100%;
z-index: 2;
transition: all 0.28s cubic-bezier(0.4, 0, 0.2, 1);
transition-property: left, background-color;
}
.switch__input:checked + .switch__label:before {
background-color: var(--accent);
}
.switch__input:checked + .switch__label:after {
left: 24px;
background-color: white;
}
</style>
<script>
import NftCard from "../components/NftCard.vue";
import FeaturedCard from "../components/FeaturedCard.vue";
import shared from "../shared";
import CloseModal from "@/assets/images/close-modal.svg";
export default {
props: ["page"],
components: {
NftCard,
FeaturedCard,
CloseModal,
},
data: function() {
return {
nfts: [],
contracts: {},
error: "",
backendFailure: false,
nftsFinishedLoading: false,
};
},
metaInfo() {
return {
meta: [{ description: "nfts and digital asset accessories" }],
};
},
computed: {
showVerified: {
get() {
return !this.showUnverified;
},
set(val) {
this.showUnverified = !val;
},
},
showUnverified: {
get() {
return this.$store.state.showUnverified;
},
set(val) {
this.$store.commit("updateShowUnverified", val);
},
},
latestForSale: {
get() {
return this.$store.state.latestForSale;
},
set(val) {
this.$store.commit("updateLatestForSale", val);
},
},
latestCurrency: {
get() {
return this.$store.state.latestCurrency;
},
set(val) {
if (["any", "ubiq", "grans"].includes(val)) {
this.$store.commit("updateLatestCurrency", val);
}
},
},
showSiteMessage: {
get() {
return this.$store.state.showSiteMessage;
},
set(val) {
this.$store.commit("updateShowSiteMessage", val);
},
},
featuredNft() {
/*
[9:10 AM] Moon: need to find a way to make what's on the front page fair though otherwise it will just be whomever last entered stuff.
[9:11 AM] rwx: That is fair
[9:11 AM] rwx: Im being 100% serious
[9:11 AM] rwx: The featured banner rotation is the solve for that
[9:11 AM] rwx: Actual CHRONOLOGICAL DISCOVERY MAKES SENSE
[9:12 AM] rwx: thata my opinion
*/
if (this.nfts.length > 0) {
return this.nfts[0];
}
return null;
},
currentPage() {
if (this.$route.params.page) {
return Number(this.$route.params.page);
} else {
return 1;
}
},
prevPageLink() {
if (this.currentPage > 2) {
return `/page/${this.currentPage - 1}`;
} else if (this.currentPage == 2) {
return "/";
} else {
return null;
}
},
nextPageLink() {
// this code is a bit lazy; ideally, we should fetch ahead and see if there is actually another page of NFTs to load
// but we don't. this will show if any NFTs are present on current page.
if (this.nfts.length > 0) {
return `/page/${this.currentPage + 1}`;
} else {
return null;
}
},
},
asyncComputed: {
nfts: {
async get() {
try {
const adultClause = this.$store.state.showAdult ? "adult=true" : "";
const pageClause = `page=${this.currentPage}`;
const forSaleClause = this.latestForSale ? `forsale=${this.latestCurrency}` : "";
const showUnverifiedClause = this.showUnverified ? "showunverified=true" : "showunverified=false";
const res = await shared.fetchJson(`${this.$apiBase}/api/nfts/latest?${adultClause}&${pageClause}&${forSaleClause}&${showUnverifiedClause}`);
this.nftsFinishedLoading = true;
return res;
} catch (e) {
console.error(e);
this.backendFailure = true;
return [];
}
},
default: [],
},
},
};
</script>

1541
src/views/NftV3.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
<template>
<div class="container notifications-page">
<h1>All Notifications</h1>
<Notifications :page="page" />
</div>
</template>
<style scoped>
.notifications-page {
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
h1 {
font-size: 48px;
line-height: 58px;
font-weight: 800;
}
</style>
<script>
import Notifications from "@/components/Notifications.vue";
export default {
components: {
Notifications,
},
computed: {
page() {
if (this.$route.params.page) {
return Number(this.$route.params.page);
} else {
return 1;
}
},
},
};
</script>

View File

@ -0,0 +1,16 @@
<template>
<div class="container">
<h1>Page Not Found</h1>
<img src="/think.jpg" alt="oh no" />
<router-link to="/" class="button button-round-accent">Back to Homepage</router-link>
</div>
</template>
<style scoped>
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>

602
src/views/Privacy.vue Normal file
View File

@ -0,0 +1,602 @@
<template>
<div class="container policy">
<h1>Privacy Policy</h1>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
Last updated: August 13, 2021
</p>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
This Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your information when You use the Service and tells You about Your privacy
rights and how the law protects You.
</p>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
We use Your Personal data to provide and improve the Service. By using the Service, You agree to the collection and use of information in accordance with this Privacy Policy.
</p>
<h2 style="margin-top:24.0pt;page-break-after:auto"><a name="h.tqyq7sdon6yo"></a>Interpretation and Definitions</h2>
<h3 style="page-break-after:auto"><a name="h.wjznxaqwaoon"></a>Interpretation</h3>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of
whether they appear in singular or in plural.
</p>
<h2 style="margin-bottom:4.0pt;page-break-after:auto">
<a name="h.bbg2re1nsip1"></a><b><span style="font-size:17.0pt;line-height:115%">Definitions</span></b>
</h2>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
For the purposes of this Privacy Policy:
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>Account</b> means a unique account created for You to access our
Service or parts of our Service.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>Company</b> (referred to as either &quot;the Company&quot;,
&quot;We&quot;, &quot;Us&quot; or &quot;Our&quot; in this Agreement) refers to token gallery.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>Cookies</b> are small files that are placed on Your computer, mobile
device or any other device by a website, containing the details of Your browsing history on that website among its many uses.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>Country</b> refers to: Delaware, United States
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>Device</b> means any device that can access the Service such as a
computer, a cellphone or a digital tablet.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>Personal Data</b> is any information that relates to an identified or
identifiable individual.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>Service</b> refers to the Website.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>Service Provider</b> means any natural or legal person who processes
the data on behalf of the Company. It refers to third-party companies or individuals employed by the Company to facilitate the Service, to provide the Service on behalf of
the Company, to perform services related to the Service or to assist the Company in analyzing how the Service is used.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>Third-party Social Media Service</b> refers to any website or any
social network website through which a User can log in or create an account to use the Service.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>Usage Data</b> refers to data collected automatically, either generated
by the use of the Service or from the Service infrastructure itself (for example, the duration of a page visit).
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>Website</b> refers to token gallery, accessible from token.gallery
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>You</b> means the individual accessing or using the Service, or the
company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable.
</p>
<h2 style="margin-top:24.0pt;page-break-after:auto"><a name="h.rwlz5nhf1hbi"></a>Collecting and Using Your Personal Data</h2>
<h3 style="page-break-after:auto"><a name="h.pr95ju2rhd3o"></a>Types of Data Collected</h3>
<h3 style="margin-top:14.0pt;page-break-after:auto">
<a name="h.lzxuicdd44oe"></a><b><span style="font-size:13.0pt;line-height:115%;">Personal Data</span></b>
</h3>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
While using Our Service, We may ask You to provide Us with certain personally identifiable information that can be used to contact or identify You. Personally identifiable
information may include, but is not limited to:
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span>Usage Data
</p>
<h3 style="margin-top:14.0pt;page-break-after:auto">
<a name="h.ma05v8d8urdt"></a><b><span style="font-size:13.0pt;line-height:115%;">Usage Data</span></b>
</h3>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
Usage Data is collected automatically when using the Service.
</p>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
Usage Data may include information such as Your Device's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that You visit,
the time and date of Your visit, the time spent on those pages, unique device identifiers and other diagnostic data.
</p>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
When You access the Service by or through a mobile device, We may collect certain information automatically, including, but not limited to, the type of mobile device You use,
Your mobile device unique ID, the IP address of Your mobile device, Your mobile operating system, the type of mobile Internet browser You use, unique device identifiers and
other diagnostic data.
</p>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
We may also collect information that Your browser sends whenever You visit our Service or when You access the Service by or through a mobile device.
</p>
<h3 style="margin-top:14.0pt;page-break-after:auto">
<a name="h.mr4or9lxahm"></a><b><span style="font-size:13.0pt;line-height:115%;">Information from Third-Party Social Media Services</span></b>
</h3>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
The Company allows You to create an account and log in to use the Service through the following Third-party Social Media Services:
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span>Google</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span>Facebook
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span>Twitter</p>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
If You decide to register through or otherwise grant us access to a Third-Party Social Media Service, We may collect Personal data that is already associated with Your
Third-Party Social Media Service's account, such as Your name, Your email address, Your activities or Your contact list associated with that account.
</p>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
You may also have the option of sharing additional information with the Company through Your Third-Party Social Media Service's account. If You choose to provide such
information and Personal Data, during registration or otherwise, You are giving the Company permission to use, share, and store it in a manner consistent with this Privacy
Policy.
</p>
<h3 style="margin-top:14.0pt;page-break-after:auto">
<a name="h.3858q8l5bdtq"></a><b><span style="font-size:13.0pt;line-height:115%;">Tracking Technologies and Cookies</span></b>
</h3>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
We use Cookies and similar tracking technologies to track the activity on Our Service and store certain information. Tracking technologies used are beacons, tags, and scripts
to collect and track information and to improve and analyze Our Service. The technologies We use may include:
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
<span style="">&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span></span><b>Cookies or Browser Cookies.</b> A cookie is a
small file placed on Your Device. You can instruct Your browser to refuse all Cookies or to indicate when a Cookie is being sent. However, if You do not accept Cookies, You
may not be able to use some parts of our Service. Unless you have adjusted Your browser setting so that it will refuse Cookies, our Service may use Cookies.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>Web Beacons.</b> Certain sections of our Service and our emails may
contain small electronic files known as web beacons (also referred to as clear gifs, pixel tags, and single-pixel gifs) that permit the Company, for example, to count users
who have visited those pages or opened an email and for other related website statistics (for example, recording the popularity of a certain section and verifying system and
server integrity).
</p>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
Cookies can be &quot;Persistent&quot; or &quot;Session&quot; Cookies. Persistent Cookies remain on Your personal computer or mobile device when You go offline, while Session
Cookies are deleted as soon as You close Your web browser.
</p>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
We use both Session and Persistent Cookies for the purposes set out below:
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>Necessary / Essential Cookies<br /> </b>Type: Session Cookies<br />
Administered by: Us<br />
Purpose: These Cookies are essential to provide You with services available through the Website and to enable You to use some of its features. They help to authenticate users
and prevent fraudulent use of user accounts. Without these Cookies, the services that You have asked for cannot be provided, and We only use these Cookies to provide You with
those services.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>Cookies Policy / Notice Acceptance Cookies<br /> </b>Type: Persistent
Cookies<br />
Administered by: Us<br />
Purpose: These Cookies identify if users have accepted the use of cookies on the Website.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>Functionality Cookies<br /> </b>Type: Persistent Cookies<br />
Administered by: Us<br />
Purpose: These Cookies allow us to remember choices You make when You use the Website, such as remembering your login details or language preference. The purpose of these
Cookies is to provide You with a more personal experience and to avoid You having to re-enter your preferences every time You use the Website.
</p>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
For more information about the cookies we use and your choices regarding cookies, please visit our Cookies Policy or the Cookies section of our Privacy Policy.
</p>
<h2 style="margin-bottom:4.0pt;page-break-after:auto">
<a name="h.4yuxswwi9ua6"></a><b><span style="font-size:17.0pt;line-height:115%">Use of Your Personal Data</span></b>
</h2>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
The Company may use Personal Data for the following purposes:
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>To provide and maintain our Service</b>, including to monitor the usage
of our Service.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>To manage Your Account:</b> to manage Your registration as a user of
the Service. The Personal Data You provide can give You access to different functionalities of the Service that are available to You as a registered user.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>For the performance of a contract:</b> the development, compliance and
undertaking of the purchase contract for the products, items or services You have purchased or of any other contract with Us through the Service.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>To contact You:</b> To contact You by email, telephone calls, SMS, or
other equivalent forms of electronic communication, such as a mobile application's push notifications regarding updates or informative communications related to the
functionalities, products or contracted services, including the security updates, when necessary or reasonable for their implementation.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>To provide You</b> with news, special offers and general information
about other goods, services and events which we offer that are similar to those that you have already purchased or enquired about unless You have opted not to receive such
information.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>To manage Your requests:</b> To attend and manage Your requests to Us.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>For business transfers:</b> We may use Your information to evaluate or
conduct a merger, divestiture, restructuring, reorganization, dissolution, or other sale or transfer of some or all of Our assets, whether as a going concern or as part of
bankruptcy, liquidation, or similar proceeding, in which Personal Data held by Us about our Service users is among the assets transferred.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>For other purposes</b>: We may use Your information for other purposes,
such as data analysis, identifying usage trends, determining the effectiveness of our promotional campaigns and to evaluate and improve our Service, products, services,
marketing and your experience.
</p>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
We may share Your personal information in the following situations:
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>With Service Providers:</b> We may share Your personal information with
Service Providers to monitor and analyze the use of our Service, to contact You.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>For business transfers:</b> We may share or transfer Your personal
information in connection with, or during negotiations of, any merger, sale of Company assets, financing, or acquisition of all or a portion of Our business to another
company.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>With Affiliates:</b> We may share Your information with Our affiliates,
in which case we will require those affiliates to honor this Privacy Policy. Affiliates include Our parent company and any other subsidiaries, joint venture partners or other
companies that We control or that are under common control with Us.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>With business partners:</b> We may share Your information with Our
business partners to offer You certain products, services or promotions.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>With other users:</b> when You share personal information or otherwise
interact in the public areas with other users, such information may be viewed by all users and may be publicly distributed outside. If You interact with other users or
register through a Third-Party Social Media Service, Your contacts on the Third-Party Social Media Service may see Your name, profile, pictures and description of Your
activity. Similarly, other users will be able to view descriptions of Your activity, communicate with You and view Your profile.
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span><b>With Your consent</b>: We may disclose Your personal information for
any other purpose with Your consent.
</p>
<h2 style="margin-bottom:4.0pt;page-break-after:auto">
<a name="h.qvcflbft9t40"></a><b><span style="font-size:17.0pt;line-height:115%">Retention of Your Personal Data</span></b>
</h2>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
The Company will retain Your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use Your Personal Data to the
extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our
legal agreements and policies.
</p>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
The Company will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period of time, except when this data is used to
strengthen the security or to improve the functionality of Our Service, or We are legally obligated to retain this data for longer time periods.
</p>
<h2 style="margin-bottom:4.0pt;page-break-after:auto">
<a name="h.36f50mtislx5"></a><b><span style="font-size:17.0pt;line-height:115%">Transfer of Your Personal Data</span></b>
</h2>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
Your information, including Personal Data, is processed at the Company's operating offices and in any other places where the parties involved in the processing are located.
It means that this information may be transferred to and maintained on computers located outside of Your state, province, country or other governmental jurisdiction where
the data protection laws may differ than those from Your jurisdiction.
</p>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
Your consent to this Privacy Policy followed by Your submission of such information represents Your agreement to that transfer.
</p>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
The Company will take all steps reasonably necessary to ensure that Your data is treated securely and in accordance with this Privacy Policy and no transfer of Your Personal
Data will take place to an organization or a country unless there are adequate controls in place including the security of Your data and other personal information.
</p>
<h2 style="margin-bottom:4.0pt;page-break-after:auto">
<a name="h.w9d7ze4beb1l"></a><b><span style="font-size:17.0pt;line-height:115%">Disclosure of Your Personal Data</span></b>
</h2>
<h3 style="margin-top:14.0pt;page-break-after:auto">
<a name="h.w2czgwipyxx0"></a><b><span style="font-size:13.0pt;line-height:115%;">Business Transactions</span></b>
</h3>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
If the Company is involved in a merger, acquisition or asset sale, Your Personal Data may be transferred. We will provide notice before Your Personal Data is transferred and
becomes subject to a different Privacy Policy.
</p>
<h3 style="margin-top:14.0pt;page-break-after:auto">
<a name="h.6lc9n2tm4wmu"></a><b><span style="font-size:13.0pt;line-height:115%;">Law enforcement</span></b>
</h3>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
Under certain circumstances, the Company may be required to disclose Your Personal Data if required to do so by law or in response to valid requests by public authorities
(e.g. a court or a government agency).
</p>
<h3 style="margin-top:14.0pt;page-break-after:auto">
<a name="h.a2grw2tjl342"></a><b><span style="font-size:13.0pt;line-height:115%;">Other legal requirements</span></b>
</h3>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
The Company may disclose Your Personal Data in the good faith belief that such action is necessary to:
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span>Comply with a legal obligation
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span>Protect and defend the rights or property of the Company
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span>Prevent or investigate possible wrongdoing in connection with the Service
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span>Protect the personal safety of Users of the Service or the public
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span>Protect against legal liability
</p>
<h2><a name="h.igt65osklq0f"></a>Security of Your Personal Data</h2>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
The security of Your Personal Data is important to Us, but remember that no method of transmission over the Internet, or method of electronic storage is 100% secure. While We
strive to use commercially acceptable means to protect Your Personal Data, We cannot guarantee its absolute security.
</p>
<h2 style="margin-top:24.0pt;page-break-after:auto"><a name="h.4132g10ol8h"></a>Children's Privacy</h2>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
Our Service does not address anyone under the age of 13. We do not knowingly collect personally identifiable information from anyone under the age of 13. If You are a parent
or guardian and You are aware that Your child has provided Us with Personal Data, please contact Us. If We become aware that We have collected Personal Data from anyone under
the age of 13 without verification of parental consent, We take steps to remove that information from Our servers.
</p>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
If We need to rely on consent as a legal basis for processing Your information and Your country requires consent from a parent, We may require Your parent's consent before We
collect and use that information.
</p>
<h2 style="margin-top:24.0pt;page-break-after:auto"><a name="h.6p0p13ptapxc"></a>Links to Other Websites</h2>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
Our Service may contain links to other websites that are not operated by Us. If You click on a third party link, You will be directed to that third party's site. We strongly
advise You to review the Privacy Policy of every site You visit.
</p>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services.
</p>
<h2 style="margin-top:24.0pt;page-break-after:auto"><a name="h.7slz5ef9m9i9"></a>Changes to this Privacy Policy</h2>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
We may update Our Privacy Policy from time to time. We will notify You of any changes by posting the new Privacy Policy on this page.
</p>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
We will let You know via email and/or a prominent notice on Our Service, prior to the change becoming effective and update the &quot;Last updated&quot; date at the top of
this Privacy Policy.
</p>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page.
</p>
<h2 style="margin-top:24.0pt;page-break-after:auto"><a name="h.6woelj4997gv"></a>Contact Us</h2>
<p
class="MsoNormal"
style="margin-top:12.0pt;margin-right:0in;margin-bottom:
12.0pt;margin-left:0in"
>
If you have any questions about this Privacy Policy, You can contact us:
</p>
<p class="MsoNormal" style="margin-left:.5in;text-indent:-.25in">
&#9679;<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span>By email: nfttokengallery@gmail.com
</p>
</div>
</template>
<style scoped>
.policy {
max-width: 950px;
margin-left: auto;
margin-right: auto;
}
h1 {
text-align: center;
}
</style>

167
src/views/Settings.vue Normal file
View File

@ -0,0 +1,167 @@
<template>
<div class="container">
<div v-if="error" class="error">{{ error }}</div>
<div class="settings-container" v-if="hasUser && userAuthenticated">
<h1>Welcome, {{ user.username }}!</h1>
<div class="youruser">
View your public user profile:
<router-link class="profile-link" :to="{ name: 'Address', params: { address: user.username } }">token.gallery/address/{{ user.username }}</router-link>
</div>
<div class="settings">
<h2>Settings</h2>
<ul>
<li>Show adult NFTs? <input type="checkbox" v-model="adult" /></li>
<p id="updated-notification" class="hidden">Settings updated.</p>
</ul>
</div>
<button @click="logout()">Log out</button>
<router-link to="/" class="button button-round-accent">Back to Homepage</router-link>
</div>
<div v-else class="unauthenticated-user">
<p>You are not logged in. Click 'Connect Wallet' to log in.</p>
<div class="back-to-home">
<a href="#" @click="$emit('connect-wallet')" class="button button-round-outline">Connect Wallet</a>
<router-link to="/" class="button button-round-accent">Back to Homepage</router-link>
</div>
</div>
</div>
</template>
<style scoped>
.container {
display: flex;
}
.settings-container {
margin: 0 auto;
}
h1 {
margin-bottom: 32px;
}
.error {
margin-bottom: 1em;
}
.metamask-checklist {
}
.metamask-checklist > tr > td {
padding-right: 5px;
}
.stores {
color: white;
}
table.stores td,
table.stores th {
color: white;
border: 1px solid white;
padding: 5px;
}
.store-blurb {
font-style: italic;
font-size: smaller;
}
#username {
text-transform: lowercase;
}
.youruser,
.settings,
.balances {
background-color: var(--dark);
padding: 1em;
border-radius: 16px;
margin-bottom: 1em;
}
#updated-notification {
font-style: italic;
margin-left: 1em;
font-weight: bold;
color: var(--red);
}
.back-to-home {
display: flex;
justify-content: center;
}
.profile-link {
text-decoration: underline;
}
.unauthenticated-user {
margin: 0 auto;
}
</style>
<script>
export default {
data: function() {
return {
error: "",
walletBalance: "0",
storedBalance: "0",
wallet10GransBalance: "0",
email: null,
emailVerified: false,
emailError: null,
nfts: [],
contracts: {},
stores: [],
requestedUsername: "",
noMetaMask: true,
};
},
components: {},
computed: {
user() {
return this.$store.state.user;
},
userId() {
return this.$store.state.user.id;
},
hasUser() {
return this.$store.state.user != null;
},
userAuthenticated() {
return this.$store.getters.isUserAuthenticated;
},
wrongChain() {
return window.ethereum && window.ethereum.chainId !== "0x" + process.env.VUE_APP_CHAIN_ID;
},
adult: {
get() {
return this.$store.state.showAdult;
},
set(value) {
this.$store.commit("updateShowAdult", value);
this.showUpdateMessage();
},
},
hasMetaMask() {
return typeof window.ethereum !== "undefined";
},
},
methods: {
showUpdateMessage() {
document.getElementById("updated-notification").classList.add("is-active");
setTimeout(() => {
document.getElementById("updated-notification").classList.remove("is-active");
}, 3000);
},
logout() {
this.$parent.logout();
},
},
};
</script>

Some files were not shown because too many files have changed in this diff Show More