-add name field validation
-disable fields after successful contribute
This commit is contained in:
Danil Kovtonyuk 2020-02-07 01:41:43 +10:00
parent aa8d72923c
commit 684a7e0a2b
4 changed files with 281 additions and 76 deletions

View File

@ -149,6 +149,7 @@
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch;
&:hover, &.is-hovered { &:hover, &.is-hovered {
color: $primary; color: $primary;
@ -166,19 +167,49 @@
} }
} }
&:focus {
outline: none;
}
.cloak { .cloak {
align-self: center; align-self: center;
} }
.buttons { .form {
flex-grow: 1; flex-grow: 1;
display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: stretch;
} }
.button.is-fullwidth + .button.is-fullwidth { .button.is-fullwidth + .button.is-fullwidth {
margin-top: 1rem; margin-top: 1rem;
} }
&-anonymous {
width: 100%;
cursor: pointer;
}
}
fieldset[disabled] {
.box {
opacity: .5;
cursor: not-allowed;
background-color: $primary-invert;
border-color: #393939;
.title {
color: $white;
}
.cloak {
path {
stroke: #393939;
}
}
}
} }
.field { .field {
@ -186,6 +217,19 @@
text-align: left; text-align: left;
} }
.help {
text-align: left;
position: absolute;
&.counter {
right: 0;
}
}
&:not(:last-child) {
margin-bottom: 1.5rem;
}
&.is-horizontal { &.is-horizontal {
display: flex; display: flex;
align-items: center; align-items: center;
@ -199,6 +243,16 @@
margin-right: 1.5rem; margin-right: 1.5rem;
} }
} }
.control.has-icons-right {
.input {
padding-right: calc(#{$control-padding-horizontal} - 1px);
}
.icon.is-right {
display: none;
}
}
} }
.currently { .currently {
@ -543,3 +597,9 @@
} }
} }
} }
fieldset {
&:not(:last-child) {
margin-bottom: $block-spacing;
}
}

58
components/Form.vue Normal file
View File

@ -0,0 +1,58 @@
<template>
<div class="form">
<div v-if="isLoggedIn" class="fields">
<b-field
:type="{ 'is-danger': hasErrorName.invalid }"
:message="{ [hasErrorName.msg]: hasErrorName.invalid }"
label="Name"
>
<b-input v-model="userName" maxlength="35"></b-input>
</b-field>
<b-field label="Company">
<b-input v-model="userCompany"></b-input>
</b-field>
</div>
<div v-else class="buttons">
<b-button @click="twitterLogIn" type="is-primary" outlined expanded>
Sign in with Twitter
</b-button>
<b-button :disabled="true" type="is-primary" outlined expanded>
Sign in with Github
</b-button>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
data() {
return {
nameErrorMessage: ''
}
},
computed: {
...mapGetters('user', ['isLoggedIn', 'hasErrorName']),
userName: {
get() {
return this.$store.state.user.name
},
set(value) {
this.$store.commit('user/SET_NAME', value)
}
},
userCompany: {
get() {
return this.$store.state.user.company
},
set(value) {
this.$store.commit('user/SET_COMPANY', value)
}
}
},
methods: {
...mapActions('user', ['makeTweet', 'twitterLogIn'])
}
}
</script>

View File

@ -1,44 +1,31 @@
<template> <template>
<div class="ceremony"> <div class="ceremony">
<h1 class="title is-size-1 is-spaced"> <h1 class="title is-size-1 is-spaced">
Hello, <span>@{{ user.handle }}</span> Hello, <span>@{{ userHandle }}</span>
</h1> </h1>
<h2 class="subtitle"> <h2 class="subtitle">
What way do you want to contribute to the Tornado.cash Trusted Setup Ceremony? What way do you want to contribute to the Tornado.cash Trusted Setup Ceremony?
</h2> </h2>
<div class="columns is-centered"> <fieldset :disabled="status.type === 'is-success'">
<div class="column is-one-third"> <div class="columns is-centered">
<div <div class="column is-one-third">
:class="{ 'is-hovered': contributionType === 'anonymous' }" <button
@click="onAnonymousHandler" :class="{ 'is-hovered': contributionType === 'anonymous' }"
class="box" @click="onAnonymousHandler"
> class="box box-anonymous"
<div class="title is-5">Anonymously</div> >
<Cloak /> <div class="title is-5">Anonymously</div>
<Cloak />
</button>
</div> </div>
</div> <div class="column is-one-third">
<div class="column is-one-third"> <div :class="{ 'is-hovered': isLoggedIn }" class="box">
<div :class="{ 'is-hovered': isLoggedIn }" class="box"> <div class="title is-5">Using a social account</div>
<div class="title is-5">Using a social account</div> <Form />
<div v-if="isLoggedIn" class="fields">
<b-field label="Name">
<b-input v-model="user.name"></b-input>
</b-field>
<b-field label="Company">
<b-input v-model="user.company"></b-input>
</b-field>
</div>
<div v-else class="buttons">
<b-button @click="logIn" type="is-primary" outlined expanded>
SignIn via Twitter
</b-button>
<b-button @click="logIn" :disabled="true" type="is-primary" outlined expanded>
SignIn via Github
</b-button>
</div> </div>
</div> </div>
</div> </div>
</div> </fieldset>
<div v-show="status.type === 'is-danger' || status.type === 'is-success'" class="status"> <div v-show="status.type === 'is-danger' || status.type === 'is-success'" class="status">
<div :class="status.type" class="status-message">{{ status.msg }}</div> <div :class="status.type" class="status-message">{{ status.msg }}</div>
@ -81,32 +68,66 @@
<script> <script>
/* eslint-disable no-console */ /* eslint-disable no-console */
import { mapGetters, mapActions } from 'vuex'
import Cloak from '@/components/Cloak' import Cloak from '@/components/Cloak'
import Form from '@/components/Form'
const timeout = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) const timeout = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
export default { export default {
components: { components: {
Cloak Cloak,
Form
}, },
data() { data() {
return { return {
contributionType: null,
contributionIndex: null,
isContributeBtnSnown: false, isContributeBtnSnown: false,
status: { status: {
type: '', type: '',
msg: '' msg: ''
}, },
user: { name: '', handle: 'Anonymous', company: '' },
loading: false loading: false
} }
}, },
computed: { computed: {
isLoggedIn() { ...mapGetters('user', ['isLoggedIn', 'hasErrorName']),
return !!this.user.name && this.user.name !== 'Anonymous' userName: {
get() {
return this.$store.state.user.name
},
set(value) {
this.$store.commit('user/SET_NAME', value)
}
},
userHandle: {
get() {
return this.$store.state.user.handle
},
set(value) {
this.$store.commit('user/SET_HANDLE', value)
}
},
userCompany: {
get() {
return this.$store.state.user.company
},
set(value) {
this.$store.commit('user/SET_COMPANY', value)
}
},
contributionType: {
get() {
return this.$store.state.user.contributionType
},
set(value) {
this.$store.commit('user/SET_CONTRIBUTION_TYPE', value)
}
}, },
isContributeBtnDisabled() { isContributeBtnDisabled() {
return !this.contributionType || (!this.isLoggedIn && this.contributionType !== 'anonymous') return (
!this.contributionType ||
(!this.isLoggedIn && this.contributionType !== 'anonymous') ||
this.hasErrorName.invalid
)
} }
}, },
async mounted() { async mounted() {
@ -115,8 +136,8 @@ export default {
const data = await response.json() const data = await response.json()
console.log('data', data) console.log('data', data)
if (data.name !== 'Anonymous') { if (data.name !== 'Anonymous') {
this.user.handle = data.handle this.userHandle = data.handle
this.user.name = data.name this.userName = data.name
// TODO check whether it's github or twitter // TODO check whether it's github or twitter
this.contributionType = 'twitter' this.contributionType = 'twitter'
} }
@ -125,33 +146,7 @@ export default {
} }
}, },
methods: { methods: {
makeTweet() { ...mapActions('user', ['makeTweet', 'logOut']),
const tweetText = `Just made the contribution %23${this.contributionIndex} to Tornado.cash Trusted Setup Ceremony! 🚀`
const popUpWindowWidth = 600
const popUpWindowHeight = 250
const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : window.screenX
const dualScreenTop = window.screenTop !== undefined ? window.screenTop : window.screenY
const width = window.innerWidth
? window.innerWidth
: document.documentElement.clientWidth
? document.documentElement.clientWidth
: screen.width
const height = window.innerHeight
? window.innerHeight
: document.documentElement.clientHeight
? document.documentElement.clientHeight
: screen.height
const systemZoom = width / window.screen.availWidth
const left = (width - popUpWindowWidth) / 2 / systemZoom + dualScreenLeft
const top = (height - popUpWindowHeight) / 2 / systemZoom + dualScreenTop
window.open(
`https://twitter.com/intent/tweet?text=${tweetText}`,
'',
`menubar=no,toolbar=no,resizable=yes,scrollbars=no,height=${popUpWindowHeight},width=${popUpWindowWidth},top=${top},left=${left}`
)
},
async makeContribution({ retry = 0 } = {}) { async makeContribution({ retry = 0 } = {}) {
try { try {
this.isContributeBtnSnown = true this.isContributeBtnSnown = true
@ -169,12 +164,12 @@ export default {
console.log('Updated params', result) console.log('Updated params', result)
this.status.msg = 'Uploading and verifying your contribution' this.status.msg = 'Uploading and verifying your contribution'
console.log('this.user.name', this.user) console.log('this.user.name', this.userName, this.userHandle, this.userCompany)
const formData = new FormData() const formData = new FormData()
formData.append('response', new Blob([result], { type: 'application/octet-stream' })) formData.append('response', new Blob([result], { type: 'application/octet-stream' }))
if (this.contributionType !== 'anonymous') { if (this.contributionType !== 'anonymous') {
formData.append('name', this.user.name) formData.append('name', this.userName)
formData.append('company', this.user.company) formData.append('company', this.userCompany)
} }
const resp = await fetch('api/response', { const resp = await fetch('api/response', {
method: 'POST', method: 'POST',
@ -184,7 +179,7 @@ export default {
this.status.msg = 'Your contribution is verified and recorded. Thank you.' this.status.msg = 'Your contribution is verified and recorded. Thank you.'
this.status.type = 'is-success' this.status.type = 'is-success'
const responseData = await resp.json() const responseData = await resp.json()
this.contributionIndex = responseData.contributionIndex this.$store.commit('user/SET_CONTRIBUTION_INDEX', responseData.contributionIndex)
} else if (resp.status === 422) { } else if (resp.status === 422) {
if (retry < 3) { if (retry < 3) {
console.log(`Looks like someone else uploaded contribution ahead of us, retrying`) console.log(`Looks like someone else uploaded contribution ahead of us, retrying`)
@ -208,13 +203,12 @@ export default {
this.loading = false this.loading = false
} }
}, },
logIn() {
this.contributionType = 'twitter'
window.location.replace('/api/connect')
},
onAnonymousHandler() { onAnonymousHandler() {
this.logOut()
this.contributionType = 'anonymous' this.contributionType = 'anonymous'
this.user = { name: '', handle: 'Anonymous', company: '' } this.userName = null
this.userHandle = 'Anonymous'
this.userCompany = ''
} }
} }
} }

93
store/user.js Normal file
View File

@ -0,0 +1,93 @@
const state = () => {
return {
name: null,
handle: 'Anonymous',
company: '',
contributionType: null,
contributionIndex: null
}
}
const mutations = {
SET_NAME(state, name) {
state.name = name
},
SET_HANDLE(state, handle) {
state.handle = handle
},
SET_COMPANY(state, company) {
state.company = company
},
SET_CONTRIBUTION_TYPE(state, contributionType) {
state.contributionType = contributionType
},
SET_CONTRIBUTION_INDEX(state, contributionIndex) {
state.contributionIndex = contributionIndex
}
}
const getters = {
isLoggedIn: (state) => {
return state.name !== null && state.name !== 'Anonymous'
},
hasErrorName: (state) => {
const name = state.name
if (name === null) {
return { invalid: false, msg: '' }
}
if (name === '') {
return { invalid: true, msg: 'Name is empty' }
}
if (name.length < 4) {
return { invalid: true, msg: 'Name is too short' }
}
if (name.length > 35) {
return { invalid: true, msg: 'Name is too long' }
}
return { invalid: false, msg: '' }
}
}
const actions = {
twitterLogIn() {
window.location.replace('/api/connect')
},
makeTweet({ state }) {
const tweetText = `Just made the contribution %23${state.contributionIndex} to Tornado.cash Trusted Setup Ceremony! 🚀`
const popUpWindowWidth = 600
const popUpWindowHeight = 250
const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : window.screenX
const dualScreenTop = window.screenTop !== undefined ? window.screenTop : window.screenY
const width = window.innerWidth
? window.innerWidth
: document.documentElement.clientWidth
? document.documentElement.clientWidth
: screen.width
const height = window.innerHeight
? window.innerHeight
: document.documentElement.clientHeight
? document.documentElement.clientHeight
: screen.height
const systemZoom = width / window.screen.availWidth
const left = (width - popUpWindowWidth) / 2 / systemZoom + dualScreenLeft
const top = (height - popUpWindowHeight) / 2 / systemZoom + dualScreenTop
window.open(
`https://twitter.com/intent/tweet?text=${tweetText}`,
'',
`menubar=no,toolbar=no,resizable=yes,scrollbars=no,height=${popUpWindowHeight},width=${popUpWindowWidth},top=${top},left=${left}`
)
},
async logOut() {
await fetch('/api/logout')
}
}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}