From ed058292bfa9fe4ff9aab22d7e8c3ffd3345b2c7 Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Tue, 19 Sep 2023 17:20:28 +0700 Subject: [PATCH 1/2] Refactor code, fix UI, variable code --- .../images/ic_category_foodndrink.png.png | Bin 1771 -> 0 bytes .../images/ic_category_other_income.png.png | Bin 2026 -> 0 bytes .../images/ic_category_transport.png.png | Bin 1653 -> 0 bytes .../src/assets/images/icon.png | Bin 1402 -> 0 bytes .../src/assets/images/icon.webp | Bin 0 -> 690 bytes .../src/js/helpers/dataProcess.js | 76 ++++++++++++++ javascript-practice/src/js/helpers/helpers.js | 98 +----------------- .../src/js/helpers/validateForm.js | 27 +++++ .../src/js/models/transaction.js | 17 +-- javascript-practice/src/js/models/user.js | 8 +- javascript-practice/src/js/models/wallet.js | 10 +- .../src/js/services/commonService.js | 4 +- .../src/js/services/walletService.js | 3 +- .../src/js/views/authenticationView.js | 2 +- javascript-practice/src/js/views/homeView.js | 18 +++- .../src/js/views/transactionView.js | 19 ++++ javascript-practice/src/pages/index.html | 41 +++++--- javascript-practice/src/pages/login.html | 1 + javascript-practice/src/pages/register.html | 1 + .../src/styles/abstracts/_variables.scss | 7 +- .../src/styles/pages/_index.scss | 32 ++++-- .../src/styles/pages/_login-register.scss | 3 +- 22 files changed, 209 insertions(+), 158 deletions(-) delete mode 100644 javascript-practice/src/assets/images/ic_category_foodndrink.png.png delete mode 100644 javascript-practice/src/assets/images/ic_category_other_income.png.png delete mode 100644 javascript-practice/src/assets/images/ic_category_transport.png.png delete mode 100644 javascript-practice/src/assets/images/icon.png create mode 100644 javascript-practice/src/assets/images/icon.webp create mode 100644 javascript-practice/src/js/helpers/dataProcess.js create mode 100644 javascript-practice/src/js/helpers/validateForm.js create mode 100644 javascript-practice/src/js/views/transactionView.js diff --git a/javascript-practice/src/assets/images/ic_category_foodndrink.png.png b/javascript-practice/src/assets/images/ic_category_foodndrink.png.png deleted file mode 100644 index 8d61471e285e172363151602a91b1d2795dcc37c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1771 zcmV;P)P17_@v`uW< z+C)tSd=y(n#Rv5nmADF~f?@@HD=Nw&?gG2)y?1&pWZ7LQ#T)A9QlHGu3M8E_S6K96< zK9NaGbu0kqi%1md+d|Z+z;*_)ea0K{r_F*BDWD@=Sx%_K$$#8wdKRlpfr zYucN6L=*XA=hq-(=gf(g<*@CfJrNxQu~La`p50#eK~diB%J%fzB{*_c>PRq_Ns964 zL=e#%LRA+Q?B90Im!408Ni)+@I2V6VGXz}#BKd?!rV_~mZRxa0aO7O_oXqk_p$EDW z0yb4r-znJR-IHDkQt9X=aY-|H5&sFqRhz`e7wyhywI+35v>E4oD>|86OoWUXui4Qn zT)~4H3Aq;ysIqeBkO_KO-ARoJt;n<#B+XtjnF((X@B#ad%tE{bRv`1yYSHWf8YHbP}{_hU!(=9UOyW#co7D2kzH! z+d{4D-3+KUbVo_C9e1jw#v}>S2^=eNm01QhaP07ebtNSwjSJS5 z#T9kK$e$73I;N~foG}{4=x9Xs?166Kq4b0y%wT{%I2ge}It0<<%qvBubBseqz0Hog zdK)ZOE2^G6MX~Y}b(e0zOghDPf*YTHk#fu7BCp9I+OBoQgP_FKf$k9>5NE{v8B@?V zHU`nXdeTgF=m^Rz)fl777?K**Nce6=C2m*T$Ca|{Fx$#tt*vX;m1Hr@NzlcJv)=9< z-mZA4Wfl7Lj_xoyl@2o3tkoFWJ*shmM}&uCNc>BfmOKurb7nv%NuCCb^=+orEEL=$ zyefrh-NvmrbRr*3C#7e{sYiD)yhl_k`NPLmNdJB#o;u)tL@|`yl3y`9lNnH za~GZ!wX&nJF2owTdB_WkOR)N@b*LngEkTLW>=B(yDC)copoQniX%n7WtXTQsVi@$U zBzKsr(D0}dad_Nd^&E+P`g`MyZ4)b0%2)I zkf_{gQ5HRL?&4*9`{QP;Oq+-LtSzX$dBZ+soZEKN9#^U?~wRG z2wIqi4O@Rle!)fffg3Ke%}D2?W|wQnoyhjAdr>W1vtlWXdHW!n@?FHdK$koTwNqx{ zn~lHW*4_K?0~WKRDysb8LA&CIr-~=c|f49wlpHtA(GSil**x7kE@iWBiy(&Ky&z84Ew4t-YcvWpG5ifY;xD z4@%iT;4wxRR(*n;D>tx-GPS|sfZq@(91!C8!nqG?XjS`0&t5!1fFDDEm^5w_md;7h z$`vy99l_y~X9BXl#9wlrk!&c!p}kSQl4g|gpc ziFqw7HVF&qRfYheg)^X#HbdNGP?okNhqef6w=4RKB8{4MbUkWhfkmdh+aG%SvCYza zHYdZRD40jhC+IRTJFUGX7xrbHZA-)F-cgvFnX521jn@4wbT$Oam{60??aSKlORb&F zjYyIwbvmXM??W9vcR9H+oUrNGTP7c#bHcBaXnxa;KV|g0sgs3@uc@7cz%N(YhJAtN zxaR!+tfT%Kwd+Cm0ZeZ`MrW@l$Bc#i)G{{s0_gzqiWgJu8# N002ovPDHLkV1h5GQ2zh` diff --git a/javascript-practice/src/assets/images/ic_category_other_income.png.png b/javascript-practice/src/assets/images/ic_category_other_income.png.png deleted file mode 100644 index 24d0828097ed44d5b8792a03428ad3817685fe3f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2026 zcmVeTtLQqLvFM&`4sas<%B{pmCu6NhFdv^Einf%8FeA%tfY3&e+x8m-h35aghn(R31@r>pZ&P%Xy z`;+Ur#P`vD8}OMzT+z6A%zOC2zj8_OT%C8o>3i5>kMPeJyzp>9Zw;}bzxqT%zh97T}uw{*u+t!$w3}XY}Tunz{gBtn(e>wU#UhIk@k;*~nh36+*={8}d$gJ3`x{Mg%E&>?O1oGCxE=r`8xX$&L{8K1<;m{hpunp= zD!R{U*@*U~HG9jMu2*8W8%#5MuPE|Elp)s5Y+G~>@XHPpsWpeg(<#BZl88M>#GF7X z00hauqW!bPs>>LD{#`R3dq~A|FR7SFWGJbTvxvU_Nkf99)W?!q;)5&6r%Q4Sr0pdU4f`S2y#Q{VkI8hZt1(1&mhu^nizU+V-Si79^|2XpYFfbWABR6M1~XTQ z914cS>H3B_1O-c1Gr7Pv}uwNIP#STKr!e-uoM6FC$QfrYm} zr)L$C%jg6nLkh?n=8dCfOF2rHQNm3tP@_Y4WOOo;m_Ue5O-m|< z5aj7(zb2UtGeWMg+*YsYK{uD;vqYa&%m|Bl?Ub57N%2qez z>ij29HO&7q2-e!Q0b18jpIM6_=e;tPKDz)xvZWi{0VKv!@Vy_x*qJbDHkQM(m}B^a zA3<+~63LV{ej$UP$Ky$_+ws&P$~DjDo1<)ngH-RJ6P&<} zw*yE9Ra`&fM^aIcOem8w*7;^sZLWZGi4DbouDJ&mMu_?CPp%(V)Q}~=6gALsBv^2iJm+iJ58Gh~T+vHoSVO5`mZrxxgU)*8Aq`ZJ1+X zhNBb-kIQLxiewGT&G9J1Z;v_fhF8XqHpZ}Oai-cY4cqXi(-pWLD5wq{N~SCwFbKq1 z!+9f>om97;6xkByE$FwK2fvcSb3{~TF$~>I!{aT(*{cpD)cjW%ntz=1yQ!$M^ETGD z?P}u^_Om>-v5s=5hLtr5^bX1e5xWINW9(7yU-uo&CQ#VZ2bca#d#%G~i~nev_O!Pv z_tQr~TV1`iz0zW~zD9nw#i$%D9_)l}=SGEkh9n9Hqni1fbk+C;J;BlYU~xqCC_+QL zEUo>l_sD@0g^nxczANZmuV>4uCCxOfm9BR$Q&YHraejJxd6AM>t6uXKLr%wCsY|Z} zT~usoT)U3~VwKQ%>Exu0?v?`omo)X??{_r}+^*2R#20~20N<7D3?3t>-~a#s07*qo IM6N<$f)+r|TL1t6 diff --git a/javascript-practice/src/assets/images/ic_category_transport.png.png b/javascript-practice/src/assets/images/ic_category_transport.png.png deleted file mode 100644 index a7eaf5bc5e4cd9f9bffd4ab913c1a424eb40cf5b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1653 zcmV-*28#KKP)CCj9mbMJ?5G*eZe275hDJHyIFh+Sa5+KNj82QjdP4I)5@FPF^qcLg> zY7nR)G1wL?A|j8rkdkPMg!-zM+EQBjo_6NWz1w|nX_@IfI%8Y;VI|X^v(G-~uDj1Z zd!K!Q2N)RyMgZEsU$(BPo@oQkB|(!|4e1!UE4rpW5^HEhTjj?1 z1dEF|Dbf%JtYB~-U>L!JW+Dicg%Mmesc$KJ)dJin2qB)ZXJTQfOol7aR*Wz|j|iL- z6@@m1iUto!P0c2|-syWG9y?vvtzSH7nih9FFw8ig$0GjiH8V~a!AV`cb6AoVbyg3- zlVXSO+!^#aopB$~iTE&lK5H##>`ZE;Iha^xYh}SKp!9keL0wXwh+-V0L5Eh-2d^s? zC_9ePfRoqqf9iv}!2$V@5;1O*n8W|V3Cf_DzyUK%(O2t7*U|0~XOF08g8BBC7Nhqu3oX*+j< z{Owj>s80CX1GutlE2h8q5fZ^~dv_uK@Yhfs0qas|uAv(a)a-i=cXu)jp&NG3f|-4I4&1i_$&8mUGq`|AHj zZqX40>wmEN!kz}hF&lbsFYMzdAQ70lMn*nT*B%D--nP~dS#zOvd<@kUg`kc<2HJy8 zN1TV-Ci`xM7C}wrow#+@$6Jvb@j$J;Eny34%}r1}nNU(w2W5Ox;lI@g@2YfoSEq-n zcV)I4Ib!`ue;7d%Ef)*kmjW-F?QWWYKUy8*`9&% zu=}7QjA9aUkC_wwG!A|bUyVE%QR^G6-U6RHp|LPc>{KI z;BbW#=YG*)vpF~qYyB57A=%+IY?v2B;k@`!yyU9HiE0IkErs9Pur9xfVq>58W8n-N zc0c8VTi&#Ed8|77ru#xnf)nSL_}VH9-r!L6y@ZA|S&znMH)`uNs2sb3CPA3Deg&_t z>Bf?2I_t#chm~e67_i~pQ%;MV83%2u1Dd}FkBztS{O-G&L<-Sc{1qZB;YKW<`DE!? zw$j;w{%o%fRS|GC>K$lvH0Za zp*m4-S6vrZ=LV&Eo<_MAjt#0)DVU!YrD%$}L8hV%(sp(lKgJx?!6Bz88aWd&Jt$*Y zSrz;8b8O^xj2Sn}0NchbJy{t`C9aESEjx>e!TVU)q;MJ*qg6&&+Q;_4v*bX{GS zqxH$p`IXm&{~p%cSlv>Dt_w&+@7C2Q8K>?TqU8unZ5YbnM7wL zZ4ILRvZb*FP)}%JO|(2HC0HU~mde*tlrY{scBIgp5_9q@wl#zr>GH8>O7Fy+Vi`jKK{-<7Jo?U=x%c z5lmcx;{9sqyKjHTD!{NfgUqf%@l&F@#sb}Dy8hIB}a5!TUZmwXh z2)L=k%ryLY!;fnVS(uk~L*N%sUmEQCqBTm~yu?N2M|0qCU!~-BqwKZpQIw@mX?q6R z`DOG|NzG8!z_l{Ho@r-5Z;^-OmdXf{#KDU^n1_OnmN{JTlq^vZcjozA&W+B_xp0E! zC3d=B2q&lEVb{FgxlD~qoI2nPWODShycF~@y?!y4BjlFKCsp379c5iPWOqi2RTo6DDLz6r4!X33!$ zw;6#g3V3;VQ3OH8`E%B~>*G@}j$`5Z5W)Rb4Y@S95=+f>@_C5EV~3uuzsJZg&!FNh zKytTROxPGFogrVLkqNn55|?%+a~bb?eVCe=iOp!@Z~Slz*tr=f<$s`0-4P`q?WVs= zT55R)g_?ypNaTY!LIf2N`}8iu{_HOJ z{BZ9%09W2#WR=w-QP4^{jC5#;IpcwNBt!zM?{YdZ+50&hS-F@W89<_70$xNDq`Pp6 zCFzW4H^x0+-Glg5m6Xb-YGjv>UOmm~vZrya$w3>)bX!K=Nk zw<+qy{|zC2`n9Kh+{i}W#S9lL^{_r554$xyA3;zO?0bslVyh;H;tJNluR@A=mPQc# zxvOnVsQARxXy|Y45OEobx36`dzXdCT8eF$$8>xx;@UmfFy6$``WcjraG-5iD>QdS$ z52}l0M$qzxoEFQn1jWPQoeN-8*%HPLld4xSO^)@Ogz=B5Mz`p09z=P@6x?j>ZxSV) zL&@Zr81~^8-+fr%i=u5se(#oO}LI(Jlb`v_-$r)MTOHNZF_}| zNOSTYCfXxWeSJ1K^I<%4NaX9*zUsPzl8UpGd=zT|!(|~@(_mNIB^%QA2^uJ_tg9le zS7|m&N!4jji}}a^_K~0W(O`G`*RYSo2^!dU_;{Y=NIOBE_(5uB7wzU8YRRM1A=iWK z@i;klzmap*PuAd@f$p}ONF2$$BT|CUH|j=gP$cJzY3k7XA1ImsAja?hSpWb407*qo IM6N<$g1zXPB>(^b diff --git a/javascript-practice/src/assets/images/icon.webp b/javascript-practice/src/assets/images/icon.webp new file mode 100644 index 0000000000000000000000000000000000000000..a9d84343ee4e6dd983ab1701e7fcd4776eb902c6 GIT binary patch literal 690 zcmV;j0!{r=Nk&Gh0ssJ4MM6+kP&il$0000G0000d001Ze06|PpNE87800DrbZIdFU zb{F^5?G!v@D=9>ly1P5vKu_S7S>`SWfV~6m4qZWO)d?bE0>;=P7erucmC?F<`TYBP zf3@D1O;Nn}frejug|7Vj0&S1;T|~A0p+nIVWfil8j%Q*mBG=IQRg9W%9LO2BsDOIRl&F$N&GA@M=dE zm|GDdjxohCwpLI$AaDTy01ycPodGH+04V@IjW(D^r6QuCG1-~`pb?2^Cjb*;CE_@W z*1$bs^x2NUS0|*5QIQxNvYDmbuYI5D)KTuhW{7-o)AzZb*vrXiBr5GkZ~y@Q{HYIk zHnBD4vrH`?z`0G6+JotLZ5_9@72VcgRy{|##Bf2w_ijU|$BECXGzF6=#TXL|F*gNB z&L%xF5B&NjxV8cq26UVz-K^X-4)BNiQXh_n>k%(5YShTM6+N>HP7NKmXjv!Z#&Q+} zH>QIi#=|S-Wsc@3Xpga9#rCm^wZ7?}ATRObi8rn)qq$YX8_KAnx*N5Xie`qTC$RN^ zou;y4XP;)$SqR;sG8oYt$h*KMIsZnQFIVFA9(oR{glt%mBcs21!nSOe{6BiaTGfC2 z{rm>Xw9Kq`cgy==0Nouu5&!@I literal 0 HcmV?d00001 diff --git a/javascript-practice/src/js/helpers/dataProcess.js b/javascript-practice/src/js/helpers/dataProcess.js new file mode 100644 index 0000000..a276218 --- /dev/null +++ b/javascript-practice/src/js/helpers/dataProcess.js @@ -0,0 +1,76 @@ +import { changeDateFormat } from './helpers'; + +export const createId = () => { + return new Date().getTime(); +}; + +export const createTransactionDetailObject = (category, transactions) => { + const totalTransaction = transactions.length; + const totalAmount = () => { + let amount = 0; + + transactions.forEach((transaction) => { + amount += transaction.amount; + }); + + return amount; + }; + const listTransaction = () => { + const results = transactions.map((transaction) => { + const dateParts = changeDateFormat(transaction.date).split(','); // ['Monday', '14', 'September', '2023'] + const day = dateParts[1]; + const fullDateString = `${dateParts[0]}, ${dateParts[2]} ${dateParts[3]}`; + const tempData = { + id: transaction.id, + day, + fullDateString, + note: transaction.note, + amount: transaction.amount, + }; + + return tempData; + }); + + results.sort((a, b) => parseInt(b.id, 10) - parseInt(a.id, 10)); + + return results; + }; + + return { + categoryName: category.name, + url: category.url, + totalTransaction, + totalAmount: totalAmount(), + transactions: listTransaction(), + }; +}; + +export const getAllCategoryNameInTransactions = (transactions) => { + const categoryName = new Set(); + + transactions.forEach((transaction) => + categoryName.add(transaction.categoryName), + ); + + return Array.from(categoryName); +}; + +export const getAllTransactionByCategoryName = (categoryName, transactions) => { + const results = transactions.filter((transaction) => { + return transaction.categoryName === categoryName; + }); + + return results; +}; + +export const convertModelToDataObject = (model) => { + const { id, ...data } = model; + + return { id, data }; +}; + +export const convertDataObjectToModel = (data) => { + const { id, ...object } = data; + + return { id, ...object.data }; +}; diff --git a/javascript-practice/src/js/helpers/helpers.js b/javascript-practice/src/js/helpers/helpers.js index 7027169..edc2eed 100644 --- a/javascript-practice/src/js/helpers/helpers.js +++ b/javascript-practice/src/js/helpers/helpers.js @@ -1,24 +1,7 @@ import * as MESSAGE from '../constants/message'; -import { TIME_OUT_SEC, REGEX, DAY, MONTH } from '../constants/config'; +import { TIME_OUT_SEC, DAY, MONTH } from '../constants/config'; import FirebaseService from '../services/firebaseService'; -/** - * Validate password - * @param {string} password Password input - * @returns {boolean} Return true if validate password success, otherwise return false - */ -export const isValidPassword = (password) => { - return REGEX.PASSWORD.test(password); -}; - -export const isValidateEmail = (email) => { - return String(email).toLowerCase().match(REGEX.EMAIL); -}; - -export const compare2Password = (password, passwordConfirm) => { - return password === passwordConfirm; -}; - /** * A waiting function with s second * @param {number} s The time will be waiting @@ -59,18 +42,6 @@ export const createToken = () => { return token; }; -export const convertModelToDataObject = (model) => { - const { id, ...data } = model; - - return { id, data }; -}; - -export const convertDataObjectToModel = (data) => { - const { id, ...object } = data; - - return { id, ...object.data }; -}; - export const getSubdirectoryURL = () => { const url = window.location.href; const parts = url.split('/'); // Results: ['http:', '', 'example.com', ''] @@ -101,73 +72,6 @@ export const changeDateFormat = (oldFormatDate) => { return `${day}, ${date}, ${month}, ${year}`; }; -export const createTransactionDetailObject = (category, transactions) => { - const totalTransaction = transactions.length; - const totalAmount = () => { - let amount = 0; - - transactions.forEach((transaction) => { - amount += transaction.amount; - }); - - return amount; - }; - const listTransaction = () => { - const results = transactions.map((transaction) => { - const dateParts = changeDateFormat(transaction.date).split(','); // ['Monday', '14', 'September', '2023'] - const day = dateParts[1]; - const fullDateString = `${dateParts[0]}, ${dateParts[2]} ${dateParts[3]}`; - const tempData = { - id: transaction.id, - day, - fullDateString, - note: transaction.note, - amount: transaction.amount, - }; - - return tempData; - }); - - results.sort((a, b) => parseInt(b.id, 10) - parseInt(a.id, 10)); - - return results; - }; - - return { - categoryName: category.name, - url: category.url, - totalTransaction, - totalAmount: totalAmount(), - transactions: listTransaction(), - }; -}; - -export const getAllCategoryNameInTransactions = (transactions) => { - const categoryName = new Set(); - - transactions.forEach((transaction) => - categoryName.add(transaction.categoryName), - ); - - return Array.from(categoryName); -}; - -export const getAllTransactionByCategoryName = (categoryName, transactions) => { - const results = transactions.filter((transaction) => { - return transaction.categoryName === categoryName; - }); - - return results; -}; - -export const renderRequiredText = (field, element) => { - const markup = ` -

${MESSAGE.REQUIRED_MESSAGE(field)}

- `; - - element.insertAdjacentHTML('afterend', markup); -}; - export const redirectToLoginPage = () => { window.location.replace('/login'); }; diff --git a/javascript-practice/src/js/helpers/validateForm.js b/javascript-practice/src/js/helpers/validateForm.js new file mode 100644 index 0000000..bf1a4f4 --- /dev/null +++ b/javascript-practice/src/js/helpers/validateForm.js @@ -0,0 +1,27 @@ +import { REGEX } from '../constants/config'; +import * as MESSAGE from '../constants/message'; + +/** + * Validate password + * @param {string} password Password input + * @returns {boolean} Return true if validate password success, otherwise return false + */ +export const isValidPassword = (password) => { + return REGEX.PASSWORD.test(password); +}; + +export const isValidateEmail = (email) => { + return String(email).toLowerCase().match(REGEX.EMAIL); +}; + +export const compare2Password = (password, passwordConfirm) => { + return password === passwordConfirm; +}; + +export const renderRequiredText = (field, element) => { + const markup = ` +

${MESSAGE.REQUIRED_MESSAGE(field)}

+ `; + + element.insertAdjacentHTML('afterend', markup); +}; diff --git a/javascript-practice/src/js/models/transaction.js b/javascript-practice/src/js/models/transaction.js index c6ff86f..9da4900 100644 --- a/javascript-practice/src/js/models/transaction.js +++ b/javascript-practice/src/js/models/transaction.js @@ -1,21 +1,12 @@ +import { createId } from '../helpers/dataProcess'; + export default class Transaction { - constructor({ - id = '', - categoryName = '', - date = '', - note = '', - amount = 0, - idUser, - }) { - this.id = id === '' ? Transaction.createIdTransactions() : id; + constructor({ id, categoryName, date, note, amount = 0, idUser }) { + this.id = !id ? createId() : id; this.categoryName = categoryName; this.date = date; this.note = note; this.amount = amount; this.idUser = idUser; } - - static createIdTransactions() { - return new Date().getTime(); - } } diff --git a/javascript-practice/src/js/models/user.js b/javascript-practice/src/js/models/user.js index b3ed5e7..f8005c6 100644 --- a/javascript-practice/src/js/models/user.js +++ b/javascript-practice/src/js/models/user.js @@ -1,12 +1,10 @@ +import { createId } from '../helpers/dataProcess'; + export default class User { constructor({ email, password, accessToken = '' }) { - this.id = User.createIdUser(); + this.id = createId(); this.email = email; this.password = password; this.accessToken = accessToken; } - - static createIdUser() { - return new Date().getTime(); - } } diff --git a/javascript-practice/src/js/models/wallet.js b/javascript-practice/src/js/models/wallet.js index 44d5a6e..3ab711d 100644 --- a/javascript-practice/src/js/models/wallet.js +++ b/javascript-practice/src/js/models/wallet.js @@ -1,13 +1,15 @@ +import { createId } from '../helpers/dataProcess'; + export default class Wallet { - constructor({ walletName = '', inflow = 0, outflow = 0, idUser }) { - this.id = Wallet.createIdWallet(); + constructor({ walletName, inflow = 0, outflow = 0, idUser }) { + this.id = createId(); this.walletName = walletName; this.inflow = inflow; this.outflow = outflow; this.idUser = idUser; } - static createIdWallet() { - return new Date().getTime(); + amountWallet() { + return +this.inflow + +this.outflow; } } diff --git a/javascript-practice/src/js/services/commonService.js b/javascript-practice/src/js/services/commonService.js index eb3d611..bb9648d 100644 --- a/javascript-practice/src/js/services/commonService.js +++ b/javascript-practice/src/js/services/commonService.js @@ -1,8 +1,8 @@ +import { timeOutConnect } from '../helpers/helpers'; import { convertDataObjectToModel, convertModelToDataObject, - timeOutConnect, -} from '../helpers/helpers'; +} from '../helpers/dataProcess'; import FirebaseService from './firebaseService'; export default class CommonService { diff --git a/javascript-practice/src/js/services/walletService.js b/javascript-practice/src/js/services/walletService.js index 7092e11..cb40e4a 100644 --- a/javascript-practice/src/js/services/walletService.js +++ b/javascript-practice/src/js/services/walletService.js @@ -1,3 +1,4 @@ +import Wallet from '../models/wallet'; import CommonService from './commonService'; export default class WalletService extends CommonService { @@ -34,6 +35,6 @@ export default class WalletService extends CommonService { async getWalletByIdUser(idUser) { const result = await this.getDataFromProp('idUser', idUser); - return result || null; + return new Wallet(result) || null; } } diff --git a/javascript-practice/src/js/views/authenticationView.js b/javascript-practice/src/js/views/authenticationView.js index 29a3fbc..105a690 100644 --- a/javascript-practice/src/js/views/authenticationView.js +++ b/javascript-practice/src/js/views/authenticationView.js @@ -5,7 +5,7 @@ import { isValidPassword, isValidateEmail, renderRequiredText, -} from '../helpers/helpers'; +} from '../helpers/validateForm'; export default class AuthenticationView extends CommonView { constructor() { diff --git a/javascript-practice/src/js/views/homeView.js b/javascript-practice/src/js/views/homeView.js index 618c17e..8e2302b 100644 --- a/javascript-practice/src/js/views/homeView.js +++ b/javascript-practice/src/js/views/homeView.js @@ -10,19 +10,25 @@ import * as MESSAGE from '../constants/message'; import CommonView from './commonView'; import Wallet from '../models/wallet'; import Transaction from '../models/transaction'; +import { formatNumber } from '../helpers/helpers'; +import { renderRequiredText } from '../helpers/validateForm'; import { createTransactionDetailObject, - formatNumber, getAllCategoryNameInTransactions, getAllTransactionByCategoryName, - renderRequiredText, -} from '../helpers/helpers'; +} from '../helpers/dataProcess'; import defaultCategoryIcon from '../../assets/images/question-icon.svg'; +import TransactionView from './transactionView'; export default class HomeView extends CommonView { constructor() { super(); + this.transactionView = new TransactionView(); + this.transactionView.initTransitionChange( + this.onTransactionViewChange.bind(this), + ); + this.tabs = document.querySelectorAll('.app__tab-item'); this.allContent = document.querySelectorAll('.app__content-item'); this.cancelBtns = document.querySelectorAll('.form__cancel-btn'); @@ -64,6 +70,10 @@ export default class HomeView extends CommonView { this.deleteTransaction = deleteTransaction; } + onTransactionViewChange() { + // TODO Load budget data + } + async loadPage() { // Init function @@ -111,7 +121,7 @@ export default class HomeView extends CommonView { const walletName = document.querySelector('.wallet__name'); const walletPrice = document.querySelector('.wallet__price'); const walletNameValue = wallet.walletName; - const walletAmountValue = wallet.inflow + wallet.outflow; + const walletAmountValue = wallet.amountWallet(); const sign = walletAmountValue >= 0 ? '+' : '-'; diff --git a/javascript-practice/src/js/views/transactionView.js b/javascript-practice/src/js/views/transactionView.js new file mode 100644 index 0000000..7057b59 --- /dev/null +++ b/javascript-practice/src/js/views/transactionView.js @@ -0,0 +1,19 @@ +export default class TransactionView { + constructor() {} + + onTransitionChange() { + this.onTransitionChangeHandler(); + } + + initTransitionChange(handler) { + this.onTransitionChangeHandler = handler; + } + + submitTransaction() { + const transactionDialog = document.getElementById('transactionDialog'); + + const saveBtn = transactionDialog.querySelector('.form__save-btn'); + + saveBtn.addEventListener('click', this.onTransitionChange); + } +} diff --git a/javascript-practice/src/pages/index.html b/javascript-practice/src/pages/index.html index 67e906f..2ddb89a 100644 --- a/javascript-practice/src/pages/index.html +++ b/javascript-practice/src/pages/index.html @@ -5,6 +5,7 @@ + Money Lover Web @@ -20,7 +21,7 @@
-

Wallet name

+
-

Currency

+
-

Initial Balance

+
- +
@@ -71,20 +74,20 @@
-

Date

+
-
+
-

Amount

+
-

Note

+
- +
@@ -127,7 +132,7 @@
-

Date

+
-

Category

+
-
+
-

Amount

+
-

Note

+
- +
@@ -222,7 +231,7 @@
- Wallet Icon + Wallet Icon

User

diff --git a/javascript-practice/src/pages/login.html b/javascript-practice/src/pages/login.html index bf48433..53cb035 100644 --- a/javascript-practice/src/pages/login.html +++ b/javascript-practice/src/pages/login.html @@ -5,6 +5,7 @@ + Login - Money Lover diff --git a/javascript-practice/src/pages/register.html b/javascript-practice/src/pages/register.html index 79ac8d5..c12b343 100644 --- a/javascript-practice/src/pages/register.html +++ b/javascript-practice/src/pages/register.html @@ -5,6 +5,7 @@ + Register - Money Lover diff --git a/javascript-practice/src/styles/abstracts/_variables.scss b/javascript-practice/src/styles/abstracts/_variables.scss index 322974e..a73b15f 100644 --- a/javascript-practice/src/styles/abstracts/_variables.scss +++ b/javascript-practice/src/styles/abstracts/_variables.scss @@ -6,8 +6,6 @@ $light-primary-color: #00bc2a; $primary-color: #2db84c; $dark-primary-color: #00710f; $primary-text-color: #000; -$secondary-text-color: #0000008a; -$secondary-light-text-color: #00000042; $amount-outcome-color: #e51c23; $amount-income-color: #039be5; $background-input-color: #f5f5f5; @@ -15,10 +13,9 @@ $dark-error-color: #ce1414; $light-error-color: #ffc3c3; $light-gray: #f4f4f4; $gray: #dddddd; -$dark-gray: #00000042; $dark-x-gray: #0000008a; -$white: #ffffff; -$black: #000000; +$white: #fff; +$black: #000; // Font size $fs-2x-sm: 11px; diff --git a/javascript-practice/src/styles/pages/_index.scss b/javascript-practice/src/styles/pages/_index.scss index 3e3b2f8..f31d251 100644 --- a/javascript-practice/src/styles/pages/_index.scss +++ b/javascript-practice/src/styles/pages/_index.scss @@ -108,6 +108,8 @@ background-color: $primary-color; border-radius: 10px; transition: all 0.3s ease-in-out; + width: 116px; + left: 152px; } } @@ -245,7 +247,7 @@ &__note { @extend .transaction__date-time; - color: $dark-gray; + color: $black; } &__outcome { @@ -506,13 +508,8 @@ line-height: 16.1px; font-weight: 500; padding: 10px 20px; - color: $dark-gray; + color: $black; cursor: pointer; - - &.active { - background-color: $primary-color; - color: $white; - } } &__cancel-btn { @@ -573,14 +570,15 @@ top: 10px; right: 5px; background-color: transparent; - border-top: 3px solid $dark-gray; - border-right: 3px solid $dark-gray; + border-top: 3px solid $black; + border-right: 3px solid $black; border-bottom: none; border-left: none; height: 7px; width: 7px; rotate: 45deg; pointer-events: none; + opacity: 0.56; } .category-input-container { @@ -666,6 +664,22 @@ @extend %d-flex; @include flex-layout($direction: column, $gap: 7px); } + + .form__save-btn { + .text { + color: $black; + opacity: 0.54; + } + + &.active { + background-color: $primary-color; + + & .text { + color: $white; + opacity: 1; + } + } + } } .close-icon { diff --git a/javascript-practice/src/styles/pages/_login-register.scss b/javascript-practice/src/styles/pages/_login-register.scss index 3171f3d..1aef124 100644 --- a/javascript-practice/src/styles/pages/_login-register.scss +++ b/javascript-practice/src/styles/pages/_login-register.scss @@ -57,7 +57,8 @@ &__description { margin-top: 15px; - color: $secondary-text-color; + color: $black; + opacity: 0.54; font-size: $fs-md; line-height: 24px; } From 600ec03c9e68f9206d1bc2969dc2f4df16e9f059 Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Wed, 20 Sep 2023 17:48:47 +0700 Subject: [PATCH 2/2] Refactor code --- .../src/js/controllers/homeController.js | 10 +- .../src/js/helpers/transform.js | 26 + .../src/js/services/walletService.js | 6 +- .../src/js/views/budgetView.js | 174 ++++ .../src/js/views/categoryView.js | 136 +++ javascript-practice/src/js/views/homeView.js | 921 +++--------------- .../src/js/views/summaryTabView.js | 37 + .../src/js/views/transactionTabView.js | 169 ++++ .../src/js/views/transactionView.js | 313 +++++- .../src/js/views/walletView.js | 180 ++++ 10 files changed, 1154 insertions(+), 818 deletions(-) create mode 100644 javascript-practice/src/js/helpers/transform.js create mode 100644 javascript-practice/src/js/views/budgetView.js create mode 100644 javascript-practice/src/js/views/categoryView.js create mode 100644 javascript-practice/src/js/views/summaryTabView.js create mode 100644 javascript-practice/src/js/views/transactionTabView.js create mode 100644 javascript-practice/src/js/views/walletView.js diff --git a/javascript-practice/src/js/controllers/homeController.js b/javascript-practice/src/js/controllers/homeController.js index e08905c..0740429 100644 --- a/javascript-practice/src/js/controllers/homeController.js +++ b/javascript-practice/src/js/controllers/homeController.js @@ -1,3 +1,4 @@ +import Transform from '../helpers/transform'; import UserService from '../services/userService'; export default class HomeController { @@ -49,16 +50,13 @@ export default class HomeController { this.handlerSaveTransaction.bind(this), HomeController.handlerClearAccessToken.bind(this), this.handlerDeleteTransaction.bind(this), + new Transform(), ); - this.homeView.addHandlerEventWalletForm(); this.homeView.loadPage(); - this.homeView.addHandlerEventBudgetForm(); - - this.homeView.handlerEventCategoryDialog(); - - this.homeView.handlerEventTransactionDialog(); + // Subscribe listener data update + this.homeView.subscribeListenerData(); } } } diff --git a/javascript-practice/src/js/helpers/transform.js b/javascript-practice/src/js/helpers/transform.js new file mode 100644 index 0000000..f989c48 --- /dev/null +++ b/javascript-practice/src/js/helpers/transform.js @@ -0,0 +1,26 @@ +export default class Transform { + constructor() { + this.signal = {}; + } + + onSendSignal(fromClass, value) { + Object.keys(this.signal).forEach((key) => { + const item = this.signal[key]; + + // Same receiver + if (item.name === fromClass) { + return; + } + + // Send signal + const { handler } = this.signal[key]; + if (handler) { + handler(value); + } + }); + } + + create(className, handler = null) { + this.signal[className] = { name: className, handler }; + } +} diff --git a/javascript-practice/src/js/services/walletService.js b/javascript-practice/src/js/services/walletService.js index cb40e4a..9e19b7a 100644 --- a/javascript-practice/src/js/services/walletService.js +++ b/javascript-practice/src/js/services/walletService.js @@ -35,6 +35,10 @@ export default class WalletService extends CommonService { async getWalletByIdUser(idUser) { const result = await this.getDataFromProp('idUser', idUser); - return new Wallet(result) || null; + if (result) { + return new Wallet(result); + } + + return null; } } diff --git a/javascript-practice/src/js/views/budgetView.js b/javascript-practice/src/js/views/budgetView.js new file mode 100644 index 0000000..20140fe --- /dev/null +++ b/javascript-practice/src/js/views/budgetView.js @@ -0,0 +1,174 @@ +import { DEFAULT_CATEGORY } from '../constants/config'; +import Transaction from '../models/transaction'; +import * as MESSAGE from '../constants/message'; +import { renderRequiredText } from '../helpers/validateForm'; + +class BudgetView { + constructor() { + this.budgetDialog = document.getElementById('budgetDialog'); + this.addBudgetBtn = document.getElementById('addBudget'); + + this.handlerEventBudgetView(); + } + + initFunction( + showErrorToast, + showSuccessToast, + toggleLoaderSpinner, + saveTransaction, + loadTransactionData, + updateAmountWallet, + loadData, + transform, + ) { + this.showErrorToast = showErrorToast; + this.showSuccessToast = showSuccessToast; + this.toggleLoaderSpinner = toggleLoaderSpinner; + this.saveTransaction = saveTransaction; + this.loadTransactionData = loadTransactionData; + this.updateAmountWallet = updateAmountWallet; + this.loadData = loadData; + this.transform = transform; + } + + subscribe() { + this.transform.create('budgetView', this.updateData.bind(this)); + } + + sendData() { + const data = { wallet: this.wallet, listTransaction: this.listTransaction }; + + this.transform.onSendSignal('budgetView', data); + } + + updateData(data) { + if (data.wallet) this.wallet = data.wallet; + + if (data.listTransactions) this.listTransaction = data.listTransactions; + + if (data.listCategory) this.listCategory = data.listCategory; + } + + addHandlerEventBudgetForm() { + this.budgetDialog.addEventListener('submit', (e) => { + e.preventDefault(); + + this.clearErrorStyleBudgetForm(); + this.submitBudgetForm(); + }); + + this.budgetDialog.addEventListener('input', () => { + this.changeBtnStyle(); + this.clearErrorStyleBudgetForm(); + }); + } + + clearErrorStyleBudgetForm() { + // Clear error style + const inputFieldEls = + this.budgetDialog.querySelectorAll('.form__input-field'); + const errorTexts = this.budgetDialog.querySelectorAll('.error-text'); + + inputFieldEls.forEach((item) => { + if (item.classList.contains('error-input')) + item.classList.remove('error-input'); + }); + + errorTexts.forEach((item) => { + if (item) item.remove(); + }); + } + + async submitBudgetForm() { + try { + const { formAddBudget } = document.forms; + const form = new FormData(formAddBudget); + const date = form.get('selected_date'); + const amount = form.get('amount'); + const note = form.get('note'); + + // Validate data user input + + if (this.validateBudgetForm(date, amount)) { + this.toggleLoaderSpinner(); // Enable loader spinner + this.budgetDialog.close(); // Close dialog + + const transaction = new Transaction({ + categoryName: DEFAULT_CATEGORY.INCOME, + date, + amount: +amount, + note, + idUser: this.wallet.idUser, + }); + + await this.saveTransaction(transaction); + + // Reload data + await this.loadTransactionData(); + await this.updateAmountWallet(); + await this.loadData(); + + // Hide loader spinner + this.toggleLoaderSpinner(); + + // Show success message + this.showSuccessToast( + MESSAGE.ADD_TRANSACTION_SUCCESS, + MESSAGE.DEFAULT_MESSAGE, + ); + + document.getElementById('formAddBudget').reset(); + } + } catch (error) { + this.showErrorToast(error); + } + } + + validateBudgetForm(date, amount) { + const inputFieldEl = + this.budgetDialog.querySelectorAll('.form__input-field'); + + if (!date || !amount) { + if (!date) { + renderRequiredText('date', inputFieldEl[0]); + inputFieldEl[0].classList.add('error-input'); + } + + if (!amount) { + renderRequiredText('amount', inputFieldEl[1]); + inputFieldEl[1].classList.add('error-input'); + } + + return false; + } + + return true; + } + + changeBtnStyle() { + const date = this.budgetDialog.querySelector('.input-date').value; + const amount = +this.budgetDialog.querySelector('.form__input-balance') + .value; + const saveBtn = this.budgetDialog.querySelector('.form__save-btn'); + + if (date.trim() && amount >= 1) { + saveBtn.classList.add('active'); + } else { + saveBtn.classList.remove('active'); + } + } + + handlerEventBudgetView() { + this.addBudgetBtn.addEventListener('click', () => { + // Set default value for date input + this.budgetDialog.querySelector("[name='selected_date']").valueAsDate = + new Date(); + + this.budgetDialog.showModal(); + }); + + this.addHandlerEventBudgetForm(); + } +} + +export default new BudgetView(); diff --git a/javascript-practice/src/js/views/categoryView.js b/javascript-practice/src/js/views/categoryView.js new file mode 100644 index 0000000..b0e7161 --- /dev/null +++ b/javascript-practice/src/js/views/categoryView.js @@ -0,0 +1,136 @@ +import { REMOVE_CATEGORY } from '../constants/config'; + +class CategoryView { + constructor() { + this.categoryDialog = document.getElementById('categoryDialog'); + this.categoryField = document.getElementById('selectCategory'); + this.closeIcon = document.querySelector('.close-icon'); + + this.handlerEventCategoryDialog(); + this.addEventSelectCategoryDialog(); + } + + initFunction(getAllCategory, transform) { + this.getAllCategory = getAllCategory; + this.transform = transform; + } + + sendData() { + const data = { listCategory: this.listCategory }; + + this.transform.onSendSignal('categoryView', data); + } + + handlerEventCategoryDialog() { + this.categoryDialog.addEventListener('input', () => { + setTimeout(() => { + this.searchCategory(); + }, 300); + }); + } + + /** + * Load category data + * @param {function} getAllCategory Get all category function + */ + async loadCategory() { + if (!this.listCategory) { + this.listCategory = await this.getAllCategory(); + + this.sendData(); + } + + if (this.listCategory) { + this.renderCategoryList(); + } + } + + searchCategory() { + const searchCategoryEl = + this.categoryDialog.querySelector("[name='category']"); + const searchValue = searchCategoryEl.value.trim().toLowerCase(); + let newListCategory = []; + + if (searchValue) { + this.listCategory.forEach((category) => { + const categoryName = category.name.trim().toLowerCase(); + + if (categoryName.includes(searchValue)) { + newListCategory.unshift(category); + } + }); + } else { + newListCategory = this.listCategory; + } + + // Render category item + this.renderCategoryList(this.keySearchCategory, newListCategory); + } + + renderCategoryList(categorySelected, listCategory = this.listCategory) { + // Remove category name unnecessary + const newListCategory = listCategory.filter( + (item) => !REMOVE_CATEGORY.includes(item.name), + ); + + const listCategoryEl = document.querySelector('.list-category'); + + listCategoryEl.innerHTML = ''; // Remove old category item + + newListCategory.forEach((category) => { + const markup = ` +
+ ${category.name} Icon +

${category.name}

+
+ `; + + listCategoryEl.insertAdjacentHTML('afterbegin', markup); + }); + } + + addEventSelectCategoryDialog() { + const categoryListEl = document.querySelector('.list-category'); + const categoryIconEl = this.categoryField.querySelector('.category-icon'); + const categoryNameEl = this.categoryField.querySelector('.category-name'); + + categoryListEl.addEventListener('click', (e) => { + const categoryItem = e.target.closest('.category-item'); + + if (categoryItem) { + const { url } = categoryItem.dataset; + const { value } = categoryItem.dataset; + + // Set url and value into category field in transaction dialog + categoryIconEl.src = url; + categoryNameEl.value = value; + + // Close select category dialog + this.categoryDialog.close(); + } + }); + + // Pass value selected to category dialog + categoryNameEl.addEventListener('click', () => { + if (categoryNameEl.value) { + // Make the keyword search category name into global + this.keySearchCategory = categoryNameEl.value; + + this.renderCategoryList(this.keySearchCategory); + } + this.categoryDialog.showModal(); + }); + + this.closeIcon.addEventListener('click', () => { + this.categoryDialog.close(); + }); + } +} + +export default new CategoryView(); diff --git a/javascript-practice/src/js/views/homeView.js b/javascript-practice/src/js/views/homeView.js index 8e2302b..450b8d9 100644 --- a/javascript-practice/src/js/views/homeView.js +++ b/javascript-practice/src/js/views/homeView.js @@ -1,53 +1,29 @@ /* eslint-disable class-methods-use-this */ -import { - TYPE_TOAST, - BTN_CONTENT, - DEFAULT_CATEGORY, - FIRST_ADD_WALLET_NOTE, - REMOVE_CATEGORY, -} from '../constants/config'; +import { TYPE_TOAST, BTN_CONTENT } from '../constants/config'; import * as MESSAGE from '../constants/message'; import CommonView from './commonView'; -import Wallet from '../models/wallet'; -import Transaction from '../models/transaction'; import { formatNumber } from '../helpers/helpers'; -import { renderRequiredText } from '../helpers/validateForm'; -import { - createTransactionDetailObject, - getAllCategoryNameInTransactions, - getAllTransactionByCategoryName, -} from '../helpers/dataProcess'; -import defaultCategoryIcon from '../../assets/images/question-icon.svg'; + +import WalletView from './walletView'; import TransactionView from './transactionView'; +import CategoryView from './categoryView'; +import BudgetView from './budgetView'; +import SummaryTabView from './summaryTabView'; +import TransactionTabView from './transactionTabView'; export default class HomeView extends CommonView { constructor() { super(); - this.transactionView = new TransactionView(); - this.transactionView.initTransitionChange( - this.onTransactionViewChange.bind(this), - ); - this.tabs = document.querySelectorAll('.app__tab-item'); this.allContent = document.querySelectorAll('.app__content-item'); this.cancelBtns = document.querySelectorAll('.form__cancel-btn'); this.saveBtns = document.querySelectorAll('.form__save-btn'); this.dialogs = document.querySelectorAll('.dialog'); - this.closeIcon = document.querySelector('.close-icon'); - - this.categoryField = document.getElementById('selectCategory'); - this.addBudgetBtn = document.getElementById('addBudget'); - this.addTransactionBtn = document.getElementById('addTransaction'); this.amountInputs = document.querySelectorAll('.form__input-balance'); - this.budgetDialog = document.getElementById('budgetDialog'); this.transactionDialog = document.getElementById('transactionDialog'); - this.categoryDialog = document.getElementById('categoryDialog'); - this.walletDialog = document.getElementById('walletDialog'); - - this.transactionForm = document.getElementById('formAddTransaction'); } initFunction( @@ -59,6 +35,7 @@ export default class HomeView extends CommonView { saveTransaction, clearAccessToken, deleteTransaction, + transform, ) { this.getInfoUserLogin = getInfoUserLogin; this.getWalletByIdUser = getWalletByIdUser; @@ -68,15 +45,97 @@ export default class HomeView extends CommonView { this.saveTransaction = saveTransaction; this.clearAccessToken = clearAccessToken; this.deleteTransaction = deleteTransaction; + + this.transform = transform; + + this.initTransactionViewFunction(); + this.initCategoryViewFunction(); + this.initBudgetViewFunction(); + this.initSummaryTabViewFunction(); + this.initTransactionTabViewFunction(); + this.initWalletViewFunction(); } - onTransactionViewChange() { - // TODO Load budget data + initCategoryViewFunction() { + CategoryView.initFunction(this.getAllCategory, this.transform); + } + + initTransactionViewFunction() { + TransactionView.initFunction( + this.toggleLoaderSpinner.bind(this), + this.deleteTransaction, + this.loadTransactionData.bind(this), + this.updateAmountWallet.bind(this), + this.loadData.bind(this), + this.showSuccessToast.bind(this), + this.showErrorToast.bind(this), + this.saveTransaction, + this.transform, + ); + } + + initBudgetViewFunction() { + BudgetView.initFunction( + this.showErrorToast.bind(this), + this.showSuccessToast.bind(this), + this.toggleLoaderSpinner.bind(this), + this.saveTransaction.bind(this), + this.loadTransactionData.bind(this), + this.updateAmountWallet.bind(this), + this.loadData.bind(this), + this.transform, + ); + } + + initFunctionCategoryView() { + CategoryView.initFunction(this.getAllCategory, this.transform); + } + + initSummaryTabViewFunction() { + SummaryTabView.initFunction(this.transform); + } + + initTransactionTabViewFunction() { + TransactionTabView.initFunction(this.transform); + } + + initWalletViewFunction() { + WalletView.initFunction( + this.transform, + this.toggleLoaderSpinner.bind(this), + this.saveWallet, + this.saveTransaction, + this.loadTransactionData.bind(this), + this.loadData.bind(this), + this.loadEvent.bind(this), + this.showSuccessToast.bind(this), + this.showErrorToast.bind(this), + ); + } + + subscribeListenerData() { + this.subscribe(); + TransactionView.subscribe(); + BudgetView.subscribe(); + SummaryTabView.subscribe(); + TransactionTabView.subscribe(); + WalletView.subscribe(); + } + + async loadData() { + // Send data to other class + this.sendData(); + + await CategoryView.loadCategory(); + + await this.loadWalletUser(); + + SummaryTabView.load(); + + TransactionTabView.loadTransactionTab(); } async loadPage() { - // Init function - this.toggleLoaderSpinner(); const user = await this.getInfoUserLogin(); @@ -87,10 +146,11 @@ export default class HomeView extends CommonView { this.user = user; this.wallet = await this.getWalletByIdUser(user.id); + this.sendData(); // Check user's wallet if have or not if (!this.wallet) { // Show add wallet dialog - this.walletDialog.showModal(); + WalletView.showDialog(); } else { // Init data await this.loadTransactionData(); @@ -104,13 +164,30 @@ export default class HomeView extends CommonView { this.toggleLoaderSpinner(); } - async loadData() { - await this.loadWalletUser(); - this.loadSummaryTab(); - await this.loadTransactionTab(); + // ---------------------LOAD DATA---------------------// + + subscribe() { + this.transform.create('homeView', this.updateData.bind(this)); + } + + sendData() { + const data = { + wallet: this.wallet, + listTransactions: this.listTransactions, + user: this.user, + }; + + this.transform.onSendSignal('homeView', data); + } + + updateData(data) { + if (data.wallet) this.wallet = data.wallet; + + if (data.listTransactions) this.listTransaction = data.listTransactions; + + if (data.listCategory) this.listCategory = data.listCategory; } - // ---------------------LOAD DATA---------------------// /** * Load wallet user */ @@ -141,7 +218,7 @@ export default class HomeView extends CommonView { let outflow = 0; // Init data first - this.transactionDetails = this.loadTransactionDetailsData(); + this.transactionDetails = TransactionTabView.loadTransactionDetailsData(); this.transactionDetails.forEach((transaction) => { if (transaction.totalAmount >= 0) { @@ -158,679 +235,14 @@ export default class HomeView extends CommonView { await this.saveWallet(this.wallet); } - /** - * Load category data - * @param {function} getAllCategory Get all category function - */ - async loadCategory() { - if (!this.listCategory) { - this.listCategory = await this.getAllCategory(); - } - - if (this.listCategory) { - this.renderCategoryList(); - } - } - - loadSummaryTab() { - const inflowValue = document.querySelector('.inflow__text--income'); - const outflowValue = document.querySelector('.outflow__text--outcome'); - const totalValue = document.querySelector('.summary__total'); - - const { inflow } = this.wallet; - const { outflow } = this.wallet; - const total = inflow + outflow; - - inflowValue.textContent = `+$ ${formatNumber(inflow)}`; - outflowValue.textContent = `-$ ${formatNumber(Math.abs(outflow))}`; - totalValue.textContent = `${total >= 0 ? '+' : '-'}$ ${formatNumber( - Math.abs(total), - )}`; - } - async loadTransactionData() { this.listTransactions = await this.getAllTransactions(this.wallet.idUser); - } - async loadTransactionTab() { - // Load category - await this.loadCategory(); - - // Init data first - this.transactionDetails = this.loadTransactionDetailsData(); - - const transactionEl = document.querySelector('.transaction'); - const listTransactionDetailEl = - transactionEl.querySelector('.transaction__list'); - - const markup = []; - - if (this.transactionDetails) { - this.transactionDetails.forEach((transactionDetail) => { - markup.push(this.transactionDetailMarkup(transactionDetail)); - }); - } - - listTransactionDetailEl.innerHTML = ''; - listTransactionDetailEl.insertAdjacentHTML('afterbegin', markup.join('\n')); - - // Load event - this.addEventTransactionItem(); - } - - loadTransactionDetailsData() { - // Get all category user have in transactions - const listCategoryInTransaction = getAllCategoryNameInTransactions( - this.listTransactions, - ); - - // Create transactions details object - const tempList = listCategoryInTransaction.map((categoryName) => { - // Get category object from list category has been loaded. - const category = this.listCategory.filter( - (item) => item.name === categoryName, - ); - - const transactions = getAllTransactionByCategoryName( - categoryName, - this.listTransactions, - ); - - return createTransactionDetailObject( - Object.assign({}, ...category), - transactions, - ); - }); - - tempList.sort( - (a, b) => - parseInt(b.transactions[0].id, 10) - parseInt(a.transactions[0].id, 10), - ); - - return tempList; - } - - transactionDetailMarkup(transactionDetail) { - const transactionTimeMarkup = () => { - const listMarkup = []; - transactionDetail.transactions.forEach((transaction) => { - const markup = ` -
-
-

${transaction.day}

-
-

- ${transaction.fullDateString} -

-

${ - transaction.note === '' ? 'None' : transaction.note - }

-
-
-

${transaction.amount >= 0 ? '+' : '-'}$ ${formatNumber( - Math.abs(transaction.amount), - )}

-
- `; - - listMarkup.push(markup); - }); - - return listMarkup.join('\n'); - }; - - return ` -
-
-
-
- Transportation icon category -
-
-

- ${transactionDetail.categoryName} -

-

${ - transactionDetail.totalTransaction - } Transactions

-
-
-

${ - transactionDetail.totalAmount >= 0 ? '+' : '-' - }$ ${formatNumber(Math.abs(transactionDetail.totalAmount))}

-
-
- - ${transactionTimeMarkup()} -
- `; + this.sendData(); } // ---------------------END---------------------// - // ---------------------TRANSACTION DIALOG---------------------// - handlerEventTransactionDialog() { - this.transactionDialog.addEventListener('submit', (e) => { - e.preventDefault(); - this.clearErrorTransactionDialog(); - this.submitTransactionDialog(); - }); - - this.transactionDialog.addEventListener('input', () => { - this.changeBtnStyleTransactionDialog(); - this.clearErrorTransactionDialog(); - }); - - // Add delete transaction event - this.deleteTransactionEvent(); - } - - clearErrorTransactionDialog() { - // Clear error style - const inputFieldEls = - this.transactionDialog.querySelectorAll('.form__input-field'); - const errorTextEls = this.transactionDialog.querySelectorAll('.error-text'); - - inputFieldEls.forEach((item) => { - if (item.classList.contains('error-input')) - item.classList.remove('error-input'); - }); - errorTextEls.forEach((item) => { - if (item) item.remove(); - }); - } - - deleteTransactionEvent() { - const deleteTransactionBtn = - this.transactionDialog.querySelector('.form__delete-btn'); - - deleteTransactionBtn.addEventListener('click', async () => { - const idEl = this.transactionDialog.querySelector( - "[name='id_transaction']", - ); - - if (idEl.value) { - try { - this.transactionDialog.close(); - this.toggleLoaderSpinner(); - - await this.deleteTransaction(idEl.value); - - // Reload data - await this.loadTransactionData(); - await this.updateAmountWallet(); - await this.loadData(); - - this.showSuccessToast('Delete success!', MESSAGE.DEFAULT_MESSAGE); - } catch (error) { - this.showErrorToast(error); - } - this.toggleLoaderSpinner(); - } - }); - } - - changeBtnStyleTransactionDialog() { - const dateInput = this.transactionDialog.querySelector( - "[name='selected_date']", - ).value; - const categoryName = this.transactionDialog.querySelector( - "[name='category_name']", - ).value; - const amountInput = - this.transactionDialog.querySelector("[name='amount']").value; - const saveBtn = this.transactionDialog.querySelector('.form__save-btn'); - - saveBtn.classList.toggle( - 'active', - dateInput && categoryName && amountInput >= 1, - ); - } - - initValueTransactionDialog(idTransaction) { - let categoryName; - - if (idTransaction) { - // Get transaction object - const transactionArr = this.listTransactions.filter( - (obj) => obj.id === idTransaction, - ); - const transaction = Object.assign({}, ...transactionArr); - // Get category object - const categoryArr = this.listCategory.filter( - (obj) => obj.name === transaction.categoryName, - ); - const category = Object.assign({}, ...categoryArr); - - const idEl = this.transactionDialog.querySelector( - "[name='id_transaction']", - ); - const dateEl = this.transactionDialog.querySelector( - "[name='selected_date']", - ); - const categoryEl = this.transactionDialog.querySelector( - "[name='category_name']", - ); - const amountEl = this.transactionDialog.querySelector("[name='amount']"); - const noteEl = this.transactionDialog.querySelector("[name='note']"); - const iconEl = this.transactionDialog.querySelector('.category-icon'); - - idEl.value = transaction.id; - dateEl.value = transaction.date; - categoryEl.value = transaction.categoryName; - amountEl.value = Math.abs(transaction.amount); - noteEl.value = transaction.note; - iconEl.src = category.url; - - categoryName = categoryEl.value; - } - - // Show delete button only if it is a edit form and not a income transaction - const deleteBtn = this.transactionDialog.querySelector('.form__delete-btn'); - const showDeleteBtn = () => { - return idTransaction && categoryName !== 'Income'; - }; - - deleteBtn.classList.toggle('hide', !showDeleteBtn()); - } - - async submitTransactionDialog() { - try { - const dateEl = this.transactionForm.querySelector( - "[name='selected_date']", - ); - const categoryNameEl = this.transactionForm.querySelector( - "[name='category_name']", - ); - const amountEl = this.transactionForm.querySelector("[name='amount']"); - const noteEl = this.transactionForm.querySelector("[name='note']"); - const idEl = this.transactionForm.querySelector( - "[name='id_transaction']", - ); - const amount = - categoryNameEl.value === 'Income' ? +amountEl.value : -+amountEl.value; // Check if this is a income transaction, amount must plus - - // Validate data user input - if ( - this.validateTransactionForm(dateEl.value, categoryNameEl.value, amount) - ) { - this.toggleLoaderSpinner(); - this.transactionDialog.close(); - - const transaction = new Transaction({ - id: idEl.value, - categoryName: categoryNameEl.value, - date: dateEl.value, - amount, - note: noteEl.value, - idUser: this.wallet.idUser, - }); - - await this.saveTransaction(transaction); - - // Reload data - await this.loadTransactionData(); - this.updateAmountWallet(); - await this.loadData(); - - if (!idEl.value) { - // Add success - this.showSuccessToast( - MESSAGE.ADD_TRANSACTION_SUCCESS, - MESSAGE.DEFAULT_MESSAGE, - ); - } else { - // Update success - this.showSuccessToast( - MESSAGE.UPDATE_TRANSACTION_SUCCESS, - MESSAGE.DEFAULT_MESSAGE, - ); - } - - this.toggleLoaderSpinner(); - this.clearInputTransactionForm(this.transactionForm); - } - } catch (error) { - this.showErrorToast(error); - this.toggleLoaderSpinner(); - } - } - - validateTransactionForm(date, categoryName, amount) { - const inputFieldEls = - this.transactionDialog.querySelectorAll('.form__input-field'); - - if (!date || !categoryName || !amount) { - if (!date) { - renderRequiredText('date', inputFieldEls[0]); - inputFieldEls[0].classList.add('error-input'); - } - - if (!categoryName) { - renderRequiredText('category', inputFieldEls[1]); - inputFieldEls[1].classList.add('error-input'); - } - - if (!amount) { - renderRequiredText('amount', inputFieldEls[2]); - inputFieldEls[2].classList.add('error-input'); - } - - return false; - } - - return true; - } - - clearInputTransactionForm() { - const categoryIcon = this.transactionForm.querySelector('.category-icon'); - - categoryIcon.src = defaultCategoryIcon; - this.keySearchCategory = null; // Delete keyword search - this.renderCategoryList(this.keySearchCategory); - this.transactionForm.reset(); - } - // ---------------------END DIALOG---------------------// - - // ---------------------SELECTED CATEGORY DIALOG---------------------// - handlerEventCategoryDialog() { - this.categoryDialog.addEventListener('input', () => { - setTimeout(() => { - this.searchCategory(); - }, 300); - }); - } - - searchCategory() { - const searchCategoryEl = - this.categoryDialog.querySelector("[name='category']"); - const searchValue = searchCategoryEl.value.trim().toLowerCase(); - let newListCategory = []; - - if (searchValue) { - this.listCategory.forEach((category) => { - const categoryName = category.name.trim().toLowerCase(); - - if (categoryName.includes(searchValue)) { - newListCategory.unshift(category); - } - }); - } else { - newListCategory = this.listCategory; - } - - // Render category item - this.renderCategoryList(this.keySearchCategory, newListCategory); - } - - renderCategoryList(categorySelected, listCategory = this.listCategory) { - // Remove category name unnecessary - const newListCategory = listCategory.filter( - (item) => !REMOVE_CATEGORY.includes(item.name), - ); - - const listCategoryEl = document.querySelector('.list-category'); - - listCategoryEl.innerHTML = ''; // Remove old category item - - newListCategory.forEach((category) => { - const markup = ` -
- ${category.name} Icon -

${category.name}

-
- `; - - listCategoryEl.insertAdjacentHTML('afterbegin', markup); - }); - } - - // ---------------------END DIALOG---------------------// - - // ---------------------ADD BUDGET DIALOG---------------------// - addHandlerEventBudgetForm() { - this.budgetDialog.addEventListener('submit', (e) => { - e.preventDefault(); - - this.clearErrorStyleBudgetForm(); - this.submitBudgetForm(); - }); - - this.budgetDialog.addEventListener('input', (e) => { - const bodyDialog = e.target.closest('.dialog__body'); - - this.changeBtnStyle(bodyDialog); - this.clearErrorStyleBudgetForm(); - }); - } - - clearErrorStyleBudgetForm() { - // Clear error style - const inputFieldEls = - this.budgetDialog.querySelectorAll('.form__input-field'); - const errorTexts = this.budgetDialog.querySelectorAll('.error-text'); - - inputFieldEls.forEach((item) => { - if (item.classList.contains('error-input')) - item.classList.remove('error-input'); - }); - - errorTexts.forEach((item) => { - if (item) item.remove(); - }); - } - - async submitBudgetForm() { - try { - const { formAddBudget } = document.forms; - const form = new FormData(formAddBudget); - const date = form.get('selected_date'); - const amount = form.get('amount'); - const note = form.get('note'); - - // Validate data user input - - if (this.validateBudgetForm(date, amount)) { - this.toggleLoaderSpinner(); // Enable loader spinner - this.budgetDialog.close(); // Close dialog - - const transaction = new Transaction({ - categoryName: DEFAULT_CATEGORY.INCOME, - date, - amount: +amount, - note, - idUser: this.wallet.idUser, - }); - - await this.saveTransaction(transaction); - - // Reload data - await this.loadTransactionData(); - await this.updateAmountWallet(); - await this.loadData(); - - // Hide loader spinner - this.toggleLoaderSpinner(); - - // Show success message - this.showSuccessToast( - MESSAGE.ADD_TRANSACTION_SUCCESS, - MESSAGE.DEFAULT_MESSAGE, - ); - - document.getElementById('formAddBudget').reset(); - } - } catch (error) { - this.showErrorToast(error); - } - } - - validateBudgetForm(date, amount) { - const inputFieldEl = - this.budgetDialog.querySelectorAll('.form__input-field'); - - if (!date || !amount) { - if (!date) { - renderRequiredText('date', inputFieldEl[0]); - inputFieldEl[0].classList.add('error-input'); - } - - if (!amount) { - renderRequiredText('amount', inputFieldEl[1]); - inputFieldEl[1].classList.add('error-input'); - } - - return false; - } - - return true; - } - - changeBtnStyle(bodyDialog) { - const date = bodyDialog.querySelector('.input-date').value; - const amount = +bodyDialog.querySelector('.form__input-balance').value; - const saveBtn = bodyDialog.querySelector('.form__save-btn'); - - if (date.trim() && amount >= 1) { - saveBtn.classList.add('active'); - } else { - saveBtn.classList.remove('active'); - } - } - // ---------------------END DIALOG---------------------// - - // ---------------------WALLET DIALOG--------------------- // - addHandlerEventWalletForm() { - this.walletDialog.addEventListener('submit', (e) => { - e.preventDefault(); - - this.clearErrorStyleWalletDialog(); - this.submitWalletForm(); - }); - - this.walletDialog.addEventListener('input', (e) => { - const bodyDialog = e.target.closest('.dialog__body'); - - this.changeBtnStyleWalletDialog(bodyDialog); - this.clearErrorStyleWalletDialog(); - }); - } - - clearErrorStyleWalletDialog() { - const inputFieldEls = - this.walletDialog.querySelectorAll('.form__input-field'); - const errorTextEls = this.walletDialog.querySelectorAll('.error-text'); - - inputFieldEls.forEach((item) => { - if (item.classList.contains('error-input')) - item.classList.remove('error-input'); - }); - - errorTextEls.forEach((item) => { - if (item) item.remove(); - }); - } - - async submitWalletForm() { - try { - // Wallet info - const { walletForm } = document.forms; - const form = new FormData(walletForm); - const walletName = form.get('walletName'); - const amount = form.get('amount'); - - if (this.validateWalletDialog(walletName, amount)) { - this.walletDialog.close(); - this.toggleLoaderSpinner(); - - this.wallet = new Wallet({ - walletName, - amount: +amount, - idUser: this.user.id, - inflow: +amount, - }); - - await this.saveWallet(this.wallet); - - // Transaction info - const transaction = new Transaction({ - categoryName: 'Income', - date: new Date().toISOString().slice(0, 10), - note: FIRST_ADD_WALLET_NOTE, - amount: +amount, - idUser: this.user.id, - }); - - await this.saveTransaction(transaction); - - // Load data and event - await this.loadTransactionData(); - await this.loadData(); - this.loadEvent(); - - this.showSuccessToast( - MESSAGE.ADD_WALLET_SUCCESS, - MESSAGE.DEFAULT_MESSAGE, - ); - - await this.loadData(); // Load data from database into page - - this.toggleLoaderSpinner(); - } - } catch (error) { - // Show toast error - this.showErrorToast(error); - this.toggleLoaderSpinner(); - } - } - - validateWalletDialog(walletName, amount) { - const inputFieldEls = - this.walletDialog.querySelectorAll('.form__input-field'); - - if (!walletName || !amount) { - if (!walletName) { - renderRequiredText('wallet name', inputFieldEls[0]); - inputFieldEls[0].classList.add('error-input'); - } - - if (!amount) { - renderRequiredText('amount', inputFieldEls[2]); - inputFieldEls[2].classList.add('error-input'); - } - - return false; - } - - return true; - } - - changeBtnStyleWalletDialog(bodyDialog) { - const walletName = bodyDialog.querySelector('.form__input-text').value; - const amount = bodyDialog.querySelector('.form__input-balance').value; - const saveBtn = bodyDialog.querySelector('.form__save-btn'); - - if (walletName.length >= 3 && amount >= 1) { - saveBtn.classList.add('active'); - } else { - saveBtn.classList.remove('active'); - } - } - - // ---------------------END DIALOG--------------------- // - showSuccessToast(title, message) { const typeToast = TYPE_TOAST.success; const btnContent = BTN_CONTENT.OK; @@ -859,8 +271,6 @@ export default class HomeView extends CommonView { loadEvent() { this.addCommonEventPage(); this.handlerTabsTransfer(); - this.addEventSelectCategoryDialog(); - this.addEventTransactionItem(); } /** @@ -901,18 +311,6 @@ export default class HomeView extends CommonView { }); }); - this.addTransactionBtn.addEventListener('click', () => { - this.showTransactionDialog(); - }); - - this.addBudgetBtn.addEventListener('click', () => { - // Set default value for date input - this.budgetDialog.querySelector("[name='selected_date']").valueAsDate = - new Date(); - - this.budgetDialog.showModal(); - }); - this.cancelBtns.forEach((btn) => { btn.addEventListener('click', () => { this.closeAllDialog(); @@ -941,85 +339,6 @@ export default class HomeView extends CommonView { }); } - showTransactionDialog(idTransaction = null) { - this.clearInputTransactionForm(); - - // Init data transaction to dialog - this.initValueTransactionDialog(idTransaction); - - if (!idTransaction) - this.transactionDialog.querySelector( - "[name='selected_date']", - ).valueAsDate = new Date(); // Set default value for date input - - // Change style submit btn - this.changeBtnStyleTransactionDialog(); - this.transactionDialog.showModal(); - } - - addEventTransactionItem() { - const transactionItemEl = document.querySelectorAll('.transaction__item'); - - if (transactionItemEl) { - transactionItemEl.forEach((item) => { - item.addEventListener('click', (e) => { - const transactionTime = e.target.closest('.transaction__time'); - const categoryNameEl = item.querySelector( - '.transaction__category-name', - ); - - if (transactionTime) { - const idTransaction = transactionTime.dataset.id; - - // If it is income transaction, don't show dialog - if (categoryNameEl.textContent.trim() !== 'Income') - this.showTransactionDialog(idTransaction); - } - }); - }); - } - } - - addEventSelectCategoryDialog() { - const categoryListEl = document.querySelector('.list-category'); - const categoryIconEl = this.categoryField.querySelector('.category-icon'); - const categoryNameEl = this.categoryField.querySelector('.category-name'); - - categoryListEl.addEventListener('click', (e) => { - const categoryItem = e.target.closest('.category-item'); - - if (categoryItem) { - const { url } = categoryItem.dataset; - const { value } = categoryItem.dataset; - - // Set url and value into category field in transaction dialog - categoryIconEl.src = url; - categoryNameEl.value = value; - - // Close select category dialog - this.categoryDialog.close(); - - // Clear error style for transaction dialog - this.clearErrorTransactionDialog(); - } - }); - - // Pass value selected to category dialog - categoryNameEl.addEventListener('click', () => { - if (categoryNameEl.value) { - // Make the keyword search category name into global - this.keySearchCategory = categoryNameEl.value; - - this.renderCategoryList(this.keySearchCategory); - } - this.categoryDialog.showModal(); - }); - - this.closeIcon.addEventListener('click', () => { - this.categoryDialog.close(); - }); - } - closeAllDialog() { this.dialogs.forEach((dialog) => { dialog.close(); diff --git a/javascript-practice/src/js/views/summaryTabView.js b/javascript-practice/src/js/views/summaryTabView.js new file mode 100644 index 0000000..7cc1022 --- /dev/null +++ b/javascript-practice/src/js/views/summaryTabView.js @@ -0,0 +1,37 @@ +import { formatNumber } from '../helpers/helpers'; + +class SummaryTabView { + initFunction(transform) { + this.transform = transform; + } + + subscribe() { + this.transform.create('summaryTabView', this.updateData.bind(this)); + } + + updateData(data) { + if (data.wallet) this.wallet = data.wallet; + + if (data.listTransactions) this.listTransaction = data.listTransactions; + + if (data.listCategory) this.listCategory = data.listCategory; + } + + load() { + const inflowValue = document.querySelector('.inflow__text--income'); + const outflowValue = document.querySelector('.outflow__text--outcome'); + const totalValue = document.querySelector('.summary__total'); + + const { inflow } = this.wallet; + const { outflow } = this.wallet; + const total = inflow + outflow; + + inflowValue.textContent = `+$ ${formatNumber(inflow)}`; + outflowValue.textContent = `-$ ${formatNumber(Math.abs(outflow))}`; + totalValue.textContent = `${total >= 0 ? '+' : '-'}$ ${formatNumber( + Math.abs(total), + )}`; + } +} + +export default new SummaryTabView(); diff --git a/javascript-practice/src/js/views/transactionTabView.js b/javascript-practice/src/js/views/transactionTabView.js new file mode 100644 index 0000000..566b7b9 --- /dev/null +++ b/javascript-practice/src/js/views/transactionTabView.js @@ -0,0 +1,169 @@ +import { + createTransactionDetailObject, + getAllCategoryNameInTransactions, + getAllTransactionByCategoryName, +} from '../helpers/dataProcess'; +import { formatNumber } from '../helpers/helpers'; +import TransactionView from './transactionView'; + +class TransactionTabView { + constructor() { + this.addEventTransactionItem(); + } + + initFunction(transform) { + this.transform = transform; + } + + subscribe() { + this.transform.create('transactionTabView', this.updateData.bind(this)); + } + + updateData(data) { + if (data.wallet) this.wallet = data.wallet; + + if (data.listTransactions) this.listTransactions = data.listTransactions; + + if (data.listCategory) this.listCategory = data.listCategory; + } + + async loadTransactionTab() { + // Load category + // Init data first + this.transactionDetails = this.loadTransactionDetailsData(); + const transactionEl = document.querySelector('.transaction'); + const listTransactionDetailEl = + transactionEl.querySelector('.transaction__list'); + const markup = []; + if (this.transactionDetails) { + this.transactionDetails.forEach((transactionDetail) => { + markup.push(this.transactionDetailMarkup(transactionDetail)); + }); + } + listTransactionDetailEl.innerHTML = ''; + listTransactionDetailEl.insertAdjacentHTML('afterbegin', markup.join('\n')); + // Load event + this.addEventTransactionItem(); + } + + loadTransactionDetailsData() { + // Get all category user have in transactions + const listCategoryInTransaction = getAllCategoryNameInTransactions( + this.listTransactions, + ); + + // Create transactions details object + const tempList = listCategoryInTransaction.map((categoryName) => { + // Get category object from list category has been loaded. + const category = this.listCategory.filter( + (item) => item.name === categoryName, + ); + + const transactions = getAllTransactionByCategoryName( + categoryName, + this.listTransactions, + ); + + return createTransactionDetailObject( + Object.assign({}, ...category), + transactions, + ); + }); + + tempList.sort( + (a, b) => + parseInt(b.transactions[0].id, 10) - parseInt(a.transactions[0].id, 10), + ); + + return tempList; + } + + // eslint-disable-next-line class-methods-use-this + transactionDetailMarkup(transactionDetail) { + const transactionTimeMarkup = () => { + const listMarkup = []; + transactionDetail.transactions.forEach((transaction) => { + const markup = ` +
+
+

${transaction.day}

+
+

+ ${transaction.fullDateString} +

+

${ + transaction.note === '' ? 'None' : transaction.note + }

+
+
+

${transaction.amount >= 0 ? '+' : '-'}$ ${formatNumber( + Math.abs(transaction.amount), + )}

+
+ `; + + listMarkup.push(markup); + }); + + return listMarkup.join('\n'); + }; + + return ` +
+
+
+
+ Transportation icon category +
+
+

+ ${transactionDetail.categoryName} +

+

${ + transactionDetail.totalTransaction + } Transactions

+
+
+

${ + transactionDetail.totalAmount >= 0 ? '+' : '-' + }$ ${formatNumber(Math.abs(transactionDetail.totalAmount))}

+
+
+ + ${transactionTimeMarkup()} +
+ `; + } + + // eslint-disable-next-line class-methods-use-this + addEventTransactionItem() { + const transactionItemEl = document.querySelectorAll('.transaction__item'); + + if (transactionItemEl) { + transactionItemEl.forEach((item) => { + item.addEventListener('click', (e) => { + const transactionTime = e.target.closest('.transaction__time'); + const categoryNameEl = item.querySelector( + '.transaction__category-name', + ); + + if (transactionTime) { + const idTransaction = transactionTime.dataset.id; + + // If it is income transaction, don't show dialog + if (categoryNameEl.textContent.trim() !== 'Income') + TransactionView.showTransactionDialog(idTransaction); + } + }); + }); + } + } +} + +export default new TransactionTabView(); diff --git a/javascript-practice/src/js/views/transactionView.js b/javascript-practice/src/js/views/transactionView.js index 7057b59..94bc61b 100644 --- a/javascript-practice/src/js/views/transactionView.js +++ b/javascript-practice/src/js/views/transactionView.js @@ -1,19 +1,312 @@ -export default class TransactionView { - constructor() {} +import * as MESSAGE from '../constants/message'; +import { renderRequiredText } from '../helpers/validateForm'; +import Transaction from '../models/transaction'; +import CategoryView from './categoryView'; - onTransitionChange() { - this.onTransitionChangeHandler(); +import defaultCategoryIcon from '../../assets/images/question-icon.svg'; + +class TransactionView { + constructor() { + this.addTransactionBtn = document.getElementById('addTransaction'); + this.transactionDialog = document.getElementById('transactionDialog'); + this.transactionForm = document.getElementById('formAddTransaction'); + + this.handlerEventTransactionDialog(); } - initTransitionChange(handler) { - this.onTransitionChangeHandler = handler; + initFunction( + toggleLoaderSpinner, + deleteTransaction, + loadTransactionData, + updateAmountWallet, + loadData, + showSuccessToast, + showErrorToast, + saveTransaction, + transform, + ) { + this.toggleLoaderSpinner = toggleLoaderSpinner; + this.deleteTransaction = deleteTransaction; + this.loadTransactionData = loadTransactionData; + this.updateAmountWallet = updateAmountWallet; + this.loadData = loadData; + this.showSuccessToast = showSuccessToast; + this.showErrorToast = showErrorToast; + this.saveTransaction = saveTransaction; + this.transform = transform; } - submitTransaction() { - const transactionDialog = document.getElementById('transactionDialog'); + subscribe() { + this.transform.create('transactionView', this.updateData.bind(this)); + } - const saveBtn = transactionDialog.querySelector('.form__save-btn'); + sendData() { + const data = { wallet: this.wallet, listTransaction: this.listTransaction }; - saveBtn.addEventListener('click', this.onTransitionChange); + this.transform.onSendSignal('transactionView', data); + } + + updateData(data) { + if (data.wallet) this.wallet = data.wallet; + + if (data.listTransactions) this.listTransactions = data.listTransactions; + + if (data.listCategory) this.listCategory = data.listCategory; + } + + handlerEventTransactionDialog() { + this.transactionDialog.addEventListener('submit', (e) => { + e.preventDefault(); + this.clearErrorTransactionDialog(); + this.submitTransactionDialog(); + }); + + this.transactionDialog.addEventListener('input', () => { + this.changeBtnStyleTransactionDialog(); + this.clearErrorTransactionDialog(); + }); + + // Add delete transaction event + this.deleteTransactionEvent(); + + this.addTransactionBtn.addEventListener('click', () => { + this.showTransactionDialog(); + }); + + this.handlerCategoryFieldEvent(); + } + + handlerCategoryFieldEvent() { + const categoryField = this.transactionForm.querySelector( + "[name='category_name']", + ); + + categoryField.addEventListener('click', () => { + this.clearErrorTransactionDialog(); + }); + } + + clearErrorTransactionDialog() { + // Clear error style + const inputFieldEls = + this.transactionDialog.querySelectorAll('.form__input-field'); + const errorTextEls = this.transactionDialog.querySelectorAll('.error-text'); + + inputFieldEls.forEach((item) => { + if (item.classList.contains('error-input')) + item.classList.remove('error-input'); + }); + errorTextEls.forEach((item) => { + if (item) item.remove(); + }); + } + + deleteTransactionEvent() { + const deleteTransactionBtn = + this.transactionDialog.querySelector('.form__delete-btn'); + + deleteTransactionBtn.addEventListener('click', async () => { + const idEl = this.transactionDialog.querySelector( + "[name='id_transaction']", + ); + + if (idEl.value) { + try { + this.transactionDialog.close(); + this.toggleLoaderSpinner(); + + await this.deleteTransaction(idEl.value); + + // Reload data + await this.loadTransactionData(); + await this.updateAmountWallet(); + await this.loadData(); + + this.showSuccessToast('Delete success!', MESSAGE.DEFAULT_MESSAGE); + } catch (error) { + this.showErrorToast(error); + } + this.toggleLoaderSpinner(); + } + }); + } + + changeBtnStyleTransactionDialog() { + const dateInput = this.transactionDialog.querySelector( + "[name='selected_date']", + ).value; + const categoryName = this.transactionDialog.querySelector( + "[name='category_name']", + ).value; + const amountInput = + this.transactionDialog.querySelector("[name='amount']").value; + const saveBtn = this.transactionDialog.querySelector('.form__save-btn'); + + saveBtn.classList.toggle( + 'active', + dateInput && categoryName && amountInput >= 1, + ); + } + + initValueTransactionDialog(idTransaction) { + let categoryName; + + if (idTransaction) { + // Get transaction object + const transactionArr = this.listTransactions.filter( + (obj) => obj.id === idTransaction, + ); + const transaction = Object.assign({}, ...transactionArr); + // Get category object + const categoryArr = this.listCategory.filter( + (obj) => obj.name === transaction.categoryName, + ); + const category = Object.assign({}, ...categoryArr); + + const idEl = this.transactionDialog.querySelector( + "[name='id_transaction']", + ); + const dateEl = this.transactionDialog.querySelector( + "[name='selected_date']", + ); + const categoryEl = this.transactionDialog.querySelector( + "[name='category_name']", + ); + const amountEl = this.transactionDialog.querySelector("[name='amount']"); + const noteEl = this.transactionDialog.querySelector("[name='note']"); + const iconEl = this.transactionDialog.querySelector('.category-icon'); + + idEl.value = transaction.id; + dateEl.value = transaction.date; + categoryEl.value = transaction.categoryName; + amountEl.value = Math.abs(transaction.amount); + noteEl.value = transaction.note; + iconEl.src = category.url; + + categoryName = categoryEl.value; + } + + // Show delete button only if it is a edit form and not a income transaction + const deleteBtn = this.transactionDialog.querySelector('.form__delete-btn'); + const showDeleteBtn = () => { + return idTransaction && categoryName !== 'Income'; + }; + + deleteBtn.classList.toggle('hide', !showDeleteBtn()); + } + + async submitTransactionDialog() { + try { + const dateEl = this.transactionForm.querySelector( + "[name='selected_date']", + ); + const categoryNameEl = this.transactionForm.querySelector( + "[name='category_name']", + ); + const amountEl = this.transactionForm.querySelector("[name='amount']"); + const noteEl = this.transactionForm.querySelector("[name='note']"); + const idEl = this.transactionForm.querySelector( + "[name='id_transaction']", + ); + const amount = + categoryNameEl.value === 'Income' ? +amountEl.value : -+amountEl.value; // Check if this is a income transaction, amount must plus + + // Validate data user input + if ( + this.validateTransactionForm(dateEl.value, categoryNameEl.value, amount) + ) { + this.toggleLoaderSpinner(); + this.transactionDialog.close(); + + const transaction = new Transaction({ + id: idEl.value, + categoryName: categoryNameEl.value, + date: dateEl.value, + amount, + note: noteEl.value, + idUser: this.wallet.idUser, + }); + + await this.saveTransaction(transaction); + + // Reload data + await this.loadTransactionData(); + this.updateAmountWallet(); + await this.loadData(); + + if (!idEl.value) { + // Add success + this.showSuccessToast( + MESSAGE.ADD_TRANSACTION_SUCCESS, + MESSAGE.DEFAULT_MESSAGE, + ); + } else { + // Update success + this.showSuccessToast( + MESSAGE.UPDATE_TRANSACTION_SUCCESS, + MESSAGE.DEFAULT_MESSAGE, + ); + } + + this.toggleLoaderSpinner(); + this.clearInputTransactionForm(this.transactionForm); + } + } catch (error) { + this.showErrorToast(error); + this.toggleLoaderSpinner(); + } + } + + validateTransactionForm(date, categoryName, amount) { + const inputFieldEls = + this.transactionDialog.querySelectorAll('.form__input-field'); + + if (!date || !categoryName || !amount) { + if (!date) { + renderRequiredText('date', inputFieldEls[0]); + inputFieldEls[0].classList.add('error-input'); + } + + if (!categoryName) { + renderRequiredText('category', inputFieldEls[1]); + inputFieldEls[1].classList.add('error-input'); + } + + if (!amount) { + renderRequiredText('amount', inputFieldEls[2]); + inputFieldEls[2].classList.add('error-input'); + } + + return false; + } + + return true; + } + + clearInputTransactionForm() { + const categoryIcon = this.transactionForm.querySelector('.category-icon'); + + categoryIcon.src = defaultCategoryIcon; + this.keySearchCategory = null; // Delete keyword search + CategoryView.renderCategoryList(this.keySearchCategory); + this.transactionForm.reset(); + } + + showTransactionDialog(idTransaction = null) { + this.clearInputTransactionForm(); + + // Init data transaction to dialog + this.initValueTransactionDialog(idTransaction); + + if (!idTransaction) + this.transactionDialog.querySelector( + "[name='selected_date']", + ).valueAsDate = new Date(); // Set default value for date input + + // Change style submit btn + this.changeBtnStyleTransactionDialog(); + this.transactionDialog.showModal(); } } + +export default new TransactionView(); diff --git a/javascript-practice/src/js/views/walletView.js b/javascript-practice/src/js/views/walletView.js new file mode 100644 index 0000000..5636457 --- /dev/null +++ b/javascript-practice/src/js/views/walletView.js @@ -0,0 +1,180 @@ +import { FIRST_ADD_WALLET_NOTE } from '../constants/config'; +import Transaction from '../models/transaction'; +import Wallet from '../models/wallet'; +import * as MESSAGE from '../constants/message'; +import { renderRequiredText } from '../helpers/validateForm'; + +class WalletView { + constructor() { + this.walletDialog = document.getElementById('walletDialog'); + + this.addHandlerEventWalletForm(); + } + + initFunction( + transform, + toggleLoaderSpinner, + saveWallet, + saveTransaction, + loadTransactionData, + loadData, + loadEvent, + showSuccessToast, + showErrorToast, + ) { + this.transform = transform; + this.toggleLoaderSpinner = toggleLoaderSpinner; + this.saveWallet = saveWallet; + this.saveTransaction = saveTransaction; + this.loadTransactionData = loadTransactionData; + this.loadData = loadData; + this.loadEvent = loadEvent; + this.showSuccessToast = showSuccessToast; + this.showErrorToast = showErrorToast; + } + + subscribe() { + this.transform.create('walletView', this.updateData.bind(this)); + } + + sendData() { + const data = { + wallet: this.wallet, + listTransactions: this.listTransactions, + user: this.user, + }; + + this.transform.onSendSignal('walletView', data); + } + + updateData(data) { + if (data.wallet) this.wallet = data.wallet; + + if (data.listTransactions) this.listTransaction = data.listTransactions; + + if (data.listCategory) this.listCategory = data.listCategory; + + if (data.user) this.user = data.user; + } + + showDialog() { + this.walletDialog.showModal(); + } + + addHandlerEventWalletForm() { + this.walletDialog.addEventListener('submit', (e) => { + e.preventDefault(); + + this.clearErrorStyleWalletDialog(); + this.submitWalletForm(); + }); + + this.walletDialog.addEventListener('input', (e) => { + const bodyDialog = e.target.closest('.dialog__body'); + + this.changeBtnStyleWalletDialog(bodyDialog); + this.clearErrorStyleWalletDialog(); + }); + } + + clearErrorStyleWalletDialog() { + const inputFieldEls = + this.walletDialog.querySelectorAll('.form__input-field'); + const errorTextEls = this.walletDialog.querySelectorAll('.error-text'); + + inputFieldEls.forEach((item) => { + if (item.classList.contains('error-input')) + item.classList.remove('error-input'); + }); + + errorTextEls.forEach((item) => { + if (item) item.remove(); + }); + } + + async submitWalletForm() { + try { + // Wallet info + const { walletForm } = document.forms; + const form = new FormData(walletForm); + const walletName = form.get('walletName'); + const amount = form.get('amount'); + + if (this.validateWalletDialog(walletName, amount)) { + this.walletDialog.close(); + this.toggleLoaderSpinner(); + + this.wallet = new Wallet({ + walletName, + amount: +amount, + idUser: this.user.id, + inflow: +amount, + }); + + this.sendData(); + + await this.saveWallet(this.wallet); + // Transaction info + const transaction = new Transaction({ + categoryName: 'Income', + date: new Date().toISOString().slice(0, 10), + note: FIRST_ADD_WALLET_NOTE, + amount: +amount, + idUser: this.user.id, + }); + await this.saveTransaction(transaction); + // Load data and event + await this.loadTransactionData(); + await this.loadData(); + this.loadEvent(); + this.showSuccessToast( + MESSAGE.ADD_WALLET_SUCCESS, + MESSAGE.DEFAULT_MESSAGE, + ); + await this.loadData(); // Load data from database into page + + this.toggleLoaderSpinner(); + } + } catch (error) { + // Show toast error + this.showErrorToast(error); + this.toggleLoaderSpinner(); + } + } + + validateWalletDialog(walletName, amount) { + const inputFieldEls = + this.walletDialog.querySelectorAll('.form__input-field'); + + if (!walletName || !amount) { + if (!walletName) { + renderRequiredText('wallet name', inputFieldEls[0]); + inputFieldEls[0].classList.add('error-input'); + } + + if (!amount) { + renderRequiredText('amount', inputFieldEls[2]); + inputFieldEls[2].classList.add('error-input'); + } + + return false; + } + + return true; + } + + // eslint-disable-next-line class-methods-use-this + changeBtnStyleWalletDialog(bodyDialog) { + const walletName = bodyDialog.querySelector('.form__input-text').value; + const amount = bodyDialog.querySelector('.form__input-balance').value; + const saveBtn = bodyDialog.querySelector('.form__save-btn'); + + if (walletName.length >= 3 && amount >= 1) { + saveBtn.classList.add('active'); + } else { + saveBtn.classList.remove('active'); + } + } +} + +export default new WalletView();