From 09346304d52150d5676b6f4290ee9df405e51d0a Mon Sep 17 00:00:00 2001 From: mpujadas Date: Sun, 21 Jan 2024 19:55:32 +0100 Subject: [PATCH 01/30] Add share button transaction page --- packages/client/components/ui/ShareIcon.tsx | 43 ++++++++++++++++++ packages/client/pages/transaction/[hash].tsx | 5 ++ .../static/images/icons/share_dark.webp | Bin 0 -> 504 bytes .../static/images/icons/share_light.webp | Bin 0 -> 516 bytes .../static/images/icons/shared_dark.webp | Bin 0 -> 1078 bytes .../static/images/icons/shared_light.webp | Bin 0 -> 1078 bytes 6 files changed, 48 insertions(+) create mode 100644 packages/client/components/ui/ShareIcon.tsx create mode 100644 packages/client/public/static/images/icons/share_dark.webp create mode 100644 packages/client/public/static/images/icons/share_light.webp create mode 100644 packages/client/public/static/images/icons/shared_dark.webp create mode 100644 packages/client/public/static/images/icons/shared_light.webp diff --git a/packages/client/components/ui/ShareIcon.tsx b/packages/client/components/ui/ShareIcon.tsx new file mode 100644 index 00000000..6f3d1a90 --- /dev/null +++ b/packages/client/components/ui/ShareIcon.tsx @@ -0,0 +1,43 @@ +import React, { useState, useContext } from 'react'; + +// Contexts +import ThemeModeContext from '../../contexts/theme-mode/ThemeModeContext'; + +// Components +import CustomImage from './CustomImage'; + +// Props +type Props = { + value: string; +}; + +const ShareIcon = ({ value }: Props) => { + // Theme Mode Context + const { themeMode } = useContext(ThemeModeContext) ?? {}; + + // States + const [copied, setCopied] = useState(false); + + // Function to handle the Copy Click event + const handleCopyClick = async () => { + await navigator.clipboard.writeText(value); + setCopied(true); + + setTimeout(() => { + setCopied(false); + }, 250); + }; + + return ( + handleCopyClick()} + /> + ); +}; + +export default ShareIcon; diff --git a/packages/client/pages/transaction/[hash].tsx b/packages/client/pages/transaction/[hash].tsx index 826b6967..ae80f9e0 100644 --- a/packages/client/pages/transaction/[hash].tsx +++ b/packages/client/pages/transaction/[hash].tsx @@ -19,6 +19,8 @@ import AddressCopy from '../../components/ui/AddressCopy'; // Types import { Transaction } from '../../types'; +import CopyIcon from '../../components/ui/CopyIcon'; +import ShareIcon from '../../components/ui/ShareIcon'; const TransactionPage = () => { // Next router @@ -158,6 +160,9 @@ const TransactionPage = () => { isSelected={tabPageIndex === 1} onClick={() => setTabPageIndex(1)} /> +
+ +
{getSelectedTab()} diff --git a/packages/client/public/static/images/icons/share_dark.webp b/packages/client/public/static/images/icons/share_dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..272f8d0ae66c9c2871f62d7ec80fe8118e1c506d GIT binary patch literal 504 zcmV89b0b?o6{-zybh*h@V6Wx=~MqP8@WP2fJhJ+ z5`cOJ03LUE(LMlTB-?G9uS6>6ED#4!ss9!&5dVX2Afo>c+(uHQRF>hWZuSqNX0&!_ zaYHFe_+W`IGCJtn2e2Fk$)+xJ0!&0iqf7@!u`qA!w&2A?9qo?cYeei$U}42>Xnbpb z`vnKhAJ}Xh9`>CaqV_YG4AsXg9nR@tXK|D4v3del&lvK?N7|>`WsycPG-|gywQs5@ znV=Z2inYiJcu79`!O{8Ze1=Z#btcKptUbEIQVd9{vE4v4(b2>N-eRJwg%4CVG8(wT zQ^&&~r;dYx&i(i9bE*KMGli98?Sm0e_XVcavnW1(MK2TSRZINEqpGjo!tu*JiddXuk3X4zz<9pdxSq;>@Gu%zbvD($HSQ6=bnjdM literal 0 HcmV?d00001 diff --git a/packages/client/public/static/images/icons/share_light.webp b/packages/client/public/static/images/icons/share_light.webp new file mode 100644 index 0000000000000000000000000000000000000000..0484e390b4a8837d94d375936349e718b3d7b8ba GIT binary patch literal 516 zcmV+f0{i_^Nk&He0RRA3MM6+kP&iEQ0RR9mF+dCuC&8d?BuBn}_o$vfr8YDOByD8L z-aph)+_r6`+!;61_gN!kBT2IaGYXIL{xWay0o=RT@CFDkr~%y2FhgsAPgpqcQK4a0 z5E>>d5K#~g3WBOQAA0cMU8T?81;j|U+csbTl`};p@D%$m(gg7z=msMC@4#&&MM`Dq z7;1a};LP|#z)GlHR*DWKeU@c_sl5ZO|F~qfnJ$2ZbIu~q06V8}4!RZePGbAiZQv^6 zbbrvQ=zc=~(m#I2o$?o!eb=+!D6`V~8zhTW*Bc#9*~46MAKjw5jr#X4(w6tQPxtN= z$M6%(ZjWjoR&SRGhR>Vf*0~ngxOD70{qYw11f$y9ETR`vdvqHbCg5T3Pa0yvu^v@Yoscwn7&0^AJ<-C_HeM|3*mpthc? zjg+mi6Kur1nr)n;L~fo*j{m?7W)sr@#kAC6sMLhK{V=z}b3)p99=+ToR1pl^p7FKn zn~RUSwhi~AAD>ZmE{!=I&ahEQw`Q2oM+p@6N8jbq3PWx|=7X1jSAL&< zk+-x$mjZB|NiR{Xu0(|tg?o3{CUZKCKF(8X>=`}%_9M6ac-Ock_oTVyo7Fa%)$b+V G;s5|%?)Y8+ literal 0 HcmV?d00001 diff --git a/packages/client/public/static/images/icons/shared_dark.webp b/packages/client/public/static/images/icons/shared_dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..c3ff6df3a087fa4d9b5a14aef652da302dd3147f GIT binary patch literal 1078 zcmV-61j+kSNk&F41ONb6MM6+kP&iB>1ONapUVs-6hr+0BBuTF2k9J>uNYgYJw{7Hz zoBA*GzPEe%?6{F6Mao+C-%h$$?Qt?>BuP*$!8gLGzsaLK!WvwFi?>$^W|aW-x}iYL z>RF0rmf+uZDA2PGX4b4iJ*#K+ETmxO4Q21y|3A-=0X|;FB>8}QmBWCq2rJTA|NCnQgG;i2qWKdQ~bB&_57D@d4P0ld)p|AnJ_yWhCZ{N zO7Fe*&QL@Nx&O-nEPs+_+rNnZJ8&CGkqXpvL&t+ZNdAL3y|{XI)qAEdKKHIJb{6yg zL9ueyBZRl7D|tWUtX`3XS1W}rX5OAj%I8zV>PuIoL#sr7N}ufC#P&0(a$6hkH-xd< zZnwLH@m4o;3&`~o!<9kzorBrXgtyErqJ4HJvXYClG*oL*geQg8v&E|U%;6mN7PazT zG$muX7L$sBlr-TZQgwIO>2SEkOVPgUv{Kv zVUF}*+QGaa4m(Q@GfNjddS^?|1UQzBk%c&l;Nvks&E&0AQzSeVJXQ>2rTO(?3;rgY&QZSp+FwD497Qawhmv)3evh}XZp!tGQb%!Mpy0Zk&Yjbk^yyN z16V11F;gKH!q&D18ZU;}2{37EpkOn^Ygl4tbUZza|CzJ?u=^4Iq2n#f{b5&*H+&3W zWq{#m8X{bnSQ{8GqGnar%izAkJ4Cv>_!_vcNRF8RNz*C3C!OQ=bdVIcsyJmg+i7ySnHsDSu?fHD|?f;v&Kw^OKk=h1g9n-q@cbF=I4{ z*+)SKIz6y?DAy@uvtp2D^Kc&6oj_JO?H*G1}C=SiZb zrrOzcCuT*F*T~E~#jGo;rSeqN>LNd^mZEE|YoU)lim+L$4geU)COQ=1f#SvDE+IEg z)r`4usthAHPEQ-USAF+{vD0q1JB0E2JyGT^sq$&p;;!;Q5wBmDq5Ye<`gW3Rm3Wf( wvp;Wq%f0*}tG`wx_RNGWqC|cM#f9ZYyhMsyaXKM@7$^Q=%0Aykh6951J literal 0 HcmV?d00001 diff --git a/packages/client/public/static/images/icons/shared_light.webp b/packages/client/public/static/images/icons/shared_light.webp new file mode 100644 index 0000000000000000000000000000000000000000..c3ff6df3a087fa4d9b5a14aef652da302dd3147f GIT binary patch literal 1078 zcmV-61j+kSNk&F41ONb6MM6+kP&iB>1ONapUVs-6hr+0BBuTF2k9J>uNYgYJw{7Hz zoBA*GzPEe%?6{F6Mao+C-%h$$?Qt?>BuP*$!8gLGzsaLK!WvwFi?>$^W|aW-x}iYL z>RF0rmf+uZDA2PGX4b4iJ*#K+ETmxO4Q21y|3A-=0X|;FB>8}QmBWCq2rJTA|NCnQgG;i2qWKdQ~bB&_57D@d4P0ld)p|AnJ_yWhCZ{N zO7Fe*&QL@Nx&O-nEPs+_+rNnZJ8&CGkqXpvL&t+ZNdAL3y|{XI)qAEdKKHIJb{6yg zL9ueyBZRl7D|tWUtX`3XS1W}rX5OAj%I8zV>PuIoL#sr7N}ufC#P&0(a$6hkH-xd< zZnwLH@m4o;3&`~o!<9kzorBrXgtyErqJ4HJvXYClG*oL*geQg8v&E|U%;6mN7PazT zG$muX7L$sBlr-TZQgwIO>2SEkOVPgUv{Kv zVUF}*+QGaa4m(Q@GfNjddS^?|1UQzBk%c&l;Nvks&E&0AQzSeVJXQ>2rTO(?3;rgY&QZSp+FwD497Qawhmv)3evh}XZp!tGQb%!Mpy0Zk&Yjbk^yyN z16V11F;gKH!q&D18ZU;}2{37EpkOn^Ygl4tbUZza|CzJ?u=^4Iq2n#f{b5&*H+&3W zWq{#m8X{bnSQ{8GqGnar%izAkJ4Cv>_!_vcNRF8RNz*C3C!OQ=bdVIcsyJmg+i7ySnHsDSu?fHD|?f;v&Kw^OKk=h1g9n-q@cbF=I4{ z*+)SKIz6y?DAy@uvtp2D^Kc&6oj_JO?H*G1}C=SiZb zrrOzcCuT*F*T~E~#jGo;rSeqN>LNd^mZEE|YoU)lim+L$4geU)COQ=1f#SvDE+IEg z)r`4usthAHPEQ-USAF+{vD0q1JB0E2JyGT^sq$&p;;!;Q5wBmDq5Ye<`gW3Rm3Wf( wvp;Wq%f0*}tG`wx_RNGWqC|cM#f9ZYyhMsyaXKM@7$^Q=%0Aykh6951J literal 0 HcmV?d00001 From e9723749b809436c159835aae609cd82efe59aaa Mon Sep 17 00:00:00 2001 From: Marina Terentii <125751323+Artentii@users.noreply.github.com> Date: Wed, 24 Jan 2024 11:07:59 +0100 Subject: [PATCH 02/30] Added style to share button --- packages/client/components/ui/ShareIcon.tsx | 122 +++++++++++++----- packages/client/pages/transaction/[hash].tsx | 32 ++--- .../public/static/images/social/Discord.webp | Bin 0 -> 982 bytes .../public/static/images/social/Linkedin.webp | Bin 0 -> 790 bytes .../public/static/images/social/Telegram.webp | Bin 0 -> 916 bytes .../public/static/images/social/Twitter.webp | Bin 0 -> 868 bytes 6 files changed, 108 insertions(+), 46 deletions(-) create mode 100644 packages/client/public/static/images/social/Discord.webp create mode 100644 packages/client/public/static/images/social/Linkedin.webp create mode 100644 packages/client/public/static/images/social/Telegram.webp create mode 100644 packages/client/public/static/images/social/Twitter.webp diff --git a/packages/client/components/ui/ShareIcon.tsx b/packages/client/components/ui/ShareIcon.tsx index 6f3d1a90..bb6ba7fe 100644 --- a/packages/client/components/ui/ShareIcon.tsx +++ b/packages/client/components/ui/ShareIcon.tsx @@ -1,42 +1,102 @@ -import React, { useState, useContext } from 'react'; +import React, { useState } from 'react'; +import Image from 'next/image'; -// Contexts -import ThemeModeContext from '../../contexts/theme-mode/ThemeModeContext'; +const socialMedia = [ + { + imgUrl: '/static/images/social/twitter.webp', + txtAlt: 'Twitter', + }, + { + imgUrl: '/static/images/social/telegram.webp', + txtAlt: 'Telegram', + }, + { + imgUrl: '/static/images/social/linkedin.webp', + txtAlt: 'LinkedIn', + }, + { + imgUrl: '/static/images/social/discord.webp', + txtAlt: 'Discord', + }, +]; -// Components -import CustomImage from './CustomImage'; +const ShareIcon = () => { + const [isOpen, setIsOpen] = useState(false); -// Props -type Props = { - value: string; -}; + const handleShareClick = () => { + setIsOpen(!isOpen); + }; -const ShareIcon = ({ value }: Props) => { - // Theme Mode Context - const { themeMode } = useContext(ThemeModeContext) ?? {}; + const handleCloseClick = () => { + setIsOpen(false); + }; - // States - const [copied, setCopied] = useState(false); + return ( +
+ {/* Share button */} +
+ Share + + + +
- // Function to handle the Copy Click event - const handleCopyClick = async () => { - await navigator.clipboard.writeText(value); - setCopied(true); + {/* Hidden share modal */} + {isOpen && ( +
+
+ Share this link via + {/* Close button */} + +
- setTimeout(() => { - setCopied(false); - }, 250); - }; +
+
+ {socialMedia.map((item, index) => ( +
+ {item.txtAlt} +
+ ))} +
- return ( - handleCopyClick()} - /> +
+ Or +
+ copy the link + + + + +
+
+
+ )} +
); }; diff --git a/packages/client/pages/transaction/[hash].tsx b/packages/client/pages/transaction/[hash].tsx index ae80f9e0..8c2e39e3 100644 --- a/packages/client/pages/transaction/[hash].tsx +++ b/packages/client/pages/transaction/[hash].tsx @@ -148,21 +148,23 @@ const TransactionPage = () => { )} {transaction && ( -
-
- setTabPageIndex(0)} - /> - setTabPageIndex(1)} - /> -
- -
+
+
+
+ setTabPageIndex(0)} + /> + setTabPageIndex(1)} + /> +
+
+ +
{getSelectedTab()} diff --git a/packages/client/public/static/images/social/Discord.webp b/packages/client/public/static/images/social/Discord.webp new file mode 100644 index 0000000000000000000000000000000000000000..3ccc2dc2f69ed6161f5b89787891b0445ec65e5e GIT binary patch literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&iD&0{{RoTL2djSHhr?Bt<>X{+06V^Vne6wv8iR z*?-$y{}02qZ6mSm-R(c_e_|9#QkX^oNWeiX#6tXkkT6j|fMnMr6(BVTBu7vq!5IlW zkT^gnI6!JZ;t`LK)JRZ+ig<*gKtc+GB&k3qN(Cz7M~DU-%9zoB(*X1Sp!`2SDx(kh z=oc0m$`M|wdcdy<-)CW`?n$wf?fBm|k8fIMGax|#7&mQnW`oNpGqJ@m*~6jKHQ9Um z|1YBEEa~)si2g&6B)M%fL;64n2PFha{sD;jeZR?jf1iOOv-{+Hfp%xdar&l#@AWZK zjp8(Oo4>7L8s9qOKAxd~QTptpbc|9)Yw^PKDM}ipvaDw)Y$RCubc7CEOYyN&tFbbR z(UsF1fqSzwQ`qV`LZ_}ZpNKK{bbe?)5kL8-#)KnO;ya1IsDffV%^WsF7|JBU%0 z50pAhRRu)Kz&`~&YGaxe4Lj)^EFLG2FGwX^>TM#oQ^GeSw zNzx{BcSL2+NaB&%L{z$V>g{%^=y5blB9|+XN!7=#J=@DODjo-2X^f3t;h-82o^@TA z3FTc33s=Rl^#)=3q!*)9I2t`;F`zQ#&#G48Ti+$kju91VTAlN!MH&iyDn~?W#%8!P z(+bzbp}?%&oDuP?_=PXwul$fIS!q!>jw^Cs_9H+Ho7RngTMR13N`tzNB^;~?)uP2E z|2X&)3vQ|K72yjq#asYHdPx9?4LuR~X|Dhq8n)5r?TvlfUeOD>+%1B{<_dY`IFF41@Dg5tdOke%0{gD5i`~g`Gnmy`wq-uMNJ5sZEgF@3? z9$dkVibHDSL1R=LQadkGUBpn}KV5Y%P~@??I;~YP5WLY-_X8AHdK%HZ(!nF_sIdda zzk#v?6({FVb1F=jr(jjt{VX4WZ#s3w$qPvMvUI~G?5MES@&V+0k-ppc>IXzkMU&!O zpE?UCxhw~T6On#iz?m%^$)o9F*%vjSX^3aT;Ftad3rdv)nd%pCb(& z(%>+WKXgdLZXm|7_ir~5X()&^5owV71;03*sIC&11x(c$Do#T#EE+-_KkyF@1w*MA zL{Jrjs-9)4h2kgTw$}+qQw?>>ck(Rm$>EGnJwiB6|D(pDaowfdeA? zKLOxv9_!*bRv@T~7vD(bvt6Pi3G(rdY-6GoD>dGcX1XcBVTbG`WAkZ z(xXXH7C{`~nNhGSn|Gh&IM4{A$X=g;oKA7VUq6(X@7y9tq)Z(W2)?6>wpBmpP3 zLL?E#mVQXcy_GRZIS(ypk`q;>5Cx%0rG!L{D`=7xE1)p6EHx%+=c+`A;?T0hnB<)+ z6ADC2gC^_4+w+r)Hj}w3O0D&3!Qm|rdMWrN>&HQHo5!vovDOTX7d%KEptU>GDU%lf z_$BLg(;1T;04(ecYlb?LxdM2zN8MV%9dV5TG6&SZIyt#$5!)sFlH8y0BpAYy#jsVc z*W2z$ty~-%J4DnuHWCf5Yr$ddV&E!za}gNZMAQ$A)T;&NqlTl$)Vo0~DBW_?flO=(QFQQ;x1>PjlP&v=! zNWkKg7&x*4OCc}^yK5ODu>B^12J0&2r@^60xoL2_W|e7hy<~(DxYrq%j~sl=k;evn zT{6HJ{LNF(vgHW8*634`gZCPJ`G|p~e)k|-=D;?eqLb`HkAQcyc+c$opXw2a7(GqD U(#-Tfoh_b5Anhp6^BlYe0K7MJ-T(jq literal 0 HcmV?d00001 diff --git a/packages/client/public/static/images/social/Telegram.webp b/packages/client/public/static/images/social/Telegram.webp new file mode 100644 index 0000000000000000000000000000000000000000..e66137286e7a71b35100e16a54cf99e10fe5a8e2 GIT binary patch literal 916 zcmV;F18e+JNk&GD0{{S5MM6+kP&iD00{{RoTL2djFXE_?Bu9$%XZh2-_tfna4Tf#o zIMVgoe^ux1`wliBd>pM3}!)kch%0 zG9po;L>ZAN5Ch3T%mPpm1FZm4W+yZmp1&kvTX8VhH3@CgK3|L9VsitI1tWv_nQ8we z37?rJJO_BVk;&uK55CO9O{GVO*xQmRQXFUzC#M|C0-d z=>G(e*zM-u{hLio0c|(`{V8Ey)aT862)6e%Tvz8!>s5PSgZcc;o6UIvfmNe@*wm0( z-D_$3A4skmS@#8mS6a^389cm~@>IafN=s;G@U?j5(`WGbo=aEo`kqHu@cf)ZSMdJa zovskT2Uog65bqtGA&@Ul9udkyJ7^Hjs6ik7IBH9n1$?GJZY6BUxuSUDUq+|n1s=1HWS zrVQ8gCM7+Mlwq6>4RVHxZe@&OWf~2DqMRJsC?4dRYC!`?*M51FP60J7m> zzZVa&KT!@-p5}tb@>sdddDd5Q2Fhgw-jAi6BPTHvy~!CmIpz6H-*PZ;@r&o0sFZ`O zXP5GxI%^;J=UwXnm1J0lXKu0%PD#dfXy#_R z2+NEt7eOht4b>r;leI3wF(Zes!6;i7bQg$HliSNMe39!q1kdFBd+OgaF@8^ddnVpX zuO`pLedkg6Mf~%^ms*oVUHWlDQtvz{*Cba@g-z~S5^%`}Ohms09Ac{#5IOzt#! qt8Yq{Nj-1L@6Sa&aZ}#RTMFXm>&>6(q^q3%zj$pawC#4g-I8cjeYw;C literal 0 HcmV?d00001 diff --git a/packages/client/public/static/images/social/Twitter.webp b/packages/client/public/static/images/social/Twitter.webp new file mode 100644 index 0000000000000000000000000000000000000000..1fa82616306eed2a8fbeab4d7c81736509930aef GIT binary patch literal 868 zcmV-q1DpI(Nk&Fo0{{S5MM6+kP&iCb0{{RoTfi0&PhpU4q&~r)zwZ4z5M*-lsqUWvA4^xx5ZJ{$I7VU+cU z#%Gl@3iKrX1IuJm|XK2G~Yo(C6od>Xi?1l?~nJf=1nJ*TsX%+YHH{UDhNF) z@HHxu>yNTwrGm|8q}hB&38jTbXYt=f89UZ|Q4MX6a?%-li_THQ6g5Z`Nusiu0+F%f z%4Q4Wj3p|YG2lJ=EXtZ`P<)9#3sKoDVKYkCFQHkh1oEmDeHKkkz0^7T9gik72aQ@Y z)rt_sBC4g9#x)8cmx?#7`G_?=6l(T6(hJmHMdJZ_COG#%RJFVli;4W^#VaM$CA~ns z6f`x!ROf<^0;Qp82h24Rin~C)gf}ro^pvHL zP`m}|cDX!$B25PoJ8?ouo@7LtCdO2nP`rFdJm1OoZ(J`-D20bdo@N0ca+mwS3nH`K zNnW^W!sR8`6D~8j-maSh$rjP_rjWOVCb&XwWOrwBX$P$BS{DpOw7cs$2PR7s z0agHZTjxO1ESTB0av)<7az~T4E+1OpGk>8E73i=ISz{=uminVLBxvbk{nUXn8^z9| ufDCNBlA?f2ES6%qZyn@hB^mI4`ur@LWjc?0Kd&9wVOcfpG-A_~YFYq4TAFhJ literal 0 HcmV?d00001 From f90664b9ed9219dca0145f20485ebafe1318365c Mon Sep 17 00:00:00 2001 From: Iuri Date: Thu, 25 Jan 2024 21:55:10 +0100 Subject: [PATCH 03/30] Implement Share Links functionallity; enhance design; add component to all share pages --- .../client/components/ui/CopyLinkIcon.tsx | 12 ++ packages/client/components/ui/ShareIcon.tsx | 100 +--------------- packages/client/components/ui/ShareMenu.tsx | 111 ++++++++++++++++++ .../client/components/ui/TooltipContainer.tsx | 3 +- .../components/ui/TooltipResponsive.tsx | 7 +- packages/client/pages/block/[id].tsx | 23 ++-- packages/client/pages/entity/[name].tsx | 21 ++-- packages/client/pages/epoch/[id].tsx | 13 +- packages/client/pages/slot/[id].tsx | 23 ++-- packages/client/pages/transaction/[hash].tsx | 10 +- packages/client/pages/validator/[id].tsx | 9 +- 11 files changed, 197 insertions(+), 135 deletions(-) create mode 100644 packages/client/components/ui/CopyLinkIcon.tsx create mode 100644 packages/client/components/ui/ShareMenu.tsx diff --git a/packages/client/components/ui/CopyLinkIcon.tsx b/packages/client/components/ui/CopyLinkIcon.tsx new file mode 100644 index 00000000..5167d97d --- /dev/null +++ b/packages/client/components/ui/CopyLinkIcon.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +const CopyLinkIcon = () => { + return ( + + + + + ); +}; + +export default CopyLinkIcon; diff --git a/packages/client/components/ui/ShareIcon.tsx b/packages/client/components/ui/ShareIcon.tsx index bb6ba7fe..dd507467 100644 --- a/packages/client/components/ui/ShareIcon.tsx +++ b/packages/client/components/ui/ShareIcon.tsx @@ -1,102 +1,10 @@ -import React, { useState } from 'react'; -import Image from 'next/image'; - -const socialMedia = [ - { - imgUrl: '/static/images/social/twitter.webp', - txtAlt: 'Twitter', - }, - { - imgUrl: '/static/images/social/telegram.webp', - txtAlt: 'Telegram', - }, - { - imgUrl: '/static/images/social/linkedin.webp', - txtAlt: 'LinkedIn', - }, - { - imgUrl: '/static/images/social/discord.webp', - txtAlt: 'Discord', - }, -]; +import React from 'react'; const ShareIcon = () => { - const [isOpen, setIsOpen] = useState(false); - - const handleShareClick = () => { - setIsOpen(!isOpen); - }; - - const handleCloseClick = () => { - setIsOpen(false); - }; - return ( -
- {/* Share button */} -
- Share - - - -
- - {/* Hidden share modal */} - {isOpen && ( -
-
- Share this link via - {/* Close button */} - -
- -
-
- {socialMedia.map((item, index) => ( -
- {item.txtAlt} -
- ))} -
- -
- Or -
- copy the link - - - - -
-
-
- )} -
+ + + ); }; diff --git a/packages/client/components/ui/ShareMenu.tsx b/packages/client/components/ui/ShareMenu.tsx new file mode 100644 index 00000000..2a919bda --- /dev/null +++ b/packages/client/components/ui/ShareMenu.tsx @@ -0,0 +1,111 @@ +import React, { useState } from 'react'; +import Image from 'next/image'; +import { useRouter } from 'next/router'; + +// Components +import TooltipContainer from './TooltipContainer'; +import TooltipResponsive from './TooltipResponsive'; +import CopyLinkIcon from './CopyLinkIcon'; +import ShareIcon from './ShareIcon'; + +// Social Media +const socialMedia = [ + { + icon: '/static/images/social/twitter.webp', + text: 'Twitter', + shareLink: 'https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}', + }, + { + icon: '/static/images/social/telegram.webp', + text: 'Telegram', + shareLink: 'https://telegram.me/share/url?url=${encodedUrl}&text=${encodedTitle}', + }, + { + icon: '/static/images/social/linkedin.webp', + text: 'LinkedIn', + shareLink: 'https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}', + }, +]; + +// Props +type Props = { + type: 'block' | 'entity' | 'epoch' | 'slot' | 'transaction' | 'validator'; +}; + +const ShareMenu = ({ type }: Props) => { + // Router + const router = useRouter(); + + // States + const [copied, setCopied] = useState(false); + + const getShareUrl = (shareLink: string) => { + const encodedUrl = `${process.env.NEXT_PUBLIC_URL_API}${router.asPath}`; + const encodedTitle = `Check out this ${type} on Ethseer!`; + + const url = shareLink.replace('${encodedUrl}', encodedUrl).replace('${encodedTitle}', encodedTitle); + + return url; + }; + + const handleCopyClick = async () => { + const encodedUrl = `${process.env.NEXT_PUBLIC_URL_API}${router.asPath}`; + + await navigator.clipboard.writeText(encodedUrl); + setCopied(true); + + setTimeout(() => { + setCopied(false); + }, 250); + }; + + return ( + +
+ Share + +
+ + +
+ Share this link via +
+
+ +
+ {socialMedia.map(item => ( + + {item.text} + + ))} +
+ +
+ Or + +
+
+ } + top='120%' + polygonRight + /> + + ); +}; + +export default ShareMenu; diff --git a/packages/client/components/ui/TooltipContainer.tsx b/packages/client/components/ui/TooltipContainer.tsx index a9783319..15be9cbd 100644 --- a/packages/client/components/ui/TooltipContainer.tsx +++ b/packages/client/components/ui/TooltipContainer.tsx @@ -4,7 +4,8 @@ const TooltipContainer = styled.div` position: relative; &:hover div { - display: flex; + visibility: visible; + opacity: 1; } `; diff --git a/packages/client/components/ui/TooltipResponsive.tsx b/packages/client/components/ui/TooltipResponsive.tsx index 5593001f..528d5706 100644 --- a/packages/client/components/ui/TooltipResponsive.tsx +++ b/packages/client/components/ui/TooltipResponsive.tsx @@ -12,9 +12,9 @@ type Props = { const TooltipResponsive = ({ width, content, top, polygonLeft, polygonRight, tooltipAbove }: Props) => { const getParentLeftPosition = () => { if (polygonLeft) { - return '-25px'; + return 'calc(50% - 35px)'; } else if (polygonRight) { - return `-${width - 50}px`; + return `calc(50% - ${width - 35}px)`; } else { return `calc(50% - ${width / 2}px)`; } @@ -56,11 +56,12 @@ const TooltipResponsive = ({ width, content, top, polygonLeft, polygonRight, too return (
{content} diff --git a/packages/client/pages/block/[id].tsx b/packages/client/pages/block/[id].tsx index 8f5c99a6..6ad02904 100644 --- a/packages/client/pages/block/[id].tsx +++ b/packages/client/pages/block/[id].tsx @@ -18,6 +18,7 @@ import Card from '../../components/ui/Card'; import TitleWithArrows from '../../components/ui/TitleWithArrows'; import InfoBox from '../../components/layouts/InfoBox'; import AddressCopy from '../../components/ui/AddressCopy'; +import ShareMenu from '../../components/ui/ShareMenu'; // Types import { BlockEL, Transaction } from '../../types'; @@ -124,13 +125,21 @@ const BlockPage = () => { const getInformationView = () => { return (
-
- setTabPageIndex(0)} /> - setTabPageIndex(1)} - /> +
+
+ setTabPageIndex(0)} + /> + setTabPageIndex(1)} + /> +
+ +
{getSelectedTab()} diff --git a/packages/client/pages/entity/[name].tsx b/packages/client/pages/entity/[name].tsx index feb4c23c..54b22929 100644 --- a/packages/client/pages/entity/[name].tsx +++ b/packages/client/pages/entity/[name].tsx @@ -15,18 +15,19 @@ import ProgressSmoothBar from '../../components/ui/ProgressSmoothBar'; import TabHeader from '../../components/ui/TabHeader'; import Title from '../../components/ui/Title'; import CardContent from '../../components/ui/CardContent'; +import ShareMenu from '../../components/ui/ShareMenu'; // Types import { Entity } from '../../types'; const EntityComponent = () => { - // Theme Mode Context - const { themeMode } = useContext(ThemeModeContext) ?? {}; - // Next router const router = useRouter(); const { network, name } = router.query; + // Theme Mode Context + const { themeMode } = useContext(ThemeModeContext) ?? {}; + // States const [entityHour, setEntityHour] = useState(null); const [entityDay, setEntityDay] = useState(null); @@ -91,6 +92,7 @@ const EntityComponent = () => { setLoading(false); } }; + // Container Entity Performance const getEntityPerformance = (entity: Entity) => { return ( @@ -230,9 +232,13 @@ const EntityComponent = () => { )} {entityDay && !showInfoBox && ( -
+
+
+ +
+
{ {entityDay.aggregate_balance?.toLocaleString()} ETH

+ {/* Blocks */}

Blocks:

@@ -306,7 +313,7 @@ const EntityComponent = () => {
{/* Time tabs */} -
+
{
{ )} {!loadingEpoch && epoch && existsEpochRef.current && ( - <> -
-
{getContentEpochStats()}
+
+
+
+ +
+ + {getContentEpochStats()}
- +
)} {!loadingEpoch && infoBoxText && } diff --git a/packages/client/pages/slot/[id].tsx b/packages/client/pages/slot/[id].tsx index 0c1485b9..33e8fd14 100644 --- a/packages/client/pages/slot/[id].tsx +++ b/packages/client/pages/slot/[id].tsx @@ -24,6 +24,7 @@ import Card from '../../components/ui/Card'; import TitleWithArrows from '../../components/ui/TitleWithArrows'; import { LargeTable, LargeTableHeader, LargeTableRow, SmallTable, SmallTableCard } from '../../components/ui/Table'; import AddressCopy from '../../components/ui/AddressCopy'; +import ShareMenu from '../../components/ui/ShareMenu'; // Types import { Block, Withdrawal } from '../../types'; @@ -175,16 +176,20 @@ const Slot = () => { //TABS - Overview & withdrawals const getInformationView = () => (
-
- setTabPageIndex(0)} /> +
+
+ setTabPageIndex(0)} /> + + {existsBlock && ( + setTabPageIndex(1)} + /> + )} +
- {existsBlock && ( - setTabPageIndex(1)} - /> - )} +
{getSelectedTab()} diff --git a/packages/client/pages/transaction/[hash].tsx b/packages/client/pages/transaction/[hash].tsx index 4d1d9d91..b322453a 100644 --- a/packages/client/pages/transaction/[hash].tsx +++ b/packages/client/pages/transaction/[hash].tsx @@ -17,11 +17,10 @@ import Card from '../../components/ui/Card'; import Title from '../../components/ui/Title'; import AddressCopy from '../../components/ui/AddressCopy'; import InfoBox from '../../components/layouts/InfoBox'; +import ShareMenu from '../../components/ui/ShareMenu'; // Types import { Transaction } from '../../types'; -import CopyIcon from '../../components/ui/CopyIcon'; -import ShareIcon from '../../components/ui/ShareIcon'; const TransactionPage = () => { // Next router @@ -150,7 +149,7 @@ const TransactionPage = () => { )} {transaction && ( -
+
{ onClick={() => setTabPageIndex(1)} />
-
- -
+ +
{getSelectedTab()} diff --git a/packages/client/pages/validator/[id].tsx b/packages/client/pages/validator/[id].tsx index 613dd7f9..11e33eaa 100644 --- a/packages/client/pages/validator/[id].tsx +++ b/packages/client/pages/validator/[id].tsx @@ -25,6 +25,7 @@ import LinkEntity from '../../components/ui/LinkEntity'; import TitleWithArrows from '../../components/ui/TitleWithArrows'; import CardContent from '../../components/ui/CardContent'; import { LargeTable, LargeTableHeader, LargeTableRow, SmallTable, SmallTableCard } from '../../components/ui/Table'; +import ShareMenu from '../../components/ui/ShareMenu'; // Types import { Validator, Slot, Withdrawal } from '../../types'; @@ -237,7 +238,7 @@ const ValidatorComponent = () => {
{ {!loadingValidator && validatorHour && (
-
{getContentValidator()}
+
+ +
+ + {getContentValidator()}
setTabPageIndex(0)} /> From a63c6bae31f52d02ec4a4eef81f9225ef85314b8 Mon Sep 17 00:00:00 2001 From: Iuri Date: Thu, 25 Jan 2024 21:57:44 +0100 Subject: [PATCH 04/30] Remove unused images --- .../public/static/images/icons/share_dark.webp | Bin 504 -> 0 bytes .../public/static/images/icons/share_light.webp | Bin 516 -> 0 bytes .../public/static/images/icons/shared_dark.webp | Bin 1078 -> 0 bytes .../public/static/images/icons/shared_light.webp | Bin 1078 -> 0 bytes .../public/static/images/social/Discord.webp | Bin 982 -> 0 bytes 5 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/client/public/static/images/icons/share_dark.webp delete mode 100644 packages/client/public/static/images/icons/share_light.webp delete mode 100644 packages/client/public/static/images/icons/shared_dark.webp delete mode 100644 packages/client/public/static/images/icons/shared_light.webp delete mode 100644 packages/client/public/static/images/social/Discord.webp diff --git a/packages/client/public/static/images/icons/share_dark.webp b/packages/client/public/static/images/icons/share_dark.webp deleted file mode 100644 index 272f8d0ae66c9c2871f62d7ec80fe8118e1c506d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 504 zcmV89b0b?o6{-zybh*h@V6Wx=~MqP8@WP2fJhJ+ z5`cOJ03LUE(LMlTB-?G9uS6>6ED#4!ss9!&5dVX2Afo>c+(uHQRF>hWZuSqNX0&!_ zaYHFe_+W`IGCJtn2e2Fk$)+xJ0!&0iqf7@!u`qA!w&2A?9qo?cYeei$U}42>Xnbpb z`vnKhAJ}Xh9`>CaqV_YG4AsXg9nR@tXK|D4v3del&lvK?N7|>`WsycPG-|gywQs5@ znV=Z2inYiJcu79`!O{8Ze1=Z#btcKptUbEIQVd9{vE4v4(b2>N-eRJwg%4CVG8(wT zQ^&&~r;dYx&i(i9bE*KMGli98?Sm0e_XVcavnW1(MK2TSRZINEqpGjo!tu*JiddXuk3X4zz<9pdxSq;>@Gu%zbvD($HSQ6=bnjdM diff --git a/packages/client/public/static/images/icons/share_light.webp b/packages/client/public/static/images/icons/share_light.webp deleted file mode 100644 index 0484e390b4a8837d94d375936349e718b3d7b8ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 516 zcmV+f0{i_^Nk&He0RRA3MM6+kP&iEQ0RR9mF+dCuC&8d?BuBn}_o$vfr8YDOByD8L z-aph)+_r6`+!;61_gN!kBT2IaGYXIL{xWay0o=RT@CFDkr~%y2FhgsAPgpqcQK4a0 z5E>>d5K#~g3WBOQAA0cMU8T?81;j|U+csbTl`};p@D%$m(gg7z=msMC@4#&&MM`Dq z7;1a};LP|#z)GlHR*DWKeU@c_sl5ZO|F~qfnJ$2ZbIu~q06V8}4!RZePGbAiZQv^6 zbbrvQ=zc=~(m#I2o$?o!eb=+!D6`V~8zhTW*Bc#9*~46MAKjw5jr#X4(w6tQPxtN= z$M6%(ZjWjoR&SRGhR>Vf*0~ngxOD70{qYw11f$y9ETR`vdvqHbCg5T3Pa0yvu^v@Yoscwn7&0^AJ<-C_HeM|3*mpthc? zjg+mi6Kur1nr)n;L~fo*j{m?7W)sr@#kAC6sMLhK{V=z}b3)p99=+ToR1pl^p7FKn zn~RUSwhi~AAD>ZmE{!=I&ahEQw`Q2oM+p@6N8jbq3PWx|=7X1jSAL&< zk+-x$mjZB|NiR{Xu0(|tg?o3{CUZKCKF(8X>=`}%_9M6ac-Ock_oTVyo7Fa%)$b+V G;s5|%?)Y8+ diff --git a/packages/client/public/static/images/icons/shared_dark.webp b/packages/client/public/static/images/icons/shared_dark.webp deleted file mode 100644 index c3ff6df3a087fa4d9b5a14aef652da302dd3147f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1078 zcmV-61j+kSNk&F41ONb6MM6+kP&iB>1ONapUVs-6hr+0BBuTF2k9J>uNYgYJw{7Hz zoBA*GzPEe%?6{F6Mao+C-%h$$?Qt?>BuP*$!8gLGzsaLK!WvwFi?>$^W|aW-x}iYL z>RF0rmf+uZDA2PGX4b4iJ*#K+ETmxO4Q21y|3A-=0X|;FB>8}QmBWCq2rJTA|NCnQgG;i2qWKdQ~bB&_57D@d4P0ld)p|AnJ_yWhCZ{N zO7Fe*&QL@Nx&O-nEPs+_+rNnZJ8&CGkqXpvL&t+ZNdAL3y|{XI)qAEdKKHIJb{6yg zL9ueyBZRl7D|tWUtX`3XS1W}rX5OAj%I8zV>PuIoL#sr7N}ufC#P&0(a$6hkH-xd< zZnwLH@m4o;3&`~o!<9kzorBrXgtyErqJ4HJvXYClG*oL*geQg8v&E|U%;6mN7PazT zG$muX7L$sBlr-TZQgwIO>2SEkOVPgUv{Kv zVUF}*+QGaa4m(Q@GfNjddS^?|1UQzBk%c&l;Nvks&E&0AQzSeVJXQ>2rTO(?3;rgY&QZSp+FwD497Qawhmv)3evh}XZp!tGQb%!Mpy0Zk&Yjbk^yyN z16V11F;gKH!q&D18ZU;}2{37EpkOn^Ygl4tbUZza|CzJ?u=^4Iq2n#f{b5&*H+&3W zWq{#m8X{bnSQ{8GqGnar%izAkJ4Cv>_!_vcNRF8RNz*C3C!OQ=bdVIcsyJmg+i7ySnHsDSu?fHD|?f;v&Kw^OKk=h1g9n-q@cbF=I4{ z*+)SKIz6y?DAy@uvtp2D^Kc&6oj_JO?H*G1}C=SiZb zrrOzcCuT*F*T~E~#jGo;rSeqN>LNd^mZEE|YoU)lim+L$4geU)COQ=1f#SvDE+IEg z)r`4usthAHPEQ-USAF+{vD0q1JB0E2JyGT^sq$&p;;!;Q5wBmDq5Ye<`gW3Rm3Wf( wvp;Wq%f0*}tG`wx_RNGWqC|cM#f9ZYyhMsyaXKM@7$^Q=%0Aykh6951J diff --git a/packages/client/public/static/images/icons/shared_light.webp b/packages/client/public/static/images/icons/shared_light.webp deleted file mode 100644 index c3ff6df3a087fa4d9b5a14aef652da302dd3147f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1078 zcmV-61j+kSNk&F41ONb6MM6+kP&iB>1ONapUVs-6hr+0BBuTF2k9J>uNYgYJw{7Hz zoBA*GzPEe%?6{F6Mao+C-%h$$?Qt?>BuP*$!8gLGzsaLK!WvwFi?>$^W|aW-x}iYL z>RF0rmf+uZDA2PGX4b4iJ*#K+ETmxO4Q21y|3A-=0X|;FB>8}QmBWCq2rJTA|NCnQgG;i2qWKdQ~bB&_57D@d4P0ld)p|AnJ_yWhCZ{N zO7Fe*&QL@Nx&O-nEPs+_+rNnZJ8&CGkqXpvL&t+ZNdAL3y|{XI)qAEdKKHIJb{6yg zL9ueyBZRl7D|tWUtX`3XS1W}rX5OAj%I8zV>PuIoL#sr7N}ufC#P&0(a$6hkH-xd< zZnwLH@m4o;3&`~o!<9kzorBrXgtyErqJ4HJvXYClG*oL*geQg8v&E|U%;6mN7PazT zG$muX7L$sBlr-TZQgwIO>2SEkOVPgUv{Kv zVUF}*+QGaa4m(Q@GfNjddS^?|1UQzBk%c&l;Nvks&E&0AQzSeVJXQ>2rTO(?3;rgY&QZSp+FwD497Qawhmv)3evh}XZp!tGQb%!Mpy0Zk&Yjbk^yyN z16V11F;gKH!q&D18ZU;}2{37EpkOn^Ygl4tbUZza|CzJ?u=^4Iq2n#f{b5&*H+&3W zWq{#m8X{bnSQ{8GqGnar%izAkJ4Cv>_!_vcNRF8RNz*C3C!OQ=bdVIcsyJmg+i7ySnHsDSu?fHD|?f;v&Kw^OKk=h1g9n-q@cbF=I4{ z*+)SKIz6y?DAy@uvtp2D^Kc&6oj_JO?H*G1}C=SiZb zrrOzcCuT*F*T~E~#jGo;rSeqN>LNd^mZEE|YoU)lim+L$4geU)COQ=1f#SvDE+IEg z)r`4usthAHPEQ-USAF+{vD0q1JB0E2JyGT^sq$&p;;!;Q5wBmDq5Ye<`gW3Rm3Wf( wvp;Wq%f0*}tG`wx_RNGWqC|cM#f9ZYyhMsyaXKM@7$^Q=%0Aykh6951J diff --git a/packages/client/public/static/images/social/Discord.webp b/packages/client/public/static/images/social/Discord.webp deleted file mode 100644 index 3ccc2dc2f69ed6161f5b89787891b0445ec65e5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&iD&0{{RoTL2djSHhr?Bt<>X{+06V^Vne6wv8iR z*?-$y{}02qZ6mSm-R(c_e_|9#QkX^oNWeiX#6tXkkT6j|fMnMr6(BVTBu7vq!5IlW zkT^gnI6!JZ;t`LK)JRZ+ig<*gKtc+GB&k3qN(Cz7M~DU-%9zoB(*X1Sp!`2SDx(kh z=oc0m$`M|wdcdy<-)CW`?n$wf?fBm|k8fIMGax|#7&mQnW`oNpGqJ@m*~6jKHQ9Um z|1YBEEa~)si2g&6B)M%fL;64n2PFha{sD;jeZR?jf1iOOv-{+Hfp%xdar&l#@AWZK zjp8(Oo4>7L8s9qOKAxd~QTptpbc|9)Yw^PKDM}ipvaDw)Y$RCubc7CEOYyN&tFbbR z(UsF1fqSzwQ`qV`LZ_}ZpNKK{bbe?)5kL8-#)KnO;ya1IsDffV%^WsF7|JBU%0 z50pAhRRu)Kz&`~&YGaxe4Lj)^EFLG2FGwX^>TM#oQ^GeSw zNzx{BcSL2+NaB&%L{z$V>g{%^=y5blB9|+XN!7=#J=@DODjo-2X^f3t;h-82o^@TA z3FTc33s=Rl^#)=3q!*)9I2t`;F`zQ#&#G48Ti+$kju91VTAlN!MH&iyDn~?W#%8!P z(+bzbp}?%&oDuP?_=PXwul$fIS!q!>jw^Cs_9H+Ho7RngTMR13N`tzNB^;~?)uP2E z|2X&)3vQ|K72yjq#asYHdPx9?4LuR~X|Dhq8n)5r?TvlfUeOD>+%1B{<_dY`IFF41@Dg5tdOke%0{gD5i`~g`Gnmy`wq-uMNJ5sZEgF@3? z9$dkVibHDSL1R=LQadkGUBpn}KV5Y%P~@??I;~YP5WLY-_X8AHdK%HZ(!nF_sIdda zzk#v?6({FVb1F=jr(jjt{VX4WZ#s3w$qPvMvUI~G?5MES@&V+0k-ppc>IXzkMU&!O zpE?UCxhw~T6On#iz?m%^$)o9F*%vjSX^3aT;Ftad3rdv Date: Sat, 27 Jan 2024 12:03:15 +0100 Subject: [PATCH 05/30] Review share button --- packages/client/components/ui/CopyLinkIcon.tsx | 12 ------------ packages/client/components/ui/LinkIcon.tsx | 2 +- packages/client/components/ui/ShareMenu.tsx | 10 +++++----- packages/client/components/ui/TooltipResponsive.tsx | 4 ++-- 4 files changed, 8 insertions(+), 20 deletions(-) delete mode 100644 packages/client/components/ui/CopyLinkIcon.tsx diff --git a/packages/client/components/ui/CopyLinkIcon.tsx b/packages/client/components/ui/CopyLinkIcon.tsx deleted file mode 100644 index 5167d97d..00000000 --- a/packages/client/components/ui/CopyLinkIcon.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; - -const CopyLinkIcon = () => { - return ( - - - - - ); -}; - -export default CopyLinkIcon; diff --git a/packages/client/components/ui/LinkIcon.tsx b/packages/client/components/ui/LinkIcon.tsx index a51ebd88..ddeacdc6 100644 --- a/packages/client/components/ui/LinkIcon.tsx +++ b/packages/client/components/ui/LinkIcon.tsx @@ -25,7 +25,7 @@ const LinkIcon = ({ forceOrange, forceBlue }: Props) => { } }; - return ; + return ; }; export default LinkIcon; diff --git a/packages/client/components/ui/ShareMenu.tsx b/packages/client/components/ui/ShareMenu.tsx index 2a919bda..344c3c93 100644 --- a/packages/client/components/ui/ShareMenu.tsx +++ b/packages/client/components/ui/ShareMenu.tsx @@ -5,8 +5,8 @@ import { useRouter } from 'next/router'; // Components import TooltipContainer from './TooltipContainer'; import TooltipResponsive from './TooltipResponsive'; -import CopyLinkIcon from './CopyLinkIcon'; import ShareIcon from './ShareIcon'; +import LinkIcon from './LinkIcon'; // Social Media const socialMedia = [ @@ -79,7 +79,7 @@ const ShareMenu = ({ type }: Props) => { {socialMedia.map(item => ( {
Or
diff --git a/packages/client/components/ui/TooltipResponsive.tsx b/packages/client/components/ui/TooltipResponsive.tsx index 528d5706..4fbfbc15 100644 --- a/packages/client/components/ui/TooltipResponsive.tsx +++ b/packages/client/components/ui/TooltipResponsive.tsx @@ -56,7 +56,7 @@ const TooltipResponsive = ({ width, content, top, polygonLeft, polygonRight, too return (
- +
); From 22156247dbc61d1b4fedd9982adad4bb7f36a31d Mon Sep 17 00:00:00 2001 From: Iuri Date: Tue, 30 Jan 2024 20:40:51 +0100 Subject: [PATCH 06/30] Finish Design and Functionality of Share Menu --- packages/client/components/ui/LinkIcon.tsx | 10 ++--- .../client/components/ui/ProgressTileBar.tsx | 2 +- packages/client/components/ui/ShareMenu.tsx | 37 +++++++++++++------ .../client/components/ui/TooltipContainer.tsx | 2 +- .../components/ui/TooltipResponsive.tsx | 17 +++++++-- packages/client/pages/_document.tsx | 1 + 6 files changed, 46 insertions(+), 23 deletions(-) diff --git a/packages/client/components/ui/LinkIcon.tsx b/packages/client/components/ui/LinkIcon.tsx index ddeacdc6..862e393b 100644 --- a/packages/client/components/ui/LinkIcon.tsx +++ b/packages/client/components/ui/LinkIcon.tsx @@ -7,18 +7,18 @@ import ThemeModeContext from '../../contexts/theme-mode/ThemeModeContext'; import CustomImage from './CustomImage'; type Props = { - forceOrange?: boolean; - forceBlue?: boolean; + forceLight?: boolean; + forceDark?: boolean; }; -const LinkIcon = ({ forceOrange, forceBlue }: Props) => { +const LinkIcon = ({ forceLight, forceDark }: Props) => { // Theme Mode Context const { themeMode } = useContext(ThemeModeContext) ?? {}; const getUrl = () => { - if (forceOrange) { + if (forceLight) { return '/static/images/icons/link_light.webp'; - } else if (forceBlue) { + } else if (forceDark) { return '/static/images/icons/link_dark.webp'; } else { return themeMode?.darkMode ? '/static/images/icons/link_dark.webp' : '/static/images/icons/link_light.webp'; diff --git a/packages/client/components/ui/ProgressTileBar.tsx b/packages/client/components/ui/ProgressTileBar.tsx index dbb2d606..89b0f445 100644 --- a/packages/client/components/ui/ProgressTileBar.tsx +++ b/packages/client/components/ui/ProgressTileBar.tsx @@ -49,7 +49,7 @@ const ProgressTileBar = ({ tooltipContent, totalBlocks, tooltipAbove }: Props) =
- +
diff --git a/packages/client/components/ui/ShareMenu.tsx b/packages/client/components/ui/ShareMenu.tsx index 344c3c93..930b7779 100644 --- a/packages/client/components/ui/ShareMenu.tsx +++ b/packages/client/components/ui/ShareMenu.tsx @@ -56,7 +56,7 @@ const ShareMenu = ({ type }: Props) => { setTimeout(() => { setCopied(false); - }, 250); + }, 750); }; return ( @@ -79,7 +79,7 @@ const ShareMenu = ({ type }: Props) => { {socialMedia.map(item => ( { {item.text} ))} -
-
- Or - + + + + + {copied ? 'Copied!' : 'Copy the link to your clipboard'} + + } + top='-140%' + polygonRight + tooltipAbove + invertColors + /> +
} diff --git a/packages/client/components/ui/TooltipContainer.tsx b/packages/client/components/ui/TooltipContainer.tsx index 15be9cbd..8dff1f72 100644 --- a/packages/client/components/ui/TooltipContainer.tsx +++ b/packages/client/components/ui/TooltipContainer.tsx @@ -3,7 +3,7 @@ import styled from '@emotion/styled'; const TooltipContainer = styled.div` position: relative; - &:hover div { + &:hover > div:last-of-type { visibility: visible; opacity: 1; } diff --git a/packages/client/components/ui/TooltipResponsive.tsx b/packages/client/components/ui/TooltipResponsive.tsx index 4fbfbc15..7fe94ad6 100644 --- a/packages/client/components/ui/TooltipResponsive.tsx +++ b/packages/client/components/ui/TooltipResponsive.tsx @@ -7,9 +7,10 @@ type Props = { polygonLeft?: boolean; polygonRight?: boolean; tooltipAbove?: boolean; + invertColors?: boolean; }; -const TooltipResponsive = ({ width, content, top, polygonLeft, polygonRight, tooltipAbove }: Props) => { +const TooltipResponsive = ({ width, content, top, polygonLeft, polygonRight, tooltipAbove, invertColors }: Props) => { const getParentLeftPosition = () => { if (polygonLeft) { return 'calc(50% - 35px)'; @@ -22,7 +23,7 @@ const TooltipResponsive = ({ width, content, top, polygonLeft, polygonRight, too const getParentTopPosition = () => { if (tooltipAbove) { - return '-100px'; + return top ?? '-100px'; } else { return top ?? '30px'; } @@ -54,9 +55,17 @@ const TooltipResponsive = ({ width, content, top, polygonLeft, polygonRight, too } }; + const parentClasses = invertColors + ? 'bg-[var(--darkGray)] dark:bg-white text-white dark:text-black' + : 'bg-white dark:bg-[var(--darkGray)] text-black dark:text-white'; + + const polygonClasses = invertColors + ? 'fill-[var(--darkGray)] dark:fill-[var(--white)]' + : 'fill-[var(--white)] dark:fill-[var(--darkGray)]'; + return (
- +
); diff --git a/packages/client/pages/_document.tsx b/packages/client/pages/_document.tsx index 26900de5..a766c5ae 100644 --- a/packages/client/pages/_document.tsx +++ b/packages/client/pages/_document.tsx @@ -28,6 +28,7 @@ export default function Document() { --depositedOrange: #f69a2e; --depositedBlue: #4caee5; --exitedPurple: #a966c1; + --linkPurple: #d1d3ff; //SHADOWS: --boxShadowGreen: 0px 2px 4px 0px #3b503d inset; From 5a63cfaeba21904f9e19d04edd96c1bd2792ed60 Mon Sep 17 00:00:00 2001 From: Iuri Date: Tue, 30 Jan 2024 20:59:41 +0100 Subject: [PATCH 07/30] Fix Tooltip of ProgressTileBar --- packages/client/components/ui/ProgressTileBar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/components/ui/ProgressTileBar.tsx b/packages/client/components/ui/ProgressTileBar.tsx index 89b0f445..469a32f6 100644 --- a/packages/client/components/ui/ProgressTileBar.tsx +++ b/packages/client/components/ui/ProgressTileBar.tsx @@ -48,9 +48,9 @@ const ProgressTileBar = ({ tooltipContent, totalBlocks, tooltipAbove }: Props) = {totalBlocks?.length < 32 && addBars(Array(32 - totalBlocks.length).fill(1))}
- -
+ +
); From 09e2a44b17113d89981db54514a3464d436f856b Mon Sep 17 00:00:00 2001 From: Iuri Date: Thu, 1 Feb 2024 20:32:48 +0100 Subject: [PATCH 08/30] Prepare Layout for Dynamic SEO Items --- packages/client/components/layouts/Layout.tsx | 42 ++++++++++++++----- packages/client/pages/_document.tsx | 16 +++---- packages/client/pages/blocks.tsx | 18 +++----- packages/client/pages/clients.tsx | 21 +++------- packages/client/pages/entities.tsx | 18 +++----- packages/client/pages/epochs.tsx | 18 +++----- packages/client/pages/slots.tsx | 18 +++----- packages/client/pages/transactions.tsx | 18 +++----- packages/client/pages/validators.tsx | 17 +++----- 9 files changed, 81 insertions(+), 105 deletions(-) diff --git a/packages/client/components/layouts/Layout.tsx b/packages/client/components/layouts/Layout.tsx index 420c2ded..5baf7618 100644 --- a/packages/client/components/layouts/Layout.tsx +++ b/packages/client/components/layouts/Layout.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import Head from 'next/head'; // Components @@ -8,24 +8,44 @@ import Background from './Background'; import Footer from './Footer'; type Props = { + title?: string; + description?: string; + keywords?: string; + canonical?: string; children?: React.ReactNode; - hideMetaDescription?: boolean; }; -const Layout = ({ hideMetaDescription, children }: Props) => { +const Layout = ({ title, description, canonical, keywords, children }: Props) => { + // States + const [url, setUrl] = useState(''); + + useEffect(() => { + setUrl(window.location.href); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const defaultTitle = 'Ethereum Blockchain Explorer - EthSeer.io'; + const defaultDescription = + "Ethseer is an Ethereum Blockchain Explorer. It provides real-time data and statistics on Ethereum's latest epochs, slots, validators and staking entities."; + const defaultKeywords = 'Ethereum, Ethereum Blockchain Explorer, Ethereum Block Explorer, Search'; + return (
- Ethereum Blockchain Explorer - EthSeer.io + {title ?? defaultTitle} + + + + + + + + - {!hideMetaDescription && ( - - )} + {canonical && } - + {url && }
diff --git a/packages/client/pages/_document.tsx b/packages/client/pages/_document.tsx index a766c5ae..b4e2b161 100644 --- a/packages/client/pages/_document.tsx +++ b/packages/client/pages/_document.tsx @@ -100,16 +100,16 @@ export default function Document() { href='https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;500;600;700;800&display=swap' rel='stylesheet' /> - + + + + + + + - - - - + diff --git a/packages/client/pages/blocks.tsx b/packages/client/pages/blocks.tsx index 4d850144..9f41310a 100644 --- a/packages/client/pages/blocks.tsx +++ b/packages/client/pages/blocks.tsx @@ -1,5 +1,4 @@ import React, { useState, useEffect } from 'react'; -import Head from 'next/head'; import { useRouter } from 'next/router'; // Axios @@ -60,17 +59,12 @@ const Blocks = () => { }; return ( - - - Blocks of the Ethereum Chain - EthSeer.io - - - - - + Ethereum Blocks diff --git a/packages/client/pages/clients.tsx b/packages/client/pages/clients.tsx index ffa9c7b5..8062d7d2 100644 --- a/packages/client/pages/clients.tsx +++ b/packages/client/pages/clients.tsx @@ -1,5 +1,4 @@ import React, { useContext } from 'react'; -import Head from 'next/head'; // Contexts import ThemeModeContext from '../contexts/theme-mode/ThemeModeContext'; @@ -64,20 +63,12 @@ const Clients = () => { ]; return ( - - - Clients used to run the Ethereum Chain - EthSeer.io - - - - - + Ethereum CL Clients diff --git a/packages/client/pages/entities.tsx b/packages/client/pages/entities.tsx index ab368d55..f976842c 100644 --- a/packages/client/pages/entities.tsx +++ b/packages/client/pages/entities.tsx @@ -1,6 +1,5 @@ import React, { useContext, useEffect, useState } from 'react'; import { useRouter } from 'next/router'; -import Head from 'next/head'; // Axios import axiosClient from '../config/axios'; @@ -67,17 +66,12 @@ const Entities = () => { //OVERVIEW PAGE return ( - - - Staking Entities of the Ethereum Beacon Chain - EthSeer.io - - - - - + Ethereum Staking Entities diff --git a/packages/client/pages/epochs.tsx b/packages/client/pages/epochs.tsx index f89515e9..cb78c8cc 100644 --- a/packages/client/pages/epochs.tsx +++ b/packages/client/pages/epochs.tsx @@ -1,5 +1,4 @@ import React, { useState, useEffect } from 'react'; -import Head from 'next/head'; import { useRouter } from 'next/router'; // Axios @@ -57,17 +56,12 @@ const Epochs = () => { }; return ( - - - Epochs of the Ethereum Beacon Chain - EthSeer.io - - - - - + Ethereum Epochs diff --git a/packages/client/pages/slots.tsx b/packages/client/pages/slots.tsx index a5d8e109..e2e1b97b 100644 --- a/packages/client/pages/slots.tsx +++ b/packages/client/pages/slots.tsx @@ -1,5 +1,4 @@ import React, { useState, useEffect } from 'react'; -import Head from 'next/head'; import { useRouter } from 'next/router'; // Axios @@ -60,17 +59,12 @@ const Slots = () => { }; return ( - - - Slots of the Ethereum Beacon Chain - EthSeer.io - - - - - + Ethereum Slots diff --git a/packages/client/pages/transactions.tsx b/packages/client/pages/transactions.tsx index 4ba36486..56a73247 100644 --- a/packages/client/pages/transactions.tsx +++ b/packages/client/pages/transactions.tsx @@ -1,5 +1,4 @@ import React, { useState, useEffect } from 'react'; -import Head from 'next/head'; import { useRouter } from 'next/router'; // Axios @@ -62,17 +61,12 @@ const Transactions = () => { }; return ( - - - Transactions of the Ethereum Chain - EthSeer.io - - - - - + Ethereum Transactions diff --git a/packages/client/pages/validators.tsx b/packages/client/pages/validators.tsx index 94f0f31f..4a4eb4aa 100644 --- a/packages/client/pages/validators.tsx +++ b/packages/client/pages/validators.tsx @@ -127,17 +127,12 @@ const Validators = () => { ); return ( - - - Validators of the Ethereum Beacon Chain - EthSeer.io - - - - - + Ethereum Validators From 9bd135c45a283fa70449bda4af01b92e10a47674 Mon Sep 17 00:00:00 2001 From: Iuri Date: Mon, 5 Feb 2024 18:46:36 +0100 Subject: [PATCH 09/30] Create Filter Components; Implement First Design of Filters Slot --- .../ui/Filters/FilterOptionChipGroup.tsx | 50 +++++++++++++++++++ .../components/ui/Filters/FilterSection.tsx | 40 +++++++++++++++ .../components/ui/Filters/FiltersButton.tsx | 21 ++++++++ .../ui/Filters/FiltersContainer.tsx | 35 +++++++++++++ .../components/ui/Filters/FiltersMenu.tsx | 34 +++++++++++++ .../components/ui/Filters/FiltersSlot.tsx | 38 ++++++++++++++ packages/client/components/ui/Pagination.tsx | 9 +++- packages/client/pages/_document.tsx | 3 +- packages/client/pages/slots.tsx | 18 ++++--- 9 files changed, 239 insertions(+), 9 deletions(-) create mode 100644 packages/client/components/ui/Filters/FilterOptionChipGroup.tsx create mode 100644 packages/client/components/ui/Filters/FilterSection.tsx create mode 100644 packages/client/components/ui/Filters/FiltersButton.tsx create mode 100644 packages/client/components/ui/Filters/FiltersContainer.tsx create mode 100644 packages/client/components/ui/Filters/FiltersMenu.tsx create mode 100644 packages/client/components/ui/Filters/FiltersSlot.tsx diff --git a/packages/client/components/ui/Filters/FilterOptionChipGroup.tsx b/packages/client/components/ui/Filters/FilterOptionChipGroup.tsx new file mode 100644 index 00000000..89db1b91 --- /dev/null +++ b/packages/client/components/ui/Filters/FilterOptionChipGroup.tsx @@ -0,0 +1,50 @@ +import React, { useState } from 'react'; + +// Props +type Props = { + options: string[]; + multipleChoice?: boolean; +}; + +const FilterOptionChipGroup = ({ options, multipleChoice }: Props) => { + // States + const [selectedOptions, setSelectedOptions] = useState([]); + + const handleClick = (option: string) => { + if (!multipleChoice) { + if (selectedOptions.includes(option)) { + setSelectedOptions([]); + } else { + setSelectedOptions([option]); + } + + return; + } + + if (selectedOptions.includes(option)) { + setSelectedOptions(selectedOptions.filter(selectedOption => selectedOption !== option)); + } else { + setSelectedOptions([...selectedOptions, option]); + } + }; + + return ( +
+ {options.map(option => ( + + ))} +
+ ); +}; + +export default FilterOptionChipGroup; diff --git a/packages/client/components/ui/Filters/FilterSection.tsx b/packages/client/components/ui/Filters/FilterSection.tsx new file mode 100644 index 00000000..b7e54cc9 --- /dev/null +++ b/packages/client/components/ui/Filters/FilterSection.tsx @@ -0,0 +1,40 @@ +import React, { useState } from 'react'; +import { PlusIcon, MinusIcon } from '@heroicons/react/24/outline'; + +// Props +type Props = { + header: string; + removeSeparator?: boolean; + children: React.ReactNode; +}; + +const FilterSection = ({ header, removeSeparator, children }: Props) => { + // States + const [showSection, setShowSection] = useState(false); + + const handleClick = () => { + setShowSection(!showSection); + }; + + return ( +
+
+ + +
{children}
+
+ + {!removeSeparator &&
} +
+ ); +}; + +export default FilterSection; diff --git a/packages/client/components/ui/Filters/FiltersButton.tsx b/packages/client/components/ui/Filters/FiltersButton.tsx new file mode 100644 index 00000000..8c847a7b --- /dev/null +++ b/packages/client/components/ui/Filters/FiltersButton.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { AdjustmentsHorizontalIcon } from '@heroicons/react/24/outline'; + +// Props +type Props = { + onClick: () => void; +}; + +const FiltersButton = ({ onClick }: Props) => { + return ( + + ); +}; + +export default FiltersButton; diff --git a/packages/client/components/ui/Filters/FiltersContainer.tsx b/packages/client/components/ui/Filters/FiltersContainer.tsx new file mode 100644 index 00000000..ddb4fc4f --- /dev/null +++ b/packages/client/components/ui/Filters/FiltersContainer.tsx @@ -0,0 +1,35 @@ +import React, { useState } from 'react'; + +// Components +import FiltersButton from './FiltersButton'; +import FiltersMenu from './FiltersMenu'; + +// Props +type Props = { + children: React.ReactNode; +}; + +const FiltersContainer = ({ children }: Props) => { + // States + const [showMenu, setShowMenu] = useState(false); + + const handleClick = () => { + setShowMenu(!showMenu); + }; + + const handleClose = () => { + setShowMenu(false); + }; + + return ( +
+ + + + {children} + +
+ ); +}; + +export default FiltersContainer; diff --git a/packages/client/components/ui/Filters/FiltersMenu.tsx b/packages/client/components/ui/Filters/FiltersMenu.tsx new file mode 100644 index 00000000..a2444b83 --- /dev/null +++ b/packages/client/components/ui/Filters/FiltersMenu.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { XCircleIcon } from '@heroicons/react/24/outline'; + +// Props +type Props = { + isVisible: boolean; + children: React.ReactNode; + onClose: () => void; +}; + +const FiltersMenu = ({ isVisible, children, onClose }: Props) => { + return ( +
+
+ Filters + + +
+ +
{children}
+ +
+ + +
+
+ ); +}; + +export default FiltersMenu; diff --git a/packages/client/components/ui/Filters/FiltersSlot.tsx b/packages/client/components/ui/Filters/FiltersSlot.tsx new file mode 100644 index 00000000..0878f0bf --- /dev/null +++ b/packages/client/components/ui/Filters/FiltersSlot.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +// Components +import FiltersContainer from './FiltersContainer'; +import FilterSection from './FilterSection'; +import FilterOptionChipGroup from './FilterOptionChipGroup'; + +const FiltersSlot = () => { + return ( + + + + + + +
Filter Epoch
+
+ + +
Filter Proposer
+
+ + +
Filter Date
+
+ + +
Filter Entity
+
+ + +
Filter CL Client
+
+
+ ); +}; + +export default FiltersSlot; diff --git a/packages/client/components/ui/Pagination.tsx b/packages/client/components/ui/Pagination.tsx index 2f4da4a0..35f342da 100644 --- a/packages/client/components/ui/Pagination.tsx +++ b/packages/client/components/ui/Pagination.tsx @@ -14,11 +14,12 @@ import PaginationIconButton from './PaginationIconButton'; type Props = { currentPage: number; totalPages: number; + fullWidth?: boolean; onChangePage: (newPage: number) => void; onChangeNumRows: (numRows: number) => void; }; -const Pagination = ({ currentPage, totalPages, onChangePage, onChangeNumRows }: Props) => { +const Pagination = ({ currentPage, totalPages, fullWidth, onChangePage, onChangeNumRows }: Props) => { const next = () => { if (currentPage < totalPages) { onChangePage(currentPage + 1); @@ -32,7 +33,11 @@ const Pagination = ({ currentPage, totalPages, onChangePage, onChangeNumRows }: }; return ( -
+
Show rows: diff --git a/packages/client/pages/_document.tsx b/packages/client/pages/_document.tsx index 26900de5..a39356bc 100644 --- a/packages/client/pages/_document.tsx +++ b/packages/client/pages/_document.tsx @@ -22,12 +22,13 @@ export default function Document() { --bgStrongLightMode: #bdbdbd50; --bgDarkMode: #343434; --bgFairDarkMode: #5b5b5b65; - --bgStrongDarkMode: #30303080; + --bgStrongDarkMode: #303030; --proposedGreen: #53945a; --missedRed: #e86666; --depositedOrange: #f69a2e; --depositedBlue: #4caee5; --exitedPurple: #a966c1; + --bgFilterSectionLight: #f4f0ff; //SHADOWS: --boxShadowGreen: 0px 2px 4px 0px #3b503d inset; diff --git a/packages/client/pages/slots.tsx b/packages/client/pages/slots.tsx index a5d8e109..5b0b60a5 100644 --- a/packages/client/pages/slots.tsx +++ b/packages/client/pages/slots.tsx @@ -14,6 +14,7 @@ import PageDescription from '../components/ui/PageDescription'; // Types import { Slot } from '../types'; +import FiltersSlot from '../components/ui/Filters/FiltersSlot'; const Slots = () => { // Router @@ -79,12 +80,17 @@ const Slots = () => { {slotsCount > 0 && ( - getSlots(page, numRowsQuery)} - onChangeNumRows={numRows => getSlots(0, numRows)} - /> +
+ getSlots(page, numRowsQuery)} + onChangeNumRows={numRows => getSlots(0, numRows)} + /> + + +
)} From c98bf49de63cc1548f7e51ea7936fe0b0d5c48da Mon Sep 17 00:00:00 2001 From: Iuri Date: Mon, 5 Feb 2024 20:28:55 +0100 Subject: [PATCH 10/30] Create Filter Number Range Selector and use it --- .../ui/Filters/FilterNumericInput.tsx | 74 +++++++++++++++ .../ui/Filters/FilterNumericRangeSelector.tsx | 90 +++++++++++++++++++ .../components/ui/Filters/FilterSection.tsx | 2 +- .../components/ui/Filters/FiltersSlot.tsx | 5 +- packages/client/styles/globals.css | 17 ++++ 5 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 packages/client/components/ui/Filters/FilterNumericInput.tsx create mode 100644 packages/client/components/ui/Filters/FilterNumericRangeSelector.tsx diff --git a/packages/client/components/ui/Filters/FilterNumericInput.tsx b/packages/client/components/ui/Filters/FilterNumericInput.tsx new file mode 100644 index 00000000..02011b8c --- /dev/null +++ b/packages/client/components/ui/Filters/FilterNumericInput.tsx @@ -0,0 +1,74 @@ +import React, { useState, useEffect } from 'react'; +import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; + +// Props +type Props = { + value?: number; + onChange?: (value: number) => void; +}; + +const FilterNumericInput = ({ value, onChange }: Props) => { + // States + const [inputValue, setInputValue] = useState(0); + + useEffect(() => { + setInputValue(value || 0); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.currentTarget.blur(); + return; + } + + // Only allow digits + if (e.key.length === 1 && !/\d/.test(e.key)) { + e.preventDefault(); + } + }; + + const handleChange = (e: React.ChangeEvent) => { + const normalizedString = e.target.value.replaceAll(',', '').replaceAll('.', ''); + + setNewValue(Number(normalizedString)); + }; + + const handleLeftArrow = () => { + setNewValue(inputValue - 1); + }; + + const handleRightArrow = () => { + setNewValue(inputValue + 1); + }; + + const setNewValue = (newValue: number) => { + const validValue = getValidValue(newValue); + setInputValue(validValue); + onChange?.(validValue); + }; + + const getValidValue = (newValue: number) => { + newValue = Math.min(9999999, newValue); + newValue = Math.max(0, newValue); + return newValue; + }; + + return ( +
+ + e.target.select()} + /> + +
+ ); +}; + +export default FilterNumericInput; diff --git a/packages/client/components/ui/Filters/FilterNumericRangeSelector.tsx b/packages/client/components/ui/Filters/FilterNumericRangeSelector.tsx new file mode 100644 index 00000000..bd7b759a --- /dev/null +++ b/packages/client/components/ui/Filters/FilterNumericRangeSelector.tsx @@ -0,0 +1,90 @@ +import React, { useState, useEffect } from 'react'; +import { MinusIcon } from '@heroicons/react/24/outline'; + +// Components +import FilterNumericInput from './FilterNumericInput'; + +// Props +type Props = { + allowRangeSelection?: boolean; + onValueChange?: (value: number) => void; + onRangeChange?: (from: number, to: number) => void; +}; + +const FilterNumericRangeSelector = ({ allowRangeSelection, onValueChange, onRangeChange }: Props) => { + // States + const [checkboxName, setCheckboxName] = useState(''); + const [singleValue, setSingleValue] = useState(0); + const [lowerBound, setLowerBound] = useState(0); + const [upperBound, setUpperBound] = useState(0); + const [showRangeSelector, setShowRangeSelector] = useState(false); + + useEffect(() => { + if (!checkboxName) { + setCheckboxName(`cbxSelectRange#${Math.random()}`); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleSingleValueChange = (value: number) => { + setSingleValue(value); + onValueChange?.(value); + }; + + const handleLowerBoundChange = (value: number) => { + setLowerBound(value); + if (upperBound < value) { + setUpperBound(value); + onRangeChange?.(value, value); + } else { + onRangeChange?.(value, upperBound); + } + }; + + const handleUpperBoundChange = (value: number) => { + setUpperBound(value); + if (value < lowerBound) { + setLowerBound(value); + onRangeChange?.(value, value); + } else { + onRangeChange?.(lowerBound, value); + } + }; + + return ( +
+ {allowRangeSelection && ( +
+ setShowRangeSelector(e.target.checked)} + /> + +
+ )} + + {showRangeSelector ? ( +
+
+ From + +
+ +
+ To + +
+
+ ) : ( + + )} +
+ ); +}; + +export default FilterNumericRangeSelector; diff --git a/packages/client/components/ui/Filters/FilterSection.tsx b/packages/client/components/ui/Filters/FilterSection.tsx index b7e54cc9..f39bc5dd 100644 --- a/packages/client/components/ui/Filters/FilterSection.tsx +++ b/packages/client/components/ui/Filters/FilterSection.tsx @@ -20,7 +20,7 @@ const FilterSection = ({ header, removeSeparator, children }: Props) => {
+ ))} +
+ ); +}; + +export default FilterOptionCardGroup; diff --git a/packages/client/components/ui/Filters/FiltersSlot.tsx b/packages/client/components/ui/Filters/FiltersSlot.tsx index dfd2a011..8b62fd67 100644 --- a/packages/client/components/ui/Filters/FiltersSlot.tsx +++ b/packages/client/components/ui/Filters/FiltersSlot.tsx @@ -6,8 +6,18 @@ import FilterSection from './FilterSection'; import FilterOptionChipGroup from './FilterOptionChipGroup'; import FilterNumericRangeSelector from './FilterNumericRangeSelector'; import FilterDateRangeSelector from './FilterDateRangeSelector'; +import FilterOptionCardGroup from './FilterOptionCardGroup'; + +// Constants +import { CLIENTS } from '../../../constants'; const FiltersSlot = () => { + const clientCardItems = CLIENTS.map(client => ({ + name: client.name, + imageUrl: client.imageUrl, + imageAlt: client.imageAlt, + })); + return ( @@ -31,7 +41,7 @@ const FiltersSlot = () => { -
Filter CL Client
+
); diff --git a/packages/client/constants/index.ts b/packages/client/constants/index.ts index 091437b7..fb3dd4b7 100644 --- a/packages/client/constants/index.ts +++ b/packages/client/constants/index.ts @@ -77,7 +77,58 @@ export const POOLS = [ 'WOLVERINE.ETH', ]; -export const CLIENTS = ['LIGHTHOUSE', 'NIMBUS', 'PRYSM', 'TEKU', 'LODESTAR']; +export const CLIENTS = [ + { + name: 'Nimbus', + imageUrl: '/static/images/blocks/cubes/clients/nimbus.webp', + imageAlt: 'Nimbus', + description: + "Nimbus is a client implementation for both Ethereum's consensus layer (eth2) and execution layer (eth1) that strives to be as lightweight as possible in terms of resources used. This allows it to perform well on embedded systems, embedded devices - including Raspberry Pis and mobile devices.", + link: 'https://nimbus.team/', + imageLanguageUrl: '/static/images/blocks/cubes/languages/nim.webp', + imageLanguageAlt: 'Nimbus Programming Language', + }, + { + name: 'Teku', + imageUrl: '/static/images/blocks/cubes/clients/teku.webp', + imageAlt: 'Teku', + description: + 'Teku is an open source Ethereum consensus client (previously called an Ethereum 2.0 client) written in Java. Teku contains a full beacon node implementation and a validator client for participating in proof of stake consensus. Written in Java and maintained by the same team behind Besu, Teku is equipped to bring staking services to businesses.', + link: 'https://consensys.io/teku', + imageLanguageUrl: '/static/images/blocks/cubes/languages/java.webp', + imageLanguageAlt: 'Teku Programming Language', + }, + { + name: 'Lighthouse', + imageUrl: '/static/images/blocks/cubes/clients/lighthouse.webp', + imageAlt: 'Lighthouse', + description: + 'Lighthouse is an Ethereum consensus client that connects to other Ethereum consensus clients to form a resilient and decentralized proof-of-stake blockchain. They implement the specification as defined in the ethereum/consensus-specs repository.', + link: 'https://lighthouse.sigmaprime.io/', + imageLanguageUrl: '/static/images/blocks/cubes/languages/rust.webp', + imageLanguageAlt: 'Lighthouse Programming Language', + }, + { + name: 'Prysm', + imageUrl: '/static/images/blocks/cubes/clients/prysm.webp', + imageAlt: 'Prysm', + description: + "Prysm is an Ethereum proof-of-stake client written in Go. You can use Prysm to participate in Ethereum's decentralized economy by running a node and, if you have 32 ETH to stake, a validator client.", + link: 'https://docs.prylabs.network/docs/getting-started', + imageLanguageUrl: '/static/images/blocks/cubes/languages/go.webp', + imageLanguageAlt: 'Prysm Programming Language', + }, + { + name: 'Lodestar', + imageUrl: '/static/images/blocks/cubes/clients/lodestar.webp', + imageAlt: 'Lodestar', + description: + "Lodestar is a consensus beacon node and validator client for the Ethereum blockchain. Lodestar's tools and libraries enable Ethereum protocol development for the JavaScript ecosystem.", + link: 'https://lodestar.chainsafe.io/', + imageLanguageUrl: '/static/images/blocks/cubes/languages/javascript.webp', + imageLanguageAlt: 'Lodestar Programming Language', + }, +]; export const ADDRESS_ZERO = '0x0000000000000000000000000000000000000000000000000000000000000000'; export const ADDRESS_ZERO_SHORT = '0x0000000000000000000000000000000000000000'; diff --git a/packages/client/pages/clients.tsx b/packages/client/pages/clients.tsx index ffa9c7b5..c11a429d 100644 --- a/packages/client/pages/clients.tsx +++ b/packages/client/pages/clients.tsx @@ -10,59 +10,13 @@ import Title from '../components/ui/Title'; import PageDescription from '../components/ui/PageDescription'; import CustomImage from '../components/ui/CustomImage'; +// Constants +import { CLIENTS } from '../constants'; + const Clients = () => { // Theme Mode Context const { themeMode } = useContext(ThemeModeContext) ?? {}; - // Clients - const clients = [ - { - name: 'Nimbus', - imgUrl: '/static/images/blocks/cubes/clients/nimbus.webp', - txtAlt: 'Nimbus', - text: "Nimbus is a client implementation for both Ethereum's consensus layer (eth2) and execution layer (eth1) that strives to be as lightweight as possible in terms of resources used. This allows it to perform well on embedded systems, embedded devices - including Raspberry Pis and mobile devices.", - link: 'https://nimbus.team/', - imgLng: '/static/images/blocks/cubes/languages/nim.webp', - txtAltLng: 'Nimbus Programming Language', - }, - { - name: 'Teku', - imgUrl: '/static/images/blocks/cubes/clients/teku.webp', - txtAlt: 'Teku', - text: 'Teku is an open source Ethereum consensus client (previously called an Ethereum 2.0 client) written in Java. Teku contains a full beacon node implementation and a validator client for participating in proof of stake consensus. Written in Java and maintained by the same team behind Besu, Teku is equipped to bring staking services to businesses.', - link: 'https://consensys.io/teku', - imgLng: '/static/images/blocks/cubes/languages/java.webp', - txtAltLng: 'Teku Programming Language', - }, - { - name: 'Lighthouse', - imgUrl: '/static/images/blocks/cubes/clients/lighthouse.webp', - txtAlt: 'Lighthouse', - text: 'Lighthouse is an Ethereum consensus client that connects to other Ethereum consensus clients to form a resilient and decentralized proof-of-stake blockchain. They implement the specification as defined in the ethereum/consensus-specs repository.', - link: 'https://lighthouse.sigmaprime.io/', - imgLng: '/static/images/blocks/cubes/languages/rust.webp', - txtAltLng: 'Lighthouse Programming Language', - }, - { - name: 'Prysm', - imgUrl: '/static/images/blocks/cubes/clients/prysm.webp', - txtAlt: 'Prysm', - text: "Prysm is an Ethereum proof-of-stake client written in Go. You can use Prysm to participate in Ethereum's decentralized economy by running a node and, if you have 32 ETH to stake, a validator client.", - link: 'https://docs.prylabs.network/docs/getting-started', - imgLng: '/static/images/blocks/cubes/languages/go.webp', - txtAltLng: 'Prysm Programming Language', - }, - { - name: 'Lodestar', - imgUrl: '/static/images/blocks/cubes/clients/lodestar.webp', - txtAlt: 'Lodestar', - text: "Lodestar is a consensus beacon node and validator client for the Ethereum blockchain. Lodestar's tools and libraries enable Ethereum protocol development for the JavaScript ecosystem.", - link: 'https://lodestar.chainsafe.io/', - imgLng: '/static/images/blocks/cubes/languages/javascript.webp', - txtAltLng: 'Lodestar Programming Language', - }, - ]; - return ( @@ -86,7 +40,7 @@ const Clients = () => { {/* Client Card */}
- {clients.map((card, index) => ( + {CLIENTS.map((card, index) => (
{
- {card.name} + {card.name.toUpperCase()} Consensus Client @@ -134,12 +88,17 @@ const Clients = () => {
-

{card.text}

+

{card.description}

Programming language: - +
From 628936de54d8ae41a7604512887d8bf7d6e150e0 Mon Sep 17 00:00:00 2001 From: Iuri Date: Wed, 7 Feb 2024 18:58:31 +0100 Subject: [PATCH 14/30] Enhance CL Client Name Design --- .../client/components/layouts/EpochOverview.tsx | 10 +++++----- packages/client/components/ui/BlockImage.tsx | 13 +++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/client/components/layouts/EpochOverview.tsx b/packages/client/components/layouts/EpochOverview.tsx index 649d468b..a09b16a7 100644 --- a/packages/client/components/layouts/EpochOverview.tsx +++ b/packages/client/components/layouts/EpochOverview.tsx @@ -49,11 +49,11 @@ const EpochOverview = ({ epoch, blocks, lastEpoch, showClient }: Props) => { }; const getEntityText = (f_pool_name: string) => { - return `Entity: ${getEntityName(f_pool_name)}`; + return `Entity: ${getEntityName(f_pool_name).toUpperCase()}`; }; const getClientText = (f_client_name: string) => { - return `Client: ${getClientName(f_client_name)}`; + return `Client: ${getClientName(f_client_name).toUpperCase()}`; }; return ( @@ -80,7 +80,7 @@ const EpochOverview = ({ epoch, blocks, lastEpoch, showClient }: Props) => { {
{showClient - ? getClientText(block.f_cl_client as string) - : getEntityText(block.f_pool_name as string)} + ? getClientText(block.f_cl_client ?? 'others') + : getEntityText(block.f_pool_name ?? 'others')} Proposer: {Number(block.f_proposer_index)?.toLocaleString()} diff --git a/packages/client/components/ui/BlockImage.tsx b/packages/client/components/ui/BlockImage.tsx index 96ae324c..a7807db5 100644 --- a/packages/client/components/ui/BlockImage.tsx +++ b/packages/client/components/ui/BlockImage.tsx @@ -1,14 +1,15 @@ -import React from 'react'; - -// Components -import CustomImage from './CustomImage'; +import React, { useContext } from 'react'; // Contexts import ThemeModeContext from '../../contexts/theme-mode/ThemeModeContext'; +// Components +import CustomImage from './CustomImage'; + // Constants import { CLIENTS, POOLS } from '../../constants'; +// Props type Props = { poolName: string; clientName?: string; @@ -21,9 +22,9 @@ type Props = { const BlockImage = ({ poolName, clientName, proposed = true, width, height, showCheck, showClient }: Props) => { // Theme Mode Context - const { themeMode } = React.useContext(ThemeModeContext) ?? {}; + const { themeMode } = useContext(ThemeModeContext) ?? {}; - const clientNames = CLIENTS.map(client => client.name); + const clientNames = CLIENTS.map(client => client.name.toUpperCase()); const getUrlEntity = () => { if (poolName && POOLS.includes(poolName.toUpperCase())) { From 70145e1e5020c57d1f0c913568f9b0bff815d3b1 Mon Sep 17 00:00:00 2001 From: Iuri Date: Wed, 7 Feb 2024 21:01:27 +0100 Subject: [PATCH 15/30] Create Filter Check List; Refactor Image Helper Functions --- packages/client/components/ui/BlockGif.tsx | 18 +--- packages/client/components/ui/BlockImage.tsx | 37 ++----- packages/client/components/ui/BlockState.tsx | 20 +--- .../components/ui/Filters/FilterCheckList.tsx | 101 ++++++++++++++++++ ...ctRangeCheckbox.tsx => FilterCheckbox.tsx} | 7 +- .../ui/Filters/FilterDateRangeSelector.tsx | 6 +- .../ui/Filters/FilterNumericRangeSelector.tsx | 6 +- .../ui/Filters/FilterOptionCardGroup.tsx | 4 +- .../components/ui/Filters/FiltersMenu.tsx | 2 +- .../components/ui/Filters/FiltersSlot.tsx | 23 +++- packages/client/constants/index.ts | 25 +---- packages/client/helpers/imageUrlsHelper.ts | 46 ++++++++ packages/client/pages/clients.tsx | 16 ++- .../go.webp | Bin .../java.webp | Bin .../javascript.webp | Bin .../nim.webp | Bin .../rust.webp | Bin 18 files changed, 215 insertions(+), 96 deletions(-) create mode 100644 packages/client/components/ui/Filters/FilterCheckList.tsx rename packages/client/components/ui/Filters/{FilterSelectRangeCheckbox.tsx => FilterCheckbox.tsx} (83%) create mode 100644 packages/client/helpers/imageUrlsHelper.ts rename packages/client/public/static/images/{blocks/cubes/languages => programming_languages}/go.webp (100%) rename packages/client/public/static/images/{blocks/cubes/languages => programming_languages}/java.webp (100%) rename packages/client/public/static/images/{blocks/cubes/languages => programming_languages}/javascript.webp (100%) rename packages/client/public/static/images/{blocks/cubes/languages => programming_languages}/nim.webp (100%) rename packages/client/public/static/images/{blocks/cubes/languages => programming_languages}/rust.webp (100%) diff --git a/packages/client/components/ui/BlockGif.tsx b/packages/client/components/ui/BlockGif.tsx index 05585368..b4b6caed 100644 --- a/packages/client/components/ui/BlockGif.tsx +++ b/packages/client/components/ui/BlockGif.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; -// Constants -import { POOLS } from '../../constants'; +// Helpers +import { getImageUrlEntity } from '../../helpers/imageUrlsHelper'; type Props = { poolName: string; @@ -14,23 +14,11 @@ const BlockGif = ({ poolName, width, height }: Props) => { const [urlImage, setUrlImage] = useState(''); useEffect(() => { - setUrlImage(`url(${getUrl()})`); + setUrlImage(`url(${getImageUrlEntity()})`); // eslint-disable-next-line react-hooks/exhaustive-deps }, [poolName]); - const getUrl = () => { - if (poolName && POOLS.includes(poolName.toUpperCase())) { - return `/static/images/blocks/covers/${poolName.toLowerCase()}.webp`; - } else if (poolName && poolName.includes('lido')) { - return '/static/images/blocks/covers/lido.webp'; - } else if (poolName && poolName.includes('whale')) { - return '/static/images/blocks/covers/whale-ethereum-entity.webp'; - } else { - return '/static/images/blocks/covers/unknown-ethereum-entity.webp'; - } - }; - if (!poolName) { return null; } diff --git a/packages/client/components/ui/BlockImage.tsx b/packages/client/components/ui/BlockImage.tsx index a7807db5..ca1e6792 100644 --- a/packages/client/components/ui/BlockImage.tsx +++ b/packages/client/components/ui/BlockImage.tsx @@ -6,8 +6,13 @@ import ThemeModeContext from '../../contexts/theme-mode/ThemeModeContext'; // Components import CustomImage from './CustomImage'; -// Constants -import { CLIENTS, POOLS } from '../../constants'; +// Helpers +import { + getImageUrlEntity, + getImageAltEntity, + getImageUrlClient, + getImageAltClient, +} from '../../helpers/imageUrlsHelper'; // Props type Props = { @@ -24,33 +29,11 @@ const BlockImage = ({ poolName, clientName, proposed = true, width, height, show // Theme Mode Context const { themeMode } = useContext(ThemeModeContext) ?? {}; - const clientNames = CLIENTS.map(client => client.name.toUpperCase()); - - const getUrlEntity = () => { - if (poolName && POOLS.includes(poolName.toUpperCase())) { - return `/static/images/blocks/cubes/${poolName.toLowerCase()}.webp`; - } else if (poolName && poolName.toLowerCase().includes('lido')) { - return '/static/images/blocks/cubes/lido.webp'; - } else if (poolName && poolName.toLowerCase().includes('whale')) { - return '/static/images/blocks/cubes/whale-ethereum-entity.webp'; - } else { - return '/static/images/blocks/cubes/unknown-ethereum-entity.webp'; - } - }; - - const getUrlClient = () => { - if (clientName && clientNames.includes(clientName.toUpperCase())) { - return `/static/images/blocks/cubes/clients/${clientName.toLowerCase()}.webp`; - } else { - return '/static/images/blocks/cubes/unknown-ethereum-entity.webp'; - } - }; - return (
diff --git a/packages/client/components/ui/BlockState.tsx b/packages/client/components/ui/BlockState.tsx index 7da2c41b..9b1b7c2c 100644 --- a/packages/client/components/ui/BlockState.tsx +++ b/packages/client/components/ui/BlockState.tsx @@ -3,8 +3,8 @@ import React from 'react'; // Components import CustomImage from './CustomImage'; -// Constants -import { POOLS } from '../../constants'; +// Helpers +import { getImageUrlEntity, getImageAltEntity } from '../../helpers/imageUrlsHelper'; type Props = { poolName: string; @@ -15,23 +15,11 @@ type Props = { }; const BlockState = ({ poolName, proposed = true, width, height, showCheck }: Props) => { - const getUrl = () => { - if (poolName && POOLS.includes(poolName.toUpperCase())) { - return `/static/images/blocks/cubes/${poolName.toLowerCase()}.webp`; - } else if (poolName && poolName.toLowerCase().includes('lido')) { - return '/static/images/blocks/cubes/lido.webp'; - } else if (poolName && poolName.toLowerCase().includes('whale')) { - return '/static/images/blocks/cubes/whale-ethereum-entity.webp'; - } else { - return '/static/images/blocks/cubes/unknown-ethereum-entity.webp'; - } - }; - return (
{ + // States + const [selectedOptions, setSelectedOptions] = useState([]); + + const handleClick = (option: FilterCheckListCard) => { + if (selectedOptions.includes(option)) { + setSelectedOptions(selectedOptions.filter(selectedOption => selectedOption !== option)); + } else { + setSelectedOptions([...selectedOptions, option]); + } + }; + + const handleRemoveOption = (option: FilterCheckListCard) => { + setSelectedOptions(selectedOptions.filter(selectedOption => selectedOption !== option)); + }; + + const handleSelectAll = (value: boolean) => { + if (value) { + setSelectedOptions(options); + } else { + setSelectedOptions([]); + } + }; + + const getItemsCountText = () => { + const itemsText = selectedOptions.length > 1 ? 'items' : 'item'; + + return `${selectedOptions.length} ${itemsText} selected`; + }; + + return ( +
+
+ {selectedOptions.map(option => ( + + ))} +
+ +
+ +
+ +
+ +
+ +
+ {options.map(option => ( + + ))} +
+ +
+ +
+ {getItemsCountText()} +
+
+ ); +}; + +export default FilterCheckList; diff --git a/packages/client/components/ui/Filters/FilterSelectRangeCheckbox.tsx b/packages/client/components/ui/Filters/FilterCheckbox.tsx similarity index 83% rename from packages/client/components/ui/Filters/FilterSelectRangeCheckbox.tsx rename to packages/client/components/ui/Filters/FilterCheckbox.tsx index cd08d6dd..ab3b38e6 100644 --- a/packages/client/components/ui/Filters/FilterSelectRangeCheckbox.tsx +++ b/packages/client/components/ui/Filters/FilterCheckbox.tsx @@ -2,10 +2,11 @@ import React, { useState, useEffect } from 'react'; // Props type Props = { + label: string; onSelectRangeChange?: (value: boolean) => void; }; -const FilterSelectRangeCheckbox = ({ onSelectRangeChange }: Props) => { +const FilterCheckbox = ({ label, onSelectRangeChange }: Props) => { // States const [checkboxName, setCheckboxName] = useState(''); @@ -21,10 +22,10 @@ const FilterSelectRangeCheckbox = ({ onSelectRangeChange }: Props) => {
onSelectRangeChange?.(e.target.checked)} />
); }; -export default FilterSelectRangeCheckbox; +export default FilterCheckbox; diff --git a/packages/client/components/ui/Filters/FilterDateRangeSelector.tsx b/packages/client/components/ui/Filters/FilterDateRangeSelector.tsx index a6031466..1140f33a 100644 --- a/packages/client/components/ui/Filters/FilterDateRangeSelector.tsx +++ b/packages/client/components/ui/Filters/FilterDateRangeSelector.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { MinusIcon } from '@heroicons/react/24/outline'; // Components -import FilterSelectRangeCheckbox from './FilterSelectRangeCheckbox'; +import FilterSelectRangeCheckbox from './FilterCheckbox'; import FilterDateInput from './FilterDateInput'; // Props @@ -46,7 +46,9 @@ const FilterDateRangeSelector = ({ allowRangeSelection, onValueChange, onRangeCh return (
- {allowRangeSelection && } + {allowRangeSelection && ( + + )} {showRangeSelector ? (
diff --git a/packages/client/components/ui/Filters/FilterNumericRangeSelector.tsx b/packages/client/components/ui/Filters/FilterNumericRangeSelector.tsx index f62e2c2f..e6421839 100644 --- a/packages/client/components/ui/Filters/FilterNumericRangeSelector.tsx +++ b/packages/client/components/ui/Filters/FilterNumericRangeSelector.tsx @@ -3,7 +3,7 @@ import { MinusIcon } from '@heroicons/react/24/outline'; // Components import FilterNumericInput from './FilterNumericInput'; -import FilterSelectRangeCheckbox from './FilterSelectRangeCheckbox'; +import FilterSelectRangeCheckbox from './FilterCheckbox'; // Props type Props = { @@ -46,7 +46,9 @@ const FilterNumericRangeSelector = ({ allowRangeSelection, onValueChange, onRang return (
- {allowRangeSelection && } + {allowRangeSelection && ( + + )} {showRangeSelector ? (
diff --git a/packages/client/components/ui/Filters/FilterOptionCardGroup.tsx b/packages/client/components/ui/Filters/FilterOptionCardGroup.tsx index 06017b86..68923e7e 100644 --- a/packages/client/components/ui/Filters/FilterOptionCardGroup.tsx +++ b/packages/client/components/ui/Filters/FilterOptionCardGroup.tsx @@ -4,7 +4,7 @@ import React, { useState } from 'react'; import CustomImage from '../CustomImage'; // Types -export type FilterOptionCard = { +type FilterOptionCard = { name: string; imageUrl: string; imageAlt: string; @@ -55,7 +55,7 @@ const FilterOptionCardGroup = ({ options, multipleChoice }: Props) => { alt={option.imageAlt} width={30} height={30} - className='w-[10px] h-[10px] lg:w-[30px] lg:h-[30px]' + className='w-[20px] h-[20px] md:w-[30px] md:h-[30px]' /> {option.name.toUpperCase()} diff --git a/packages/client/components/ui/Filters/FiltersMenu.tsx b/packages/client/components/ui/Filters/FiltersMenu.tsx index ef018135..e6c36df3 100644 --- a/packages/client/components/ui/Filters/FiltersMenu.tsx +++ b/packages/client/components/ui/Filters/FiltersMenu.tsx @@ -13,7 +13,7 @@ const FiltersMenu = ({ isVisible, children, onClose }: Props) => {
Filters diff --git a/packages/client/components/ui/Filters/FiltersSlot.tsx b/packages/client/components/ui/Filters/FiltersSlot.tsx index 8b62fd67..58cff866 100644 --- a/packages/client/components/ui/Filters/FiltersSlot.tsx +++ b/packages/client/components/ui/Filters/FiltersSlot.tsx @@ -7,15 +7,30 @@ import FilterOptionChipGroup from './FilterOptionChipGroup'; import FilterNumericRangeSelector from './FilterNumericRangeSelector'; import FilterDateRangeSelector from './FilterDateRangeSelector'; import FilterOptionCardGroup from './FilterOptionCardGroup'; +import FilterCheckList from './FilterCheckList'; + +// Helpers +import { + getImageUrlEntity, + getImageAltEntity, + getImageUrlClient, + getImageAltClient, +} from '../../../helpers/imageUrlsHelper'; // Constants -import { CLIENTS } from '../../../constants'; +import { POOLS, CLIENTS } from '../../../constants'; const FiltersSlot = () => { + const poolsCardItems = POOLS.map(pool => ({ + name: pool, + imageUrl: getImageUrlEntity(pool), + imageAlt: getImageAltEntity(pool), + })); + const clientCardItems = CLIENTS.map(client => ({ name: client.name, - imageUrl: client.imageUrl, - imageAlt: client.imageAlt, + imageUrl: getImageUrlClient(client.name), + imageAlt: getImageAltClient(client.name), })); return ( @@ -37,7 +52,7 @@ const FiltersSlot = () => { -
Filter Entity
+
diff --git a/packages/client/constants/index.ts b/packages/client/constants/index.ts index fb3dd4b7..20faff92 100644 --- a/packages/client/constants/index.ts +++ b/packages/client/constants/index.ts @@ -80,53 +80,38 @@ export const POOLS = [ export const CLIENTS = [ { name: 'Nimbus', - imageUrl: '/static/images/blocks/cubes/clients/nimbus.webp', - imageAlt: 'Nimbus', description: "Nimbus is a client implementation for both Ethereum's consensus layer (eth2) and execution layer (eth1) that strives to be as lightweight as possible in terms of resources used. This allows it to perform well on embedded systems, embedded devices - including Raspberry Pis and mobile devices.", link: 'https://nimbus.team/', - imageLanguageUrl: '/static/images/blocks/cubes/languages/nim.webp', - imageLanguageAlt: 'Nimbus Programming Language', + language: 'nim', }, { name: 'Teku', - imageUrl: '/static/images/blocks/cubes/clients/teku.webp', - imageAlt: 'Teku', description: 'Teku is an open source Ethereum consensus client (previously called an Ethereum 2.0 client) written in Java. Teku contains a full beacon node implementation and a validator client for participating in proof of stake consensus. Written in Java and maintained by the same team behind Besu, Teku is equipped to bring staking services to businesses.', link: 'https://consensys.io/teku', - imageLanguageUrl: '/static/images/blocks/cubes/languages/java.webp', - imageLanguageAlt: 'Teku Programming Language', + language: 'java', }, { name: 'Lighthouse', - imageUrl: '/static/images/blocks/cubes/clients/lighthouse.webp', - imageAlt: 'Lighthouse', description: 'Lighthouse is an Ethereum consensus client that connects to other Ethereum consensus clients to form a resilient and decentralized proof-of-stake blockchain. They implement the specification as defined in the ethereum/consensus-specs repository.', link: 'https://lighthouse.sigmaprime.io/', - imageLanguageUrl: '/static/images/blocks/cubes/languages/rust.webp', - imageLanguageAlt: 'Lighthouse Programming Language', + language: 'rust', }, { name: 'Prysm', - imageUrl: '/static/images/blocks/cubes/clients/prysm.webp', - imageAlt: 'Prysm', description: "Prysm is an Ethereum proof-of-stake client written in Go. You can use Prysm to participate in Ethereum's decentralized economy by running a node and, if you have 32 ETH to stake, a validator client.", link: 'https://docs.prylabs.network/docs/getting-started', - imageLanguageUrl: '/static/images/blocks/cubes/languages/go.webp', - imageLanguageAlt: 'Prysm Programming Language', + language: 'go', }, { name: 'Lodestar', - imageUrl: '/static/images/blocks/cubes/clients/lodestar.webp', - imageAlt: 'Lodestar', description: "Lodestar is a consensus beacon node and validator client for the Ethereum blockchain. Lodestar's tools and libraries enable Ethereum protocol development for the JavaScript ecosystem.", link: 'https://lodestar.chainsafe.io/', - imageLanguageUrl: '/static/images/blocks/cubes/languages/javascript.webp', - imageLanguageAlt: 'Lodestar Programming Language', + language: 'javascript', }, ]; diff --git a/packages/client/helpers/imageUrlsHelper.ts b/packages/client/helpers/imageUrlsHelper.ts new file mode 100644 index 00000000..f775e688 --- /dev/null +++ b/packages/client/helpers/imageUrlsHelper.ts @@ -0,0 +1,46 @@ +// Constants +import { POOLS, CLIENTS } from '../constants'; + +export const getImageUrlEntity = (poolName?: string) => { + if (poolName && POOLS.includes(poolName.toUpperCase())) { + return `/static/images/blocks/cubes/${poolName.toLowerCase()}.webp`; + } else if (poolName && poolName.toLowerCase().includes('lido')) { + return '/static/images/blocks/cubes/lido.webp'; + } else if (poolName && poolName.toLowerCase().includes('whale')) { + return '/static/images/blocks/cubes/whale-ethereum-entity.webp'; + } else { + return '/static/images/blocks/cubes/unknown-ethereum-entity.webp'; + } +}; + +export const getImageAltEntity = (poolName?: string) => { + if (poolName && POOLS.includes(poolName.toUpperCase())) { + return `${poolName} entity logo`; + } else { + return 'others entity logo'; + } +}; + +export const getImageUrlClient = (clientName?: string) => { + if (clientName && CLIENTS.find(client => client.name.toUpperCase() === clientName.toUpperCase())) { + return `/static/images/blocks/cubes/clients/${clientName.toLowerCase()}.webp`; + } else { + return '/static/images/blocks/cubes/unknown-ethereum-entity.webp'; + } +}; + +export const getImageAltClient = (clientName?: string) => { + if (clientName && CLIENTS.find(client => client.name.toUpperCase() === clientName.toUpperCase())) { + return `${clientName} client logo`; + } else { + return 'others client logo'; + } +}; + +export const getImageUrlLanguage = (language: string) => { + return `/static/images/programming_languages/${language.toLowerCase()}.webp`; +}; + +export const getImageAltLanguage = (language: string) => { + return `${language} programming language logo`; +}; diff --git a/packages/client/pages/clients.tsx b/packages/client/pages/clients.tsx index c11a429d..dc71fef1 100644 --- a/packages/client/pages/clients.tsx +++ b/packages/client/pages/clients.tsx @@ -10,6 +10,14 @@ import Title from '../components/ui/Title'; import PageDescription from '../components/ui/PageDescription'; import CustomImage from '../components/ui/CustomImage'; +// Helpers +import { + getImageUrlClient, + getImageAltClient, + getImageUrlLanguage, + getImageAltLanguage, +} from '../helpers/imageUrlsHelper'; + // Constants import { CLIENTS } from '../constants'; @@ -51,8 +59,8 @@ const Clients = () => {
{ Programming language: diff --git a/packages/client/public/static/images/blocks/cubes/languages/go.webp b/packages/client/public/static/images/programming_languages/go.webp similarity index 100% rename from packages/client/public/static/images/blocks/cubes/languages/go.webp rename to packages/client/public/static/images/programming_languages/go.webp diff --git a/packages/client/public/static/images/blocks/cubes/languages/java.webp b/packages/client/public/static/images/programming_languages/java.webp similarity index 100% rename from packages/client/public/static/images/blocks/cubes/languages/java.webp rename to packages/client/public/static/images/programming_languages/java.webp diff --git a/packages/client/public/static/images/blocks/cubes/languages/javascript.webp b/packages/client/public/static/images/programming_languages/javascript.webp similarity index 100% rename from packages/client/public/static/images/blocks/cubes/languages/javascript.webp rename to packages/client/public/static/images/programming_languages/javascript.webp diff --git a/packages/client/public/static/images/blocks/cubes/languages/nim.webp b/packages/client/public/static/images/programming_languages/nim.webp similarity index 100% rename from packages/client/public/static/images/blocks/cubes/languages/nim.webp rename to packages/client/public/static/images/programming_languages/nim.webp diff --git a/packages/client/public/static/images/blocks/cubes/languages/rust.webp b/packages/client/public/static/images/programming_languages/rust.webp similarity index 100% rename from packages/client/public/static/images/blocks/cubes/languages/rust.webp rename to packages/client/public/static/images/programming_languages/rust.webp From a93a6701123747582ee8d0e748446aa6759e4c38 Mon Sep 17 00:00:00 2001 From: Iuri Date: Thu, 8 Feb 2024 18:37:16 +0100 Subject: [PATCH 16/30] Remove Block Gif --- packages/client/components/ui/BlockGif.tsx | 42 ---------------------- 1 file changed, 42 deletions(-) delete mode 100644 packages/client/components/ui/BlockGif.tsx diff --git a/packages/client/components/ui/BlockGif.tsx b/packages/client/components/ui/BlockGif.tsx deleted file mode 100644 index b4b6caed..00000000 --- a/packages/client/components/ui/BlockGif.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React, { useState, useEffect } from 'react'; - -// Helpers -import { getImageUrlEntity } from '../../helpers/imageUrlsHelper'; - -type Props = { - poolName: string; - width: number; - height: number; -}; - -const BlockGif = ({ poolName, width, height }: Props) => { - // States - const [urlImage, setUrlImage] = useState(''); - - useEffect(() => { - setUrlImage(`url(${getImageUrlEntity()})`); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [poolName]); - - if (!poolName) { - return null; - } - - return ( -
-
-
-
-
-
-
-
-
-
-
-
- ); -}; - -export default BlockGif; From fe5e8777dbef42a601a760d57900e9ab987f9f10 Mon Sep 17 00:00:00 2001 From: Iuri Date: Thu, 8 Feb 2024 19:34:02 +0100 Subject: [PATCH 17/30] Add Readonly Attribute to Checkbox Input --- packages/client/components/ui/Filters/FilterCheckList.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/client/components/ui/Filters/FilterCheckList.tsx b/packages/client/components/ui/Filters/FilterCheckList.tsx index 2c46d6ec..a0b9e8fc 100644 --- a/packages/client/components/ui/Filters/FilterCheckList.tsx +++ b/packages/client/components/ui/Filters/FilterCheckList.tsx @@ -74,7 +74,12 @@ const FilterCheckList = ({ options }: Props) => {
{options.map(option => ( - + +
); diff --git a/packages/client/components/ui/Filters/FiltersSlot.tsx b/packages/client/components/ui/Filters/FiltersSlot.tsx index 58cff866..d922a86e 100644 --- a/packages/client/components/ui/Filters/FiltersSlot.tsx +++ b/packages/client/components/ui/Filters/FiltersSlot.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; // Components import FiltersContainer from './FiltersContainer'; @@ -17,46 +17,149 @@ import { getImageAltClient, } from '../../../helpers/imageUrlsHelper'; +// Types +import { Range, FilterCheckListCard, FilterOptionCard, FiltersSlot as FiltersSlotType } from '../../../types'; + // Constants import { POOLS, CLIENTS } from '../../../constants'; -const FiltersSlot = () => { +// Props +type Props = { + onChangeFilters?: (filters: FiltersSlotType) => void; +}; + +const FiltersSlot = ({ onChangeFilters }: Props) => { + // Pool Cards const poolsCardItems = POOLS.map(pool => ({ name: pool, imageUrl: getImageUrlEntity(pool), imageAlt: getImageAltEntity(pool), })); + // Client Cards const clientCardItems = CLIENTS.map(client => ({ name: client.name, imageUrl: getImageUrlClient(client.name), imageAlt: getImageAltClient(client.name), })); + // States + const [status, setStatus] = useState(undefined); + const [epoch, setEpoch] = useState>(0); + const [validator, setValidator] = useState(0); + const [date, setDate] = useState>(new Date()); + const [entities, setEntities] = useState([]); + const [clients, setClients] = useState([]); + const [filterStatus, setFilterStatus] = useState(false); + const [filterEpoch, setFilterEpoch] = useState(false); + const [filterValidator, setFilterValidator] = useState(false); + const [filterDate, setFilterDate] = useState(false); + const [filterEntity, setFilterEntity] = useState(false); + const [filterClient, setFilterClient] = useState(false); + + const handleStatusChange = (status: string[]) => { + setStatus(status.length > 0 ? status[0] : undefined); + }; + + const handleEpochChange = (value: number | Range) => { + setEpoch(value); + }; + + const handleValidatorChange = (value: number | Range) => { + if (typeof value === 'number') { + setValidator(value); + } + }; + + const handleDateChange = (value: Date | Range) => { + setDate(value); + }; + + const handleApply = () => { + const filters: FiltersSlotType = {}; + + // Status + if (filterStatus && status) filters.status = status; + + // Epoch + if (filterEpoch) { + if (typeof epoch === 'number') { + filters.epoch = epoch; + } else { + filters.lowerEpoch = epoch.lower; + filters.upperEpoch = epoch.upper; + } + } + + // Validator + if (filterValidator) filters.validator = validator; + + // Date + if (filterDate) { + if (date instanceof Date) { + filters.date = date; + } else { + filters.lowerDate = date.lower; + filters.upperDate = date.upper; + } + } + + // Entities + if (filterEntity && entities.length > 0) filters.entities = entities.map(entity => entity.name); + + // Clients + if (filterClient && clients.length > 0) filters.clients = clients.map(client => client.name); + + onChangeFilters?.(filters); + }; + + const handleClearAll = () => { + setStatus(undefined); + setEpoch({ lower: 0, upper: 0 }); + setValidator(0); + setDate({ lower: new Date(), upper: new Date() }); + setEntities([]); + setClients([]); + + setTimeout(() => { + setEpoch(0); + setDate(new Date()); + }, 0); + }; + return ( - - - + + + - - + + - - + + - - + + - - + + - - + + ); diff --git a/packages/client/pages/slots.tsx b/packages/client/pages/slots.tsx index 5b0b60a5..f358ecd8 100644 --- a/packages/client/pages/slots.tsx +++ b/packages/client/pages/slots.tsx @@ -11,10 +11,10 @@ import SlotsList from '../components/layouts/Slots'; import Pagination from '../components/ui/Pagination'; import Title from '../components/ui/Title'; import PageDescription from '../components/ui/PageDescription'; +import FiltersSlot from '../components/ui/Filters/FiltersSlot'; // Types -import { Slot } from '../types'; -import FiltersSlot from '../components/ui/Filters/FiltersSlot'; +import { Slot, FiltersSlot as FiltersSlotType } from '../types'; const Slots = () => { // Router @@ -27,27 +27,32 @@ const Slots = () => { const [currentPage, setCurrentPage] = useState(0); const [loading, setLoading] = useState(true); const [numRowsQuery, setNumRowsQuery] = useState(0); + const [currentFilters, setCurrentFilters] = useState({}); useEffect(() => { if (network && slots.length === 0) { - getSlots(0, 10); + getSlots(0, 10, {}); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [network]); - const getSlots = async (page: number, limit: number) => { + const getSlots = async (page: number, limit: number, filters: FiltersSlotType) => { try { setLoading(true); setCurrentPage(page); setNumRowsQuery(limit); + setCurrentFilters(filters); + + console.log('Filters', filters); const response = await axiosClient.get('/api/slots', { params: { network, page, limit, + ...filters, }, }); @@ -85,11 +90,11 @@ const Slots = () => { currentPage={currentPage} totalPages={Math.ceil(slotsCount / numRowsQuery)} fullWidth - onChangePage={page => getSlots(page, numRowsQuery)} - onChangeNumRows={numRows => getSlots(0, numRows)} + onChangePage={page => getSlots(page, numRowsQuery, currentFilters)} + onChangeNumRows={numRows => getSlots(0, numRows, currentFilters)} /> - + getSlots(0, numRowsQuery, filters)} />
)} diff --git a/packages/client/types/index.ts b/packages/client/types/index.ts index 4fcc5398..44e8d299 100644 --- a/packages/client/types/index.ts +++ b/packages/client/types/index.ts @@ -119,3 +119,33 @@ export type Entity = { proposed_blocks_performance: number; missed_blocks_performance: number; }; + +export type Range = { + lower: T; + upper: T; +}; + +export type FilterCheckListCard = { + name: string; + imageUrl: string; + imageAlt: string; +}; + +export type FilterOptionCard = { + name: string; + imageUrl: string; + imageAlt: string; +}; + +export type FiltersSlot = { + status?: string; + epoch?: number; + lowerEpoch?: number; + upperEpoch?: number; + validator?: number; + date?: Date; + lowerDate?: Date; + upperDate?: Date; + entities?: string[]; + clients?: string[]; +}; From 55efe4e49512cedd84e053317bf299b0614f0acc Mon Sep 17 00:00:00 2001 From: Iuri Date: Sun, 18 Feb 2024 12:14:40 +0100 Subject: [PATCH 19/30] Implement Slot Filters on the Server --- .../ui/Filters/FiltersContainer.tsx | 7 +- .../components/ui/Filters/FiltersSlot.tsx | 19 ++- packages/client/pages/slots.tsx | 2 - packages/client/types/index.ts | 5 +- packages/server/controllers/slots.ts | 120 ++++++++++++++---- packages/server/routes/slots.ts | 10 ++ 6 files changed, 131 insertions(+), 32 deletions(-) diff --git a/packages/client/components/ui/Filters/FiltersContainer.tsx b/packages/client/components/ui/Filters/FiltersContainer.tsx index 99937ead..f6aa940c 100644 --- a/packages/client/components/ui/Filters/FiltersContainer.tsx +++ b/packages/client/components/ui/Filters/FiltersContainer.tsx @@ -19,6 +19,11 @@ const FiltersContainer = ({ children, onApply, onClearAll }: Props) => { setShowMenu(!showMenu); }; + const handleApply = () => { + setShowMenu(false); + onApply?.(); + }; + const handleClose = () => { setShowMenu(false); }; @@ -27,7 +32,7 @@ const FiltersContainer = ({ children, onApply, onClearAll }: Props) => {
- + {children}
diff --git a/packages/client/components/ui/Filters/FiltersSlot.tsx b/packages/client/components/ui/Filters/FiltersSlot.tsx index d922a86e..1e2cdaaa 100644 --- a/packages/client/components/ui/Filters/FiltersSlot.tsx +++ b/packages/client/components/ui/Filters/FiltersSlot.tsx @@ -97,10 +97,23 @@ const FiltersSlot = ({ onChangeFilters }: Props) => { // Date if (filterDate) { if (date instanceof Date) { - filters.date = date; + const minDate = new Date(date); + minDate.setHours(0, 0, 0, 0); + + const maxDate = new Date(date); + maxDate.setHours(23, 59, 59, 999); + + filters.lowerDate = minDate.toISOString(); + filters.upperDate = maxDate.toISOString(); } else { - filters.lowerDate = date.lower; - filters.upperDate = date.upper; + const minDate = new Date(date.lower); + minDate.setHours(0, 0, 0, 0); + + const maxDate = new Date(date.upper); + maxDate.setHours(23, 59, 59, 999); + + filters.lowerDate = minDate.toISOString(); + filters.upperDate = maxDate.toISOString(); } } diff --git a/packages/client/pages/slots.tsx b/packages/client/pages/slots.tsx index f358ecd8..1f037ce8 100644 --- a/packages/client/pages/slots.tsx +++ b/packages/client/pages/slots.tsx @@ -45,8 +45,6 @@ const Slots = () => { setNumRowsQuery(limit); setCurrentFilters(filters); - console.log('Filters', filters); - const response = await axiosClient.get('/api/slots', { params: { network, diff --git a/packages/client/types/index.ts b/packages/client/types/index.ts index 44e8d299..40bb1ef8 100644 --- a/packages/client/types/index.ts +++ b/packages/client/types/index.ts @@ -143,9 +143,8 @@ export type FiltersSlot = { lowerEpoch?: number; upperEpoch?: number; validator?: number; - date?: Date; - lowerDate?: Date; - upperDate?: Date; + lowerDate?: string; + upperDate?: string; entities?: string[]; clients?: string[]; }; diff --git a/packages/server/controllers/slots.ts b/packages/server/controllers/slots.ts index ff7d18a0..1423f6e6 100644 --- a/packages/server/controllers/slots.ts +++ b/packages/server/controllers/slots.ts @@ -5,44 +5,118 @@ export const getSlots = async (req: Request, res: Response) => { try { - const { network, page = 0, limit = 128 } = req.query; + const { + network, + page = 0, + limit = 128, + status, + epoch, + lowerEpoch, + upperEpoch, + validator, + lowerDate, + upperDate, + entities, + clients, + } = req.query; const pgPool = pgPools[network as string]; const skip = Number(page) * Number(limit); - const [ slotsEpoch, withdrawals, count ] = + const where: string[] = []; + + if (status && typeof status === 'string') { + if (status.toLowerCase() === 'proposed') { + where.push(`pd.f_proposed = true`); + } else if (status.toLowerCase() === 'missed') { + where.push(`pd.f_proposed = false`); + } + } + + if (epoch) { + where.push(`(pd.f_proposer_slot >= ${Number(epoch) * 32}) AND + (pd.f_proposer_slot < ${(Number(epoch) + 1) * 32})`); + } + + if (lowerEpoch) { + where.push(`pd.f_proposer_slot >= ${Number(lowerEpoch) * 32}`); + } + + if (upperEpoch) { + where.push(`pd.f_proposer_slot < ${(Number(upperEpoch) + 1) * 32}`); + } + + if (validator) { + where.push(`pd.f_val_idx = ${Number(validator)}`); + } + + if (lowerDate) { + const minTime = new Date(lowerDate as string).getTime(); + where.push(`(g.f_genesis_time + pd.f_proposer_slot * 12) * 1000 >= ${minTime}`); + } + + if (upperDate) { + const maxTime = new Date(upperDate as string).getTime(); + where.push(`(g.f_genesis_time + pd.f_proposer_slot * 12) * 1000 < ${maxTime}`); + } + + if (entities && Array.isArray(entities) && entities.length > 0) { + const entitiesArray = entities.map(x => typeof x === 'string' ? `'${x.toLowerCase()}'` : '').filter(x => x !== ''); + where.push(`pk.f_pool_name IN (${entitiesArray.join(',')})`); + } + + let joinClient = ''; + + if (network === 'mainnet' && clients && Array.isArray(clients) && clients.length > 0) { + joinClient = 'LEFT OUTER JOIN t_slot_client_guesses scg ON pd.f_proposer_slot = scg.f_slot'; + + const clientsArray = clients.map(x => typeof x === 'string' ? `'${x.toLowerCase()}'` : '').filter(x => x !== ''); + where.push(`LOWER(scg.f_best_guess_single) IN (${clientsArray.join(',')})`); + } + + const [ slots, count ] = await Promise.all([ pgPool.query(` - SELECT t_proposer_duties.*, t_eth2_pubkeys.f_pool_name - FROM t_proposer_duties - LEFT OUTER JOIN t_eth2_pubkeys ON t_proposer_duties.f_val_idx = t_eth2_pubkeys.f_val_idx - ORDER BY f_proposer_slot DESC - OFFSET ${skip} - LIMIT ${Number(limit)} - `), - pgPool.query(` - SELECT f_slot, f_amount - FROM t_withdrawals + SELECT + pd.f_proposer_slot, + pd.f_val_idx, + pd.f_proposed, + pk.f_pool_name, + COALESCE(SUM(w.f_amount), 0) AS withdrawals + FROM + t_proposer_duties pd + LEFT OUTER JOIN + t_eth2_pubkeys pk ON pd.f_val_idx = pk.f_val_idx + LEFT OUTER JOIN + t_withdrawals w ON pd.f_proposer_slot = w.f_slot + CROSS JOIN + t_genesis g + ${joinClient} + ${where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''} + GROUP BY + pd.f_proposer_slot, pd.f_val_idx, pd.f_proposed, pk.f_pool_name + ORDER BY + pd.f_proposer_slot DESC OFFSET ${skip} LIMIT ${Number(limit)} `), pgPool.query(` - SELECT COUNT(*) AS count - FROM t_proposer_duties + SELECT + COUNT(*) AS count + FROM + t_proposer_duties pd + LEFT OUTER JOIN + t_eth2_pubkeys pk ON pd.f_val_idx = pk.f_val_idx + CROSS JOIN + t_genesis g + ${joinClient} + ${where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''} `) ]); - const slots = slotsEpoch.rows.map((slot: any) => ({ - ...slot, - withdrawals: - withdrawals.rows - .filter((withdrawal: any) => withdrawal.f_slot === slot.f_proposer_slot) - .reduce((acc: number, withdrawal: any) => acc + Number(withdrawal.f_amount), 0), - })); - res.json({ - slots, + slots: slots.rows, totalCount: Number(count.rows[0].count), }); diff --git a/packages/server/routes/slots.ts b/packages/server/routes/slots.ts index e94eff7c..8f29d998 100644 --- a/packages/server/routes/slots.ts +++ b/packages/server/routes/slots.ts @@ -25,6 +25,16 @@ router.get('/stats', [ router.get('/', [ query('network').not().isEmpty(), query('network').custom(existsNetwork), + query('page').optional().isInt({ min: 0, max: 2147483647 }), + query('limit').optional().isInt({ min: 0, max: 100 }), + query('epoch').optional().isInt({ min: 0, max: 2147483647 }), + query('lowerEpoch').optional().isInt({ min: 0, max: 2147483647 }), + query('upperEpoch').optional().isInt({ min: 0, max: 2147483647 }), + query('validator').optional().isInt({ min: 0, max: 2147483647 }), + query('lowerDate').optional().isISO8601(), + query('upperDate').optional().isISO8601(), + query('entities').optional().isArray(), + query('clients').optional().isArray(), checkFields, ], getSlots); From 2351cc5f055b6f2cc21f927da1daf01b980e9846 Mon Sep 17 00:00:00 2001 From: Iuri Date: Mon, 19 Feb 2024 19:23:59 +0100 Subject: [PATCH 20/30] Implement Slot Orphan Status Filter --- .../components/ui/Filters/FiltersMenu.tsx | 2 +- packages/client/pages/slots.tsx | 8 +++--- packages/server/controllers/slots.ts | 26 ++++++++++++------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/packages/client/components/ui/Filters/FiltersMenu.tsx b/packages/client/components/ui/Filters/FiltersMenu.tsx index 08a5c033..31e49ea5 100644 --- a/packages/client/components/ui/Filters/FiltersMenu.tsx +++ b/packages/client/components/ui/Filters/FiltersMenu.tsx @@ -13,7 +13,7 @@ type Props = { const FiltersMenu = ({ isVisible, children, onApply, onClearAll, onClose }: Props) => { return (
diff --git a/packages/client/pages/slots.tsx b/packages/client/pages/slots.tsx index 1f037ce8..025f9698 100644 --- a/packages/client/pages/slots.tsx +++ b/packages/client/pages/slots.tsx @@ -82,8 +82,8 @@ const Slots = () => { one validator can propose a block, and the other validators need to attest on the canonical chain. - {slotsCount > 0 && ( -
+
+ {slotsCount > 0 && ( { onChangePage={page => getSlots(page, numRowsQuery, currentFilters)} onChangeNumRows={numRows => getSlots(0, numRows, currentFilters)} /> + )} +
getSlots(0, numRowsQuery, filters)} />
- )} +
diff --git a/packages/server/controllers/slots.ts b/packages/server/controllers/slots.ts index 1423f6e6..f72e8a08 100644 --- a/packages/server/controllers/slots.ts +++ b/packages/server/controllers/slots.ts @@ -28,42 +28,44 @@ export const getSlots = async (req: Request, res: Response) => { if (status && typeof status === 'string') { if (status.toLowerCase() === 'proposed') { - where.push(`pd.f_proposed = true`); + where.push('(pd.f_proposed = true) AND (o.f_slot IS NULL)'); } else if (status.toLowerCase() === 'missed') { - where.push(`pd.f_proposed = false`); + where.push('(pd.f_proposed = false) AND (o.f_slot IS NULL)'); + } else if (status.toLowerCase() === 'orphan') { + where.push('(o.f_slot IS NOT NULL)'); } } if (epoch) { where.push(`(pd.f_proposer_slot >= ${Number(epoch) * 32}) AND - (pd.f_proposer_slot < ${(Number(epoch) + 1) * 32})`); + (pd.f_proposer_slot < ${(Number(epoch) + 1) * 32})`); } if (lowerEpoch) { - where.push(`pd.f_proposer_slot >= ${Number(lowerEpoch) * 32}`); + where.push(`(pd.f_proposer_slot >= ${Number(lowerEpoch) * 32})`); } if (upperEpoch) { - where.push(`pd.f_proposer_slot < ${(Number(upperEpoch) + 1) * 32}`); + where.push(`(pd.f_proposer_slot < ${(Number(upperEpoch) + 1) * 32})`); } if (validator) { - where.push(`pd.f_val_idx = ${Number(validator)}`); + where.push(`(pd.f_val_idx = ${Number(validator)})`); } if (lowerDate) { const minTime = new Date(lowerDate as string).getTime(); - where.push(`(g.f_genesis_time + pd.f_proposer_slot * 12) * 1000 >= ${minTime}`); + where.push(`((g.f_genesis_time + pd.f_proposer_slot * 12) * 1000 >= ${minTime})`); } if (upperDate) { const maxTime = new Date(upperDate as string).getTime(); - where.push(`(g.f_genesis_time + pd.f_proposer_slot * 12) * 1000 < ${maxTime}`); + where.push(`((g.f_genesis_time + pd.f_proposer_slot * 12) * 1000 < ${maxTime})`); } if (entities && Array.isArray(entities) && entities.length > 0) { const entitiesArray = entities.map(x => typeof x === 'string' ? `'${x.toLowerCase()}'` : '').filter(x => x !== ''); - where.push(`pk.f_pool_name IN (${entitiesArray.join(',')})`); + where.push(`(pk.f_pool_name IN (${entitiesArray.join(',')}))`); } let joinClient = ''; @@ -72,7 +74,7 @@ export const getSlots = async (req: Request, res: Response) => { joinClient = 'LEFT OUTER JOIN t_slot_client_guesses scg ON pd.f_proposer_slot = scg.f_slot'; const clientsArray = clients.map(x => typeof x === 'string' ? `'${x.toLowerCase()}'` : '').filter(x => x !== ''); - where.push(`LOWER(scg.f_best_guess_single) IN (${clientsArray.join(',')})`); + where.push(`(LOWER(scg.f_best_guess_single) IN (${clientsArray.join(',')}))`); } const [ slots, count ] = @@ -90,6 +92,8 @@ export const getSlots = async (req: Request, res: Response) => { t_eth2_pubkeys pk ON pd.f_val_idx = pk.f_val_idx LEFT OUTER JOIN t_withdrawals w ON pd.f_proposer_slot = w.f_slot + LEFT OUTER JOIN + t_orphans o ON pd.f_proposer_slot = o.f_slot CROSS JOIN t_genesis g ${joinClient} @@ -108,6 +112,8 @@ export const getSlots = async (req: Request, res: Response) => { t_proposer_duties pd LEFT OUTER JOIN t_eth2_pubkeys pk ON pd.f_val_idx = pk.f_val_idx + LEFT OUTER JOIN + t_orphans o ON pd.f_proposer_slot = o.f_slot CROSS JOIN t_genesis g ${joinClient} From 46eb41541ec4ec454e0bdc449f150cde8ff6f274 Mon Sep 17 00:00:00 2001 From: Iuri Date: Wed, 21 Feb 2024 20:09:26 +0100 Subject: [PATCH 21/30] Implement Design for Small Screens --- .../components/ui/Filters/FiltersButton.tsx | 2 +- .../components/ui/Filters/FiltersMenu.tsx | 2 +- packages/client/components/ui/Pagination.tsx | 50 +++++++++---------- .../components/ui/PaginationIconButton.tsx | 3 +- packages/client/pages/slots.tsx | 4 +- packages/client/tailwind.config.js | 3 ++ 6 files changed, 33 insertions(+), 31 deletions(-) diff --git a/packages/client/components/ui/Filters/FiltersButton.tsx b/packages/client/components/ui/Filters/FiltersButton.tsx index 8c847a7b..a39317e0 100644 --- a/packages/client/components/ui/Filters/FiltersButton.tsx +++ b/packages/client/components/ui/Filters/FiltersButton.tsx @@ -12,7 +12,7 @@ const FiltersButton = ({ onClick }: Props) => { className='flex items-center gap-x-2 bg-[var(--white)] dark:bg-[var(--bgDarkMode)] hover:bg-[var(--lightGray)] hover:dark:bg-[var(--lightGray)] border-2 border-[var(--black)] dark:border-[var(--white)] hover:border-[var(--white)] px-2 py-1 rounded-lg font-normal text-[14px] md:text-[16px] text-[var(--black)] dark:text-[var(--white)] hover:text-[var(--white)]' onClick={onClick} > - Filters + Filters ); diff --git a/packages/client/components/ui/Filters/FiltersMenu.tsx b/packages/client/components/ui/Filters/FiltersMenu.tsx index 31e49ea5..b59fd632 100644 --- a/packages/client/components/ui/Filters/FiltersMenu.tsx +++ b/packages/client/components/ui/Filters/FiltersMenu.tsx @@ -15,7 +15,7 @@ const FiltersMenu = ({ isVisible, children, onApply, onClearAll, onClose }: Prop
Filters diff --git a/packages/client/components/ui/Pagination.tsx b/packages/client/components/ui/Pagination.tsx index 35f342da..5695e48b 100644 --- a/packages/client/components/ui/Pagination.tsx +++ b/packages/client/components/ui/Pagination.tsx @@ -34,40 +34,23 @@ const Pagination = ({ currentPage, totalPages, fullWidth, onChangePage, onChange return (
-
- - Show rows: - - -
- - -
-
+
onChangePage(0)} disabled={currentPage === 0}> - + - +
- - Page{' '} + + Page{' '} {(currentPage + 1).toLocaleString()} {' '} @@ -79,17 +62,34 @@ const Pagination = ({ currentPage, totalPages, fullWidth, onChangePage, onChange
- + onChangePage(totalPages - 1)} disabled={currentPage === totalPages - 1} > - +
+
+ + Show rows: + + +
+ + +
); }; diff --git a/packages/client/components/ui/PaginationIconButton.tsx b/packages/client/components/ui/PaginationIconButton.tsx index b1e39ae5..6e38c8fd 100644 --- a/packages/client/components/ui/PaginationIconButton.tsx +++ b/packages/client/components/ui/PaginationIconButton.tsx @@ -11,11 +11,10 @@ type Props = { const PaginationIconButton = ({ disabled, children, onClick }: Props) => { return ( {children} diff --git a/packages/client/pages/slots.tsx b/packages/client/pages/slots.tsx index 025f9698..a7dbe016 100644 --- a/packages/client/pages/slots.tsx +++ b/packages/client/pages/slots.tsx @@ -82,7 +82,7 @@ const Slots = () => { one validator can propose a block, and the other validators need to attest on the canonical chain. -
+
{slotsCount > 0 && ( { /> )} -
+
getSlots(0, numRowsQuery, filters)} />
diff --git a/packages/client/tailwind.config.js b/packages/client/tailwind.config.js index 4c804969..eef3b67c 100644 --- a/packages/client/tailwind.config.js +++ b/packages/client/tailwind.config.js @@ -5,6 +5,9 @@ module.exports = { theme: { extend: {}, screens: { + xs: '512px', + // => @media (min-width: 512px) { ... } + sm: '640px', // => @media (min-width: 640px) { ... } From 429e1c08a603ad8c666c4063e1cd7e4190d8f0ce Mon Sep 17 00:00:00 2001 From: Marina Terentii <125751323+Artentii@users.noreply.github.com> Date: Thu, 22 Feb 2024 09:02:21 +0100 Subject: [PATCH 22/30] Filters styling adjustments --- .../components/ui/Filters/FilterOptionCardGroup.tsx | 2 +- .../components/ui/Filters/FilterOptionChipGroup.tsx | 4 ++-- .../client/components/ui/Filters/FilterSection.tsx | 2 +- .../client/components/ui/Filters/FiltersButton.tsx | 4 ++-- .../client/components/ui/Filters/FiltersMenu.tsx | 12 +++++++++--- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/client/components/ui/Filters/FilterOptionCardGroup.tsx b/packages/client/components/ui/Filters/FilterOptionCardGroup.tsx index b051ea6b..181898d0 100644 --- a/packages/client/components/ui/Filters/FilterOptionCardGroup.tsx +++ b/packages/client/components/ui/Filters/FilterOptionCardGroup.tsx @@ -51,7 +51,7 @@ const FilterOptionCardGroup = ({ selectedOptions, options, multipleChoice, onSel {options.map(option => ( ); diff --git a/packages/client/components/ui/Filters/FiltersMenu.tsx b/packages/client/components/ui/Filters/FiltersMenu.tsx index b59fd632..3b654c49 100644 --- a/packages/client/components/ui/Filters/FiltersMenu.tsx +++ b/packages/client/components/ui/Filters/FiltersMenu.tsx @@ -18,7 +18,7 @@ const FiltersMenu = ({ isVisible, children, onApply, onClearAll, onClose }: Prop } flex-col bg-[var(--white)] dark:bg-[var(--bgDarkMode)] border-2 border-[var(--black)] dark:border-[var(--white)] rounded-lg p-4 text-[var(--black)] dark:text-[var(--white)] text-[14px] md:text-[16px] gap-y-4 w-[91.667vw] sm:w-[370px]`} >
- Filters + Filters
@@ -26,10 +26,16 @@ const FiltersMenu = ({ isVisible, children, onApply, onClearAll, onClose }: Prop
{children}
- -
From 2271113f7d0fc5a167ea055fb092306c458c4a47 Mon Sep 17 00:00:00 2001 From: Marina Terentii <125751323+Artentii@users.noreply.github.com> Date: Thu, 22 Feb 2024 09:06:45 +0100 Subject: [PATCH 23/30] Changed border pagination --- packages/client/components/ui/Pagination.tsx | 4 ++-- packages/client/components/ui/PaginationIconButton.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/client/components/ui/Pagination.tsx b/packages/client/components/ui/Pagination.tsx index 5695e48b..0c79d1b8 100644 --- a/packages/client/components/ui/Pagination.tsx +++ b/packages/client/components/ui/Pagination.tsx @@ -73,7 +73,7 @@ const Pagination = ({ currentPage, totalPages, fullWidth, onChangePage, onChange
-
+
Show rows: @@ -81,7 +81,7 @@ const Pagination = ({ currentPage, totalPages, fullWidth, onChangePage, onChange