From 719221dd82594923cc7d8c8b52c467ed7ad6ee42 Mon Sep 17 00:00:00 2001 From: jiho Date: Thu, 22 Aug 2024 19:28:33 +0900 Subject: [PATCH 01/10] feat(fe): settings page save --- apps/frontend/app/(main)/settings/page.tsx | 208 +++++++++++++++++++++ apps/frontend/public/invisible.png | Bin 0 -> 1695 bytes apps/frontend/public/settings.png | Bin 0 -> 1460 bytes 3 files changed, 208 insertions(+) create mode 100644 apps/frontend/app/(main)/settings/page.tsx create mode 100644 apps/frontend/public/invisible.png create mode 100644 apps/frontend/public/settings.png diff --git a/apps/frontend/app/(main)/settings/page.tsx b/apps/frontend/app/(main)/settings/page.tsx new file mode 100644 index 0000000000..133788c56d --- /dev/null +++ b/apps/frontend/app/(main)/settings/page.tsx @@ -0,0 +1,208 @@ +'use client' + +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { cn } from '@/lib/utils' +import codedangSymbol from '@/public/codedang-editor.svg' +import invisible from '@/public/invisible.png' +import { zodResolver } from '@hookform/resolvers/zod' +import Image from 'next/image' +import React from 'react' +import { useForm } from 'react-hook-form' +import { FaCheck } from 'react-icons/fa6' +// import { IoWarningOutline } from 'react-icons/io5' +import { z } from 'zod' + +interface SettingsFormat { + // username: string + currentPassword: string + newPassword: string + confirmPassword: string + name: string + major: string + // studentId: string + // email: string + // verificationCode: string + // firstName: string + // lastName: string +} + +const schema = z + .object({ + // currentPassword: z.string().min(1, { message: 'Required' }), + currentPassword: z + .string() + .min(1, { message: 'Required' }) + .min(8) + .max(20) + .refine((data) => { + const invalidPassword = /^([a-z]*|[A-Z]*|[0-9]*|[^a-zA-Z0-9]*)$/ + return !invalidPassword.test(data) + }), + // newPassword: z.string().min(8, { message: 'Password must be at least 8 characters' }).optional(), + newPassword: z + .string() + .min(1, { message: 'Required' }) + .min(8) + .max(20) + .refine((data) => { + const invalidPassword = /^([a-z]*|[A-Z]*|[0-9]*|[^a-zA-Z0-9]*)$/ + return !invalidPassword.test(data) + }), + // confirmPassword: z.string().optional(), + confirmPassword: z.string().min(1, { message: 'Required' }), + name: z + .string() + .min(1, { message: 'Required' }) + .regex(/^[a-zA-Z]+$/, { message: 'only English supported' }) + }) + .refine( + (data: { newPassword: string; confirmPassword: string }) => + data.newPassword === data.confirmPassword, + { + path: ['confirmPassword'], + message: 'Incorrect' + } + ) + +function requiredMessage(message?: string) { + return ( +
+ {message === 'Required'} +

{message}

+
+ ) +} + +export default function Page() { + const { + register, + handleSubmit, + // watch, + // formState: { errors, isDirty, isValid } + formState: { errors } + } = useForm({ + resolver: zodResolver(schema), + mode: 'onChange', + defaultValues: { + currentPassword: '', + newPassword: '', + confirmPassword: '', + name: '', + major: '' + } + }) + + // const currentPassword = watch('currentPassword') + + const onSubmit = () => {} + + return ( +
+
+
+ codedang +

CODEDANG

+
+

Online Judge Platform for SKKU

+
+ +
+

Settings

+

+ You can change your information +

+ + + + +
+
+ + + codedang + +
+ +
+
{requiredMessage('Required')}
+ +
+
+ + + codedang + +
+
+ +
+
+ + + codedang + +
+
+ +
+ + + + + + + + + + +
+ +
+
+
+ ) +} diff --git a/apps/frontend/public/invisible.png b/apps/frontend/public/invisible.png new file mode 100644 index 0000000000000000000000000000000000000000..8a5b1e0b5d510d86d247644def13122095bd8622 GIT binary patch literal 1695 zcmZ`)X*}Br0{tgVS`k_e!%#abq@-weR9vZw5RwpP^XZI zv_qx;0hObNJYUYVJrr01Ie-BG=I(z0B)-6JIt)P!8p#Q0d8xm6XdsaU4*~#4{srH= z0tJAhSPGHglLT6+T6TMHujR<-$;cB5gaX0r2?Al}Kpg^wa#Su82wq-ZK0WN2yH44b zH`^yEH!6!tq3@pylBVv4Gb2sn)_w^or=H8E8lMSp3~fv83kT&6fDxrolrfx4)#jF) z8(930f|puZdFs>V=GAj^5O_yd*SUp-g%8`?w(dCwWT23fk`laEDH}pa*R|y{+kA6m zRn5@t-$zDD?Qpo}y)S)znUe|y#=*)FJu|SdyPLc|u)eVo;ef{5r9i10aX?jIpyw%_N8V`Z7ru!5{R|a^ML{zb%TTMmN;DC4&$#dY6n@TtgMV<)2?+s zx86Cb0KKZdyu7Sfm~-+4bW*Ngr}6G`!SLRTiS*#&$7Y=M+R zbXA*LxMwIZjc0m*-zpTh@87r7%=qvF37!yv3FiI&@guE@LA^&=?y2|dLq!_}z;*W4 z*S){kOkpBMknQd5RJF~XFnG2%xMR*OJ~o)=7}zb5TnTNZkkopScVe%|vixju7cQLL z=imBn`G8kx$CiSlEdws8i3aM=Wy^iUTl24_RMlVQ)PaB z`D!;N);6@ncVjDfE+>0>a?;}6yLV!IV}|}W-{i5Z>q%k!L9!~WP61XqFVVa$7K=mu z0N+;l75TpdMFcT3ddsb9#6g zbh6?o@4Tqq5tS;N2t+k?J+nr2uo}i$twU%HxLk;_AxXEkn4<-qp6S<$U9Uk>|EXmQ zjoSP%!J|kflL-g4eGx$ub)Bz6x6XGasOD5RoS5$-dj{3t@GKXUl-1SO9l)DT&CJZy~< z6}A14&WD)t6Qf#4WV3o9mRBdTQVnXNkF~~yc|RSq6B$&h|AHWbu_RETa;K%a#Bl`i zz?$)DSDf!OAIKd?b9oSzuY705c#!U5Gi^OZ+H2{?;8O2(<_n$^J>L=o=i3)8~WgijW)j8Re64tMZE5wiEgC^Gr% zF@m(e-z6gZSEDqp`}2tywikyy6b~D{`0SyflVXu>lKi}DZEelh8nT72Xv*pW@~>_0 zrbBk`n5svFxF5k<-%KIiI<-T;I@WKJD6jwtzAAa36lS_YFFqhSs|AXU(? zM0uq4J3La14^}iQ^D@Vw7c~SMJp?qlyru@Du=1CMY_%0>{6;f4Q}1Ud%}U|ue^MJB zZBV3{AUjikysNgmZ@W^q`8{ui5%bX{6N5WV*Ko~Dffn97aKC(MfPgIg`yvW+E7KDm zRjGye_-15Rj*i$ZfvOo4YwU1Yx&e-`L6iZ11@fzFQn4H-#*U1_tnvDsRC mabzEQUB&JHul1iZzJIKMAvq%5J}El<6M*94No;YVr~en9bp3Mx literal 0 HcmV?d00001 diff --git a/apps/frontend/public/settings.png b/apps/frontend/public/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..f950dda84f1dfa03dcbb700c0b2c003e07e501dc GIT binary patch literal 1460 zcmV;l1xxygP)@~0drDELIAGL9O(c600d`2O+f$vv5yP6JoUR=DsesocK_tNd?sVz+_;@F1 zfaiG}Q{j`e6J}cncz1Vq`7q-BpmbbaU9Ex%Wk~=w8NhR%KRrD?kcg-Qa9<1ZAQe_& zjR7G3eYZfQ8z2drczI$GRu}-{T@b$~eS(sJxi-ZjY)L>#VgeZH%7Y@c0vimdt3}5N zYYH)T20&yZE4@L?*@6uT@FaeCVhN*IKw<_QnBT-cs1!YUaIf1Hq@!XBSPFD%L-({gpZ8#<%z?J4rs%{)YMd7#rv@W5>^mR z&LEvmSEA@2+X;~s)EV}%*nGc{p&yX2ZP75`=H`Y{_f8Cvgd;@T{AFQb;X_<{Aqz-w z#wd~*wtWP2pm@@Osqx)Q*sN^)r-1G{GEm0O*dSbCIwflqVK_Ct>V{lMUv_zU+1L~8 zD2~p~&K4yBKmzu#Q~&7ta9q~r=H~vuTIy$KXOB$c&ZE%z0UrxtbAz#OFfI!RsD_-% zWviW5X>xM%Z?u1IwGZM5Gyw*@)+t6!R@lW;#q&xcC^rQjwN2#`zZE4p_40^75#J8dcOa7ur{Emlq_gy1g#2&l_Xg&8Re&5&GY1w7Hakh->>03zv# zxc>Ib%gY@|!A8X2icGk|E~TeK2dKMzwy~kB8H8Mk=_jqO`z{^1rmpE=0CzzwUrYkB z_#JKBuZ{*EE_|0BC*ur?YsAfYj8W8g$=FJ(y`?(_`5z;7E>0KHQaXY{Xtg$myJ41K z7jqX)!tk`Nk%>qoC9B}ncX?n!FK4V9GALqX8jZ$({*LxUrfzLWmQLa0o1ifu{x8#N zlvUGa3o7ZcAJFZ=WWN?e4CNIWRqhVj;hIFiFy9gK0@_^}VT2JzFvJfseOCT~sl Date: Thu, 22 Aug 2024 10:31:10 +0000 Subject: [PATCH 02/10] Optimised images with calibre/image-actions --- apps/frontend/public/invisible.png | Bin 1695 -> 1220 bytes apps/frontend/public/settings.png | Bin 1460 -> 834 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/apps/frontend/public/invisible.png b/apps/frontend/public/invisible.png index 8a5b1e0b5d510d86d247644def13122095bd8622..baefa5c2d6de1b238412d0b3ce4d32a0bb201d1f 100644 GIT binary patch delta 1180 zcmV;N1Y`T34a5nMIDZ6oNkl>N5XSTW|2e%ErH6tA4<33DT4+I`P(i_h zhaU3s2tu%V-H$voyNS<0F14CuemfsKv%5_zDJqRrAOHd&5dZ;@2!H@c1V8{J0w4eq z0T2L*00@AjasZpnrahfb(qah!A0Hp>)6-M?^70}G-T*GjzkghU;0WMa`GVjG;BYwf zD&GnMe(US&>kNIpUbm~&s$DD=?ep{VpL^qP_zeH^`uckQ&iD6sdpsVeWdWa`pJot# ze}B(`9SZDIa_)uS!fyg*ZC08U1DFslO2^1$DLiy7m&}E0T;5UQPqN`2*G1+LoLQxLV%Bc zdwXklyWRPJ5Wu^^{O9NA{24|rJEO9+26{F0{r$ate0-!pK>iM>U1g_E;#d}i*pTxa zt^n}T$gv+D9tJ~TzAOubg-dFf%J04;0|I++U=-H|p?|SZeytE7+wHb02#uBUYleV; zJ+#)udhW(j`87ibRd!<`42@NT03hJTKI~N85mMeF0RGpQ@qmC}+O<5m6DT65M~$?3 zC0jr<%)2>92c4lfR#&o z)sAAz2!9MK$cYDym+R>>S1$-LmXH(E`YzO?OaL&uS6!ugK!|Y&Im}Bp6m2e~j zVE0#36kq|t695M9U;r>9PZxAOVwn)&-aK7UOnoQ_fK{yOn}o$$918-<;GyqI12-VJSxdCektbvyD995JeQUbpv1XjWkJNlE0aV#qc1!5@h z)_=V$Ah0H3seR(6gpZ7wriYp&YzXX9yIDhqVWy#UFt#?tgup1bgt9DJOOoM=Qrd;q z?ugOr$c;VjP5GRXZD?7cN29-*rhLie98l6_Ltob_pg2@fOYKic$$F>@vl#Ip))g^g zh6RL}dXE_ZR-YKl1jh)0G5a=OoEsYtP=ALR6ND-j=dydoV{u1SEl+1dU~Ut&fKcTI z4K@V4WNup52jNzRlmY<(x7saA-N4LPzSsqk^U`<@lsuR{=IS@;rI>$C6}5W=&Gj}_ zCGT_XgSQxn@8fUavos>xn~7s{wG>qhouc?RAn0QAAII<)Vibc-4DBmi7X_Xrv&F!tC= zxHfzzWfE5kf)4L43_$=2LlA%r2t$u25P$&?WB~;sCtKqbH0=S0000=37-v+IDY^Eb5ch_0Itp)=>Px#1ZP1_K>z@;j|==^1pojBG)Y83RCodH zoa;*EJQP54yn~9OprW9l;15AT-~SB+1qFWyUO_=nP*7C7eBZ1>hBJq@*-3Vrj*|uD z%%FJMtYmj*CvES)UzkiXhD;_YSwLd4fW%}0iOB*IlLaIu3x7yV7Lb@MATe1$VzPk5 zWC4lE0uqx2Bqj?;Oq76|n;Y}?_GU8S1Pi#jx-#eI=jQnMI7^5iNC1t0eSKa0o5s%) z!toZ+-}sg+A)KDin())plQ}v%s{HxH#Dv-2-Yz~hKq%kI=llD6@yYS^^<_RkKmYjM zk&zKIJUm={H-G=0oSZBU{y#M}Wx7hwcTt|+&nT!OMgom`cXwAbI>%rmhk}rqnHe)b zKVN)mbr21DA-f_pCD=xPcz7rpy$u){D=RCVkkJd>mZBkn#y>bXD8frB78e)I!oq@? zot-r)=*8>{(vUz*=f09s0x%j@S65pX(Tm-f^^+t3BYy%BZH1+9>@=ix$C02h67a#B=D>7_9e=+#;m%RECv zL=z!NO@F{(=y-g5jFzAY=SPS{Mhx2Lts;IISIe&b@0%VGR55`YdP83{;;h0+Hrdp?K-U^pi9^g{*$ZAx&32Wn0lFbaI`Ct?BgB8{Tf;5uv@ z5+vclF5Jh$cll&hA^}{yM(BaEi)=%JWS0fmnFf?Qq@zG2fX3HaB&oW{HYB)urq+6} z&nOTJ(4sCkN5c2_chrozPJ(NZMr#Csbbk~$1!(qua7~WJ#>S${7F7~l*P*%CV)r|_ za0-Z>rVrw}NKRa%c2p+8l?iFDc1coFrvUAflG2ofHc<&d6t^^Som78gX-kldK20QZ z3#R}IOxm%rj=_6`rUX}K(!SS8igTh6q%lpmux+G{vU!4*1S*zwXs$;{E2E+`lz*Y5 zH40p*VAZf~V^PP|Iiw()4ZjjZ0yIYfjp+KWs**s{60|mR(fg%~LgHGG)+msa1PB#X z-_?`^yOApv8s4X~iUrUht{5uCHMig*>dF#LNYLv2%;tSMs@HmeeZ(ggrs*!|MQuvZ zD76!Me3vD?SH$YVgA{48ir&OxcYo92NF)De)N;PQcnjc*>Xs9wut*}ga7M!J?rvqz z(vX1Q*co%^Lmk1}Qc`T^pb@FBaM0*hAPx8W`f7%Ts{2!u&nFNnLwY|%ZB684V{cxJ zCavYb6c33|!rt54GoeK7jAMIWY@v&hq?v)VvBup0yRlmwqXcNgPGEbVYkz_!r7a0s zn{yl8p6<)v5{^2xJ>8-uLGv-uCM0M!KBzyUq#?oP)FFE<2SLKc#YJU?^3M?(x{1{pqgFY>g?)b<^9<6|rrv=kA-RBltPK19 zIPI7Aag^VOcs!s zEFdviKw`3h#AE@9$pR9S1tca5NK6)xm@FVMSwLd`2aI#E1H$+bqyPW_07*qoM6N<$ Ef;!v#ssI20 diff --git a/apps/frontend/public/settings.png b/apps/frontend/public/settings.png index f950dda84f1dfa03dcbb700c0b2c003e07e501dc..05e7458e9c4186c24161b4350e410f339a0946bd 100644 GIT binary patch delta 791 zcmV+y1L*v;3&IAFIDZ24NklQ42Cm8|KtP7ACJ%xGD1ek2K5H<2F(V^ z2JQwL!6SGCjgX1}edp_0wkT7yk1_0;w z?>z?BlmI*e0Dm}V013r_F984=>lQTt?+s`(IRk*fAXf}{UOBeMZ$ASt zHh#;|z$^%$aI6X|l(e6o0l1UIsvnOQ1u#}K@LVWg)oRHu4SHWH1}QNB*aCwR_kAo+ zAXb1uZa-jq_e7B*&aDiV3&3I_n7JxJN=FD5YP&9Eegu#WCjgB_iSrD3>jI%>x=u3~ zh&>lxaerxkoP0A^R*#7#^S`HczQo1Xq0-(V4^r!mmVUw)hY)JHn;$OoI3AA|;%<79 zA-Z^7BF~Q!fMlu5vVBY9H53~Fj&3hQwx@`}lpaWJ00ce%!O?gHz3)||SrUL_9-ybv zT|DGHd7Z&@d?^4}<eVA+5wtw+dp<_gLdmXp4DM5*o;`h);VtnV| z>jkpcY+Azd2GBDTGxs1U<%Nl~SalgC00CGq;UQt;lB3{vub7hS$yd72FGj4pU`%OF zBujs`_iVL_)T+QBK0RNTqd)3MvL#1tcT1j6h_#2QrB0~C3ZQYW^JUAHEt|!@0VM|0 V@DTnAxZwZ*002ovPDHLkV1jJlUVs1q delta 1422 zcmV;91#$Yq2DA&1IDY^Eb5ch_0Itp)=>Px#1ZP1_K>z@;j|==^1pojANl8RORCodH zTf0&lNff_lsWS_ zpo&LexeD(usrG+^+?kr1UgkyBf49yzPE4RXCpGg2!A8IWQ+xMIF6G&KR@3Z z8yj20pDQw4@b2&L-Bc=7IyyS?!Uk#?u(Y(~V1iHh=*W-*bQDicPD(-jX@3KE{`&g* ze|ZMP6OiEC%*;%!(P(_u^{4y|7Z(==c_zfS04Dq!)R)qihsRmBlM7145Dv@IZ7K?>)bBqMWWA`l*1|Btay>0Pb|@ z`1p7yXn^N=98=+wv=e4q2Y7dPclj{l{h)MQU0tn$31vwDHW|QkoYl=I7^E zCMG6Cw|^!p!O-pPEhD-o!+iNlHxXv4wO)Kp%@ z`>_HNRuE0jAe~NEqUayn36T}l8TPT*e7}*QACRzZ(JV{lM zUv_zU+1L~8D2~p~&K4yBKmzu#Q~&7ta9q~r=H~vuTIy$KXOB$c&ZE%z0UrxtbAz#O zFfI!RsD_-%WviW5X>xM%Z?u1IwGZM5Gyw*@)+t6!R@lW;#q&xcC^rQjwN2#`zZE4p z_$-Uv`K3~+k78C?+K5vJGs^mS2axa*e12;6&3-b% zvQ50@t2jN9Y27lD> zzlbkVZH`0$6yd9GV~iz$N+l`c)s;3~HtDSxLOE4Ae>?qz_ORTT0dZ7&O#sEU72*)( zg6{kIfq-Z`psQ&jTTFOTl_3=e?1YVBuCZIVi)eBi5?crvLMH4p>NGZk(D;Ej%i{pE zJP1{3`iv5(yaWxaP;R$EDpA6n&wq&-BKZNH!~~^JTz@+VD7J!>2&l_Xg&8Re&5&GY z1w7Hakh->>03zv#xc>Ib%gY@|!A8X2icGk|E~TeK2dKMzwy~kB8H8Mk=_jqO`z{^1 zrmpE=0CzzwUrYkB_#JKBuZ{*EE_|0BC*ur?YsAfYj8W8g$=FJ(y`?(_`F|fHbuLa9 z(o#BtLTI%%hPz>wU>9>2O~UZBu91mIBqgih)OUGcLN8~m8!{+jWEzdefBug4M5b Date: Sat, 24 Aug 2024 22:38:32 +0900 Subject: [PATCH 03/10] feat(fe): finish ui for settings withou api --- apps/frontend/app/(main)/settings/page.tsx | 379 +++++++++++++++--- .../components/auth/HeaderAuthPanel.tsx | 12 + 2 files changed, 340 insertions(+), 51 deletions(-) diff --git a/apps/frontend/app/(main)/settings/page.tsx b/apps/frontend/app/(main)/settings/page.tsx index 133788c56d..be1248a99b 100644 --- a/apps/frontend/app/(main)/settings/page.tsx +++ b/apps/frontend/app/(main)/settings/page.tsx @@ -1,48 +1,137 @@ 'use client' import { Button } from '@/components/ui/button' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from '@/components/ui/command' import { Input } from '@/components/ui/input' +import { + Popover, + PopoverContent, + PopoverTrigger +} from '@/components/ui/popover' +import { ScrollArea } from '@/components/ui/scroll-area' import { cn } from '@/lib/utils' +import invisible from '@/public/24_invisible.svg' +import visible from '@/public/24_visible.svg' import codedangSymbol from '@/public/codedang-editor.svg' -import invisible from '@/public/invisible.png' import { zodResolver } from '@hookform/resolvers/zod' import Image from 'next/image' -import React from 'react' +import React, { useEffect } from 'react' +import { useState } from 'react' import { useForm } from 'react-hook-form' -import { FaCheck } from 'react-icons/fa6' +import { FaCheck, FaChevronDown } from 'react-icons/fa6' // import { IoWarningOutline } from 'react-icons/io5' import { z } from 'zod' interface SettingsFormat { - // username: string + username: string currentPassword: string newPassword: string confirmPassword: string name: string major: string - // studentId: string + studentId: string // email: string // verificationCode: string // firstName: string // lastName: string } +const majors = [ + '학과 정보 없음', + '자유전공계열', + '인문과학계열', + '유학·동양학과', + '국어국문학과', + '영어영문학과', + '프랑스어문학과', + '중어중문학과', + '독어독문학과', + '러시아어문학과', + '한문학과', + '사학과', + '철학과', + '문헌정보학과', + '사회과학계열', + '행정학과', + '정치외교학과', + '미디어커뮤니케이션학과', + '사회학과', + '사회복지학과', + '심리학과', + '소비자학과', + '아동·청소년학과', + '경제학과', + '통계학과', + '경영학과', + '글로벌리더학부', + '글로벌경제학과', + '글로벌경영학과', + '교육학과', + '한문교육과', + '영상학과', + '의상학과', + '자연과학계열', + '생명과학과', + '수학과', + '물리학과', + '화학과', + '식품생명공학과', + '바이오메카트로닉스학과', + '융합생명공학과', + '전자전기공학부', + '공학계열', + '화학공학/고분자공학부', + '신소재공학부', + '기계공학부', + '건설환경공학부', + '시스템경영공학과', + '나노공학과', + '소프트웨어학과', + '반도체시스템공학과', + '지능형소프트웨어학과', + '글로벌바이오메디컬공학과', + '반도체융합공학과', + '에너지학과', + '양자정보공학과', + '건축학과', + '소재부품융합공학과', + '약학과', + '의예과', + '수학교육과', + '컴퓨터교육과', + '글로벌융합학부', + '데이터사이언스융합전공', + '인공지능융합전공', + '컬처앤테크놀로지융합전공', + '자기설계융합전공', + '연기예술학과', + '무용학과', + '미술학과', + '디자인학과', + '스포츠과학과' +] + const schema = z .object({ // currentPassword: z.string().min(1, { message: 'Required' }), - currentPassword: z - .string() - .min(1, { message: 'Required' }) - .min(8) - .max(20) - .refine((data) => { - const invalidPassword = /^([a-z]*|[A-Z]*|[0-9]*|[^a-zA-Z0-9]*)$/ - return !invalidPassword.test(data) - }), + currentPassword: z.string().min(1, { message: 'Required' }), + // .min(8) + // .max(20) + // .refine((data) => { + // const invalidPassword = /^([a-z]*|[A-Z]*|[0-9]*|[^a-zA-Z0-9]*)$/ + // return !invalidPassword.test(data) + // }), // newPassword: z.string().min(8, { message: 'Password must be at least 8 characters' }).optional(), newPassword: z .string() - .min(1, { message: 'Required' }) + .min(1) .min(8) .max(20) .refine((data) => { @@ -50,7 +139,7 @@ const schema = z return !invalidPassword.test(data) }), // confirmPassword: z.string().optional(), - confirmPassword: z.string().min(1, { message: 'Required' }), + confirmPassword: z.string().min(1), name: z .string() .min(1, { message: 'Required' }) @@ -67,9 +156,8 @@ const schema = z function requiredMessage(message?: string) { return ( -
- {message === 'Required'} -

{message}

+
+ {message}
) } @@ -78,9 +166,12 @@ export default function Page() { const { register, handleSubmit, - // watch, - // formState: { errors, isDirty, isValid } - formState: { errors } + getValues, + setValue, + // trigger, + watch, + formState: { errors, isDirty } + // formState: { errors } } = useForm({ resolver: zodResolver(schema), mode: 'onChange', @@ -93,9 +184,36 @@ export default function Page() { } }) - // const currentPassword = watch('currentPassword') + // Check if Current Password is correct + const [isCheckButtonClicked, setIsCheckButtonClicked] = + useState(false) + const [isPasswordCorrect, setIsPasswordCorrect] = useState(false) + const [newPasswordAble, setNewPasswordAble] = useState(false) + const [passwordShow, setPasswordShow] = useState(false) + const [newPasswordShow, setNewPasswordShow] = useState(false) + const [confirmPasswordShow, setConfirmPasswordShow] = useState(false) + // const [passwordConfirmed, setPasswordConfirmed] = useState(false) + const [majorOpen, setMajorOpen] = useState(false) + const [majorValue, setMajorValue] = useState('') + // const [saveButtonAble, setSaveButtonAble] = useState(false) + + const currentPassword = watch('currentPassword') + const newPassword = watch('newPassword') + const confirmPassword = watch('confirmPassword') + const name = watch('name') + const isPasswordsMatch = newPassword === confirmPassword && newPassword !== '' + + // New Password Input 창과 Re-enter Password Input 창의 border 색상을 일치하는지 여부에 따라 바꿈 + useEffect(() => { + if (isPasswordsMatch) { + setValue('newPassword', newPassword) + setValue('confirmPassword', confirmPassword) + } + }, [isPasswordsMatch, newPassword, confirmPassword]) - const onSubmit = () => {} + const onSubmit = () => { + // setSaveButtonAble(true) + } return (
@@ -123,81 +241,240 @@ export default function Page() {

You can change your information

- + + {/* ID */} + {/* 해야 할 일 : Input placeholder만 api 활용하여 넣기 */} + - + {/* Current password */} + {/* 해야 할 일 : API 연결해서 입력값이랑 기존password값 비교하는 로직 */} +
- - codedang + setPasswordShow(!passwordShow)} + > + {passwordShow ? ( + visible + ) : ( + invisible + )}
-
-
{requiredMessage('Required')}
+ {errors.currentPassword && + errors.currentPassword.message === 'Required' && + requiredMessage('Required')} + {!errors.currentPassword && + isCheckButtonClicked && + (isPasswordCorrect ? ( +
+ Correct +
+ ) : ( +
+ Incorrect +
+ ))} + {/* New password */} + {/* 해야 할 일 : API 연결작업 */}
- - codedang + setNewPasswordShow(!newPasswordShow)} + > + {newPasswordShow ? ( + visible + ) : ( + invisible + )}
+ {errors.newPassword && ( +
+
    +
  • 8-20 characters
  • +
  • + Include two of the following: capital letters, small letters, + numbers +
  • +
+
+ )} + {/* Re-enter new password */} + {/* 해야 할 일 : API 연결작업 */}
{ + if (watch('newPassword') != val) { + return 'Incorrect' + } + // setPasswordConfirmed(true) + } + })} + className={`flex justify-stretch border-neutral-300 ring-0 placeholder:text-neutral-400 focus-visible:ring-0 disabled:bg-neutral-200 ${ + isPasswordsMatch + ? 'border-primary' + : errors.confirmPassword && + confirmPassword && + 'border-red-500' + } `} /> - - codedang + setConfirmPasswordShow(!confirmPasswordShow)} + > + {confirmPasswordShow ? ( + visible + ) : ( + invisible + )}
+ {getValues('confirmPassword') && + (isPasswordsMatch ? ( +
+ Correct +
+ ) : ( +
+ Incorrect +
+ ))}
- + {/* Name */} + {/* 해야 할 일 : API 연결작업 */} + - + {/* Student ID */} + {/* 수정불가 */} + - - + {/* First Major */} + {/* 해야 할 일 : API 연결작업 */} + +
+ + + + + + + + + No major found. + + + {majors?.map((major) => ( + { + setMajorValue(currentValue) + setMajorOpen(false) + }} + > + + {major} + + ))} + + + + + + +
+ {/* Save Button */}
diff --git a/apps/frontend/components/auth/HeaderAuthPanel.tsx b/apps/frontend/components/auth/HeaderAuthPanel.tsx index ba12eb0395..3efe1bc4a7 100644 --- a/apps/frontend/components/auth/HeaderAuthPanel.tsx +++ b/apps/frontend/components/auth/HeaderAuthPanel.tsx @@ -82,6 +82,18 @@ export default function HeaderAuthPanel({ )} + + + Settings + + Date: Sun, 25 Aug 2024 02:13:23 +0900 Subject: [PATCH 04/10] feat(fe): save --- apps/frontend/app/(main)/settings/page.tsx | 93 ++++++++++++++++----- apps/frontend/public/invisible.png | Bin 1220 -> 0 bytes 2 files changed, 70 insertions(+), 23 deletions(-) delete mode 100644 apps/frontend/public/invisible.png diff --git a/apps/frontend/app/(main)/settings/page.tsx b/apps/frontend/app/(main)/settings/page.tsx index be1248a99b..091d4279e9 100644 --- a/apps/frontend/app/(main)/settings/page.tsx +++ b/apps/frontend/app/(main)/settings/page.tsx @@ -16,7 +16,8 @@ import { PopoverTrigger } from '@/components/ui/popover' import { ScrollArea } from '@/components/ui/scroll-area' -import { cn } from '@/lib/utils' +// import { baseUrl } from '@/lib/constants' +import { cn, safeFetcherWithAuth } from '@/lib/utils' import invisible from '@/public/24_invisible.svg' import visible from '@/public/24_visible.svg' import codedangSymbol from '@/public/codedang-editor.svg' @@ -26,6 +27,7 @@ import React, { useEffect } from 'react' import { useState } from 'react' import { useForm } from 'react-hook-form' import { FaCheck, FaChevronDown } from 'react-icons/fa6' +import { toast } from 'sonner' // import { IoWarningOutline } from 'react-icons/io5' import { z } from 'zod' @@ -34,15 +36,20 @@ interface SettingsFormat { currentPassword: string newPassword: string confirmPassword: string - name: string + realName: string major: string studentId: string - // email: string - // verificationCode: string - // firstName: string - // lastName: string } +// 선택적인 필드만 포함된 타입 정의 +type UpdatePayload = Partial<{ + studentId: string + password: string + newPassword: string + realName: string + major: string +}> + const majors = [ '학과 정보 없음', '자유전공계열', @@ -168,10 +175,8 @@ export default function Page() { handleSubmit, getValues, setValue, - // trigger, watch, formState: { errors, isDirty } - // formState: { errors } } = useForm({ resolver: zodResolver(schema), mode: 'onChange', @@ -179,7 +184,7 @@ export default function Page() { currentPassword: '', newPassword: '', confirmPassword: '', - name: '', + realName: '', major: '' } }) @@ -195,12 +200,12 @@ export default function Page() { // const [passwordConfirmed, setPasswordConfirmed] = useState(false) const [majorOpen, setMajorOpen] = useState(false) const [majorValue, setMajorValue] = useState('') - // const [saveButtonAble, setSaveButtonAble] = useState(false) + const [saveDisable, setSaveDisable] = useState(true) const currentPassword = watch('currentPassword') const newPassword = watch('newPassword') const confirmPassword = watch('confirmPassword') - const name = watch('name') + const realName = watch('realName') const isPasswordsMatch = newPassword === confirmPassword && newPassword !== '' // New Password Input 창과 Re-enter Password Input 창의 border 색상을 일치하는지 여부에 따라 바꿈 @@ -211,8 +216,54 @@ export default function Page() { } }, [isPasswordsMatch, newPassword, confirmPassword]) - const onSubmit = () => { - // setSaveButtonAble(true) + // Save Button 활성화 조건을 관리하기 위해 useEffect를 추가 + useEffect(() => { + if (isPasswordCorrect && isPasswordsMatch && realName) { + setSaveDisable(false) // 조건이 모두 만족되면 Save 버튼 활성화 + } else { + setSaveDisable(true) // 조건이 하나라도 불만족 시 Save 버튼 비활성화 + } + }, [isPasswordCorrect, isPasswordsMatch, realName]) + + // API로 변경된 정보 전송 + const onSubmit = async (data: SettingsFormat) => { + console.log('실행됨') + if (!isPasswordsMatch) { + return + } + + try { + setSaveDisable(true) + + // 필요 없는 필드 제외 + const updatePayload: UpdatePayload = {} + if (data.studentId || data.studentId !== '') { + updatePayload.studentId = data.studentId + } + if (data.currentPassword || data.currentPassword !== '') { + updatePayload.password = data.currentPassword + } + if (data.newPassword || data.newPassword !== '') { + updatePayload.newPassword = data.newPassword + } + if (data.realName || data.realName !== '') { + updatePayload.realName = data.realName + } + if (data.major || data.major !== '') { + updatePayload.major = data.major + } + + const response = await safeFetcherWithAuth.patch('user', { + json: updatePayload + }) + + if (response.ok) { + toast.success('Successfully updated your information') + } + } catch (error) { + toast.error('Failed to update your information') + setSaveDisable(false) + } } return ( @@ -317,9 +368,7 @@ export default function Page() { isPasswordsMatch ? 'border-primary' : (errors.newPassword && newPassword && 'border-red-500') || - (confirmPassword && - errors.confirmPassword && - 'border-red-500') + (confirmPassword && 'border-red-500') } `} /> Name {/* Student ID */} @@ -471,10 +518,10 @@ export default function Page() { {/* Save Button */}
diff --git a/apps/frontend/public/invisible.png b/apps/frontend/public/invisible.png deleted file mode 100644 index baefa5c2d6de1b238412d0b3ce4d32a0bb201d1f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1220 zcmV;#1UvhQP)^3c1c7*RCwC$oXukNIpUbm~&s$DD=?ep{VpL^qP_zeH^`uckQ&iD6sdpsVeWdWa`pJot#e}B(` z9SZDIa_)uS!fyg*ZC08U1DFslO2^1$DLiy7m&JWv5Q!SQdrYkn^6xRl!u~2@k5Fp#_wkimXmGWzbfPg);*2H@5 z#!~q;LkLxNV<8NURf7N^;Kn}eRNWC$-XZ}0*O>8ufMD9SJh&4mBBw`r1KUz=be+573DV07kh05N5NPDFi4L7UAWJaK9WEnhb!IOMBIhV#^2& zE69lljhE}`GgmJNF_w@M)A}ydqf7uWyH{PMdO(PA4|qRT|2ZW!Z7j2%STps25aS+L zRn(N~Y2y`mLl5TxAv7|PPI_BG<2m|qVXaQ7rcP*N!hd`B5TXDuyHuftQuF!TY@QHu zL--zZEmDjXG+voctQ)|CTTRyRJ&!<~l6?gMq||$2u8>&Im}Bp6m2e~jVE0#36kq|t z695M9U;r>9PZxAOVwn)&-aK7UOnoQ_fK{yOn}o$$918-<;GyqI12-VJSxdCektbvyD995JeQUbpv1XjWkJNlE0aV#qc1!5@h*1aqsuqI)t zed4BskBph7hngg82<%e3Swn_lrlE8&wl>6sz$mwbvMgFllHrO{+J)Bch|%lFjXmy7 z`J9q%Xj!30qraM_e97e;P|{^XU)L(2I8;$f?N3O_dZ-Jt81W$16)|Fl1%#M-j~M_~ zpBT#o#|VKj`!-*k8ygQ$hZz%uDi-Imd&XmNM^!CPXG36a6SaU)WxEr70T zLaNDLffvfCFzZJ4WO)YOC;;@qusXEwqI8QH^CSRVOZNyGC@}WeO1L(BCuI^>3W5&r zE(}2c3PTWp3_pkQ&>UCsmUK?Fbm1VADH0w56p0gwoQ i07wKt03-r%Kk^TVr>tKqbH0=S0000 Date: Mon, 26 Aug 2024 02:30:13 +0900 Subject: [PATCH 05/10] feat(fe): change password name major at settings and prevent leave page --- apps/frontend/app/(main)/settings/page.tsx | 369 +++++++++--------- .../components/auth/SignUpRegister.tsx | 76 +--- apps/frontend/lib/constants.ts | 79 ++++ 3 files changed, 272 insertions(+), 252 deletions(-) diff --git a/apps/frontend/app/(main)/settings/page.tsx b/apps/frontend/app/(main)/settings/page.tsx index 091d4279e9..6250a9e4bc 100644 --- a/apps/frontend/app/(main)/settings/page.tsx +++ b/apps/frontend/app/(main)/settings/page.tsx @@ -16,150 +16,75 @@ import { PopoverTrigger } from '@/components/ui/popover' import { ScrollArea } from '@/components/ui/scroll-area' -// import { baseUrl } from '@/lib/constants' +import { majors } from '@/lib/constants' import { cn, safeFetcherWithAuth } from '@/lib/utils' import invisible from '@/public/24_invisible.svg' import visible from '@/public/24_visible.svg' import codedangSymbol from '@/public/codedang-editor.svg' import { zodResolver } from '@hookform/resolvers/zod' +import type { NavigateOptions } from 'next/dist/shared/lib/app-router-context.shared-runtime' import Image from 'next/image' +import { useRouter } from 'next/navigation' import React, { useEffect } from 'react' import { useState } from 'react' import { useForm } from 'react-hook-form' import { FaCheck, FaChevronDown } from 'react-icons/fa6' import { toast } from 'sonner' -// import { IoWarningOutline } from 'react-icons/io5' import { z } from 'zod' interface SettingsFormat { - username: string currentPassword: string newPassword: string confirmPassword: string realName: string - major: string studentId: string } +interface getProfile { + username: string // ID + userProfile: { + realName: string + } + studentId: string + major: string +} + // 선택적인 필드만 포함된 타입 정의 type UpdatePayload = Partial<{ - studentId: string password: string newPassword: string realName: string + studentId: string major: string }> -const majors = [ - '학과 정보 없음', - '자유전공계열', - '인문과학계열', - '유학·동양학과', - '국어국문학과', - '영어영문학과', - '프랑스어문학과', - '중어중문학과', - '독어독문학과', - '러시아어문학과', - '한문학과', - '사학과', - '철학과', - '문헌정보학과', - '사회과학계열', - '행정학과', - '정치외교학과', - '미디어커뮤니케이션학과', - '사회학과', - '사회복지학과', - '심리학과', - '소비자학과', - '아동·청소년학과', - '경제학과', - '통계학과', - '경영학과', - '글로벌리더학부', - '글로벌경제학과', - '글로벌경영학과', - '교육학과', - '한문교육과', - '영상학과', - '의상학과', - '자연과학계열', - '생명과학과', - '수학과', - '물리학과', - '화학과', - '식품생명공학과', - '바이오메카트로닉스학과', - '융합생명공학과', - '전자전기공학부', - '공학계열', - '화학공학/고분자공학부', - '신소재공학부', - '기계공학부', - '건설환경공학부', - '시스템경영공학과', - '나노공학과', - '소프트웨어학과', - '반도체시스템공학과', - '지능형소프트웨어학과', - '글로벌바이오메디컬공학과', - '반도체융합공학과', - '에너지학과', - '양자정보공학과', - '건축학과', - '소재부품융합공학과', - '약학과', - '의예과', - '수학교육과', - '컴퓨터교육과', - '글로벌융합학부', - '데이터사이언스융합전공', - '인공지능융합전공', - '컬처앤테크놀로지융합전공', - '자기설계융합전공', - '연기예술학과', - '무용학과', - '미술학과', - '디자인학과', - '스포츠과학과' -] - -const schema = z - .object({ - // currentPassword: z.string().min(1, { message: 'Required' }), - currentPassword: z.string().min(1, { message: 'Required' }), - // .min(8) - // .max(20) - // .refine((data) => { - // const invalidPassword = /^([a-z]*|[A-Z]*|[0-9]*|[^a-zA-Z0-9]*)$/ - // return !invalidPassword.test(data) - // }), - // newPassword: z.string().min(8, { message: 'Password must be at least 8 characters' }).optional(), - newPassword: z - .string() - .min(1) - .min(8) - .max(20) - .refine((data) => { - const invalidPassword = /^([a-z]*|[A-Z]*|[0-9]*|[^a-zA-Z0-9]*)$/ - return !invalidPassword.test(data) - }), - // confirmPassword: z.string().optional(), - confirmPassword: z.string().min(1), - name: z - .string() - .min(1, { message: 'Required' }) - .regex(/^[a-zA-Z]+$/, { message: 'only English supported' }) - }) - .refine( - (data: { newPassword: string; confirmPassword: string }) => - data.newPassword === data.confirmPassword, - { - path: ['confirmPassword'], - message: 'Incorrect' - } - ) +// const schemaUpdate = z.object({ +// studentId: z.string().optional(), +// password: z.string().optional(), +// newPassword: z.string().optional(), +// realName: z.string().optional(), +// major: z.string().optional() +// }) + +const schemaSettings = z.object({ + currentPassword: z.string().min(1, { message: 'Required' }).optional(), + newPassword: z + .string() + .min(1) + .min(8) + .max(20) + .refine((data) => { + const invalidPassword = /^([a-z]*|[A-Z]*|[0-9]*|[^a-zA-Z0-9]*)$/ + return !invalidPassword.test(data) + }) + .optional(), + confirmPassword: z.string().optional(), + realName: z + .string() + .regex(/^[a-zA-Z\s]+$/, { message: 'Only English Allowed' }) + .optional(), + studentId: z.string().optional() +}) function requiredMessage(message?: string) { return ( @@ -170,6 +95,30 @@ function requiredMessage(message?: string) { } export default function Page() { + const [defaultProfileValues, setdefaultProfileValues] = useState({ + username: '', + userProfile: { + realName: '' + }, + studentId: '', + major: '' + }) + + useEffect(() => { + const fetchDefaultProfile = async () => { + try { + const data: getProfile = await safeFetcherWithAuth.get('user').json() + console.log(data) + setMajorValue(data.major) + setdefaultProfileValues(data) + } catch (error) { + console.error('Failed to fetch profile:', error) + toast.error('Failed to load profile data') + } + } + fetchDefaultProfile() + }, []) + const { register, handleSubmit, @@ -178,18 +127,56 @@ export default function Page() { watch, formState: { errors, isDirty } } = useForm({ - resolver: zodResolver(schema), + resolver: zodResolver(schemaSettings), mode: 'onChange', defaultValues: { currentPassword: '', newPassword: '', confirmPassword: '', - realName: '', - major: '' + realName: defaultProfileValues.userProfile.realName, + studentId: defaultProfileValues.studentId } }) - // Check if Current Password is correct + // const beforeUnloadHandler = (event: BeforeUnloadEvent) => { + // // Recommended + // event.preventDefault() + + // // Included for legacy support, e.g. Chrome/Edge < 119 + // event.returnValue = true + // return true + // } + + /** + * Prompt the user with a confirmation dialog when they try to navigate away from the page. + */ + const useConfirmNavigation = () => { + const router = useRouter() + + useEffect(() => { + const originalPush = router.push + const newPush = ( + href: string, + options?: NavigateOptions | undefined + ): void => { + const isConfirmed = window.confirm( + 'Are you sure you want to leave?\nYour changes have not been saved.\nIf you leave this page, all changes will be lost.\nDo you still want to proceed?' + ) + if (isConfirmed) { + originalPush(href, options) + } + } + router.push = newPush + // window.onbeforeunload = beforeUnloadHandler + return () => { + router.push = originalPush + // window.onbeforeunload = null + } + }, [router, isDirty]) + } + + useConfirmNavigation() + const [isCheckButtonClicked, setIsCheckButtonClicked] = useState(false) const [isPasswordCorrect, setIsPasswordCorrect] = useState(false) @@ -197,17 +184,26 @@ export default function Page() { const [passwordShow, setPasswordShow] = useState(false) const [newPasswordShow, setNewPasswordShow] = useState(false) const [confirmPasswordShow, setConfirmPasswordShow] = useState(false) - // const [passwordConfirmed, setPasswordConfirmed] = useState(false) const [majorOpen, setMajorOpen] = useState(false) const [majorValue, setMajorValue] = useState('') - const [saveDisable, setSaveDisable] = useState(true) - const currentPassword = watch('currentPassword') const newPassword = watch('newPassword') const confirmPassword = watch('confirmPassword') const realName = watch('realName') const isPasswordsMatch = newPassword === confirmPassword && newPassword !== '' + // saveAble1, saveAble2 둘 중 하나라도 true 면 Save 버튼 활성화 + const saveAblePassword: boolean = + !!currentPassword && + !!newPassword && + !!confirmPassword && + isPasswordCorrect && + newPasswordAble && + isPasswordsMatch + const saveAbleOthers: boolean = + !!realName || !!(majorValue !== defaultProfileValues.major) + const saveAble = saveAblePassword || saveAbleOthers + // New Password Input 창과 Re-enter Password Input 창의 border 색상을 일치하는지 여부에 따라 바꿈 useEffect(() => { if (isPasswordsMatch) { @@ -216,53 +212,82 @@ export default function Page() { } }, [isPasswordsMatch, newPassword, confirmPassword]) - // Save Button 활성화 조건을 관리하기 위해 useEffect를 추가 - useEffect(() => { - if (isPasswordCorrect && isPasswordsMatch && realName) { - setSaveDisable(false) // 조건이 모두 만족되면 Save 버튼 활성화 - } else { - setSaveDisable(true) // 조건이 하나라도 불만족 시 Save 버튼 비활성화 - } - }, [isPasswordCorrect, isPasswordsMatch, realName]) - - // API로 변경된 정보 전송 const onSubmit = async (data: SettingsFormat) => { - console.log('실행됨') - if (!isPasswordsMatch) { - return - } - try { - setSaveDisable(true) - - // 필요 없는 필드 제외 + // 필요 없는 필드 제외 (defaultProfileValues와 값이 같은 것들은 제외) const updatePayload: UpdatePayload = {} - if (data.studentId || data.studentId !== '') { - updatePayload.studentId = data.studentId + if (data.realName !== defaultProfileValues.userProfile.realName) { + updatePayload.realName = data.realName } - if (data.currentPassword || data.currentPassword !== '') { + if (majorValue !== defaultProfileValues.major) { + updatePayload.major = majorValue + } + if (data.currentPassword !== 'tmppassword1') { updatePayload.password = data.currentPassword } - if (data.newPassword || data.newPassword !== '') { + if (data.newPassword !== 'tmppassword1') { updatePayload.newPassword = data.newPassword } - if (data.realName || data.realName !== '') { - updatePayload.realName = data.realName - } - if (data.major || data.major !== '') { - updatePayload.major = data.major - } const response = await safeFetcherWithAuth.patch('user', { json: updatePayload }) - if (response.ok) { + // 성공시 페이지 이동이 일어나지 않고, 입력 사항을 갱신한 초기 상태를 노출함 toast.success('Successfully updated your information') + setTimeout(() => { + window.location.reload() + }, 1500) } } catch (error) { - toast.error('Failed to update your information') - setSaveDisable(false) + console.error(error) + toast.error('Failed to update your information, Please try again') + setTimeout(() => { + window.location.reload() + }, 1500) + } + } + + const onSubmitClick = () => { + return () => { + // submit 되기위해, watch로 확인되는 값이 default값과 같으면 setValue를 통해서 defaultProfileValues로 변경 + if (realName === '') { + setValue('realName', defaultProfileValues.userProfile.realName) + } + if (majorValue === defaultProfileValues.major) { + setMajorValue(defaultProfileValues.major) + } + if (currentPassword === '') { + setValue('currentPassword', 'tmppassword1') + } + if (newPassword === '') { + setValue('newPassword', 'tmppassword1') + } + if (confirmPassword === '') { + setValue('confirmPassword', 'tmppassword1') + } + } + } + + const checkPassword = async () => { + setIsCheckButtonClicked(true) + try { + const response = await safeFetcherWithAuth.post('auth/login', { + json: { + username: defaultProfileValues.username, + password: currentPassword + }, + credentials: 'omit' + }) + + console.log(response) + if (response.status === 201) { + setIsPasswordCorrect(true) + setNewPasswordAble(true) + toast.success('Correct password') + } + } catch { + console.error('Failed to check password') } } @@ -294,17 +319,14 @@ export default function Page() {

{/* ID */} - {/* 해야 할 일 : Input placeholder만 api 활용하여 넣기 */} {/* Current password */} - {/* 해야 할 일 : API 연결해서 입력값이랑 기존password값 비교하는 로직 */}
@@ -329,13 +351,7 @@ export default function Page() { @@ -356,7 +372,6 @@ export default function Page() { ))} {/* New password */} - {/* 해야 할 일 : API 연결작업 */}
{/* Name */} - {/* 해야 할 일 : API 연결작업 */} + {realName && + errors.realName && + requiredMessage(errors.realName.message)} {/* Student ID */} - {/* 수정불가 */} {/* First Major */} - {/* 해야 할 일 : API 연결작업 */}
@@ -475,10 +488,12 @@ export default function Page() { role="combobox" className={cn( 'justify-between border-gray-200 font-normal text-neutral-600 hover:bg-white', - !majorValue ? 'text-neutral-300' : 'border-primary' + majorValue === defaultProfileValues.major + ? 'text-neutral-300' + : 'border-primary' )} > - {!majorValue ? '원래 전공' : majorValue} + {!majorValue ? defaultProfileValues.major : majorValue} @@ -518,10 +533,10 @@ export default function Page() { {/* Save Button */}
diff --git a/apps/frontend/components/auth/SignUpRegister.tsx b/apps/frontend/components/auth/SignUpRegister.tsx index 182b0d4b03..cd11109a0d 100644 --- a/apps/frontend/components/auth/SignUpRegister.tsx +++ b/apps/frontend/components/auth/SignUpRegister.tsx @@ -16,6 +16,7 @@ import { } from '@/components/ui/popover' import { ScrollArea } from '@/components/ui/scroll-area' import { baseUrl } from '@/lib/constants' +import { majors } from '@/lib/constants' import { cn } from '@/lib/utils' import useSignUpModalStore from '@/stores/signUpModal' import { zodResolver } from '@hookform/resolvers/zod' @@ -39,81 +40,6 @@ interface SignUpFormInput { passwordAgain: string } -const majors = [ - '학과 정보 없음', - '자유전공계열', - '인문과학계열', - '유학·동양학과', - '국어국문학과', - '영어영문학과', - '프랑스어문학과', - '중어중문학과', - '독어독문학과', - '러시아어문학과', - '한문학과', - '사학과', - '철학과', - '문헌정보학과', - '사회과학계열', - '행정학과', - '정치외교학과', - '미디어커뮤니케이션학과', - '사회학과', - '사회복지학과', - '심리학과', - '소비자학과', - '아동·청소년학과', - '경제학과', - '통계학과', - '경영학과', - '글로벌리더학부', - '글로벌경제학과', - '글로벌경영학과', - '교육학과', - '한문교육과', - '영상학과', - '의상학과', - '자연과학계열', - '생명과학과', - '수학과', - '물리학과', - '화학과', - '식품생명공학과', - '바이오메카트로닉스학과', - '융합생명공학과', - '전자전기공학부', - '공학계열', - '화학공학/고분자공학부', - '신소재공학부', - '기계공학부', - '건설환경공학부', - '시스템경영공학과', - '나노공학과', - '소프트웨어학과', - '반도체시스템공학과', - '지능형소프트웨어학과', - '글로벌바이오메디컬공학과', - '반도체융합공학과', - '에너지학과', - '양자정보공학과', - '건축학과', - '소재부품융합공학과', - '약학과', - '의예과', - '수학교육과', - '컴퓨터교육과', - '글로벌융합학부', - '데이터사이언스융합전공', - '인공지능융합전공', - '컬처앤테크놀로지융합전공', - '자기설계융합전공', - '연기예술학과', - '무용학과', - '미술학과', - '디자인학과', - '스포츠과학과' -] - const FIELD_NAMES = [ 'username', 'password', diff --git a/apps/frontend/lib/constants.ts b/apps/frontend/lib/constants.ts index 1666de92cd..5bdccabc12 100644 --- a/apps/frontend/lib/constants.ts +++ b/apps/frontend/lib/constants.ts @@ -45,3 +45,82 @@ export const levels = [ 'Level4', 'Level5' ] as const + +/** + * The list of majors that students can choose. + * @constant + */ +export const majors = [ + '학과 정보 없음', + '자유전공계열', + '인문과학계열', + '유학·동양학과', + '국어국문학과', + '영어영문학과', + '프랑스어문학과', + '중어중문학과', + '독어독문학과', + '러시아어문학과', + '한문학과', + '사학과', + '철학과', + '문헌정보학과', + '사회과학계열', + '행정학과', + '정치외교학과', + '미디어커뮤니케이션학과', + '사회학과', + '사회복지학과', + '심리학과', + '소비자학과', + '아동·청소년학과', + '경제학과', + '통계학과', + '경영학과', + '글로벌리더학부', + '글로벌경제학과', + '글로벌경영학과', + '교육학과', + '한문교육과', + '영상학과', + '의상학과', + '자연과학계열', + '생명과학과', + '수학과', + '물리학과', + '화학과', + '식품생명공학과', + '바이오메카트로닉스학과', + '융합생명공학과', + '전자전기공학부', + '공학계열', + '화학공학/고분자공학부', + '신소재공학부', + '기계공학부', + '건설환경공학부', + '시스템경영공학과', + '나노공학과', + '소프트웨어학과', + '반도체시스템공학과', + '지능형소프트웨어학과', + '글로벌바이오메디컬공학과', + '반도체융합공학과', + '에너지학과', + '양자정보공학과', + '건축학과', + '소재부품융합공학과', + '약학과', + '의예과', + '수학교육과', + '컴퓨터교육과', + '글로벌융합학부', + '데이터사이언스융합전공', + '인공지능융합전공', + '컬처앤테크놀로지융합전공', + '자기설계융합전공', + '연기예술학과', + '무용학과', + '미술학과', + '디자인학과', + '스포츠과학과' +] as const From 290467d6a110233f5e562887f1e8e3792717e2f3 Mon Sep 17 00:00:00 2001 From: jiho Date: Mon, 26 Aug 2024 03:10:29 +0900 Subject: [PATCH 06/10] fix(fe): add loading --- apps/frontend/app/(main)/settings/page.tsx | 30 ++++++++++++++-------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/apps/frontend/app/(main)/settings/page.tsx b/apps/frontend/app/(main)/settings/page.tsx index 6250a9e4bc..1269a5ab56 100644 --- a/apps/frontend/app/(main)/settings/page.tsx +++ b/apps/frontend/app/(main)/settings/page.tsx @@ -49,7 +49,6 @@ interface getProfile { major: string } -// 선택적인 필드만 포함된 타입 정의 type UpdatePayload = Partial<{ password: string newPassword: string @@ -108,12 +107,13 @@ export default function Page() { const fetchDefaultProfile = async () => { try { const data: getProfile = await safeFetcherWithAuth.get('user').json() - console.log(data) setMajorValue(data.major) setdefaultProfileValues(data) + setIsLoading(false) } catch (error) { console.error('Failed to fetch profile:', error) toast.error('Failed to load profile data') + setIsLoading(false) } } fetchDefaultProfile() @@ -138,6 +138,7 @@ export default function Page() { } }) + // 현재 뒤로가기 창닫기 새로고침은 모달창 안띄움 // const beforeUnloadHandler = (event: BeforeUnloadEvent) => { // // Recommended // event.preventDefault() @@ -152,7 +153,6 @@ export default function Page() { */ const useConfirmNavigation = () => { const router = useRouter() - useEffect(() => { const originalPush = router.push const newPush = ( @@ -162,6 +162,7 @@ export default function Page() { const isConfirmed = window.confirm( 'Are you sure you want to leave?\nYour changes have not been saved.\nIf you leave this page, all changes will be lost.\nDo you still want to proceed?' ) + if (isConfirmed) { originalPush(href, options) } @@ -191,7 +192,7 @@ export default function Page() { const confirmPassword = watch('confirmPassword') const realName = watch('realName') const isPasswordsMatch = newPassword === confirmPassword && newPassword !== '' - + const [isLoading, setIsLoading] = useState(true) // saveAble1, saveAble2 둘 중 하나라도 true 면 Save 버튼 활성화 const saveAblePassword: boolean = !!currentPassword && @@ -204,7 +205,7 @@ export default function Page() { !!realName || !!(majorValue !== defaultProfileValues.major) const saveAble = saveAblePassword || saveAbleOthers - // New Password Input 창과 Re-enter Password Input 창의 border 색상을 일치하는지 여부에 따라 바꿈 + // New Password Input 창과 Re-enter Password Input 창의 border 색상을, 일치하는지 여부에 따라 바꿈 useEffect(() => { if (isPasswordsMatch) { setValue('newPassword', newPassword) @@ -269,6 +270,7 @@ export default function Page() { } } + // 확인 필요 const checkPassword = async () => { setIsCheckButtonClicked(true) try { @@ -280,11 +282,9 @@ export default function Page() { credentials: 'omit' }) - console.log(response) if (response.status === 201) { setIsPasswordCorrect(true) setNewPasswordAble(true) - toast.success('Correct password') } } catch { console.error('Failed to check password') @@ -321,7 +321,7 @@ export default function Page() { {/* ID */} @@ -460,7 +460,9 @@ export default function Page() { {/* Name */} @@ -471,7 +473,9 @@ export default function Page() { {/* Student ID */} - {!majorValue ? defaultProfileValues.major : majorValue} + {isLoading + ? 'Loading...' + : !majorValue + ? defaultProfileValues.major + : majorValue} From 718debec1bb0b0acc7ed0465ff4c4f98400885a2 Mon Sep 17 00:00:00 2001 From: jiho Date: Mon, 26 Aug 2024 03:30:11 +0900 Subject: [PATCH 07/10] fix(fe): handle href error --- apps/frontend/app/(main)/settings/page.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/frontend/app/(main)/settings/page.tsx b/apps/frontend/app/(main)/settings/page.tsx index 1269a5ab56..7d676e6a28 100644 --- a/apps/frontend/app/(main)/settings/page.tsx +++ b/apps/frontend/app/(main)/settings/page.tsx @@ -162,9 +162,17 @@ export default function Page() { const isConfirmed = window.confirm( 'Are you sure you want to leave?\nYour changes have not been saved.\nIf you leave this page, all changes will be lost.\nDo you still want to proceed?' ) - + // Error occurs if I just put 'href' without 'href === ...' code.. if (isConfirmed) { - originalPush(href, options) + if ( + href === '/settings' || + href === '/notice' || + href === '/problem' || + href === '/contest' || + href === '/' + ) { + originalPush(href, options) + } } } router.push = newPush From 48b89b129283bf70dfec6dbaeb442fb34ec0a602 Mon Sep 17 00:00:00 2001 From: jiho Date: Mon, 26 Aug 2024 12:18:23 +0900 Subject: [PATCH 08/10] fix(fe): change checkPassword --- apps/frontend/app/(main)/settings/page.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/frontend/app/(main)/settings/page.tsx b/apps/frontend/app/(main)/settings/page.tsx index 7d676e6a28..84f032910d 100644 --- a/apps/frontend/app/(main)/settings/page.tsx +++ b/apps/frontend/app/(main)/settings/page.tsx @@ -286,8 +286,7 @@ export default function Page() { json: { username: defaultProfileValues.username, password: currentPassword - }, - credentials: 'omit' + } }) if (response.status === 201) { From 7ad0e00e3aa55b5135a77efee1e571b7ecb82fb6 Mon Sep 17 00:00:00 2001 From: jiho Date: Mon, 26 Aug 2024 14:10:40 +0900 Subject: [PATCH 09/10] fix(fe): change fetcher type to safeFetcher --- apps/frontend/app/(main)/settings/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/frontend/app/(main)/settings/page.tsx b/apps/frontend/app/(main)/settings/page.tsx index 84f032910d..00a6a342b8 100644 --- a/apps/frontend/app/(main)/settings/page.tsx +++ b/apps/frontend/app/(main)/settings/page.tsx @@ -17,7 +17,7 @@ import { } from '@/components/ui/popover' import { ScrollArea } from '@/components/ui/scroll-area' import { majors } from '@/lib/constants' -import { cn, safeFetcherWithAuth } from '@/lib/utils' +import { cn, safeFetcher, safeFetcherWithAuth } from '@/lib/utils' import invisible from '@/public/24_invisible.svg' import visible from '@/public/24_visible.svg' import codedangSymbol from '@/public/codedang-editor.svg' @@ -282,7 +282,7 @@ export default function Page() { const checkPassword = async () => { setIsCheckButtonClicked(true) try { - const response = await safeFetcherWithAuth.post('auth/login', { + const response = await safeFetcher.post('auth/login', { json: { username: defaultProfileValues.username, password: currentPassword From 1565eb92ab312eb307d4e2d3eea9802dcc6494bb Mon Sep 17 00:00:00 2001 From: jiho Date: Mon, 26 Aug 2024 14:16:40 +0900 Subject: [PATCH 10/10] fix(fe): delete comments --- apps/frontend/app/(main)/settings/page.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/frontend/app/(main)/settings/page.tsx b/apps/frontend/app/(main)/settings/page.tsx index 00a6a342b8..fa7821ab7b 100644 --- a/apps/frontend/app/(main)/settings/page.tsx +++ b/apps/frontend/app/(main)/settings/page.tsx @@ -138,7 +138,6 @@ export default function Page() { } }) - // 현재 뒤로가기 창닫기 새로고침은 모달창 안띄움 // const beforeUnloadHandler = (event: BeforeUnloadEvent) => { // // Recommended // event.preventDefault() @@ -242,7 +241,6 @@ export default function Page() { json: updatePayload }) if (response.ok) { - // 성공시 페이지 이동이 일어나지 않고, 입력 사항을 갱신한 초기 상태를 노출함 toast.success('Successfully updated your information') setTimeout(() => { window.location.reload() @@ -278,7 +276,6 @@ export default function Page() { } } - // 확인 필요 const checkPassword = async () => { setIsCheckButtonClicked(true) try {