This commit is contained in:
syuilo 2018-02-10 17:01:32 +09:00
parent 4f1795b97b
commit c869883d76
7 changed files with 217 additions and 257 deletions

View File

@ -13,7 +13,7 @@
</form> </form>
</template> </template>
<script lang="ts"> <script>
import Vue from 'vue'; import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({

View File

@ -1,9 +1,9 @@
<template> <template>
<form @submit.prevent="onSubmit" autocomplete="off"> <form class="form" @submit.prevent="onSubmit" autocomplete="off">
<label class="username"> <label class="username">
<p class="caption">%fa:at%%i18n:common.tags.mk-signup.username%</p> <p class="caption">%fa:at%%i18n:common.tags.mk-signup.username%</p>
<input v-model="username" type="text" pattern="^[a-zA-Z0-9-]{3,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required @keyup="onChangeUsername"/> <input v-model="username" type="text" pattern="^[a-zA-Z0-9-]{3,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required @keyup="onChangeUsername"/>
<p class="profile-page-url-preview" v-if="refs.username.value != '' && username-state != 'invalidFormat' && username-state != 'minRange' && username-state != 'maxRange'">{ _URL_ + '/' + refs.username.value }</p> <p class="profile-page-url-preview" v-if="username != '' && username-state != 'invalidFormat' && username-state != 'minRange' && username-state != 'maxRange'">{ _URL_ + '/' + refs.username.value }</p>
<p class="info" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%%i18n:common.tags.mk-signup.checking%</p> <p class="info" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%%i18n:common.tags.mk-signup.checking%</p>
<p class="info" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.available%</p> <p class="info" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.available%</p>
<p class="info" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.unavailable%</p> <p class="info" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.unavailable%</p>
@ -30,7 +30,7 @@
</label> </label>
<label class="recaptcha"> <label class="recaptcha">
<p class="caption"><template v-if="recaptchaed">%fa:toggle-on%</template><template v-if="!recaptchaed">%fa:toggle-off%</template>%i18n:common.tags.mk-signup.recaptcha%</p> <p class="caption"><template v-if="recaptchaed">%fa:toggle-on%</template><template v-if="!recaptchaed">%fa:toggle-off%</template>%i18n:common.tags.mk-signup.recaptcha%</p>
<div v-if="recaptcha" class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" data-sitekey="recaptcha.site_key"></div> <div v-if="recaptcha" class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" :data-sitekey="recaptchaSitekey"></div>
</label> </label>
<label class="agree-tou"> <label class="agree-tou">
<input name="agree-tou" type="checkbox" autocomplete="off" required/> <input name="agree-tou" type="checkbox" autocomplete="off" required/>
@ -43,16 +43,98 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
const getPasswordStrength = require('syuilo-password-strength'); const getPasswordStrength = require('syuilo-password-strength');
import import { docsUrl, lang, recaptchaSitekey } from '../../../config';
const aboutUrl = `${_DOCS_URL_}/${_LANG_}/tou`;
export default Vue.extend({ export default Vue.extend({
methods: { props: ['os'],
onSubmit() { data() {
return {
username: '',
password: '',
retypedPassword: '',
touUrl: `${docsUrl}/${lang}/tou`,
recaptchaSitekey,
recaptchaed: false,
usernameState: null,
passwordStrength: '',
passwordRetypeState: null
} }
}, },
methods: {
onChangeUsername() {
if (this.username == '') {
this.usernameState = null;
return;
}
const err =
!this.username.match(/^[a-zA-Z0-9\-]+$/) ? 'invalid-format' :
this.username.length < 3 ? 'min-range' :
this.username.length > 20 ? 'max-range' :
null;
if (err) {
this.usernameState = err;
return;
}
this.usernameState = 'wait';
this.os.api('username/available', {
username: this.username
}).then(result => {
this.usernameState = result.available ? 'ok' : 'unavailable';
}).catch(err => {
this.usernameState = 'error';
});
},
onChangePassword() {
if (this.password == '') {
this.passwordStrength = '';
return;
}
const strength = getPasswordStrength(this.password);
this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
(this.$refs.passwordMetar as any).style.width = `${strength * 100}%`;
},
onChangePasswordRetype() {
if (this.retypedPassword == '') {
this.passwordRetypeState = null;
return;
}
this.passwordRetypeState = this.password == this.retypedPassword ? 'match' : 'not-match';
},
onSubmit() {
this.os.api('signup', {
username: this.username,
password: this.password,
'g-recaptcha-response': (window as any).grecaptcha.getResponse()
}).then(() => {
this.os.api('signin', {
username: this.username,
password: this.password
}).then(() => {
location.href = '/';
});
}).catch(() => {
alert('%i18n:common.tags.mk-signup.some-error%');
(window as any).grecaptcha.reset();
this.recaptchaed = false;
});
}
},
created() {
(window as any).onRecaptchaed = () => {
this.recaptchaed = true;
};
(window as any).onRecaptchaExpired = () => {
this.recaptchaed = false;
};
},
mounted() { mounted() {
const head = document.getElementsByTagName('head')[0]; const head = document.getElementsByTagName('head')[0];
const script = document.createElement('script'); const script = document.createElement('script');
@ -63,269 +145,133 @@ export default Vue.extend({
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
:scope .form
min-width 302px
label
display block display block
min-width 302px margin 16px 0
overflow hidden
> form > .caption
margin 0 0 4px 0
color #828888
font-size 0.95em
label > [data-fa]
margin-right 0.25em
color #96adac
> .info
display block
margin 4px 0
font-size 0.8em
> [data-fa]
margin-right 0.3em
&.username
.profile-page-url-preview
display block display block
margin 16px 0 margin 4px 8px 0 4px
font-size 0.8em
color #888
> .caption &:empty
margin 0 0 4px 0 display none
color #828888
font-size 0.95em
> [data-fa] &:not(:empty) + .info
margin-right 0.25em margin-top 0
color #96adac
> .info &.password
.meter
display block
margin-top 8px
width 100%
height 8px
&[data-strength='']
display none
&[data-strength='low']
> .value
background #d73612
&[data-strength='medium']
> .value
background #d7ca12
&[data-strength='high']
> .value
background #61bb22
> .value
display block display block
margin 4px 0 width 0%
font-size 0.8em height 100%
background transparent
border-radius 4px
transition all 0.1s ease
> [data-fa] [type=text], [type=password]
margin-right 0.3em user-select text
display inline-block
cursor auto
padding 0 12px
margin 0
width 100%
line-height 44px
font-size 1em
color #333 !important
background #fff !important
outline none
border solid 1px rgba(0, 0, 0, 0.1)
border-radius 4px
box-shadow 0 0 0 114514px #fff inset
transition all .3s ease
&.username &:hover
.profile-page-url-preview border-color rgba(0, 0, 0, 0.2)
display block transition all .1s ease
margin 4px 8px 0 4px
font-size 0.8em
color #888
&:empty &:focus
display none color $theme-color !important
border-color $theme-color
box-shadow 0 0 0 1024px #fff inset, 0 0 0 4px rgba($theme-color, 10%)
transition all 0s ease
&:not(:empty) + .info &:disabled
margin-top 0 opacity 0.5
&.password .agree-tou
.meter padding 4px
display block border-radius 4px
margin-top 8px
width 100%
height 8px
&[data-strength=''] &:hover
display none background #f4f4f4
&[data-strength='low'] &:active
> .value background #eee
background #d73612
&[data-strength='medium'] &, *
> .value cursor pointer
background #d7ca12
&[data-strength='high'] p
> .value display inline
background #61bb22 color #555
> .value button
display block margin 0 0 32px 0
width 0% padding 16px
height 100% width 100%
background transparent font-size 1em
border-radius 4px color #fff
transition all 0.1s ease background $theme-color
border-radius 3px
[type=text], [type=password] &:hover
user-select text background lighten($theme-color, 5%)
display inline-block
cursor auto
padding 0 12px
margin 0
width 100%
line-height 44px
font-size 1em
color #333 !important
background #fff !important
outline none
border solid 1px rgba(0, 0, 0, 0.1)
border-radius 4px
box-shadow 0 0 0 114514px #fff inset
transition all .3s ease
&:hover &:active
border-color rgba(0, 0, 0, 0.2) background darken($theme-color, 5%)
transition all .1s ease
&:focus
color $theme-color !important
border-color $theme-color
box-shadow 0 0 0 1024px #fff inset, 0 0 0 4px rgba($theme-color, 10%)
transition all 0s ease
&:disabled
opacity 0.5
.agree-tou
padding 4px
border-radius 4px
&:hover
background #f4f4f4
&:active
background #eee
&, *
cursor pointer
p
display inline
color #555
button
margin 0 0 32px 0
padding 16px
width 100%
font-size 1em
color #fff
background $theme-color
border-radius 3px
&:hover
background lighten($theme-color, 5%)
&:active
background darken($theme-color, 5%)
</style> </style>
<script lang="typescript">
this.mixin('api');
this.usernameState = null;
this.passwordStrength = '';
this.passwordRetypeState = null;
this.recaptchaed = false;
this.aboutUrl = `${_DOCS_URL_}/${_LANG_}/tou`;
window.onRecaptchaed = () => {
this.recaptchaed = true;
this.update();
};
window.onRecaptchaExpired = () => {
this.recaptchaed = false;
this.update();
};
this.on('mount', () => {
this.update({
recaptcha: {
site_key: _RECAPTCHA_SITEKEY_
}
});
const head = document.getElementsByTagName('head')[0];
const script = document.createElement('script');
script.setAttribute('src', 'https://www.google.com/recaptcha/api.js');
head.appendChild(script);
});
this.onChangeUsername = () => {
const username = this.$refs.username.value;
if (username == '') {
this.update({
usernameState: null
});
return;
}
const err =
!username.match(/^[a-zA-Z0-9\-]+$/) ? 'invalid-format' :
username.length < 3 ? 'min-range' :
username.length > 20 ? 'max-range' :
null;
if (err) {
this.update({
usernameState: err
});
return;
}
this.update({
usernameState: 'wait'
});
this.api('username/available', {
username: username
}).then(result => {
this.update({
usernameState: result.available ? 'ok' : 'unavailable'
});
}).catch(err => {
this.update({
usernameState: 'error'
});
});
};
this.onChangePassword = () => {
const password = this.$refs.password.value;
if (password == '') {
this.passwordStrength = '';
return;
}
const strength = getPasswordStrength(password);
this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
this.update();
this.$refs.passwordMetar.style.width = `${strength * 100}%`;
};
this.onChangePasswordRetype = () => {
const password = this.$refs.password.value;
const retypedPassword = this.$refs.passwordRetype.value;
if (retypedPassword == '') {
this.passwordRetypeState = null;
return;
}
this.passwordRetypeState = password == retypedPassword ? 'match' : 'not-match';
};
this.onsubmit = e => {
e.preventDefault();
const username = this.$refs.username.value;
const password = this.$refs.password.value;
const locker = document.body.appendChild(document.createElement('mk-locker'));
this.api('signup', {
username: username,
password: password,
'g-recaptcha-response': grecaptcha.getResponse()
}).then(() => {
this.api('signin', {
username: username,
password: password
}).then(() => {
location.href = '/';
});
}).catch(() => {
alert('%i18n:common.tags.mk-signup.some-error%');
grecaptcha.reset();
this.recaptchaed = false;
locker.parentNode.removeChild(locker);
});
return false;
};
</script>

11
src/web/app/config.ts Normal file
View File

@ -0,0 +1,11 @@
declare const _HOST_: string;
declare const _URL_: string;
declare const _DOCS_URL_: string;
declare const _LANG_: string;
declare const _RECAPTCHA_SITEKEY_: string;
export const host = _HOST_;
export const url = _URL_;
export const docsUrl = _DOCS_URL_;
export const lang = _LANG_;
export const recaptchaSitekey = _RECAPTCHA_SITEKEY_;

View File

@ -18,7 +18,7 @@
</div> </div>
</footer> </footer>
<modal name="signup"> <modal name="signup">
<mk-signup/> <mk-signup></mk-signup>
</modal> </modal>
</div> </div>
</template> </template>

View File

@ -70,6 +70,9 @@ export default (callback: (os: MiOS, launch: () => Vue) => void, sw = false) =>
// アプリ基底要素マウント // アプリ基底要素マウント
document.body.innerHTML = '<div id="app"></div>'; document.body.innerHTML = '<div id="app"></div>';
// Register global components
require('./common/views/components');
const launch = () => { const launch = () => {
return new Vue({ return new Vue({
router: new VueRouter({ router: new VueRouter({

View File

@ -7,7 +7,7 @@ import { pattern, replacement } from '../../../src/common/build/fa';
export default () => ({ export default () => ({
enforce: 'pre', enforce: 'pre',
test: /\.(tag|js|ts)$/, test: /\.(vue|js|ts)$/,
exclude: /node_modules/, exclude: /node_modules/,
loader: StringReplacePlugin.replace({ loader: StringReplacePlugin.replace({
replacements: [{ replacements: [{

View File

@ -10,7 +10,7 @@ export default lang => {
return { return {
enforce: 'pre', enforce: 'pre',
test: /\.(tag|js|ts)$/, test: /\.(vue|js|ts)$/,
exclude: /node_modules/, exclude: /node_modules/,
loader: StringReplacePlugin.replace({ loader: StringReplacePlugin.replace({
replacements: [{ replacements: [{