Working outfit switch, started on styled components

This commit is contained in:
Mentor Palokaj 2021-11-27 17:14:06 +01:00
parent c0d5e193ad
commit 605e977bad
11 changed files with 219 additions and 2291 deletions

File diff suppressed because it is too large Load Diff

View File

@ -32,13 +32,10 @@ exports.generateNewOutfit = async function( req, res ) {
// Validate message
const messageObject = JSON.parse( message )
const { signer, rocketeerId, chainId } = messageObject
if( signer.toLowerCase() !== confirmedSignatory.toLowerCase() || !rocketeerId || chainId !== chain || !network ) throw new Error( `Invalid message` )
const network = chainId == '0x1' ? 'mainnet' : 'rinkeby'
if( signer.toLowerCase() !== confirmedSignatory.toLowerCase() || !rocketeerId || !network ) throw new Error( `Invalid generateNewOutfit message with ${signer}, ${confirmedSignatory}, ${rocketeerId}, ${network}` )
if( rocketeerId != id ) throw new Error( `Invalid Rocketeer in message` )
// Set chain based on envronnment
const chain = process.env.NODE_ENV == 'development' ? '0x4' : '0x1'
const network = chain == '0x1' ? 'mainnet' : 'rinkeby'
// Generate new rocketeer svg
const mediaLink = await generateNewOutfitFromId( id, network )
@ -88,7 +85,8 @@ exports.setPrimaryOutfit = async function( req, res ) {
// Validate message
const messageObject = JSON.parse( message )
let { signer, outfitId, chainId } = messageObject
if( signer.toLowerCase() !== confirmedSignatory.toLowerCase() || !outfitId || chainId !== chain || !network ) throw new Error( `Invalid message` )
const network = chainId == '0x1' ? 'mainnet' : 'rinkeby'
if( signer.toLowerCase() !== confirmedSignatory.toLowerCase() || outfitId == undefined || !network ) throw new Error( `Invalid setPrimaryOutfit message with ${ signer }, ${confirmedSignatory}, ${outfitId}, ${chainId}, ${network}` )
// Validate id format
outfitId = Math.floor( Math.abs( outfitId ) )
@ -97,10 +95,6 @@ exports.setPrimaryOutfit = async function( req, res ) {
// Set ID to string so firestore can handle it
outfitId = `${ outfitId }`
// Set chain based on envronnment
const chain = process.env.NODE_ENV == 'development' ? '0x4' : '0x1'
const network = chain == '0x1' ? 'mainnet' : 'rinkeby'
// Retreive old Rocketeer data
const rocketeer = await db.collection( `${ network }Rocketeers` ).doc( id ).get().then( dataFromSnap )
@ -116,6 +110,8 @@ exports.setPrimaryOutfit = async function( req, res ) {
image: `https://storage.googleapis.com/rocketeer-nft.appspot.com/${ network }Rocketeers/${ imagePath }`
}, { merge: true } )
return res.json( { success: true } )
} catch( e ) {

View File

@ -29,7 +29,7 @@ exports.generateNewOutfitFromId = async function( id, network='mainnet' ) {
// Check whether this Rocketeer is allowed to change
const timeUntilAllowedToChange = newOutfitAllowedInterval - ( Date.now() - last_outfit_change )
if( timeUntilAllowedToChange > 0 ) throw new Error( `You changed your outfit too recently, a change is avalable in ${ Math.floor( timeUntilAllowedToChange / ( 1000 * 60 * 60 ) ) } hours` )
if( timeUntilAllowedToChange > 0 ) throw new Error( `You changed your outfit too recently, a change is avalable in ${ Math.floor( timeUntilAllowedToChange / ( 1000 * 60 * 60 ) ) } hours (${ new Date( Date.now() + timeUntilAllowedToChange ).toString() })` )
// Grab attributes that will not change
const staticAttributes = rocketeer.attributes.filter( ( { trait_type } ) => ![ 'last outfit change', 'available outfits' ].includes( trait_type ) )

116
minter/package-lock.json generated
View File

@ -1203,6 +1203,29 @@
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz",
"integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg=="
},
"@emotion/is-prop-valid": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
"requires": {
"@emotion/memoize": "0.7.4"
}
},
"@emotion/memoize": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
},
"@emotion/stylis": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
"integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
},
"@emotion/unitless": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
},
"@eslint/eslintrc": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
@ -3886,6 +3909,49 @@
"@babel/helper-define-polyfill-provider": "^0.2.2"
}
},
"babel-plugin-styled-components": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.1.tgz",
"integrity": "sha512-U3wmORxerYBiqcRCo6thItIosEIga3F+ph0jJPkiOZJjyhpZyUZFQV9XvrZ2CbBIihJ3rDBC/itQ+Wx3VHMauw==",
"requires": {
"@babel/helper-annotate-as-pure": "^7.16.0",
"@babel/helper-module-imports": "^7.16.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"lodash": "^4.17.11"
},
"dependencies": {
"@babel/helper-annotate-as-pure": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.0.tgz",
"integrity": "sha512-ItmYF9vR4zA8cByDocY05o0LGUkp1zhbTQOH1NFyl5xXEqlTJQCEJjieriw+aFpxo16swMxUnUiKS7a/r4vtHg==",
"requires": {
"@babel/types": "^7.16.0"
}
},
"@babel/helper-module-imports": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz",
"integrity": "sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg==",
"requires": {
"@babel/types": "^7.16.0"
}
},
"@babel/types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.0.tgz",
"integrity": "sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg==",
"requires": {
"@babel/helper-validator-identifier": "^7.15.7",
"to-fast-properties": "^2.0.0"
}
}
}
},
"babel-plugin-syntax-jsx": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
"integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY="
},
"babel-plugin-syntax-object-rest-spread": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz",
@ -4662,6 +4728,11 @@
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz",
"integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg=="
},
"camelize": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz",
"integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
},
"caniuse-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@ -5270,6 +5341,11 @@
"postcss": "^7.0.5"
}
},
"css-color-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
"integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU="
},
"css-color-names": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
@ -5353,6 +5429,16 @@
"resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
"integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w=="
},
"css-to-react-native": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz",
"integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==",
"requires": {
"camelize": "^1.0.0",
"css-color-keywords": "^1.0.0",
"postcss-value-parser": "^4.0.2"
}
},
"css-tree": {
"version": "1.0.0-alpha.37",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
@ -8104,6 +8190,14 @@
"minimalistic-crypto-utils": "^1.0.1"
}
},
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"requires": {
"react-is": "^16.7.0"
}
},
"hoopy": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
@ -14620,6 +14714,11 @@
"safe-buffer": "^5.0.1"
}
},
"shallowequal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
},
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@ -15278,6 +15377,23 @@
"schema-utils": "^2.7.0"
}
},
"styled-components": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.3.tgz",
"integrity": "sha512-++4iHwBM7ZN+x6DtPPWkCI4vdtwumQ+inA/DdAsqYd4SVgUKJie5vXyzotA00ttcFdQkCng7zc6grwlfIfw+lw==",
"requires": {
"@babel/helper-module-imports": "^7.0.0",
"@babel/traverse": "^7.4.5",
"@emotion/is-prop-valid": "^0.8.8",
"@emotion/stylis": "^0.8.4",
"@emotion/unitless": "^0.7.4",
"babel-plugin-styled-components": ">= 1.12.0",
"css-to-react-native": "^3.0.0",
"hoist-non-react-statics": "^3.0.0",
"shallowequal": "^1.1.0",
"supports-color": "^5.5.0"
}
},
"stylehacks": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz",

View File

@ -11,6 +11,7 @@
"react-dom": "^17.0.2",
"react-router-dom": "^6.0.0",
"react-scripts": "4.0.3",
"styled-components": "^5.3.3",
"use-interval": "^1.4.0",
"web-vitals": "^1.1.2",
"web3": "^1.6.0"

View File

@ -48,7 +48,7 @@ p.row {
}
/*Login button*/
a.button {
.button {
display: flex;
flex-direction: row;
align-items: center;

View File

@ -2,6 +2,8 @@ import { Container } from './components/generic'
import { useState, useEffect } from 'react'
import { HashRouter} from 'react-router-dom'
import Router from './components/router'
import Theme from './components/atoms/Theme'
function App() {
@ -22,14 +24,19 @@ function App() {
// ///////////////////////////////
// Rendering
// ///////////////////////////////
if( error || loading ) return <Container>
if( error || loading ) return <Theme>
<Container>
<p>{ error || loading }</p>
</Container>
return <HashRouter>
</Theme>
return <Theme>
<HashRouter>
<Router />
</HashRouter>
</Theme>
}

View File

@ -38,7 +38,7 @@ export default function Verifier() {
const signature = await sign( JSON.stringify( {
signer: address.toLowerCase(),
outfitId: outfitId,
outfitId,
chainId,
} ), address )
@ -46,6 +46,48 @@ export default function Verifier() {
setLoading( 'Updating profile' )
const { error, success } = await callApi( `/rocketeer/${ rocketeerId }/outfits`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify( signature )
} )
if( error ) throw new Error( error )
alert( `Success! Outfit changed, please click "refresh metadata" on Opensea to update it there.\nForwarding you to the tools homepage.` )
navigate( `/` )
} catch( e ) {
log( e )
alert( e.message )
} finally {
setLoading( false )
}
}
async function generateNewOutfit( ) {
try {
log( `Generating new outfit for #${ rocketeerId }` )
setLoading( `Generating new outfit for #${ rocketeerId }` )
alert( 'You will be prompted to sign a message, this is NOT a transaction' )
const signature = await sign( JSON.stringify( {
signer: address.toLowerCase(),
rocketeerId,
chainId,
} ), address )
log( 'Making request with ', signature )
setLoading( 'Generating new outfit, this can take a minute' )
const { error, success } = await callApi( `/rocketeer/${ rocketeerId }/outfits`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@ -54,8 +96,9 @@ export default function Verifier() {
if( error ) throw new Error( error )
alert( `Success! Outfit changed, please click "refresh metadata" on Opensea to update it there.\nForwarding you to the tools homepage.` )
navigate( `/` )
alert( `Success! Outfit generated.` )
window?.location.reload()
} catch( e ) {
@ -84,8 +127,16 @@ export default function Verifier() {
// If the selected rocketeer is available, compute it's available outfits to an easy to access property
if( selected ) {
const { value: outfits } = selected.attributes.find( ( { trait_type } ) => trait_type === 'available outfits' ) || 0
const newOutfitAllowedInterval = 1000 * 60 * 60 * 24 * 30
const { value: outfits } = selected.attributes.find( ( { trait_type } ) => trait_type === 'available outfits' ) || { value: 0 }
const { value: last_outfit_change } = selected.attributes.find( ( { trait_type } ) => trait_type === 'last outfit change' ) || { value: 0 }
const timeUntilAllowedToChange = newOutfitAllowedInterval - ( Date.now() - last_outfit_change )
selected.outfits = outfits
selected.last_outfit_change = last_outfit_change
selected.new_outfit_available = timeUntilAllowedToChange < 0
selected.when_new_outfit = new Date( Date.now() + timeUntilAllowedToChange )
}
log( "Selecting rocketeer ", selected )
@ -93,7 +144,7 @@ export default function Verifier() {
// Set the selected rocketeer to state
if( selected ) setRocketeer( selected )
}, [ rocketeerId, rocketeers ] )
}, [ rocketeerId, rocketeers.length ] )
// ///////////////////////////////
// Rendering
@ -124,16 +175,18 @@ export default function Verifier() {
// Changing room
if( rocketeer ) return <Container id="avatar" className={ rocketeer.outfits > 0 ? 'wide' : '' }>
<h1>Changing room for Rocketeer { rocketeerId }</h1>
<h1>{ rocketeer.name }</h1>
<img key={ rocketeer.id } className='rocketeer' src={ rocketeer.image } alt={ `Rocketeer number ${ rocketeer.id }` } />
{ rocketeer.new_outfit_available ? <button onClick={ generateNewOutfit } className="button">Generate new outfit</button> : <p>New outfit available on { rocketeer.when_new_outfit.toString() }</p> }
<p>This Rocketeer has { 1 + rocketeer.outfits } outfits. { rocketeer.outfits > 0 && 'Click any outfit to select it as primary.' }</p>
<div className="row">
<img key={ rocketeer.id + 0 } onClick={ f => setPrimaryOutfit( 0 ) } className='rocketeer' src={ rocketeer.image } alt={ `Rocketeer number ${ rocketeer.id }` } />
<img key={ rocketeer.id + 0 } onClick={ f => setPrimaryOutfit( 0 ) } className='rocketeer' src={ rocketeer.image.replace( /-\d\.jpg/, '.jpg' ) } alt={ `Rocketeer number ${ rocketeer.id }` } />
{ Array.from( Array( rocketeer.outfits ) ).map( ( val, i ) => {
return <img onClick={ f => setPrimaryOutfit( i ) } key={ rocketeer.id + i } className='rocketeer' src={ rocketeer.image.replace( '.jpg', `-${ i + 1 }.jpg` ) } alt={ `Rocketeer number ${ rocketeer.id }` } />
return <img onClick={ f => setPrimaryOutfit( i + 1 ) } key={ rocketeer.id + i } className='rocketeer' src={ rocketeer.image.replace( /-\d\.jpg/, `-${ i + 1 }.jpg` ) } alt={ `Rocketeer number ${ rocketeer.id }` } />
} ) }
</div>

View File

@ -3,7 +3,7 @@ import Metamask from './metamask'
import Verifier from './verifier'
import Avatar from './avatar'
import Portfolio from './portfolio'
import Outfits from './outfits'
import Outfits from './organisms/Outfits'
import { useState, useEffect } from 'react'
import { log } from '../modules/helpers'
import { useAddress } from '../modules/web3'

View File

@ -14,7 +14,7 @@ export async function callApi( path, options={} ) {
const chain = ( isLocal || querySaysTestnet ) ? 'testnet' : 'mainnet'
const callPath = api[ chain ] + path
log( 'Calling ', callPath )
log( 'Calling ', callPath, ' with ', options )
return fetch( `${ api[ chain ] }${ path }`, options ).then( res => res.json() )
}

View File

@ -224,12 +224,13 @@ export function useTokenIds() {
const [ tokens, setTokens ] = useState( [] )
// Grab tokens from contract
useEffect( f => {
useEffect( ( ) => {
// Do nothing if there is no data yet
if( !contract || !balance || !address ) return
// Load initial supply value
let cancelled = false;
( async () => {
try {
@ -238,8 +239,9 @@ export function useTokenIds() {
const id = await contract.tokenOfOwnerByIndex( address, index )
return id.toString()
} ) )
log( 'Tokens detected: ', ids )
setTokens( ids )
if( !cancelled ) setTokens( ids )
} catch( e ) {
@ -249,9 +251,11 @@ export function useTokenIds() {
} )( )
return () => cancelled = true
}, [ contract, address, balance ] )
return tokens
}