From a8598d6595d103bdc69581ad77caa1db40da8e4a Mon Sep 17 00:00:00 2001 From: Kevin Mehrabi <4044328+KevinMehrabi@users.noreply.github.com> Date: Tue, 28 Jul 2026 18:18:38 -0400 Subject: [PATCH 1/2] Add fixture-driven dashboard --- frontend/package-lock.json | 14 + frontend/package.json | 1 + frontend/public/assets/USDtz.svg | 22 + frontend/public/assets/XTZ.svg | 10 + frontend/public/assets/fXTZ.svg | 27 + frontend/public/assets/fstXTZ.svg | 39 + frontend/public/assets/ftzBTC.svg | 27 + frontend/public/assets/fusdt.svg | 27 + frontend/public/assets/fusdtz.svg | 29 + frontend/public/assets/guilloche-source.svg | 1 + frontend/public/assets/stacy_logo.png | Bin 0 -> 32941 bytes frontend/public/assets/tezHeader.svg | 7 + frontend/public/assets/tezfin-logo.svg | 9 + frontend/public/assets/tzBTC.svg | 20 + frontend/public/assets/usdt.svg | 3 + frontend/src/app/App.test.tsx | 97 +- frontend/src/app/App.tsx | 854 +++++++- frontend/src/app/fixtures.ts | 210 ++ frontend/src/app/types.ts | 71 + frontend/src/styles/global.css | 2115 ++++++++++++++++++- 20 files changed, 3500 insertions(+), 83 deletions(-) create mode 100644 frontend/public/assets/USDtz.svg create mode 100644 frontend/public/assets/XTZ.svg create mode 100644 frontend/public/assets/fXTZ.svg create mode 100644 frontend/public/assets/fstXTZ.svg create mode 100644 frontend/public/assets/ftzBTC.svg create mode 100644 frontend/public/assets/fusdt.svg create mode 100644 frontend/public/assets/fusdtz.svg create mode 100644 frontend/public/assets/guilloche-source.svg create mode 100644 frontend/public/assets/stacy_logo.png create mode 100644 frontend/public/assets/tezHeader.svg create mode 100644 frontend/public/assets/tezfin-logo.svg create mode 100644 frontend/public/assets/tzBTC.svg create mode 100644 frontend/public/assets/usdt.svg create mode 100644 frontend/src/app/fixtures.ts create mode 100644 frontend/src/app/types.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bb70dd73..f2dd4d26 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "@fontsource-variable/fraunces": "5.3.0", "@fontsource/instrument-sans": "5.3.0", "@fontsource/spline-sans-mono": "5.3.0", + "@phosphor-icons/react": "2.1.10", "react": "19.2.0", "react-dom": "19.2.0" }, @@ -1067,6 +1068,19 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@phosphor-icons/react": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz", + "integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">= 16.8", + "react-dom": ">= 16.8" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.38", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8365892f..dfd8b9d3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "@fontsource-variable/fraunces": "5.3.0", "@fontsource/instrument-sans": "5.3.0", "@fontsource/spline-sans-mono": "5.3.0", + "@phosphor-icons/react": "2.1.10", "react": "19.2.0", "react-dom": "19.2.0" }, diff --git a/frontend/public/assets/USDtz.svg b/frontend/public/assets/USDtz.svg new file mode 100644 index 00000000..359ecc7c --- /dev/null +++ b/frontend/public/assets/USDtz.svg @@ -0,0 +1,22 @@ + + + Artboard + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/assets/XTZ.svg b/frontend/public/assets/XTZ.svg new file mode 100644 index 00000000..3af971a9 --- /dev/null +++ b/frontend/public/assets/XTZ.svg @@ -0,0 +1,10 @@ + + + XTZ.e6d31ff6a59ad3bc98d68fe7c064a5eb + + + + + + + \ No newline at end of file diff --git a/frontend/public/assets/fXTZ.svg b/frontend/public/assets/fXTZ.svg new file mode 100644 index 00000000..d306e580 --- /dev/null +++ b/frontend/public/assets/fXTZ.svg @@ -0,0 +1,27 @@ + + + ꜰXTZ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/assets/fstXTZ.svg b/frontend/public/assets/fstXTZ.svg new file mode 100644 index 00000000..f5c67b0c --- /dev/null +++ b/frontend/public/assets/fstXTZ.svg @@ -0,0 +1,39 @@ + + + stXTZ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/assets/ftzBTC.svg b/frontend/public/assets/ftzBTC.svg new file mode 100644 index 00000000..9d37e781 --- /dev/null +++ b/frontend/public/assets/ftzBTC.svg @@ -0,0 +1,27 @@ + + + ꜰtzBTC + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/assets/fusdt.svg b/frontend/public/assets/fusdt.svg new file mode 100644 index 00000000..fda925ba --- /dev/null +++ b/frontend/public/assets/fusdt.svg @@ -0,0 +1,27 @@ + + + ꜰUSDt + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/assets/fusdtz.svg b/frontend/public/assets/fusdtz.svg new file mode 100644 index 00000000..60df3ceb --- /dev/null +++ b/frontend/public/assets/fusdtz.svg @@ -0,0 +1,29 @@ + + + ꜰUSDtz + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/assets/guilloche-source.svg b/frontend/public/assets/guilloche-source.svg new file mode 100644 index 00000000..f1e747d0 --- /dev/null +++ b/frontend/public/assets/guilloche-source.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/assets/stacy_logo.png b/frontend/public/assets/stacy_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..114e66026ab2feb13021ab4ddad7deeacbe9f1c2 GIT binary patch literal 32941 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

)Q9<@%HHe(F?<~t4(c9!)Nczz| zbrpNQO9MbV)|Im&W_dk7dGu8W;zAKD62$pN?$Jf{_>1Kp@*}DxRF7Ouy98Std!ZB$ zFBU_FOyEe-W0|Kf8a;raI%aiv)KQ_pYfmm1|2FzdtyqJ2qA5~Jfbcym9FeL0toVb$ z$xyljpfAI+OPmNBIEX8Ocv>J>Q^Yb|;G6I;iPyNEuaAdjLXeCBr>$|rb>9%PkeN}Qkc-B@oeJpGQNRG-hXU<#v`^A`XR|%IJ8{Q1CJr$2` z{fYfWlJ$dfri9zYWWD*@tW7H2!>nyufEc<+LY?sAWt&Grk%UHqZ^j%5hvIZ2F&Tw4Ipf3*4#Kh9;DI>wQ=%A1g}OcL zM@XN&qJRP62-=(y+*RP5Ts(ZL&@=}Cm#;x6r%DaTmoa?nUXNED8EkpskwsJAGPp-_ z-NS4|T7d9D)1u`s4d3595n@dMREe03OeEHl#E{N&dBA5UKy0E))ZvUeNd9FRt{!Yd zWJH$&uUtv+M%l^!np>(7AX2?2)%*iBv1c*z} zb;AG)k0e-XCN+rK%QYGVNWRB5mL0+bo3WEFOHG|vCpW;v{Xp_wnbbvh-bEh()c$tt zqk>VN)~sBBO6yewp^iq6SpsLrVgn%gABQq(*+73Nu7G>Ey<$S7_1BLqDtnJ1MN?U> zy0<4IfXEj*UEvRXHu9~8nSqGfj4y<5%-C4Ph{4NbidY0gAvj0_a2ueY7ySS6VU* z3v_R<@_#4_5n)8CacSzD{5C2tbsFsHRWt^o8zw$a>mSE;7e6ta6=GxD9x*=F{=GYw zpTD~&m-gdc>~65T+(T zgsv6oM|t^p#(zi<4e=)Mk4!iStA{Pxz;8@Hv5X7K!-J$M7iIA_roY!nUQJ&3_et6%Cmr>XrMBR-*{ulS6Qi%hI`W;y#fd`F87t|>*0bj`O#(T50|$8#_h%G?zmGgO^gAr zsFNhf=gi%1>!7GJey^O#=8Zh1BcR9}dQun4{P8^1l-W?k!VG$-K}0b5uVFwrQvco; zLj^q;jKcqFHq5(5K)BxLa_YW81mHgvbIatmhW~x!im4S;%zfqMJ$me>>WN+hgqf}O z84=<4T-fo|+G#C;qDGH4QXMLhNtuf&!l@F#WfwG&5F|lkq?fny@`s|53ZbS!45k;2edM$DuAF2~li)pwAAiJ^CE1h4 zb#jm>I9d0FU8<6SD%F!B0>)mY6)g3f+|kfNHdw}}xWh6*P_1#xaA9S6;0O9pgS zq%n+px(d;1N}%?oA6_x(N9Z7RV>z~`%`#;T5LuqjDAl#1==RubwOLdvT89RVbS86M zNDb8Ye11ezta8EyKQj(uh+$#Cl^i7BU^UyD?8vrr-lMv8Gkn1iFhX*%WKXBN4xmS? zprFqb4o4wEm1YmNbS0f^_f?D*4Vq1x6{zLO4rL@_eQvfr%SB2&R8L z$8J6Hy3t+)m_Yp9#t1|#Zugc$FOY*oFca@`4egvD(X^W~RreQtp z)R_O4>((qi(AznWo*I-BK$u~<&!_vueC@J-S^dJS^XEJ`bH;2@U0oZqK$I{y%>6_h zgh{pogv{b{hX@IbGvZ#g0E?!$NF{wUTu$0x)1v&HpG2^E;9JF%j^J6QoN>nq zLj@T`ny;F$1}Ty(M#0WSSk*-4mE(k-p@SeYi@PLutfi>-$#VvtgsebQg zOUK{Jx+gmK_sF#!c>xF~U)HZ*FYUWmU3Jwhix)4btF5iZ(r-)515<1x=0_S5h@qR8 zWK0YaATEUqwMZxFMrCjuK!oTF#bDzwpYJD(u#Ob5SbkU8yqHx22f=Ve5$iNU=-!lw z`(e^OucI-zRNZ$yyL2_UM{g$tlYNT30EC&Z!Ma{Vh}UM#nzeu4ym@O^u3Qz{zkiQK zvB57Mgs~HTSv*Nc%E>Bv)OF-XpDMR%xVU7ZgP8AgnXX}J%S%nnY*QWyif|MY7pQmc zM9q+;0t$LT9>A8e+5`S3h!+bU6m=2=tg>kH)wf)6(KhNG=~fB$reS#l2nz*3bb{pC zYp>n7aN(jmmM&c;w(sAL-Frl0b7C8etY#RX-c<4UX9+X{#H)DaP3j}*k^}`XK@K7< zS^b9(S+!7}03t;+0L^9v_w463r0e7Uyw5KVSs*sT+ms=y)D^D20hu+BM>iPg|osBlzR?-S`)txQKF)XKFu|0hGxH7*mIGG`Ndmq{S3T(QRH+z0C?Nuu1aTpl} z4;q4f)IXgr?gR$}^{B<|wayAuzw_jW6TZNX^NIWLw|9<+d@o(fAGLNXi3$>u%==sS ze{0p!tC#&5x3z7-Pjnqyuyc`BE{QTd3C*o|BtW?JbGjV4hJBb)BKAafz}y;znK<1W zxzx~nm*a#n>T@F-qmsFC5W2a$T;t>#5R%HLDRq7^J?eB@O9M6AF0Ft4QpG3q)i4l2 za_O3&&S#&{J)}<(KoX}uf}>u$d-o~?k^9qY54|~Q#fZywr_HIEkx#NMoB;76DteJp zEWL%vnNp3;)IscuL+pxeg&q=wn~JdtGTZVqdJptV9CEoYBe!b&!a312!4|e!qp z1HCJwI-W!iVY_C%1++7>t)X_gZRf&u>(+J9g*_WD-AtR^Cmh5qocahsGGhGr`nAhG zzjE4F=2lnv4{6qf1yLJwq7-R~@~B=h8~_R9UnWVKSW#>%LUr~B|B!W(pa47}X0fw# zX*aWqW0Oyd#U|%da#?egNq-HbXV7;szs0Ui5Bpn}+7Df^Zs}#lOnDYA?7eAMp8|vx zAxJn!a^6MfZ7W-T?uyX$%3*OpLF|Oo0lU&Qf@%( z2uFRmCql%67esCe?1&tM0D*Ngx4ce@w~awmf1A_{ z3w>S}smY?$ZW;CRl;RyNSq4CIWhPoWDw^R`jfq@4XrOh0 zt$vXS4w1v!j%@m?Ujf1ja-mhFsMId{`rNZ65_+bF<(gQb?Kh@BEZV^TMVTt~w{QfhOA&XeYN#ai`Y!mw{dQp~LkIViwX z9mn!rxsvb{!w0_})<;HFm)APJy?*;rb=Nt!t-MewU(Mg=>&YQ$px2i0m97lOe^p7neH480}2? zU}gXzl?t(|V=ETW=f`=HZRs&dBY+XKT0^K%Lv3KqvrTd6GENZ+ZUaCdX5}KTWm|#t>hBbNlEE-l*JXN-s~^EZ5DN?J z3$;zxqU8BfG3TBjP0bM*1cD0t$(&eydRl}nR&|I)Ri*_Ge)Ea76W4UPkM!{*U?zQc z!U4icGI~7Bz^YI9)Ok-WxW933>#ER{NXQj*XdqoZnd=tGngmy3g{c=QlXW2!v5M58 zVoP|lXo@sQxA{JW2Qnu>Y%<;tYncmGQ%Psg26T#^nfCxo9Egd%z;7F|xwLVy*3f5T z2Uk9M_2m1(W)W4&hn=6&@7>WK?HUjOVMEa=@p}ap??Nw`|LCIMv|SOI(_GfJ*{gXp z=p~G~B}J}dJB5BuMOvYlVkr|Xi9p&3Ln`X#C|(<`mR`QOknFMb>FGDivs;Y--^qo> zp58qE8o3CKZ;^p56^(!xM# z#ee|_I|PK3ka`I~a`Ej8x19gtC36~PH~q{$+-}i=78RW2sQD5S9>e|=SPI1q>kI_5 zu0?oiYudhz!o3kl65HE91bfPVSkz-HKv24DSlk8b;CXI|iaHj4g#vlVE4P~T_H8*3 z;TuLQHg&MwYANeD_|$XPj-T_THRo>aTI|%z*txx}LvNoSPyjJ2PmrvadWlF}`18wd zYMj%2Rp_j6izn`;2NyxoMP)R1+98*ZwT+Xnb9Ovk;)LOFwM1<0cw3r#Ef|vfdg9Gt zg=@a-?XIe)t7ek>00_Vx3~~FpT=Pm&L?2w{*bv92OTyvR&WN=(Pt$8w|Mc2PH?t_N zAwSW|c(NuBNPw`ia#t(JqYRXN@goa2v|Z6QvuSGcE3QJ9W(mUyp&K_LagVi)m2IRF zo-WA|9m{nOApqOK;9jXC^ck^9ykb9jlYxyKKu_;pJQkqd^>EWoSWlnU$$4ue0^C!E_s6+aEOdiFDbvHd@s(I z{NeWB-=S^SzEgO($PuXtUYqpjQ_V(iKu2F6C}|LxHmrv2b)_}MlT zHtma&`y$Vi1|VkRyK|ETk1lxT;1>=|Yn;^hV02_OGR0n|i9_(SiZ<&uhW<#CsK_vF z3;YcuyWzT#e~1_hrZg;a8tOh%G)wYH58z}~CkN3qkEpBji;j*a0E8Bc{MR~J`N^*< zh+3{BRHs)PqJ*Poh7UgQ^`aN1J$%F2|0B5uILC=}8A)F6)OSugfUr4T+yo;M-BcF* z{F1vn&JRtuPq#gL)5=@)m?x&~Kd^_7#xzY!B(rSkSe-Xi8z%nJ`bU5YA&FEHZvKf) z618d*g-+~^j`_XZwt-{)j5@Zp)kvQ#6Xc^h z^OSqeUvp=WYsMwl0O#oUy5*t3?1!>E6rO7qbW@=fWMlb8OZhj;cObvD&#!;Jw0P2x zZ*1GP{rdOc-)yO>+K0U~M(F^GB_5A6+)7umoI~*_w>Mb9&yemov&0>zsHt&gC#OpvjD+EO3 zULhRyvlWdJQ1l%R2gIz+uw;dVW^*C0z8|33X&8@UPzE)N-D7bR7}2ITMmR#>d1zf( zrR+4AZn+P+4?b3X)027sG$}JY720rA1AX-A^7+^`RJ39&rewkkh2E3x6jN ze}?xA-xpk$_xT$aJFj;=&);}Q#pFd(uvYxn+qVE9L8!5|9Eh5dF#uJ6(+tk@s9OAmFK$225&FK+Zv;~2~!!LWS z;qN~7rRjgdefWnY)YYXGn9~>-K2)+!dH92|^VCWkH*SkW?{J^Cz*Me*+0b3pa`8{=+|JMMv>RF%F@nP4pwXLu$^xJWSwgu_Il;ct25 z1)LKI#_!JS@^`oE=JUA3XPFocaZmjRqAB!uI4>6E01)H_vvFNbjX3kHi^br<<0L@L zdYMO~-mGU&^*T~-SwGg7m6Y|B_vo}nP{0J|us9q})gEgP7A6AEjIy@f`lBzGeeb=O zf4pA;3yZdkX~pL-L@I|}44>xsZ|0cYk;VaXkYNSm zL;_)YEiNlKRVcg6@KzjASq2R)5Qh%_SxlL_Qdk|(+B;3vy~>oJz`O*81K`49RjhW! zX|-XWx5iex-u_0RGy025#&3Le@#23n{Oh|!se$Vr(yR8Tc{pD~WSU|I)NQUeU@X@z z>&mWOyNWkFyW!mWK+PB1cWCD=Qf?d`!>UHQt>_AU?BErTC|)2U0;bnGh}1{OL9BLB z)wET7f76xXGZf{p9bC4GI(t-A9iqj*TdZ39D!?*Pu8=hJBZCzr(?QUlcpR*Oh)7tV zY-tYRa@s_&qp8)UMgNY4YEO(S^uKlY&FAknzuc{lzi)l~*4aA1bjpcQW*d7tp7I=| zhejs|4w_@`yYD^@5bA^p6B=-QiT{81=id#hf2whXC1Sq{k%8vf5>A)R;!=abkccSZ z1okchO@kefzkozRgc%PqTu1J83>A}#&J%}WT6S25BC9T(_E6Q-mpF?a!MJ|-@J=zf zc=GWN8#NKpbN;rAWq;^*6meI5H@5Wptl?hQ*ETq?=D&Jmr*mp}NP_)GOK z#c&6#Pn#XuTWOx+4o?MXVhXSO9DM|9!o)V&ADSvKCc z(ZWf{?$=-W)hh*uUMm`A9V9*%3&)ql8XU#Ko0wydd!6wp!b3v-$226wuLSl2(IL>s zIbQ&dgZ~5$oV`H2m|3$bAMATYJhSENVvK8wh+@YpG9>dq%f#bH{f9Z4zsUpm98x@? z>#=xaV*vhG5vb}x%k{Tx5<`Yh=oGWC#2;p5RYQQeSfHarSKljigbrKn*59_Yhu&N; z^KW&_m*0dXiANGesF5`oA0tML7URc{6NQC?A!h9t z!$y6jaqx%>{8lXx^mvp#fw24ec)YD@!l>5zJJwzF@v(Uj5SI>`9bdBsSO&v8_WtSL zMh~0@NQRVvVH-M&P)fH{D8*BYl})})ai8dUwEgb~-kuo_MoWWrHg7aww`yKxoaWKb z(c|$m;!&*#TS8m+>d(bm+v}}?mUfH3xk+hmZc$oWvC$`z*NfdtiV71&MS~KBg#|H> z#}jqCT^%-?qZuJg>%!sSUYMu1Oq@8rxwN#jt)!#`tEU{FtX#QhpUmLcKM + + + + + + diff --git a/frontend/public/assets/tezfin-logo.svg b/frontend/public/assets/tezfin-logo.svg new file mode 100644 index 00000000..ca2a242a --- /dev/null +++ b/frontend/public/assets/tezfin-logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/public/assets/tzBTC.svg b/frontend/public/assets/tzBTC.svg new file mode 100644 index 00000000..484f240e --- /dev/null +++ b/frontend/public/assets/tzBTC.svg @@ -0,0 +1,20 @@ + + + Artboard Copy 8 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/assets/usdt.svg b/frontend/public/assets/usdt.svg new file mode 100644 index 00000000..908769ba --- /dev/null +++ b/frontend/public/assets/usdt.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/app/App.test.tsx b/frontend/src/app/App.test.tsx index 41893d34..af335d0c 100644 --- a/frontend/src/app/App.test.tsx +++ b/frontend/src/app/App.test.tsx @@ -1,15 +1,100 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; +import { + cleanup, + fireEvent, + render, + screen, + within, +} from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { App } from "./App"; describe("App", () => { - it("renders the TezFin application frame", () => { + beforeEach(() => { + window.history.replaceState({}, "", "/?state=near-liquidation"); + }); + + afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); + }); + + it("renders the complete approved dashboard state", () => { + render(); + + expect( + screen.getByRole("link", { name: "TezFin dashboard" }), + ).toBeInTheDocument(); + expect(screen.getByRole("link", { name: "Skip to content" })).toHaveAttribute( + "href", + "#dashboard", + ); + expect( + screen.getByRole("alert"), + ).toHaveTextContent("Your position is close to liquidation"); + expect( + screen.getByRole("region", { name: "Supplying" }), + ).toBeInTheDocument(); + expect( + screen.getByRole("region", { name: "Borrowing" }), + ).toBeInTheDocument(); + expect( + screen.getByRole("region", { name: "Assets to Supply" }), + ).toBeInTheDocument(); + expect( + screen.getByRole("region", { name: "Assets to Borrow" }), + ).toBeInTheDocument(); + expect(screen.getByRole("main")).toHaveAttribute("id", "dashboard"); + }); + + it("switches between the approved account fixtures", () => { render(); - expect(screen.getByLabelText("TezFin")).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "Disconnected" })); + expect( - screen.getByRole("navigation", { name: "Primary navigation" }), + screen.getByRole("heading", { name: "Your TezFin passbook" }), ).toBeInTheDocument(); - expect(screen.getByRole("main")).toHaveAttribute("id", "main-content"); + expect( + screen.getAllByRole("button", { name: "Connect wallet" }), + ).toHaveLength(2); + expect(screen.queryByRole("alert")).not.toBeInTheDocument(); + expect(window.location.search).toContain("state=disconnected"); + }); + + it("requires an explicit debt choice before opening a repayment review", () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: "Choose debt to repay" })); + + const chooser = screen.getByRole("dialog", { name: "Choose debt to repay" }); + expect(chooser).toHaveTextContent( + "TezFin will not select or batch debts automatically", + ); + + const usdTzChoice = within(chooser).getByRole("button", { + name: /USDtz.*2,000 USDtz/s, + }); + fireEvent.click(usdTzChoice); + + expect( + screen.getByRole("dialog", { name: "Repay USDtz" }), + ).toBeInTheDocument(); + expect( + screen.getByText("Offline fixture — wallet and RPC access are disabled"), + ).toBeInTheDocument(); + }); + + it("keeps wallet connection local to the offline fixture", () => { + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + window.history.replaceState({}, "", "/?state=disconnected"); + + render(); + fireEvent.click(screen.getAllByRole("button", { name: "Connect wallet" })[0]); + + expect( + screen.getByRole("status"), + ).toHaveTextContent("Wallet access is disabled in this offline design preview."); + expect(fetchMock).not.toHaveBeenCalled(); }); }); diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx index 117e35b8..fbd44e40 100644 --- a/frontend/src/app/App.tsx +++ b/frontend/src/app/App.tsx @@ -1,18 +1,856 @@ +import { + ArrowSquareOut, + CaretDown, + CaretRight, + CheckCircle, + Copy, + List, + ShieldCheck, + WarningCircle, + Wallet, + X, +} from "@phosphor-icons/react"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { calculateTotals, scenarios } from "./fixtures"; +import type { + AssetId, + MarketFixture, + OperationStep, + ScenarioId, + TransactionAction, + TransactionDraft, +} from "./types"; + +const scenarioOrder: ScenarioId[] = [ + "disconnected", + "fresh", + "supplying", + "active", + "near-liquidation", +]; + +const currency = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: 2, + maximumFractionDigits: 2, +}); + +const compactCurrency = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + notation: "compact", + maximumFractionDigits: 2, +}); + +function formatAmount(value: number): string { + return new Intl.NumberFormat("en-US", { + maximumFractionDigits: value < 1 ? 6 : value < 100 ? 4 : 2, + }).format(value); +} + +function riskBand(percent: number): "normal" | "attention" | "high" | "critical" { + if (percent >= 95) return "critical"; + if (percent >= 85) return "high"; + if (percent >= 70) return "attention"; + return "normal"; +} + +function initialScenario(): ScenarioId { + const value = new URLSearchParams(window.location.search).get("state") as ScenarioId | null; + return value && scenarioOrder.includes(value) ? value : "near-liquidation"; +} + +function operationSteps(action: TransactionAction, market: MarketFixture): OperationStep[] { + const fToken = `f${market.name}`; + const accrue: OperationStep = { + label: "Accrue interest", + detail: "Refresh all 5 configured TezFin markets", + }; + const liquidity: OperationStep = { + label: "Refresh account liquidity", + detail: "Comptroller · updateAccountLiquidityWithView", + }; + + if (action === "Supply" || action === "Repay") { + const steps: OperationStep[] = [accrue]; + + if (market.standard === "FA1.2") { + steps.push( + { label: "Reset allowance", detail: `${market.name} · approve 0` }, + { label: "Grant exact allowance", detail: `${market.name} → ${fToken}` }, + ); + } + + if (market.standard === "FA1.2 packed") { + steps.push({ + label: "Permission path blocked", + detail: "Packed FA1.2 approval requires contract verification", + }); + } + + if (market.standard === "FA2") { + steps.push({ + label: "Add temporary operator", + detail: `${market.name} → ${fToken}`, + }); + } + + steps.push({ + label: action === "Supply" ? "Supply asset" : "Repay debt", + detail: `${fToken} · ${action === "Supply" ? "mint" : "repayBorrow"}`, + }); + + if (market.standard === "FA2") { + steps.push({ + label: "Remove temporary operator", + detail: `${market.name} → ${fToken}`, + }); + } + + return steps; + } + + if (action === "Withdraw") { + return [ + liquidity, + { label: "Withdraw supplied asset", detail: `${fToken} · redeem` }, + ]; + } + + if (action === "Borrow") { + return [ + liquidity, + { label: "Borrow asset", detail: `${fToken} · borrow` }, + ]; + } + + return [ + liquidity, + { + label: action, + detail: `Comptroller · ${action === "Enable collateral" ? "enterMarkets" : "exitMarket"}`, + }, + ]; +} + +function actionSide(action: TransactionAction): "supply" | "borrow" | "collateral" { + if (action === "Supply" || action === "Withdraw") return "supply"; + if (action === "Borrow" || action === "Repay") return "borrow"; + return "collateral"; +} + +function AssetIdentity({ market, fToken = false }: { market: MarketFixture; fToken?: boolean }) { + return ( +

+ + + {market.name} + {fToken ? `f${market.name} · ${currency.format(market.price)}` : `${market.subtitle} · ${currency.format(market.price)}`} + +
+ ); +} + +function RiskMeter({ percent, compact = false }: { percent: number; compact?: boolean }) { + const shown = Math.min(Math.max(percent, 0), 100); + const band = riskBand(percent); + + return ( +
+ +
+ ); +} + +function PositionPassbook({ + scenario, + markets, + onRepay, + onAddCollateral, +}: { + scenario: ScenarioId; + markets: MarketFixture[]; + onRepay: () => void; + onAddCollateral: () => void; +}) { + const fixture = scenarios[scenario]; + const totals = calculateTotals(markets); + const band = riskBand(totals.limitUsedPercent); + const suppliedMarkets = markets.filter((market) => market.supplied > 0).length; + const borrowedMarkets = markets.filter((market) => market.borrowed > 0).length; + + if (!fixture.connected) { + return ( +
+ +
+

Your position

+

Your TezFin passbook

+

Connect a Tezos wallet to see supplied assets, open borrows, and liquidation headroom in one place.

+ + Offline preview — wallet access is disabled +
+
+ ); + } + + return ( + <> + {band === "critical" && ( +
+ + + Your position is close to liquidation + Only {currency.format(totals.headroomUsd)} of borrowing headroom remains in this preview. + + + + + +
+ )} + +
+ +
+
+

Supplied

+

{currency.format(totals.suppliedUsd)}

+

across {suppliedMarkets} market{suppliedMarkets === 1 ? "" : "s"}

+
+
+

Estimated net rate

+

{totals.estimatedNetRate >= 0 ? "+" : "−"}{Math.abs(totals.estimatedNetRate).toFixed(2)}%

+

variable · fixture calculation

+
+
+

Borrowed

+

{currency.format(totals.borrowedUsd)}

+

{borrowedMarkets === 0 ? "no open lines" : `${borrowedMarkets} open line${borrowedMarkets === 1 ? "" : "s"}`}

+
+
+ +
+
+ Borrow limit used {totals.limitUsedPercent.toFixed(1)}% + {totals.borrowLimitUsd > 0 ? `${currency.format(totals.headroomUsd)} headroom` : "Supply collateral to unlock borrowing"} +
+ +
+ Collateral {currency.format(totals.collateralUsd)} + Borrow limit {currency.format(totals.borrowLimitUsd)} +
+
+
+ + ); +} + +type BoardKind = "supplying" | "borrowing" | "supply-market" | "borrow-market"; + +function MarketBoard({ + kind, + markets, + connected, + onOpen, + onCollateral, +}: { + kind: BoardKind; + markets: MarketFixture[]; + connected: boolean; + onOpen: (market: MarketFixture, action: TransactionAction) => void; + onCollateral: (market: MarketFixture) => void; +}) { + const isSupply = kind === "supplying" || kind === "supply-market"; + const isPosition = kind === "supplying" || kind === "borrowing"; + const title = { + supplying: "Supplying", + borrowing: "Borrowing", + "supply-market": "Assets to Supply", + "borrow-market": "Assets to Borrow", + }[kind]; + const action: TransactionAction = { + supplying: "Withdraw", + borrowing: "Repay", + "supply-market": "Supply", + "borrow-market": "Borrow", + }[kind] as TransactionAction; + const boardMarkets = kind === "supplying" + ? markets.filter((market) => market.supplied > 0) + : kind === "borrowing" + ? markets.filter((market) => market.borrowed > 0) + : markets; + const total = boardMarkets.reduce((sum, market) => { + const amount = kind === "borrowing" ? market.borrowed : kind === "supplying" ? market.supplied : 0; + return sum + amount * market.price; + }, 0); + + return ( +
+
+

{title}

+ {isPosition && {boardMarkets.length} asset{boardMarkets.length === 1 ? "" : "s"} {currency.format(total)}} +
+ + {boardMarkets.length === 0 ? ( +
+ {kind === "supplying" + ? "Nothing supplied yet — choose an asset below to preview a deposit." + : "No open lines — borrowing stays empty until collateral is supplied."} +
+ ) : ( +
+ + {boardMarkets.map((market) => { + const balance = kind === "supplying" ? market.supplied : market.borrowed; + const disabled = !connected && !isPosition; + + return ( +
+ + {isPosition ? ( +
+ {(isSupply ? market.supplyApy : market.borrowApy).toFixed(2)}% +
+ ) : ( +
+ {kind === "supply-market" ? formatAmount(market.walletBalance) : compactCurrency.format(market.liquidityUsd)} + {kind === "supply-market" && {currency.format(market.walletBalance * market.price)}} +
+ )} + {isPosition ? ( +
+ {formatAmount(balance)} {market.name} + {currency.format(balance * market.price)} +
+ ) : ( +
+ {(isSupply ? market.supplyApy : market.borrowApy).toFixed(2)}% +
+ )} + {kind === "supplying" && ( +