diff --git a/web/package.json b/web/package.json index d9650e7..99b6b4c 100644 --- a/web/package.json +++ b/web/package.json @@ -10,6 +10,7 @@ "dependencies": { "@mdi/font": "5.9.55", "autoprefixer": "^10.4.2", + "pinia": "^2.0.11", "postcss": "^8.4.6", "tailwindcss": "^3.0.18", "vue": "^3.2.25", diff --git a/web/src/App.vue b/web/src/App.vue index 385cca8..400a617 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -21,7 +21,22 @@ export default defineComponent({ } }, beforeCreate () { - this.$store.commit("initializeStore"); + const store = localStorage.getItem("store"); + if (!store) + return; + + const restoredState = JSON.parse(store); + if (!restoredState) + return; + + state.Session = restoredState.Session; + state.CurrentBudgetID = restoredState.CurrentBudgetID; + state.ShowMenu = restoredState.ShowMenu; + state.ExpandMenu = restoredState.ExpandMenu; + + for (const budget of restoredState.Budgets || []) { + state.Budgets.set(budget[0], budget[1]); + } } }) diff --git a/web/src/main.ts b/web/src/main.ts index 5e0c9c5..601ab38 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -2,12 +2,12 @@ import { createApp } from 'vue' import App from './App.vue' import './index.css' import router from './router' -import { store, key } from './store' +import { createPinia } from 'pinia' import { SET_CURRENT_ACCOUNT, SET_CURRENT_BUDGET } from './store/action-types' const app = createApp(App) app.use(router) -app.use(store, key) +app.use(createPinia()) app.mount('#app') router.beforeEach(async (to, from, next) => { diff --git a/web/src/store/index.ts b/web/src/store/index.ts index caa51fc..920e664 100644 --- a/web/src/store/index.ts +++ b/web/src/store/index.ts @@ -3,98 +3,21 @@ import { createStore, Store, createLogger } from 'vuex' import { LOGIN_SUCCESS, LOGOUT, TITLE } from './mutation-types' import { FETCH_ACCOUNT, FETCH_BUDGET, GET, REGISTER, IMPORT_YNAB, LOGIN, NEW_BUDGET, POST, SET_CURRENT_ACCOUNT, SET_CURRENT_BUDGET } from './action-types' import { budgetStore } from './budget' - -export interface State { - Session: { - Token?: string - User?: string - }, - ShowMenu?: boolean, - ExpandMenu?: boolean, - Budgets: Map, - CurrentBudgetID?: string, -} - -export interface Budget { - ID: string - Name: string - AvailableBalance: number -} +import { Budget, useSessionStore } from '../stores/session' export const key: InjectionKey> = Symbol() export const store = createStore({ state: { - Session: { - Token: undefined, - User: undefined - }, ShowMenu: undefined, - Budgets: new Map(), CurrentBudgetID: undefined, }, mutations: { - deleteBudget(state: State, budgetid: string) { - state.Budgets.delete(budgetid) - }, - toggleMenu(state) { - state.ShowMenu = !state.ShowMenu; - }, - toggleMenuSize(state) { - state.ExpandMenu = !state.ExpandMenu; - }, - initializeStore(state) { - const store = localStorage.getItem("store"); - if (!store) - return; - - const restoredState = JSON.parse(store); - if (!restoredState) - return; - - state.Session = restoredState.Session; - state.CurrentBudgetID = restoredState.CurrentBudgetID; - state.ShowMenu = restoredState.ShowMenu; - state.ExpandMenu = restoredState.ExpandMenu; - - for (const budget of restoredState.Budgets || []) { - state.Budgets.set(budget[0], budget[1]); - } - }, [TITLE](state, title) { document.title = "Budgeteer - " + title; }, - [LOGIN_SUCCESS](state, result) { - state.Session = { - User: result.User, - Token: result.Token - }; - for (const budget of result.Budgets) { - state.Budgets.set(budget.ID, budget) - } - }, - addBudget(state, budget) { - state.Budgets.set(budget.ID, budget); - }, - [LOGOUT](state) { - state.Session = { Token: undefined, User: undefined }; - state.Budgets.clear(); - }, - setCurrentBudgetID(state, budgetid) { - state.CurrentBudgetID = budgetid; - }, }, actions: { - [LOGIN]({ state, commit }, login) { - return fetch("/api/v1/user/login", { method: "POST", body: JSON.stringify(login) }) - .then(x => x.json()) - .then(x => commit(LOGIN_SUCCESS, x)) - }, - [REGISTER]({ state, commit }, login) { - return fetch("/api/v1/user/register", { method: "POST", body: JSON.stringify(login) }) - .then(x => x.json()) - .then(x => commit(LOGIN_SUCCESS, x)) - }, [IMPORT_YNAB]({ getters, dispatch }, formData) { return dispatch("POST", { path: "/budget/" + getters.CurrentBudget.ID + "/import/ynab", body: formData }); }, @@ -137,9 +60,6 @@ export const store = createStore({ }, }, getters: { - Budgets(state) { - return state.Budgets.values(); - }, AuthHeaders(state) { return { 'Authorization': 'Bearer ' + state.Session.Token @@ -171,9 +91,10 @@ export const store = createStore({ }) store.subscribe((mutation, state) => { + + const sessionStore = useSessionStore(); let persistedState = { - Session: state.Session, - Budgets: [...state.Budgets], + Session: sessionStore, // Accounts: [...state.Accounts], CurrentBudgetID: state.CurrentBudgetID, //CurrentAccountID: state.CurrentAccountID, diff --git a/web/src/stores/budgets.ts b/web/src/stores/budgets.ts new file mode 100644 index 0000000..9e122c9 --- /dev/null +++ b/web/src/stores/budgets.ts @@ -0,0 +1,16 @@ +import { defineStore } from "pinia"; + +interface State { + CurrentBudgetID: string | null, +} + +export const useBudgetsStore = defineStore('budget', { + state: (): State => ({ + CurrentBudgetID: null + }), + actions: { + setCurrentBudgetID(budgetid : string) { + this.CurrentBudgetID = budgetid; + }, + } +}) \ No newline at end of file diff --git a/web/src/stores/session.ts b/web/src/stores/session.ts new file mode 100644 index 0000000..ad6bdb3 --- /dev/null +++ b/web/src/stores/session.ts @@ -0,0 +1,69 @@ +import { defineStore } from 'pinia' + +interface State { + Token: string | null + User: string | null + Budgets: Map, +} + +export interface Budget { + ID: string + Name: string + AvailableBalance: number +} + +export const useSessionStore = defineStore('session', { + // convert to a function + state: (): State => ({ + Token: null, + User: null, + Budgets: new Map(), + }), + getters: { + Budgets(): IterableIterator { + return this.Budgets.values(); + }, + /*// must define return type because of using `this` + fullUserDetails (state): FullUserDetails { + // import from other stores + const authPreferencesStore = useAuthPreferencesStore() + const authEmailStore = useAuthEmailStore() + return { + ...state, + // other getters now on `this` + fullName: this.fullName, + ...authPreferencesStore.$state, + ...authEmailStore.details + } + + // alternative if other modules are still in Vuex + // return { + // ...state, + // fullName: this.fullName, + // ...vuexStore.state.auth.preferences, + // ...vuexStore.getters['auth/email'].details + // } + }*/ + }, + actions: { + loginSuccess(x : any) { + this.User = x.User; + this.Token = x.Token; + this.Budgets = x.Budgets; + }, + login(login: any) { + return fetch("/api/v1/user/login", { method: "POST", body: JSON.stringify(login) }) + .then(x => x.json()) + .then(x => this.loginSuccess(x)); + }, + register(login : any) { + return fetch("/api/v1/user/register", { method: "POST", body: JSON.stringify(login) }) + .then(x => x.json()) + .then(x => this.loginSuccess(x)) + }, + // easily reset state using `$reset` + logout() { + this.$reset() + } + } +}) diff --git a/web/src/stores/settings.ts b/web/src/stores/settings.ts new file mode 100644 index 0000000..ef671a2 --- /dev/null +++ b/web/src/stores/settings.ts @@ -0,0 +1,21 @@ +import { defineStore } from "pinia"; + +interface State { + ShowMenu?: boolean, + ExpandMenu?: boolean, +} + +export const useSettingsStore = defineStore('settings', { + state: (): State => ({ + ShowMenu: undefined, + ExpandMenu: false, + }), + actions: { + toggleMenu() { + this.ShowMenu = !this.ShowMenu; + }, + toggleMenuSize() { + this.ExpandMenu = !this.ExpandMenu; + }, + } +}); \ No newline at end of file diff --git a/web/yarn.lock b/web/yarn.lock index 2fdd5ae..f0565d7 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1588,6 +1588,11 @@ resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.0.0-beta.21.1.tgz#f1410f53c42aa67fa3b01ca7bdba891f69d7bc97" integrity sha512-FqC4s3pm35qGVeXRGOjTsRzlkJjrBLriDS9YXbflHLsfA9FrcKzIyWnLXoNm+/7930E8rRakXuAc2QkC50swAw== +"@vue/devtools-api@^6.0.0-beta.21": + version "6.0.5" + resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.0.5.tgz#7e35cfee4f44ada65cde0d19341fbaeb0ae353f4" + integrity sha512-2nM84dzo3B63pKgxwoArlT1d/yqSL0y2lG2GiyyGhwpyPTwkfIuJHlCNbputCoSCNnT6MMfenK1g7nv7Mea19A== + "@vue/reactivity-transform@3.2.29": version "3.2.29" resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.29.tgz#a08d606e10016b7cf588d1a43dae4db2953f9354" @@ -6260,6 +6265,14 @@ pify@^4.0.1: resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== +pinia@^2.0.11: + version "2.0.11" + resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.0.11.tgz#ff03c714f5e5f16207280a4fc2eab01f3701ee2b" + integrity sha512-JzcmnMqu28PNWOjDgEDK6fTrIzX8eQZKPPKvu/fpHdpXARUj1xeVdFi3YFIMOWswqaBd589cpmAMdSSTryI9iw== + dependencies: + "@vue/devtools-api" "^6.0.0-beta.21" + vue-demi "*" + pkg-dir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" @@ -8167,6 +8180,11 @@ vue-cli-plugin-vuetify@~2.4.5: semver "^7.1.2" shelljs "^0.8.3" +vue-demi@*: + version "0.12.1" + resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.12.1.tgz#f7e18efbecffd11ab069d1472d7a06e319b4174c" + integrity sha512-QL3ny+wX8c6Xm1/EZylbgzdoDolye+VpCXRhI2hug9dJTP3OUJ3lmiKN3CsVV3mOJKwFi0nsstbgob0vG7aoIw== + vue-hot-reload-api@^2.3.0: version "2.3.4" resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"