From e9be11bf80ee3920c076cb641604509c21b7824a Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 5 Jun 2024 10:36:40 +0200 Subject: [PATCH 01/19] Create paginated Organizations list page --- public/images/fallback-account-dark.png | Bin 0 -> 35261 bytes src/components/Organizations/Card.tsx | 50 ++++++++++++++++++ src/components/Pagination/Pagination.tsx | 45 ++++++++++++++++ .../Pagination/PaginationProvider.tsx | 50 ++++++++++++++++++ src/pages/LoadingError.tsx | 16 ++++++ src/pages/Organization/List.tsx | 50 ++++++++++++++++++ src/pages/{ => Organization}/Organization.tsx | 0 src/queries/organizations.ts | 29 ++++++++++ src/router/index.tsx | 11 +++- 9 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 public/images/fallback-account-dark.png create mode 100644 src/components/Organizations/Card.tsx create mode 100644 src/components/Pagination/Pagination.tsx create mode 100644 src/components/Pagination/PaginationProvider.tsx create mode 100644 src/pages/LoadingError.tsx create mode 100644 src/pages/Organization/List.tsx rename src/pages/{ => Organization}/Organization.tsx (100%) create mode 100644 src/queries/organizations.ts diff --git a/public/images/fallback-account-dark.png b/public/images/fallback-account-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..e0ce171c16961b4e9613e4c246c02a408d799a3f GIT binary patch literal 35261 zcmXt9b9Cii7k#yDbEdX!>(%Ylwr$(?)Oxi&<<$1no?>eI>+g^6tt22HGMuN9`e+Owrc4-_+3)|wxH$8d zskH6o7HIMIrfb0DEB5?i$5#9Jv+nPm!(%xNZBsKFNvbhF4dv&DZ9W?1d%V^0^<(*U z=M`~OC7*xE-MM~NqR{46TmIH3zeqs#fkS{xK;FC0hyTs~lefgngl>8r&mL32tN3Vu z(6d8yz{}2^pZN8R(gxN9Q`_zR>(=H40V@|vHFNPHT~YTdC*LKxhf*1TPaBrMy1&JT z{&wAT-BE-A{qw_wl7G#tnCU|u0=7HuZuqT#Z9Q($Zwxvi6J3^`ksqLjk+f#TGK1aW zZ11(o26H;%k!abrehj^zH1)9R8qB>`_8U7_w=Pjxg9)p$4tW@L6K`*( z5{_;GdVw^1O>7}>QCL^sN(g1IIEL}TZ&$Sn`bQ}`ShRx@T{aL|f5?98#MfE0xc!O0 zWn&Wm?bN6~Dr~f}WPI(8U5ma+{!PthXm*rFXS(AcJ7+>5AvrotSGH1ul*<~V;lj|e ztl>k%z+uyorlDig6G!X=4&lPo^RVngLebd|gH+4m`M#5cXOYc$OW!oj`AQgbrQLc% zTUFcog3ou+92BMJzG?R;py&uw!tH)TQ~uNa1usLZ>tt=JZNq#0+{4$PrB40*a3!F@ zde>%YPlu+b*581G*saCCOB^-Vv<8n3o1uPBolaO-swT^!pWgbg+5DFjn6_!6w#~l# zY;;4YyXYjBuR#|H-DBPI)Zs(jGQ-EhI1j_-=g2ru`o)UdYmWN*UdO%0zm%|S)%;Px zpF(!+tKooW!lU00JTCD@MyU&J4tXOs?EC&%cWX6r5t9g(+}eJX8ovmcCAt<=vI>gbLW*hL7af6dK*Ibw#)q=zx*?PDk1VM z|9(#$PG(9E^4S)?HgrzduFSi!2ZMiAj`sYSvvZ1(pWo+iRtt-5J%QUilVi4DT;KjzugF25uM1-nu)t z1zr3D@i+-AMXi&wiF)-A%PGyq4MX+m?nO09HtxM&c8M=+dsEM(jl+~J=IR}F<9h(pt*lS{wpWt+WEqP@( zC$`0$YKdfl)ae_yf=Dgin1f#k!tENn#PhXN8=Qy%<>mr7=#>YSc`Sj6fhz(wkViJ& z8js7KRA2`|f;TN&fuxlm8sQ#&sf&XhA-kJ4s{7D5=&C2n!ZX3H8&0hp?QwF$&&^Uz zPUVe_7Un@<2icMvnn`2H(-RUi=RHFNelD^;6l0^Uiv!FVyUguo*Iklqd<9=JazYQ4sT?H$|c7aiV7 z2D6%2fQ_LA>y~=#!qiX2Z4T%)==(Z4ND6ug$ub1DP7yc0hRQJODt4ieiteB{aDEAAG6r{G>qV8!nNk5z^qw3 z`B9TP_&b1lXdf%~E8t_r5OfP0m$AJpdJcuHoxRfGK3Vemqv!e$gS58Y)2Y*cpdkO{(U~`rpQVMNU+WX+V7WCiptKC5#&o%sL3t zsr!`-_XaGTPID^T$WkcTM}ct24o;11({1A!Qf?K|Z4)D!S#Lw>Eyb-KC>27A18>Wt z%rPr=(SuzOtqWJ`ix{4BKJ@2B(Kx)-h?*S?kyQsODoLa|L63C_1C44EmlbZ~FIn2G zOvgWVEO`6F;x0qy8%Yz&?*T>*U^5zC9N-o?BsJX}Sy9DK%Y^5X1f@j2T8lpjX-Lfl&Y3#xiGk z4hNX7)U**yHvvi4Z$xSf=Sfe~Gc+4=KCgCZm;HWquPZRagbLw^AZucUh41=EA8XNN zQ(?CqU|_vdC=xqbD#nt=C4A5P2E7Ni62d7Vf$^jZ!&^t5jA&qthysT9xTts8-maBK z1{x*qgA@+ei^YTxabQxpDMU8FZi@M;~eA@YhC&qBX&R~YkTt^8tvIt#OmkDV&$OJ1n zN4SB+#ydEEh)|#SBd~YQoDMc<83V#p^dqo}Q^YHpfTSrTBV$11P$>qJ?zY5VqdI88 z9dbn#V6yTvpOIx%r6dx8=)VZbF#2dV)q9EyMC1-%Y5>D$0H;7h*F|Dfr0s4g`AmM}#Q)fUlPsEBSw226Z6vM_ap z*;K~t=3qR)yB4~xuSXp)oSuS$u`&~lIwLC7p4BReurtPS3j9eWKxzq#i@0e6b)0%I z61)?Uw!1S5*9l`)E5;{@c=4#BbUz=LI5&8GfTMMjnWyLe}vtaccwW3@d?+q+%!8{n!qm|=KBFu3D4%jMvQJGj<(U99>H zM)fk0+-^rK^bDQ-hM`qdp*G9zC#6NUEB_Grq11?PrN$%w*avj5bv%|wfk!!zR)}Mp zflGAcM7|g?i5C}H*v|fh-RQuwhME#lf?Hfu!W2 z#neP~`rt;QDItRK%-ksnb8`4CTX;O}bs)QdOuLqgAg`%$$1m9Fv@tsFBOp)oEU98SyAJnit*e#i(8zdidI6cM4 zIuxVm5)Al+>7Kc7wHDb6Jo(S6a(XQYWehSLf(b;HlJIT@Mk-1@|EuQZy)h+;T@wH4 z12p770)C{_bPiqR%~W~IH$z=W*kk|qoT#yCL{Sc684$z^i+MsKn3KIR^pIl-qml)? zX(~J<-qj?TGr=F*JLOpt)GnelE@@S)Csd&Lj~uW~@1)&s4Dvt1Hrw$^`>ev?2!Dn% zN0=K2Vcj~;?#dn_n_lqhumfe(&_u-)i^+=A2>`3u9(J+zBH6fofl5&o9bv63*bj~k z^@(Q{5~Bnmu$1-beN>HRz29Ix4o*$~PE?@d9JdUDzNOaW!W-u*g)d{M#DubqLV+)7 zj0jdci$)VsXX3?Bfm3Y2T?_4uEzG4@?1uz`0nrcG*txZ>hU^o0EOcW^MJ#{DADW)$ zobt19Ghrc3TU+d^nKqrez(`+7m7EnPr z)u3(rd4Qg!e3+GsZxf8T7cIFw;12IcC7Y}HJG$mYD*aKOO(dipN{!5#r?dfTP?|Y8 z6)*iHT3Wt=~P&9;uFA*3!wPQC z%k_I658lE31uaBD3$6qMY_X1dm8hZqC}yNd2Gf=7SB+ASCmQ-(J3u)$(EKN==YsQr}wAs zAn$0%17qaAcV2e#Enn$i2x2G=4X~Nc0(TjsAZ3v@SFV43*euy>QR*1b~kc5({MA$;(ALNG@6U(IleSln;4 zxE;YiHZ3VlBxqw4Wt;XW78&2dWn1cenfbq;7Y1|cL&D-&vXr5ifl*W!l%U&RBQvXE z*`R1JKNtNNh@2HZ+p8$3FcmkEBI7QZCkuuiK!7=dgQ`C!T~?HUY%3A|4OB9W_CbaL z!xN~Qu+_NY(XeliH%2BJ3T7I^vE3t;w1CC)T!M~ERJ27Nt$vzvdwW-U?4t6284Cmq z(~J}02}Z2L0)mBoy1T43Ya}DIi10Bk#0RuQauiyWvC;4xsKdJI?W!mi7GebCt+Mf` z8ApM&Mt>b`wC}aCWEbqT(c$eO7U!njTPl;r)-Q}CJ%y3eQ1_|Y1S!1LwqUTuSh}1{ z*NZ-=Psmw?_);C0&`NNLgTu@CXx2jjr%Pv}E4~S1 zL=hNLT(f2#4hci91$Lof;gVp=@Sc&z4X@Nkh%*5wCs5~3sgO7{XCM<) z*!O2@)aT@km4_H9NmxQkj^}?LQBJAt)@dN`#kOz#Ro~lr+eylID+LE8(H=pf55m^> z=;{W;cw}bP)f04asy9C>;2yXFqq_aR)-D)d#* zAsD=+ihAxt??Y|jlU|`BQ-T!c zD7edobGZZx1skddv8;sm60xKgz!N!RZGVRu%~eO#(6oaRqc>Ay6C?={rY}%4an))T zuf7`orJg5cRy>oKjFTNpLdJKU83;#~5p#MI#1V=o!3R_7#5KTBJirtP?ueq;>6;VnoDQooNKi0Uf4PeA555uxmO*BT?J`vC#yX!v#wQd zM!5&w?^JYis_ocpTp>(#InoFG3F7kY-*qGK&`h>%r6%Gl?_PK?7qpa#;leF2KSmg?^{ zf`3`~W;G8ZS7_8!cH>{taNoup0lD*H4OJ?gs{W?Q2RyNit<*7!hwNlgzkkm-ORX+=N+VOkeCUj+2%Qzl7**`5+5A|QDq{LBwz6ptD!r0eRy*+uFVLjJvtLbEt zq&d^xtnQbv)I3=;hH~cy_JSAFhpM61gBTPFT5I#< zbF8Rl-%@Frtc^it()#oyV(Rso?gbyu+P^99izk&i_ zQw)UVxqDdOS5*RpcLv(`?2pB;E$8i?sZ_(0@NtY3Anzq%Ne(3y!x;cTDR@;JSKuOe z%_~SI7~^g=+0tvJ?;OTTCl!j>UHv+^uhzwC>O|r}x+S#q6k#oDx?zlI$WuRA8~kq{5|Y&bA1%_F<201(kbo(XA=D{g)IK!vpXnsa^-0 zI}Eq6%FQBA6wjf*>D`2l#8ykY101k~;@w?5%XziE!BNEG9_Mh$22DVl?|&;q@eO%v zgE>V3qz6QQ=|q5WkRRPOp;dXmp50fjOAVBy%t(LO>pPctbbGHXL-W=!M$90Ib9^H~qVr>lw)dZSlTCQKywayCHRp|`pgW=@3yp^+^r@VD;oisnLv64 zB~#G~eh48R)MjPv$hK#~;WmuwulX@9Ir7upQG!?a`vwtKfm7MJ%r&Kp-s!!yu`+}@ z_(`)+-~icK^-3TKqA*@;w7m$K636m4`)FnhcSx4DMw;8A z-utb&uj`Bs{o~l`{4}pf=H`D~o^s)$9T2s*!vD=)3y?Ow6R{|49=Y5G$z;JMjz!`n zre$gzjdo}M9S1Y32Ayc8$X&oaQS)&_r1Ft5uabtDlXoL=V$l}++GN|z0a zCf~nrlprMj)#KX$-Et*LvNBtLN(_lso8W#|krjyi#-p}8yM+jH!}$~6wh$JyNqQQu zpaVf&QSt|aleKA$XF$@co02m(fr6^LiuFjd5vq-%cbs4;G>OV#T%<=v@1nUPiwq68 zV;F3wZ~QlsvwwGQYxLuI>8G@F49twV=#n)!9+lniKS}-mMY$6nnfUD`MQP<1Qo(-; zS6GLQ84dNIE~|-T-^}!$7|y>HIIWTDOU*6g3u0N0(FP#8xk7HrY8q{bYUIO}Y4QaZ znAc(q1z`&8s6s2FU5C7+Qh zC#~LND5*W-%-qK#+Mbr2hv*ohIR#pehSqZhUv6fv#kaU89)$I%Cl5PH;T)3LH_(In z{2Rtqnp;3)C7H{^i|U~aU_&i@6UN@$0pD9hjCqk#l55YD6s<_QcB;eOv7bX{pA6DS z$I_eQ!I2glgP>b6m@a-hm2(i7p{Q_etsKz5JiG#GdomT5t1_U{}U3VxJSBKU2|@Y zyjgbOT6(q8uW`8WMACJhG8=E|R$W!S)1#cmuYJS$x@1-3N~i0XpG>visD3L(;qsTB zoe5Qg=i*~jh1mpdXaakNZDkFrLImmK#P!^x0*J51g@oF;p>_{%1@r@DtYv|ONs*qS z+T}q|S*eVQOqf_O>)tfH%SwXcdHW3A-g*6Hqfz_9aDlj^_-l*5J&J0K*ohLAnegUA z&cG?!Y!;AUHtI!3-71i=q7X%s+08D`$*kd<$jC5vsey-FX2wh$sQtV}A^F=#9r_pao#G2e64yd?HxXlK^^DFu|I z(66C^h}Msjz`@YyLpO9r6z8^(TI(%YY1&Xe(U{+%!KfOmZ$UU3Ox^B_GdM1!JiTl% z=zAas6ZXE)9qK12U*>5qqQ^Dw2q2J%hW<8PK*O!6csPqw6yJ1!!SG5;MZG8|o3>>x zS{EhMN+~>KauoUlPS%rs?k)@r%~f6cyd@|(vHVnO;UOToInnQL3x-jHx-RF^&&@!? z#!B1;*r`%=xiap>fwV??k+3xm8eAr6B4wEP7Q8XatG+W&@a!nGpc(n&^uS^L-TI}I z&}}sv0%fTR%fG7cOVxPr^J>Cd0;G?NQAN&}zV?iP9_`TpnL*rNBqyGL3eaRWC^RVh z_rUMi!jgAZ4!KH|>US?viMTq)jD?6wY#JilI!8Z<>uT0t+0AUS9QGXLpgiH1`(KO< zz~H-;3m9GU&5NB?O9f8XlX^;CZ%ZGwl+oWZw%jr0EN~2eXVALJQZnCl-Y9Bu<_B9c zXe)cPr%iA@@drH_l2HR6^2XP3!T$CqRQmIj#h@M^>j^d39M&f*Zo;4jui^WuOpsJe zGvM6+F~RBRqMYoSq(dFy&gp^nik-|rBviU@7b-O%t%s;D$y2{VBr-#*^MBMZArC+} zy}6Dxj8VKThf0yLn|Q7ggyE1U)u&QCt4JdH*Y`FUZXK-9p02L8JivLtkjR!ia)Vqz zyeJVV_aee2EOdrhh)v1$FKSVaH$E_Ypm4o>`}0b0=eB#kA5$6RgZZt8zv2+!g}U|V z;lb;8SCvPr)NLh}lzGleEnP+?iq_5QtVL)U&Z7d?Sh>LxJYEAR$_~bmGiwY%YF61t z=bF#AsU=2LFp87 zO36(o+g_c*0mQ(pi=H5!O@+Y90Y<*9G{T)Ac%qC;EKijuSEoBEcCD9tW=&q{If|Q4 zD?;(5FY+pa9@{8y*ULXf-DdsLEq6Cw;`-;zYUszpDeoa>m$*AVk!dm#`FlPo>=kc4 zbZ zxHASG^f{`Xj0Bk0FxMSDY->r2OPo}Rsi+=SAs|hu6hV8INO|B*99zj#x{)fB_}3fw zYmn{j%}<%boF({dr5c8YN|SL=XZA}suct5JoI>`Vm$QmUv5a5QycS+~`0LH7&Gg@- zDUD6(k(@_>X4LQj zk-en4OH=kO$sArC9b@Wb#xtnz{fN@y!&vb>^9N@fHjpR4g)&tgmZp%t3*6u$y_%>4 zu&2nOvX`h%;X^?>kRoDPdKx_gj2=W9=rcI(Vp!>V@$3$z7T|7!7M`Vt|BGvpjf*&$ zR(kexUHX(<>-(~N=y3CDS{r78)vCNfB%rLAd?)jM2eeOcV)4l9O?=eUNr{2wm8-}=y3j$ zkk8zST%Q2a$l6V54Or4ib=Ul>t@%8>?8Y_@2-Sp;NcFr=^O`)@&?c^1Q0LpwYba3| zbe_ajpPOSK4f>a@wO}$Oz)_c;`cd!aa!L7AdAPt?xXc&}5gKUV5SWcGWLE5F%t*!T z2RL(W4cVaxa`t4{E3H_vsr(MIQn{>Bj6dZsPV$nl@PnNKbWtWnyNk}bSd%*|+s81Q zPAjwq=3bn(i@#U(RTX1Xvs%$f*Hi?dY9_aos<;7eC>^!BAmf<Imh?^8|q|zVnosUoQvCDPNAgQn2=iC&bUl5IChXepN9mjOAlz$WP(FNq5JL z1=uGwDWmjGno`MvV(5i#Eyjbv1(C7=Gw{U?QL!87r)Jo*9J`AwsF@}$!)LabMbB6E zmbhs#TPs0@rDCvVM0w*4NIGOChu20ihFH|x*18Ziuof9lPX{^i>6oS`Kpdga5(P|Y zisNI)>>rt4!w*3$y9@_24j=+0cORqhrXL~%($Q!v866&m26wcvSH z5R;9NxPI8`e63J*W%1~sg53p~AIeF*BL!L+C;MV>;Z5HVHC!DwVbMPfiwb&VceG2L z_j*q-lX!5(jLN*6=L&5V2J4ePnftNI?W^Q>jP+ssN`0K>{NyaF`{1f)Tk~@n9&myv z@o*C}!OFt@c3 z`c+j4%Hy8(@YFmU~pt)wc~$ zs5QU|R@?h4w4p5{ZI{SMZDa9wlvoX5B_gzwYAj)&u4;J=B4VWrb#q6rkovpRFO!E&{~iFt3SIu}iBa$a8IS?nmHn8@NYt=ULi^Gz3Z^Vkiq zoR8gZ1n~x0$tn7(0vq);M0{pmLfVYp`mpN4C9qiiyuua>A5WXBPZM9TF+zQsNFyp{4bF2 zk7HSnuOWOnXQ|-fUw|@)`l+Xeww!;7cLi_lqX4092uD<82^(6cUU>`ih~_to;jM6Z zv{e|~X)P#u0(_}^&KLx%sA>y<3>Ds@KmmNMh;>M=hPDykJ>@s8)#4(2@{9|eCc@Vp5KtkpUi5iba+Mv`804`bo=0P z!m$_!Ri|e}YFfCXVnDaeZdnz!ifL(z2wd>lx+XVkDfGEgpW4v2wN;M_{9h-A_RTXF zSDX2)g(njvrWKmAzmP~^?YAq-IQqYl+$l;R6QzBVab*-f&po<3r?!!9Pu6N&>sy)C zHY0k{M~DIhK<9;P$VIcqO;cOk$DN&=0_*Ci>jsj5-*+85jN#dwHwGtLP zwBF(wI{N3~BX@ps5DRzHSA>0C?FA}wJ6n%-y!Jaw3nAl@O4J1UfJJpJDaU}YxUC$FL?}qP2bZ|a z6aqJYFS^0X5orkf-a@p`iA-Mo>JzIWz>R~4E6V6VtSW~{co9r17JCUKV>7M+X>^4y z!cm&2yXH!$lcpK&FDK~>DpV&B48kEEsPr=^*yR-BllA7In<%sKn-=GBLV(ztyg{Xv zHFATc-F2RQc0Ym@!@ctsu4zrHbc;bxDk0q1Stf&&K3B4e0=u|3nTm-EAEvpngb))@Yh)Cv!ESWU&T-)d^?7s?X6xZn?T*fTn&sfq4PKc4~ zxWd9@PSTpe9asHghZg9b#id1II)`35C29g&1RK|Wm(q)vZ^;SMtE_NXZ%%iJx2CG6Q*hssN-Lfl%EUWZ$GtFF|N2{I> zv}0v~gYI1avdp3li>`n1VvemG|BF=Ej=TrYa`S%5c`c>W6XPhFxy!@jF#w?H9De`q z?gIdTg;|M-0cFI*{$IQNRk57wmn0xP2*eCiQY}>hA-gB;B^L5(qhie|Q~^Wb3pFg4 z`VUG2W=qJdoSZOq5s*LVHHdU`-P|0pAp1#BrKqSRaA>Z7mCTY4!(==r-dP<=Bsi94dPO=_R(!kB!yP=S5UmEjIyl&-JX-;V)UPTQx zO)^&v5e%v3sE6DNI%Q&+r9I_(E|x$?k#y9yzYTZn)c3Isn)7lf5q`%;AgXp{rpv~^ zrgTzUavjHEWvt3=e3Wh|3x27(Pr8RCoE76jk^V%3B zae71Vz;sUQ8*K=LXCI9xc|-A^Ssqu4?UE_vo_#l7Cm9qr#IanqdwpGP&7lhX#A}~l z|B!{q{GjaFS}u|j{^WFY+FVW**l~uz^<_0xb*B_cOPqnYw765yHr&C6t?h8J$=kmJ zlU}G!=IZMwV^%5n=a%5{=sJDA%~$jM4oFdSZkDOL{r6Q<4s9$aB@Q3~gaG(tVjkzd z&cHcJYrA}vnalmRf|UxS|5tzx<0_*d0doY83{OPyz*~w10FVM?#6{FR*Z%eRxaR!z zqI&Y#Lcs|pl@6p7P70OAPnOO}H2sr$zM4)%ZJ&J4Ij+cuz}Z%|qFk=nSO8(Qu6|gJmd-*!(@+^>n;|UX*CF4OdY3mF39{F0*)8?9=nI)SqR|u45 zZkoM-Km~|XW1@c@L57G(e0rYr%<30!LWhWiWmURX1=JXt&j+MG@Vr0`)Ues@$d@2emC22VWL>uQa*x&$g0e^ zsp6Eu{iFlC%n&;tAO5?KuU@96pi~htA<6}vuZD~kGJVGu*6(Jn2v>?5uxQOl}n z!+0Hy9d?8$+3ERtsU`dPTG8`=B&``2@#(T`070(zw>(J`aM0RTTgzXL~Z zR{~sNk4Gfg?!|b z2YUR40@1`36L#jLh*>(ok`&9$d|gwm{RB^7uKo>$UV&(*rl-d_&QHoN@bU0O zxCxJvlF;VX*JCW%LE4DK`1r=X+bHJg0O=-5ZR0dh&%Q(IeHZo~S>A=CS`!0TZL zlEHszn0C^VQe{SKUT!V9vSf_Sq67K&oUfZOF!ttpe4LIZ@hU-rH=gLMsIbyKd<9tn z=IJ3JA)@7G6D#ABjlI1FG&NujN5%LGzwBr3@b9HAPNwWH?#)Bp64E%zq504E;*h%@ zc&B2|T54L!vbVX|*@wP4iv7(nc60=alcRktcMs6NlPDO8iHOYF))^!vB@^#G#YNTe ztK*PRu%d4i!`m@JLv(^K%b9S5BQb6z_Vy}`y$@UBe~(GzObvr(q<)HvrZ-oalW`Lw zn>acqy7^l*lpxW)ITzQ}LU5lkq-|~2JZ07BkQNv9>pa!3Z*8P(g>6hatzP8?Z%*@cLNBx4?qz4K+9^1SeD2iRU^xqgW$dL-?AT*u)6jF-nu zp8Oq{k;GpRr0`xI5EG`Kd$a(BHA_$|o>!jRJ|He|gcL{iYETcfeSleUhY?POOqc;Yc|+6i9hB0UR0M# z!edzUOXmJ1PIuSq$DDEml`+Vou{xA8kS9d~0B!424ViL;e9OayDO8mcyFmFAPQ`x> z0zMXRzx}@Rim~}Z=eNpmQD4z0(YCJe<)snf;XwFSR77ZHV;IW0 zwK^q;iedcJ=BmXGwVY0H-kR6+i}l{rH#PML#;Z94_Os@)qjg)5Ok5vAn5Y@^7lIvA zxCty3t>lU|Wa1JFDlq+eDw_ZIuidVH?dc?|r&SL9i5P>p7Y+;_fD&@-;x#N6u(09f z<=pY~S1wBw=gr^uCB}d~kJk-eOiV2DxA9)X2r)$u38(`sJHX&(d+rP1prN5lj)JYu zYyp;dnWa77^bUFj{8T7n0;H0f%nO`lWWhRs-wT4O)+*J|LK5mHe zMD6Dkig1~{qw^R7Fp(qnxxW6vBuyD? z$gK6z|HTqK%*=g1(1js%zHlsAV~LG2`(`I7C_3FaSDzzSg*b#XAD%0gn(%j_O(T!k$5m4NRYCcJxIdX)Uc}WRO^Ct{E zHEnTNEaLHR+mopD*iCK`ymSx5bO?O6w1ZO2g&iNYu!_m>mrw7q8gtF}3+TdHe z=pjw(>N}~6Rf7Fi%T%W~?)a7#EQ+K^$F9w4mhagKpnYIrsUC-UDJDtkCcA>j$(vGf z>tO&(;;A2kpSb;E85Z>>t*9tZ9b>b^YG{QNO>nu2G7OP`ck`RJeVYz{fZtf<+Sx}S zl%Tf6mXJ+xuQAOP$>$#N5H8n)jrH|0L;4E@+vLSVlLN%RsQ-&xzx;&ui$oJ; zOGgFHuq)M~fAwg+<=N_OmeL9bkoVqa+oFZu53I1DODlt`C7*6IHeC2!;G{bvaI{xxmM$Dugj^Ebny&>hIy38@6^3y_yF6}Sn@zJv>OK-!%IpSQJj zitw10z7-;fuKGeFJZno5(_k0&tbP85WJ%)b@K~>ldWzpNsW|<5)<21Yf&%kv!fkMO z=g%K)mi30mR>GVC0J4VB9osA@)Jb2@Yx|O0Kpf^=04p8f;@z z(~@c)xpf*2P2r~S9|(y@dv@r9w!?Ml>J_#3Bdb5e0i>MQV7NB#oXt^3iCR{#+zHOCtWiK?bZo!*zj^Z-!WVET zzy5~WQZXf#vBE+!7IyX$M5MTa=cJ#txl!SuHJ4XNbq$Wst)bg(!ppNWll&PfIX}JX zs$B-D_aB`t!A%>ym%UgDV>->y>Vd9e2qQXAo_a(E z28@FAcw*}39q)ngqaR%^F*qCcZHN(rPz-#y$Pr$Sj`bVIuz`0-LHG#u+AX~r@}`rb z_#NtEj=x25f)K>!0B04{oRa@?^m2awG&E;4qd|=l|1WQlN+4E$i6<$a0Ms&J3o!;= zs)Kbh@GL2YdpfP2zJ*R4?iGs{EuKezG4hvwEBtC$zQkE%aoBc-ym4g+}AY8Sr`5HGpFMs?!)~*#KQSfDwagi8_Oo!RqBB8yf{GY~SVM!u9^h znDKR%xnJuD-dh$ez#hFu_AX14Auj*V>+zN|s@vWWT(mfOa-_JGqzIG;Xhs%~2iUU> zYU>8Hmr%!vyuFB{Qk-;ZGnd{9wV*KI%s_|qN9V_IA{MHRl1`T{u+!OmQjCiGtGG zvl@DH6c!ct>&gDC6^^Ol7@2_4ul#EI*7{^doLnU7kGAsV`SXmt+P#Pa%|e2Ze?Z8M zRD)J*P>b>D2(eFW@f@{{8|dYSkzd3qHvcKKzkY&1gEl;78eVBMF}GiX4K-%+H(>Gu zlu1M8e_0lceEen`iW-gxQbo=1fhy+ zE_D_x)veUeYzK}aUtMKN^zRUS&8DZLBkET=lzNe2BMjz9iH^3pxmn_O;^neW1^WJx zzFeNK_=7-bd3{|jN?bH%yt1UZ1Uu&oa1s1;qL=wvtYo&lKZVp6qiv5+y49P_U_s*? zgzQBH*bOhBtPx4&1x2w_uwv7=n{}`}ln2MbcjGGA}nn+ru;V=_=}5+S-lSPg})t zWu8{G)_8g^;X+gYGT!DDs2=frzNY_$CCaxouAg8*QG>qNUrN&05fbEhhK zNK6`Fo%;vDH9*hp-nAwhBUjf{h@ik&adL9>sL>klHP^5!to{XamvZ@>CkR|Z!fIUG z(sJzPeoesBm&P2#P~s<3sTAyd+=AGVWNlWRsz4+vt=FT=qiGfJJU+b z+M}0I?0_}U(;mIAn|o&lpS<5v?C#DxG#x_z+=;dnRi5UPX9TQ7bgkD?rpQG9HhvqY ztHX3N^H&jK99!KePy58PC`7cHR2@yTJy-}X!9BPW+%>qnySuw5Sa5fj!@=D>5Fi8y?(Xgm z0si^!TKA2YGc(;?wQJX|szyfFKts5R9{cS`>|rf`z6px znZ85BM9nr!8S-O(0%rOH$a-0gkW$k&@6$JtV6j4U4=*pZicTQKfZB7VU-+ytPOC9k z@cJ>H=3(9Ca<$#?J6jf}x<Fr+9MRwDY_o?1S67U*;*u51X#JVk@mVL=1S94SD}eNPiZ&Fl5!Nj7l$g7yu)o?Nc}4gexvhm+8Wn$5e%xnQhmlZqbSzGO zVSw5M(hmuvxRHb$uvg3k*L1iIpdb)m6g$_p8Wp; zh3_67v~s%60F%V+xr9Rq#sWB(00|oK*8kfp%t&QmfBeqdBQClh3<*d`Bpcdwesvp4 z7+?f_=+AB4X7k+G`sGKmh7xRC-CXd%jP@Mo?P4Ynw%esUxTHjzR2Hq-XhcFtdO%HQ z(yTiL_>SNGX(EkcP641|sX-K{+$IEvA&R+0pJ)`x2x6nGIS>o&Qi_~;Vu1Zuv4z^a z{Qbjw>};P^gUUn3bNx?G8IG+iNLQt;aU^`>-)z9+pi%Fqzx&Jd(}8Rb&y!VEyvuu6 zKY-tY?4&BwOII!d3GGC!cuZet9#SO(^}|~LHGOmt{Iv_qjVpG50?qfttJFDf5q ziiuynYQ!Mqh13y_{aYj%RUai;=H#|RUw8Z|RLB=0KBK`EVZBev!}clxvH@QU+I`}+ zbvynfN{0fTGdT?g{>bw?=LLcUAY4TO@}v&Q6kepbIU~*|ASz*fg#FDbFh&wDb*sxL z&{5*~-SIW=sTm9YTPCQ8Gxkr@Jr9hO&}7KQTk?a+duPS%rLa=w_LgPE(Jiq{rsu`q zk@Is$=x1>yjk5+nwkn@EU{P_88(j4B=*oAx< zYBzfLa~{_wR>R1Vu(S(kZO+o*qFi6=i}d~MZA}s0A9r<1_SrQqU%ju5bVHvbP#PPt zt>kQ!o?ip!Kg6f3Dt`o3-GSTI9N%Uk{;qg^^(|cYTpYB98dR1k{`os5L)Z5uf&Dr9M&N|(u+k|c1!YNd2|$kV@n z|6Z=6JL3@t@wkwS4!^|hl;awygEsXQc82R=rt>>yJmjc*&7Vb=XIqkvXo$5D)*jZw z&GP^8uHL=DXgqcc+GzjYOm8HK*Urj7|0V)DJfRr>p58>CKD6M*LzD(MHa5W?2M33^ zfq}vAmPFUXaqhjnzl!xqJHjgnWDc$in>`ULeewzN##L|%gcr4IvQ-=0bJ|l45bT}- zzeF{y#IAbfux-8QW*^&sqfradiQ?aZ1`0+jMDOP#2u7?VAWa`389hR$)6t$8{6{-r zp+#Wv^7AY~w9MtXi=tMvtS6QzNu?SLoYj&_tY0Ax3+F2a9&H!tahrK>5y~eOm0~V- zKHftoH+wTS(81b_)Ii`#^(_L(EII}qfnWs){bAJ&4e-&xImh8fuG%(dvkZZ=vu%1? z606Pi+&Wu@bAr_eWkO0=N+hhitWVHm(1UEWa#`4>u?_UyQfYaWP7c5F;88{D=g4xk ze;v#AAdbeK&n0oDzy18&nTr$#oiFHz^WQt9idBw}kHrK?4x}l?5~X3cAFp=yo)naR znL^`9PF@TXI(3OkY%VHpOVKx#;?S~+vZd$E$P1+B?c5p5`SozL#y*`;pi(*eT6(n8}#QqL(l}6@d>5ZZxPqO60zGlzu~qnnRp; z3(#)k3riieWeQfzsVKjPk3<@9(Q;xjj2SE&w*Bw5d3huPueUQFKZIPqyfQXz&-C;- z{(;Qw-aLH-X=+8Ioqf4c^ORlyY2vT3yV3OzcW7GZKB&mWoj$)bO&G`_pZeWOW2b$< zpd>-nWwehT)V_P7#f0+jF~N@%NB;;598{Hxa-y_2GK@2iB);DRIZm|8=X8oMfq@54 zGAE+w$_Ubj*5!9EeB9seL?W2$=;1V&#>-shwGwrXOa$Fq3Pn{kQvXp&;71eV4(X~T z%7Q-*3=qkH%eKbw+jp;iNcfp8C{}eHObb8*i>;^% zNg>f?o#Rh`4Wz<}zWNfr1=JME?cIq>27o7hghhAe(bmw=)zQboFnSZRU`iisKb7OBr0ro%QjO0Ir*+@U5J)AhF+Zz7UT1O2yV zBL;v6%==FR0alt4dPey_SAV`d{U_j~k&3hC*pVuC-Ga&4sk`-+?RIk)@eAAhpC|32 zDjyxq=Q0bC^pLWO_Kp61(#{mSXQi4kxv&8xeXIdgc%V|itg!rYk^D#tHu{&e*$%W{7AV!{`Y1@3f_dM>tY39jc(+9QkbfJ!0B$&xfL*d<8# zY|%kteMHI~7gzqLL;80FBH$W8E)x+Ep+e6`2RRKpzDb-opwMF+GIMPG-RNx%VV-~g zM_OxUqG$k5sj{R*l~7~uBxqYZStSYFd^1}?y(X(E>a~A=feUOsP&|_M@JeSWT6KI_ zrW=_vto7-^hyfbdpMPqknsjOusxn8gWHe#{d;3AsIk2J`zq>bQs4}G9Se^ zJi({l9Ps8V&Rkx8Au29jjSxakwUrNJ!r6K9H$-@OwRUO$C{E1p)BWG0-rl=|-%sBC zady)87htGz3CVoMXW3yeyF4qWR3BCRZ8W%s6x|EF50!nsk*}@oI>)e)Qz8D@a&~s0 zrr@8k$x_!?juf4-Y~aZs)O!0D@d$ap&|n<5Vi@yS!k(N`>ktbJaf)Iy(oJOoHyVDV z0o_9NU48+Pp@RUVi&XdGmYA{e-HGM4_7LrE>@ZAqy*lL-czSGgxz8`s9>MAJeOJ$Z z)7*+E>i<`(C84obOM>Abc7luTUgLrFeSWw2kn_7Vq|5|(B{oX_ja&r>)QI$O;Hfr>Jo)Bxg z{NdgnNW>;60y??^E%ww>DFO%?v5JM7gL-L8viD9i*$0<;gl@`hs!s~R70b879?xboc%(hZZn4;Ln%tD1qA-nrM9IFEr~fT%OR8jEEakvTLyV5B zeS~c69+3JcK*A$f_oUslC{eF|*BH1rD3hg+Q)iT$bOv}iSrKQga;^l%HjZ!@zOJZw zzp=cbE)8-teaF&adtH$jn{OdjS+4Ulg=PwaK5N!pn7#wnhyar1%hl9eS)mI! zC(`sfdqeBH=bby>dA~>Xe_Zf$wlZS>5nO3DxU;ZK zj!PCj=ylrZAv_(?DSS(E+A|zqy0Xmbb3W~<+kriEp4O||b)$G$uGTO)^Ocy3uyp^R zki-A*EA`xwdYpcHzuEcyu;?O}?Mds~8q}9y-A?>&`0UG?2)#l}qzob@ryy!$UsN|I zLkSZq_(9!}6JdY2(FN*_#EWTvJ;L*MZqmLQ{thyEOOQzH%(wc3@Yayni7)@(3lL9Y z!Fa)9{=O~Cg|v6x`J~Dh+hUF70FR1Jd~w1qd~}nEIzRiPvN6-4`Guy(x5=MA%d-WG zeXDtk_urXX@m%7Tuo%HILH+vysfd|YWlfOX>-|d5W%|nT%Xy6!5sJu^ZR^dc9E;5D z1BLI-hipkSBIRdBoUE8(sAWSn?s)=^J0^U)chuFjnAZTuuE&2^J$GpJ^xE8yxj3tD zzt=7Wy^XuijX%wsyhs!vYM!O) zlaqp4eNiU7Up?<{ecz%teEu0r&j)qt<1Fm>-ri}_po=^L8}P?7dmgiG?J;;%;s64X zU{StBMj25zBFzMC4l&dkHormkR&M_cu3QR7dnXiLe?SuQKOi;r_KEo3ywr*m z@YAb!{;14;d_El;Qv(FF0$tzQJe95)a`<$IF6op!KU*&zP|OH{a1Rca&YXW&HVH=9 zA0crD%ih!qUf4Elr3bnR`wPZCu5;HIn>bn#4FS{&(5nZmJF2lK5t|TB!j?ZcAUeoT zddh!kYA)P#R<2x8_zMGX?e9wvP>+}01GgCnl0!iW-P^v0wzBzomn=cwSDx(*`Sx&` z=y^+QGt2;S+VtDN`+AK7SJ^)Fef?oQOc3o^C9kx|sMYm=0e?QF)d8cWvpljPs{aDs z+}oGn$)NK$PISEuOE3b638Ew?fm&9(caSHUAyVjBJ)_>z8L}T!+|?(5^437EA^i`Bsnq+j5@#!z_l3 z_t4th4~8s`yR9eQVij#lB6QAT;BF8504ohVj`Q!29#el>@4aygx3WAC{7k*O3t^#L(zj0M}QRpZrkjls4I zsI=RYKOV{%GGjVu8XwX!Awxlkn+b(^f`RQs;7xC_c^K5)pBN} z1Hxop#ChMhhjqB*@eV^}gVF?Khgpk0(;c+gN#efj4wV&Xup9_|Vvp9i^`>N)VyG0- zjvJhjleskqrmFX{ucGZ+srX)OT7}Z2uzjts{0f5S`fFc-H!h9+4f`W-%f=vl&>ec< zffA|cL!rUSewN<-Y5)84Mcmdo+B&>itVKYNI-Cf@N*O~3wEOUrwQ|ZBcHe58xDbYs z8ZjzVWvUx9ZmdBM@-I^&IAi*zrUWJu7Q>zw$3}e>>XsR*!)9{{Wpy^I=LMS@D16HZ z&2!nk5&@94bpyrY%ENa55`sX`HD=oW20m`VUyX%H`%W&bzwQBvmydifXFAZI$ zmE+3vM(PrYtETcL-J5aoCr<2Jq$7Rk(!28Ujs@bjUYM7Sxo@QYH<-+M-S_M;mEfW8 z{3Jav@atX>RQ;|RUJnBGeAa3Bm}gRw6My%#MRaM0l|C7q9YAaKBOu&6J*^sxk4+r6 zG{YvFSszVjt)fj>sF7Lf^3&tB-t#mj|61lm%Qxka6NuU9Ii|3OWx4(qiqDs*I0aEi(o8aHgs zc0R~0iyll%JJxMcn2!`mFgDOt_Y=c|W+3*?oX4*Di3JXghbf zHCv~`-QV`|FY8wLw20&UcLSSFmzueBRZr+Sw@1FvcrZ~jzJj=$imipm?uNnV86@+G z50VO9v4@c8@++66{hLDgK5c{CGF~r5NhX}zC3`!Ysqb@t2F;Uj$1L~BjmRwg#zM04 z)wXOK9K__EwzFL=K%(M=lIsbMGxUGv7we8bAVMB`6jxoGa1Firq7I{Xgt@etxR88F z&%eEI8D`jyVJ{m_5NqVv*j`d_KW{e4yxdZU_;bT>LfN|d5SpaaEG5bCvI|ed<&n&e zjs30MBiC3z zRzyr38X3oiaj`)XN<`kIX}Y-S^QuGP^}u#pz}=YVd?Y>pIq@n9I%{qHnvsh5r62E9 zL>+4?t!nfY)A&gU5;$v3X)A4$b*-`IzOWE01rqxVW%)E$V?;2}3fLfDyF zS~~&hdpsL&Wl*P zppW~Qxs)FH#oW!V(K;_#NaQNKoVt$J4-5pye{@sn^@(lDes;UODd-2BTU0SRHnuo# zM|Wx^p;4;Q*?91n`@1GV6+F~*@?c@?j!eyNT^iCN`BlJPZ_g8_Vr7Zl2t8P$ z{=q~a!Ca(D;;2lZM7|awR>*vbJB+JZsS!1mIes-1-U2jIsrKV-;DPn3-;UOP;E3** zwN|hBWZZ?!LTozM#ZI1R8tZSP;=BaQ>l3u>Ufvf2EiTPwrTlBbw9s>?H0bLF8!lZv`-d~nWw zU=7XJF6nZPcJ2zly>3+TkC{kqp7;GNvpJHixaz$gE?jZ*n%Xlq2q2g2*!nb=Z6Q$9 z%9$o}ZH=m#0IH0*14z|vrrq@G&A_5%V7ZEB1Dt+$Q?S;Ad{OCq%={Wkc+1+IUeO=q z*=IZPLHxTD38JA1T8Z}<1+}!)pqa%l*4_@~%9(Xni^N=y9lG27i$^+FUb6c^TgNC} z&wEbfSNn4g*m&I%idVO9r~N^#*ocg^Ml64xeZ{nJaoF*1eY>4E+C%{$r9BrBaS?vy zI6x)A>NoEv1a-Lb*6-RMLm!s%y!%q=^<^Jee&I~~?iG*3*_?)w*nyMk^wAgj7E5}A zy@_P$Q{=c>6(9FkN1nk3_M=SZ;Eil(%g=V0uzxx$5-A=?{ zH9_vOU~uSh_Lg?;e-%7D>05*dY{@lx->=KFA=fHYW!(V$=5l)D`Zqa3L zSq{Ect#Ex|_AQfpcdA4pT3WdBsPMR|bWv7p4JvDp=LGo`GjK^{>m2=w&N&t5)sO?Aj?1%lgd0lE7+$Ml7Ico=HUAL zgSAs*DXCgTIrXQi1#Inmr<)e!%fGpc&FA*1>|YnF&?N~*YZir8`$%w}^9#uqzF8M~ zhC>VsHCq@FArn)Dcv#P%GdE*QY!B zW6*M`FS({2wqsfSE9%B?KSvywPh512)Winj9VH+KMlPM&bzMjbvy|b$Q!jtm*ebar z%GOM)-fmF3*zb1J#KB$4$vL?Ct{4LVNLC$1f$50as>xALemWli#ZWMnx2+upd-j$* zb@|K@R{N9B-)wpE;>8@sUlVKOl_8N+yfNKeVOhHDtA-H8=xORXC3O`<=}udO)IVF&yB-fY=>lF@K(j@)wrXLfA9k6+FRNUgjI zBNV}ArwvQeGVY7N*7?uT?=0B|OG<{Uv8J~Qd?R35)T1a9Ml zhS^_@2Ub%{%fi5pA#y}Un}C4l;4Vs{5g$d7#^73a^2Ajoi2O5lTU>_Y3xuV?_b|bk znYjqCr8)g}^7J2NmGvqz6MtoG+zclWu2$M4s94)WZzif$oX`9OQ^QEGMKH1Bot7OP zIPS7b1Wk;gHMOqWu-b#QBJzf(XQ3T!WUPdio4h&O6DH&yeuiu(rJTC+VK87>Pg(A) zU%6WA*W7E8JN;Bm|cK9;iZfXXek3Ho18~B7TFEQ-IJL8B2 zTd0NqN;M^0^Nxhn)wD|-@PS+++8AwZHb4O#w0hQ`Q6dJ8GZ)~-~yLD6c} zk|S=u?OV!ckBiYF`Vyh)cl)L?;Y2GN}R3x#FRG+ zxEQ_O#JrsGE!~~;Z0dw;s^K9{u@?Rm8~hQtJ)Ve>iyWR=8UJlXEh9~OdU`ZxB&?AP zW!vCEaZXCiphT6>BiWpI@hMb-G-bkegwJ1_PF5yXk!BDPuHxdD z+$unD#556S+oh(q79KRqIHoVhD~JYlxF~~^qou{l$rU}7Zd$jw z1!HFwF9eiGhVBlV_0}&|RiiB}dzO#K*o`4ia~ezQ>x$g!*h0w&Pd5h^H$4I58hS`~ zr#d~8Q{U(`z$>Ukn=NsuvNVAl8~96Y(|MtX9kHwW!K<&kNG1Z2}Tv{B!Oh9q@!HPm#p3}9Mx&}Ju8 z(=5|50^Sr#lJPlOxcf~THS0i?3_dwQ&l)*T$8_eN2^;P0ID567wcS^<)6+>~d-6FW z<_Vqdk+@zL)HAbK`T04!&&#^Utl3k}_l!!-&4snQ(f2$=tEuBj|3$wU@M!u}x}sZ4 zw*i){Spk*Ekvjo-t|Wb`n1KQD-i?ptUlAM|9wW7vla(W)X)iGxM@rVKXV`07RAjMh zNzAK-ZJyWJn~b$@fd?Og*_38~Y@2l|G+DD{7F5u;HZ>_%R3b4cD5R)jaWJ#MM4)~q zjJ*yUh>*BgC1TE4_$z$Ypu6SSsk7v%o5(8Yos=?Ms{Ks1>rLxv!_eE(CU%Ant*6%Js2I5jS=f^4viH6($o1PxnvpAA zXwvYWIbQ-wOx?D(1*{@y-pZ%K_b}&Hb7sq0S z;IPTLX3OcUtlu1*gHNTUu6artb46Cy<2T=<^Q#?YH0Z|!ZmhD+ZCCePRe&)prY8ZY zU=MF^hv(`AB+w6-wgF4C(MKM=p<(=V7Bf#zO`t2?v#HsIJdwtfm%!0M#rDH(o9U#fGX{$e=e-Iu@KG$iu?} zXfR9^b`Gu@82C}5YJT%14GYRqq?XapScWga6%pEKFJ*;+$|x=M2Fzn25USHFy;q(- zHA!sjCQs1s2YWOrlv#O)1pgYwU=l^IFQXu@j=Dy(_d%`~Ijiu5@9BTP4DxEF_3k!r zSXwl#zg+*T_YSEr})8do%Ahf-4avVnv&JwjR{xof%ZZu|CJr+^;V zc9h^HQxm|!TU%N%L0wO~$r7N;mZp?7DWHl9LcjE;1i`L3qQXM|Xj`kgy}kV*O)o)g z$sP{Xh6EQyjmfh(LyJdebc z2J^Rm@83bwM4(I8X6h(3Yl0mI_cJl-pECMmD0xN2q~V>y)Ku)=LI{rbahB$EF%t5Wd))e^Drui{Yd~Om=R}W+xb$K<4?6_rM6+*R~0Od zrq*a6O1Y=-OgG-#SRbEL4ZM)DWs>?>|RcB=7!(aQzQYJg&BBm^mvz?ICvkCq& z^}!eW*tC4sgj`UgO_@C|2_pMNAY(kjSY0-sbx=~iPr)xp*qjW~RbwP_(>KcC${x>| zIM5jgDWa{@W<0Ie1?G`3fzZRr?dU9&4*{0Jj1#SX`}CVl=r2ON)V+4KG+31*bz;T| zJVS&Aiyl8*cuT@-Dmyk-aM1_lx@13_C|{&ZJ4q!WYE~~jI!gUJ&Nu1K?wts@s~NyG z>ih!^P-n{Q%*@wFap*DmrN>mN^}PT%{;CouNK|ka3i}+W67qoFxP>(ZwVRvE;T=1P zi8rxtcHM?1agrv13!ToN6GbAHV(hsXx(r#E7#WrMa`)%+NM!XGwHa|__=sk=ZxRQ# z`oh2j~6P_CPYYcd3YsLo`PtW65@rnwuwfN7Re z#@W)+6IHQhnV3`*6i~*;#+<2>Mg9Evt27zPZ82M(R*zCkKbTG?9=EKEsp`L0IR!pN zKKqVTRN(Lf5AyWHdAUCus2T~K4|oA0h()4eI!*GAy=I+PAlq44Ss4W45994^!p6?e zn1%n!@$Nd+86KTsg4uh*Zk;~A9V1A!E63tXRI%!G>nTHjFP@o44=&nsg=7}eBC=&7 zx3;d>xL_O}mO=LRETrjxO-)h3TFlClHgCLq@`nX2+2ceNIkWK&y865!>ocLC^S8d; z&KG6v=6n2Q6foh^TQp4s`YNa9=EBU`Miy!el5=BY+X^eJy?AKXR=e)=_AIcv43Eod zA@bA%ijZVoU0spLP+)`-sXiV9Oj}um`m-fXM|&fw>3L>`iD?uDwOf()Ff-WujGlez3V^(Gc%(&v#~X`c$~fX z^msDI)U!n4^On7Wc_ln;g~Umh-SwbV8Gw}PileEC_Z;4R~1Pl7rsPla-jtm6%v*7UB`}eW)=JrT3hX7|- zoH=G}81w{yrpmN{4}H#`klNYV0T6oW%rpA*mV*T=Iwd!2drgtXv1Dd*&%%{2S9)QA zf}b7cGi<{Wy*&c_)I`(!snOfa)@5n_5*N2YYX=jKVMy&juI7cN&^@S7bf@uc`l7XzRlG6(e3`jW(XO|^-w|`w4!QbBWsOBa z5?x}_bZ0&&$fr_wU#0;{SR5Rj5+JUvoTW%pN^5Aeq&b6aru>DJ)HE5kL7Sd#|A1ak z_>Z3a(1F5qZ3iU%ljz$92BF?Wu9hD_Us5O3)P$7aQHcudP~o2hNhmQ9fKdaiXo-sQ zaueXtk5oN8(+C5E7`Zsmae-6G6y?eeLx%>!+S%DzziIvCn7n>VhyE^h*jA=MK=Sy0 zK84s5o``&QS!`!KTNUqlzUR5h5Om zhAmbF78fL$T3tnMT#5l0JEyWOh}e^-?W_;VHhS2GJ|D8lKHh7U}3j#15V9>M4if%wKFE6LdNR|N>0nn_3ii)wZ|HgsB!r(rBCIeHd3H-Ip zSK_Fkw~dzgW7)Xc71S%j7aJFc>U%TJEh;|gj}KVOXu6bTmEuFI7O)zcB6{UkcoC!8 z5s2_2AHNH0cJl}rYy*g85IDY!{QQIW-fpKO7s~M({)gyI3Os+ROl0btk&(X@>d^+( zAO3(d!+n3wfu_ay?^PKJO|-Lep`4H|CBwN5V+EdJ@>r+thZKMI@riY0x?Hn`X z9H)*~j44v%`+o?*7%~3k)ol!$tznB7PHLvd?D#!Xo|@EUd)043aO2gyT>RpQZx0(T z>ZIu5nu=z{Au{@>P{K_P zlH}h!n`LQJ+n_cXO{a&t{bp?^2W~`eC)M)oUN6NsvIpJnLFu1dlBxg*RAVEo~d^vT0e0|t}jE>5!udI9$ z`)IvP0!8sJ9J+C<6VWY4$ytfc9oWb^cLF_5M(oFHRjRo>QSXHX?av6}kD1 zCV2fG1|7&=v$591Cfj;CjAc7MO-RN%jH>oLyOos;DY8`80r|mmUHM{LTqqGRyEjli zrI48SzfwWCQO{_X%>UlQmp-YHJvN5uyt4?`Id>th+=0>q`@7PFO+e(>Ai2mF9srC;;qYMavMNMgTlAN1pm0~Kso%P_a`YEa?HXUDP89gy9Ti5yiJcSoCp z?ZZP-o4n!L9-{~u2simbdrC?|l4PMX$$=h)yDW>_$^0(;bFg}t?2-&r6NELaoo#r{FJENL*=*i@sL zF+V?fj7WnMQj+{P4&qMwCp{X#lpgiQ47sJ=y%VlZ@%tu0Ldhcu#D5fkr+eqMqCVvv zxXjki+GiUqfEVKfVck2u6Qm2yc+^qCUczgNbt((j9E%OBry~LAx&vzCQUjwr8$Nx+ z2OCuAWg$LHAB{nP!J6RNu>)~BjWpHzm=>h?lgRht#FHW=y2T-{*A)Oi%lk-yrW#VO=+d_yb$$r8dwvywfx^&bUbX z?pY~@@q6ne{Tp6VV&W;|FMaFld6xq5z`B8{xX@0} zGm-6TC;aOv@>xQF7BNIufdkZ(tuR}u;o9%K0W~Sa|Fv5e3?B*G zQmVG)LqbYQss>EU+&tw3Y(Rj_je%jqI+u|J2dS`vzF1i#kSk>0XZP1mPP2o4H74F+ zxM-k&;@PO&)7BNMV8C3|&lA-+w`^LUNQk^eLAn`={+jtVMl(^S=;aO0L z>+r-M)_*F2)34;`VyLSw3HJ?E6=M4ErSj2?^w@A&rBk(mca@vn8!v*c3ds(Zs_-y4 zH*?@3fjd3Zh^z+>JS5P=v`XG-z_G1 zeBBx2c%az4SRbcKF*T2&NbelX8cv3-bw_%ji1cVkq0C%fC-ksmV`sdNr2058^ldtOkTw++=t^~*%|=tK}| zVKG7Y_S*@dBSga(AZGwHRWeB9hn)@50q%Xi+Ws~1XGwD*`5HrNTj|Lc`DDIC!CK9) z{3-d%7|GBg#Y^J+Sa%em3zZu5&ARsLm6fPJ*E(qd%=H(7oSivb{d>A;&UFGVq!=Wx z{n~4xQ|CbF*gG+ZCWH)kH6{ z{WJ^u0~Qm-7^R}gIhrMVwQth@$Bq%bCwlvnpE59LJ(KJi7$AFRA19DkyuY`$M0XV{1P2MJRiaZbvgsG5IYsDl-)smr0tII~x6Tc;db zr^@D}MLU^}^8;nAKwInG9;q9&soKK)UmFb}sKGPh&}c${{9&3%eqwk_UDRbZLu*j3 z{>z;0TSWyUxTLG7uch_#gS!|ryd57HAHpW%R?1tgVGP1y=MTVDH2gMwQlBxw%7RtEhx(F=SYFsU^1y%< zzfp(H3>TTBkZ3Io=$gL1vO8CJUKi_OHd(;T$l;wYx!KAp^rTHQ58~C?4>FXHWs^3R zIa`1weQYLZ&Q{PQhz;jqS0F3V<@tL&QYv$1n@Xfm;-gN^V|WjYI%9 zQJ8(dXU_w)^H@*HB+h>!v!ps>YnAxVkEZUAxnw+ ziyn6Tvtt7~KR#e36)B_rSrhnK9~5RpUWIT~>yPYL-?hj~U&Rm;VG`co70oPKV7Kxq zceiN{jsEch`pL2)HLoJ&UMmuvBWwb+_my`H;N4@4A3~r|(a@&OJQJlU zFUb-gQFC%p2?~LX(t~5=OoM8}9=e%ed7Qdfv-m@>XSJj(KjH&JmIOcJ3x#AXXgP{c zr5eY!nZPekYIu0~_VLP0Ji4NYVFU=R!#e^b@h;xpK!&&A@0|f29Ub*}gbyAu|2`c- zig>Um*TNWG*NL?DMVf~?^rXfgYJ~trib9bZ)7>LrJgYPj9r}D6rF4`^%Z>LsO>SMY zBvoP3Xy0-mh)*M_HZp|?9rZ0O<_IWFI#Z*}$(DkxDw)j`p zP(Qbj{&^fG|9mAj4ORk$$K41G)@zz7<`4QZkddRoB~Bw(W#(jVBH1|)pKeVXD*h& z3fL&1cd)g^+FmC|p3=Hg``5VXH+6bA&fKAKQz#?ZH~@nL&~Bn{r2GcQCb~ubk%QPn z{sieQrvAi*ebaHRe^gt#_}6}YTltsJ z{ibB#qb`8H@oa#j$EvyG(gv`^ZCT@*F^m*W%p2Y~L^yLc{49BDEGCTy0VSTef#ea` z76&(ARPV{Z$1tr+hZgAb$eK7f^&$DsXZYmugZ>J?UBydS@`$VF<(qFYYisk*ecJF9pPh; zJfgFs<$MDdJRIMSFNh~pXp`jxY9fK=LVRy^+9*97x6UdB?i2*PUH;(U;IxC;Cr_%? zaZNYGeUFqj(y?{4=VE4L1ff@(k@jix+bz_!@LR^8JgHIB#CbCqsNZ`@b&W9kM#YCO zumizM7ciirPf4TZp+yw}M96hU<+Lp)vPUl^c3Ew0UFMkIV|b&}uAC)yR`C1IzrhxXV~Yu?lOK!1L4u%36w!{} zUL~MrezNm`FU5sxUt@lWJYmVM3)3{W-I*UiO(NtsWVekZx(sB*_}`nM3h<#Dm+aXH zV&{hDtvON-e|~JY=9-FGbi0A$stG^*)7DuJ3uJc)D=iDJx(YMie7RArqEQ;$cCUp{ zWDk11VC3F6T^!*@5s-)z&u-t%$5cdgW^_iT^xcT6XJURVT5V<70xMd#vU!WD{>Y{kxw4}W65U^^04u4ZQmN1d zfS}V^r6G6jQ1AL0A{Jx0T#^jgRD?hsjz*l%WJDDH&3+%ziOXlRG_foWLYrV((}Xpf zu?zyrq|-EaT{#|)aqD+}M}7M6g-q{g0sy9Z`2$tOVE}O;NE}Mg0e~EU924gV$N`l7 zKJXFvLINPh6rd@AF9?_dxKK%61fG1A2o|rmVu1w!!cp0AdlCSIqA&ntGZ_K^Yc}Jp ztu5HDD}hy&Uf9~A$!sR2?^DSz5GE}+ zT2j?6BUCY?5T8n=v_*(0DeJrDBn~o=2sPlM>~xo2fwb zzw_BFy*xkX_l}S0<@q`1^LdiKPm}4C^SK0A4X zD<>zydGSI{r&E-VkCp3wUm}+S*)~@UL+F}DbJu0eW^i0rB9}ut2*>o;c#p^-@`Z*46UF0kjvP(V@%sS!@Zm%L?6c44 z*|TTd`aWU{$*lpbBK3+*IgIFtYNOWu`!Wjk*eskj6V)bwdQ?2U#hz~zQJWIg23T+3 zzD)q|#TQ@D<`ozv)sdDOf%Vp{TX^#1$s7N-QmIh)L6`k~pKx&SMo|@%%Ot~1Cd6>a zrSQJ_)mQ66Yd9LQ^nJR0azbOvS~F_|Q~-!Ty0x|1+kU!g$s(7@ykVS)Sk0QvcnlyH zYJ?L22kA7;Y@1~|O?$dd-QJ1~qYxd1>D`?j@%YITnagHLC<-4Q9MFGx^oW&MOlH&R z^+hUFl_!%47q_>O&t{{75fNZV<1q_W<&$bvj4g||vRN8i77s@wj-t$(JvZ|Cn667) zTtrgRdRn)ts$8vB5ry^5=oaOko50*;Q%%$O?%liCERdocbQ33=Al(e@n|OQs{^o5h zL50;7*V=7r*J^85)H;7~96~7WudDp9g<8dGqDchShYueD07adL$c-O00UXB>wr#Vb zC^$YorsvO}uhvP0-6>U7*|IDk4=hV1A+D~#=``&Eu)>pXq*62wk39hHaL8M`yTZ-o z;F>0|vjcJ*kPz6)W=W_jSW3ECEO4_}aIsjV@o0qo{e4*TIj2%7c*`YcbzOYZ>2NY! z?Hb)y^Q~M;2?1kghh5i&CIptg&$(QVG$G&z0r&cSP9_qRN~Lhs>jlD4kBjaa{nwZlwa(_c>zD^6@dnJ&yy!pkOkQQc9XkCa6>@(F%_ZPTPWu!1&8B?2pb=}n$H_IcPHfV;G&bkN!-pypt>-Ryfi&C*j zLQyckz6N%7PzErI$7w%gn)cJDa(in_D6tqP<8gZW;)Oie->0kVYp#^bVmKP1kk8XM zuU_%V$qC&o7MNNVYpE1S2zb8FTU%RbKYc293`1P^dYnuqNvJB{+}vQ{c}Rs_I}!rs zXe8e|J|+;s^L=nn>xgg`QxBsCsqMOAq|pHsKn<$Nwj`CN`t;p=8{ zIo5R@p6|2S?;`?fN?SEK)*(?*unVB)y3kW8a$FZmJkE31MY&iciTel`0ycjPfu62yH*SJ zkgPgy(_ut#j!&}(e+7gJu4&FI!L|SZ002ov JPDHLkV1kMW--ZAH literal 0 HcmV?d00001 diff --git a/src/components/Organizations/Card.tsx b/src/components/Organizations/Card.tsx new file mode 100644 index 0000000..5e595e2 --- /dev/null +++ b/src/components/Organizations/Card.tsx @@ -0,0 +1,50 @@ +import { Box, Card, CardBody, Text } from '@chakra-ui/react' +import { Trans, useTranslation } from 'react-i18next' +import { ReducedTextAndCopy } from '~components/CopyBtn' +import { OrganizationProvider, useOrganization } from '@vocdoni/react-providers' +import { OrganizationAvatar as Avatar, OrganizationName } from '@vocdoni/chakra-components' + +interface IOrganizationCardProps { + id: string + electionCount?: number +} + +const OrganizationCard = ({ id, ...rest }: IOrganizationCardProps) => { + return ( + + + + ) +} + +const OrganizationCardContent = ({ id, electionCount }: IOrganizationCardProps) => { + const { organization } = useOrganization() + const { t } = useTranslation() + + return ( + + + + + + + + {id} + + + + Process: {{ count: electionCount }} + + + + + ) +} + +export default OrganizationCard diff --git a/src/components/Pagination/Pagination.tsx b/src/components/Pagination/Pagination.tsx new file mode 100644 index 0000000..0e1fd7f --- /dev/null +++ b/src/components/Pagination/Pagination.tsx @@ -0,0 +1,45 @@ +import { Button, ButtonGroup } from '@chakra-ui/react' +import { ReactElement, useMemo } from 'react' +import { generatePath, Link as RouterLink, useParams } from 'react-router-dom' +import { usePagination, useRoutedPagination } from './PaginationProvider' + +export const Pagination = () => { + const { page, setPage, totalPages } = usePagination() + + const pages: ReactElement[] = useMemo(() => { + const pages: ReactElement[] = [] + for (let i = 0; i < totalPages; i++) { + pages.push( + + ) + } + return pages + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [page, totalPages]) + + return {pages.map((page) => page)} +} + +export const RoutedPagination = () => { + const { path, totalPages } = useRoutedPagination() + const { page }: { page?: number } = useParams() + + const p = Number(page) || 0 + + const pages: ReactElement[] = useMemo(() => { + const pages: ReactElement[] = [] + for (let i = 0; i < totalPages; i++) { + pages.push( + + ) + } + return pages + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [p, totalPages]) + + return {pages.map((page) => page)} +} diff --git a/src/components/Pagination/PaginationProvider.tsx b/src/components/Pagination/PaginationProvider.tsx new file mode 100644 index 0000000..d0c1e5d --- /dev/null +++ b/src/components/Pagination/PaginationProvider.tsx @@ -0,0 +1,50 @@ +import { createContext, PropsWithChildren, useContext, useState } from 'react' + +export type PaginationContextProps = { + page: number + setPage: (page: number) => void + totalPages: number +} + +export type RoutedPaginationContextProps = Omit & { + path: string +} + +const PaginationContext = createContext(undefined) +const RoutedPaginationContext = createContext(undefined) + +export const usePagination = (): PaginationContextProps => { + const context = useContext(PaginationContext) + if (!context) { + throw new Error('usePagination must be used within a PaginationProvider') + } + return context +} + +export const useRoutedPagination = (): RoutedPaginationContextProps => { + const context = useContext(RoutedPaginationContext) + if (!context) { + throw new Error('useRoutedPagination must be used within a RoutedPaginationProvider') + } + return context +} + +export type PaginationProviderProps = Pick + +export type RoutedPaginationProviderProps = PaginationProviderProps & { + path: string +} + +export const RoutedPaginationProvider = ({ + totalPages, + path, + ...rest +}: PropsWithChildren) => { + return +} + +export const PaginationProvider = ({ totalPages, ...rest }: PropsWithChildren) => { + const [page, setPage] = useState(0) + + return +} diff --git a/src/pages/LoadingError.tsx b/src/pages/LoadingError.tsx new file mode 100644 index 0000000..992dba1 --- /dev/null +++ b/src/pages/LoadingError.tsx @@ -0,0 +1,16 @@ +import { Alert, AlertIcon, Code, Stack } from '@chakra-ui/react' +import { Trans } from 'react-i18next' + +const LoadingError = ({ error }: { error: Error }) => { + return ( + + + + Looks like the content you were accessing threw an error. + + {error.message} + + ) +} + +export default LoadingError diff --git a/src/pages/Organization/List.tsx b/src/pages/Organization/List.tsx new file mode 100644 index 0000000..72a2bac --- /dev/null +++ b/src/pages/Organization/List.tsx @@ -0,0 +1,50 @@ +import { Box, Flex, Heading, Text } from '@chakra-ui/react' +import { useTranslation } from 'react-i18next' +import OrganizationCard from '~components/Organizations/Card' +import { useOrganizationCount, useOrganizationList } from '~src/queries/organizations' +import { Loading } from '~src/router/SuspenseLoader' +import { RoutedPaginationProvider } from '~components/Pagination/PaginationProvider' +import { RoutedPagination } from '~components/Pagination/Pagination' +import { useParams } from 'react-router-dom' + +const OrganizationList = () => { + const { t } = useTranslation() + + const { page }: { page?: number } = useParams() + + const { data: orgsCount, isLoading: isLoadingCount, error: countError } = useOrganizationCount() + const { data: orgs, isLoading: isLoadingOrgs } = useOrganizationList({ page: Number(page || 0) }) + + const count = orgsCount?.count || 0 + + const title = t('organizations.organizations_list') + const subtitle = t('organizations.organizations_count', { count: count }) + + if (isLoadingCount) return + + return ( + + + + + {title} + + {subtitle} + + input + + + {isLoadingOrgs ? ( + + ) : ( + orgs?.organizations.map((org) => ( + + )) + )} + + + + ) +} + +export default OrganizationList diff --git a/src/pages/Organization.tsx b/src/pages/Organization/Organization.tsx similarity index 100% rename from src/pages/Organization.tsx rename to src/pages/Organization/Organization.tsx diff --git a/src/queries/organizations.ts b/src/queries/organizations.ts new file mode 100644 index 0000000..a8bd0fc --- /dev/null +++ b/src/queries/organizations.ts @@ -0,0 +1,29 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { useClient } from '@vocdoni/react-providers' +import { ExtendedSDKClient } from '@vocdoni/extended-sdk' +import { IChainOrganizationCountResponse, IChainOrganizationListResponse } from '@vocdoni/sdk' + +export const useOrganizationList = ({ + page, + organizationId, + ...options +}: { + page: number + organizationId?: string +} & Omit, 'queryKey'>) => { + const { client } = useClient() + return useQuery({ + queryKey: ['organizations', 'list', page], + queryFn: () => client.organizationList(page, organizationId), + ...options, + }) +} + +export const useOrganizationCount = (options?: Omit, 'queryKey'>) => { + const { client } = useClient() + return useQuery({ + queryKey: ['organizations', 'count'], + queryFn: client.organizationCount, + ...options, + }) +} diff --git a/src/router/index.tsx b/src/router/index.tsx index bfe879c..2fcc6e3 100644 --- a/src/router/index.tsx +++ b/src/router/index.tsx @@ -7,7 +7,8 @@ import RouteError from '~pages/RouteError' import Layout from '~src/layout/Default' const Home = lazy(() => import('~pages/Home')) -const Organization = lazy(() => import('~pages/Organization')) +const Organization = lazy(() => import('~pages/Organization/Organization')) +const OrganizationList = lazy(() => import('~pages/Organization/List')) const Vote = lazy(() => import('~pages/Vote')) export const RoutesProvider = () => { @@ -26,6 +27,14 @@ export const RoutesProvider = () => { ), }, + { + path: '/organizations/:page?', + element: ( + + + + ), + }, { path: '/process/:pid', element: ( From c8b33100037406d61c3869985781d71018b5eceb Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 5 Jun 2024 11:58:07 +0200 Subject: [PATCH 02/19] Implement organization id filter --- src/layout/inputs.tsx | 13 +++++++++++++ src/pages/Organization/List.tsx | 33 ++++++++++++++++++++++++++++----- src/queries/organizations.ts | 2 +- src/router/index.tsx | 2 +- src/utils/debounce.ts | 15 +++++++++++++++ 5 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 src/layout/inputs.tsx create mode 100644 src/utils/debounce.ts diff --git a/src/layout/inputs.tsx b/src/layout/inputs.tsx new file mode 100644 index 0000000..0d84c4f --- /dev/null +++ b/src/layout/inputs.tsx @@ -0,0 +1,13 @@ +import { Input, InputGroup, InputLeftElement, InputProps } from '@chakra-ui/react' +import { BiSearchAlt } from 'react-icons/bi' + +export const InputSearch = (props: InputProps) => { + return ( + + + + + + + ) +} diff --git a/src/pages/Organization/List.tsx b/src/pages/Organization/List.tsx index 72a2bac..a2e3ddd 100644 --- a/src/pages/Organization/List.tsx +++ b/src/pages/Organization/List.tsx @@ -5,21 +5,42 @@ import { useOrganizationCount, useOrganizationList } from '~src/queries/organiza import { Loading } from '~src/router/SuspenseLoader' import { RoutedPaginationProvider } from '~components/Pagination/PaginationProvider' import { RoutedPagination } from '~components/Pagination/Pagination' -import { useParams } from 'react-router-dom' +import { generatePath, useNavigate, useParams } from 'react-router-dom' +import { InputSearch } from '~src/layout/inputs' +import { debounce } from '~utils/debounce' const OrganizationList = () => { const { t } = useTranslation() - const { page }: { page?: number } = useParams() + const { page, orgId }: { page?: number; orgId?: string } = useParams() + const navigate = useNavigate() const { data: orgsCount, isLoading: isLoadingCount, error: countError } = useOrganizationCount() - const { data: orgs, isLoading: isLoadingOrgs } = useOrganizationList({ page: Number(page || 0) }) + const { data: orgs, isLoading: isLoadingOrgs } = useOrganizationList({ + page: Number(page || 0), + organizationId: orgId, + }) const count = orgsCount?.count || 0 const title = t('organizations.organizations_list') const subtitle = t('organizations.organizations_count', { count: count }) + const debouncedSearch = debounce((value) => { + const getPath = () => { + // If previous state has not org id, ensure to look at the first page + if (!orgId || orgId.length === 0) { + return generatePath('/organizations/:page?/:orgId?', { page: '0', orgId: value as string }) + } + return generatePath('/organizations/:page?/:orgId?', { page: page?.toString() || '0', orgId: value as string }) + } + navigate(getPath()) + }, 1000) + + const searchOnChange = (event: any) => { + debouncedSearch(event.target.value) + } + if (isLoadingCount) return return ( @@ -31,9 +52,11 @@ const OrganizationList = () => { {subtitle} - input + + + - + {isLoadingOrgs ? ( ) : ( diff --git a/src/queries/organizations.ts b/src/queries/organizations.ts index a8bd0fc..7559858 100644 --- a/src/queries/organizations.ts +++ b/src/queries/organizations.ts @@ -13,7 +13,7 @@ export const useOrganizationList = ({ } & Omit, 'queryKey'>) => { const { client } = useClient() return useQuery({ - queryKey: ['organizations', 'list', page], + queryKey: ['organizations', 'list', page, organizationId], queryFn: () => client.organizationList(page, organizationId), ...options, }) diff --git a/src/router/index.tsx b/src/router/index.tsx index 2fcc6e3..ff70c39 100644 --- a/src/router/index.tsx +++ b/src/router/index.tsx @@ -28,7 +28,7 @@ export const RoutesProvider = () => { ), }, { - path: '/organizations/:page?', + path: '/organizations/:page?/:orgId?', element: ( diff --git a/src/utils/debounce.ts b/src/utils/debounce.ts new file mode 100644 index 0000000..09ac4f8 --- /dev/null +++ b/src/utils/debounce.ts @@ -0,0 +1,15 @@ +// Debounce function +export const debounce = void>( + func: T, + delay: number +): ((...args: Parameters) => void) => { + let timeoutId: NodeJS.Timeout + return (...args: Parameters) => { + if (timeoutId) { + clearTimeout(timeoutId) + } + timeoutId = setTimeout(() => { + func(...args) + }, delay) + } +} From 35c95e331101a368878a9d50a6103d3c5ea8f29e Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 5 Jun 2024 12:01:24 +0200 Subject: [PATCH 03/19] Show organization id preview --- src/components/Organizations/Card.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/Organizations/Card.tsx b/src/components/Organizations/Card.tsx index 5e595e2..e6ab16f 100644 --- a/src/components/Organizations/Card.tsx +++ b/src/components/Organizations/Card.tsx @@ -18,7 +18,7 @@ const OrganizationCard = ({ id, ...rest }: IOrganizationCardProps) => { } const OrganizationCardContent = ({ id, electionCount }: IOrganizationCardProps) => { - const { organization } = useOrganization() + const { organization, loading } = useOrganization() const { t } = useTranslation() return ( @@ -33,7 +33,13 @@ const OrganizationCardContent = ({ id, electionCount }: IOrganizationCardProps) /> - + {loading ? ( + + {id} + + ) : ( + + )} {id} From f73b894ba2b14d24b8eb3a706591014c04fdcfae Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 5 Jun 2024 12:12:16 +0200 Subject: [PATCH 04/19] Split logic --- .../Organizations/OrganizationsList.tsx | 76 +++++++++++++++++++ src/pages/Organization/List.tsx | 68 +---------------- 2 files changed, 80 insertions(+), 64 deletions(-) create mode 100644 src/components/Organizations/OrganizationsList.tsx diff --git a/src/components/Organizations/OrganizationsList.tsx b/src/components/Organizations/OrganizationsList.tsx new file mode 100644 index 0000000..3b51708 --- /dev/null +++ b/src/components/Organizations/OrganizationsList.tsx @@ -0,0 +1,76 @@ +import { Box, Flex, Heading, Text } from '@chakra-ui/react' +import { useTranslation } from 'react-i18next' +import { InputSearch } from '~src/layout/inputs' +import { useOrganizationCount, useOrganizationList } from '~queries/organizations' +import { debounce } from '~utils/debounce' +import { generatePath, useNavigate, useParams } from 'react-router-dom' +import { RoutedPaginationProvider } from '~components/Pagination/PaginationProvider' +import { Loading } from '~src/router/SuspenseLoader' +import OrganizationCard from '~components/Organizations/Card' +import { RoutedPagination } from '~components/Pagination/Pagination' + +export const OrganizationsListHeader = () => { + const { t } = useTranslation() + const { data: orgsCount, isLoading, error: countError } = useOrganizationCount() + const { page, orgId }: { page?: number; orgId?: string } = useParams() + const navigate = useNavigate() + + const count = orgsCount?.count || 0 + + const debouncedSearch = debounce((value) => { + const getPath = () => { + // If previous state has not org id, ensure to look at the first page + if (!orgId || orgId.length === 0) { + return generatePath('/organizations/:page?/:orgId?', { page: '0', orgId: value as string }) + } + return generatePath('/organizations/:page?/:orgId?', { page: page?.toString() || '0', orgId: value as string }) + } + navigate(getPath()) + }, 1000) + + const searchOnChange = (event: any) => { + debouncedSearch(event.target.value) + } + + return ( + + + + {t('organizations.organizations_list')} + + {!isLoading && {t('organizations.organizations_count', { count: count })}} + + + + + + ) +} + +export const OrganizationsList = () => { + const { page, orgId }: { page?: number; orgId?: string } = useParams() + const { data: orgsCount, isLoading: isLoadingCount, error: countError } = useOrganizationCount() + const count = orgsCount?.count || 0 + + const { data: orgs, isLoading: isLoadingOrgs } = useOrganizationList({ + page: Number(page || 0), + organizationId: orgId, + }) + + const isLoading = isLoadingCount || isLoadingOrgs + + if (isLoading) { + return + } + + return ( + <> + + {orgs?.organizations.map((org) => ( + + ))} + + + + ) +} diff --git a/src/pages/Organization/List.tsx b/src/pages/Organization/List.tsx index a2e3ddd..ec2364d 100644 --- a/src/pages/Organization/List.tsx +++ b/src/pages/Organization/List.tsx @@ -1,71 +1,11 @@ -import { Box, Flex, Heading, Text } from '@chakra-ui/react' -import { useTranslation } from 'react-i18next' -import OrganizationCard from '~components/Organizations/Card' -import { useOrganizationCount, useOrganizationList } from '~src/queries/organizations' -import { Loading } from '~src/router/SuspenseLoader' -import { RoutedPaginationProvider } from '~components/Pagination/PaginationProvider' -import { RoutedPagination } from '~components/Pagination/Pagination' -import { generatePath, useNavigate, useParams } from 'react-router-dom' -import { InputSearch } from '~src/layout/inputs' -import { debounce } from '~utils/debounce' +import { Flex } from '@chakra-ui/react' +import { OrganizationsList, OrganizationsListHeader } from '~components/Organizations/OrganizationsList' const OrganizationList = () => { - const { t } = useTranslation() - - const { page, orgId }: { page?: number; orgId?: string } = useParams() - const navigate = useNavigate() - - const { data: orgsCount, isLoading: isLoadingCount, error: countError } = useOrganizationCount() - const { data: orgs, isLoading: isLoadingOrgs } = useOrganizationList({ - page: Number(page || 0), - organizationId: orgId, - }) - - const count = orgsCount?.count || 0 - - const title = t('organizations.organizations_list') - const subtitle = t('organizations.organizations_count', { count: count }) - - const debouncedSearch = debounce((value) => { - const getPath = () => { - // If previous state has not org id, ensure to look at the first page - if (!orgId || orgId.length === 0) { - return generatePath('/organizations/:page?/:orgId?', { page: '0', orgId: value as string }) - } - return generatePath('/organizations/:page?/:orgId?', { page: page?.toString() || '0', orgId: value as string }) - } - navigate(getPath()) - }, 1000) - - const searchOnChange = (event: any) => { - debouncedSearch(event.target.value) - } - - if (isLoadingCount) return - return ( - - - - {title} - - {subtitle} - - - - - - - {isLoadingOrgs ? ( - - ) : ( - orgs?.organizations.map((org) => ( - - )) - )} - - + + ) } From c59f771affa25ced0ae5126096db1f7215675f84 Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 5 Jun 2024 12:20:22 +0200 Subject: [PATCH 05/19] Show error --- src/components/Organizations/OrganizationsList.tsx | 12 +++++++++++- src/{pages => layout}/LoadingError.tsx | 4 ++-- 2 files changed, 13 insertions(+), 3 deletions(-) rename src/{pages => layout}/LoadingError.tsx (75%) diff --git a/src/components/Organizations/OrganizationsList.tsx b/src/components/Organizations/OrganizationsList.tsx index 3b51708..4c7e113 100644 --- a/src/components/Organizations/OrganizationsList.tsx +++ b/src/components/Organizations/OrganizationsList.tsx @@ -8,6 +8,7 @@ import { RoutedPaginationProvider } from '~components/Pagination/PaginationProvi import { Loading } from '~src/router/SuspenseLoader' import OrganizationCard from '~components/Organizations/Card' import { RoutedPagination } from '~components/Pagination/Pagination' +import LoadingError from '~src/layout/LoadingError' export const OrganizationsListHeader = () => { const { t } = useTranslation() @@ -52,7 +53,12 @@ export const OrganizationsList = () => { const { data: orgsCount, isLoading: isLoadingCount, error: countError } = useOrganizationCount() const count = orgsCount?.count || 0 - const { data: orgs, isLoading: isLoadingOrgs } = useOrganizationList({ + const { + data: orgs, + isLoading: isLoadingOrgs, + isError, + error, + } = useOrganizationList({ page: Number(page || 0), organizationId: orgId, }) @@ -63,6 +69,10 @@ export const OrganizationsList = () => { return } + if (!orgs || orgs?.organizations.length === 0 || isError) { + return + } + return ( <> diff --git a/src/pages/LoadingError.tsx b/src/layout/LoadingError.tsx similarity index 75% rename from src/pages/LoadingError.tsx rename to src/layout/LoadingError.tsx index 992dba1..e3360e6 100644 --- a/src/pages/LoadingError.tsx +++ b/src/layout/LoadingError.tsx @@ -1,14 +1,14 @@ import { Alert, AlertIcon, Code, Stack } from '@chakra-ui/react' import { Trans } from 'react-i18next' -const LoadingError = ({ error }: { error: Error }) => { +const LoadingError = ({ error }: { error: Error | undefined | null }) => { return ( Looks like the content you were accessing threw an error. - {error.message} + {error && {error.message}} ) } From 9cb80080f0abb290cd47b35d0251a831f4c327de Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 5 Jun 2024 12:21:20 +0200 Subject: [PATCH 06/19] Fix footer relative path --- src/components/Footer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index b1556a6..0f9bd96 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -38,7 +38,7 @@ export const Footer = () => { - Vocdoni + Vocdoni From ab4efc8b1c488370a3d8b22515f7b53672cbd1a5 Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 5 Jun 2024 12:21:28 +0200 Subject: [PATCH 07/19] Add some intl --- src/i18n/locales/ca.json | 54 ++++++++++++++++++++++++++++++++++++++++ src/i18n/locales/en.json | 52 ++++++++++++++++++++++++++++++++++++++ src/i18n/locales/es.json | 54 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+) diff --git a/src/i18n/locales/ca.json b/src/i18n/locales/ca.json index ab9ec74..8442d70 100644 --- a/src/i18n/locales/ca.json +++ b/src/i18n/locales/ca.json @@ -1,22 +1,76 @@ { + "blocks": { + "proposer": "Proposer:" + }, + "copy": { + "copied_to_the_clipboard": "", + "copy_to_clipboard": "" + }, + "featured": { + "a_cutting_edge_voting_protocol": "A cutting edge voting protocol", + "anonymous_image_alt": "", + "edge_protocol_image_alt": "", + "inexpensive_image_alt": "", + "know_more": "Know more", + "leveraging": "A fully anonymous voting system ensuring data availability<1>Leveraging on decentralized technologies", + "open_source_image_alt": "", + "scalable_image_alt": "", + "verifiable_image_alt": "" + }, "home": { "subtitle": "The most flexible and secure voting protocol to organize any kind of voting process: single-choice, multiple-choice, weighted, quadratic voting and much more.", "title": "" }, + "links": { + "about": "", + "blocks": "", + "blog": "", + "docs": "", + "help": "", + "organizations": "", + "processes": "", + "stats": "", + "support": "", + "tools": "", + "transactions": "", + "validators": "", + "verify_vote": "" + }, + "organization": { + "process_count_one": "<0>Process: {{count}}", + "process_count_many": "<0>Process: {{count}}", + "process_count_other": "<0>Process: {{count}}" + }, + "organizations": { + "organizations_count_one": "", + "organizations_count_many": "", + "organizations_count_other": "", + "organizations_list": "" + }, "stats": { "average_block_time": "", + "block_height": "", + "blockchain_info": "", "electionCount_one": "", "electionCount_many": "", "electionCount_other": "", + "genesis_block_date": "", + "in_sync": "", + "latest_blocks": "", + "network_id": "", + "nr_of_validators": "", "organizations_one": "", "organizations_many": "", "organizations_other": "", "seconds_one": "", "seconds_many": "", "seconds_other": "", + "sync_status": "", + "syncing": "", "total_elections": "", "total_organizations": "", "total_votes": "", + "view_all_blocks": "View all blocks", "votes_one": "", "votes_many": "", "votes_other": "" diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 3e22554..2cbbc19 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1,19 +1,71 @@ { + "blocks": { + "proposer": "Proposer:" + }, + "copy": { + "copied_to_the_clipboard": "", + "copy_to_clipboard": "" + }, + "featured": { + "a_cutting_edge_voting_protocol": "A cutting edge voting protocol", + "anonymous_image_alt": "", + "edge_protocol_image_alt": "", + "inexpensive_image_alt": "", + "know_more": "Know more", + "leveraging": "A fully anonymous voting system ensuring data availability<1>Leveraging on decentralized technologies", + "open_source_image_alt": "", + "scalable_image_alt": "", + "verifiable_image_alt": "" + }, "home": { "subtitle": "The most flexible and secure voting protocol to organize any kind of voting process: single-choice, multiple-choice, weighted, quadratic voting and much more.", "title": "Vocdoni explorer" }, + "links": { + "about": "", + "blocks": "", + "blog": "", + "docs": "", + "help": "", + "organizations": "", + "processes": "", + "stats": "", + "support": "", + "tools": "", + "transactions": "", + "validators": "", + "verify_vote": "" + }, + "organization": { + "process_count_one": "<0>Process: {{count}}", + "process_count_other": "<0>Process: {{count}}" + }, + "organizations": { + "organizations_count_one": "", + "organizations_count_other": "", + "organizations_list": "Organizations" + }, "stats": { "average_block_time": "Average block time", + "block_height": "", + "blockchain_info": "", "electionCount_one": "{{ count }} election", "electionCount_other": "{{ count }} elections", + "genesis_block_date": "", + "in_sync": "", + "latest_blocks": "", + "network_id": "", + "nr_of_validators": "", "organizations_one": "{{ count }} organization", "organizations_other": "{{ count }} organizations", "seconds_one": "{{ count }} second", "seconds_other": "{{ count }} seconds", + "sync_status": "", + "syncing": "", "total_elections": "Total processes", "total_organizations": "Total organizations", "total_votes": "Total votes", + "view_all_blocks": "View all blocks", "votes_one": "{{ count }} vote", "votes_other": "{{ count }} votes" } diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index ab9ec74..8442d70 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -1,22 +1,76 @@ { + "blocks": { + "proposer": "Proposer:" + }, + "copy": { + "copied_to_the_clipboard": "", + "copy_to_clipboard": "" + }, + "featured": { + "a_cutting_edge_voting_protocol": "A cutting edge voting protocol", + "anonymous_image_alt": "", + "edge_protocol_image_alt": "", + "inexpensive_image_alt": "", + "know_more": "Know more", + "leveraging": "A fully anonymous voting system ensuring data availability<1>Leveraging on decentralized technologies", + "open_source_image_alt": "", + "scalable_image_alt": "", + "verifiable_image_alt": "" + }, "home": { "subtitle": "The most flexible and secure voting protocol to organize any kind of voting process: single-choice, multiple-choice, weighted, quadratic voting and much more.", "title": "" }, + "links": { + "about": "", + "blocks": "", + "blog": "", + "docs": "", + "help": "", + "organizations": "", + "processes": "", + "stats": "", + "support": "", + "tools": "", + "transactions": "", + "validators": "", + "verify_vote": "" + }, + "organization": { + "process_count_one": "<0>Process: {{count}}", + "process_count_many": "<0>Process: {{count}}", + "process_count_other": "<0>Process: {{count}}" + }, + "organizations": { + "organizations_count_one": "", + "organizations_count_many": "", + "organizations_count_other": "", + "organizations_list": "" + }, "stats": { "average_block_time": "", + "block_height": "", + "blockchain_info": "", "electionCount_one": "", "electionCount_many": "", "electionCount_other": "", + "genesis_block_date": "", + "in_sync": "", + "latest_blocks": "", + "network_id": "", + "nr_of_validators": "", "organizations_one": "", "organizations_many": "", "organizations_other": "", "seconds_one": "", "seconds_many": "", "seconds_other": "", + "sync_status": "", + "syncing": "", "total_elections": "", "total_organizations": "", "total_votes": "", + "view_all_blocks": "View all blocks", "votes_one": "", "votes_many": "", "votes_other": "" From fd8590d91925c25e8b8fd98b6f343ab1fbbd5f89 Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 5 Jun 2024 12:36:50 +0200 Subject: [PATCH 08/19] Refactor name --- src/components/Organizations/OrganizationsList.tsx | 2 +- src/layout/{inputs.tsx => Inputs.tsx} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/layout/{inputs.tsx => Inputs.tsx} (100%) diff --git a/src/components/Organizations/OrganizationsList.tsx b/src/components/Organizations/OrganizationsList.tsx index 4c7e113..bfa821d 100644 --- a/src/components/Organizations/OrganizationsList.tsx +++ b/src/components/Organizations/OrganizationsList.tsx @@ -1,6 +1,6 @@ import { Box, Flex, Heading, Text } from '@chakra-ui/react' import { useTranslation } from 'react-i18next' -import { InputSearch } from '~src/layout/inputs' +import { InputSearch } from '~src/layout/Inputs' import { useOrganizationCount, useOrganizationList } from '~queries/organizations' import { debounce } from '~utils/debounce' import { generatePath, useNavigate, useParams } from 'react-router-dom' diff --git a/src/layout/inputs.tsx b/src/layout/Inputs.tsx similarity index 100% rename from src/layout/inputs.tsx rename to src/layout/Inputs.tsx From a5bb16c1ece121159375e713d89e0d8ddf73bd4f Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 5 Jun 2024 12:50:21 +0200 Subject: [PATCH 09/19] Refactor to extrapolate layout --- .../Organizations/OrganizationsList.tsx | 22 ++------------ src/layout/ListPageLayout.tsx | 30 +++++++++++++++++++ src/pages/Organization/List.tsx | 20 +++++++++---- 3 files changed, 48 insertions(+), 24 deletions(-) create mode 100644 src/layout/ListPageLayout.tsx diff --git a/src/components/Organizations/OrganizationsList.tsx b/src/components/Organizations/OrganizationsList.tsx index bfa821d..6e9a292 100644 --- a/src/components/Organizations/OrganizationsList.tsx +++ b/src/components/Organizations/OrganizationsList.tsx @@ -1,5 +1,3 @@ -import { Box, Flex, Heading, Text } from '@chakra-ui/react' -import { useTranslation } from 'react-i18next' import { InputSearch } from '~src/layout/Inputs' import { useOrganizationCount, useOrganizationList } from '~queries/organizations' import { debounce } from '~utils/debounce' @@ -9,15 +7,13 @@ import { Loading } from '~src/router/SuspenseLoader' import OrganizationCard from '~components/Organizations/Card' import { RoutedPagination } from '~components/Pagination/Pagination' import LoadingError from '~src/layout/LoadingError' +import { useTranslation } from 'react-i18next' -export const OrganizationsListHeader = () => { +export const OrganizationsFilter = () => { const { t } = useTranslation() - const { data: orgsCount, isLoading, error: countError } = useOrganizationCount() const { page, orgId }: { page?: number; orgId?: string } = useParams() const navigate = useNavigate() - const count = orgsCount?.count || 0 - const debouncedSearch = debounce((value) => { const getPath = () => { // If previous state has not org id, ensure to look at the first page @@ -33,19 +29,7 @@ export const OrganizationsListHeader = () => { debouncedSearch(event.target.value) } - return ( - - - - {t('organizations.organizations_list')} - - {!isLoading && {t('organizations.organizations_count', { count: count })}} - - - - - - ) + return } export const OrganizationsList = () => { diff --git a/src/layout/ListPageLayout.tsx b/src/layout/ListPageLayout.tsx new file mode 100644 index 0000000..4670a18 --- /dev/null +++ b/src/layout/ListPageLayout.tsx @@ -0,0 +1,30 @@ +import { Box, Flex, Heading, Text } from '@chakra-ui/react' +import { PropsWithChildren, ReactNode } from 'react' + +const ListPageLayout = ({ + title, + subtitle, + rightComponent, + children, +}: { + title: string + subtitle?: string + rightComponent?: ReactNode +} & PropsWithChildren) => { + return ( + + + + + {title} + + {subtitle && {subtitle}} + + {rightComponent && {rightComponent}} + + {children} + + ) +} + +export default ListPageLayout diff --git a/src/pages/Organization/List.tsx b/src/pages/Organization/List.tsx index ec2364d..e21f782 100644 --- a/src/pages/Organization/List.tsx +++ b/src/pages/Organization/List.tsx @@ -1,12 +1,22 @@ -import { Flex } from '@chakra-ui/react' -import { OrganizationsList, OrganizationsListHeader } from '~components/Organizations/OrganizationsList' +import { OrganizationsFilter, OrganizationsList } from '~components/Organizations/OrganizationsList' +import ListPageLayout from '~src/layout/ListPageLayout' +import { useOrganizationCount } from '~queries/organizations' +import { useTranslation } from 'react-i18next' const OrganizationList = () => { + const { t } = useTranslation() + const { data: orgsCount, isLoading, error: countError } = useOrganizationCount() + + const subtitle = !isLoading ? t('organizations.organizations_count', { count: orgsCount?.count || 0 }) : '' + return ( - - + } + > - + ) } From bd26b1610d09609183c39beb979953dfa338d92b Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 5 Jun 2024 14:31:18 +0200 Subject: [PATCH 10/19] Use paths constants --- src/components/Organizations/OrganizationsList.tsx | 5 +++-- src/components/TopBar.tsx | 4 +++- src/router/index.tsx | 13 +++++++++---- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/components/Organizations/OrganizationsList.tsx b/src/components/Organizations/OrganizationsList.tsx index 6e9a292..9cbf05a 100644 --- a/src/components/Organizations/OrganizationsList.tsx +++ b/src/components/Organizations/OrganizationsList.tsx @@ -8,6 +8,7 @@ import OrganizationCard from '~components/Organizations/Card' import { RoutedPagination } from '~components/Pagination/Pagination' import LoadingError from '~src/layout/LoadingError' import { useTranslation } from 'react-i18next' +import { ORGANIZATIONS_LIST_PATH } from '~src/router' export const OrganizationsFilter = () => { const { t } = useTranslation() @@ -18,9 +19,9 @@ export const OrganizationsFilter = () => { const getPath = () => { // If previous state has not org id, ensure to look at the first page if (!orgId || orgId.length === 0) { - return generatePath('/organizations/:page?/:orgId?', { page: '0', orgId: value as string }) + return generatePath(ORGANIZATIONS_LIST_PATH, { page: '0', query: value as string }) } - return generatePath('/organizations/:page?/:orgId?', { page: page?.toString() || '0', orgId: value as string }) + return generatePath(ORGANIZATIONS_LIST_PATH, { page: page?.toString() || '0', query: value as string }) } navigate(getPath()) }, 1000) diff --git a/src/components/TopBar.tsx b/src/components/TopBar.tsx index 9380592..542986f 100644 --- a/src/components/TopBar.tsx +++ b/src/components/TopBar.tsx @@ -13,7 +13,9 @@ import { } from '@chakra-ui/react' import { useTranslation } from 'react-i18next' import { RxHamburgerMenu } from 'react-icons/rx' +import { generatePath } from 'react-router-dom' import { VocdoniEnvironment } from '~constants' +import { ORGANIZATIONS_LIST_PATH } from '~src/router' interface HeaderLink { name: string @@ -42,7 +44,7 @@ export const TopBar = () => { const links: HeaderLink[] = [ { name: t('links.organizations'), - url: '', + url: generatePath(ORGANIZATIONS_LIST_PATH, { page: null, query: null }), }, { name: t('links.processes'), diff --git a/src/router/index.tsx b/src/router/index.tsx index ff70c39..c134a7e 100644 --- a/src/router/index.tsx +++ b/src/router/index.tsx @@ -6,6 +6,11 @@ import { createBrowserRouter, RouteObject, RouterProvider } from 'react-router-d import RouteError from '~pages/RouteError' import Layout from '~src/layout/Default' +export const BASE_PATH = '/' +export const ORGANIZATIONS_LIST_PATH = '/organizations/:page?/:query?' +export const PROCESS_PATH = '/process/:pid' +export const ORGANIZATION_PATH = '/organization/:pid' + const Home = lazy(() => import('~pages/Home')) const Organization = lazy(() => import('~pages/Organization/Organization')) const OrganizationList = lazy(() => import('~pages/Organization/List')) @@ -15,7 +20,7 @@ export const RoutesProvider = () => { const { client } = useClient() const routes: RouteObject[] = [ { - path: '/', + path: BASE_PATH, element: , errorElement: , children: [ @@ -28,7 +33,7 @@ export const RoutesProvider = () => { ), }, { - path: '/organizations/:page?/:orgId?', + path: ORGANIZATIONS_LIST_PATH, element: ( @@ -36,7 +41,7 @@ export const RoutesProvider = () => { ), }, { - path: '/process/:pid', + path: PROCESS_PATH, element: ( @@ -45,7 +50,7 @@ export const RoutesProvider = () => { loader: async ({ params }) => await client.fetchElection(params.pid), }, { - path: '/organization/:pid', + path: ORGANIZATION_PATH, element: ( From 4a069220592493460ca2172bde849c2e507ccda0 Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 5 Jun 2024 15:28:07 +0200 Subject: [PATCH 11/19] Create loading cards loader --- .../Organizations/OrganizationsList.tsx | 4 ++-- src/components/Stats/LatestBlocks.tsx | 15 +++------------ src/router/SuspenseLoader.tsx | 14 +++++++++++++- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/components/Organizations/OrganizationsList.tsx b/src/components/Organizations/OrganizationsList.tsx index 9cbf05a..5ec6098 100644 --- a/src/components/Organizations/OrganizationsList.tsx +++ b/src/components/Organizations/OrganizationsList.tsx @@ -3,7 +3,7 @@ import { useOrganizationCount, useOrganizationList } from '~queries/organization import { debounce } from '~utils/debounce' import { generatePath, useNavigate, useParams } from 'react-router-dom' import { RoutedPaginationProvider } from '~components/Pagination/PaginationProvider' -import { Loading } from '~src/router/SuspenseLoader' +import { LoadingCards } from '~src/router/SuspenseLoader' import OrganizationCard from '~components/Organizations/Card' import { RoutedPagination } from '~components/Pagination/Pagination' import LoadingError from '~src/layout/LoadingError' @@ -51,7 +51,7 @@ export const OrganizationsList = () => { const isLoading = isLoadingCount || isLoadingOrgs if (isLoading) { - return + return } if (!orgs || orgs?.organizations.length === 0 || isError) { diff --git a/src/components/Stats/LatestBlocks.tsx b/src/components/Stats/LatestBlocks.tsx index 5b2df73..e2d7684 100644 --- a/src/components/Stats/LatestBlocks.tsx +++ b/src/components/Stats/LatestBlocks.tsx @@ -1,8 +1,9 @@ -import { Button, Card, CardBody, SkeletonText, Stack } from '@chakra-ui/react' +import { Button, Stack } from '@chakra-ui/react' import { Trans } from 'react-i18next' import { BlockCard } from '~components/Blocks/BlockCard' import { useBlockList } from '~queries/blocks' import { useChainInfo } from '~queries/stats' +import { LoadingCards } from '~src/router/SuspenseLoader' export const LatestBlocks = () => { const blockListSize = 4 @@ -17,17 +18,7 @@ export const LatestBlocks = () => { const isLoading = isLoadingStats || isLoadingBlocks if (isLoading || !stats || !stats?.height || !blocks) { - return ( - - {Array.from({ length: blockListSize }).map((_, i) => ( - - - - - - ))} - - ) + return } return ( diff --git a/src/router/SuspenseLoader.tsx b/src/router/SuspenseLoader.tsx index f7ccf93..1445e8c 100644 --- a/src/router/SuspenseLoader.tsx +++ b/src/router/SuspenseLoader.tsx @@ -1,4 +1,4 @@ -import { Spinner, Square, Text } from '@chakra-ui/react' +import { Card, CardBody, SkeletonText, Spinner, Square, Stack, Text } from '@chakra-ui/react' import { ReactNode, Suspense } from 'react' import { Trans } from 'react-i18next' @@ -11,6 +11,18 @@ export const Loading = () => ( ) +export const LoadingCards = ({ length = 4 }: { length?: number }) => ( + + {Array.from({ length }).map((_, i) => ( + + + + + + ))} + +) + export const SuspenseLoader = ({ children }: { children: ReactNode }) => ( }>{children} ) From 4a4a3ee2f3ed21400b9efaa3484ce93db4a13256 Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 5 Jun 2024 15:34:54 +0200 Subject: [PATCH 12/19] Clean code --- .../Organizations/OrganizationsList.tsx | 16 +++++++--------- src/pages/Organization/List.tsx | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/components/Organizations/OrganizationsList.tsx b/src/components/Organizations/OrganizationsList.tsx index 5ec6098..c37446e 100644 --- a/src/components/Organizations/OrganizationsList.tsx +++ b/src/components/Organizations/OrganizationsList.tsx @@ -35,7 +35,7 @@ export const OrganizationsFilter = () => { export const OrganizationsList = () => { const { page, orgId }: { page?: number; orgId?: string } = useParams() - const { data: orgsCount, isLoading: isLoadingCount, error: countError } = useOrganizationCount() + const { data: orgsCount, isLoading: isLoadingCount } = useOrganizationCount() const count = orgsCount?.count || 0 const { @@ -59,13 +59,11 @@ export const OrganizationsList = () => { } return ( - <> - - {orgs?.organizations.map((org) => ( - - ))} - - - + + {orgs?.organizations.map((org) => ( + + ))} + + ) } diff --git a/src/pages/Organization/List.tsx b/src/pages/Organization/List.tsx index e21f782..8ef05c7 100644 --- a/src/pages/Organization/List.tsx +++ b/src/pages/Organization/List.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next' const OrganizationList = () => { const { t } = useTranslation() - const { data: orgsCount, isLoading, error: countError } = useOrganizationCount() + const { data: orgsCount, isLoading } = useOrganizationCount() const subtitle = !isLoading ? t('organizations.organizations_count', { count: orgsCount?.count || 0 }) : '' From 301d9c737fc5b618fba5aacd8e32e43a08dea6f1 Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 5 Jun 2024 15:41:45 +0200 Subject: [PATCH 13/19] Use organization image --- src/components/Organizations/Card.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Organizations/Card.tsx b/src/components/Organizations/Card.tsx index e6ab16f..1623ae8 100644 --- a/src/components/Organizations/Card.tsx +++ b/src/components/Organizations/Card.tsx @@ -2,7 +2,7 @@ import { Box, Card, CardBody, Text } from '@chakra-ui/react' import { Trans, useTranslation } from 'react-i18next' import { ReducedTextAndCopy } from '~components/CopyBtn' import { OrganizationProvider, useOrganization } from '@vocdoni/react-providers' -import { OrganizationAvatar as Avatar, OrganizationName } from '@vocdoni/chakra-components' +import { OrganizationImage as Avatar, OrganizationName } from '@vocdoni/chakra-components' interface IOrganizationCardProps { id: string From 5bfc35f9e8221b7e9db36d926651028f870b226b Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 5 Jun 2024 15:51:35 +0200 Subject: [PATCH 14/19] Import images --- src/components/Footer.tsx | 4 +++- src/components/Home/FeaturedContent.tsx | 22 +++++++++++++++------- src/components/TopBar.tsx | 10 +++++++--- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 0f9bd96..911317b 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -2,6 +2,8 @@ import { Box, Flex, Img, Link } from '@chakra-ui/react' import React from 'react' import { useTranslation } from 'react-i18next' +import logo from '/images/logo-classic.svg' + export const Footer = () => { const { i18n } = useTranslation() @@ -38,7 +40,7 @@ export const Footer = () => { - Vocdoni + Vocdoni diff --git a/src/components/Home/FeaturedContent.tsx b/src/components/Home/FeaturedContent.tsx index bc3b9bd..0ba87bd 100644 --- a/src/components/Home/FeaturedContent.tsx +++ b/src/components/Home/FeaturedContent.tsx @@ -1,39 +1,47 @@ import { Box, Button, Flex, Image, Text } from '@chakra-ui/react' import { Trans, useTranslation } from 'react-i18next' +import anonymous from 'images/featured/anonymous.png' +import open from 'images/featured/open-source.png' +import scalable from 'images/featured/scalable.png' +import inexpensive from 'images/featured/inexpensive.png' +import censorship from 'images/featured/censorship_subtitle.png' +import verifiable from 'images/featured/verifiable.png' +import edge from '/images/featured/edge-protocol.png' + export const FeaturedContent = () => { const { t } = useTranslation() const icons = [ { width: '96px', - src: 'images/featured/anonymous.png', + src: anonymous, alt: t('featured.anonymous_image_alt'), }, { width: '110px', - src: 'images/featured/open-source.png', + src: open, alt: t('featured.open_source_image_alt'), }, { width: '84px', - src: 'images/featured/scalable.png', + src: scalable, alt: t('featured.scalable_image_alt'), }, { width: '98px', - src: 'images/featured/inexpensive.png', + src: inexpensive, alt: t('featured.inexpensive_image_alt'), }, { width: '70px', - src: 'images/featured/censorship_subtitle.png', + src: censorship, alt: t('featured.open_source_image_alt'), }, { width: '100px', - src: 'images/featured/verifiable.png', + src: verifiable, alt: t('featured.verifiable_image_alt'), }, ] @@ -72,7 +80,7 @@ export const FeaturedContent = () => { Know more - {t('featured.edge_protocol_image_alt')} + {t('featured.edge_protocol_image_alt')} diff --git a/src/components/TopBar.tsx b/src/components/TopBar.tsx index 542986f..70d6a71 100644 --- a/src/components/TopBar.tsx +++ b/src/components/TopBar.tsx @@ -17,6 +17,10 @@ import { generatePath } from 'react-router-dom' import { VocdoniEnvironment } from '~constants' import { ORGANIZATIONS_LIST_PATH } from '~src/router' +import logoUrl from '/images/logo-header.png' +import logoStgUrl from '/images/logo-header-stg.png' +import logoDevUrl from '/images/logo-header-dev.png' + interface HeaderLink { name: string url: string @@ -31,13 +35,13 @@ export const TopBar = () => { let headerUrl switch (env) { case 'prod': - headerUrl = '/images/logo-header.png' + headerUrl = logoUrl break case 'stg': - headerUrl = '/images/logo-header-stg.png' + headerUrl = logoStgUrl break default: - headerUrl = '/images/logo-header-dev.png' + headerUrl = logoDevUrl break } From 9a7de73ef74af56be3683a4c510835f458a2ff0c Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 5 Jun 2024 16:20:13 +0200 Subject: [PATCH 15/19] Refator to PaginatedOrganizationsList --- src/components/Organizations/OrganizationsList.tsx | 2 +- src/pages/Organization/List.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Organizations/OrganizationsList.tsx b/src/components/Organizations/OrganizationsList.tsx index c37446e..1afb934 100644 --- a/src/components/Organizations/OrganizationsList.tsx +++ b/src/components/Organizations/OrganizationsList.tsx @@ -33,7 +33,7 @@ export const OrganizationsFilter = () => { return } -export const OrganizationsList = () => { +export const PaginatedOrganizationsList = () => { const { page, orgId }: { page?: number; orgId?: string } = useParams() const { data: orgsCount, isLoading: isLoadingCount } = useOrganizationCount() const count = orgsCount?.count || 0 diff --git a/src/pages/Organization/List.tsx b/src/pages/Organization/List.tsx index 8ef05c7..c757454 100644 --- a/src/pages/Organization/List.tsx +++ b/src/pages/Organization/List.tsx @@ -1,4 +1,4 @@ -import { OrganizationsFilter, OrganizationsList } from '~components/Organizations/OrganizationsList' +import { OrganizationsFilter, PaginatedOrganizationsList } from '~components/Organizations/OrganizationsList' import ListPageLayout from '~src/layout/ListPageLayout' import { useOrganizationCount } from '~queries/organizations' import { useTranslation } from 'react-i18next' @@ -15,7 +15,7 @@ const OrganizationList = () => { subtitle={subtitle} rightComponent={} > - + ) } From 2c3685bd3d89fee2d31fe193c31ebc43110eac6b Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 5 Jun 2024 16:26:01 +0200 Subject: [PATCH 16/19] Create Loding layout --- .../Organizations/OrganizationsList.tsx | 2 +- src/components/Stats/LatestBlocks.tsx | 2 +- src/layout/Loading.tsx | 23 ++++++++++++++++++ src/router/SuspenseLoader.tsx | 24 +------------------ 4 files changed, 26 insertions(+), 25 deletions(-) create mode 100644 src/layout/Loading.tsx diff --git a/src/components/Organizations/OrganizationsList.tsx b/src/components/Organizations/OrganizationsList.tsx index 1afb934..8e6f27e 100644 --- a/src/components/Organizations/OrganizationsList.tsx +++ b/src/components/Organizations/OrganizationsList.tsx @@ -3,12 +3,12 @@ import { useOrganizationCount, useOrganizationList } from '~queries/organization import { debounce } from '~utils/debounce' import { generatePath, useNavigate, useParams } from 'react-router-dom' import { RoutedPaginationProvider } from '~components/Pagination/PaginationProvider' -import { LoadingCards } from '~src/router/SuspenseLoader' import OrganizationCard from '~components/Organizations/Card' import { RoutedPagination } from '~components/Pagination/Pagination' import LoadingError from '~src/layout/LoadingError' import { useTranslation } from 'react-i18next' import { ORGANIZATIONS_LIST_PATH } from '~src/router' +import { LoadingCards } from '~src/layout/Loading' export const OrganizationsFilter = () => { const { t } = useTranslation() diff --git a/src/components/Stats/LatestBlocks.tsx b/src/components/Stats/LatestBlocks.tsx index e2d7684..82f5a42 100644 --- a/src/components/Stats/LatestBlocks.tsx +++ b/src/components/Stats/LatestBlocks.tsx @@ -3,7 +3,7 @@ import { Trans } from 'react-i18next' import { BlockCard } from '~components/Blocks/BlockCard' import { useBlockList } from '~queries/blocks' import { useChainInfo } from '~queries/stats' -import { LoadingCards } from '~src/router/SuspenseLoader' +import { LoadingCards } from '~src/layout/Loading' export const LatestBlocks = () => { const blockListSize = 4 diff --git a/src/layout/Loading.tsx b/src/layout/Loading.tsx new file mode 100644 index 0000000..f8c4b05 --- /dev/null +++ b/src/layout/Loading.tsx @@ -0,0 +1,23 @@ +import { Card, CardBody, SkeletonText, Spinner, Square, Stack, Text } from '@chakra-ui/react' +import { Trans } from 'react-i18next' + +export const Loading = () => ( + + + + Loading... + + +) + +export const LoadingCards = ({ length = 4 }: { length?: number }) => ( + + {Array.from({ length }).map((_, i) => ( + + + + + + ))} + +) diff --git a/src/router/SuspenseLoader.tsx b/src/router/SuspenseLoader.tsx index 1445e8c..44e4753 100644 --- a/src/router/SuspenseLoader.tsx +++ b/src/router/SuspenseLoader.tsx @@ -1,27 +1,5 @@ -import { Card, CardBody, SkeletonText, Spinner, Square, Stack, Text } from '@chakra-ui/react' import { ReactNode, Suspense } from 'react' -import { Trans } from 'react-i18next' - -export const Loading = () => ( - - - - Loading... - - -) - -export const LoadingCards = ({ length = 4 }: { length?: number }) => ( - - {Array.from({ length }).map((_, i) => ( - - - - - - ))} - -) +import { Loading } from '~src/layout/Loading' export const SuspenseLoader = ({ children }: { children: ReactNode }) => ( }>{children} From 990c1893a2d1bcdcbd11e496203dd8ec32095aa7 Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 5 Jun 2024 16:32:57 +0200 Subject: [PATCH 17/19] Reset page every time filter change --- .../Organizations/OrganizationsList.tsx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/components/Organizations/OrganizationsList.tsx b/src/components/Organizations/OrganizationsList.tsx index 8e6f27e..44a3b63 100644 --- a/src/components/Organizations/OrganizationsList.tsx +++ b/src/components/Organizations/OrganizationsList.tsx @@ -7,23 +7,15 @@ import OrganizationCard from '~components/Organizations/Card' import { RoutedPagination } from '~components/Pagination/Pagination' import LoadingError from '~src/layout/LoadingError' import { useTranslation } from 'react-i18next' -import { ORGANIZATIONS_LIST_PATH } from '~src/router' import { LoadingCards } from '~src/layout/Loading' +import { ORGANIZATIONS_LIST_PATH } from '~src/router' export const OrganizationsFilter = () => { const { t } = useTranslation() - const { page, orgId }: { page?: number; orgId?: string } = useParams() const navigate = useNavigate() const debouncedSearch = debounce((value) => { - const getPath = () => { - // If previous state has not org id, ensure to look at the first page - if (!orgId || orgId.length === 0) { - return generatePath(ORGANIZATIONS_LIST_PATH, { page: '0', query: value as string }) - } - return generatePath(ORGANIZATIONS_LIST_PATH, { page: page?.toString() || '0', query: value as string }) - } - navigate(getPath()) + navigate(generatePath(ORGANIZATIONS_LIST_PATH, { page: '0', query: value as string })) }, 1000) const searchOnChange = (event: any) => { @@ -34,7 +26,7 @@ export const OrganizationsFilter = () => { } export const PaginatedOrganizationsList = () => { - const { page, orgId }: { page?: number; orgId?: string } = useParams() + const { page, query }: { page?: number; query?: string } = useParams() const { data: orgsCount, isLoading: isLoadingCount } = useOrganizationCount() const count = orgsCount?.count || 0 @@ -45,7 +37,7 @@ export const PaginatedOrganizationsList = () => { error, } = useOrganizationList({ page: Number(page || 0), - organizationId: orgId, + organizationId: query, }) const isLoading = isLoadingCount || isLoadingOrgs From 7c9700ced34ba2e648a4fcf9501ec863e6070838 Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 5 Jun 2024 16:37:03 +0200 Subject: [PATCH 18/19] Fix relative path --- src/components/Home/FeaturedContent.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/Home/FeaturedContent.tsx b/src/components/Home/FeaturedContent.tsx index 0ba87bd..be9c25b 100644 --- a/src/components/Home/FeaturedContent.tsx +++ b/src/components/Home/FeaturedContent.tsx @@ -1,12 +1,12 @@ import { Box, Button, Flex, Image, Text } from '@chakra-ui/react' import { Trans, useTranslation } from 'react-i18next' -import anonymous from 'images/featured/anonymous.png' -import open from 'images/featured/open-source.png' -import scalable from 'images/featured/scalable.png' -import inexpensive from 'images/featured/inexpensive.png' -import censorship from 'images/featured/censorship_subtitle.png' -import verifiable from 'images/featured/verifiable.png' +import anonymous from '/images/featured/anonymous.png' +import open from '/images/featured/open-source.png' +import scalable from '/images/featured/scalable.png' +import inexpensive from '/images/featured/inexpensive.png' +import censorship from '/images/featured/censorship_subtitle.png' +import verifiable from '/images/featured/verifiable.png' import edge from '/images/featured/edge-protocol.png' export const FeaturedContent = () => { From 02c5ce293ecc3740ab33a85cfa5be4dbc5b763ca Mon Sep 17 00:00:00 2001 From: selankon Date: Wed, 5 Jun 2024 16:59:02 +0200 Subject: [PATCH 19/19] Use path constant --- src/components/Organizations/OrganizationsList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Organizations/OrganizationsList.tsx b/src/components/Organizations/OrganizationsList.tsx index 44a3b63..623cbee 100644 --- a/src/components/Organizations/OrganizationsList.tsx +++ b/src/components/Organizations/OrganizationsList.tsx @@ -51,7 +51,7 @@ export const PaginatedOrganizationsList = () => { } return ( - + {orgs?.organizations.map((org) => ( ))}