mirror of
https://github.com/Dannecron/ich-lerne-deutsch.git
synced 2025-12-25 12:52:35 +03:00
Add user data logic. Some refactoring.
User can add articles and parts, it stores in firestore.
This commit is contained in:
@@ -45,7 +45,17 @@
|
|||||||
<v-btn v-if="!expandDetails" class="primary" flat :to="{ name: 'article', params: { articleId: article.id } }">
|
<v-btn v-if="!expandDetails" class="primary" flat :to="{ name: 'article', params: { articleId: article.id } }">
|
||||||
Открыть
|
Открыть
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn v-if="expandDetails" class="primary" flat>Добавить</v-btn>
|
<v-btn v-if="expandDetails && canAddArticle(article.id)"
|
||||||
|
class="primary"
|
||||||
|
flat
|
||||||
|
@click="addArticle(article.id)"
|
||||||
|
>
|
||||||
|
Добавить
|
||||||
|
</v-btn>
|
||||||
|
<div v-if="getUserDataArticle(article.id)" class="ml-2">
|
||||||
|
<v-icon color="white">work_outline</v-icon>
|
||||||
|
Добавлено {{ getArticleAddedAt(article.id) | formattedDate }}
|
||||||
|
</div>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-flex>
|
</v-flex>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
@@ -54,6 +64,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
import YoutubeButton from '@/components/Article/YoutubeButton';
|
import YoutubeButton from '@/components/Article/YoutubeButton';
|
||||||
import { getArticleLevel, declOfNum } from '@/helpers';
|
import { getArticleLevel, declOfNum } from '@/helpers';
|
||||||
|
|
||||||
@@ -74,9 +85,24 @@
|
|||||||
const partsCountWord = declOfNum(partsCount, ['часть', 'части', 'частей']);
|
const partsCountWord = declOfNum(partsCount, ['часть', 'части', 'частей']);
|
||||||
return `${partsCount} ${partsCountWord}`;
|
return `${partsCount} ${partsCountWord}`;
|
||||||
},
|
},
|
||||||
|
...mapGetters(['isUserAuthentificated', 'getProcessing', 'userData']),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getArticleLevel: getArticleLevel,
|
getArticleLevel,
|
||||||
|
getUserDataArticle(articleId) {
|
||||||
|
return this.userData.articles[articleId];
|
||||||
|
},
|
||||||
|
canAddArticle(articleId) {
|
||||||
|
const article = this.getUserDataArticle(articleId);
|
||||||
|
return this.isUserAuthentificated && !this.getProcessing && !article;
|
||||||
|
},
|
||||||
|
addArticle(articleId) {
|
||||||
|
this.$store.dispatch('addUserArticle', articleId);
|
||||||
|
},
|
||||||
|
getArticleAddedAt(articleId) {
|
||||||
|
const article = this.getUserDataArticle(articleId);
|
||||||
|
return article.addedAt;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
YoutubeButton,
|
YoutubeButton,
|
||||||
|
|||||||
@@ -6,7 +6,13 @@
|
|||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn flat class="primary" :to="{
|
<div v-if="finishedAt" class="mr-2">
|
||||||
|
<v-icon dark>check</v-icon> Завершено {{ finishedAt | formattedDate }}
|
||||||
|
</div>
|
||||||
|
<v-btn v-if="isUserArticleAdded"
|
||||||
|
flat
|
||||||
|
class="primary"
|
||||||
|
:to="{
|
||||||
name: 'articlePart',
|
name: 'articlePart',
|
||||||
params: { articleId: this.articleId, partId: part.id },
|
params: { articleId: this.articleId, partId: part.id },
|
||||||
}"
|
}"
|
||||||
@@ -19,6 +25,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
part: {
|
part: {
|
||||||
@@ -30,6 +38,25 @@
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['isUserAuthentificated', 'getProcessing', 'userData']),
|
||||||
|
currentUserArticle() {
|
||||||
|
return this.userData.articles[this.articleId];
|
||||||
|
},
|
||||||
|
currentUserArticlePart() {
|
||||||
|
const article = this.currentUserArticle;
|
||||||
|
return article ? article.parts[this.part.id] : null;
|
||||||
|
},
|
||||||
|
isUserArticleAdded() {
|
||||||
|
return this.isUserAuthentificated && !this.getProcessing && !!this.currentUserArticle;
|
||||||
|
},
|
||||||
|
finishedAt() {
|
||||||
|
const { currentUserArticlePart } = this;
|
||||||
|
return currentUserArticlePart ? currentUserArticlePart.finishedAt : null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Vuex from 'vuex';
|
|||||||
import articleModule from '@/store/articles';
|
import articleModule from '@/store/articles';
|
||||||
import generalModule from '@/store/general';
|
import generalModule from '@/store/general';
|
||||||
import userModule from '@/store/user';
|
import userModule from '@/store/user';
|
||||||
|
import userDataModule from '@/store/userData';
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
||||||
@@ -12,5 +13,6 @@ export default new Vuex.Store({
|
|||||||
articleModule,
|
articleModule,
|
||||||
generalModule,
|
generalModule,
|
||||||
userModule,
|
userModule,
|
||||||
|
userDataModule,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,9 +54,10 @@ export default {
|
|||||||
signOut() {
|
signOut() {
|
||||||
firebase.auth().signOut();
|
firebase.auth().signOut();
|
||||||
},
|
},
|
||||||
stateChanged({ commit }, payload) {
|
stateChanged({ commit, dispatch }, payload) {
|
||||||
if (payload) {
|
if (payload) {
|
||||||
commit('setUser', payload.uid);
|
commit('setUser', payload.uid);
|
||||||
|
dispatch('loadUserData', payload.uid);
|
||||||
} else {
|
} else {
|
||||||
commit('unSetUser');
|
commit('unSetUser');
|
||||||
}
|
}
|
||||||
@@ -64,5 +65,6 @@ export default {
|
|||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
isUserAuthentificated: state => state.user.isAuthentificated,
|
isUserAuthentificated: state => state.user.isAuthentificated,
|
||||||
|
userId: state => state.user.uid,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
111
src/store/userData.js
Normal file
111
src/store/userData.js
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
const defaultUserData = {
|
||||||
|
articles: {},
|
||||||
|
words: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
state: {
|
||||||
|
userData: defaultUserData,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
loadUserData({ commit }, userId) {
|
||||||
|
commit('setProcessing', true);
|
||||||
|
|
||||||
|
let userDataRef = Vue.$db.collection('userData').doc(userId);
|
||||||
|
userDataRef.get()
|
||||||
|
.then((data) => {
|
||||||
|
let userData = data.exists ? data.data() : defaultUserData;
|
||||||
|
|
||||||
|
if (!userData.articles) {
|
||||||
|
userData.articles = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
commit('setUserData', userData);
|
||||||
|
})
|
||||||
|
.catch(e => window.console.error(e));
|
||||||
|
|
||||||
|
commit('setProcessing', false);
|
||||||
|
},
|
||||||
|
addUserArticle({ commit, getters }, articleId) {
|
||||||
|
commit('setProcessing', true);
|
||||||
|
|
||||||
|
const userDataRef = Vue.$db.collection('userData').doc(getters.userId);
|
||||||
|
const article = {
|
||||||
|
addedAt: new Date(),
|
||||||
|
parts: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
userDataRef.set({
|
||||||
|
articles: {
|
||||||
|
[articleId]: article,
|
||||||
|
}
|
||||||
|
}, { merge: true })
|
||||||
|
.then(() => commit('addUserArticle', { articleId, article }))
|
||||||
|
.catch(e => window.console.error(e));
|
||||||
|
|
||||||
|
commit('setProcessing', false);
|
||||||
|
},
|
||||||
|
updateUserArticlePartStats({ commit, getters }, { articleId, partId }) {
|
||||||
|
const userDataRef = Vue.$db.collection('userData').doc(getters.userId);
|
||||||
|
const timestamp = new Date();
|
||||||
|
const articles = getters.userData.articles;
|
||||||
|
|
||||||
|
if (!articles.hasOwnProperty(articleId) || !articles[articleId].parts.hasOwnProperty(partId)) {
|
||||||
|
userDataRef.update({
|
||||||
|
[`articles.${articleId}.parts.${partId}.addedAt`]: timestamp,
|
||||||
|
})
|
||||||
|
.then(() => commit('addUserArticlePart', { articleId, partId, timestamp }))
|
||||||
|
.catch(e => window.console.error(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
userDataRef.update({
|
||||||
|
[`articles.${articleId}.parts.${partId}.lastOpenedAt`]: timestamp,
|
||||||
|
})
|
||||||
|
.then(() => commit('openUserArticlePart', { articleId, partId, timestamp }))
|
||||||
|
.catch(e => window.console.error(e));
|
||||||
|
},
|
||||||
|
finishUserArticlePart({ commit, getters }, { articleId, partId, rating }) {
|
||||||
|
commit('setProcessing', true);
|
||||||
|
|
||||||
|
const userDataRef = Vue.$db.collection('userData').doc(getters.userId);
|
||||||
|
const timestamp = new Date();
|
||||||
|
|
||||||
|
userDataRef.update({
|
||||||
|
[`articles.${articleId}.parts.${partId}.finishedAt`]: timestamp,
|
||||||
|
[`articles.${articleId}.parts.${partId}.rating`]: rating,
|
||||||
|
})
|
||||||
|
.then(() => commit('finishUserArticlePart', { articleId, partId, timestamp, rating }))
|
||||||
|
.catch(e => window.console.error(e));
|
||||||
|
|
||||||
|
commit('setProcessing', false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
setUserData(state, payload) {
|
||||||
|
Vue.set(state, 'userData', payload);
|
||||||
|
},
|
||||||
|
addUserArticle(state, { articleId, article }) {
|
||||||
|
Vue.set(state.userData.articles, articleId, article);
|
||||||
|
},
|
||||||
|
addUserArticlePart(state, { articleId, partId, timestamp }) {
|
||||||
|
if (state.userData.articles[articleId].parts[partId]) {
|
||||||
|
Vue.set(state.userData.articles[articleId].parts[partId], 'addedAt', timestamp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.set(state.userData.articles[articleId].parts, partId, { addedAt: timestamp });
|
||||||
|
},
|
||||||
|
openUserArticlePart(state, { articleId, partId, timestamp }) {
|
||||||
|
Vue.set(state.userData.articles[articleId].parts[partId], 'lastOpenedAt', timestamp);
|
||||||
|
},
|
||||||
|
finishUserArticlePart(state, { articleId, partId, timestamp, rating }) {
|
||||||
|
Vue.set(state.userData.articles[articleId].parts[partId], 'finishedAt', timestamp);
|
||||||
|
Vue.set(state.userData.articles[articleId].parts[partId], 'rating', rating);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
userData: state => state.userData,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,12 +1,58 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container grid-list-md v-if="part">
|
<v-container grid-list-md v-if="part">
|
||||||
<v-layout row wrap>
|
<v-layout row wrap>
|
||||||
|
<v-flex v-if="finishedAt" xs12 sm10 offset-sm1>
|
||||||
|
<v-card>
|
||||||
|
<v-img src="https://img.icons8.com/nolan/64/000000/inspection.png"
|
||||||
|
max-width="100px"
|
||||||
|
>
|
||||||
|
</v-img>
|
||||||
|
<v-card-title primary-title>
|
||||||
|
<div class="headline">Чтение этой части завершено {{ finishedAt | formattedDate }}</div>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-actions>
|
||||||
|
<span>Оценка</span>
|
||||||
|
<v-rating v-model="storedRating" color="success" half-increments readonly large></v-rating>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-flex>
|
||||||
<v-flex xs12 sm10 offset-sm1>
|
<v-flex xs12 sm10 offset-sm1>
|
||||||
<book-part-content :part="part"></book-part-content>
|
<book-part-content :part="part"></book-part-content>
|
||||||
</v-flex>
|
</v-flex>
|
||||||
|
|
||||||
<v-flex xs12 sm10 offset-sm1>
|
<v-flex xs12 sm10 offset-sm1>
|
||||||
<book-part-words :words="part.words"></book-part-words>
|
<book-part-words :words="part.words"></book-part-words>
|
||||||
</v-flex>
|
</v-flex>
|
||||||
|
|
||||||
|
<v-flex xs12 sm10 offset-sm1 class="text-xs-center">
|
||||||
|
<v-dialog v-model="finishDialog" persistent max-width="600px">
|
||||||
|
<v-btn v-if="!finishedAt" slot="activator" color="success" dark @click.stop="finishDialog = true">
|
||||||
|
<v-icon>check</v-icon> Завершить
|
||||||
|
</v-btn>
|
||||||
|
<v-card>
|
||||||
|
<v-img src="https://img.icons8.com/nolan/64/000000/inspection.png"
|
||||||
|
max-width="100px"
|
||||||
|
>
|
||||||
|
</v-img>
|
||||||
|
<v-card-title primary-title>
|
||||||
|
<div class="headline">Чтение этой части завершено.</div>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<span>Оценка</span>
|
||||||
|
<v-rating v-model="rating" color="success" half-increments large></v-rating>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="primary" dark flat @click.native="finishDialog = false">
|
||||||
|
<v-icon>close</v-icon> Закрыть
|
||||||
|
</v-btn>
|
||||||
|
<v-btn color="success" dark flat @click.native="finishWork">
|
||||||
|
<v-icon>check</v-icon> Подтвердить
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</v-flex>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
@@ -31,13 +77,56 @@
|
|||||||
BookPartContent,
|
BookPartContent,
|
||||||
BookPartWords,
|
BookPartWords,
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
currentUserArticle() {
|
||||||
|
const articles = this.$store.getters.userData.articles;
|
||||||
|
if (!articles) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return articles[this.articleId];
|
||||||
|
},
|
||||||
|
currentUserArticlePart() {
|
||||||
|
const article = this.currentUserArticle;
|
||||||
|
if (!article) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const articleParts = article.parts;
|
||||||
|
if (!articleParts) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return articleParts[this.partId];
|
||||||
|
},
|
||||||
|
finishedAt() {
|
||||||
|
const articlePart = this.currentUserArticlePart;
|
||||||
|
return articlePart ? articlePart.finishedAt : null
|
||||||
|
},
|
||||||
|
storedRating() {
|
||||||
|
const articlePart = this.currentUserArticlePart;
|
||||||
|
return articlePart ? articlePart.rating : 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
finishWork() {
|
||||||
|
this.$store.dispatch('finishUserArticlePart', {
|
||||||
|
articleId: this.articleId,
|
||||||
|
partId: this.partId,
|
||||||
|
rating: this.rating,
|
||||||
|
});
|
||||||
|
this.finishDialog = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
part: null,
|
part: null,
|
||||||
|
finishDialog: false,
|
||||||
|
rating: 0,
|
||||||
}),
|
}),
|
||||||
created() {
|
created() {
|
||||||
|
const { articleId, partId } = this;
|
||||||
Vue.$db.collection('articleParts')
|
Vue.$db.collection('articleParts')
|
||||||
.where('articleId', '==', this.articleId)
|
.where('articleId', '==', articleId)
|
||||||
.where('articlePartId', '==', this.partId)
|
.where('articlePartId', '==', partId)
|
||||||
.get()
|
.get()
|
||||||
.then((querySnapshot) => {
|
.then((querySnapshot) => {
|
||||||
const snapDocs = querySnapshot.docs;
|
const snapDocs = querySnapshot.docs;
|
||||||
@@ -45,7 +134,13 @@
|
|||||||
this.part = Object.assign({}, snapDocs[0].data());
|
this.part = Object.assign({}, snapDocs[0].data());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(e => console.error(e));
|
.then(() => {
|
||||||
|
this.$store.dispatch('updateUserArticlePartStats', {
|
||||||
|
articleId,
|
||||||
|
partId,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(e => window.console.error(e));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user