From 4b54a9ce5246ffb5c511a87639a8d6c5c73d3c29 Mon Sep 17 00:00:00 2001 From: Paolo Arena Date: Thu, 30 Jan 2020 01:20:56 +0100 Subject: [PATCH] - Downline User - Not registered if already exists. - Forgot Password --- src/boot/googlemap.ts | 5 + src/boot/mycharts.ts | 5 + src/boot/vuetelinput.ts | 7 + src/components/CMyDashboard/CMyDashboard.scss | 0 src/components/CMyDashboard/CMyDashboard.ts | 73 ++++++++++ src/components/CMyDashboard/CMyDashboard.vue | 70 ++++++++++ src/components/CMyDashboard/index.ts | 1 + src/components/CRequisiti/CRequisiti.scss | 14 ++ src/components/CRequisiti/CRequisiti.ts | 19 +++ src/components/CRequisiti/CRequisiti.vue | 15 +++ src/components/CRequisiti/index.ts | 1 + src/components/CUserBadge/CUserBadge.scss | 7 + src/components/CUserBadge/CUserBadge.ts | 58 ++++++++ src/components/CUserBadge/CUserBadge.vue | 41 ++++++ src/components/CUserBadge/index.ts | 1 + src/components/CVideo/CVideo.scss | 0 src/components/CVideo/CVideo.ts | 21 +++ src/components/CVideo/CVideo.vue | 27 ++++ src/components/CVideo/index.ts | 1 + src/model/NotevoleStore.ts | 26 ++++ src/store/Modules/NotevoleStore.ts | 92 +++++++++++++ src/typings/libs/google.d.ts | 7 + src/validation/aportadorexist.ts | 25 ++++ src/views/admin/dbop/dbop.scss | 0 src/views/admin/dbop/dbop.ts | 25 ++++ src/views/admin/dbop/dbop.vue | 27 ++++ src/views/admin/dbop/index.ts | 1 + .../request-resetpwd-validate.ts | 18 +++ src/views/updatepassword/updatepassword.scss | 0 src/views/updatepassword/updatepassword.ts | 125 ++++++++++++++++++ src/views/updatepassword/updatepassword.vue | 81 ++++++++++++ 31 files changed, 793 insertions(+) create mode 100644 src/boot/googlemap.ts create mode 100644 src/boot/mycharts.ts create mode 100644 src/boot/vuetelinput.ts create mode 100644 src/components/CMyDashboard/CMyDashboard.scss create mode 100644 src/components/CMyDashboard/CMyDashboard.ts create mode 100644 src/components/CMyDashboard/CMyDashboard.vue create mode 100644 src/components/CMyDashboard/index.ts create mode 100644 src/components/CRequisiti/CRequisiti.scss create mode 100644 src/components/CRequisiti/CRequisiti.ts create mode 100644 src/components/CRequisiti/CRequisiti.vue create mode 100644 src/components/CRequisiti/index.ts create mode 100644 src/components/CUserBadge/CUserBadge.scss create mode 100644 src/components/CUserBadge/CUserBadge.ts create mode 100644 src/components/CUserBadge/CUserBadge.vue create mode 100644 src/components/CUserBadge/index.ts create mode 100644 src/components/CVideo/CVideo.scss create mode 100644 src/components/CVideo/CVideo.ts create mode 100644 src/components/CVideo/CVideo.vue create mode 100644 src/components/CVideo/index.ts create mode 100644 src/model/NotevoleStore.ts create mode 100644 src/store/Modules/NotevoleStore.ts create mode 100644 src/typings/libs/google.d.ts create mode 100644 src/validation/aportadorexist.ts create mode 100644 src/views/admin/dbop/dbop.scss create mode 100644 src/views/admin/dbop/dbop.ts create mode 100644 src/views/admin/dbop/dbop.vue create mode 100644 src/views/admin/dbop/index.ts create mode 100644 src/views/updatepassword/request-resetpwd-validate.ts create mode 100644 src/views/updatepassword/updatepassword.scss create mode 100644 src/views/updatepassword/updatepassword.ts create mode 100644 src/views/updatepassword/updatepassword.vue diff --git a/src/boot/googlemap.ts b/src/boot/googlemap.ts new file mode 100644 index 0000000..11289eb --- /dev/null +++ b/src/boot/googlemap.ts @@ -0,0 +1,5 @@ +import google from '../googlemap' + +export default ({ app, router, Vue }) => { + Vue.prototype.$google = google +} diff --git a/src/boot/mycharts.ts b/src/boot/mycharts.ts new file mode 100644 index 0000000..6eff0e8 --- /dev/null +++ b/src/boot/mycharts.ts @@ -0,0 +1,5 @@ +import Chartkick from 'vue-chartkick' + +export default async ({ Vue }) => { + Vue.use(Chartkick) +} diff --git a/src/boot/vuetelinput.ts b/src/boot/vuetelinput.ts new file mode 100644 index 0000000..2631223 --- /dev/null +++ b/src/boot/vuetelinput.ts @@ -0,0 +1,7 @@ +import VueTelInput from 'vue-tel-input' + +// "async" is optional +export default async ({ Vue }) => { + // something to do + Vue.use(VueTelInput) +} diff --git a/src/components/CMyDashboard/CMyDashboard.scss b/src/components/CMyDashboard/CMyDashboard.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/components/CMyDashboard/CMyDashboard.ts b/src/components/CMyDashboard/CMyDashboard.ts new file mode 100644 index 0000000..04be2ee --- /dev/null +++ b/src/components/CMyDashboard/CMyDashboard.ts @@ -0,0 +1,73 @@ +import { Component, Prop, Watch } from 'vue-property-decorator' + +import { UserStore } from '../../store/Modules' +import { DefaultUser } from '@src/store/Modules/UserStore' + +import MixinUsers from '../../mixins/mixin-users' +import { CProfile } from '../CProfile' +import { CTitleBanner } from '../CTitleBanner' +import { CMyFieldDb } from '../CMyFieldDb' +import { CCopyBtn } from '../CCopyBtn' +import { CUserBadge } from '../CUserBadge' + +@Component({ + components: { CProfile, CTitleBanner, CMyFieldDb, CCopyBtn, CUserBadge } +}) + +export default class CMyDashboard extends MixinUsers { + @Prop({ required: true }) public username + + public $v + public $q + public dashboard = { aportador: DefaultUser, numpeople_aportador: 0, downline: [], downbyuser: [] } + + public mythis() { + return this + } + + public created() { + this.update_username() + } + + @Watch('this.username') + public changeusername() { + this.update_username() + + } + public update_username() { + if (this.username === '') + this.username = this.getMyUsername() + + UserStore.actions.getDashboard({ username: this.username }).then((ris) => { + // console.log('getDashboard', ris) + if (ris.aportador === undefined) { + this.dashboard.aportador = DefaultUser + } else { + this.dashboard.aportador = ris.aportador + } + if (ris.numpeople_aportador === undefined) { + this.dashboard.numpeople_aportador = 0 + } else { + this.dashboard.numpeople_aportador = ris.numpeople_aportador + } + if (ris.downline === undefined) { + this.dashboard.downline = [] + } else { + this.dashboard.downline = ris.downline + } + + if (ris.downbyuser === undefined) { + this.dashboard.downbyuser = [] + } else { + this.dashboard.downbyuser = ris.downbyuser + } + + // console.log('this.dashboard', this.dashboard) + }) + } + + get getRefLink() { + return UserStore.getters.getRefLink(this.username) + } + +} diff --git a/src/components/CMyDashboard/CMyDashboard.vue b/src/components/CMyDashboard/CMyDashboard.vue new file mode 100644 index 0000000..7f1ec89 --- /dev/null +++ b/src/components/CMyDashboard/CMyDashboard.vue @@ -0,0 +1,70 @@ + + + + diff --git a/src/components/CMyDashboard/index.ts b/src/components/CMyDashboard/index.ts new file mode 100644 index 0000000..182cf89 --- /dev/null +++ b/src/components/CMyDashboard/index.ts @@ -0,0 +1 @@ +export {default as CMyDashboard} from './CMyDashboard.vue' diff --git a/src/components/CRequisiti/CRequisiti.scss b/src/components/CRequisiti/CRequisiti.scss new file mode 100644 index 0000000..af61bca --- /dev/null +++ b/src/components/CRequisiti/CRequisiti.scss @@ -0,0 +1,14 @@ +.requisiti_on, .requisiti_off{ + margin: 4px; + border-radius: 3rem; + padding: 8px; +} + +.requisiti_on { + border: solid 3px #49b502; +} + +.requisiti_off { + border: solid 3px #ef0901; +} + diff --git a/src/components/CRequisiti/CRequisiti.ts b/src/components/CRequisiti/CRequisiti.ts new file mode 100644 index 0000000..1494630 --- /dev/null +++ b/src/components/CRequisiti/CRequisiti.ts @@ -0,0 +1,19 @@ +import Vue from 'vue' +import { Component, Prop, Watch } from 'vue-property-decorator' + +import { toolsext } from '@src/store/Modules/toolsext' + +@Component({}) + +export default class CRequisiti extends Vue { + @Prop({ required: true }) public statebool: boolean + @Prop({ required: true }) public msgTrue: string + @Prop({ required: true }) public msgFalse: string + + get getcl() { + if (this.statebool) + return 'requisiti_on' + else + return 'requisiti_off' + } +} diff --git a/src/components/CRequisiti/CRequisiti.vue b/src/components/CRequisiti/CRequisiti.vue new file mode 100644 index 0000000..a4208ba --- /dev/null +++ b/src/components/CRequisiti/CRequisiti.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/src/components/CRequisiti/index.ts b/src/components/CRequisiti/index.ts new file mode 100644 index 0000000..0b4142f --- /dev/null +++ b/src/components/CRequisiti/index.ts @@ -0,0 +1 @@ +export {default as CRequisiti} from './CRequisiti.vue' diff --git a/src/components/CUserBadge/CUserBadge.scss b/src/components/CUserBadge/CUserBadge.scss new file mode 100644 index 0000000..76b9d30 --- /dev/null +++ b/src/components/CUserBadge/CUserBadge.scss @@ -0,0 +1,7 @@ +.myuserbadge{ + margin-top: 8px; + margin-bottom: 8px; + border: solid 1px #4198ef; + border-radius: 1rem; + padding: 2px; +} diff --git a/src/components/CUserBadge/CUserBadge.ts b/src/components/CUserBadge/CUserBadge.ts new file mode 100644 index 0000000..ee18331 --- /dev/null +++ b/src/components/CUserBadge/CUserBadge.ts @@ -0,0 +1,58 @@ +import Vue from 'vue' +import { GlobalStore } from '@store' +import { UserStore } from '../../store/Modules' +import { Component, Prop, Watch } from 'vue-property-decorator' +import { toolsext } from '@src/store/Modules/toolsext' + +import { validationMixin } from 'vuelidate' + +import MixinBase from '../../mixins/mixin-base' +import { IUserFields } from '../../model' + +@Component({ + name: 'CUserBadge', + components: { } +}) + +export default class CUserBadge extends MixinBase { + @Prop({ required: true }) public index: number + @Prop({ required: false, default: false }) public yourinvite: boolean + @Prop({ required: true }) public user: IUserFields + @Prop({ required: true }) public numpeople: number + @Prop({ required: true }) public mycolor: string + public $v + public $t: any + + public getletter(user) { + return user.name[0].toUpperCase() + } + + public getnumber(user, index) { + return index + } + + public getstatecolor(user) { + return (user.profile.teleg_id) ? 'green' : 'gray' + } + + public getmoneycolor(user) { + return (user.made_gift) ? 'green' : 'gray' + } + + get madegift() { + return UserStore.state.my.made_gift + } + + public getzoomcolor(user) { + return (user.profile.saw_zoom_presentation) ? 'green' : 'gray' + } + + public get2peoplecolor() { + return (this.getnumpeople() >= 2) ? 'green' : 'gray' + } + + public getnumpeople() { + return this.numpeople + } + +} diff --git a/src/components/CUserBadge/CUserBadge.vue b/src/components/CUserBadge/CUserBadge.vue new file mode 100644 index 0000000..fab6437 --- /dev/null +++ b/src/components/CUserBadge/CUserBadge.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/src/components/CUserBadge/index.ts b/src/components/CUserBadge/index.ts new file mode 100644 index 0000000..e3421e2 --- /dev/null +++ b/src/components/CUserBadge/index.ts @@ -0,0 +1 @@ +export {default as CUserBadge} from './CUserBadge.vue' diff --git a/src/components/CVideo/CVideo.scss b/src/components/CVideo/CVideo.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/components/CVideo/CVideo.ts b/src/components/CVideo/CVideo.ts new file mode 100644 index 0000000..2175c59 --- /dev/null +++ b/src/components/CVideo/CVideo.ts @@ -0,0 +1,21 @@ +import Vue from 'vue' +import { Component, Prop, Watch } from 'vue-property-decorator' +import { UserStore } from '@store' +import { tools } from '../../store/Modules/tools' +import { toolsext } from '@src/store/Modules/toolsext' + +import MixinBase from '../../mixins/mixin-base' +import { CTitleBanner } from '../CTitleBanner' + +@Component({ + components: { } +}) + +export default class CVideo extends MixinBase { + @Prop({ required: true }) public myvideokey: string + @Prop({ required: false, default: '' }) public title: boolean + + get getvideotit() { + return this.title + } +} diff --git a/src/components/CVideo/CVideo.vue b/src/components/CVideo/CVideo.vue new file mode 100644 index 0000000..108d59c --- /dev/null +++ b/src/components/CVideo/CVideo.vue @@ -0,0 +1,27 @@ + + + + diff --git a/src/components/CVideo/index.ts b/src/components/CVideo/index.ts new file mode 100644 index 0000000..a4cbf07 --- /dev/null +++ b/src/components/CVideo/index.ts @@ -0,0 +1 @@ +export {default as CVideo} from './CVideo.vue' diff --git a/src/model/NotevoleStore.ts b/src/model/NotevoleStore.ts new file mode 100644 index 0000000..e3eb3a2 --- /dev/null +++ b/src/model/NotevoleStore.ts @@ -0,0 +1,26 @@ +import { IUserFields, IUserProfile } from '@src/model/UserStore' + +export interface ICheckUser { + verified_email?: boolean + teleg_id?: number + profile?: IUserProfile +} + +export interface INotData { + num_tot_lista?: number + num_reg_lista?: number + num_reg?: number + email_non_verif?: number + num_teleg_attivo?: number + num_teleg_pending?: number + num_part_zoom?: number + num_users_dream?: number + arr_nations?: string + lastsreg?: IUserFields[] + checkuser?: ICheckUser | any + reg_daily?: string +} + +export interface INotevoleState { + datastat: INotData +} diff --git a/src/store/Modules/NotevoleStore.ts b/src/store/Modules/NotevoleStore.ts new file mode 100644 index 0000000..64a0d31 --- /dev/null +++ b/src/store/Modules/NotevoleStore.ts @@ -0,0 +1,92 @@ +import Api from '@api' +import { IAction, INotevoleState, IParamsQuery } from 'model' +import { storeBuilder } from './Store/Store' + +import { serv_constants } from '../Modules/serv_constants' +import { tools } from '../Modules/tools' +import { toolsext } from '@src/store/Modules/toolsext' +import { GlobalStore, NotevoleStore, Todos, Projects, CalendarStore, UserStore } from '@store' +import globalroutines from './../../globalroutines/index' + +import { static_data } from '@src/db/static_data' +import { db_data } from '@src/db/db_data' + +import translate from './../../globalroutines/util' +import * as Types from '@src/store/Api/ApiTypes' +import { ICalendarState, ICfgServer } from '@src/model' + +const state: INotevoleState = { + datastat: { + + } +} + +const b = storeBuilder.module('NotevoleModule', state) + +namespace Getters { + // const fullName = b.read(function fullName(state): string { + // return state.NotevoleInfos.firstname?capitalize(state.NotevoleInfos.firstname) + " " + capitalize(state.NotevoleInfos.lastname):null; + // }) + + // const isNotevoleInvalid = b.read((mystate) => { + // try { + // const ris = (mystate.my._id === undefined) || (mystate.my._id.trim() === '') || (mystate.my.tokens[0] === undefined) + // // console.log('state._id', state._id, 'ris', ris) + // return ris + // } catch (e) { + // return true + // } + // }, 'isNotevoleInvalid') + + export const getters = { + } + +} + +namespace Mutations { + export const mutations = { + } +} + +namespace Actions { + + async function notevoleload(context) { + + const paramquery = { + locale: tools.getLocale(), + username: UserStore.state.my.username + } + + return await Api.SendReq('/ayni/load', 'POST', paramquery) + .then((res) => { + // console.log('res', res) + state.datastat = res.data.datastat + state.datastat.arr_nations = JSON.parse(state.datastat.arr_nations) + state.datastat.reg_daily = JSON.parse(state.datastat.reg_daily) + state.datastat.checkuser = JSON.parse(state.datastat.checkuser) + + return state.datastat + }).catch((error) => { + return null + }) + } + + export const actions = { + notevoleload: b.dispatch(notevoleload) + } + +} + +const stateGetter = b.state() + +// Module +const NotevoleModule = { + get state() { + return stateGetter() + }, + actions: Actions.actions, + getters: Getters.getters, + mutations: Mutations.mutations +} + +export default NotevoleModule diff --git a/src/typings/libs/google.d.ts b/src/typings/libs/google.d.ts new file mode 100644 index 0000000..ac92fc8 --- /dev/null +++ b/src/typings/libs/google.d.ts @@ -0,0 +1,7 @@ +import { google } from '../../googlemap' + +declare module 'vue/types/vue' { + interface Vue { + $google: google + } +} diff --git a/src/validation/aportadorexist.ts b/src/validation/aportadorexist.ts new file mode 100644 index 0000000..f64b29d --- /dev/null +++ b/src/validation/aportadorexist.ts @@ -0,0 +1,25 @@ +import { default as Axios, AxiosResponse } from 'axios' +// import { IPayload } from 'model' +import { GlobalConfig, PayloadMessageTypes } from '../common' +import { tools } from '../store/Modules/tools' + +// const SITE_URL = GlobalConfig.uri.site +const VALIDATE_USER_URL = process.env.MONGODB_HOST + '/users/' + +export function aportadorexist(userName: string) { + if (userName === tools.APORTADOR_NONE) + return true + + let onSuccess = (res: AxiosResponse) => { + // console.log('res.status', res.status) + return res.status === PayloadMessageTypes.statusfound + } + + return Axios.get(VALIDATE_USER_URL + process.env.APP_ID + '/' + userName) + .then(onSuccess) + .catch((err) => { + // console.log('err', err) + return false + }) + +} diff --git a/src/views/admin/dbop/dbop.scss b/src/views/admin/dbop/dbop.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/views/admin/dbop/dbop.ts b/src/views/admin/dbop/dbop.ts new file mode 100644 index 0000000..93d7c51 --- /dev/null +++ b/src/views/admin/dbop/dbop.ts @@ -0,0 +1,25 @@ +import Vue from 'vue' +import { Component } from 'vue-property-decorator' + +import { GlobalStore } from '@store' +import { tools } from '../../../store/Modules/tools' +import { UserStore } from '../../../store/Modules' +import { CTitleBanner } from '../../../components/CTitleBanner' + +@Component({ + components: { CTitleBanner } +}) +export default class Dbop extends Vue { + public ris: string = '' + + public async changeCellInt() { + + const mydata = { + dbop: 'changeCellInt' + } + + this.ris = await UserStore.actions.execDbOp({ mydata }) + + } + +} diff --git a/src/views/admin/dbop/dbop.vue b/src/views/admin/dbop/dbop.vue new file mode 100644 index 0000000..ecf4e0a --- /dev/null +++ b/src/views/admin/dbop/dbop.vue @@ -0,0 +1,27 @@ + + + + diff --git a/src/views/admin/dbop/index.ts b/src/views/admin/dbop/index.ts new file mode 100644 index 0000000..3da4479 --- /dev/null +++ b/src/views/admin/dbop/index.ts @@ -0,0 +1 @@ +export {default as dbop} from './dbop.vue' diff --git a/src/views/updatepassword/request-resetpwd-validate.ts b/src/views/updatepassword/request-resetpwd-validate.ts new file mode 100644 index 0000000..2a483ce --- /dev/null +++ b/src/views/updatepassword/request-resetpwd-validate.ts @@ -0,0 +1,18 @@ +import { ISignupOptions } from 'model' +import { email, minLength, required, sameAs } from 'vuelidate/lib/validators' +// import { ValidationRuleset } from 'vuelidate' +import { complexity, registeredemail, registereduser, aportadorexist } from '../../validation' + +export const validations = { + form: { + repeatPassword: { + required, + sameAsPassword: sameAs('password') + }, + password: { + required, + minLength: minLength(8), + complexity + } + } +} diff --git a/src/views/updatepassword/updatepassword.scss b/src/views/updatepassword/updatepassword.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/views/updatepassword/updatepassword.ts b/src/views/updatepassword/updatepassword.ts new file mode 100644 index 0000000..d87f1a0 --- /dev/null +++ b/src/views/updatepassword/updatepassword.ts @@ -0,0 +1,125 @@ +import Vue from 'vue' +import { Component, Prop, Watch } from 'vue-property-decorator' +import { serv_constants } from '../../store/Modules/serv_constants' + +import { UserStore } from '../../store/Modules/index' +import { tools } from '../../store/Modules/tools' +import { Logo } from '../../components/logo' +import { validationMixin } from 'vuelidate' +import { CTitleBanner } from '../../components/CTitleBanner' +import { validations } from './request-resetpwd-validate' + +@Component({ + mixins: [validationMixin], + validations, + components: { Logo, CTitleBanner } +}) + +export default class Updatepassword extends Vue { + public $q + public $t + public $v + + public emailsent = false + public form = { + password: '', + repeatPassword: '', + tokenforgot: '', + email: '', + idapp: '' + } + + public created() { + // this.load() + } + + get emailinviata() { + return this.emailsent + } + + public submit() { + this.$v.form.$touch() + + if (this.$v.form.$error) { + tools.showNotif(this.$q, this.$t('reg.err.errore_generico')) + return + } + + this.$q.loading.show({ message: this.$t('reset.incorso') }) + + // console.log('this.$route.query', this.$route.query) + this.form.tokenforgot = this.$route.query.tokenforgot.toString() + this.form.email = this.$route.query.email.toString() + this.form.idapp = process.env.APP_ID + + console.log(this.form) + UserStore.actions.resetpwd(this.form) + .then((ris) => { + console.log('ris', ris) + if (ris.code === serv_constants.RIS_CODE_OK) + this.$router.push('/signin') + else if (ris.code === serv_constants.RIS_CODE_TOKEN_RESETPASSWORD_NOT_FOUND) + tools.showNegativeNotif(this.$q, this.$t('reset.token_scaduto')) + else + tools.showNegativeNotif(this.$q, this.$t('fetch.errore_server')) + + this.$q.loading.hide() + }).catch(error => { + console.log('ERROR = ' + error) + this.$q.loading.hide() + }) + + } + + public errorMsg(cosa: string, item: any) { + try { + if (!item.$error) { + return '' + } + // console.log('errorMsg', cosa, item) + if (item.$params.email && !item.email) { + return this.$t('reg.err.email') + } + + if (cosa === 'repeatpassword') { + if (!item.sameAsPassword) { + return this.$t('reg.err.sameaspassword') + } + } + + // console.log('item', item) + + if (item.minLength !== undefined) { + if (!item.minLength) { + return this.$t('reg.err.atleast') + ` ${item.$params.minLength.min} ` + this.$t('reg.err.char') + } + } + if (item.complexity !== undefined) { + if (!item.complexity) { + return this.$t('reg.err.complexity') + } + } +// if (!item.maxLength) { return this.$t('reg.err.notmore') + ` ${item.$params.maxLength.max} ` + this.$t('reg.err.char') } + + if (item.required !== undefined) { + if (!item.required) { + return this.$t('reg.err.required') + } + } + + // console.log(' ....avanti') + if (cosa === 'email') { + // console.log("EMAIL " + item.isUnique); + // console.log(item); + if (!item.isUnique) { + return this.$t('reg.err.duplicate_email') + } + } + + return '' + } catch (error) { + // console.log("ERR : " + error); + } + } + +} diff --git a/src/views/updatepassword/updatepassword.vue b/src/views/updatepassword/updatepassword.vue new file mode 100644 index 0000000..5ac5396 --- /dev/null +++ b/src/views/updatepassword/updatepassword.vue @@ -0,0 +1,81 @@ + + + + +