Frontend prettified

This commit is contained in:
Mentor Palokaj 2021-11-29 14:25:54 +01:00
parent 82dca9cb88
commit 23fda2aee2
26 changed files with 287 additions and 365 deletions

View File

@ -17,7 +17,7 @@
"web3": "^1.6.0"
},
"scripts": {
"start": "react-scripts start",
"start": "DISABLE_ESLINT_PLUGIN=true react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"

View File

@ -1,4 +1,4 @@
import { Container } from './components/generic'
import Container from './components/atoms/Container'
import { useState, useEffect } from 'react'
import { HashRouter} from 'react-router-dom'
import Router from './components/router'
@ -24,16 +24,11 @@ function App() {
// ///////////////////////////////
// Rendering
// ///////////////////////////////
if( error || loading ) return <Theme>
<Container>
<p>{ error || loading }</p>
</Container>
</Theme>
return <Theme>
<HashRouter>
<Router />
{ error || loading ? <Container> <p>{ error || loading }</p> </Container> : <Router /> }
</HashRouter>
</Theme>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 2c5.52 0 10 4.48 10 10s-4.48 10-10 10S2 17.52 2 12 6.48 2 12 2zM6.023 15.416C7.491 17.606 9.695 19 12.16 19c2.464 0 4.669-1.393 6.136-3.584A8.968 8.968 0 0 0 12.16 13a8.968 8.968 0 0 0-6.137 2.416zM12 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></svg>

After

Width:  |  Height:  |  Size: 374 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-4.987-3.744A7.966 7.966 0 0 0 12 20c1.97 0 3.773-.712 5.167-1.892A6.979 6.979 0 0 0 12.16 16a6.981 6.981 0 0 0-5.147 2.256zM5.616 16.82A8.975 8.975 0 0 1 12.16 14a8.972 8.972 0 0 1 6.362 2.634 8 8 0 1 0-12.906.187zM12 13a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></svg>

After

Width:  |  Height:  |  Size: 495 B

View File

@ -0,0 +1,10 @@
<svg width="71" height="55" viewBox="0 0 71 55" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M60.1045 4.8978C55.5792 2.8214 50.7265 1.2916 45.6527 0.41542C45.5603 0.39851 45.468 0.440769 45.4204 0.525289C44.7963 1.6353 44.105 3.0834 43.6209 4.2216C38.1637 3.4046 32.7345 3.4046 27.3892 4.2216C26.905 3.0581 26.1886 1.6353 25.5617 0.525289C25.5141 0.443589 25.4218 0.40133 25.3294 0.41542C20.2584 1.2888 15.4057 2.8186 10.8776 4.8978C10.8384 4.9147 10.8048 4.9429 10.7825 4.9795C1.57795 18.7309 -0.943561 32.1443 0.293408 45.3914C0.299005 45.4562 0.335386 45.5182 0.385761 45.5576C6.45866 50.0174 12.3413 52.7249 18.1147 54.5195C18.2071 54.5477 18.305 54.5139 18.3638 54.4378C19.7295 52.5728 20.9469 50.6063 21.9907 48.5383C22.0523 48.4172 21.9935 48.2735 21.8676 48.2256C19.9366 47.4931 18.0979 46.6 16.3292 45.5858C16.1893 45.5041 16.1781 45.304 16.3068 45.2082C16.679 44.9293 17.0513 44.6391 17.4067 44.3461C17.471 44.2926 17.5606 44.2813 17.6362 44.3151C29.2558 49.6202 41.8354 49.6202 53.3179 44.3151C53.3935 44.2785 53.4831 44.2898 53.5502 44.3433C53.9057 44.6363 54.2779 44.9293 54.6529 45.2082C54.7816 45.304 54.7732 45.5041 54.6333 45.5858C52.8646 46.6197 51.0259 47.4931 49.0921 48.2228C48.9662 48.2707 48.9102 48.4172 48.9718 48.5383C50.038 50.6034 51.2554 52.5699 52.5959 54.435C52.6519 54.5139 52.7526 54.5477 52.845 54.5195C58.6464 52.7249 64.529 50.0174 70.6019 45.5576C70.6551 45.5182 70.6887 45.459 70.6943 45.3942C72.1747 30.0791 68.2147 16.7757 60.1968 4.9823C60.1772 4.9429 60.1437 4.9147 60.1045 4.8978ZM23.7259 37.3253C20.2276 37.3253 17.3451 34.1136 17.3451 30.1693C17.3451 26.225 20.1717 23.0133 23.7259 23.0133C27.308 23.0133 30.1626 26.2532 30.1066 30.1693C30.1066 34.1136 27.28 37.3253 23.7259 37.3253ZM47.3178 37.3253C43.8196 37.3253 40.9371 34.1136 40.9371 30.1693C40.9371 26.225 43.7636 23.0133 47.3178 23.0133C50.9 23.0133 53.7545 26.2532 53.6986 30.1693C53.6986 34.1136 50.9 37.3253 47.3178 37.3253Z" fill="#23272A"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="71" height="55" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0H24V24H0z"/><path d="M3 21v-2h2V4c0-.552.448-1 1-1h12c.552 0 1 .448 1 1v15h2v2H3zm12-10h-2v2h2v-2z"/></svg>

After

Width:  |  Height:  |  Size: 215 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M9.33 11.5h2.17A4.5 4.5 0 0 1 16 16H8.999L9 17h8v-1a5.578 5.578 0 0 0-.886-3H19a5 5 0 0 1 4.516 2.851C21.151 18.972 17.322 21 13 21c-2.761 0-5.1-.59-7-1.625L6 10.071A6.967 6.967 0 0 1 9.33 11.5zM5 19a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v9zM18 5a3 3 0 1 1 0 6 3 3 0 0 1 0-6zm-7-3a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></svg>

After

Width:  |  Height:  |  Size: 471 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M19 21H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9a1 1 0 0 1-1 1zM6 19h12V9.157l-6-5.454-6 5.454V19z"/></svg>

After

Width:  |  Height:  |  Size: 257 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 2.05V13h10.95c-.501 5.053-4.765 9-9.95 9-5.523 0-10-4.477-10-10 0-5.185 3.947-9.449 9-9.95zm2 0A10.003 10.003 0 0 1 21.95 11H13V2.05z"/></svg>

After

Width:  |  Height:  |  Size: 275 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5.33 15.929A13.064 13.064 0 0 1 5 13c0-5.088 2.903-9.436 7-11.182C16.097 3.564 19 7.912 19 13c0 1.01-.114 1.991-.33 2.929l2.02 1.796a.5.5 0 0 1 .097.63l-2.458 4.096a.5.5 0 0 1-.782.096l-2.254-2.254a1 1 0 0 0-.707-.293H9.414a1 1 0 0 0-.707.293l-2.254 2.254a.5.5 0 0 1-.782-.096l-2.458-4.095a.5.5 0 0 1 .097-.631l2.02-1.796zM12 13a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -1,18 +1,34 @@
import { Link } from 'react-router-dom'
import styled from 'styled-components'
const DynamicButton = ( { to, ...props } ) => to ? <link { ...props } to={ to } /> : <button { ...props } />
const DynamicButton = ( { to='', onClick, ...props } ) => to && !to.includes( 'http' ) ? <Link { ...props } to={ to } /> : <button onClick={ onClick || ( () => window.open( to, '_blank' ).focus() ) } { ...props } />
export default styled( DynamicButton )`
const PrettyButton = styled( DynamicButton )`
display: flex;
flex-direction: row;
flex-direction: ${ ( { direction='row' } ) => direction };
align-items: center;
justify-content: center;
border: 1px solid rgba( 0, 0, 0, .3 );
color: rgba( 0, 0, 0, .8 );
border: 1px solid ${ ( { theme } ) => theme.colors.text };
color: ${ ( { theme } ) => theme.colors.text };
text-decoration: none;
font-size: 1.5rem;
padding: .5rem 1.1rem .5rem 1rem;
margin-top: 1rem;
margin: 1rem .5rem;
&:hover {
box-shadow: 0 0 20px 2px rgb(0 0 0 / 20%);
}
& img {
height: 50px;
width: auto;
margin: ${ ( { direction='row' } ) => direction == 'row' ? '0 1rem 0 0' : '1rem' };
}
`
export default ( { icon, ...props } ) => !icon ? <PrettyButton { ...props } /> : <PrettyButton { ...props }>
<img alt="Button icon" src={ icon } />
{ props.children }
</PrettyButton>

View File

@ -1,7 +1,8 @@
import styled from 'styled-components'
import Home from '../../assets/home-2-line.svg'
import { useNavigate } from 'react-router-dom'
// Image that behaves like a background image
import LaunchBackground from '../../assets/undraw_launch_day_4e04.svg'
const BackgroundImage = styled.img.attrs( props => ( {
// src: LaunchBackground
} ) )`
@ -21,7 +22,7 @@ const Wrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: ${ ( { justify='flex-start' } ) => justify };
justify-content: ${ ( { justify='center' } ) => justify };
min-height: 100vh;
width: 100%;
padding: ${ ( { gutter=true } ) => gutter ? '0 max( 1rem, calc( 25vw - 4rem ) )' : 'none' };
@ -30,10 +31,27 @@ const Wrapper = styled.div`
& * {
box-sizing: border-box;
}
& #home {
position: fixed;
top: 0;
right: 0;
padding: 1rem;
width: 70px;
height: 70px;
opacity: .8;
}
`
// Container that always has the background image
export default ( { children, ...props } ) => <Wrapper { ...props }>
<BackgroundImage key='background' />
{ children }
</Wrapper>
export default ( { children, ...props } ) => {
const navigate = useNavigate()
return <Wrapper { ...props }>
<img id="home" onClick={ f => navigate( '/' ) } src={ Home } />
<BackgroundImage key='background' />
{ children }
</Wrapper>
}

View File

@ -2,7 +2,7 @@ import styled from 'styled-components'
export default styled.section`
position: relative;
padding: ${ ( { gutter=false } ) => gutter ? '5rem max( 1rem, calc( 25vw - 4rem ) )' : '5rem 0' };
padding: ${ ( { topGutter=true, gutter=false } ) => gutter ? `${ topGutter ? '5rem' : '0' } max( 1rem, calc( 25vw - 4rem ) )` : `${ topGutter ? '5rem' : '0' } 0` };
display: flex;
flex-direction: ${ ( { direction } ) => direction || 'column' };
width: ${ ( { width } ) => width || '100%' };

View File

@ -9,6 +9,8 @@ export const Text = styled.p`
padding: ${ ( { banner } ) => banner ? '.5rem 1rem' : 'initial' };
box-shadow: ${ ( { banner } ) => banner ? '0 0 20px 2px rgb(0 0 0 / 70%)' : '' };
text-align: ${ ( { align } ) => align || 'left' };
max-width: 90%;
overflow-wrap: anywhere;
`
export const H1 = styled.h1`

View File

@ -12,11 +12,12 @@ export const Container = ( { className, children, ...props } ) => <main { ...pro
</main>
export const Loading = ( { message } ) => <Container>
export const Loading = ( { message, children } ) => <Container>
<div className="loading">
<div className="lds-dual-ring"></div>
<p>{ message }</p>
{ children }
</div>
</Container>

View File

@ -0,0 +1,58 @@
import styled from 'styled-components'
import { useRef } from 'react'
const Input = styled.div`
display: flex;
flex-direction: ${ ( { type } ) => type == 'radio' ? 'row' : 'column' };
margin: 1rem 0;
justify-content: center;
width: 350px;
& input {
background: ${ ( { theme } ) => theme.colors.backdrop };
border: none;
border-left: 2px solid ${ ( { theme } ) => theme.colors.primary };
}
& input {
padding: 1rem 2rem 1rem 1rem;
width: ${ ( { type } ) => type == 'radio' ? 'auto' : '100%' };
}
& label {
opacity: .5;
font-style: italic;
margin-bottom: .5rem;
display: flex;
width: ${ ( { type } ) => type == 'radio' ? 'auto' : '100%' };
color: ${ ( { theme } ) => theme.colors.text };
span {
display: flex;
align-items: center;
justify-content: center;
font-size: .9rem;
margin-left: auto;
font-style: normal;
background: ${ ( { theme } ) => theme.colors.hint };
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
}
}
`
export default ( { onChange, type, label, info, id, ...props } ) => {
const { current: internalId } = useRef( id || `input-${ Math.random() }` )
return <Input type={ type }>
{ label && <label htmlFor={ internalId }>{ label } { info && <span onClick={ f => alert( info ) }>?</span> }</label> }
<input data-testid={ internalId } { ...props } id={ internalId } onChange={ onChange } type={ type || 'text' } />
</Input>
}

View File

@ -32,9 +32,10 @@ const Spinner = styled.div`
`
export default ( { message, ...props } ) => <Container justify="center" { ...props }>
export default ( { message, children, ...props } ) => <Container justify="center" { ...props }>
<Spinner />
{ message && <Text align="center">{ message }</Text> }
{ children }
</Container>

View File

@ -1,10 +1,14 @@
import { Container, Loading } from './generic'
import '../App.css'
import Container from '../atoms/Container'
import Section from '../atoms/Section'
import { H1, Text } from '../atoms/Text'
import Avatar from '../molecules/Avatar'
import Input from '../molecules/Input'
import Loading from '../molecules/Loading'
import { useState, useEffect } from 'react'
import { log } from '../modules/helpers'
import { useRocketeerImages, callApi } from '../modules/api'
import { useAddress, useChainId, useBalanceOf, sign } from '../modules/web3'
import { log } from '../../modules/helpers'
import { useRocketeerImages, callApi } from '../../modules/api'
import { useAddress, useChainId, sign } from '../../modules/web3'
export default function Verifier() {
@ -27,6 +31,9 @@ export default function Verifier() {
try {
// Validate iput as eth1 address
if( !validatorAddress.match( /0x[a-zA-Z0-9]{40}/ ) ) throw new Error( `Please input a valid ETH1 address.` )
const confirmed = window.confirm( `This will assign Rocketeer ${ id } to ${ validatorAddress }.\n\nMetamask will ask you to sign a message, this is NOT A TRANSACTION.` )
if( !confirmed ) throw new Error( `Operation cancelled` )
@ -75,36 +82,30 @@ export default function Verifier() {
// Rendering
// ///////////////////////////////
if( loading ) return <Loading message={ loading } />
return <Container id="avatar" className={ rocketeers.length > 1 ? 'wide' : '' }>
return <Container>
<h1>Rocketeer avatar attribution</h1>
<H1>Rocketeer avatar attribution</H1>
<p>Input the address you want to assign the avatar to.</p>
<input type='text' onChange={ ( { target } ) => setValidatorAddress( target.value ) } value={ validatorAddress } />
<Text>Input the address you want to assign the avatar to.</Text>
<Input type='text' onChange={ ( { target } ) => setValidatorAddress( target.value ) } value={ validatorAddress } />
<p>Select the network you want to assign for:</p>
<div className="radios">
<div className="row">
<input onClick={ f => setNetwork( 'mainnet' ) } id="mainnet" type="radio" name="network" checked={ network == 'mainnet' }/>
<label onClick={ f => setNetwork( 'mainnet' ) } for="mainnet">Mainnet</label>
</div>
<div className="row">
<input onClick={ f => setNetwork( 'testnet' ) } id="testnet" type="radio" name="network" checked={ network == 'testnet' }/>
<label onClick={ f => setNetwork( 'testnet' ) } for="testnet">Testnet</label>
</div>
</div>
<Text>Select the network you want to assign for:</Text>
<Section topGutter={ false } direction="column">
<Input label="Mainnet" onClick={ f => setNetwork( 'mainnet' ) } id="mainnet" type="radio" name="network" checked={ network == 'mainnet' }/>
<Input label="Testnet" onClick={ f => setNetwork( 'testnet' ) } id="testnet" type="radio" name="network" checked={ network == 'testnet' }/>
</Section>
<p>Click the Rocketeer you want to assign to this address.</p>
<div className="row">
<Text>Click the Rocketeer you want to assign to this address.</Text>
<Section direction="row">
{ rocketeers.map( ( { id, src } ) => {
return <img key={ id } onClick={ f => attribute( id ) } className='rocketeer' src={ src } alt={ `Rocketeer number ${ id }` } />
return <Avatar key={ id } onClick={ f => attribute( id ) } src={ src } alt={ `Rocketeer number ${ id }` } />
} ) }
</div>
</Section>
</Container>
}

View File

@ -1,9 +1,24 @@
import Fox from '../assets/metamask-fox.svg'
import { Container, Loading } from './generic'
// Icons
import Fox from '../../assets/metamask-fox.svg'
import Discord from '../../assets/discord-logo-black.svg'
import Mint from '../../assets/rocket-fill.svg'
import Avatar from '../../assets/account-circle-fill.svg'
import Outfits from '../../assets/door-closed-fill.svg'
import Portfolio from '../../assets/pie-chart-fill.svg'
// Functionality
import { useState, useEffect } from 'react'
import { log } from '../modules/helpers'
import { useAddress, getAddress } from '../modules/web3'
import { Link } from 'react-router-dom'
import { log } from '../../modules/helpers'
import { useAddress, getAddress } from '../../modules/web3'
// Visual
import Container from '../atoms/Container'
import { H1 } from '../atoms/Text'
import Button from '../atoms/Button'
import Section from '../atoms/Section'
import Loading from '../molecules/Loading'
// ///////////////////////////////
@ -54,25 +69,20 @@ export default function ComponentName( ) {
// ///////////////////////////////
// Loading component
if( loading ) return <Loading message={ loading } />
// Error interface
if( error ) return <Container>
<p>{ error }</p>
</Container>
if( loading || error ) return <Loading message={ loading || error } />
// Actions menu
if( address ) return <Container>
<h1>Rocketeer Tools</h1>
<H1>Rocketeer NFT Tools</H1>
<div>
<Link className='button' to='/mint'>Mint Rocketeer</Link>
<Link className='button' to='/portfolio'>View Rocketeer Portfolio</Link>
<Link className='button' to='/outfits'>Use Changing Room</Link>
<Link className='button' to='/verify'>Discord verify</Link>
<Link className='button' to='/avatar'>Set address avatar</Link>
</div>
<Section direction="row">
<Button direction="column" icon={ Mint } to='/mint'>Mint Rocketeer</Button>
<Button direction="column" icon={ Portfolio } to='/portfolio'>Rocketeer Portfolio</Button>
<Button direction="column" icon={ Outfits } to='/outfits'>Changing Room</Button>
<Button direction="column" icon={ Discord } to='/verify'>Discord verify</Button>
<Button direction="column" icon={ Avatar } to='/avatar'>Set node avatar</Button>
</Section>
</Container>
@ -80,11 +90,10 @@ export default function ComponentName( ) {
// Login interface
return <Container>
<h1>Rocketeer Interface</h1>
<a className="button" href="/#" onClick={ metamasklogin }>
<img alt="metamask fox" src={ Fox } />
<H1>Rocketeer Interface</H1>
<Button icon={ Fox } onClick={ metamasklogin }>
Connect wallet
</a>
</Button>
</Container>

View File

@ -1,11 +1,15 @@
import Fox from '../assets/metamask-fox.svg'
import { Container } from './generic'
import '../App.css'
import Fox from '../../assets/metamask-fox.svg'
import Container from '../atoms/Container'
import { H1, Text } from '../atoms/Text'
import Button from '../atoms/Button'
import Input from '../molecules/Input'
import Loading from '../molecules/Loading'
import { useState, useEffect } from 'react'
import { useAddress, useTotalSupply, useContract, useChainId } from '../modules/web3'
import { log, setListenerAndReturnUnlistener } from '../modules/helpers'
import { useAddress, useTotalSupply, useContract, useChainId } from '../../modules/web3'
import { log, setListenerAndReturnUnlistener } from '../../modules/helpers'
export default function Minter() {
@ -78,21 +82,14 @@ export default function Minter() {
// Rendering
// ///////////////////////////////
if( error || loading ) return <Container>
{ error && <p>{ error }</p> }
{ !error && loading && <div className="loading">
<div className="lds-dual-ring"></div>
<p>{ loading }</p>
{ txHash && <a className="button" rel='noreferrer' target="_blank" href={ `https://${ chainId === '0x1' ? 'etherscan' : 'rinkeby.etherscan' }.io/tx/${ txHash }` }>View tx on Etherscan</a> }
</div> }
</Container>
if( error || loading ) return <Loading message={ error || loading }>
{ txHash && <Button to={ `https://${ chainId === '0x1' ? 'etherscan' : 'rinkeby.etherscan' }.io/tx/${ txHash }` }>View tx on Etherscan</Button> }
</Loading>
if( mintedTokenId ) return <Container>
<h1>Minting Successful!</h1>
<a className="button" rel="noreferrer" target="_blank" alt="Link to opensea details of Rocketeer" href='/#/portfolio'>View your Rocketeers</a>
<H1>Minting Successful!</H1>
<Button to='/portfolio'>View your Rocketeers</Button>
</Container>
@ -100,15 +97,15 @@ export default function Minter() {
return (
<Container>
<h1>Rocketeer Minter</h1>
<p>We are ready to mint! There are currently { totalSupply } minted Rocketeers.</p>
<H1>Rocketeer Minter</H1>
<Text>We are ready to mint! There are currently { totalSupply } minted Rocketeers.</Text>
<label htmlFor='address'>Minting to:</label>
<input id='address' value={ address } disabled />
{ contract && <a className="button" href="/#" onClick={ mintRocketeer }>
<img alt="metamask fox" src={ Fox } />
<Input label="Minting to:" id="address" value={ address } info="This is the currently selected address in your Metamask" disabled />
{ contract && <Button icon={ Fox } onClick={ mintRocketeer }>
Mint new Rocketeer
</a> }
</Button> }
</Container>

View File

@ -1,4 +1,3 @@
import { useState, useEffect } from 'react'
import { useRocketeers, callApi } from '../../modules/api'
import { useChainId, useAddress, sign } from '../../modules/web3'
@ -164,7 +163,7 @@ export default function Verifier() {
if( !rocketeers.length || loading ) return <Loading message={ loading || "Loading Rocketeers, please make sure you selected the right wallet" } />
// Rocketeer selector
if(!rocketeer ) return <Container>
if(!rocketeer ) return <Container justify="flex-start">
<H1>Rocketeers</H1>
<Text>Click on a Rocketeer to manage it's outfits</Text>
@ -184,7 +183,7 @@ export default function Verifier() {
</Container>
// Changing room
if( rocketeer ) return <Container gutter={ false }>
if( rocketeer ) return <Container justify="flex-start" gutter={ false }>
{ /* Header */ }
<Hero background={ rocketeer.image } gutter={ true } shadow={ true }>

View File

@ -0,0 +1,52 @@
import Container from '../atoms/Container'
import Section from '../atoms/Section'
import { H1, Text } from '../atoms/Text'
import Avatar from '../molecules/Avatar'
import Loading from '../molecules/Loading'
import { useState, useEffect } from 'react'
import { useRocketeerImages } from '../../modules/api'
import { useAddress } from '../../modules/web3'
export default function Verifier() {
// ///////////////////////////////
// State management
// ///////////////////////////////
const address = useAddress()
const metamaskAddress = useAddress()
const [ validatorAddress, setValidatorAddress ] = useState( )
const rocketeers = useRocketeerImages()
// ///////////////////////////////
// Lifecycle
// ///////////////////////////////
useEffect( f => {
if( !validatorAddress && metamaskAddress ) setValidatorAddress( metamaskAddress )
}, [ metamaskAddress, validatorAddress ] )
// ///////////////////////////////
// Rendering
// ///////////////////////////////
if( !address && !rocketeers.length ) return <Loading message="Loading your Rocketeers, make sure you're connected to the right wallet address" />
return <Container>
<H1>Portfolio</H1>
<Text>Click a Rocketeer to view it's details.</Text>
<Section direction="row">
{ rocketeers.map( ( { id, src } ) => {
return <Avatar onClick={ f => window.location.href =`https://viewer.rocketeer.fans/?rocketeer=${ id }` } key={ id } src={ src } alt={ `Rocketeer number ${ id }` } />
} ) }
<Text>Rocketeers owned by: { address }.</Text>
</Section>
</Container>
}

View File

@ -1,9 +1,11 @@
import { Container } from './generic'
import '../App.css'
import Container from '../atoms/Container'
import { H1, Text } from '../atoms/Text'
import Button from '../atoms/Button'
import Input from '../molecules/Input'
import { useState, useEffect } from 'react'
import { log } from '../modules/helpers'
import { useAddress, useChainId, useBalanceOf } from '../modules/web3'
import { log } from '../../modules/helpers'
import { useAddress, useChainId, useBalanceOf } from '../../modules/web3'
import { useParams } from 'react-router-dom'
export default function Verifier() {
@ -83,29 +85,29 @@ export default function Verifier() {
// ///////////////////////////////
log('Rendering with ', message, verifyUrl )
if( message ) return <Container>
{ message.balance > 0 && <p> { message.username } has { message.balance } Rocketeers on chain { chainId }</p> }
{ message.balance < 1 && <p>🛑 Computer says no</p> }
{ error && <p>Something went wrong, contact #support in Discord</p> }
{ message.balance > 0 && <Text> { message.username } has { message.balance } Rocketeers on chain { chainId }</Text> }
{ message.balance < 1 && <Text>🛑 Computer says no</Text> }
{ error && <Text>Something went wrong, contact #support in Discord</Text> }
</Container>
if( verifyUrl ) return <Container>
{ !balance && <p>Checking your on-chain balance...</p> }
{ !balance && <Text>Checking your on-chain balance...</Text> }
{ balance && <>
<h1>Verification URL</h1>
<p>Post this in the Discord channel #get-verified:</p>
<p>{ verifyUrl }</p>
<H1>Verification URL</H1>
<Text align="center">Post this in the Discord channel #get-verified:</Text>
<Text align="center">{ verifyUrl }</Text>
</> }
</Container>
return <Container>
<h1>Verify your hodlr status</h1>
<p>Verify your Rocketeer status by logging in with your wallet. This does NOT trigger a transaction. Therefore it is free.</p>
<input onChange={ e => setUsername( e.target.value ) } type="text" placeholder="Your Discord username" />
<a onClick={ showVerificationUrl } href="/#" className="button">Verify</a>
<H1 align="center">Verify your hodlr status</H1>
<Text align="center">Verify your Rocketeer status by logging in with your wallet. This does NOT trigger a transaction. Therefore it is free.</Text>
<Input onChange={ e => setUsername( e.target.value ) } type="text" placeholder="Your Discord username"/>
<Button onClick={ showVerificationUrl }>Verify</Button>
</Container>
}

View File

@ -1,198 +0,0 @@
import { Container, Loading } from './generic'
import '../App.css'
import { useState, useEffect } from 'react'
import { useRocketeers, callApi } from '../modules/api'
import { useChainId, useAddress, sign } from '../modules/web3'
import { log } from '../modules/helpers'
import { useParams, useNavigate } from 'react-router'
export default function Verifier() {
const { rocketeerId } = useParams()
const navigate = useNavigate()
// ///////////////////////////////
// State management
// ///////////////////////////////
const address = useAddress()
const metamaskAddress = useAddress()
const [ validatorAddress, setValidatorAddress ] = useState( )
const rocketeers = useRocketeers( rocketeerId )
const chainId = useChainId()
const [ rocketeer, setRocketeer ] = useState( )
const [ loading, setLoading ] = useState( )
/* ///////////////////////////////
// Functions
// /////////////////////////////*/
async function setPrimaryOutfit( outfitId ) {
try {
log( `Setting outfit ${ outfitId } for Rocketeer #${ rocketeerId }` )
setLoading( `Setting outfit ${ outfitId } for Rocketeer #${ rocketeerId }` )
alert( 'You will be prompted to sign a message, this is NOT a transaction' )
const signature = await sign( JSON.stringify( {
signer: address.toLowerCase(),
outfitId,
chainId,
} ), address )
log( 'Making request with ', signature )
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' },
body: JSON.stringify( signature )
} )
if( error ) throw new Error( error )
alert( `Success! Outfit generated.` )
window?.location.reload()
} catch( e ) {
log( e )
alert( e.message )
} finally {
setLoading( false )
}
}
// ///////////////////////////////
// Lifecycle
// ///////////////////////////////
useEffect( f => {
if( !validatorAddress && metamaskAddress ) setValidatorAddress( metamaskAddress )
}, [ metamaskAddress, validatorAddress ] )
useEffect( f => {
// Find the data for the clicked Rocketeer
const selected = rocketeers.find( ( { id } ) => id === rocketeerId )
// If the selected rocketeer is available, compute it's available outfits to an easy to access property
if( selected ) {
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 )
// Set the selected rocketeer to state
if( selected ) setRocketeer( selected )
}, [ rocketeerId, rocketeers.length ] )
// ///////////////////////////////
// Rendering
// ///////////////////////////////
if( !rocketeers.length || loading ) return <Loading message={ loading || "Loading Rocketeers, please make sure you selected the right wallet" } />
// Rocketeer selector
if(!rocketeer ) return <Container id="avatar" className={ rocketeers.length > 1 ? 'wide' : '' }>
<h1>Rocketeers</h1>
<p>Click on a Rocketeer to manage it's outfits</p>
<div className="row">
{ rocketeers.map( ( { id, image } ) => {
return <img id={ `rocketeer-${ id }` } onClick={ f => navigate( `/outfits/${ id }` ) } key={ id } className='rocketeer' src={ image } alt={ `Rocketeer number ${ id }` } />
} ) }
<p className="row">Rocketeers owned by: { address }.</p>
</div>
</Container>
// Changing room
if( rocketeer ) return <Container id="avatar" className={ rocketeer.outfits > 0 ? 'wide' : '' }>
<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.replace( /-\d\.jpg/, '.jpg' ) } alt={ `Rocketeer number ${ rocketeer.id }` } />
{ Array.from( Array( rocketeer.outfits ) ).map( ( val, i ) => {
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>
<p className="row">Rocketeers owned by: { address }.</p>
</Container>
}

View File

@ -1,48 +0,0 @@
import { Container } from './generic'
import '../App.css'
import { useState, useEffect } from 'react'
import { useRocketeerImages } from '../modules/api'
import { useAddress } from '../modules/web3'
export default function Verifier() {
// ///////////////////////////////
// State management
// ///////////////////////////////
const address = useAddress()
const metamaskAddress = useAddress()
const [ validatorAddress, setValidatorAddress ] = useState( )
const rocketeers = useRocketeerImages()
// ///////////////////////////////
// Lifecycle
// ///////////////////////////////
useEffect( f => {
if( !validatorAddress && metamaskAddress ) setValidatorAddress( metamaskAddress )
}, [ metamaskAddress, validatorAddress ] )
// ///////////////////////////////
// Rendering
// ///////////////////////////////
return <Container id="avatar" className={ rocketeers.length > 1 ? 'wide' : '' }>
<h1>Portfolio</h1>
<p>Click a Rocketeer to view it's details.</p>
<div className="row">
{ rocketeers.map( ( { id, src } ) => {
return <img onClick={ f => window.location.href =`https://viewer.rocketeer.fans/?rocketeer=${ id }` } key={ id } className='rocketeer' src={ src } alt={ `Rocketeer number ${ id }` } />
} ) }
<p className="row">Rocketeers owned by: { address }.</p>
</div>
</Container>
}

View File

@ -1,8 +1,8 @@
import Minter from './minter'
import Metamask from './metamask'
import Verifier from './verifier'
import Avatar from './avatar'
import Portfolio from './portfolio'
import Minter from './organisms/Minter'
import Home from './organisms/Home'
import Verifier from './organisms/Verifier'
import Avatar from './organisms/Avatar'
import Portfolio from './organisms/Portfolio'
import Outfits from './organisms/Outfits'
import { useState, useEffect } from 'react'
import { log } from '../modules/helpers'
@ -46,7 +46,7 @@ function Router() {
// ///////////////////////////////
return <Routes>
<Route exact path='/' element={ <Metamask /> } />
<Route exact path='/' element={ <Home /> } />
<Route exact path='/mint' element={ <Minter /> } />
<Route path='/verify/' element={ <Verifier /> }>
<Route path='/verify/:verificationCode' element={ <Verifier /> } />