mirror of
https://github.com/Dannecron/ich-lerne-deutsch.git
synced 2025-12-25 21:02:35 +03:00
Add user words logic
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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() {
|
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>
|
||||||
|
|||||||
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) {
|
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
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 commit('setProcessing', false);
|
||||||
await EventBus.notify(EVENTS.USER.DATA_CHANGED);
|
await EventBus.notify(EVENTS.USER.PROFILE_CHANGED);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user