Skip to content

Commit

Permalink
Merge pull request #29 from copa-ch/feat/create-tournament
Browse files Browse the repository at this point in the history
feat(home): add create tournament form
  • Loading branch information
Gery Hirschfeld authored Mar 7, 2020
2 parents f72f1a4 + b1c1831 commit a31f161
Show file tree
Hide file tree
Showing 18 changed files with 723 additions and 106 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"name": "copa-frontend",
"version": "1.11.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
Expand All @@ -15,6 +14,7 @@
"@fortawesome/free-solid-svg-icons": "^5.11.2",
"@fortawesome/vue-fontawesome": "^0.1.7",
"@types/qs": "^6.5.3",
"@types/validator": "^12.0.1",
"@vue/composition-api": "^0.4.0",
"axios": "^0.19.0",
"buefy": "^0.8.5",
Expand All @@ -26,6 +26,7 @@
"qs": "^6.9.0",
"register-service-worker": "^1.6.2",
"v-clipboard": "^2.2.2",
"validator": "^12.2.0",
"vue": "^2.6.10",
"vue-class-component": "^7.1.0",
"vue-i18n": "^8.15.0",
Expand Down
139 changes: 132 additions & 7 deletions src/app/components/CreateTournamentForm.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,147 @@
<template>
<section class="create-tournament-form">
<h1 class="title is-1 has-text-black">Create Form TODO</h1>
<h1 class="subtitle is-2 has-text-black">Create Form TODO</h1>
<section class="create-tournament-form has-text-left">
<h1 class="title is-2 has-text-black">{{ $t('createTournament.title') }}</h1>
<h2 class="subtitle is-4 has-text-black">{{ $t('createTournament.subtitle') }}</h2>

<transition name="fade">
<b-notification
v-if="hasError"
type="is-danger"
aria-close-label="Close notification"
role="alert">
{{ $t('createTournament.submitFailed') }}
</b-notification>
</transition>

<TextInput icon="trophy"
label="createTournament.name.label"
placeholder="createTournament.name.placeholder"
:state="nameState"
:message="nameMessage"
:loading="isPending"
v-model="name"/>

<TextInput icon="user"
label="createTournament.owner.label"
placeholder="createTournament.owner.placeholder"
:state="ownerState"
:message="ownerMessage"
:loading="isPending"
v-model="owner"/>

<TextInput icon="envelope"
label="createTournament.email.label"
placeholder="createTournament.email.placeholder"
:state="emailState"
:message="emailMessage"
:loading="isPending"
v-model="email"/>

<br>

<b-button
class="create-tournament-submit-button"
type="is-primary"
:disabled="isPending"
:loading="isPending"
@click="submit">
{{ $t('createTournament.submitButton') }}
</b-button>

</section>
</template>

<script lang="ts">
import {defineComponent} from '@vue/composition-api'
import {computed, defineComponent, ref, watch, Ref} from '@vue/composition-api'
import TextInput from '@/app/components/form/TextInput.vue'
import {useApiTournamentCreate} from '@/app/effects/tournament.api.effect'
import isEmail from 'validator/lib/isEmail'
import isEmpty from 'validator/lib/isEmpty'
import Spinner from '@/app/components/layout/Spinner.vue'
export default defineComponent({
setup() {
components: {
TextInput, Spinner,
},
setup(props, {root}) {
const isPristine = ref(true)
const {name, nameState, nameMessage, isNameInvalid} = useNameInput(isPristine)
const {owner, ownerState, ownerMessage, isOwnerInvalid} = useOwnerInput(isPristine)
const {email, emailState, emailMessage, isEmailInvalid} = useEmailInput(isPristine)
const {isPending, hasError, createTournament, createdTournament} = useApiTournamentCreate()
const submit = async () => {
isPristine.value = false
if (!isNameInvalid.value && !isOwnerInvalid.value && !isEmailInvalid.value) {
await createTournament({
name: name.value,
owner: owner.value,
email: email.value,
})
if (!hasError) {
await root.$router.push({
name: 'admin-teams',
params: {id: createdTournament.value.adminId},
})
}
}
}
return {}
return {
isPending, submit, hasError,
name, nameState, nameMessage,
owner, ownerState, ownerMessage,
email, emailState, emailMessage,
}
},
})
function useNameInput(isPristine: Ref<boolean>) {
const name = ref('')
const isNameInvalid = ref(false)
const isNamePristine = ref(true)
watch(name, () => isNameInvalid.value = isEmpty(name.value))
return {
nameState: computed(() => isPristine.value ? '' : isNameInvalid.value ? 'is-danger' : 'is-success'),
nameMessage: computed(() => isNameInvalid.value && !isPristine.value ? 'createTournament.name.message' : ''),
name, isNameInvalid, isNamePristine,
}
}
function useOwnerInput(isPristine: Ref<boolean>) {
const owner = ref('')
const isOwnerInvalid = ref(false)
const isOwnerPristine = ref(true)
watch(owner, () => isOwnerInvalid.value = isEmpty(owner.value))
return {
ownerState: computed(() => isPristine.value ? '' : isOwnerInvalid.value ? 'is-danger' : 'is-success'),
ownerMessage: computed(() => isOwnerInvalid.value && !isPristine.value ? 'createTournament.owner.message' : ''),
owner, isOwnerInvalid, isOwnerPristine,
}
}
function useEmailInput(isPristine: Ref<boolean>) {
const email = ref('')
const isEmailInvalid = ref(false)
const isEmailPristine = ref(true)
watch(email, () => isEmailInvalid.value = !isEmail(email.value))
return {
emailState: computed(() => isPristine.value ? '' : isEmailInvalid.value ? 'is-danger' : 'is-success'),
emailMessage: computed(() => isEmailInvalid.value && !isPristine.value ? 'createTournament.email.message' : ''),
email, isEmailInvalid, isEmailPristine,
}
}
</script>

<style lang="scss">
@import '../../styles/variables.scss';
@import 'src/styles/utilities/all';
@include mobile() {
button.create-tournament-submit-button {
display: flex;
width: 100%;
}
}
</style>
153 changes: 153 additions & 0 deletions src/app/components/form/TextInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<template>
<div class="field copa-field" :class="loading ? 'is-loading':''">
<label class="label">{{ $t(label) }}</label>
<p :class="`control ${hasIconLeft ? 'has-icons-left':''} has-icons-right`">
<input class="input"
:class="state"
:type="type"
:disabled="disabled || loading"
:readonly="loading"
:maxlength="maxlength"
:placeholder="$t(placeholder)"
:value="modelValue"
@input="onInput"/>
<b-icon
v-if="icon"
class="is-left"
:icon="icon"
:pack="'fas'"/>
<transition name="slide">
<b-icon
v-if="hasIconRight && !loading"
class="is-right"
:class="iconRightClass"
:icon="iconRight"
:pack="'fas'"/>
</transition>
</p>
<transition name="slide">
<p v-if="message" class="help" :class="iconRightClass">{{ $t(message) }}</p>
</transition>
</div>
</template>

<script lang="ts">
import {computed, defineComponent, ref, watch} from '@vue/composition-api'
export default defineComponent({
props: {
value: [Number, String],
label: String,
type: {
type: String,
default: 'text',
},
state: String,
message: String,
icon: String,
placeholder: String,
maxlength: Number,
loading: Boolean,
disabled: Boolean,
},
setup(props, {emit}) {
const modelValue = ref(props.value)
watch(() => props.value, (changedValue: any) => modelValue.value = changedValue)
return {
modelValue,
onInput: (event: any) => {
modelValue.value = event.target.value
emit('input', modelValue.value)
},
hasIconLeft: computed(() => !!props.icon),
hasIconRight: computed(() => !!props.state || !!props.loading),
iconRight: computed(() => {
if (props.loading) {
return ''
}
if (props.state) {
if (props.state === 'is-success') {
return 'check'
} else if (props.state === 'is-danger') {
return 'exclamation'
} else if (props.state === 'is-warning') {
return 'exclamation-triangle'
}
}
return ''
}),
iconRightClass: computed(() => {
if (props.state) {
return `has-text-${props.state.substring(3)}`
}
return ''
}),
}
},
})
</script>

<style lang="scss">
@import 'src/styles/utilities/all';
.copa-field {
position: relative;
min-height: 105px;
&:not(:last-child) {
margin-bottom: 0.25rem;
}
input {
border-left: none;
border-right: none;
border-top: none;
border-bottom-width: 2px;
border-radius: 4px 4px 0 0;
outline: none !important;
box-shadow: none !important;
}
&.is-loading input {
background: linear-gradient(-45deg, $white-ter 40%, $white, $white-ter 60%);
background-size: 300% 350%;
animation: loading 2s infinite linear;
animation-delay: 0s;
border-color: $grey-lightest !important;
}
&:not(.is-loading) input:disabled {
background: $white-bis;
}
&.is-loading .is-right {
animation: rotating 2s linear infinite;
color: $purple;
}
.control {
&::after {
content: "";
position: absolute;
bottom: 0;
height: 0;
width: 0;
left: 50%;
transition: left 0.3s, width 0.3s, height 0.3s;
background: linear-gradient(45deg, $purple, $turquoise);
}
}
&:focus-within {
.control::after {
height: 2px;
left: 0;
width: 100%;
}
}
}
</style>
4 changes: 2 additions & 2 deletions src/app/components/layout/Footer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<script lang="ts">
import {defineComponent} from '@vue/composition-api'
import {useAppConfig} from '@/app/effects/config.effect'
import {useApiInformation} from '@/app/effects/api-information.effect'
import {useApiInformation} from '@/app/effects/meta.api.effect'
export default defineComponent({
setup() {
Expand All @@ -36,7 +36,7 @@
</script>

<style lang="scss">
@import '../../../styles/variables.scss';
@import 'src/styles/utilities/variables';
footer.footer {
position: relative;
Expand Down
Loading

0 comments on commit a31f161

Please sign in to comment.