Add user words logic

This commit is contained in:
2019-03-17 14:45:29 +07:00
parent b611c49904
commit b7c00b352b
11 changed files with 271 additions and 29 deletions

View File

@@ -27,7 +27,7 @@
<v-toolbar app dark class="primary"> <v-toolbar app dark class="primary">
<v-toolbar-side-icon @click.stop="drawer = !drawer" class="hidden-md-and-up"></v-toolbar-side-icon> <v-toolbar-side-icon @click.stop="drawer = !drawer" class="hidden-md-and-up"></v-toolbar-side-icon>
<router-link to="/" tag="span" style="cursor: pointer;"> <router-link to="/" tag="span" style="cursor: pointer;">
<v-toolbar-title v-text="'Dannc Ich Lerne Deutsch'"></v-toolbar-title> <v-toolbar-title v-text="'Ich Lerne Deutsch'"></v-toolbar-title>
</router-link> </router-link>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-toolbar-items class="hidden-sm-and-down"> <v-toolbar-items class="hidden-sm-and-down">

View File

@@ -2,19 +2,7 @@
<v-card> <v-card>
<v-card-title> <v-card-title>
<div class="headline"> <div class="headline">
<v-tooltip bottom> <original-word :wordEntity="wordEntity"></original-word>
<v-avatar v-if="isWord" color="teal" size="45" slot="activator">
<span class="white--text">W</span>
</v-avatar>
<span>Слово / das word</span>
</v-tooltip>
<v-tooltip bottom>
<v-avatar v-if="isRedewndung" color="indigo" size="45" slot="activator">
<span class="white--text">RW</span>
</v-avatar>
<span>Выражение / die Redewendung</span>
</v-tooltip>
{{ getFullOriginalWord(wordEntity) }}
</div> </div>
</v-card-title> </v-card-title>
@@ -39,7 +27,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { getFullOriginalWord, WORD_TYPES } from '@/utils'; import OriginalWord from '@/components/Article/Word/OriginalWord';
export default { export default {
props: { props: {
@@ -56,18 +44,11 @@ export default {
}), }),
computed: { computed: {
...mapGetters(['userData', 'getProcessing']), ...mapGetters(['userData', 'getProcessing']),
isWord() {
return this.wordEntity.type === WORD_TYPES.WORD;
},
isRedewndung() {
return this.wordEntity.type === WORD_TYPES.REDEWNNDUNG;
},
isProcessing() { isProcessing() {
return this.getProcessing; return this.getProcessing;
}, },
}, },
methods: { methods: {
getFullOriginalWord,
addWord(entity) { addWord(entity) {
const userWords = this.userData.words; const userWords = this.userData.words;
const wordAdded = userWords[entity.key]; const wordAdded = userWords[entity.key];
@@ -86,5 +67,8 @@ export default {
this.$store.dispatch('addUserWord', entity); this.$store.dispatch('addUserWord', entity);
}, },
}, },
components: {
OriginalWord,
},
}; };
</script> </script>

View File

@@ -0,0 +1,87 @@
<template>
<div v-if="wordEntity">
<v-tooltip bottom>
<v-avatar v-if="isWord" :size="size" color="teal" slot="activator">
<span class="white--text">W</span>
</v-avatar>
<span>Слово / das word</span>
</v-tooltip>
<v-tooltip bottom>
<v-avatar v-if="isRedewndung" :size="size" color="indigo" slot="activator">
<span class="white--text">RW</span>
</v-avatar>
<span>Выражение / die Redewendung</span>
</v-tooltip>
{{ fullOriginalWord }}
<v-icon v-if="canPronounceWord && showAudio" @click="pronounceWord">music_note</v-icon>
</div>
</template>
<script>
import { getFullOriginalWord, WORD_TYPES } from '@/utils';
export default {
props: {
wordEntity: {
type: Object,
default: null,
},
size: {
type: Number,
default: 45,
},
showAudio: {
type: Boolean,
default: false,
},
},
data: () => ({
voice: null,
}),
computed: {
isWord() {
return this.wordEntity.type === WORD_TYPES.WORD;
},
isRedewndung() {
return this.wordEntity.type === WORD_TYPES.REDEWNNDUNG;
},
canPronounceWord() {
return !!this.voice;
},
fullOriginalWord() {
return this.getFullOriginalWord(this.wordEntity);
},
},
methods: {
getFullOriginalWord,
pronounceWord() {
if (!this.canPronounceWord) {
return;
}
const message = new SpeechSynthesisUtterance();
message.voice = this.voice;
message.rate = 1;
message.pitch = 1;
message.volume = 1;
message.text = this.fullOriginalWord;
speechSynthesis.speak(message);
},
},
created() {
if ('speechSynthesis' in window) {
const germanVoices = speechSynthesis.getVoices()
.filter(voice => voice.lang === 'de');
if (germanVoices.length) {
this.voice = germanVoices[0];
}
}
},
}
</script>

View File

@@ -163,12 +163,12 @@ export default {
}, },
}, },
created() { created() {
this.$bus.$on(EVENTS.USER.DATA_CHANGED, () => { this.$bus.$on(EVENTS.USER.PROFILE_CHANGED, () => {
this.dialog = false; this.dialog = false;
}); });
}, },
beforeDestroy() { beforeDestroy() {
this.$bus.$off(EVENTS.USER.DATA_CHANGED); this.$bus.$off(EVENTS.USER.PROFILE_CHANGED);
}, },
}; };
</script> </script>

View File

@@ -0,0 +1,122 @@
<template>
<div>
<v-card v-if="currentWord" class="mt-2" dark>
<v-card-title>
<div class="headline">
<original-word :wordEntity="currentWord" :showAudio="true"></original-word>
</div>
</v-card-title>
<v-divider></v-divider>
<v-card-text v-if="currentWord.showTranslation" class="headline">
{{ currentWord.transText }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
v-if="!currentWord.showTranslation"
@click="currentWord.showTranslation = true"
color="primary"
dark
small
>
<v-icon>visibility</v-icon> Показать перевод
</v-btn>
<v-btn
@click="learnWord"
color="success"
dark
small
>
<v-icon>check</v-icon> Изучено
</v-btn>
</v-card-actions>
</v-card>
<v-card>
<v-card-title v-if="words.length" class="display-1">
<span>Все слова на сегодня ({{ words.length }})</span>
</v-card-title>
<v-card-title v-else>
<span>Нет слов для изучения</span>
</v-card-title>
<v-card-text>
<v-list>
<div v-for="(word, index) in words" :key="index">
<v-list-tile @click="selectCurrentWord(word)">
<div class="title pa-1">
<original-word :wordEntity="word" :size="35"></original-word>
</div>
</v-list-tile>
</div>
</v-list>
</v-card-text>
</v-card>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import OriginalWord from '@/components/Article/Word/OriginalWord';
import { buildDate } from '@/filters';
import { EVENTS } from '@/utils';
export default {
data: () => ({
words: [],
currentWord: null,
}),
computed: {
...mapGetters(['userData']),
userWords() {
return this.userData.words;
}
},
methods: {
setWords() {
this.words = [];
const userWords = this.userWords;
for (let property in userWords) {
if (userWords.hasOwnProperty(property)) {
const word = userWords[property];
const nextShowDate = buildDate(word.nextShowDate);
const isWordAvailable = nextShowDate <= new Date();
if (isWordAvailable) {
this.words.push({ ...word, key: property, showTranslation: false });
}
}
}
this.currentWord = this.words.length > 0 ? this.words[0] : null;
},
learnWord() {
this.$store.dispatch('processUserLearnWord', this.currentWord.key);
},
selectCurrentWord(word) {
this.currentWord = { ...word, showTranslation: false };
},
},
mounted() {
this.setWords();
},
created() {
this.$bus.$on(EVENTS.USER.DATA_LOADED, () => {
this.setWords();
});
this.$bus.$on(EVENTS.USER.WORD_UPDATED, () => {
this.setWords();
});
},
beforeDestroy() {
this.$bus.$off(EVENTS.USER.DATA_LOADED);
this.$bus.$off(EVENTS.USER.WORD_UPDATED)
},
components: {
OriginalWord,
},
};
</script>

View File

@@ -1,12 +1,17 @@
const formattedDate = (value) => { export const buildDate = (value) => {
if (value === null) { if (value === null) {
return null; return null;
} }
if (value instanceof Date) { if (value instanceof Date) {
return value.toLocaleDateString(); return value;
} }
return value.toDate().toLocaleDateString(); return value.toDate();
}
const formattedDate = (value) => {
const buildDate = buildDate(value);
return buildDate ? buildDate.toLocaleDateString() : null;
}; };
export default formattedDate; export default formattedDate;

1
src/filters/index.js Normal file
View File

@@ -0,0 +1 @@
export * from '@/filters/formattedDate';

View File

@@ -119,7 +119,7 @@ export default {
}); });
await commit('setProcessing', false); await commit('setProcessing', false);
await EventBus.notify(EVENTS.USER.DATA_CHANGED); await EventBus.notify(EVENTS.USER.PROFILE_CHANGED);
}, },
}, },
getters: { getters: {

View File

@@ -1,4 +1,8 @@
import Vue from 'vue'; import Vue from 'vue';
import firebase from 'firebase/app';
import 'firebase/firestore';
import { EventBus, EVENTS } from '@/utils';
const defaultUserData = { const defaultUserData = {
articles: {}, articles: {},
@@ -30,6 +34,7 @@ export default {
}) })
.catch(e => window.console.error(e)); .catch(e => window.console.error(e));
await EventBus.notify(EVENTS.USER.DATA_LOADED);
await commit('setProcessing', false); await commit('setProcessing', false);
}, },
async addUserArticle({ commit, getters }, articleId) { async addUserArticle({ commit, getters }, articleId) {
@@ -108,6 +113,33 @@ export default {
.then(() => commit('openUserArticlePart', { articleId, partId, timestamp })) .then(() => commit('openUserArticlePart', { articleId, partId, timestamp }))
.catch(e => window.console.error(e)); .catch(e => window.console.error(e));
}, },
processUserLearnWord({ commit, getters }, wordKey) {
const word = getters.userData.words[wordKey];
const userDataRef = Vue.$db.collection('userData').doc(getters.userId);
if (word.bucket === 5) {
return userDataRef.update({
[`words.${wordKey}`]: firebase.firestore.FieldValue.delete(),
})
.then(() => commit('removeUserWord', wordKey))
.then(() => EventBus.notify(EVENTS.USER.WORD_UPDATED, { wordKey }));
}
let nextShowDate = new Date();
nextShowDate.setDate((new Date().getDate() + word.bucket * 2));
word.nextShowDate = nextShowDate;
word.bucket = word.bucket + 1;
userDataRef.set({
words: {
[wordKey]: word,
},
}, { merge: true })
.then(() => commit('updateUserWord', { word, wordKey }))
.then(() => EventBus.notify(EVENTS.USER.WORD_UPDATED, { wordKey }));
},
}, },
mutations: { mutations: {
setUserData(state, payload) { setUserData(state, payload) {
@@ -134,6 +166,12 @@ export default {
Vue.set(state.userData.articles[articleId].parts[partId], 'finishedAt', timestamp); Vue.set(state.userData.articles[articleId].parts[partId], 'finishedAt', timestamp);
Vue.set(state.userData.articles[articleId].parts[partId], 'rating', rating); Vue.set(state.userData.articles[articleId].parts[partId], 'rating', rating);
}, },
removeUserWord(state, wordKey) {
Vue.delete(state.userData.words, wordKey);
},
updateUserWord(state, { word, wordKey }) {
Vue.set(state.userData.words, wordKey, word);
},
}, },
getters: { getters: {
userData: state => state.userData, userData: state => state.userData,

View File

@@ -2,7 +2,9 @@ import Vue from 'vue';
export const EVENTS = { export const EVENTS = {
USER: { USER: {
DATA_CHANGED: 'user-profile-data-changed', DATA_LOADED: 'user-data-loaded',
PROFILE_CHANGED: 'user-profile-data-changed',
WORD_UPDATED: 'user-data-word-updated',
}, },
}; };

View File

@@ -19,6 +19,7 @@
<v-tab-item :key="'myArticles'"> <v-tab-item :key="'myArticles'">
</v-tab-item> </v-tab-item>
<v-tab-item :key="'myWords'"> <v-tab-item :key="'myWords'">
<user-profile-words></user-profile-words>
</v-tab-item> </v-tab-item>
</v-tabs> </v-tabs>
</v-flex> </v-flex>
@@ -29,6 +30,7 @@
<script> <script>
import UserProfileData from '@/components/User/ProfileData'; import UserProfileData from '@/components/User/ProfileData';
import UserProfileWords from '@/components/User/ProfileWords';
export default { export default {
beforeMount() { beforeMount() {
@@ -39,6 +41,7 @@ export default {
}), }),
components: { components: {
UserProfileData, UserProfileData,
UserProfileWords,
}, },
}; };
</script> </script>