mirror of
https://github.com/Dannecron/ich-lerne-deutsch.git
synced 2025-12-25 12:52:35 +03:00
Add user words logic
This commit is contained in:
@@ -27,7 +27,7 @@
|
||||
<v-toolbar app dark class="primary">
|
||||
<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;">
|
||||
<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>
|
||||
<v-spacer></v-spacer>
|
||||
<v-toolbar-items class="hidden-sm-and-down">
|
||||
|
||||
@@ -2,19 +2,7 @@
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<div class="headline">
|
||||
<v-tooltip bottom>
|
||||
<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) }}
|
||||
<original-word :wordEntity="wordEntity"></original-word>
|
||||
</div>
|
||||
</v-card-title>
|
||||
|
||||
@@ -39,7 +27,7 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getFullOriginalWord, WORD_TYPES } from '@/utils';
|
||||
import OriginalWord from '@/components/Article/Word/OriginalWord';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -56,18 +44,11 @@ export default {
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters(['userData', 'getProcessing']),
|
||||
isWord() {
|
||||
return this.wordEntity.type === WORD_TYPES.WORD;
|
||||
},
|
||||
isRedewndung() {
|
||||
return this.wordEntity.type === WORD_TYPES.REDEWNNDUNG;
|
||||
},
|
||||
isProcessing() {
|
||||
return this.getProcessing;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getFullOriginalWord,
|
||||
addWord(entity) {
|
||||
const userWords = this.userData.words;
|
||||
const wordAdded = userWords[entity.key];
|
||||
@@ -86,5 +67,8 @@ export default {
|
||||
this.$store.dispatch('addUserWord', entity);
|
||||
},
|
||||
},
|
||||
components: {
|
||||
OriginalWord,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
87
src/components/Article/Word/OriginalWord.vue
Normal file
87
src/components/Article/Word/OriginalWord.vue
Normal 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>
|
||||
|
||||
@@ -163,12 +163,12 @@ export default {
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$bus.$on(EVENTS.USER.DATA_CHANGED, () => {
|
||||
this.$bus.$on(EVENTS.USER.PROFILE_CHANGED, () => {
|
||||
this.dialog = false;
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$bus.$off(EVENTS.USER.DATA_CHANGED);
|
||||
this.$bus.$off(EVENTS.USER.PROFILE_CHANGED);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
122
src/components/User/ProfileWords.vue
Normal file
122
src/components/User/ProfileWords.vue
Normal 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>
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
const formattedDate = (value) => {
|
||||
export const buildDate = (value) => {
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
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;
|
||||
1
src/filters/index.js
Normal file
1
src/filters/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from '@/filters/formattedDate';
|
||||
@@ -119,7 +119,7 @@ export default {
|
||||
});
|
||||
|
||||
await commit('setProcessing', false);
|
||||
await EventBus.notify(EVENTS.USER.DATA_CHANGED);
|
||||
await EventBus.notify(EVENTS.USER.PROFILE_CHANGED);
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import Vue from 'vue';
|
||||
import firebase from 'firebase/app';
|
||||
import 'firebase/firestore';
|
||||
|
||||
import { EventBus, EVENTS } from '@/utils';
|
||||
|
||||
const defaultUserData = {
|
||||
articles: {},
|
||||
@@ -30,6 +34,7 @@ export default {
|
||||
})
|
||||
.catch(e => window.console.error(e));
|
||||
|
||||
await EventBus.notify(EVENTS.USER.DATA_LOADED);
|
||||
await commit('setProcessing', false);
|
||||
},
|
||||
async addUserArticle({ commit, getters }, articleId) {
|
||||
@@ -108,6 +113,33 @@ export default {
|
||||
.then(() => commit('openUserArticlePart', { articleId, partId, timestamp }))
|
||||
.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: {
|
||||
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], 'rating', rating);
|
||||
},
|
||||
removeUserWord(state, wordKey) {
|
||||
Vue.delete(state.userData.words, wordKey);
|
||||
},
|
||||
updateUserWord(state, { word, wordKey }) {
|
||||
Vue.set(state.userData.words, wordKey, word);
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
userData: state => state.userData,
|
||||
|
||||
@@ -2,7 +2,9 @@ import Vue from 'vue';
|
||||
|
||||
export const EVENTS = {
|
||||
USER: {
|
||||
DATA_CHANGED: 'user-profile-data-changed',
|
||||
DATA_LOADED: 'user-data-loaded',
|
||||
PROFILE_CHANGED: 'user-profile-data-changed',
|
||||
WORD_UPDATED: 'user-data-word-updated',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<v-tab-item :key="'myArticles'">
|
||||
</v-tab-item>
|
||||
<v-tab-item :key="'myWords'">
|
||||
<user-profile-words></user-profile-words>
|
||||
</v-tab-item>
|
||||
</v-tabs>
|
||||
</v-flex>
|
||||
@@ -29,6 +30,7 @@
|
||||
|
||||
<script>
|
||||
import UserProfileData from '@/components/User/ProfileData';
|
||||
import UserProfileWords from '@/components/User/ProfileWords';
|
||||
|
||||
export default {
|
||||
beforeMount() {
|
||||
@@ -39,6 +41,7 @@ export default {
|
||||
}),
|
||||
components: {
|
||||
UserProfileData,
|
||||
UserProfileWords,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user